From f7cf752fb244d7c82b47fc23ba9da79d09cadbdc Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:22:14 +0200 Subject: [PATCH 001/134] Bump version to 4.9.8b Update the __version__ string in src/__init__.py to 4.9.8b to reflect the latest changes. --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 44adeaf..9bbd51f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "4.9.7" +__version__ = "4.9.8b" __author__ = "oop7" From 2574ed0467a5e0100e88f53f856886a5d6982463 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:23:31 +0200 Subject: [PATCH 002/134] Add Flatpak badge to Linux install options Included a Flathub badge in the Linux installation table to indicate Flatpak bundle availability. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2ded665..74e6e94 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ ytsage | ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian package | | ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portable | | ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM package | +| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle | #### ЁЯНО macOS From 67d0919237ab33bf7e79b4d37f116dee3f57d223 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:40:07 +0200 Subject: [PATCH 003/134] Add Flatpak-specific desktop file to build workflow Introduces creation of a dedicated desktop file for Flatpak packaging in the build-linux workflow. Ensures the Exec and Icon fields conform to Flatpak requirements and updates the install step to use the new desktop file. --- .github/workflows/build-linux.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 194e601..c5dbef4 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -392,6 +392,21 @@ jobs: # Define manifest ID APP_ID="io.github.oop7.YTSage" + # Create a specific desktop file for Flatpak + # - Exec must be 'ytsage' (in path), not /usr/bin/ytsage + # - Icon must match the App ID (io.github.oop7.YTSage) + cat > flatpak.desktop < ytsage_flatpak.json < Date: Wed, 7 Jan 2026 18:11:06 +0200 Subject: [PATCH 004/134] Add DENO_APP_BIN_PATH env override and Flatpak support Introduces support for overriding DENO_APP_BIN_PATH via environment variable, enabling compatibility with Flatpak packaging. Updates the build workflow to set the DENO_APP_BIN_PATH environment variable and ensures parent directories for custom binary paths are created if needed. --- .github/workflows/build-linux.yml | 3 ++- src/utils/ytsage_constants.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index c5dbef4..bf7182f 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -423,7 +423,8 @@ jobs: "--device=dri", "--share=network", "--filesystem=host", - "--env=YTDLP_APP_BIN_PATH=/var/data/yt-dlp" + "--env=YTDLP_APP_BIN_PATH=/var/data/yt-dlp", + "--env=DENO_APP_BIN_PATH=/var/data/deno" ], "modules": [ { diff --git a/src/utils/ytsage_constants.py b/src/utils/ytsage_constants.py index 2f4d7b2..d1a6a8c 100644 --- a/src/utils/ytsage_constants.py +++ b/src/utils/ytsage_constants.py @@ -166,7 +166,13 @@ elif OS_NAME == "Darwin": # macOS else: # Linux DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip" DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip.sha256sum" - DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno" + + # Check for environment variable override (critical for Flatpak support) + _deno_env_path = os.environ.get("DENO_APP_BIN_PATH") + if _deno_env_path: + DENO_APP_BIN_PATH: Path = Path(_deno_env_path) + else: + DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno" # FFmpeg download links (Essentials build - always latest version) FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z" @@ -203,4 +209,8 @@ else: # Ensure custom yt-dlp directory exists if set if OS_NAME not in ["Windows", "Darwin"]: - YTDLP_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True) + # Ensure parent directories exist for custom paths + if "YTDLP_APP_BIN_PATH" in globals(): + YTDLP_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True) + if "DENO_APP_BIN_PATH" in globals(): + DENO_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True) From a4df946169295b19e047b479bfec85bb2ea20ec2 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:32:20 +0200 Subject: [PATCH 005/134] Fix extraction path for Deno executable Changed the extraction target directory for the Deno executable to use the parent of DENO_APP_BIN_PATH instead of APP_BIN_DIR, ensuring the executable is placed in the correct location. --- src/core/ytsage_deno.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/ytsage_deno.py b/src/core/ytsage_deno.py index 2c4304f..94ddeb6 100644 --- a/src/core/ytsage_deno.py +++ b/src/core/ytsage_deno.py @@ -171,8 +171,9 @@ class DownloadDenoThread(QThread): self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip") return - # Extract to app bin directory - zip_ref.extract(executable_name, APP_BIN_DIR) + # Extract to app bin directory + target_dir = DENO_APP_BIN_PATH.parent + zip_ref.extract(executable_name, target_dir) # Verify the extracted file exists exe_path = DENO_APP_BIN_PATH From 26d063327c66150ba0030678ad1bfb6a8e40433c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:45:51 +0200 Subject: [PATCH 006/134] Fix indentation for zip extraction block Corrected the indentation so that the extraction of the executable occurs within the context of the open zip file. This ensures the file is extracted while the zip is accessible, preventing potential errors. --- src/core/ytsage_deno.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/ytsage_deno.py b/src/core/ytsage_deno.py index 94ddeb6..4d76e0d 100644 --- a/src/core/ytsage_deno.py +++ b/src/core/ytsage_deno.py @@ -171,9 +171,9 @@ class DownloadDenoThread(QThread): self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip") return - # Extract to app bin directory - target_dir = DENO_APP_BIN_PATH.parent - zip_ref.extract(executable_name, target_dir) + # Extract to app bin directory (while zip_ref is open) + target_dir = DENO_APP_BIN_PATH.parent + zip_ref.extract(executable_name, target_dir) # Verify the extracted file exists exe_path = DENO_APP_BIN_PATH From c16e84105e3861cc7ebefeaa0aa04ef7c88c53ef Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:01:52 +0200 Subject: [PATCH 007/134] Increase subprocess timeout to 300 seconds Extended the timeout for the subprocess running yt-dlp from 60 to 300 seconds to accommodate longer-running download or processing tasks. --- src/gui/ytsage_gui_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 7e4161f..7258767 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -1969,7 +1969,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from # Execute command with hidden console window on Windows # Extra logic moved to src\utils\ytsage_constants.py - result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS) if result.returncode != 0: logger.error(f"yt-dlp failed: {result.stderr}") From f8f2910f06789a053d58594c0dcaeb1513c2d006 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:10:01 +0200 Subject: [PATCH 008/134] Fix f-string backslash SyntaxError in AboutDialog Refactored AboutDialog to avoid backslashes in f-string expressions by assigning HTML links to variables before formatting. This resolves a SyntaxError encountered when running YTSage with Python 3.11. --- src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index 3ac4a39..c25481f 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -178,14 +178,16 @@ class AboutDialog(QDialog): info_layout = QHBoxLayout() info_layout.setSpacing(15) + author_link = 'oop7' author_label = QLabel( - f"{_('about.author', author='oop7')}" + f"{_('about.author', author=author_link)}" ) author_label.setOpenExternalLinks(True) info_layout.addWidget(author_label) + repo_link = 'YTSage' repo_label = QLabel( - f"{_('about.github', repo='YTSage')}" + f"{_('about.github', repo=repo_link)}" ) repo_label.setOpenExternalLinks(True) info_layout.addWidget(repo_label) From fd192455f5838a8d36faa3e79d7c666ef847cf76 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:21:06 +0200 Subject: [PATCH 009/134] Handle Path objects in config JSON serialization Added a custom converter to serialize Path objects as strings when saving the application configuration to JSON. This prevents serialization errors if Path objects are present in the config. --- src/core/ytsage_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/ytsage_utils.py b/src/core/ytsage_utils.py index 158acae..2e6c345 100644 --- a/src/core/ytsage_utils.py +++ b/src/core/ytsage_utils.py @@ -362,8 +362,14 @@ def load_config() -> Dict[str, Any]: def save_config(config: Dict[str, Any]) -> bool: """Save the application configuration to file.""" try: + # Convert any Path objects to strings for JSON serialization + def _convert_path(obj): + if isinstance(obj, Path): + return str(obj) + raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable") + with open(APP_CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(config, f, ensure_ascii=False, indent=2) + json.dump(config, f, ensure_ascii=False, indent=2, default=_convert_path) return True except Exception as e: logger.exception(f"Error saving config: {e}") From 2b7cce133347de6ccfa51d7c176db41b2b7242a1 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:26:26 +0200 Subject: [PATCH 010/134] Add threaded loading and spinner to history dialog Introduces a HistoryLoaderThread to load history entries and pre-fetch thumbnails in the background, preventing UI freezes. Adds a loading spinner and disables the 'Clear All' button while loading, improving user experience during history retrieval. --- .../ytsage_dialogs_history.py | 100 ++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index f705ee6..12fc717 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Optional import requests from PIL import Image -from PySide6.QtCore import Qt, QSize, Signal +from PySide6.QtCore import Qt, QSize, Signal, QThread, QTimer from PySide6.QtGui import QPixmap, QIcon from PySide6.QtWidgets import ( QDialog, @@ -27,6 +27,7 @@ from PySide6.QtWidgets import ( QMenu, QMessageBox, QSizePolicy, + QProgressBar, ) from src.utils.ytsage_history_manager import HistoryManager @@ -38,6 +39,46 @@ if TYPE_CHECKING: from src.gui.ytsage_gui_main import YTSageApp +class HistoryLoaderThread(QThread): + """Thread to load history and pre-fetch thumbnails.""" + + finished = Signal(list) + + def run(self): + try: + # HistoryManager uses get_all_entries, not get_all + entries = HistoryManager.get_all_entries() + + # Pre-fetch thumbnails so UI doesn't freeze + for entry in entries: + thumbnail_url = entry.get("thumbnail_url") + entry_id = entry.get("id", "") + + if not thumbnail_url or not entry_id: + continue + + thumbnail_filename = f"{entry_id}.jpg" + thumbnail_path = APP_THUMBNAILS_DIR / thumbnail_filename + + # If not exists, download it + if not thumbnail_path.exists(): + try: + response = requests.get(thumbnail_url, timeout=5) + if response.status_code == 200: + APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True) + + image = Image.open(BytesIO(response.content)) + image.save(thumbnail_path, "JPEG", quality=95, optimize=True) + except Exception as e: + logger.debug(f"Error caching thumbnail in background: {e}") + + self.finished.emit(entries) + + except Exception as e: + logger.error(f"Error loading history: {e}") + self.finished.emit([]) + + class HistoryEntryWidget(QFrame): """Widget representing a single history entry.""" @@ -362,7 +403,23 @@ class HistoryDialog(QDialog): self.entry_widgets = [] self.setup_ui() - self.load_history() + + # Show loading state initially + self.show_loading_state() + + # Start loading history in background after a short delay + # to ensure the dialog is shown first + QTimer.singleShot(100, self.start_loading_history) + + def start_loading_history(self): + """Start the background thread to load history.""" + self.loader_thread = HistoryLoaderThread() + self.loader_thread.finished.connect(self.on_history_loaded) + self.loader_thread.start() + + def on_history_loaded(self, entries): + """Called when history is loaded from background thread.""" + self.load_history_entries(entries) def setup_ui(self): """Setup the dialog UI.""" @@ -466,15 +523,43 @@ class HistoryDialog(QDialog): } """) - def load_history(self): - """Load and display history entries.""" - # Clear existing widgets + def clear_history_list(self): + """Clear existing history widgets.""" for widget in self.entry_widgets: widget.deleteLater() self.entry_widgets.clear() + + def show_loading_state(self): + """Show loading indicator.""" + # Clear existing content + self.clear_history_list() - # Get history entries - entries = HistoryManager.get_all_entries() + loading_widget = QWidget() + loading_layout = QVBoxLayout(loading_widget) + loading_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + spinner_label = QLabel("тП│") # Simple spinner icon + spinner_label.setStyleSheet("font-size: 48px; color: #c90000;") + spinner_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + loading_layout.addWidget(spinner_label) + + # text_label removed as per user request + + self.history_layout.addWidget(loading_widget) + self.entry_widgets.append(loading_widget) + + self.status_label.setText("Loading...") + self.clear_all_btn.setEnabled(False) + + def load_history(self): + """Load and display history entries.""" + if hasattr(self, 'history_container'): + self.show_loading_state() + self.start_loading_history() + + def load_history_entries(self, entries): + """Populate the history list with entries.""" + self.clear_history_list() if not entries: self.show_empty_state() @@ -496,6 +581,7 @@ class HistoryDialog(QDialog): else: status_text = _("history.entries_count", count=count) self.status_label.setText(status_text) + self.clear_all_btn.setEnabled(True) def show_empty_state(self): """Show empty state when there's no history.""" From ffb938b181c6a8c02e3566648d6177800ec03798 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:43:01 +0200 Subject: [PATCH 011/134] Add playlist filter and show video durations in selection dialog Introduces a filter input to the PlaylistSelectionDialog for searching videos by title and displays video durations next to each entry. Updates English translations to support the new filter placeholder and analysis status message. --- languages/en.json | 2 + .../ytsage_dialogs_selection.py | 51 +++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/languages/en.json b/languages/en.json index 0fff390..8f7684b 100644 --- a/languages/en.json +++ b/languages/en.json @@ -92,6 +92,7 @@ "sponsorblock_description": "Select which types of video segments to automatically remove during download.\nSponsorBlock uses community-submitted data to identify these segments.", "select_subtitles": "Select Subtitles", "filter_languages_placeholder": "Filter languages (e.g., en, es)...", + "filter_playlist_placeholder": "Filter videos...", "no_subtitles_available": "No subtitles available", "matching": "matching" }, @@ -336,6 +337,7 @@ "analyzing_updating_table": "Analyzing (95%)... Updating format table", "analysis_complete": "Analysis complete!", "analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info", + "analyzing_fetching_first_video": "Analyzing... Fetching formats for first video", "analyzing_processing_data": "Analyzing (60%)... Processing data", "analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats", "analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail", diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py index 9d36b6d..bead261 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py @@ -208,6 +208,27 @@ class PlaylistSelectionDialog(QDialog): # Main layout main_layout = QVBoxLayout(self) + # Filter Input + self.filter_input = QLineEdit() + self.filter_input.setPlaceholderText(_("dialogs.filter_playlist_placeholder")) + self.filter_input.textChanged.connect(self.filter_list) + self.filter_input.setStyleSheet( + """ + QLineEdit { + background-color: #363636; + border: 2px solid #3d3d3d; + border-radius: 4px; + padding: 5px; + min-height: 30px; + color: white; + } + QLineEdit:focus { + border-color: #ff0000; + } + """ + ) + main_layout.addWidget(self.filter_input) + # Top buttons (Select/Deselect All) button_layout = QHBoxLayout() select_all_btn = QPushButton(_("buttons.select_all")) @@ -339,6 +360,13 @@ class PlaylistSelectionDialog(QDialog): pass # Ignore invalid numbers return selected_indices + def filter_list(self, text: str) -> None: + """Filter the list of checkboxes based on title.""" + text = text.lower() + for checkbox in self.checkboxes: + title = (checkbox.property("full_title") or "").lower() + checkbox.setVisible(text in title) + def _populate_list(self, previously_selected_string) -> None: """Populates the scroll area with checkboxes for each video.""" selected_indices = self._parse_selection_string(previously_selected_string) @@ -356,12 +384,29 @@ class PlaylistSelectionDialog(QDialog): video_index = index + 1 # yt-dlp uses 1-based indexing title = entry.get("title", f"Video {video_index}") - # Shorten title if too long - display_title = (title[:70] + "...") if len(title) > 73 else title + + # Format duration + duration = entry.get("duration") + duration_str = "" + if duration: + try: + m, s = divmod(int(duration), 60) + h, m = divmod(m, 60) + if h > 0: + duration_str = f" [{h}:{m:02d}:{s:02d}]" + else: + duration_str = f" [{m:02d}:{s:02d}]" + except (ValueError, TypeError): + pass + + # Shorten title if too long but keep enough space for duration + max_len = 65 + display_title = (title[:max_len] + "...") if len(title) > max_len + 3 else title - checkbox = QCheckBox(f"{video_index}. {display_title}") + checkbox = QCheckBox(f"{video_index}. {display_title}{duration_str}") checkbox.setChecked(video_index in selected_indices) checkbox.setProperty("video_index", video_index) # Store index + checkbox.setProperty("full_title", title) # Store full title for filtering checkbox.setStyleSheet( """ QCheckBox { From 67fdb508f401ecd5e8bd475b83ea663b44fd8819 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:43:08 +0200 Subject: [PATCH 012/134] Optimize playlist extraction with yt-dlp flat-playlist Switches to using yt-dlp's --flat-playlist and --dump-single-json for faster initial playlist info extraction. Now fetches minimal info for all videos quickly, then retrieves full details for the first video only to populate format information, improving performance and reducing unnecessary data processing. --- src/gui/ytsage_gui_main.py | 44 ++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 7258767..2b6598c 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -1952,7 +1952,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from url = f"https://www.youtube.com/playlist?list={playlist_id}" # Build command for basic info extraction - cmd = [yt_dlp_path, "--dump-json", "--no-warnings", url] + # Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json + # This fetches minimal info for all videos quickly without downloading full details for each + cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] # Add cookies if available if self.cookie_file_path: @@ -1998,21 +2000,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.signals.update_status.emit(_("main_ui.analyzing_processing_data")) - if first_info.get("_type") == "playlist" or len(json_lines) > 1: + if first_info.get("_type") == "playlist": # Handle playlist self.is_playlist = True self.playlist_info = first_info self.selected_playlist_items = None - self.playlist_entries = [] - - # Parse all entries - for line in json_lines: - try: - entry = json.loads(line) - if entry.get("_type") != "playlist": # Skip playlist metadata - self.playlist_entries.append(entry) - except json.JSONDecodeError: - continue + self.playlist_entries = first_info.get("entries", []) if not self.playlist_entries: logger.error("Playlist contains no valid videos.") @@ -2021,8 +2014,31 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.signals.playlist_select_btn_visible.emit(False) return - # Use first video for format information - self.video_info = self.playlist_entries[0] + # Even with flat-playlist, we need one video's formats to populate the table. + # Just use the first video URL to get full details quickly. + self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video")) + first_video_entry = self.playlist_entries[0] + first_video_url = first_video_entry.get("url") + + # Fetch full info for just the first video to get formats + cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url] + # Add cookies & proxy to this single request too + if self.cookie_file_path: + cmd_single.extend(["--cookies", str(self.cookie_file_path)]) + elif self.browser_cookies_option: + cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option]) + if self.proxy_url: + cmd_single.extend(["--proxy", self.proxy_url]) + if self.geo_proxy_url: + cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url]) + + result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) + + if result_single.returncode == 0: + self.video_info = json.loads(result_single.stdout) + else: + # Fallback to whatever minimal info we have, might fail table population + self.video_info = first_video_entry # Update playlist info label playlist_text = _("playlist.display_format", From d087c306f2c92e05d22d353dac8d10e874b3fcb2 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:48:45 +0200 Subject: [PATCH 013/134] Add 'analyzing_fetching_first_video' translation key Introduced the 'analyzing_fetching_first_video' string to all supported language files for consistent UI messaging during the analysis process. --- languages/ar.json | 1 + languages/de.json | 1 + languages/es.json | 1 + languages/fr.json | 1 + languages/hi.json | 1 + languages/id.json | 1 + languages/it.json | 1 + languages/ja.json | 1 + languages/pl.json | 1 + languages/pt.json | 1 + languages/ru.json | 1 + languages/tr.json | 1 + languages/zh.json | 1 + 13 files changed, 13 insertions(+) diff --git a/languages/ar.json b/languages/ar.json index 458c65b..3cd4a0e 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "╪з┘Д╪к╪н┘Д┘К┘Д (95%)... ╪м╪з╪▒┘К ╪к╪н╪п┘К╪л ╪м╪п┘И┘Д ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к", "analysis_complete": "╪з┘Г╪к┘Е┘Д ╪з┘Д╪к╪н┘Д┘К┘Д!", "analyzing_extracting_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (30%)... ╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к", + "analyzing_fetching_first_video": "╪к╪н┘Д┘К┘Д... ╪м╪з╪▒┘К ╪м┘Д╪и ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к ┘Д┘Д┘Б┘К╪п┘К┘И ╪з┘Д╪г┘И┘Д", "analyzing_processing_data": "╪з┘Д╪к╪н┘Д┘К┘Д (60%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪и┘К╪з┘Ж╪з╪к", "analyzing_processing_formats_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (75%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к", "analyzing_loading_thumbnail_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (85%)... ╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ╪з┘Д╪╡┘И╪▒╪й ╪з┘Д┘Е╪╡╪║╪▒╪й", diff --git a/languages/de.json b/languages/de.json index 9418482..3e9d727 100644 --- a/languages/de.json +++ b/languages/de.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "Analysiere (95%)... Formattabelle wird aktualisiert", "analysis_complete": "Analyse abgeschlossen!", "analyzing_extracting_ytdlp": "Analysiere (30%)... Informationen werden extrahiert", + "analyzing_fetching_first_video": "Analysiere... Formate f├╝r das erste Video werden abgerufen", "analyzing_processing_data": "Analysiere (60%)... Daten werden verarbeitet", "analyzing_processing_formats_ytdlp": "Analysiere (75%)... Formate werden verarbeitet", "analyzing_loading_thumbnail_ytdlp": "Analysiere (85%)... Thumbnail wird geladen", diff --git a/languages/es.json b/languages/es.json index 2202449..465fcdd 100644 --- a/languages/es.json +++ b/languages/es.json @@ -319,6 +319,7 @@ "analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos", "analysis_complete": "┬бAn├бlisis completo!", "analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo informaci├│n", + "analyzing_fetching_first_video": "Analizando... Obteniendo formatos del primer video", "analyzing_processing_data": "Analizando (60%)... Procesando datos", "analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos", "analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura", diff --git a/languages/fr.json b/languages/fr.json index 1f57977..51a2f6b 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "Analyse (95%)... Mise ├а jour du tableau des formats", "analysis_complete": "Analyse termin├йe !", "analyzing_extracting_ytdlp": "Analyse (30%)... Extraction d'informations", + "analyzing_fetching_first_video": "Analyse... R├йcup├йration des formats pour la premi├иre vid├йo", "analyzing_processing_data": "Analyse (60%)... Traitement des donn├йes", "analyzing_processing_formats_ytdlp": "Analyse (75%)... Traitement des formats", "analyzing_loading_thumbnail_ytdlp": "Analyse (85%)... Chargement de la miniature", diff --git a/languages/hi.json b/languages/hi.json index 6e8ce90..bf4360c 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (95%)... рдкреНрд░рд╛рд░реВрдк рддрд╛рд▓рд┐рдХрд╛ рдЕрдкрдбреЗрдЯ рд╣реЛ рд░рд╣реА рд╣реИ", "analysis_complete": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдкреВрд░реНрдг!", "analyzing_extracting_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (30%)... рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ", + "analyzing_fetching_first_video": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдХрд░ рд░рд╣рд╛ рд╣реИ... рдкрд╣рд▓реЗ рд╡реАрдбрд┐рдпреЛ рдХреЗ рд▓рд┐рдП рдкреНрд░рд╛рд░реВрдк рдкреНрд░рд╛рдкреНрдд рдХрд░ рд░рд╣рд╛ рд╣реИ", "analyzing_processing_data": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (60%)... рдбреЗрдЯрд╛ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣рд╛ рд╣реИ", "analyzing_processing_formats_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (75%)... рдкреНрд░рд╛рд░реВрдк рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ", "analyzing_loading_thumbnail_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (85%)... рдердВрдмрдиреЗрд▓ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ", diff --git a/languages/id.json b/languages/id.json index c9358b6..f1e60f2 100644 --- a/languages/id.json +++ b/languages/id.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "Menganalisis (95%)... Memperbarui tabel format", "analysis_complete": "Analisis selesai!", "analyzing_extracting_ytdlp": "Menganalisis (30%)... Mengekstrak informasi", + "analyzing_fetching_first_video": "Menganalisis... Mengambil format untuk video pertama", "analyzing_processing_data": "Menganalisis (60%)... Memproses data", "analyzing_processing_formats_ytdlp": "Menganalisis (75%)... Memproses format", "analyzing_loading_thumbnail_ytdlp": "Menganalisis (85%)... Memuat thumbnail", diff --git a/languages/it.json b/languages/it.json index e2a94b3..7cfaf98 100644 --- a/languages/it.json +++ b/languages/it.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "Analisi (95%)... Aggiornamento tabella formati", "analysis_complete": "Analisi completata!", "analyzing_extracting_ytdlp": "Analisi (30%)... Estrazione informazioni", + "analyzing_fetching_first_video": "Analisi... Recupero dei formati per il primo video", "analyzing_processing_data": "Analisi (60%)... Elaborazione dati", "analyzing_processing_formats_ytdlp": "Analisi (75%)... Elaborazione formati", "analyzing_loading_thumbnail_ytdlp": "Analisi (85%)... Caricamento miniatura", diff --git a/languages/ja.json b/languages/ja.json index 9cdfa27..4b8d4e2 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -336,6 +336,7 @@ "analyzing_updating_table": "шзгцЮРф╕н (95%)... уГХуВйуГ╝уГЮуГГуГИуГЖуГ╝уГЦуГлуВТцЫ┤цЦ░ф╕н", "analysis_complete": "шзгцЮРхоМф║Жя╝Б", "analyzing_extracting_ytdlp": "шзгцЮРф╕н (30%)... цГЕха▒уВТцК╜хЗ║ф╕н", + "analyzing_fetching_first_video": "шзгцЮРф╕н... цЬАхИЭуБоуГУуГЗуВкуБох╜вх╝ПуВТхПЦх╛ЧуБЧуБжуБДуБ╛уБЩ", "analyzing_processing_data": "шзгцЮРф╕н (60%)... уГЗуГ╝уВ┐уВТхЗжчРЖф╕н", "analyzing_processing_formats_ytdlp": "шзгцЮРф╕н (75%)... уГХуВйуГ╝уГЮуГГуГИуВТхЗжчРЖф╕н", "analyzing_loading_thumbnail_ytdlp": "шзгцЮРф╕н (85%)... уВ╡уГауГНуВдуГлуВТшкнуБ┐ш╛╝уБ┐ф╕н", diff --git a/languages/pl.json b/languages/pl.json index b4b3043..8db7dc4 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -335,6 +335,7 @@ "analyzing_processing_subtitles": "Analizowanie (85%)... Przetwarzanie napis├│w", "analyzing_updating_table": "Analizowanie (95%)... Aktualizowanie tabeli format├│w", "analysis_complete": "Analiza zako┼Дczona!", + "analyzing_fetching_first_video": "Analizowanie... Pobieranie format├│w dla pierwszego filmu", "analyzing_extracting_ytdlp": "Analizowanie (30%)... Wyodr─Щbnianie informacji", "analyzing_processing_data": "Analizowanie (60%)... Przetwarzanie danych", "analyzing_processing_formats_ytdlp": "Analizowanie (75%)... Przetwarzanie format├│w", diff --git a/languages/pt.json b/languages/pt.json index ae43495..27f3152 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -335,6 +335,7 @@ "analyzing_processing_subtitles": "Analisando (85%)... Processando legendas", "analyzing_updating_table": "Analisando (95%)... Atualizando tabela de formatos", "analysis_complete": "An├бlise completa!", + "analyzing_fetching_first_video": "Analisando... Obtendo formatos do primeiro v├нdeo", "analyzing_extracting_ytdlp": "Analisando (30%)... Extraindo informa├з├╡es", "analyzing_processing_data": "Analisando (60%)... Processando dados", "analyzing_processing_formats_ytdlp": "Analisando (75%)... Processando formatos", diff --git a/languages/ru.json b/languages/ru.json index 863c824..1c7448b 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -335,6 +335,7 @@ "analyzing_processing_subtitles": "╨Р╨╜╨░╨╗╨╕╨╖ (85%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Б╤Г╨▒╤В╨╕╤В╤А╨╛╨▓", "analyzing_updating_table": "╨Р╨╜╨░╨╗╨╕╨╖ (95%)... ╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡ ╤В╨░╨▒╨╗╨╕╤Ж╤Л ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓", "analysis_complete": "╨Р╨╜╨░╨╗╨╕╨╖ ╨╖╨░╨▓╨╡╤А╤И╨╡╨╜!", + "analyzing_fetching_first_video": "╨Р╨╜╨░╨╗╨╕╨╖... ╨Я╨╛╨╗╤Г╤З╨╡╨╜╨╕╨╡ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓ ╨┤╨╗╤П ╨┐╨╡╤А╨▓╨╛╨│╨╛ ╨▓╨╕╨┤╨╡╨╛", "analyzing_extracting_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (30%)... ╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕", "analyzing_processing_data": "╨Р╨╜╨░╨╗╨╕╨╖ (60%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╨┤╨░╨╜╨╜╤Л╤Е", "analyzing_processing_formats_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (75%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓", diff --git a/languages/tr.json b/languages/tr.json index bb33054..906be44 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -335,6 +335,7 @@ "analyzing_processing_subtitles": "Analiz ediliyor (85%)... Altyaz─▒lar i┼Яleniyor", "analyzing_updating_table": "Analiz ediliyor (95%)... Format tablosu g├╝ncelleniyor", "analysis_complete": "Analiz tamamland─▒!", + "analyzing_fetching_first_video": "Analiz ediliyor... ─░lk video i├зin formatlar al─▒n─▒yor", "analyzing_extracting_ytdlp": "Analiz ediliyor (30%)... Bilgiler ├з─▒kar─▒l─▒yor", "analyzing_processing_data": "Analiz ediliyor (60%)... Veriler i┼Яleniyor", "analyzing_processing_formats_ytdlp": "Analiz ediliyor (75%)... Formatlar i┼Яleniyor", diff --git a/languages/zh.json b/languages/zh.json index f750b34..da2e429 100644 --- a/languages/zh.json +++ b/languages/zh.json @@ -335,6 +335,7 @@ "analyzing_processing_subtitles": "хИЖцЮРф╕н (85%)... цнгхЬихдДчРЖхнЧх╣Х", "analyzing_updating_table": "хИЖцЮРф╕н (95%)... цнгхЬицЫ┤цЦ░ца╝х╝Пшби", "analysis_complete": "хИЖцЮРхоМцИРя╝Б", + "analyzing_fetching_first_video": "хИЖцЮРф╕н... цнгхЬишО╖хПЦчммф╕Аф╕кшзЖщвСчЪДца╝х╝П", "analyzing_extracting_ytdlp": "хИЖцЮРф╕н (30%)... цПРхПЦф┐бцБп", "analyzing_processing_data": "хИЖцЮРф╕н (60%)... цнгхЬихдДчРЖцХ░цНо", "analyzing_processing_formats_ytdlp": "хИЖцЮРф╕н (75%)... цнгхЬихдДчРЖца╝х╝П", From 5ed67cb32f4490677b20ddeae569c38faabe4269 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:58:50 +0200 Subject: [PATCH 014/134] Bump version to 5.0.0b Update the __version__ string to 5.0.0b to reflect the next major beta release. --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 9bbd51f..cc62bb6 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "4.9.8b" +__version__ = "5.0.0b" __author__ = "oop7" From 6a4264eeab4ab5d1bbe7dd288b32ba91058e5c30 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:04:23 +0200 Subject: [PATCH 015/134] Add contents write permission to release workflow Updated the release-all GitHub Actions workflow to include 'contents: write' permission, enabling the workflow to perform actions that require write access to repository contents. --- .github/workflows/release-all.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml index 7e0fdef..c46dd44 100644 --- a/.github/workflows/release-all.yml +++ b/.github/workflows/release-all.yml @@ -8,6 +8,9 @@ on: required: true type: string +permissions: + contents: write + jobs: release-windows: uses: ./.github/workflows/build-windows.yml From ca7ac168fe570c63222d14d2b0f57ca490c7dee5 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:13:08 +0200 Subject: [PATCH 016/134] Update badges in README with download stats Replaces the generic downloads badge with separate PyPI and GitHub downloads badges for improved clarity. Enhances visibility of project statistics in the README. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 74e6e94..77e6d6d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://badge.fury.io/py/ytsage) [![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) -[![Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=download&logoColor=white)](https://pepy.tech/project/ytsage) +[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=pypi%20downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) +[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=github%20downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) **A modern YouTube downloader with a clean PySide6 interface.** From 9a81c4b56499f6d149de334add28a660d521df32 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:16:35 +0200 Subject: [PATCH 017/134] Update badge labels in README Changed the 'pypi downloads' and 'github downloads' badge labels to 'downloads' for consistency and improved readability. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77e6d6d..c5548cc 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ [![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://badge.fury.io/py/ytsage) [![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) -[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=pypi%20downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) -[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=github%20downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) +[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) +[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) **A modern YouTube downloader with a clean PySide6 interface.** From 0d1eb35ba247805266fa2f309e063c9dab4aeb2d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:15:14 +0200 Subject: [PATCH 018/134] Add debug log for yt-dlp command execution Inserted a debug log statement to output the full yt-dlp command before execution. This will help diagnose issues related to command construction and subprocess invocation. --- src/gui/ytsage_gui_main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 2b6598c..935d489 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -1969,6 +1969,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from if self.geo_proxy_url: cmd.extend(["--geo-verification-proxy", self.geo_proxy_url]) + logger.debug(f"Executing yt-dlp command: {cmd}") + # Execute command with hidden console window on Windows # Extra logic moved to src\utils\ytsage_constants.py result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS) From a92ffd3b011a3143d8c3c4e3bc1c01fc4ae26126 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:16:35 +0200 Subject: [PATCH 019/134] Download video thumbnails asynchronously in GUI Refactored thumbnail downloading to use a QThread for asynchronous operation, preventing UI blocking. Added ThumbnailDownloadThread class and updated VideoInfoMixin to handle threaded download and error reporting. --- src/gui/ytsage_gui_video_info.py | 45 +++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/gui/ytsage_gui_video_info.py b/src/gui/ytsage_gui_video_info.py index 06b282f..8ab17f8 100644 --- a/src/gui/ytsage_gui_video_info.py +++ b/src/gui/ytsage_gui_video_info.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast import requests from PIL import Image -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget @@ -21,6 +21,26 @@ if TYPE_CHECKING: from src.gui.ytsage_gui_main import YTSageApp +class ThumbnailDownloadThread(QThread): + """Thread to download thumbnail image asynchronously.""" + finished = Signal(bytes) + error = Signal(str) + + def __init__(self, url): + super().__init__() + self.url = url + + def run(self): + try: + response = requests.get(self.url, timeout=10) + if response.status_code == 200: + self.finished.emit(response.content) + else: + self.error.emit(f"HTTP Error: {response.status_code}") + except Exception as e: + self.error.emit(str(e)) + + class VideoInfoMixin: def setup_video_info_section(self) -> QHBoxLayout: self = cast("YTSageApp", self) # for autocompletion and type inference. @@ -368,14 +388,21 @@ class VideoInfoMixin: def download_thumbnail(self, url) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. - try: - # Store both thumbnail URL and video URL - self.thumbnail_url = url - self.video_url = self.url_input.text() # Get actual video URL + # Store both thumbnail URL and video URL + self.thumbnail_url = url + self.video_url = self.url_input.text() # Get actual video URL - # Download thumbnail but don't save yet - response = requests.get(url) - self.thumbnail_image = Image.open(BytesIO(response.content)) + # Create and start loader thread + # Keep reference to avoid garbage collection + self.thumbnail_thread = ThumbnailDownloadThread(url) + self.thumbnail_thread.finished.connect(self._on_thumbnail_downloaded) + self.thumbnail_thread.error.connect(lambda e: logger.error(f"Error loading thumbnail: {e}")) + self.thumbnail_thread.start() + + def _on_thumbnail_downloaded(self, content: bytes) -> None: + self = cast("YTSageApp", self) + try: + self.thumbnail_image = Image.open(BytesIO(content)) # Display thumbnail image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS) @@ -385,7 +412,7 @@ class VideoInfoMixin: pixmap.loadFromData(img_byte_arr.getvalue()) self.thumbnail_label.setPixmap(pixmap) except Exception as e: - logger.exception(f"Error loading thumbnail: {e}") + logger.exception(f"Error processing thumbnail image: {e}") def download_thumbnail_file(self, video_url, path) -> bool: self = cast("YTSageApp", self) # for autocompletion and type inference. From ea54f469ac5c94a78e1fa5996140478d5ab20697 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:16:48 +0200 Subject: [PATCH 020/134] Improve history loading and thumbnail caching UX Refactors the history loading process to emit entries as soon as they are loaded, allowing the UI to display immediately. Thumbnail downloads are now handled in the background, with widgets updated via a new signal when thumbnails are cached. This prevents UI freezes and improves responsiveness by showing placeholders until thumbnails are available. --- .../ytsage_dialogs_history.py | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 12fc717..29e474f 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -42,14 +42,18 @@ if TYPE_CHECKING: class HistoryLoaderThread(QThread): """Thread to load history and pre-fetch thumbnails.""" - finished = Signal(list) + entries_loaded = Signal(list) + thumbnail_loaded = Signal(str) def run(self): try: # HistoryManager uses get_all_entries, not get_all entries = HistoryManager.get_all_entries() - # Pre-fetch thumbnails so UI doesn't freeze + # Emit entries immediately so UI shows up + self.entries_loaded.emit(entries) + + # Pre-fetch thumbnails in background for entry in entries: thumbnail_url = entry.get("thumbnail_url") entry_id = entry.get("id", "") @@ -69,14 +73,13 @@ class HistoryLoaderThread(QThread): image = Image.open(BytesIO(response.content)) image.save(thumbnail_path, "JPEG", quality=95, optimize=True) + self.thumbnail_loaded.emit(entry_id) except Exception as e: logger.debug(f"Error caching thumbnail in background: {e}") - self.finished.emit(entries) - except Exception as e: logger.error(f"Error loading history: {e}") - self.finished.emit([]) + self.entries_loaded.emit([]) class HistoryEntryWidget(QFrame): @@ -246,40 +249,13 @@ class HistoryEntryWidget(QFrame): except Exception as e: logger.debug(f"Error loading cached thumbnail: {e}") - # Download thumbnail - try: - response = requests.get(thumbnail_url, timeout=5) - response.raise_for_status() - - image = Image.open(BytesIO(response.content)) - - # Don't resize, keep original quality and just save at higher quality - # The QPixmap scaling will handle the display size with high quality - - # Save to cache with higher quality - try: - APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True) - image.save(thumbnail_path, "JPEG", quality=95, optimize=True) - except Exception as e: - logger.debug(f"Error caching thumbnail: {e}") - - # Convert to QPixmap - image_bytes = BytesIO() - image.save(image_bytes, format="JPEG", quality=95) - image_bytes.seek(0) - - pixmap = QPixmap() - pixmap.loadFromData(image_bytes.read()) - - if not pixmap.isNull(): - # Don't scale here, let setScaledContents handle it - self.thumbnail_label.setPixmap(pixmap) - else: - self.set_placeholder_thumbnail() - - except Exception as e: - logger.debug(f"Error downloading thumbnail: {e}") - self.set_placeholder_thumbnail() + # If not in cache, just set placeholder. + # Background thread will download it and notify parent to reload. + self.set_placeholder_thumbnail() + + def reload_thumbnail(self): + """Reload thumbnail from cache (called when background download finishes).""" + self.load_thumbnail() def set_placeholder_thumbnail(self): """Set a placeholder when thumbnail is not available.""" @@ -401,6 +377,7 @@ class HistoryDialog(QDialog): super().__init__(parent) self.parent_app = parent self.entry_widgets = [] + self.entry_widgets_map = {} # Map entry_id -> widget self.setup_ui() @@ -414,12 +391,18 @@ class HistoryDialog(QDialog): def start_loading_history(self): """Start the background thread to load history.""" self.loader_thread = HistoryLoaderThread() - self.loader_thread.finished.connect(self.on_history_loaded) + self.loader_thread.entries_loaded.connect(self.on_history_loaded) + self.loader_thread.thumbnail_loaded.connect(self.update_entry_thumbnail) self.loader_thread.start() def on_history_loaded(self, entries): """Called when history is loaded from background thread.""" self.load_history_entries(entries) + + def update_entry_thumbnail(self, entry_id: str): + """Called when a thumbnail is downloaded in the background.""" + if entry_id in self.entry_widgets_map: + self.entry_widgets_map[entry_id].reload_thumbnail() def setup_ui(self): """Setup the dialog UI.""" @@ -528,6 +511,7 @@ class HistoryDialog(QDialog): for widget in self.entry_widgets: widget.deleteLater() self.entry_widgets.clear() + self.entry_widgets_map.clear() def show_loading_state(self): """Show loading indicator.""" @@ -572,6 +556,11 @@ class HistoryDialog(QDialog): widget.redownload_requested.connect(self.handle_redownload) self.history_layout.addWidget(widget) + + # Map widget by ID for updates + entry_id = entry.get("id") + if entry_id: + self.entry_widgets_map[entry_id] = widget self.entry_widgets.append(widget) # Update status From 29f713cf1733e123c9c052f4eb78d7676890835c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:19:49 +0200 Subject: [PATCH 021/134] Refine HistoryEntryWidget layout and sizing Adjusted padding, margin, and spacing for a more compact layout. Standardized the thumbnail size to 240x135 (16:9 ratio) and reduced the menu button size for better visual balance. --- .../ytsage_dialogs_history.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 29e474f..6345ff5 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -103,8 +103,8 @@ class HistoryEntryWidget(QFrame): background-color: #1d1e22; border: 1px solid #2a2d36; border-radius: 8px; - padding: 10px; - margin: 5px; + padding: 4px; + margin: 2px; } QFrame:hover { background-color: #252830; @@ -113,12 +113,12 @@ class HistoryEntryWidget(QFrame): """) main_layout = QHBoxLayout(self) - main_layout.setSpacing(15) - main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(12) + main_layout.setContentsMargins(8, 8, 8, 8) - # Thumbnail - Larger size to utilize available space + # Thumbnail - Standard YouTube 16:9 ratio (e.g., 240x135) self.thumbnail_label = QLabel() - self.thumbnail_label.setFixedSize(280, 158) # 16:9 ratio, larger to fill space + self.thumbnail_label.setFixedSize(240, 135) self.thumbnail_label.setStyleSheet(""" QLabel { border: 2px solid #3d3d3d; @@ -206,15 +206,16 @@ class HistoryEntryWidget(QFrame): # Three-dot menu button self.menu_button = QPushButton("тЛо") - self.menu_button.setFixedSize(40, 40) + self.menu_button.setFixedSize(32, 32) self.menu_button.setStyleSheet(""" QPushButton { background-color: #2a2d36; border: none; - border-radius: 20px; + border-radius: 16px; color: white; - font-size: 24px; + font-size: 18px; font-weight: bold; + padding-bottom: 5px; /* Adjust vertical alignment of dots */ } QPushButton:hover { background-color: #3a3d46; From 5f17fabc50755675e92cdbea82e88e6e6646542d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:56:56 +0200 Subject: [PATCH 022/134] Refactor history dialog to use virtualized list and SQLite Replaces the history dialog's widget-per-entry approach with a virtualized QListView using a custom model and delegate for improved performance. Switches the HistoryManager backend from JSON file storage to SQLite, including automatic migration of legacy data. Adds support for efficient search, thumbnail caching, and context menu actions in the new UI. --- .../ytsage_dialogs_history.py | 960 ++++++++---------- src/utils/ytsage_history_manager.py | 574 ++++++----- 2 files changed, 752 insertions(+), 782 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 6345ff5..58308db 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -1,19 +1,26 @@ """ History Dialog for YTSage application. -Displays download history with thumbnails and provides options to redownload or remove entries. +Displays download history with thumbnails using a virtualized list for performance. """ import os import subprocess +import json from datetime import datetime from io import BytesIO from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, List, Any, Dict import requests from PIL import Image -from PySide6.QtCore import Qt, QSize, Signal, QThread, QTimer -from PySide6.QtGui import QPixmap, QIcon +from PySide6.QtCore import ( + Qt, QSize, Signal, QThread, QTimer, QAbstractListModel, + QModelIndex, QRect, QPoint, QEvent +) +from PySide6.QtGui import ( + QPixmap, QIcon, QPainter, QColor, QFont, QBrush, QPen, + QMouseEvent, QDesktopServices, QAction, QCursor, QPainterPath +) from PySide6.QtWidgets import ( QDialog, QVBoxLayout, @@ -21,13 +28,13 @@ from PySide6.QtWidgets import ( QLabel, QLineEdit, QPushButton, - QScrollArea, + QListView, QWidget, - QFrame, QMenu, QMessageBox, - QSizePolicy, - QProgressBar, + QStyledItemDelegate, + QStyle, + QApplication ) from src.utils.ytsage_history_manager import HistoryManager @@ -43,18 +50,19 @@ class HistoryLoaderThread(QThread): """Thread to load history and pre-fetch thumbnails.""" entries_loaded = Signal(list) - thumbnail_loaded = Signal(str) + thumbnail_loaded = Signal(str, bytes) # entry_id, image_bytes def run(self): try: - # HistoryManager uses get_all_entries, not get_all + # Load entries from DB entries = HistoryManager.get_all_entries() - - # Emit entries immediately so UI shows up self.entries_loaded.emit(entries) - # Pre-fetch thumbnails in background + # Background thumbnail loader for entry in entries: + if self.isInterruptionRequested(): + break + thumbnail_url = entry.get("thumbnail_url") entry_id = entry.get("id", "") @@ -64,594 +72,494 @@ class HistoryLoaderThread(QThread): thumbnail_filename = f"{entry_id}.jpg" thumbnail_path = APP_THUMBNAILS_DIR / thumbnail_filename - # If not exists, download it if not thumbnail_path.exists(): try: response = requests.get(thumbnail_url, timeout=5) if response.status_code == 200: APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True) - image = Image.open(BytesIO(response.content)) - image.save(thumbnail_path, "JPEG", quality=95, optimize=True) - self.thumbnail_loaded.emit(entry_id) + # Optimize image before saving + img_io = BytesIO(response.content) + image = Image.open(img_io) + + # Save to disk + image.save(thumbnail_path, "JPEG", quality=90, optimize=True) + + # Emit bytes for memory cache + self.thumbnail_loaded.emit(entry_id, response.content) except Exception as e: - logger.debug(f"Error caching thumbnail in background: {e}") - + logger.debug(f"Error caching thumbnail: {e}") + except Exception as e: logger.error(f"Error loading history: {e}") self.entries_loaded.emit([]) -class HistoryEntryWidget(QFrame): - """Widget representing a single history entry.""" +class HistoryModel(QAbstractListModel): + """List Model for History Entries.""" - remove_requested = Signal(str) # Emit entry ID when remove is requested - redownload_requested = Signal(dict) # Emit entry data when redownload is requested - - def __init__(self, entry: dict, parent=None): + EntryRole = Qt.ItemDataRole.UserRole + 1 + IdRole = Qt.ItemDataRole.UserRole + 2 + ThumbnailRole = Qt.ItemDataRole.UserRole + 3 + + def __init__(self, entries=None, parent=None): super().__init__(parent) - self.entry = entry - self.entry_id = entry.get("id", "") + self._entries = entries or [] + self.thumbnail_cache = {} # Map entry_id -> QPixmap + + def rowCount(self, parent=QModelIndex()): + return len(self._entries) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if not index.isValid() or not (0 <= index.row() < len(self._entries)): + return None - self.setup_ui() + entry = self._entries[index.row()] + entry_id = entry.get("id") + + if role == self.EntryRole: + return entry + + elif role == self.IdRole: + return entry_id + + elif role == self.ThumbnailRole: + return self.thumbnail_cache.get(entry_id) + + elif role == Qt.ItemDataRole.DisplayRole: + return entry.get("title", "") + + return None + + def update_entries(self, entries): + self.beginResetModel() + self._entries = entries + self.endResetModel() + + def remove_item(self, row): + if 0 <= row < len(self._entries): + self.beginRemoveRows(QModelIndex(), row, row) + del self._entries[row] + self.endRemoveRows() + + def update_thumbnail(self, entry_id, pixmap): + """Update cache and notify view.""" + self.thumbnail_cache[entry_id] = pixmap + # Find index for this ID + for i, entry in enumerate(self._entries): + if entry.get("id") == entry_id: + idx = self.index(i) + self.dataChanged.emit(idx, idx, [self.ThumbnailRole]) + break + + +class HistoryDelegate(QStyledItemDelegate): + """Delegate to render history cards similar to the widgets.""" - def setup_ui(self): - """Setup the UI for this history entry.""" - self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised) - self.setStyleSheet(""" - QFrame { - background-color: #1d1e22; - border: 1px solid #2a2d36; - border-radius: 8px; - padding: 4px; - margin: 2px; - } - QFrame:hover { - background-color: #252830; - border-color: #3a3d46; - } - """) + menu_clicked = Signal(QModelIndex, QPoint) # Signal for menu click + + def __init__(self, parent=None): + super().__init__(parent) + self.padding = 10 + self.thumb_width = 240 + self.thumb_height = 135 + # Increased card height to accommodate spacing + self.card_height = 175 + # Define margins for spacing between cards + self.h_margin = 10 + self.v_margin = 8 + + def sizeHint(self, option, index): + return QSize(option.rect.width(), self.card_height) + + def paint(self, painter, option, index): + entry = index.data(HistoryModel.EntryRole) + if not entry: + return + + painter.save() + painter.setRenderHint(QPainter.RenderHint.Antialiasing) - main_layout = QHBoxLayout(self) - main_layout.setSpacing(12) - main_layout.setContentsMargins(8, 8, 8, 8) + rect = option.rect + # Apply margins for spacing + card_rect = rect.adjusted(self.h_margin, self.v_margin, -self.h_margin, -self.v_margin) - # Thumbnail - Standard YouTube 16:9 ratio (e.g., 240x135) - self.thumbnail_label = QLabel() - self.thumbnail_label.setFixedSize(240, 135) - self.thumbnail_label.setStyleSheet(""" - QLabel { - border: 2px solid #3d3d3d; - border-radius: 6px; - background-color: #15181b; - } - """) - self.thumbnail_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.thumbnail_label.setScaledContents(True) + is_hover = option.state & QStyle.StateFlag.State_MouseOver + bg_color = QColor("#252830") if is_hover else QColor("#1d1e22") + border_color = QColor("#3a3d46") if is_hover else QColor("#2a2d36") - # Load thumbnail - self.load_thumbnail() + # Draw Card + path = QPainterPath() + path.addRoundedRect(card_rect, 8, 8) - main_layout.addWidget(self.thumbnail_label, alignment=Qt.AlignmentFlag.AlignTop) + painter.fillPath(path, QBrush(bg_color)) + painter.setPen(QPen(border_color, 1)) + painter.drawPath(path) - # Info section - info_layout = QVBoxLayout() - info_layout.setSpacing(5) + # Draw Thumbnail + thumb_rect = QRect( + card_rect.left() + 10, + card_rect.top() + 10, + self.thumb_width, + self.thumb_height + ) + + pixmap = index.data(HistoryModel.ThumbnailRole) + if pixmap and not pixmap.isNull(): + scaled = pixmap.scaled( + thumb_rect.size(), + Qt.AspectRatioMode.KeepAspectRatioByExpanding, + Qt.TransformationMode.SmoothTransformation + ) + # Clip to rect + painter.setClipRect(thumb_rect) + painter.drawPixmap(thumb_rect.topLeft(), scaled) + painter.setClipping(False) + else: + painter.fillRect(thumb_rect, QColor("#15181b")) + painter.setPen(QPen(QColor("#666666"))) + icon_char = "ЁЯО╡" if entry.get("is_audio_only") else "ЁЯУ╣" + painter.setFont(QFont("Segoe UI Emoji", 24)) + painter.drawText(thumb_rect, Qt.AlignmentFlag.AlignCenter, icon_char) + + # Draw Border around thumb + painter.setPen(QPen(QColor("#3d3d3d"), 2)) + painter.drawRect(thumb_rect) + + # Text Area + text_x = thumb_rect.right() + 12 + text_width = card_rect.right() - text_x - 50 # Title - title = self.entry.get("title", _("video_info.unknown_title")) - self.title_label = QLabel(title) - self.title_label.setWordWrap(True) - self.title_label.setStyleSheet(""" - QLabel { - font-size: 14px; - font-weight: bold; - color: #ffffff; - } - """) - info_layout.addWidget(self.title_label) + title_rect = QRect(text_x, thumb_rect.top(), text_width, 50) + painter.setPen(QColor("#ffffff")) + font_title = QFont() + font_title.setBold(True) + font_title.setPixelSize(14) + painter.setFont(font_title) - # Channel (if available) - channel = self.entry.get("channel") + # Use simple alignment flags + painter.drawText(title_rect, Qt.AlignmentFlag.AlignLeft | Qt.TextFlag.TextWordWrap, entry.get("title", "")) + + current_y = title_rect.bottom() + 5 + + # Channel + channel = entry.get("channel") if channel: - channel_label = QLabel(f"{_('video_info.channel')}: {channel}") - channel_label.setStyleSheet("color: #cccccc; font-size: 12px;") - info_layout.addWidget(channel_label) + painter.setPen(QColor("#cccccc")) + font_meta = QFont() + font_meta.setPixelSize(12) + painter.setFont(font_meta) + painter.drawText(text_x, current_y, f"{_('video_info.channel')}: {channel}") + current_y += 18 + + # Date + date_str = entry.get("download_date", "")[:16].replace('T', ' ') + if date_str: + painter.setPen(QColor("#aaaaaa")) + painter.setFont(QFont("Arial", 11)) + painter.drawText(text_x, current_y, f"{_('history.downloaded_on', date=date_str)}") + current_y += 25 + + # Badge + is_audio = entry.get("is_audio_only", False) + badge_text = _("history.audio_download") if is_audio else _("history.video_download") + badge_color = QColor("#0066cc") if is_audio else QColor("#c90000") - # Download date - download_date = self.entry.get("download_date", "") - if download_date: - try: - dt = datetime.fromisoformat(download_date) - date_str = dt.strftime("%Y-%m-%d %H:%M") - date_label = QLabel(_("history.downloaded_on", date=date_str)) - date_label.setStyleSheet("color: #aaaaaa; font-size: 11px;") - info_layout.addWidget(date_label) - except Exception as e: - logger.debug(f"Error parsing date: {e}") + badge_rect = QRect(text_x, current_y, 80, 20) + painter.setBrush(QBrush(badge_color)) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawRoundedRect(badge_rect, 3, 3) - # File size and type - file_size = self.entry.get("file_size", 0) - is_audio = self.entry.get("is_audio_only", False) + painter.setPen(QColor("white")) + font_badge = QFont() + font_badge.setBold(True) + font_badge.setPixelSize(10) + painter.setFont(font_badge) + painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, badge_text) - size_type_layout = QHBoxLayout() - - # File type badge - type_badge = QLabel(_("history.audio_download") if is_audio else _("history.video_download")) - type_badge.setStyleSheet(f""" - QLabel {{ - background-color: {'#c90000' if not is_audio else '#0066cc'}; - color: white; - padding: 2px 8px; - border-radius: 3px; - font-size: 10px; - font-weight: bold; - }} - """) - size_type_layout.addWidget(type_badge) - - # File size + # File Size + file_size = entry.get("file_size", 0) if file_size > 0: size_str = self.format_file_size(file_size) - size_label = QLabel(_("history.file_size", size=size_str)) - size_label.setStyleSheet("color: #aaaaaa; font-size: 11px;") - size_type_layout.addWidget(size_label) - - size_type_layout.addStretch() - info_layout.addLayout(size_type_layout) - - info_layout.addStretch() - - main_layout.addLayout(info_layout, 1) - - # Three-dot menu button - self.menu_button = QPushButton("тЛо") - self.menu_button.setFixedSize(32, 32) - self.menu_button.setStyleSheet(""" - QPushButton { - background-color: #2a2d36; - border: none; - border-radius: 16px; - color: white; - font-size: 18px; - font-weight: bold; - padding-bottom: 5px; /* Adjust vertical alignment of dots */ - } - QPushButton:hover { - background-color: #3a3d46; - } - QPushButton:pressed { - background-color: #c90000; - } - """) - self.menu_button.clicked.connect(self.show_menu) - - main_layout.addWidget(self.menu_button, alignment=Qt.AlignmentFlag.AlignTop) - - def load_thumbnail(self): - """Load and display the thumbnail.""" - thumbnail_url = self.entry.get("thumbnail_url") - - if not thumbnail_url: - self.set_placeholder_thumbnail() - return - - # Check if thumbnail is cached - thumbnail_filename = f"{self.entry_id}.jpg" - thumbnail_path = APP_THUMBNAILS_DIR / thumbnail_filename - - if thumbnail_path.exists(): - try: - pixmap = QPixmap(str(thumbnail_path)) - if not pixmap.isNull(): - # Don't scale here, let setScaledContents handle it - self.thumbnail_label.setPixmap(pixmap) - return - except Exception as e: - logger.debug(f"Error loading cached thumbnail: {e}") - - # If not in cache, just set placeholder. - # Background thread will download it and notify parent to reload. - self.set_placeholder_thumbnail() + painter.setPen(QColor("#aaaaaa")) + painter.setFont(QFont("Arial", 11)) + painter.drawText(badge_rect.right() + 10, current_y + 14, size_str) + + # Menu Button + menu_rect = self.get_menu_rect(card_rect) + + # Check hover on menu button specifically + mouse_pos = QCursor.pos() + if option.widget: + mouse_pos = option.widget.mapFromGlobal(mouse_pos) + + if menu_rect.contains(mouse_pos): + painter.setPen(QColor("#c90000")) + else: + painter.setPen(QColor("#ffffff")) + + painter.setFont(QFont("Arial", 18, QFont.Weight.Bold)) + painter.drawText(menu_rect, Qt.AlignmentFlag.AlignCenter, "тЛо") + + painter.restore() + + def get_menu_rect(self, card_rect): + return QRect(card_rect.right() - 40, card_rect.top() + 10, 30, 30) + + def editorEvent(self, event, model, option, index): + """Handle mouse clicks.""" + if event.type() == QEvent.Type.MouseButtonRelease: + if event.button() == Qt.MouseButton.LeftButton: + # Use same margins as paint to ensure hit consistency + card_rect = option.rect.adjusted(self.h_margin, self.v_margin, -self.h_margin, -self.v_margin) + menu_rect = self.get_menu_rect(card_rect) + + if menu_rect.contains(event.pos()): + self.menu_clicked.emit(index, event.globalPos()) + return True + + return super().editorEvent(event, model, option, index) - def reload_thumbnail(self): - """Reload thumbnail from cache (called when background download finishes).""" - self.load_thumbnail() - - def set_placeholder_thumbnail(self): - """Set a placeholder when thumbnail is not available.""" - self.thumbnail_label.setText("ЁЯУ╣" if not self.entry.get("is_audio_only") else "ЁЯО╡") - self.thumbnail_label.setStyleSheet(""" - QLabel { - border: 1px solid #3d3d3d; - border-radius: 4px; - background-color: #15181b; - color: #666666; - font-size: 48px; - } - """) - def format_file_size(self, size_bytes: int) -> str: - """Format file size in human-readable format.""" for unit in ['B', 'KB', 'MB', 'GB']: if size_bytes < 1024.0: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024.0 return f"{size_bytes:.1f} TB" + + +class HistoryDialog(QDialog): + """Dialog to display and manage download history.""" - def show_menu(self): - """Show the context menu with options.""" + redownload_requested = Signal(dict) + + def __init__(self, parent: Optional["YTSageApp"] = None): + super().__init__(parent) + self.parent_app = parent + + self.setup_ui() + self.show_loading_state() + + QTimer.singleShot(100, self.start_loading_history) + + def setup_ui(self): + self.setWindowTitle(_("history.title")) + self.resize(850, 600) + self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint) + self.setStyleSheet(""" + QDialog { background-color: #15181b; } + QLabel { color: #ffffff; } + """) + + layout = QVBoxLayout(self) + + # --- Header --- + header = QHBoxLayout() + title = QLabel(_("history.title")) + title.setStyleSheet("font-size: 18px; font-weight: bold;") + header.addWidget(title) + header.addStretch() + + self.clear_btn = QPushButton(_("history.clear_all")) + self.clear_btn.setStyleSheet(""" + QPushButton { + background-color: #c90000; color: white; padding: 6px 12px; + border: none; border-radius: 4px; font-weight: bold; + } + QPushButton:hover { background-color: #a50000; } + QPushButton:disabled { background-color: #555555; color: #aaaaaa; } + """) + self.clear_btn.clicked.connect(self.clear_all_history) + header.addWidget(self.clear_btn) + layout.addLayout(header) + + # --- Search --- + self.search_input = QLineEdit() + self.search_input.setPlaceholderText(_("history.search_placeholder")) + self.search_input.setStyleSheet(""" + QLineEdit { + padding: 8px; border: 2px solid #2a2d36; border-radius: 4px; + background-color: #1b2021; color: white; + } + """) + self.search_input.textChanged.connect(self.filter_history) + layout.addWidget(self.search_input) + + # --- List View --- + self.list_view = QListView() + self.list_view.setStyleSheet(""" + QListView { + background-color: transparent; + border: none; + outline: none; + } + QListView::item { + border: none; + background: transparent; + } + """) + self.list_view.setVerticalScrollMode(QListView.ScrollMode.ScrollPerPixel) + self.list_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.list_view.setUniformItemSizes(True) + self.list_view.setSelectionMode(QListView.SelectionMode.NoSelection) + self.list_view.setMouseTracking(True) + self.list_view.setResizeMode(QListView.ResizeMode.Adjust) + + self.model = HistoryModel([], self) + self.list_view.setModel(self.model) + + self.delegate = HistoryDelegate(self.list_view) + self.delegate.menu_clicked.connect(self.show_context_menu) + self.list_view.setItemDelegate(self.delegate) + + self.list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.list_view.customContextMenuRequested.connect(self.on_context_menu_requested) + + layout.addWidget(self.list_view) + + # --- Status --- + self.status_label = QLabel() + self.status_label.setStyleSheet("color: #aaaaaa; font-size: 12px;") + layout.addWidget(self.status_label) + + def show_loading_state(self): + self.status_label.setText("Loading history...") + self.clear_btn.setEnabled(False) + + def start_loading_history(self): + self.loader_thread = HistoryLoaderThread() + self.loader_thread.entries_loaded.connect(self.on_entries_loaded) + self.loader_thread.thumbnail_loaded.connect(self.on_thumbnail_loaded) + self.loader_thread.start() + + def on_entries_loaded(self, entries): + self.model.update_entries(entries) + + count = len(entries) + if count == 0: + self.status_label.setText(_("history.no_history")) + self.clear_btn.setEnabled(False) + else: + self.status_label.setText(_("history.entries_count", count=count)) + self.clear_btn.setEnabled(True) + + self.load_cached_thumbnails(entries) + + def load_cached_thumbnails(self, entries): + for entry in entries: + eid = entry.get("id") + if not eid: continue + + p = APP_THUMBNAILS_DIR / f"{eid}.jpg" + if p.exists(): + pix = QPixmap(str(p)) + if not pix.isNull(): + self.model.thumbnail_cache[eid] = pix + + def on_thumbnail_loaded(self, entry_id, data_bytes): + pixmap = QPixmap() + pixmap.loadFromData(data_bytes) + if not pixmap.isNull(): + self.model.update_thumbnail(entry_id, pixmap) + + def on_context_menu_requested(self, pos): + index = self.list_view.indexAt(pos) + if index.isValid(): + global_pos = self.list_view.mapToGlobal(pos) + self.show_context_menu(index, global_pos) + + def show_context_menu(self, index, global_pos): + entry = index.data(HistoryModel.EntryRole) + if not entry: return + menu = QMenu(self) menu.setStyleSheet(""" QMenu { - background-color: #2a2d36; - border: 1px solid #3a3d46; - color: white; - padding: 5px; + background-color: #2a2d36; border: 1px solid #3a3d46; color: white; } QMenu::item { padding: 8px 20px; - border-radius: 4px; } QMenu::item:selected { background-color: #c90000; } """) - # Open file location - open_action = menu.addAction("ЁЯУБ " + _("history.open_location")) - open_action.triggered.connect(self.open_file_location) - - # Redownload - redownload_action = menu.addAction("тмЗя╕П " + _("history.redownload")) - redownload_action.triggered.connect(self.redownload) - + act_open = menu.addAction("ЁЯУБ " + _("history.open_location")) + act_redownload = menu.addAction("тмЗя╕П " + _("history.redownload")) menu.addSeparator() + act_remove = menu.addAction("ЁЯЧСя╕П " + _("history.remove")) - # Remove from history - remove_action = menu.addAction("ЁЯЧСя╕П " + _("history.remove")) - remove_action.triggered.connect(self.remove_from_history) + action = menu.exec(global_pos) - # Show menu at button position - menu.exec(self.menu_button.mapToGlobal(self.menu_button.rect().bottomLeft())) - - def open_file_location(self): - """Open the file location in the system file explorer.""" - file_path = Path(self.entry.get("file_path", "")) + if action == act_open: + self.open_file_location(entry) + elif action == act_redownload: + self.redownload_entry(entry) + elif action == act_remove: + self.remove_entry(index) + + def open_file_location(self, entry): + path_str = entry.get("file_path", "") + if not path_str: return - if not file_path.exists(): - QMessageBox.warning( - self, - _("history.file_not_found"), - _("history.file_not_found_message", path=str(file_path)) - ) + path = Path(path_str) + if not path.exists(): + QMessageBox.warning(self, "Error", f"File not found: {path}") return - - try: - # On Windows, use explorer with /select to highlight the file - if os.name == "nt": - subprocess.run(['explorer', '/select,', str(file_path)], creationflags=SUBPROCESS_CREATIONFLAGS) - # On macOS, use open with -R to reveal in Finder - elif subprocess.sys.platform == "darwin": - subprocess.run(['open', '-R', str(file_path)]) - # On Linux, try to open the folder - else: - folder_path = file_path.parent - subprocess.run(['xdg-open', str(folder_path)]) - logger.info(f"Opened file location: {file_path}") + try: + if os.name == "nt": + subprocess.run(['explorer', '/select,', str(path)], creationflags=SUBPROCESS_CREATIONFLAGS) + elif subprocess.sys.platform == "darwin": + subprocess.run(['open', '-R', str(path)]) + else: + folder_path = path.parent + subprocess.run(['xdg-open', str(folder_path)]) except Exception as e: - logger.exception(f"Error opening file location: {e}") - QMessageBox.warning(self, "Error", f"Could not open file location: {str(e)}") - - def redownload(self): - """Request redownload of this entry.""" + logger.error(f"Failed to open file: {e}") + + def redownload_entry(self, entry): reply = QMessageBox.question( self, _("history.redownload_confirm_title"), - _("history.redownload_confirm_message", title=self.entry.get("title", "")), + _("history.redownload_confirm_message", title=entry.get("title")), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - if reply == QMessageBox.StandardButton.Yes: - self.redownload_requested.emit(self.entry) - - def remove_from_history(self): - """Request removal of this entry from history.""" + self.redownload_requested.emit(entry) + self.accept() + + def remove_entry(self, index): + entry = index.data(HistoryModel.EntryRole) reply = QMessageBox.question( self, _("history.remove_confirm_title"), - _("history.remove_confirm_message", title=self.entry.get("title", "")), + _("history.remove_confirm_message", title=entry.get("title")), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - if reply == QMessageBox.StandardButton.Yes: - self.remove_requested.emit(self.entry_id) + if HistoryManager.remove_entry(entry.get("id")): + self.model.remove_item(index.row()) + self.status_label.setText( + _("history.entries_count", count=self.model.rowCount()) + ) - -class HistoryDialog(QDialog): - """Dialog to display and manage download history.""" - - redownload_requested = Signal(dict) # Signal to request redownload in main window - - def __init__(self, parent: Optional["YTSageApp"] = None): - super().__init__(parent) - self.parent_app = parent - self.entry_widgets = [] - self.entry_widgets_map = {} # Map entry_id -> widget - - self.setup_ui() - - # Show loading state initially - self.show_loading_state() - - # Start loading history in background after a short delay - # to ensure the dialog is shown first - QTimer.singleShot(100, self.start_loading_history) - - def start_loading_history(self): - """Start the background thread to load history.""" - self.loader_thread = HistoryLoaderThread() - self.loader_thread.entries_loaded.connect(self.on_history_loaded) - self.loader_thread.thumbnail_loaded.connect(self.update_entry_thumbnail) - self.loader_thread.start() - - def on_history_loaded(self, entries): - """Called when history is loaded from background thread.""" - self.load_history_entries(entries) - - def update_entry_thumbnail(self, entry_id: str): - """Called when a thumbnail is downloaded in the background.""" - if entry_id in self.entry_widgets_map: - self.entry_widgets_map[entry_id].reload_thumbnail() - - def setup_ui(self): - """Setup the dialog UI.""" - self.setWindowTitle(_("history.title")) - self.setMinimumSize(700, 500) - self.resize(850, 600) - - # Set window flags - self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint) - - layout = QVBoxLayout(self) - layout.setSpacing(10) - layout.setContentsMargins(20, 20, 20, 20) - - # Header with title and buttons - header_layout = QHBoxLayout() - - title_label = QLabel(_("history.title")) - title_label.setStyleSheet(""" - QLabel { - font-size: 18px; - font-weight: bold; - color: white; - } - """) - header_layout.addWidget(title_label) - - header_layout.addStretch() - - # Clear all button - self.clear_all_btn = QPushButton(_("history.clear_all")) - self.clear_all_btn.setStyleSheet(""" - QPushButton { - background-color: #c90000; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #a50000; - } - QPushButton:pressed { - background-color: #800000; - } - """) - self.clear_all_btn.clicked.connect(self.clear_all_history) - header_layout.addWidget(self.clear_all_btn) - - layout.addLayout(header_layout) - - # Search bar - self.search_input = QLineEdit() - self.search_input.setPlaceholderText(_("history.search_placeholder")) - self.search_input.setStyleSheet(""" - QLineEdit { - padding: 10px; - border: 2px solid #1b2021; - border-radius: 4px; - background-color: #1b2021; - color: #ffffff; - font-size: 13px; - } - """) - self.search_input.textChanged.connect(self.filter_history) - layout.addWidget(self.search_input) - - # Scroll area for history entries - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setStyleSheet(""" - QScrollArea { - border: none; - background-color: transparent; - } - """) - - # Container for history entries - self.history_container = QWidget() - self.history_layout = QVBoxLayout(self.history_container) - self.history_layout.setSpacing(10) - self.history_layout.setContentsMargins(0, 0, 0, 0) - self.history_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - scroll_area.setWidget(self.history_container) - layout.addWidget(scroll_area) - - # Status label - self.status_label = QLabel() - self.status_label.setStyleSheet("color: #aaaaaa; font-size: 12px;") - layout.addWidget(self.status_label) - - # Apply dark theme - self.setStyleSheet(""" - QDialog { - background-color: #15181b; - } - QLabel { - color: #ffffff; - } - """) - - def clear_history_list(self): - """Clear existing history widgets.""" - for widget in self.entry_widgets: - widget.deleteLater() - self.entry_widgets.clear() - self.entry_widgets_map.clear() - - def show_loading_state(self): - """Show loading indicator.""" - # Clear existing content - self.clear_history_list() - - loading_widget = QWidget() - loading_layout = QVBoxLayout(loading_widget) - loading_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - - spinner_label = QLabel("тП│") # Simple spinner icon - spinner_label.setStyleSheet("font-size: 48px; color: #c90000;") - spinner_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - loading_layout.addWidget(spinner_label) - - # text_label removed as per user request - - self.history_layout.addWidget(loading_widget) - self.entry_widgets.append(loading_widget) - - self.status_label.setText("Loading...") - self.clear_all_btn.setEnabled(False) - - def load_history(self): - """Load and display history entries.""" - if hasattr(self, 'history_container'): - self.show_loading_state() - self.start_loading_history() - - def load_history_entries(self, entries): - """Populate the history list with entries.""" - self.clear_history_list() - - if not entries: - self.show_empty_state() - return - - # Create widgets for each entry - for entry in entries: - widget = HistoryEntryWidget(entry, self.history_container) - widget.remove_requested.connect(self.remove_entry) - widget.redownload_requested.connect(self.handle_redownload) - - self.history_layout.addWidget(widget) - - # Map widget by ID for updates - entry_id = entry.get("id") - if entry_id: - self.entry_widgets_map[entry_id] = widget - self.entry_widgets.append(widget) - - # Update status - count = len(entries) - if count == 1: - status_text = _("history.one_entry") - else: - status_text = _("history.entries_count", count=count) - self.status_label.setText(status_text) - self.clear_all_btn.setEnabled(True) - - def show_empty_state(self): - """Show empty state when there's no history.""" - empty_widget = QWidget() - empty_layout = QVBoxLayout(empty_widget) - empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - - icon_label = QLabel("ЁЯУВ") - icon_label.setStyleSheet("font-size: 64px; color: #555555;") - icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.addWidget(icon_label) - - title_label = QLabel(_("history.no_history")) - title_label.setStyleSheet("font-size: 16px; color: #888888; font-weight: bold;") - title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.addWidget(title_label) - - desc_label = QLabel(_("history.no_history_description")) - desc_label.setStyleSheet("font-size: 13px; color: #666666;") - desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.addWidget(desc_label) - - self.history_layout.addWidget(empty_widget) - self.entry_widgets.append(empty_widget) - - self.status_label.setText("") - self.clear_all_btn.setEnabled(False) - - def filter_history(self, query: str): - """Filter history entries based on search query.""" - if not query: - # Show all entries - for widget in self.entry_widgets: - widget.show() - return - - # Hide/show based on query - query_lower = query.lower() - visible_count = 0 - - for widget in self.entry_widgets: - if isinstance(widget, HistoryEntryWidget): - title = (widget.entry.get("title") or "").lower() - channel = (widget.entry.get("channel") or "").lower() - - if query_lower in title or query_lower in channel: - widget.show() - visible_count += 1 - else: - widget.hide() - - def remove_entry(self, entry_id: str): - """Remove an entry from history.""" - success = HistoryManager.remove_entry(entry_id) - - if success: - # Reload history - self.load_history() - logger.info(f"Removed entry from history: {entry_id}") - else: - QMessageBox.warning(self, "Error", "Failed to remove entry from history") - def clear_all_history(self): - """Clear all history entries.""" reply = QMessageBox.question( self, _("history.clear_confirm_title"), _("history.clear_confirm_message"), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - if reply == QMessageBox.StandardButton.Yes: - count = HistoryManager.clear_history() - self.load_history() - logger.info(f"Cleared all history: {count} entries") - - def handle_redownload(self, entry: dict): - """Handle redownload request.""" - # Emit signal to parent window - self.redownload_requested.emit(entry) - - # Close dialog - self.accept() + HistoryManager.clear_history() + self.model.update_entries([]) + self.clear_btn.setEnabled(False) + self.status_label.setText(_("history.no_history")) + + def filter_history(self, query): + results = HistoryManager.search_entries(query) + self.model.update_entries(results) + self.load_cached_thumbnails(results) diff --git a/src/utils/ytsage_history_manager.py b/src/utils/ytsage_history_manager.py index b915f00..90fdbbe 100644 --- a/src/utils/ytsage_history_manager.py +++ b/src/utils/ytsage_history_manager.py @@ -3,144 +3,259 @@ History Manager Module ====================== This module provides **thread-safe** centralized management for download -history in YTSage. It handles reading, writing, and managing download history -stored in a JSON file. - -Thread safety is ensured using a reentrant lock (`RLock`), so multiple threads -can safely access or modify history concurrently. +history in YTSage using SQLite for high performance and scalability. Features -------- -- Thread-safe operations for getting, adding, and removing history entries. -- Loads history from a JSON file (`APP_HISTORY_FILE`). -- Creates the history file if missing or corrupt. -- Manages download history with metadata including thumbnails, file paths, and download options. -- Provides safe error handling with logging instead of raising exceptions. -- Persists updates back to disk automatically. +- Scalable: Uses SQLite instead of parsing potentially large JSON files. +- Thread-safe: Handles database connections safely. +- Migration: Automatically migrates legacy JSON history to SQLite. +- CRUD: Create, Read, Delete, Clear operations for history entries. Usage ----- from src.utils.ytsage_history_manager import HistoryManager # Add a download to history -HistoryManager.add_entry( - title="Video Title", - url="https://youtube.com/watch?v=...", - thumbnail_url="https://...", - file_path="/path/to/file.mp4", - format_id="137+140", - is_audio_only=False, - resolution="1080p", - download_options={...} -) +HistoryManager.add_entry(...) # Get all history entries history = HistoryManager.get_all_entries() +# Get recent entries (limit + offset support planned) # Remove an entry HistoryManager.remove_entry(entry_id) # Clear all history HistoryManager.clear_history() - -Design Notes ------------- -- History entries are stored in `HistoryManager._history` (a list of dicts). -- Each entry has a unique ID based on timestamp. -- All modifications trigger a save (`_save`) to keep JSON in sync. -- Logs actions and errors using the app's central logger. -- Uses `RLock` to allow safe concurrent access from multiple threads. - -Exceptions ----------- -- Any issues during file I/O (permissions, disk errors, JSON corruption) - are caught and logged. The application continues running with an empty - history when possible. """ import json +import sqlite3 import threading import time from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional -from src.utils.ytsage_constants import APP_HISTORY_FILE +from src.utils.ytsage_constants import APP_HISTORY_FILE, APP_DATA_DIR from src.utils.ytsage_logger import logger class HistoryManager: """ - Thread-safe history manager for YTSage. - - Provides methods to load, save, get, add, and remove download history entries. - Automatically persists changes to disk. + Thread-safe history manager for YTSage using SQLite. """ _lock = threading.RLock() - _history_file = APP_HISTORY_FILE - _history: List[Dict[str, Any]] = [] - _loaded = False + # Define DB file next to the old JSON file + _db_file = APP_DATA_DIR / "ytsage_history.db" + _initialized = False @classmethod - def _load(cls) -> None: - """ - Loads download history from a JSON file if it exists and is valid. - If the file is missing or corrupt, initializes with an empty history. - Logs actions and errors during the process. - """ - with cls._lock: - if cls._history_file.exists(): - try: - with open(cls._history_file, "r", encoding="utf-8") as f: - data = json.load(f) - # Ensure it's a list - if isinstance(data, list): - cls._history = data - else: - cls._history = [] - logger.warning("History file format invalid, initialized empty history.") - logger.info(f"History loaded from file: {len(cls._history)} entries.") - except json.JSONDecodeError: - cls._history = [] - logger.warning("History file corrupt, initialized empty history.") - except Exception as e: - cls._history = [] - logger.error(f"Error loading history file: {e}") - else: - cls._history = [] - cls._save() - logger.info("History file not found, created empty history.") - cls._loaded = True + def _init_db(cls): + """Initialize the database: create table and migrate if needed.""" + if cls._initialized: + return - @classmethod - def _save(cls) -> None: - """ - Save current history to JSON file. - - Note: - May raise exceptions if the file cannot be written due to permission issues, - disk errors, or other I/O problems. - """ with cls._lock: + # Check if file exists to know if we need to migrate or just create schema + db_exists = cls._db_file.exists() + legacy_json_exists = APP_HISTORY_FILE.exists() + try: - # Ensure parent directory exists - cls._history_file.parent.mkdir(parents=True, exist_ok=True) + # Ensure directory exists + cls._db_file.parent.mkdir(parents=True, exist_ok=True) - with open(cls._history_file, "w", encoding="utf-8") as f: - json.dump(cls._history, f, indent=2, ensure_ascii=False) - logger.debug(f"History saved to file: {len(cls._history)} entries.") - except (OSError, PermissionError) as e: - logger.exception(f"Failed to save history: {e}") - except Exception as e: - logger.exception(f"Unexpected error while saving history: {e}") + with sqlite3.connect(cls._db_file, check_same_thread=False) as conn: + cursor = conn.cursor() + + # Create table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS history ( + id TEXT PRIMARY KEY, + title TEXT, + url TEXT, + channel TEXT, + file_path TEXT, + download_date TEXT, + file_size INTEGER, + thumbnail_url TEXT, + format_id TEXT, + resolution TEXT, + is_audio_only INTEGER, + duration TEXT, + options TEXT, + timestamp REAL + ) + """) + + # Index for faster sorting by date + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_timestamp + ON history (timestamp DESC) + """) + + conn.commit() + + # If we just created the DB and have a JSON file, migrate + if not db_exists and legacy_json_exists: + cls._migrate_legacy_json() + + cls._initialized = True + + except sqlite3.Error as e: + logger.error(f"Failed to initialize history database: {e}") @classmethod - def _ensure_loaded(cls) -> None: - """Ensure history is loaded before any operation.""" - with cls._lock: - if not cls._loaded: - cls._load() + def _migrate_legacy_json(cls): + """Migrate legacy JSON history to SQLite.""" + logger.info("Migrating legacy history JSON to SQLite...") + try: + with open(APP_HISTORY_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + + if isinstance(data, list): + count = 0 + with sqlite3.connect(cls._db_file, check_same_thread=False) as conn: + cursor = conn.cursor() + for entry in data: + try: + # Safely extract download_options logic if complex + options_json = json.dumps(entry.get("download_options", {})) + + # Construct timestamp from isoformat if missing + ts = entry.get("timestamp") + if not ts and "download_date" in entry: + try: + dt = datetime.fromisoformat(entry["download_date"]) + ts = dt.timestamp() + except Exception: + ts = time.time() + + cursor.execute(""" + INSERT OR IGNORE INTO history ( + id, title, url, channel, file_path, download_date, + file_size, thumbnail_url, format_id, resolution, + is_audio_only, duration, options, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + entry.get("id", str(int(time.time()*1000))), + entry.get("title", ""), + entry.get("url", ""), + entry.get("channel", "Unknown"), + entry.get("file_path", ""), + entry.get("download_date", ""), + entry.get("file_size", 0), + entry.get("thumbnail_url", ""), + entry.get("format_id", ""), + entry.get("resolution", ""), + 1 if entry.get("is_audio_only") else 0, + entry.get("duration", ""), + options_json, + ts or time.time() + )) + count += 1 + except Exception as e: + logger.error(f"Skipped invalid entry during migration: {e}") + + conn.commit() + + logger.info(f"Successfully migrated {count} history entries.") + + # Rename old JSON to .bak to avoid re-migration, or keep as backup + try: + APP_HISTORY_FILE.rename(APP_HISTORY_FILE.with_suffix(".json.bak")) + except Exception as e: + logger.warning(f"Could not rename legacy history file: {e}") + + except Exception as e: + logger.error(f"Migration failed: {e}") + + @classmethod + def _get_connection(cls): + """Get a database connection.""" + cls._init_db() + return sqlite3.connect(cls._db_file, check_same_thread=False) + + @classmethod + def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]: + """ + Get all history entries, sorted by most recent first. + + Args: + limit: Optional limit on number of entries to return (most recent first) + + Returns: + List of dictionary entries. + """ + entries = [] + try: + with cls._lock: # Lock for simple concurrency safety + with cls._get_connection() as conn: + # Return dict-like rows + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + query = "SELECT * FROM history ORDER BY timestamp DESC" + params = () + + if limit is not None: + query += " LIMIT ?" + params = (limit,) + + cursor.execute(query, params) + rows = cursor.fetchall() + + for row in rows: + entry = dict(row) + # Convert boolean back + entry["is_audio_only"] = bool(entry["is_audio_only"]) + # Parse options JSON + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] # Remove internal column + entries.append(entry) + + except Exception as e: + logger.error(f"Error fetching history: {e}") + + return entries + + @classmethod + def get_entry(cls, entry_id: str) -> Optional[Dict[str, Any]]: + """ + Get a specific history entry by ID. + + Args: + entry_id: The unique entry ID + + Returns: + Entry dictionary or None if not found + """ + try: + with cls._lock: + with cls._get_connection() as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT * FROM history WHERE id = ?", (entry_id,)) + row = cursor.fetchone() + + if row: + entry = dict(row) + entry["is_audio_only"] = bool(entry["is_audio_only"]) + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] + return entry + return None + except Exception as e: + logger.error(f"Error fetching entry {entry_id}: {e}") + return None @classmethod def add_entry( @@ -158,141 +273,99 @@ class HistoryManager: download_options: Optional[Dict[str, Any]] = None, ) -> str: """ - Add a new download entry to history. - - Args: - title: Video/audio title - url: Original URL - thumbnail_url: Thumbnail URL (can be None) - file_path: Path to downloaded file - format_id: Format ID used for download - is_audio_only: Whether it's audio-only download - resolution: Resolution string (e.g., "1080p", "best audio") - file_size: File size in bytes (optional) - channel: Channel name (optional) - duration: Duration string (optional) - download_options: Dictionary of all download options used (optional) - - Returns: - str: The unique ID of the created entry + Add a new entry to the download history. """ - cls._ensure_loaded() + if download_options is None: + download_options = {} + + timestamp = time.time() + # Ensure unique ID + unique_id = f"{int(timestamp * 1000)}" + download_date = datetime.fromtimestamp(timestamp).isoformat() - with cls._lock: - # Generate unique ID based on timestamp - entry_id = f"{int(time.time() * 1000)}" - - # Get file size if not provided - if file_size is None: - try: - file_path_obj = Path(file_path) - if file_path_obj.exists(): - file_size = file_path_obj.stat().st_size - except Exception as e: - logger.debug(f"Could not get file size: {e}") - file_size = 0 - - entry = { - "id": entry_id, - "title": title, - "url": url, - "thumbnail_url": thumbnail_url, - "file_path": file_path, - "download_date": datetime.now().isoformat(), - "format_id": format_id, - "is_audio_only": is_audio_only, - "resolution": resolution, - "file_size": file_size or 0, - "channel": channel, - "duration": duration, - "download_options": download_options or {}, - } - - # Add to beginning of list (most recent first) - cls._history.insert(0, entry) - cls._save() - - logger.info(f"Added entry to history: {title}") - return entry_id + # Determine file size if not provided + if file_size is None: + try: + p = Path(file_path) + if p.exists(): + file_size = p.stat().st_size + except Exception: + file_size = 0 - @classmethod - def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]: - """ - Retrieve all history entries. - - Args: - limit: Optional limit on number of entries to return (most recent first) - - Returns: - List of history entry dictionaries - """ - cls._ensure_loaded() + # Allow None for optional strings + channel = channel or "Unknown" + duration = duration or "" + thumbnail_url = thumbnail_url or "" - with cls._lock: - if limit is not None: - return cls._history[:limit] - return cls._history.copy() - - @classmethod - def get_entry(cls, entry_id: str) -> Optional[Dict[str, Any]]: - """ - Get a specific history entry by ID. - - Args: - entry_id: The unique entry ID - - Returns: - Entry dictionary or None if not found - """ - cls._ensure_loaded() - - with cls._lock: - for entry in cls._history: - if entry.get("id") == entry_id: - return entry.copy() - return None + try: + with cls._lock: + with cls._get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO history ( + id, title, url, channel, file_path, download_date, + file_size, thumbnail_url, format_id, resolution, + is_audio_only, duration, options, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + unique_id, + title, + url, + channel, + str(file_path), + download_date, + file_size, + thumbnail_url, + format_id, + resolution, + 1 if is_audio_only else 0, + duration, + json.dumps(download_options), + timestamp + )) + conn.commit() + + logger.info(f"Added history entry: {title}") + return unique_id + + except Exception as e: + logger.error(f"Error adding history entry: {e}") + return "" @classmethod def remove_entry(cls, entry_id: str) -> bool: - """ - Remove a specific entry from history. - - Args: - entry_id: The unique entry ID to remove - - Returns: - bool: True if entry was found and removed, False otherwise - """ - cls._ensure_loaded() - - with cls._lock: - for i, entry in enumerate(cls._history): - if entry.get("id") == entry_id: - removed = cls._history.pop(i) - cls._save() - logger.info(f"Removed entry from history: {removed.get('title', 'Unknown')}") - return True - - logger.debug(f"Entry ID '{entry_id}' not found in history.") + """Remove an entry from history by ID.""" + try: + with cls._lock: + with cls._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM history WHERE id = ?", (entry_id,)) + if cursor.rowcount > 0: + conn.commit() + logger.info(f"Removed history entry: {entry_id}") + return True + return False + except Exception as e: + logger.error(f"Error removing history entry: {e}") return False @classmethod def clear_history(cls) -> int: - """ - Clear all history entries. - - Returns: - int: Number of entries that were cleared - """ - cls._ensure_loaded() - - with cls._lock: - count = len(cls._history) - cls._history = [] - cls._save() - logger.info(f"Cleared all history: {count} entries removed.") + """Clear all history entries.""" + try: + with cls._lock: + with cls._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM history") + count = cursor.rowcount + conn.commit() + logger.info("History cleared") return count - + except Exception as e: + logger.error(f"Error clearing history: {e}") + return 0 + @classmethod def search_entries(cls, query: str) -> List[Dict[str, Any]]: """ @@ -304,45 +377,34 @@ class HistoryManager: Returns: List of matching history entries """ - cls._ensure_loaded() - if not query: return cls.get_all_entries() - - query_lower = query.lower() - - with cls._lock: - results = [] - for entry in cls._history: - # Search in title, channel, and URL - title = (entry.get("title") or "").lower() - channel = (entry.get("channel") or "").lower() - url = (entry.get("url") or "").lower() - - if query_lower in title or query_lower in channel or query_lower in url: - results.append(entry.copy()) - return results - - @classmethod - def get_statistics(cls) -> Dict[str, Any]: - """ - Get statistics about download history. - - Returns: - Dictionary with statistics (total_downloads, total_size, etc.) - """ - cls._ensure_loaded() - - with cls._lock: - total_downloads = len(cls._history) - total_size = sum(entry.get("file_size", 0) for entry in cls._history) - video_count = sum(1 for entry in cls._history if not entry.get("is_audio_only", False)) - audio_count = sum(1 for entry in cls._history if entry.get("is_audio_only", False)) + entries = [] + try: + search_pattern = f"%{query}%" + with cls._lock: + with cls._get_connection() as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM history + WHERE title LIKE ? OR channel LIKE ? OR url LIKE ? + ORDER BY timestamp DESC + """, (search_pattern, search_pattern, search_pattern)) + rows = cursor.fetchall() + + for row in rows: + entry = dict(row) + entry["is_audio_only"] = bool(entry["is_audio_only"]) + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] + entries.append(entry) + + except Exception as e: + logger.error(f"Error searching history: {e}") - return { - "total_downloads": total_downloads, - "total_size": total_size, - "video_count": video_count, - "audio_count": audio_count, - } + return entries From 7c2e46114108b47c4201283658ce68aab4c956b6 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:05:17 +0200 Subject: [PATCH 023/134] Refactor HistoryManager to use persistent DB connection Replaces per-operation SQLite connections with a persistent connection for improved efficiency and thread safety. Adds additional indexes for faster search on title, channel, and URL. Refactors all database access methods to use the persistent connection and simplifies transaction handling. --- src/utils/ytsage_history_manager.py | 374 +++++++++++++++------------- 1 file changed, 196 insertions(+), 178 deletions(-) diff --git a/src/utils/ytsage_history_manager.py b/src/utils/ytsage_history_manager.py index 90fdbbe..63e84e2 100644 --- a/src/utils/ytsage_history_manager.py +++ b/src/utils/ytsage_history_manager.py @@ -50,6 +50,7 @@ class HistoryManager: _lock = threading.RLock() # Define DB file next to the old JSON file _db_file = APP_DATA_DIR / "ytsage_history.db" + _connection = None _initialized = False @classmethod @@ -67,36 +68,46 @@ class HistoryManager: # Ensure directory exists cls._db_file.parent.mkdir(parents=True, exist_ok=True) - with sqlite3.connect(cls._db_file, check_same_thread=False) as conn: - cursor = conn.cursor() - - # Create table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS history ( - id TEXT PRIMARY KEY, - title TEXT, - url TEXT, - channel TEXT, - file_path TEXT, - download_date TEXT, - file_size INTEGER, - thumbnail_url TEXT, - format_id TEXT, - resolution TEXT, - is_audio_only INTEGER, - duration TEXT, - options TEXT, - timestamp REAL - ) - """) - - # Index for faster sorting by date - cursor.execute(""" - CREATE INDEX IF NOT EXISTS idx_timestamp - ON history (timestamp DESC) - """) - - conn.commit() + # We use a persistent connection to avoid churn + if cls._connection is None: + cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False) + cls._connection.row_factory = sqlite3.Row + + cursor = cls._connection.cursor() + + # Create table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS history ( + id TEXT PRIMARY KEY, + title TEXT, + url TEXT, + channel TEXT, + file_path TEXT, + download_date TEXT, + file_size INTEGER, + thumbnail_url TEXT, + format_id TEXT, + resolution TEXT, + is_audio_only INTEGER, + duration TEXT, + options TEXT, + timestamp REAL + ) + """) + + # Index for faster sorting by date + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_timestamp + ON history (timestamp DESC) + """) + + # Indexes for faster search (title, channel, url) + # This prevents full table scans during search + cursor.execute("CREATE INDEX IF NOT EXISTS idx_title ON history (title)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_channel ON history (channel)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_url ON history (url)") + + cls._connection.commit() # If we just created the DB and have a JSON file, migrate if not db_exists and legacy_json_exists: @@ -117,66 +128,74 @@ class HistoryManager: if isinstance(data, list): count = 0 - with sqlite3.connect(cls._db_file, check_same_thread=False) as conn: - cursor = conn.cursor() - for entry in data: - try: - # Safely extract download_options logic if complex - options_json = json.dumps(entry.get("download_options", {})) - - # Construct timestamp from isoformat if missing - ts = entry.get("timestamp") - if not ts and "download_date" in entry: - try: - dt = datetime.fromisoformat(entry["download_date"]) - ts = dt.timestamp() - except Exception: - ts = time.time() - - cursor.execute(""" - INSERT OR IGNORE INTO history ( - id, title, url, channel, file_path, download_date, - file_size, thumbnail_url, format_id, resolution, - is_audio_only, duration, options, timestamp - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - entry.get("id", str(int(time.time()*1000))), - entry.get("title", ""), - entry.get("url", ""), - entry.get("channel", "Unknown"), - entry.get("file_path", ""), - entry.get("download_date", ""), - entry.get("file_size", 0), - entry.get("thumbnail_url", ""), - entry.get("format_id", ""), - entry.get("resolution", ""), - 1 if entry.get("is_audio_only") else 0, - entry.get("duration", ""), - options_json, - ts or time.time() - )) - count += 1 - except Exception as e: - logger.error(f"Skipped invalid entry during migration: {e}") - - conn.commit() - - logger.info(f"Successfully migrated {count} history entries.") - - # Rename old JSON to .bak to avoid re-migration, or keep as backup + # Use the persistent connection + conn = cls._get_connection() try: - APP_HISTORY_FILE.rename(APP_HISTORY_FILE.with_suffix(".json.bak")) + with conn: # Transaction + cursor = conn.cursor() + for entry in data: + try: + # Safely extract download_options logic if complex + options_json = json.dumps(entry.get("download_options", {})) + + # Construct timestamp from isoformat if missing + ts = entry.get("timestamp") + if not ts and "download_date" in entry: + try: + dt = datetime.fromisoformat(entry["download_date"]) + ts = dt.timestamp() + except Exception: + ts = time.time() + + cursor.execute(""" + INSERT OR IGNORE INTO history ( + id, title, url, channel, file_path, download_date, + file_size, thumbnail_url, format_id, resolution, + is_audio_only, duration, options, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + entry.get("id", str(int(time.time()*1000))), + entry.get("title", ""), + entry.get("url", ""), + entry.get("channel", "Unknown"), + entry.get("file_path", ""), + entry.get("download_date", ""), + entry.get("file_size", 0), + entry.get("thumbnail_url", ""), + entry.get("format_id", ""), + entry.get("resolution", ""), + 1 if entry.get("is_audio_only") else 0, + entry.get("duration", ""), + options_json, + ts or time.time() + )) + count += 1 + except Exception as e: + logger.error(f"Skipped invalid entry during migration: {e}") + + logger.info(f"Successfully migrated {count} history entries.") + + # Rename old JSON to .bak to avoid re-migration, or keep as backup + try: + APP_HISTORY_FILE.rename(APP_HISTORY_FILE.with_suffix(".json.bak")) + except Exception as e: + logger.warning(f"Could not rename legacy history file: {e}") + except Exception as e: - logger.warning(f"Could not rename legacy history file: {e}") + logger.error(f"Migration transaction failed: {e}") except Exception as e: logger.error(f"Migration failed: {e}") @classmethod def _get_connection(cls): - """Get a database connection.""" + """Get the persistent database connection.""" cls._init_db() - return sqlite3.connect(cls._db_file, check_same_thread=False) + if cls._connection is None: + # Should be created in _init_db, but just in case + cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False) + cls._connection.row_factory = sqlite3.Row + return cls._connection @classmethod def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]: @@ -192,32 +211,33 @@ class HistoryManager: entries = [] try: with cls._lock: # Lock for simple concurrency safety - with cls._get_connection() as conn: - # Return dict-like rows - conn.row_factory = sqlite3.Row - cursor = conn.cursor() + # Use persistent connection + conn = cls._get_connection() + # conn.row_factory is already set in _init_db/_get_connection + + cursor = conn.cursor() + + query = "SELECT * FROM history ORDER BY timestamp DESC" + params = () + + if limit is not None: + query += " LIMIT ?" + params = (limit,) - query = "SELECT * FROM history ORDER BY timestamp DESC" - params = () - - if limit is not None: - query += " LIMIT ?" - params = (limit,) - - cursor.execute(query, params) - rows = cursor.fetchall() - - for row in rows: - entry = dict(row) - # Convert boolean back - entry["is_audio_only"] = bool(entry["is_audio_only"]) - # Parse options JSON - try: - entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} - except json.JSONDecodeError: - entry["download_options"] = {} - del entry["options"] # Remove internal column - entries.append(entry) + cursor.execute(query, params) + rows = cursor.fetchall() + + for row in rows: + entry = dict(row) + # Convert boolean back + entry["is_audio_only"] = bool(entry["is_audio_only"]) + # Parse options JSON + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] # Remove internal column + entries.append(entry) except Exception as e: logger.error(f"Error fetching history: {e}") @@ -237,21 +257,20 @@ class HistoryManager: """ try: with cls._lock: - with cls._get_connection() as conn: - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT * FROM history WHERE id = ?", (entry_id,)) - row = cursor.fetchone() - - if row: - entry = dict(row) - entry["is_audio_only"] = bool(entry["is_audio_only"]) - try: - entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} - except json.JSONDecodeError: - entry["download_options"] = {} - del entry["options"] - return entry + conn = cls._get_connection() + cursor = conn.cursor() + cursor.execute("SELECT * FROM history WHERE id = ?", (entry_id,)) + row = cursor.fetchone() + + if row: + entry = dict(row) + entry["is_audio_only"] = bool(entry["is_audio_only"]) + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] + return entry return None except Exception as e: logger.error(f"Error fetching entry {entry_id}: {e}") @@ -299,32 +318,32 @@ class HistoryManager: try: with cls._lock: - with cls._get_connection() as conn: - cursor = conn.cursor() - - cursor.execute(""" - INSERT INTO history ( - id, title, url, channel, file_path, download_date, - file_size, thumbnail_url, format_id, resolution, - is_audio_only, duration, options, timestamp - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - unique_id, - title, - url, - channel, - str(file_path), - download_date, - file_size, - thumbnail_url, - format_id, - resolution, - 1 if is_audio_only else 0, - duration, - json.dumps(download_options), - timestamp - )) - conn.commit() + conn = cls._get_connection() + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO history ( + id, title, url, channel, file_path, download_date, + file_size, thumbnail_url, format_id, resolution, + is_audio_only, duration, options, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + unique_id, + title, + url, + channel, + str(file_path), + download_date, + file_size, + thumbnail_url, + format_id, + resolution, + 1 if is_audio_only else 0, + duration, + json.dumps(download_options), + timestamp + )) + conn.commit() logger.info(f"Added history entry: {title}") return unique_id @@ -338,13 +357,13 @@ class HistoryManager: """Remove an entry from history by ID.""" try: with cls._lock: - with cls._get_connection() as conn: - cursor = conn.cursor() - cursor.execute("DELETE FROM history WHERE id = ?", (entry_id,)) - if cursor.rowcount > 0: - conn.commit() - logger.info(f"Removed history entry: {entry_id}") - return True + conn = cls._get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM history WHERE id = ?", (entry_id,)) + if cursor.rowcount > 0: + conn.commit() + logger.info(f"Removed history entry: {entry_id}") + return True return False except Exception as e: logger.error(f"Error removing history entry: {e}") @@ -355,11 +374,11 @@ class HistoryManager: """Clear all history entries.""" try: with cls._lock: - with cls._get_connection() as conn: - cursor = conn.cursor() - cursor.execute("DELETE FROM history") - count = cursor.rowcount - conn.commit() + conn = cls._get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM history") + count = cursor.rowcount + conn.commit() logger.info("History cleared") return count except Exception as e: @@ -384,25 +403,24 @@ class HistoryManager: try: search_pattern = f"%{query}%" with cls._lock: - with cls._get_connection() as conn: - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute(""" - SELECT * FROM history - WHERE title LIKE ? OR channel LIKE ? OR url LIKE ? - ORDER BY timestamp DESC - """, (search_pattern, search_pattern, search_pattern)) - rows = cursor.fetchall() - - for row in rows: - entry = dict(row) - entry["is_audio_only"] = bool(entry["is_audio_only"]) - try: - entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} - except json.JSONDecodeError: - entry["download_options"] = {} - del entry["options"] - entries.append(entry) + conn = cls._get_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM history + WHERE title LIKE ? OR channel LIKE ? OR url LIKE ? + ORDER BY timestamp DESC + """, (search_pattern, search_pattern, search_pattern)) + rows = cursor.fetchall() + + for row in rows: + entry = dict(row) + entry["is_audio_only"] = bool(entry["is_audio_only"]) + try: + entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {} + except json.JSONDecodeError: + entry["download_options"] = {} + del entry["options"] + entries.append(entry) except Exception as e: logger.error(f"Error searching history: {e}") From e2c4dfea4edca9a1d34007fcd4737edf3bc7e649 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:10:34 +0200 Subject: [PATCH 024/134] Increase menu button area in history delegate Expanded the clickable area for the menu button in HistoryDelegate to 60x50px and adjusted text width accordingly. This improves usability and addresses issues with the left side of the button not responding to clicks. --- .../ytsage_gui_dialogs/ytsage_dialogs_history.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 58308db..5227656 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -228,7 +228,7 @@ class HistoryDelegate(QStyledItemDelegate): # Text Area text_x = thumb_rect.right() + 12 - text_width = card_rect.right() - text_x - 50 + text_width = card_rect.right() - text_x - 85 # Leave even more space for the larger 60px button # Title title_rect = QRect(text_x, thumb_rect.top(), text_width, 50) @@ -305,7 +305,18 @@ class HistoryDelegate(QStyledItemDelegate): painter.restore() def get_menu_rect(self, card_rect): - return QRect(card_rect.right() - 40, card_rect.top() + 10, 30, 30) + # Widen the clickable area significantly (60x50) and shift slightly left + # to fix "left side not working" issues. + w = 60 + h = 50 + margin_right = 5 + margin_top = 5 + return QRect( + card_rect.right() - w - margin_right, + card_rect.top() + margin_top, + w, + h + ) def editorEvent(self, event, model, option, index): """Handle mouse clicks.""" From a871ded15f84c1c7268bf87b7833e05039afa4b9 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:52:48 +0200 Subject: [PATCH 025/134] Move update check to background thread Refactored the update checking logic into a QThread subclass to prevent blocking the UI during network requests. The update check now runs asynchronously, and the dialog is shown via a signal when an update is available. --- src/gui/ytsage_gui_main.py | 83 ++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 935d489..e08ff05 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -8,7 +8,7 @@ import markdown import pyglet import requests from packaging import version -from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot +from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QApplication, @@ -53,7 +53,51 @@ from src.utils.ytsage_localization import LocalizationManager, _ from src.utils.ytsage_history_manager import HistoryManager +class UpdateCheckThread(QThread): + update_available = Signal(str, str, str) # version, url, changelog + + def __init__(self, current_version): + super().__init__() + self.current_version = current_version + + def run(self): + try: + # Get the latest version info from PyPI (no rate limiting unlike GitHub API) + response = requests.get( + "https://pypi.org/pypi/ytsage/json", + timeout=10, + ) + response.raise_for_status() + + pypi_data = response.json() + latest_version = pypi_data["info"]["version"] + + # Compare versions + if version.parse(latest_version) > version.parse(self.current_version): + release_url = "https://github.com/oop7/YTSage/releases/latest" + + # Try to fetch changelog from GitHub (with fallback if rate-limited) + changelog = "View the full changelog on the [GitHub Releases](https://github.com/oop7/YTSage/releases) page." + try: + gh_response = requests.get( + "https://api.github.com/repos/oop7/YTSage/releases/latest", + headers={"Accept": "application/vnd.github.v3+json"}, + timeout=5, + ) + if gh_response.status_code == 200: + gh_data = gh_response.json() + changelog = gh_data.get("body", changelog) + except Exception: + # Silently fallback to static message if GitHub API fails + pass + + self.update_available.emit(latest_version, release_url, changelog) + except Exception as e: + logger.debug(f"Failed to check for updates: {e}") + + class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from mixins + def __init__(self) -> None: super().__init__() @@ -1161,39 +1205,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.signals.update_status.emit(_("download.resumed")) def check_for_updates(self) -> None: - try: - # Get the latest version info from PyPI (no rate limiting unlike GitHub API) - response = requests.get( - "https://pypi.org/pypi/ytsage/json", - timeout=10, - ) - response.raise_for_status() - - pypi_data = response.json() - latest_version = pypi_data["info"]["version"] - - # Compare versions - if version.parse(latest_version) > version.parse(self.version): - release_url = "https://github.com/oop7/YTSage/releases/latest" - - # Try to fetch changelog from GitHub (with fallback if rate-limited) - changelog = "View the full changelog on the [GitHub Releases](https://github.com/oop7/YTSage/releases) page." - try: - gh_response = requests.get( - "https://api.github.com/repos/oop7/YTSage/releases/latest", - headers={"Accept": "application/vnd.github.v3+json"}, - timeout=5, - ) - if gh_response.status_code == 200: - gh_data = gh_response.json() - changelog = gh_data.get("body", changelog) - except Exception: - # Silently fallback to static message if GitHub API fails (rate limit, etc.) - pass - - self.show_update_dialog(latest_version, release_url, changelog) - except Exception as e: - logger.exception(f"Failed to check for updates: {e}") + """Starts the update check in a background thread.""" + self.update_thread = UpdateCheckThread(self.version) + self.update_thread.update_available.connect(self.show_update_dialog) + self.update_thread.start() def show_update_dialog(self, latest_version, release_url, changelog) -> None: # Added changelog parameter msg = QDialog(self) From 4d4d9e10b9a42325d7dee631db635a4225a56a54 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:57:37 +0200 Subject: [PATCH 026/134] Refactor updater checks to use QThread for async operations Replaces manual threading for FFmpeg and Deno version checks with dedicated QThread subclasses (FFmpegCheckThread and DenoCheckThread). This improves integration with the Qt event loop, enables use of signals and slots for UI updates, and enhances error handling. --- .../ytsage_dialogs_updater.py | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index 82c61aa..9f74457 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -9,7 +9,7 @@ import threading from typing import Optional, Tuple, TYPE_CHECKING, cast import requests -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt, Signal, QThread, Slot from PySide6.QtWidgets import ( QCheckBox, QGroupBox, @@ -157,6 +157,32 @@ def check_ffmpeg_version() -> Tuple[bool, str, str]: return False, "Error", "Error" +class FFmpegCheckThread(QThread): + finished = Signal(bool, str, str) + error = Signal(str) + + def run(self): + try: + update_available, current_version, latest_version = check_ffmpeg_version() + self.finished.emit(update_available, current_version, latest_version) + except Exception as e: + logger.exception(f"Error checking FFmpeg version: {e}") + self.error.emit(str(e)) + + +class DenoCheckThread(QThread): + finished = Signal(bool, str, str) + error = Signal(str) + + def run(self): + try: + update_available, current_version, latest_version = check_deno_update() + self.finished.emit(update_available, current_version, latest_version) + except Exception as e: + logger.exception(f"Error checking Deno version: {e}") + self.error.emit(str(e)) + + class UpdaterTabWidget(QWidget): """Widget for the Updater tab in Custom Options dialog.""" @@ -754,22 +780,20 @@ class UpdaterTabWidget(QWidget): "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" ) - # Run check in background thread - def check_thread(): - try: - update_available, current_version, latest_version = check_ffmpeg_version() - - # Update UI in main thread - self.check_button.setEnabled(True) - self._update_check_results(update_available, current_version, latest_version) - - except Exception as e: - logger.exception(f"Error checking FFmpeg version: {e}") - self.check_button.setEnabled(True) - self._show_check_error(str(e)) - - thread = threading.Thread(target=check_thread, daemon=True) - thread.start() + self.ffmpeg_check_thread = FFmpegCheckThread() + self.ffmpeg_check_thread.finished.connect(self._on_ffmpeg_check_finished) + self.ffmpeg_check_thread.error.connect(self._on_ffmpeg_check_error) + self.ffmpeg_check_thread.start() + + @Slot(bool, str, str) + def _on_ffmpeg_check_finished(self, update_available, current_version, latest_version): + self.check_button.setEnabled(True) + self._update_check_results(update_available, current_version, latest_version) + + @Slot(str) + def _on_ffmpeg_check_error(self, error): + self.check_button.setEnabled(True) + self._show_check_error(error) def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None: """Handle completion of version check.""" @@ -819,22 +843,20 @@ class UpdaterTabWidget(QWidget): "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" ) - # Run check in background thread - def check_thread(): - try: - update_available, current_version, latest_version = check_deno_update() - - # Update UI in main thread - self.deno_check_button.setEnabled(True) - self._update_deno_check_results(update_available, current_version, latest_version) - - except Exception as e: - logger.exception(f"Error checking Deno version: {e}") - self.deno_check_button.setEnabled(True) - self._show_deno_check_error(str(e)) - - thread = threading.Thread(target=check_thread, daemon=True) - thread.start() + self.deno_check_thread = DenoCheckThread() + self.deno_check_thread.finished.connect(self._on_deno_check_finished) + self.deno_check_thread.error.connect(self._on_deno_check_error) + self.deno_check_thread.start() + + @Slot(bool, str, str) + def _on_deno_check_finished(self, update_available, current_version, latest_version): + self.deno_check_button.setEnabled(True) + self._update_deno_check_results(update_available, current_version, latest_version) + + @Slot(str) + def _on_deno_check_error(self, error): + self.deno_check_button.setEnabled(True) + self._show_deno_check_error(error) def _update_deno_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None: """Handle completion of Deno version check.""" From 9759fa565f88ec2f093f0af3232c9705c289f23b Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:35:02 +0200 Subject: [PATCH 027/134] Refactor UI startup and stylesheet handling Moved the main application stylesheet to a new src/gui/ytsage_stylesheet.py module and applied it via StyleSheet.MAIN. Deferred blocking startup checks (FFmpeg, yt-dlp, Deno, updates) until after the UI is shown to improve responsiveness. Improved subtitle file deletion logic and added destination filename capture in downloader. Added custom style for auto-update checkbox in updater dialog. --- main.py | 17 -- src/core/ytsage_downloader.py | 19 +- .../ytsage_dialogs_updater.py | 26 ++ src/gui/ytsage_gui_main.py | 228 +++--------------- src/gui/ytsage_stylesheet.py | 126 ++++++++++ 5 files changed, 198 insertions(+), 218 deletions(-) create mode 100644 src/gui/ytsage_stylesheet.py diff --git a/main.py b/main.py index e0ceba7..ce49b44 100644 --- a/main.py +++ b/main.py @@ -3,8 +3,6 @@ import sys from PySide6.QtWidgets import QApplication, QMessageBox from src.utils.ytsage_logger import logger -from src.core.ytsage_yt_dlp import check_ytdlp_binary, setup_ytdlp # Import the new yt-dlp setup functions -from src.core.ytsage_deno import check_deno_binary, setup_deno # Import the new Deno setup functions from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main @@ -22,21 +20,6 @@ def main(): logger.info("Starting YTSage application") app = QApplication(sys.argv) - # Get the expected binary path and check if it exists - if not check_ytdlp_binary(): - # No app-specific binary found, show setup dialog regardless of Python package - logger.warning("No yt-dlp binary found, starting setup process") - yt_dlp_path = setup_ytdlp() - if yt_dlp_path == "yt-dlp": # If user canceled or something went wrong - logger.warning("yt-dlp not configured properly") - - # Check for Deno binary - if not check_deno_binary(): - logger.warning("No Deno binary found, starting setup process") - deno_path = setup_deno() - if deno_path == "deno": # If user canceled or something went wrong - logger.warning("Deno not configured properly") - window = YTSageApp() # Instantiate the main application class window.show() logger.info("Application window shown, entering main loop") diff --git a/src/core/ytsage_downloader.py b/src/core/ytsage_downloader.py index 12f1ed2..48b99b6 100644 --- a/src/core/ytsage_downloader.py +++ b/src/core/ytsage_downloader.py @@ -189,9 +189,12 @@ class DownloadThread(QThread): def safe_delete(path: Path) -> bool: try: - path.unlink(missing_ok=True) - logger.debug(f"Deleted subtitle file: {path.name}") - return True + # Check if file exists before trying to delete + if path.exists(): + path.unlink(missing_ok=True) + logger.debug(f"Deleted subtitle file: {path.name}") + return True + return False except Exception as e: logger.exception(f"Error deleting subtitle file {path}: {e}") return False @@ -410,8 +413,6 @@ class DownloadThread(QThread): self._terminate_process_tree(self.process) # Add delay before cleanup to allow file handles to be released - # Force garbage collection to help release resources - gc.collect() time.sleep(2) self.cleanup_partial_files() self.status_signal.emit(_("download.cancelled")) @@ -622,6 +623,14 @@ class DownloadThread(QThread): if "Downloading webpage" in line or "Extracting URL" in line: self.status_signal.emit(_("download.fetching_info")) self.progress_signal.emit(0) + elif "[download] Destination:" in line: + # Extract the destination filename + match = re.search(r"Destination: (.+)", line) + if match: + dest_path = match.group(1).strip() + self.current_filename = Path(dest_path).name + self.last_file_path = dest_path + logger.debug(f"Captured destination filename: {self.current_filename}") elif "Downloading API JSON" in line: self.status_signal.emit(_("download.processing_playlist")) self.progress_signal.emit(0) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index 9f74457..06853cd 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -492,6 +492,32 @@ class UpdaterTabWidget(QWidget): # Enable/Disable auto-update checkbox self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates")) + self.auto_update_enabled.setStyleSheet( + """ + QCheckBox { + color: #ffffff; + spacing: 5px; + padding: 3px; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + border-radius: 9px; + } + QCheckBox::indicator:unchecked { + border: 2px solid #666666; + background: #1d1e22; + border-radius: 9px; + } + QCheckBox::indicator:checked { + border: 2px solid #c90000; + background: #c90000; + border-radius: 9px; + } + QCheckBox:disabled { color: #888888; } + QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } + """ + ) auto_update_layout.addWidget(self.auto_update_enabled) # Frequency options diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index e08ff05..d5852c4 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -51,6 +51,7 @@ from src.utils.ytsage_logger import logger from src.utils.ytsage_config_manager import ConfigManager from src.utils.ytsage_localization import LocalizationManager, _ from src.utils.ytsage_history_manager import HistoryManager +from src.gui.ytsage_stylesheet import StyleSheet class UpdateCheckThread(QThread): @@ -105,30 +106,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from saved_language = ConfigManager.get("language") or "en" LocalizationManager.initialize(saved_language) - # Check for FFmpeg before proceeding - if not check_ffmpeg(): - self.show_ffmpeg_dialog() - - # Check for yt-dlp in our app's bin directory or system PATH - ytdlp_path = get_yt_dlp_path() - if ytdlp_path == "yt-dlp": # Not found in app dir or PATH - self.show_ytdlp_setup_dialog() - else: - logger.info(f"Using yt-dlp from: {ytdlp_path}") - - # Check for Deno in our app's bin directory - deno_path = get_deno_path() - if deno_path == "deno": # Not found in app dir - self.show_deno_setup_dialog() - else: - logger.info(f"Using Deno from: {deno_path}") - self.version = APP_VERSION - self.check_for_updates() - - # Check for auto-updates if enabled - self.check_auto_update_ytdlp() - load_saved_path(self) # Load custom icon if ICON_PATH.exists(): @@ -176,178 +154,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.analysis_completed = False self.init_ui() - self.setStyleSheet( - """ - QMainWindow { - background-color: #15181b; - } - QWidget { - background-color: #15181b; - color: #ffffff; - } - QLineEdit { - padding: 5px 15px; - border: 2px solid #2a2d2e; - border-radius: 6px; - background-color: #1b2021; - color: #ffffff; - font-size: 13px; - } - QLineEdit:focus { - border-color: #ff6b6b; - } - QPushButton { - padding: 8px 15px; - background-color: #c90000; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - } - QPushButton:hover { - background-color: #a50000; - } - QPushButton:pressed { - background-color: #800000; - } - QPushButton:disabled { - background-color: #3d3d3d; - color: #888888; - } - QTableWidget { - border: 2px solid #1b2021; - border-radius: 4px; - background-color: #1b2021; - gridline-color: #1b2021; - } - QHeaderView::section { - background-color: #15181b; - padding: 5px; - border: 1px solid #1b2021; - color: #ffffff; - } - QProgressBar { - border: 2px solid #1b2021; - border-radius: 4px; - text-align: center; - color: white; - } - QProgressBar::chunk { - background-color: #c90000; - border-radius: 2px; - } - QLabel { - color: #ffffff; - } - /* Style for filter buttons */ - QPushButton.filter-btn { - background-color: #1b2021; - padding: 5px 10px; - margin: 0 5px; - } - QPushButton.filter-btn:checked { - background-color: #c90000; - } - QPushButton.filter-btn:hover { - background-color: #444444; - } - QPushButton.filter-btn:checked:hover { - background-color: #a50000; - } - /* Modern Scrollbar Styling */ - QScrollBar:vertical { - border: none; - background: #15181b; - width: 14px; - margin: 15px 0 15px 0; - border-radius: 7px; - } - QScrollBar::handle:vertical { - background: #404040; - min-height: 30px; - border-radius: 7px; - } - QScrollBar::handle:vertical:hover { - background: #505050; - } - QScrollBar::sub-line:vertical { - border: none; - background: #15181b; - height: 15px; - border-top-left-radius: 7px; - border-top-right-radius: 7px; - subcontrol-position: top; - subcontrol-origin: margin; - } - QScrollBar::add-line:vertical { - border: none; - background: #15181b; - height: 15px; - border-bottom-left-radius: 7px; - border-bottom-right-radius: 7px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - QScrollBar::sub-line:vertical:hover, - QScrollBar::add-line:vertical:hover { - background: #404040; - } - QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { - background: none; - width: 0; - height: 0; - } - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: none; - } - /* Horizontal Scrollbar */ - QScrollBar:horizontal { - border: none; - background: #15181b; - height: 14px; - margin: 0 15px 0 15px; - border-radius: 7px; - } - QScrollBar::handle:horizontal { - background: #404040; - min-width: 30px; - border-radius: 7px; - } - QScrollBar::handle:horizontal:hover { - background: #505050; - } - QScrollBar::sub-line:horizontal { - border: none; - background: #15181b; - width: 15px; - border-top-left-radius: 7px; - border-bottom-left-radius: 7px; - subcontrol-position: left; - subcontrol-origin: margin; - } - QScrollBar::add-line:horizontal { - border: none; - background: #15181b; - width: 15px; - border-top-right-radius: 7px; - border-bottom-right-radius: 7px; - subcontrol-position: right; - subcontrol-origin: margin; - } - QScrollBar::sub-line:horizontal:hover, - QScrollBar::add-line:horizontal:hover { - background: #404040; - } - QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { - background: none; - width: 0; - height: 0; - } - QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { - background: none; - } - """ - ) + + # Defer heavy start-up tasks to ensure UI renders immediately + QTimer.singleShot(100, self._perform_startup_checks) + + self.setStyleSheet(StyleSheet.MAIN) self.signals.update_progress.connect(self.update_progress_bar) # After adding format buttons @@ -361,7 +172,32 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from # Initialize UI state based on current mode self.handle_mode_change() - # Init_sound method is removed, serve no purpose. + + def _perform_startup_checks(self): + """Perform potentially blocking startup checks after UI is shown.""" + + # Check for FFmpeg before proceeding + if not check_ffmpeg(): + self.show_ffmpeg_dialog() + + # Check for yt-dlp in our app's bin directory or system PATH + ytdlp_path = get_yt_dlp_path() + if ytdlp_path == "yt-dlp": # Not found in app dir or PATH + self.show_ytdlp_setup_dialog() + else: + logger.info(f"Using yt-dlp from: {ytdlp_path}") + + # Check for Deno in our app's bin directory + deno_path = get_deno_path() + if deno_path == "deno": # Not found in app dir + self.show_deno_setup_dialog() + else: + logger.info(f"Using Deno from: {deno_path}") + + self.check_for_updates() + + # Check for auto-updates if enabled + QTimer.singleShot(2000, self.check_auto_update_ytdlp) # Further delay auto-update check def play_notification_sound(self) -> None: """Play notification sound asynchronously (non-blocking).""" diff --git a/src/gui/ytsage_stylesheet.py b/src/gui/ytsage_stylesheet.py new file mode 100644 index 0000000..99dfcbc --- /dev/null +++ b/src/gui/ytsage_stylesheet.py @@ -0,0 +1,126 @@ + +class StyleSheet: + MAIN = """ + QMainWindow { + background-color: #15181b; + } + QWidget { + background-color: #15181b; + color: #ffffff; + } + QLineEdit { + padding: 5px 15px; + border: 2px solid #2a2d2e; + border-radius: 6px; + background-color: #1b2021; + color: #ffffff; + font-size: 13px; + } + QLineEdit:focus { + border-color: #ff6b6b; + } + QPushButton { + padding: 8px 15px; + background-color: #c90000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #a50000; + } + QPushButton:pressed { + background-color: #800000; + } + QPushButton:disabled { + background-color: #3d3d3d; + color: #888888; + } + QTableWidget { + border: 2px solid #1b2021; + border-radius: 4px; + background-color: #1b2021; + gridline-color: #1b2021; + } + QHeaderView::section { + background-color: #15181b; + padding: 5px; + border: 1px solid #1b2021; + color: #ffffff; + } + QProgressBar { + border: 2px solid #1b2021; + border-radius: 4px; + text-align: center; + color: white; + } + QProgressBar::chunk { + background-color: #c90000; + border-radius: 2px; + } + QLabel { + color: #ffffff; + } + /* Style for filter buttons */ + QPushButton.filter-btn { + background-color: #1b2021; + padding: 5px 10px; + margin: 0 5px; + } + QPushButton.filter-btn:checked { + background-color: #c90000; + } + QPushButton.filter-btn:hover { + background-color: #444444; + } + QPushButton.filter-btn:checked:hover { + background-color: #a50000; + } + /* Modern Scrollbar Styling */ + QScrollBar:vertical { + border: none; + background: #15181b; + width: 14px; + margin: 15px 0 15px 0; + border-radius: 7px; + } + QScrollBar::handle:vertical { + background: #404040; + min-height: 30px; + border-radius: 7px; + } + QScrollBar::handle:vertical:hover { + background: #505050; + } + QScrollBar::sub-line:vertical { + border: none; + background: #15181b; + height: 15px; + border-top-left-radius: 7px; + border-top-right-radius: 7px; + subcontrol-position: top; + subcontrol-origin: margin; + } + QScrollBar::add-line:vertical { + border: none; + background: #15181b; + height: 15px; + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + subcontrol-position: bottom; + subcontrol-origin: margin; + } + QScrollBar::sub-line:vertical:hover, + QScrollBar::add-line:vertical:hover { + background: #404040; + } + QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + background: none; + width: 0; + height: 0; + } + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; + } + """ From 370836b814738ccdf4d21d3644c64c11280b8443 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:46:16 +0200 Subject: [PATCH 028/134] Refactor GUI styles to use centralized StyleSheet class Moved all inline widget style definitions in ytsage_gui_main.py to named constants in the StyleSheet class in ytsage_stylesheet.py. This centralizes and simplifies style management, improves maintainability, and ensures consistent theming across the application. --- src/gui/ytsage_gui_main.py | 419 ++--------------------------------- src/gui/ytsage_stylesheet.py | 290 ++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 396 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index d5852c4..e4caede 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -250,24 +250,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.paste_button.clicked.connect(self.paste_url) self.paste_button.setMinimumHeight(42) self.paste_button.setMinimumWidth(115) - self.paste_button.setStyleSheet(""" - QPushButton { - padding: 9px 20px; - background-color: #1b2021; - border: 2px solid #2a2d2e; - border-radius: 5px; - color: #ffffff; - font-weight: 600; - font-size: 13px; - } - QPushButton:hover { - background-color: #252829; - border-color: #3a3d3e; - } - QPushButton:pressed { - background-color: #1a1d1e; - } - """) + self.paste_button.setStyleSheet(StyleSheet.PASTE_BUTTON) # Analyze button with app's red theme self.analyze_button = QPushButton(_("buttons.analyze")) @@ -275,27 +258,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.analyze_button.setEnabled(False) # Disabled until URL is entered self.analyze_button.setMinimumHeight(42) self.analyze_button.setMinimumWidth(115) - self.analyze_button.setStyleSheet(""" - QPushButton { - padding: 9px 20px; - background-color: #c90000; - border: none; - border-radius: 5px; - color: white; - font-weight: 600; - font-size: 13px; - } - QPushButton:hover { - background-color: #a50000; - } - QPushButton:pressed { - background-color: #800000; - } - QPushButton:disabled { - background-color: #3d3d3d; - color: #888888; - } - """) + self.analyze_button.setStyleSheet(StyleSheet.ANALYZE_BUTTON) url_layout.addWidget(self.url_input, 1) url_layout.addWidget(self.paste_button) @@ -325,24 +288,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.playlist_select_btn = QPushButton(_("buttons.select_videos")) self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog) self.playlist_select_btn.setVisible(False) - self.playlist_select_btn.setStyleSheet( - """ - QPushButton { - padding: 6px 12px; - background-color: #1d1e22; - border: 1px solid #c90000; - border-radius: 4px; - color: white; - font-weight: normal; - text-align: left; - padding-left: 10px; - } - QPushButton:hover { - background-color: #2a2d36; - border-color: #a50000; - } - """ - ) + self.playlist_select_btn.setStyleSheet(StyleSheet.PLAYLIST_BUTTON) layout.addWidget(self.playlist_select_btn) # --- End Playlist Info Section --- @@ -365,86 +311,20 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.video_button = QPushButton(_("buttons.video")) self.video_button.setCheckable(True) self.video_button.setChecked(True) # Set video as default - self.video_button.setStyleSheet( - """ - QPushButton { - padding: 8px 15px; - background-color: #1d1e22; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - } - QPushButton:checked { - background-color: #c90000; - } - QPushButton:hover { - background-color: #2a2d36; - } - QPushButton:checked:hover { - background-color: #a50000; - } - """ - ) + self.video_button.setStyleSheet(StyleSheet.FORMAT_TOGGLE_BUTTON) self.format_buttons.addButton(self.video_button) self.format_layout.addWidget(self.video_button) # Audio button self.audio_button = QPushButton(_("buttons.audio_only")) self.audio_button.setCheckable(True) - self.audio_button.setStyleSheet( - """ - QPushButton { - padding: 8px 15px; - background-color: #1d1e22; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - } - QPushButton:checked { - background-color: #c90000; - } - QPushButton:hover { - background-color: #2a2d36; - } - QPushButton:checked:hover { - background-color: #a50000; - } - """ - ) + self.audio_button.setStyleSheet(StyleSheet.FORMAT_TOGGLE_BUTTON) self.format_buttons.addButton(self.audio_button) self.format_layout.addWidget(self.audio_button) # Add Merge Subtitles checkbox (Moved here) self.merge_subs_checkbox = QCheckBox(_("main_ui.merge_subtitles")) - self.merge_subs_checkbox.setStyleSheet( - """ - QCheckBox { - color: #ffffff; - padding: 5px; - margin-left: 20px; /* Consistent margin */ - } - QCheckBox::indicator { - width: 18px; - height: 18px; - border-radius: 9px; - } - QCheckBox::indicator:unchecked { - border: 2px solid #666666; - background: #1d1e22; - border-radius: 9px; - } - QCheckBox::indicator:checked { - border: 2px solid #c90000; - background: #c90000; - border-radius: 9px; - } - /* Add disabled state styling if needed */ - QCheckBox:disabled { color: #888888; } - QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } - """ - ) + self.merge_subs_checkbox.setStyleSheet(StyleSheet.CHECKBOX) # Initially disable it, will be enabled if subtitles are selected later self.merge_subs_checkbox.setEnabled(False) self.format_layout.addWidget(self.merge_subs_checkbox) @@ -453,60 +333,21 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.save_thumbnail_checkbox = QCheckBox(_("main_ui.save_thumbnail")) self.save_thumbnail_checkbox.setChecked(False) self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail) - self.save_thumbnail_checkbox.setStyleSheet( - """ - QCheckBox { - color: #ffffff; - padding: 5px; - margin-left: 20px; - } - QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; } - QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #1d1e22; border-radius: 9px; } - QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; border-radius: 9px; } - QCheckBox:disabled { color: #888888; } - QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } - """ - ) + self.save_thumbnail_checkbox.setStyleSheet(StyleSheet.CHECKBOX) self.format_layout.addWidget(self.save_thumbnail_checkbox) # Add Save Description Checkbox (Moved here) self.save_description_checkbox = QCheckBox(_("main_ui.save_description")) self.save_description_checkbox.setChecked(False) self.save_description_checkbox.stateChanged.connect(self.toggle_save_description) - self.save_description_checkbox.setStyleSheet( - """ - QCheckBox { - color: #ffffff; - padding: 5px; - margin-left: 20px; - } - QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; } - QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #1d1e22; border-radius: 9px; } - QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; border-radius: 9px; } - QCheckBox:disabled { color: #888888; } - QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } - """ - ) + self.save_description_checkbox.setStyleSheet(StyleSheet.CHECKBOX) self.format_layout.addWidget(self.save_description_checkbox) # Add Embed Chapters Checkbox self.embed_chapters_checkbox = QCheckBox(_("main_ui.embed_chapters")) self.embed_chapters_checkbox.setChecked(False) self.embed_chapters_checkbox.stateChanged.connect(self.toggle_embed_chapters) - self.embed_chapters_checkbox.setStyleSheet( - """ - QCheckBox { - color: #ffffff; - padding: 5px; - margin-left: 20px; - } - QCheckBox::indicator { width: 18px; height: 18px; border-radius: 9px; } - QCheckBox::indicator:unchecked { border: 2px solid #666666; background: #1d1e22; border-radius: 9px; } - QCheckBox::indicator:checked { border: 2px solid #c90000; background: #c90000; border-radius: 9px; } - QCheckBox:disabled { color: #888888; } - QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } - """ - ) + self.embed_chapters_checkbox.setStyleSheet(StyleSheet.CHECKBOX) self.format_layout.addWidget(self.embed_chapters_checkbox) self.format_layout.addStretch() @@ -567,36 +408,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from # Progress section with improved styling progress_layout = QVBoxLayout() self.progress_bar = QProgressBar() - self.progress_bar.setStyleSheet( - """ - QProgressBar { - border: 2px solid #3d3d3d; - border-radius: 4px; - text-align: center; - color: white; - background-color: #363636; - height: 25px; - } - QProgressBar::chunk { - background-color: #ff0000; - border-radius: 2px; - } - """ - ) + self.progress_bar.setStyleSheet(StyleSheet.PROGRESS_BAR) progress_layout.addWidget(self.progress_bar) # Add download details label with improved styling self.download_details_label = QLabel() self.download_details_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.download_details_label.setStyleSheet( - """ - QLabel { - color: #cccccc; - font-size: 12px; - padding: 5px; - } - """ - ) + self.download_details_label.setStyleSheet(StyleSheet.STATUS_LABEL) progress_layout.addWidget(self.download_details_label) # Create a horizontal layout for status label and open folder button @@ -604,40 +422,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.status_label = QLabel(_("app.ready")) self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.status_label.setStyleSheet( - """ - QLabel { - color: #cccccc; - font-size: 12px; - padding: 5px; - } - """ - ) + self.status_label.setStyleSheet(StyleSheet.STATUS_LABEL) status_layout.addWidget(self.status_label) # Add "Open Folder" button (initially hidden) self.open_folder_btn = QPushButton("ЁЯУБ") self.open_folder_btn.setToolTip(_("buttons.open_folder")) self.open_folder_btn.setFixedSize(30, 30) - self.open_folder_btn.setStyleSheet( - """ - QPushButton { - background-color: #2a2d2e; - color: #cccccc; - border: 1px solid #404040; - border-radius: 5px; - font-size: 16px; - padding: 2px; - } - QPushButton:hover { - background-color: #3a3d3e; - border: 1px solid #505050; - } - QPushButton:pressed { - background-color: #1a1d1e; - } - """ - ) + self.open_folder_btn.setStyleSheet(StyleSheet.OPEN_FOLDER_BUTTON) self.open_folder_btn.clicked.connect(self.open_download_folder) self.open_folder_btn.setVisible(False) # Hidden by default status_layout.addWidget(self.open_folder_btn) @@ -1092,17 +884,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from f"" ) message_label.setWordWrap(True) - message_label.setStyleSheet( - """ - QLabel { - background-color: #1d1e22; - border: 1px solid #3d3d3d; - border-radius: 6px; - padding: 15px; - margin: 5px 0; - } - """ - ) + message_label.setStyleSheet(StyleSheet.UPDATE_DIALOG_MESSAGE) layout.addWidget(message_label) # Changelog Section @@ -1126,34 +908,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from logger.warning(f"Error converting changelog markdown to HTML: {e}", exc_info=True) changelog_text.setPlainText(changelog) # Fallback to plain text - changelog_text.setStyleSheet( - """ - QTextEdit { - background-color: #1d1e22; - border: 2px solid #3d3d3d; - border-radius: 6px; - color: #ffffff; - padding: 10px; - font-family: 'Segoe UI', Arial, sans-serif; - font-size: 12px; - line-height: 1.4; - } - QScrollBar:vertical { - border: none; - background: #1d1e22; - width: 12px; - border-radius: 6px; - } - QScrollBar::handle:vertical { - background: #404040; - min-height: 20px; - border-radius: 6px; - } - QScrollBar::handle:vertical:hover { - background: #505050; - } - """ - ) + changelog_text.setStyleSheet(StyleSheet.UPDATE_DIALOG_CHANGELOG) changelog_text.setMaximumHeight(180) # Limit height layout.addWidget(changelog_text) @@ -1163,50 +918,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from download_btn = QPushButton(_('update_dialog.download_update')) download_btn.clicked.connect(lambda: self.open_release_page(release_url)) - download_btn.setStyleSheet( - """ - QPushButton { - padding: 10px 20px; - background-color: #c90000; - border: none; - border-radius: 6px; - color: white; - font-weight: bold; - font-size: 13px; - min-width: 140px; - } - QPushButton:hover { - background-color: #a50000; - } - QPushButton:pressed { - background-color: #800000; - } - """ - ) + download_btn.setStyleSheet(StyleSheet.UPDATE_DIALOG_DOWNLOAD_BTN) remind_btn = QPushButton(_('update_dialog.remind_later')) remind_btn.clicked.connect(msg.close) - remind_btn.setStyleSheet( - """ - QPushButton { - padding: 10px 20px; - background-color: #3d3d3d; - border: 1px solid #555555; - border-radius: 6px; - color: white; - font-weight: bold; - font-size: 13px; - min-width: 140px; - } - QPushButton:hover { - background-color: #4d4d4d; - border-color: #666666; - } - QPushButton:pressed { - background-color: #2d2d2d; - } - """ - ) + remind_btn.setStyleSheet(StyleSheet.UPDATE_DIALOG_REMIND_BTN) button_layout.addStretch() button_layout.addWidget(download_btn) @@ -1214,19 +930,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from layout.addLayout(button_layout) # Style the dialog with improved theme matching - msg.setStyleSheet( - """ - QDialog { - background-color: #15181b; - border: 1px solid #3d3d3d; - border-radius: 8px; - } - QLabel { - color: #ffffff; - font-size: 12px; - } - """ - ) + msg.setStyleSheet(StyleSheet.UPDATE_DIALOG_MAIN) msg.show() @@ -1419,31 +1123,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from msg_box.setWindowIcon(self.windowIcon()) # Style the dialog - msg_box.setStyleSheet( - """ - QMessageBox { - background-color: #2b2b2b; - } - QLabel { - color: #ffffff; - } - QPushButton { - padding: 8px 15px; - background-color: #ff0000; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - min-width: 80px; - } - QPushButton:hover { - background-color: #cc0000; - } - """ - ) + msg_box.setStyleSheet(StyleSheet.FILE_EXISTS_DIALOG) msg_box.exec() + # --- Add Toggle Methods Here --- def toggle_save_thumbnail(self, state) -> None: logger.debug(f"Raw thumbnail state received: {state}") # Debug: Print raw state @@ -1693,22 +1377,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from self.force_keyframes = dialog.get_force_keyframes() if self.download_section: - self.time_range_btn.setStyleSheet( - """ - QPushButton { - padding: 8px 15px; - background-color: #c90000; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - border: 2px solid white; - } - QPushButton:hover { - background-color: #a50000; - } - """ - ) + self.time_range_btn.setStyleSheet(StyleSheet.TIME_RANGE_BTN_ACTIVE) self.time_range_btn.setToolTip(f"Section set: {self.download_section}") else: # Reset to default style if no section is selected @@ -1726,28 +1395,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from success_dialog.setWindowTitle("yt-dlp Setup") success_dialog.setText(f"yt-dlp has been successfully configured at:\n{yt_dlp_path}") success_dialog.setWindowIcon(self.windowIcon()) - success_dialog.setStyleSheet( - """ - QMessageBox { - background-color: #15181b; - color: #ffffff; - } - QLabel { - color: #ffffff; - } - QPushButton { - padding: 8px 15px; - background-color: #c90000; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - } - QPushButton:hover { - background-color: #a50000; - } - """ - ) + success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) success_dialog.exec() def show_deno_setup_dialog(self) -> None: @@ -1759,28 +1407,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from success_dialog.setWindowTitle(_("deno.setup_required")) success_dialog.setText(f"{_('deno.success')}\n{deno_path}") success_dialog.setWindowIcon(self.windowIcon()) - success_dialog.setStyleSheet( - """ - QMessageBox { - background-color: #15181b; - color: #ffffff; - } - QLabel { - color: #ffffff; - } - QPushButton { - padding: 8px 15px; - background-color: #c90000; - border: none; - border-radius: 4px; - color: white; - font-weight: bold; - } - QPushButton:hover { - background-color: #a50000; - } - """ - ) + success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) success_dialog.exec() def _analyze_url_with_subprocess(self, url) -> None: diff --git a/src/gui/ytsage_stylesheet.py b/src/gui/ytsage_stylesheet.py index 99dfcbc..465a8ad 100644 --- a/src/gui/ytsage_stylesheet.py +++ b/src/gui/ytsage_stylesheet.py @@ -124,3 +124,293 @@ class StyleSheet: background: none; } """ + + PASTE_BUTTON = """ + QPushButton { + padding: 9px 20px; + background-color: #1b2021; + border: 2px solid #2a2d2e; + border-radius: 5px; + color: #ffffff; + font-weight: 600; + font-size: 13px; + } + QPushButton:hover { + background-color: #252829; + border-color: #3a3d3e; + } + QPushButton:pressed { + background-color: #1a1d1e; + } + """ + + ANALYZE_BUTTON = """ + QPushButton { + padding: 9px 20px; + background-color: #c90000; + border: none; + border-radius: 5px; + color: white; + font-weight: 600; + font-size: 13px; + } + QPushButton:hover { + background-color: #a50000; + } + QPushButton:pressed { + background-color: #800000; + } + QPushButton:disabled { + background-color: #3d3d3d; + color: #888888; + } + """ + + PLAYLIST_BUTTON = """ + QPushButton { + padding: 6px 12px; + background-color: #1d1e22; + border: 1px solid #c90000; + border-radius: 4px; + color: white; + font-weight: normal; + text-align: left; + padding-left: 10px; + } + QPushButton:hover { + background-color: #2a2d36; + border-color: #a50000; + } + """ + + FORMAT_TOGGLE_BUTTON = """ + QPushButton { + padding: 8px 15px; + background-color: #1d1e22; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:checked { + background-color: #c90000; + } + QPushButton:hover { + background-color: #2a2d36; + } + QPushButton:checked:hover { + background-color: #a50000; + } + """ + + CHECKBOX = """ + QCheckBox { + color: #ffffff; + padding: 5px; + margin-left: 20px; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + border-radius: 9px; + } + QCheckBox::indicator:unchecked { + border: 2px solid #666666; + background: #1d1e22; + border-radius: 9px; + } + QCheckBox::indicator:checked { + border: 2px solid #c90000; + background: #c90000; + border-radius: 9px; + } + QCheckBox:disabled { color: #888888; } + QCheckBox::indicator:disabled { border-color: #555555; background: #444444; } + """ + + PROGRESS_BAR = """ + QProgressBar { + border: 2px solid #3d3d3d; + border-radius: 4px; + text-align: center; + color: white; + background-color: #363636; + height: 25px; + } + QProgressBar::chunk { + background-color: #ff0000; + border-radius: 2px; + } + """ + + STATUS_LABEL = """ + QLabel { + color: #cccccc; + font-size: 12px; + padding: 5px; + } + """ + + OPEN_FOLDER_BUTTON = """ + QPushButton { + background-color: #2a2d2e; + color: #cccccc; + border: 1px solid #404040; + border-radius: 5px; + font-size: 16px; + padding: 2px; + } + QPushButton:hover { + background-color: #3a3d3e; + border: 1px solid #505050; + } + QPushButton:pressed { + background-color: #1a1d1e; + } + """ + + UPDATE_DIALOG_MESSAGE = """ + QLabel { + background-color: #1d1e22; + border: 1px solid #3d3d3d; + border-radius: 6px; + padding: 15px; + margin: 5px 0; + } + """ + + UPDATE_DIALOG_CHANGELOG = """ + QTextEdit { + background-color: #1d1e22; + border: 2px solid #3d3d3d; + border-radius: 6px; + color: #ffffff; + padding: 10px; + font-family: 'Segoe UI', Arial, sans-serif; + font-size: 12px; + line-height: 1.4; + } + QScrollBar:vertical { + border: none; + background: #1d1e22; + width: 12px; + border-radius: 6px; + } + QScrollBar::handle:vertical { + background: #404040; + min-height: 20px; + border-radius: 6px; + } + QScrollBar::handle:vertical:hover { + background: #505050; + } + """ + + UPDATE_DIALOG_DOWNLOAD_BTN = """ + QPushButton { + padding: 10px 20px; + background-color: #c90000; + border: none; + border-radius: 6px; + color: white; + font-weight: bold; + font-size: 13px; + min-width: 140px; + } + QPushButton:hover { + background-color: #a50000; + } + QPushButton:pressed { + background-color: #800000; + } + """ + + UPDATE_DIALOG_REMIND_BTN = """ + QPushButton { + padding: 10px 20px; + background-color: #3d3d3d; + border: 1px solid #555555; + border-radius: 6px; + color: white; + font-weight: bold; + font-size: 13px; + min-width: 140px; + } + QPushButton:hover { + background-color: #4d4d4d; + border-color: #666666; + } + QPushButton:pressed { + background-color: #2d2d2d; + } + """ + + UPDATE_DIALOG_MAIN = """ + QDialog { + background-color: #15181b; + border: 1px solid #3d3d3d; + border-radius: 8px; + } + QLabel { + color: #ffffff; + font-size: 12px; + } + """ + + TIME_RANGE_BTN_ACTIVE = """ + QPushButton { + padding: 8px 15px; + background-color: #c90000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + border: 2px solid white; + } + QPushButton:hover { + background-color: #a50000; + } + """ + + FILE_EXISTS_DIALOG = """ + QMessageBox { + background-color: #2b2b2b; + } + QLabel { + color: #ffffff; + } + QPushButton { + padding: 8px 15px; + background-color: #ff0000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + min-width: 80px; + } + QPushButton:hover { + background-color: #cc0000; + } + """ + + SETUP_SUCCESS_DIALOG = """ + QMessageBox { + background-color: #15181b; + color: #ffffff; + } + QLabel { + color: #ffffff; + } + QPushButton { + padding: 8px 15px; + background-color: #c90000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #a50000; + } + """ + From d5d7a0186dbb7123f8f9d06ac978c7b769ac5853 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:00:13 +0200 Subject: [PATCH 029/134] Refactor analysis logic into AnalysisMixin Moved all analysis-related methods from YTSageApp in ytsage_gui_main.py to a new AnalysisMixin in ytsage_gui_analysis.py. This improves code organization and separation of concerns by isolating analysis logic from the main application class. --- src/gui/ytsage_gui_analysis.py | 257 +++++++++++++++++++++++++++++++++ src/gui/ytsage_gui_main.py | 253 +------------------------------- 2 files changed, 259 insertions(+), 251 deletions(-) create mode 100644 src/gui/ytsage_gui_analysis.py diff --git a/src/gui/ytsage_gui_analysis.py b/src/gui/ytsage_gui_analysis.py new file mode 100644 index 0000000..06463a7 --- /dev/null +++ b/src/gui/ytsage_gui_analysis.py @@ -0,0 +1,257 @@ +from typing import TYPE_CHECKING, cast + +import json +import threading +import subprocess +from PySide6.QtCore import QMetaObject, Qt, Q_ARG +from PySide6.QtWidgets import QMessageBox + +from src.core.ytsage_utils import validate_video_url, parse_yt_dlp_error +from src.core.ytsage_yt_dlp import get_yt_dlp_path +from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS +from src.utils.ytsage_localization import _ +from src.utils.ytsage_logger import logger + +if TYPE_CHECKING: + from src.gui.ytsage_gui_main import YTSageApp + +class AnalysisMixin: + def analyze_url(self) -> None: + self = cast("YTSageApp", self) + if self.is_updating_ytdlp: + QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message")) + return + + url = self.url_input.text().strip() + if not url: + self.signals.update_status.emit(_("main_ui.invalid_url_or_enter")) + return + + # Validate URL before processing + is_valid, error_message = validate_video_url(url) + if not is_valid: + QMessageBox.warning(self, _("main_ui.error_title"), error_message) + return + + # Reset analysis state and disable controls + self.analysis_completed = False + self.toggle_analysis_dependent_controls(enabled=False) + + self.signals.update_status.emit(_("main_ui.analyzing_preparing")) + self.is_analyzing = True + threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() + + def _analyze_url_thread(self, url) -> None: + self = cast("YTSageApp", self) + try: + self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic")) + + # Clean up the URL to handle both playlist and video URLs + if "list=" in url and "watch?v=" in url: + playlist_id = url.split("list=")[1].split("&")[0] + url = f"https://www.youtube.com/playlist?list={playlist_id}" + + # Always use subprocess to call yt-dlp binary (Python package removed) + self._analyze_url_with_subprocess(url) + + except Exception as e: + logger.exception(f"Error in analysis: {e}") + self.signals.update_status.emit(_("errors.generic_error", error=str(e))) + # Ensure playlist UI is hidden on error too + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + finally: + self.is_analyzing = False + + + def _analyze_url_with_subprocess(self, url) -> None: + """Analyze URL using yt-dlp executable when Python module is not available""" + self = cast("YTSageApp", self) + + try: + yt_dlp_path = get_yt_dlp_path() + if not yt_dlp_path: + logger.error("yt-dlp executable not found. Please install yt-dlp first.") + self.signals.update_status.emit(_("errors.ytdlp_not_found")) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp")) + + # Clean up the URL to handle both playlist and video URLs + if "list=" in url and "watch?v=" in url: + playlist_id = url.split("list=")[1].split("&")[0] + url = f"https://www.youtube.com/playlist?list={playlist_id}" + + # Build command for basic info extraction + # Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json + # This fetches minimal info for all videos quickly without downloading full details for each + cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] + + # Add cookies if available + if self.cookie_file_path: + cmd.extend(["--cookies", str(self.cookie_file_path)]) + elif self.browser_cookies_option: + cmd.extend(["--cookies-from-browser", self.browser_cookies_option]) + + # Add proxy settings if available + if self.proxy_url: + cmd.extend(["--proxy", self.proxy_url]) + + if self.geo_proxy_url: + cmd.extend(["--geo-verification-proxy", self.geo_proxy_url]) + + logger.debug(f"Executing yt-dlp command: {cmd}") + + # Execute command with hidden console window on Windows + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS) + + if result.returncode != 0: + logger.error(f"yt-dlp failed: {result.stderr}") + self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr)) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()] + + if not json_lines: + logger.error("No data returned from yt-dlp") + self.signals.update_status.emit(_("errors.no_data_returned")) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + try: + first_info = json.loads(json_lines[0]) + except json.JSONDecodeError as e: + logger.error(f"Failed to parse yt-dlp output: {e}") + self.signals.update_status.emit(_("errors.parse_failed", error=str(e))) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + self.signals.update_status.emit(_("main_ui.analyzing_processing_data")) + + if first_info.get("_type") == "playlist": + # Handle playlist + self.is_playlist = True + self.playlist_info = first_info + self.selected_playlist_items = None + self.playlist_entries = first_info.get("entries", []) + + if not self.playlist_entries: + logger.error("Playlist contains no valid videos.") + self.signals.update_status.emit(_("errors.playlist_no_videos")) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + # Even with flat-playlist, we need one video's formats to populate the table. + # Just use the first video URL to get full details quickly. + self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video")) + first_video_entry = self.playlist_entries[0] + first_video_url = first_video_entry.get("url") + + # Fetch full info for just the first video to get formats + cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url] + # Add cookies & proxy to this single request too + if self.cookie_file_path: + cmd_single.extend(["--cookies", str(self.cookie_file_path)]) + elif self.browser_cookies_option: + cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option]) + if self.proxy_url: + cmd_single.extend(["--proxy", self.proxy_url]) + if self.geo_proxy_url: + cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url]) + + result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) + + if result_single.returncode == 0: + self.video_info = json.loads(result_single.stdout) + else: + # Fallback to whatever minimal info we have, might fail table population + self.video_info = first_video_entry + + # Update playlist info label + playlist_text = _("playlist.display_format", + title=first_info.get('title', _('playlist.unknown')), + count=len(self.playlist_entries)) + + self.signals.playlist_info_label_text.emit(playlist_text) + self.signals.playlist_info_label_visible.emit(True) + + # Show playlist selection button + self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all")) + self.signals.playlist_select_btn_visible.emit(True) + + else: + # Handle single video + self.is_playlist = False + self.video_info = first_info + self.playlist_entries = [] + self.selected_playlist_items = None + + # Hide playlist UI + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + + # Verify we have format information + if not self.video_info or "formats" not in self.video_info: + logger.error("No format information available") + self.signals.update_status.emit(_("errors.no_format_info")) + self.signals.playlist_info_label_visible.emit(False) + self.signals.playlist_select_btn_visible.emit(False) + return + + self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp")) + self.all_formats = self.video_info["formats"] + + # Update UI + self.update_video_info(self.video_info) + + # Update thumbnail + self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) + # Try to get thumbnail from playlist info first + # Fallback to video thumbnail if playlist thumbnail not found or not a playlist + thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail") + + self.download_thumbnail(thumbnail_url) + + # Save thumbnail if enabled + if self.save_thumbnail: + self.download_thumbnail_file(self.video_url, self.last_path) + + # Handle subtitles + self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) + self.selected_subtitles = [] + self.available_subtitles = self.video_info.get("subtitles", {}) + self.available_automatic_subtitles = self.video_info.get("automatic_captions", {}) + + # Update subtitle UI + self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected")) + + # Update format table + self.signals.update_status.emit(_("main_ui.analyzing_updating_table")) + self.video_button.setChecked(True) + self.audio_button.setChecked(False) + self.filter_formats() + + self.signals.update_status.emit(_("main_ui.analysis_complete")) + + # Mark analysis as complete and enable analysis-dependent controls + self.analysis_completed = True + QMetaObject.invokeMethod( + self, + "toggle_analysis_dependent_controls", + Qt.ConnectionType.QueuedConnection, + Q_ARG(bool, True), + ) + + except subprocess.TimeoutExpired: + logger.error("Analysis timed out") + self.signals.update_status.emit(_("errors.timeout")) + except Exception as e: + logger.exception(f"Unexpected error in analysis: {e}") + self.signals.update_status.emit(_("errors.generic_error", error=str(e))) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index e4caede..82d8d94 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -46,6 +46,7 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__ ) from src.gui.ytsage_gui_format_table import FormatTableMixin from src.gui.ytsage_gui_video_info import VideoInfoMixin +from src.gui.ytsage_gui_analysis import AnalysisMixin from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS from src.utils.ytsage_logger import logger from src.utils.ytsage_config_manager import ConfigManager @@ -97,7 +98,7 @@ class UpdateCheckThread(QThread): logger.debug(f"Failed to check for updates: {e}") -class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from mixins +class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Inherit from mixins def __init__(self) -> None: super().__init__() @@ -457,51 +458,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from """Enable or disable the Analyze button based on URL input content.""" self.analyze_button.setEnabled(bool(text.strip())) - def analyze_url(self) -> None: - if self.is_updating_ytdlp: - QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message")) - return - url = self.url_input.text().strip() - if not url: - self.signals.update_status.emit(_("main_ui.invalid_url_or_enter")) - return - - # Validate URL before processing - is_valid, error_message = validate_video_url(url) - if not is_valid: - QMessageBox.warning(self, _("main_ui.error_title"), error_message) - return - - # Reset analysis state and disable controls - self.analysis_completed = False - self.toggle_analysis_dependent_controls(enabled=False) - - self.signals.update_status.emit(_("main_ui.analyzing_preparing")) - self.is_analyzing = True - threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() - - def _analyze_url_thread(self, url) -> None: - try: - self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic")) - - # Clean up the URL to handle both playlist and video URLs - if "list=" in url and "watch?v=" in url: - playlist_id = url.split("list=")[1].split("&")[0] - url = f"https://www.youtube.com/playlist?list={playlist_id}" - - # Always use subprocess to call yt-dlp binary (Python package removed) - self._analyze_url_with_subprocess(url) - - except Exception as e: - logger.exception(f"Error in analysis: {e}") - self.signals.update_status.emit(_("errors.generic_error", error=str(e))) - # Ensure playlist UI is hidden on error too - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - finally: - self.is_analyzing = False def paste_url(self) -> None: clipboard = QApplication.clipboard() @@ -1410,210 +1367,4 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) success_dialog.exec() - def _analyze_url_with_subprocess(self, url) -> None: - """Analyze URL using yt-dlp executable when Python module is not available""" - try: - yt_dlp_path = get_yt_dlp_path() - if not yt_dlp_path: - logger.error("yt-dlp executable not found. Please install yt-dlp first.") - self.signals.update_status.emit(_("errors.ytdlp_not_found")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp")) - - # Clean up the URL to handle both playlist and video URLs - if "list=" in url and "watch?v=" in url: - playlist_id = url.split("list=")[1].split("&")[0] - url = f"https://www.youtube.com/playlist?list={playlist_id}" - - # Build command for basic info extraction - # Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json - # This fetches minimal info for all videos quickly without downloading full details for each - cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] - - # Add cookies if available - if self.cookie_file_path: - cmd.extend(["--cookies", str(self.cookie_file_path)]) - elif self.browser_cookies_option: - cmd.extend(["--cookies-from-browser", self.browser_cookies_option]) - - # Add proxy settings if available - if self.proxy_url: - cmd.extend(["--proxy", self.proxy_url]) - - if self.geo_proxy_url: - cmd.extend(["--geo-verification-proxy", self.geo_proxy_url]) - - logger.debug(f"Executing yt-dlp command: {cmd}") - - # Execute command with hidden console window on Windows - # Extra logic moved to src\utils\ytsage_constants.py - result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS) - - if result.returncode != 0: - logger.error(f"yt-dlp failed: {result.stderr}") - self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr)) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()] - - if not json_lines: - logger.error("No data returned from yt-dlp") - self.signals.update_status.emit(_("errors.no_data_returned")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - try: - first_info = json.loads(json_lines[0]) - except json.JSONDecodeError as e: - logger.error(f"Failed to parse yt-dlp output: {e}") - self.signals.update_status.emit(_("errors.parse_failed", error=str(e))) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_processing_data")) - - if first_info.get("_type") == "playlist": - # Handle playlist - self.is_playlist = True - self.playlist_info = first_info - self.selected_playlist_items = None - self.playlist_entries = first_info.get("entries", []) - - if not self.playlist_entries: - logger.error("Playlist contains no valid videos.") - self.signals.update_status.emit(_("errors.playlist_no_videos")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - # Even with flat-playlist, we need one video's formats to populate the table. - # Just use the first video URL to get full details quickly. - self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video")) - first_video_entry = self.playlist_entries[0] - first_video_url = first_video_entry.get("url") - - # Fetch full info for just the first video to get formats - cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url] - # Add cookies & proxy to this single request too - if self.cookie_file_path: - cmd_single.extend(["--cookies", str(self.cookie_file_path)]) - elif self.browser_cookies_option: - cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option]) - if self.proxy_url: - cmd_single.extend(["--proxy", self.proxy_url]) - if self.geo_proxy_url: - cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url]) - - result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) - - if result_single.returncode == 0: - self.video_info = json.loads(result_single.stdout) - else: - # Fallback to whatever minimal info we have, might fail table population - self.video_info = first_video_entry - - # Update playlist info label - playlist_text = _("playlist.display_format", - title=first_info.get('title', _('playlist.unknown')), - count=len(self.playlist_entries)) - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_info_label_text.emit(playlist_text) - self.signals.playlist_info_label_visible.emit(True) - - # Show playlist selection button - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all")) - - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_select_btn_visible.emit(True) - - else: - # Handle single video - self.is_playlist = False - self.video_info = first_info - self.playlist_entries = [] - self.selected_playlist_items = None - - # Hide playlist UI - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_info_label_visible.emit(False) - - # update signal method from QMetaObject.invokeMethod to signals - self.signals.playlist_select_btn_visible.emit(False) - - # Verify we have format information - if not self.video_info or "formats" not in self.video_info: - logger.error("No format information available") - self.signals.update_status.emit(_("errors.no_format_info")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp")) - self.all_formats = self.video_info["formats"] - - # Update UI - self.update_video_info(self.video_info) - - # Update thumbnail - self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) - # Try to get thumbnail from playlist info first - # Fallback to video thumbnail if playlist thumbnail not found or not a playlist - thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail") - - self.download_thumbnail(thumbnail_url) - - # Save thumbnail if enabled - if self.save_thumbnail: - self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue] - - # Handle subtitles - self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) - self.selected_subtitles = [] - self.available_subtitles = self.video_info.get("subtitles", {}) - self.available_automatic_subtitles = self.video_info.get("automatic_captions", {}) - - # Update subtitle UI - # update signal method from QMetaObject.invokeMethod to signals - self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected")) - - # Update format table - self.signals.update_status.emit(_("main_ui.analyzing_updating_table")) - self.video_button.setChecked(True) - self.audio_button.setChecked(False) - self.filter_formats() - - self.signals.update_status.emit(_("main_ui.analysis_complete")) - - # Mark analysis as complete and enable analysis-dependent controls - self.analysis_completed = True - QMetaObject.invokeMethod( - self, - "toggle_analysis_dependent_controls", - Qt.ConnectionType.QueuedConnection, - Q_ARG(bool, True), - ) - - except subprocess.TimeoutExpired: - logger.error("Analysis timed out. Please try again.") - self.signals.update_status.emit(_("errors.analysis_timeout")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - except json.JSONDecodeError as e: - logger.error(f"Failed to parse yt-dlp output: {e}") - self.signals.update_status.emit(f"Error: Failed to parse yt-dlp output: {e}") - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - except Exception as e: - logger.error(f"Analysis failed: {e}") - self.signals.update_status.emit(_("errors.analysis_failed", error=str(e))) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) From 64560fabb290b9a67bde21e6d9c8357d27a35014 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:25:52 +0200 Subject: [PATCH 030/134] Replace pyglet with QtMultimedia for audio playback Removed pyglet dependency and migrated notification sound playback to use PySide6.QtMultimedia's QMediaPlayer and QAudioOutput. Updated workflow and requirements files to remove pyglet and ensure PySide6.QtMultimedia is included in build steps. --- .github/workflows/build-linux.yml | 3 +-- .github/workflows/build-macos.yml | 3 +-- .github/workflows/build-windows.yml | 3 +-- requirements.txt | 1 - src/gui/ytsage_gui_main.py | 16 ++++++++++------ 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index bf7182f..845593a 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -107,11 +107,11 @@ jobs: "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", + "PySide6.QtMultimedia", "requests", "PIL", "packaging", "markdown", - "pyglet", "loguru", "setuptools", ], @@ -125,7 +125,6 @@ jobs: "PySide6.QtXml", "PySide6.QtSql", "PySide6.QtHelp", - "PySide6.QtMultimedia", "PySide6.QtQml", "PySide6.QtQuick", "PySide6.QtWebEngineCore", diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 5531531..9a578c5 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -92,11 +92,11 @@ jobs: "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", + "PySide6.QtMultimedia", "requests", "PIL", "packaging", "markdown", - "pyglet", "loguru", "setuptools", ], @@ -110,7 +110,6 @@ jobs: "PySide6.QtXml", "PySide6.QtSql", "PySide6.QtHelp", - "PySide6.QtMultimedia", "PySide6.QtQml", "PySide6.QtQuick", "PySide6.QtWebEngineCore", diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b64a4b3..e7966da 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -85,11 +85,11 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtCore",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtGui",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWidgets",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtMultimedia",' Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",' Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",' Add-Content -Path "setup_cxfreeze.py" -Value ' "markdown",' - Add-Content -Path "setup_cxfreeze.py" -Value ' "pyglet",' Add-Content -Path "setup_cxfreeze.py" -Value ' "loguru",' Add-Content -Path "setup_cxfreeze.py" -Value ' "setuptools",' Add-Content -Path "setup_cxfreeze.py" -Value " ]," @@ -103,7 +103,6 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtXml",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSql",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtHelp",' - Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtMultimedia",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQml",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQuick",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWebEngineCore",' diff --git a/requirements.txt b/requirements.txt index e6aed9d..550f74e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ requests>=2.32.5 pillow>=12.0.0 packaging>=25.0 markdown>=3.10 -pyglet>=2.1.11 loguru>=0.7.3 setuptools>=80.9.0 \ No newline at end of file diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 82d8d94..be71532 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -5,11 +5,11 @@ import webbrowser from pathlib import Path import markdown -import pyglet import requests from packaging import version -from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal +from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl from PySide6.QtGui import QIcon +from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from PySide6.QtWidgets import ( QApplication, QButtonGroup, @@ -154,6 +154,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Track if video analysis is completed self.analysis_completed = False + # Initialize audio player + self.player = QMediaPlayer() + self.audio_output = QAudioOutput() + self.player.setAudioOutput(self.audio_output) + self.init_ui() # Defer heavy start-up tasks to ensure UI renders immediately @@ -208,10 +213,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.warning(f"Notification sound file not found at: {SOUND_PATH}") return - # Play the sound using pyglet - # no need for the thread, as .play() is async - sound = pyglet.media.load(str(SOUND_PATH), streaming=False) - sound.play() + # Play the sound using QtMultimedia + self.player.setSource(QUrl.fromLocalFile(str(SOUND_PATH))) + self.player.play() logger.debug("Notification sound played") except Exception as e: logger.exception(f"Error playing notification sound: {e}") From 3ae18678e82d82fa4b1f7c169786b8f49f466b4f Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:28:25 +0200 Subject: [PATCH 031/134] Remove pyglet from dependencies list in README The pyglet library has been removed from the dependencies table in the README, indicating it is no longer used for audio playback in the project. --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index c5548cc..841153e 100644 --- a/README.md +++ b/README.md @@ -482,10 +482,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file markdown Markdown Rendering - - pyglet - Audio Playback - loguru Logging From 217c03379eed9d63c5d75ef6e91652fae9c4cd40 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:30:15 +0200 Subject: [PATCH 032/134] Add ytsage_stylesheet.py to project structure in README Updated the README to include the new ytsage_stylesheet.py file under the YTSage GUI directory, reflecting the addition of stylesheet definitions to the project. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 841153e..bf4a712 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,7 @@ YTSage/ тФВ тФЬтФАтФА ЁЯУД ytsage_gui_format_table.py # Format table functionality тФВ тФЬтФАтФА ЁЯУД ytsage_gui_main.py # Main application window тФВ тФЬтФАтФА ЁЯУД ytsage_gui_video_info.py # Video information display + | тФЬтФАтФА ЁЯУД ytsage_stylesheet.py # Stylesheet definitions тФВ тФФтФАтФА ЁЯУБ ytsage_gui_dialogs/ # Dialog classes тФВ тФЬтФАтФА ЁЯУД __init__.py # Dialogs package init тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_base.py # Basic dialogs From 02ede913d2bb3fb086394dc40c17afef6317c6e8 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:50:24 +0200 Subject: [PATCH 033/134] Refactor analysis to use QThread for thread safety Replaces the use of Python's threading with a dedicated QThread-based AnalysisThread for URL analysis, ensuring all UI updates are performed via Qt signals for thread safety. Adds cancellation support for analysis, improves error handling, and updates the main window's close event to properly terminate the analysis thread if running. This refactor enhances stability and responsiveness of the GUI during analysis operations. --- src/gui/ytsage_gui_analysis.py | 523 ++++++++++++++++++++------------- src/gui/ytsage_gui_main.py | 9 + 2 files changed, 325 insertions(+), 207 deletions(-) diff --git a/src/gui/ytsage_gui_analysis.py b/src/gui/ytsage_gui_analysis.py index 06463a7..b1bcaa2 100644 --- a/src/gui/ytsage_gui_analysis.py +++ b/src/gui/ytsage_gui_analysis.py @@ -1,9 +1,8 @@ -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast import json -import threading import subprocess -from PySide6.QtCore import QMetaObject, Qt, Q_ARG +from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal from PySide6.QtWidgets import QMessageBox from src.core.ytsage_utils import validate_video_url, parse_yt_dlp_error @@ -15,9 +14,256 @@ from src.utils.ytsage_logger import logger if TYPE_CHECKING: from src.gui.ytsage_gui_main import YTSageApp + +class AnalysisThread(QThread): + """ + Thread-safe QThread for URL analysis. + All results are passed back via signals to ensure thread safety. + """ + # Signals for status updates + status_update = Signal(str) + + # Signals for playlist UI + playlist_info_visible = Signal(bool) + playlist_info_text = Signal(str) + playlist_select_btn_visible = Signal(bool) + playlist_select_btn_text = Signal(str) + + # Signal for analysis results - passes all data at once + analysis_complete = Signal(dict) + + # Signal for errors + analysis_error = Signal(str) + + # Signal when thread finishes (success or failure) + analysis_finished = Signal() + + def __init__( + self, + url: str, + cookie_file_path: Optional[str] = None, + browser_cookies_option: Optional[str] = None, + proxy_url: Optional[str] = None, + geo_proxy_url: Optional[str] = None, + parent=None + ) -> None: + super().__init__(parent) + self.url = url + self.cookie_file_path = cookie_file_path + self.browser_cookies_option = browser_cookies_option + self.proxy_url = proxy_url + self.geo_proxy_url = geo_proxy_url + self._cancelled = False + + def cancel(self) -> None: + """Request cancellation of the analysis.""" + self._cancelled = True + + def run(self) -> None: + """Main thread execution - performs URL analysis.""" + try: + self.status_update.emit(_("main_ui.analyzing_extracting_basic")) + + url = self.url + # Clean up the URL to handle both playlist and video URLs + if "list=" in url and "watch?v=" in url: + playlist_id = url.split("list=")[1].split("&")[0] + url = f"https://www.youtube.com/playlist?list={playlist_id}" + + self._analyze_url_with_subprocess(url) + + except Exception as e: + logger.exception(f"Error in analysis: {e}") + self.analysis_error.emit(_("errors.generic_error", error=str(e))) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + finally: + self.analysis_finished.emit() + + def _add_auth_options(self, cmd: List[str]) -> None: + """Add authentication and proxy options to command.""" + if self.cookie_file_path: + cmd.extend(["--cookies", str(self.cookie_file_path)]) + elif self.browser_cookies_option: + cmd.extend(["--cookies-from-browser", self.browser_cookies_option]) + + if self.proxy_url: + cmd.extend(["--proxy", self.proxy_url]) + + if self.geo_proxy_url: + cmd.extend(["--geo-verification-proxy", self.geo_proxy_url]) + + def _analyze_url_with_subprocess(self, url: str) -> None: + """Analyze URL using yt-dlp executable.""" + if self._cancelled: + return + + yt_dlp_path = get_yt_dlp_path() + if not yt_dlp_path: + logger.error("yt-dlp executable not found. Please install yt-dlp first.") + self.analysis_error.emit(_("errors.ytdlp_not_found")) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + self.status_update.emit(_("main_ui.analyzing_extracting_ytdlp")) + + # Build command for basic info extraction + cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] + self._add_auth_options(cmd) + + logger.debug(f"Executing yt-dlp command: {cmd}") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=300, + creationflags=SUBPROCESS_CREATIONFLAGS + ) + except subprocess.TimeoutExpired: + logger.error("Analysis timed out") + self.analysis_error.emit(_("errors.timeout")) + return + + if self._cancelled: + return + + if result.returncode != 0: + logger.error(f"yt-dlp failed: {result.stderr}") + self.analysis_error.emit(_("errors.ytdlp_failed", error=result.stderr)) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()] + + if not json_lines: + logger.error("No data returned from yt-dlp") + self.analysis_error.emit(_("errors.no_data_returned")) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + try: + first_info = json.loads(json_lines[0]) + except json.JSONDecodeError as e: + logger.error(f"Failed to parse yt-dlp output: {e}") + self.analysis_error.emit(_("errors.parse_failed", error=str(e))) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + if self._cancelled: + return + + self.status_update.emit(_("main_ui.analyzing_processing_data")) + + # Prepare result data + result_data: Dict[str, Any] = { + "is_playlist": False, + "playlist_info": None, + "playlist_entries": [], + "video_info": None, + "all_formats": [], + "available_subtitles": {}, + "available_automatic_subtitles": {}, + "thumbnail_url": None, + } + + if first_info.get("_type") == "playlist": + result_data["is_playlist"] = True + result_data["playlist_info"] = first_info + playlist_entries = first_info.get("entries", []) + result_data["playlist_entries"] = playlist_entries + + if not playlist_entries: + logger.error("Playlist contains no valid videos.") + self.analysis_error.emit(_("errors.playlist_no_videos")) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + # Fetch full info for the first video to get formats + self.status_update.emit(_("main_ui.analyzing_fetching_first_video")) + first_video_entry = playlist_entries[0] + first_video_url = first_video_entry.get("url") + + cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url] + self._add_auth_options(cmd_single) + + try: + result_single = subprocess.run( + cmd_single, capture_output=True, text=True, timeout=60, + creationflags=SUBPROCESS_CREATIONFLAGS + ) + if result_single.returncode == 0: + result_data["video_info"] = json.loads(result_single.stdout) + else: + result_data["video_info"] = first_video_entry + except subprocess.TimeoutExpired: + result_data["video_info"] = first_video_entry + + if self._cancelled: + return + + # Update playlist UI via signals + playlist_text = _("playlist.display_format", + title=first_info.get('title', _('playlist.unknown')), + count=len(playlist_entries)) + self.playlist_info_text.emit(playlist_text) + self.playlist_info_visible.emit(True) + self.playlist_select_btn_text.emit(_("main_ui.select_videos_all")) + self.playlist_select_btn_visible.emit(True) + + else: + # Handle single video + result_data["is_playlist"] = False + result_data["video_info"] = first_info + result_data["playlist_entries"] = [] + result_data["playlist_info"] = None + + # Hide playlist UI + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + + # Verify we have format information + video_info = result_data["video_info"] + if not video_info or "formats" not in video_info: + logger.error("No format information available") + self.analysis_error.emit(_("errors.no_format_info")) + self.playlist_info_visible.emit(False) + self.playlist_select_btn_visible.emit(False) + return + + self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp")) + result_data["all_formats"] = video_info.get("formats", []) + + # Get thumbnail URL + self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) + playlist_info = result_data.get("playlist_info") or {} + thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail") + result_data["thumbnail_url"] = thumbnail_url + + # Handle subtitles + self.status_update.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) + result_data["available_subtitles"] = video_info.get("subtitles", {}) + result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {}) + + self.status_update.emit(_("main_ui.analyzing_updating_table")) + + # Emit all results at once + self.analysis_complete.emit(result_data) + + class AnalysisMixin: + """Mixin class providing URL analysis functionality for YTSageApp.""" + + # Track the current analysis thread + _analysis_thread: Optional[AnalysisThread] = None + def analyze_url(self) -> None: + """Start URL analysis in a background thread.""" self = cast("YTSageApp", self) + if self.is_updating_ytdlp: QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message")) return @@ -33,225 +279,88 @@ class AnalysisMixin: QMessageBox.warning(self, _("main_ui.error_title"), error_message) return + # Cancel any existing analysis thread + if self._analysis_thread is not None and self._analysis_thread.isRunning(): + self._analysis_thread.cancel() + self._analysis_thread.wait(1000) # Wait up to 1 second + # Reset analysis state and disable controls self.analysis_completed = False self.toggle_analysis_dependent_controls(enabled=False) self.signals.update_status.emit(_("main_ui.analyzing_preparing")) self.is_analyzing = True - threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() - def _analyze_url_thread(self, url) -> None: + # Create and configure the analysis thread + self._analysis_thread = AnalysisThread( + url=url, + cookie_file_path=self.cookie_file_path, + browser_cookies_option=self.browser_cookies_option, + proxy_url=self.proxy_url, + geo_proxy_url=self.geo_proxy_url, + parent=self + ) + + # Connect signals to handlers + self._analysis_thread.status_update.connect(self.signals.update_status.emit) + self._analysis_thread.playlist_info_visible.connect(self.signals.playlist_info_label_visible.emit) + self._analysis_thread.playlist_info_text.connect(self.signals.playlist_info_label_text.emit) + self._analysis_thread.playlist_select_btn_visible.connect(self.signals.playlist_select_btn_visible.emit) + self._analysis_thread.playlist_select_btn_text.connect(self.signals.playlist_select_btn_text.emit) + self._analysis_thread.analysis_complete.connect(self._on_analysis_complete) + self._analysis_thread.analysis_error.connect(self._on_analysis_error) + self._analysis_thread.analysis_finished.connect(self._on_analysis_finished) + + # Start the thread + self._analysis_thread.start() + + def _on_analysis_complete(self, result_data: Dict[str, Any]) -> None: + """Handle successful analysis completion - runs in main thread.""" self = cast("YTSageApp", self) - try: - self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic")) + + # Update instance variables with results (safe - we're in main thread) + self.is_playlist = result_data["is_playlist"] + self.playlist_info = result_data["playlist_info"] + self.playlist_entries = result_data["playlist_entries"] + self.video_info = result_data["video_info"] + self.all_formats = result_data["all_formats"] + self.available_subtitles = result_data["available_subtitles"] + self.available_automatic_subtitles = result_data["available_automatic_subtitles"] + self.selected_playlist_items = None + self.selected_subtitles = [] - # Clean up the URL to handle both playlist and video URLs - if "list=" in url and "watch?v=" in url: - playlist_id = url.split("list=")[1].split("&")[0] - url = f"https://www.youtube.com/playlist?list={playlist_id}" - - # Always use subprocess to call yt-dlp binary (Python package removed) - self._analyze_url_with_subprocess(url) - - except Exception as e: - logger.exception(f"Error in analysis: {e}") - self.signals.update_status.emit(_("errors.generic_error", error=str(e))) - # Ensure playlist UI is hidden on error too - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - finally: - self.is_analyzing = False - - - def _analyze_url_with_subprocess(self, url) -> None: - """Analyze URL using yt-dlp executable when Python module is not available""" - self = cast("YTSageApp", self) - - try: - yt_dlp_path = get_yt_dlp_path() - if not yt_dlp_path: - logger.error("yt-dlp executable not found. Please install yt-dlp first.") - self.signals.update_status.emit(_("errors.ytdlp_not_found")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp")) - - # Clean up the URL to handle both playlist and video URLs - if "list=" in url and "watch?v=" in url: - playlist_id = url.split("list=")[1].split("&")[0] - url = f"https://www.youtube.com/playlist?list={playlist_id}" - - # Build command for basic info extraction - # Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json - # This fetches minimal info for all videos quickly without downloading full details for each - cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] - - # Add cookies if available - if self.cookie_file_path: - cmd.extend(["--cookies", str(self.cookie_file_path)]) - elif self.browser_cookies_option: - cmd.extend(["--cookies-from-browser", self.browser_cookies_option]) - - # Add proxy settings if available - if self.proxy_url: - cmd.extend(["--proxy", self.proxy_url]) - - if self.geo_proxy_url: - cmd.extend(["--geo-verification-proxy", self.geo_proxy_url]) - - logger.debug(f"Executing yt-dlp command: {cmd}") - - # Execute command with hidden console window on Windows - result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS) - - if result.returncode != 0: - logger.error(f"yt-dlp failed: {result.stderr}") - self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr)) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()] - - if not json_lines: - logger.error("No data returned from yt-dlp") - self.signals.update_status.emit(_("errors.no_data_returned")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - try: - first_info = json.loads(json_lines[0]) - except json.JSONDecodeError as e: - logger.error(f"Failed to parse yt-dlp output: {e}") - self.signals.update_status.emit(_("errors.parse_failed", error=str(e))) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_processing_data")) - - if first_info.get("_type") == "playlist": - # Handle playlist - self.is_playlist = True - self.playlist_info = first_info - self.selected_playlist_items = None - self.playlist_entries = first_info.get("entries", []) - - if not self.playlist_entries: - logger.error("Playlist contains no valid videos.") - self.signals.update_status.emit(_("errors.playlist_no_videos")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - # Even with flat-playlist, we need one video's formats to populate the table. - # Just use the first video URL to get full details quickly. - self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video")) - first_video_entry = self.playlist_entries[0] - first_video_url = first_video_entry.get("url") - - # Fetch full info for just the first video to get formats - cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url] - # Add cookies & proxy to this single request too - if self.cookie_file_path: - cmd_single.extend(["--cookies", str(self.cookie_file_path)]) - elif self.browser_cookies_option: - cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option]) - if self.proxy_url: - cmd_single.extend(["--proxy", self.proxy_url]) - if self.geo_proxy_url: - cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url]) - - result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS) - - if result_single.returncode == 0: - self.video_info = json.loads(result_single.stdout) - else: - # Fallback to whatever minimal info we have, might fail table population - self.video_info = first_video_entry - - # Update playlist info label - playlist_text = _("playlist.display_format", - title=first_info.get('title', _('playlist.unknown')), - count=len(self.playlist_entries)) - - self.signals.playlist_info_label_text.emit(playlist_text) - self.signals.playlist_info_label_visible.emit(True) - - # Show playlist selection button - self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all")) - self.signals.playlist_select_btn_visible.emit(True) - - else: - # Handle single video - self.is_playlist = False - self.video_info = first_info - self.playlist_entries = [] - self.selected_playlist_items = None - - # Hide playlist UI - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - - # Verify we have format information - if not self.video_info or "formats" not in self.video_info: - logger.error("No format information available") - self.signals.update_status.emit(_("errors.no_format_info")) - self.signals.playlist_info_label_visible.emit(False) - self.signals.playlist_select_btn_visible.emit(False) - return - - self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp")) - self.all_formats = self.video_info["formats"] - - # Update UI - self.update_video_info(self.video_info) - - # Update thumbnail - self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) - # Try to get thumbnail from playlist info first - # Fallback to video thumbnail if playlist thumbnail not found or not a playlist - thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail") + # Update UI components (safe - we're in main thread) + self.update_video_info(self.video_info) + # Download thumbnail + thumbnail_url = result_data.get("thumbnail_url") + if thumbnail_url: self.download_thumbnail(thumbnail_url) - # Save thumbnail if enabled - if self.save_thumbnail: - self.download_thumbnail_file(self.video_url, self.last_path) + # Save thumbnail if enabled + if self.save_thumbnail: + self.download_thumbnail_file(self.video_url, self.last_path) - # Handle subtitles - self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) - self.selected_subtitles = [] - self.available_subtitles = self.video_info.get("subtitles", {}) - self.available_automatic_subtitles = self.video_info.get("automatic_captions", {}) + # Update subtitle UI + self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected")) - # Update subtitle UI - self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected")) + # Update format table + self.video_button.setChecked(True) + self.audio_button.setChecked(False) + self.filter_formats() - # Update format table - self.signals.update_status.emit(_("main_ui.analyzing_updating_table")) - self.video_button.setChecked(True) - self.audio_button.setChecked(False) - self.filter_formats() + self.signals.update_status.emit(_("main_ui.analysis_complete")) - self.signals.update_status.emit(_("main_ui.analysis_complete")) + # Mark analysis as complete and enable controls + self.analysis_completed = True + self.toggle_analysis_dependent_controls(enabled=True) - # Mark analysis as complete and enable analysis-dependent controls - self.analysis_completed = True - QMetaObject.invokeMethod( - self, - "toggle_analysis_dependent_controls", - Qt.ConnectionType.QueuedConnection, - Q_ARG(bool, True), - ) + def _on_analysis_error(self, error_message: str) -> None: + """Handle analysis error - runs in main thread.""" + self = cast("YTSageApp", self) + self.signals.update_status.emit(error_message) - except subprocess.TimeoutExpired: - logger.error("Analysis timed out") - self.signals.update_status.emit(_("errors.timeout")) - except Exception as e: - logger.exception(f"Unexpected error in analysis: {e}") - self.signals.update_status.emit(_("errors.generic_error", error=str(e))) + def _on_analysis_finished(self) -> None: + """Handle analysis thread completion - runs in main thread.""" + self = cast("YTSageApp", self) + self.is_analyzing = False diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index be71532..3727b32 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -950,6 +950,15 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def closeEvent(self, event) -> None: """Handle application close event to ensure proper cleanup of background threads.""" try: + # Stop the analysis thread if it's running + if hasattr(self, "_analysis_thread") and self._analysis_thread is not None and self._analysis_thread.isRunning(): + logger.info("Stopping analysis thread...") + self._analysis_thread.cancel() + if not self._analysis_thread.wait(2000): # Wait up to 2 seconds + logger.warning("Force terminating analysis thread...") + self._analysis_thread.terminate() + self._analysis_thread.wait(1000) + # Stop the auto-update thread if it's running if hasattr(self, "auto_update_thread") and self.auto_update_thread.isRunning(): logger.info("Stopping auto-update thread...") From b9b0285e954a901fc008b3f641822322a1e52b32 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:54:08 +0200 Subject: [PATCH 034/134] Refactor config handling to use ConfigManager Replaced direct usage of load_config and save_config with ConfigManager in ytsage_utils.py and ytsage_dialogs_update.py. Removed legacy config functions from ytsage_utils.py to centralize configuration management and improve maintainability. --- src/core/ytsage_utils.py | 83 ++++--------------- .../ytsage_dialogs_update.py | 11 +-- 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/src/core/ytsage_utils.py b/src/core/ytsage_utils.py index 2e6c345..6303d69 100644 --- a/src/core/ytsage_utils.py +++ b/src/core/ytsage_utils.py @@ -107,9 +107,10 @@ def update_version_cache(tool_name: str, version_info: str, path: Optional[str], def load_version_cache_from_config() -> None: """Load cached version info from config file.""" + from src.utils.ytsage_config_manager import ConfigManager + try: - config = load_config() - cached_versions = config.get("cached_versions", {}) + cached_versions = ConfigManager.get("cached_versions") or {} for tool_name, cache_data in cached_versions.items(): if tool_name in _version_cache: @@ -120,10 +121,10 @@ def load_version_cache_from_config() -> None: def save_version_cache_to_config() -> None: """Save version cache to config file.""" + from src.utils.ytsage_config_manager import ConfigManager + try: - config = load_config() - config["cached_versions"] = _version_cache.copy() - save_config(config) + ConfigManager.set("cached_versions", _version_cache.copy()) except Exception as e: logger.exception(f"Error saving version cache: {e}") @@ -323,57 +324,7 @@ def get_ffmpeg_version_direct() -> str: # get_app_data_dir() moved to src\utils\ytsage_constants.py # get_config_file_path() moved to src\utils\ytsage_constants.py # ensure_app_data_dir() moved to src\utils\ytsage_constants.py - - -def load_config() -> Dict[str, Any]: - """Load the application configuration from file.""" - default_config: Dict[str, Any] = { - "download_path": str(USER_HOME_DIR / "Downloads"), - "speed_limit_value": None, - "speed_limit_unit_index": 0, - "cookie_file_path": None, - "last_used_cookie_file": None, - "auto_update_ytdlp": True, # Enable auto-update by default - "auto_update_frequency": "daily", # daily, weekly, or startup - "last_update_check": 0, # timestamp of last check - "cached_versions": { - "ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, - "ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, - }, - } - - try: - if APP_CONFIG_FILE.exists(): - with open(APP_CONFIG_FILE, "r", encoding="utf-8") as f: - config = json.load(f) - # Merge with defaults to ensure all keys exist - for key, value in default_config.items(): - if key not in config: - config[key] = value - return config - except (json.JSONDecodeError, UnicodeError, Exception) as e: - logger.exception(f"Error reading config file: {e}") - # If config file is corrupted, create a new one with defaults - save_config(default_config) - - return default_config - - -def save_config(config: Dict[str, Any]) -> bool: - """Save the application configuration to file.""" - try: - # Convert any Path objects to strings for JSON serialization - def _convert_path(obj): - if isinstance(obj, Path): - return str(obj) - raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable") - - with open(APP_CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(config, f, ensure_ascii=False, indent=2, default=_convert_path) - return True - except Exception as e: - logger.exception(f"Error saving config: {e}") - return False +# load_config() and save_config() removed - use ConfigManager instead def check_ffmpeg() -> bool: @@ -599,15 +550,15 @@ def update_yt_dlp() -> bool: def should_check_for_auto_update() -> bool: """Check if auto-update should be performed based on user settings.""" + from src.utils.ytsage_config_manager import ConfigManager + try: - config = load_config() - # Check if auto-update is enabled - if not config.get("auto_update_ytdlp", False): + if not ConfigManager.get("auto_update_ytdlp"): return False - frequency: str = config.get("auto_update_frequency", "daily") - last_check: float = config.get("last_update_check", 0) + frequency: str = ConfigManager.get("auto_update_frequency") or "daily" + last_check: float = ConfigManager.get("last_update_check") or 0 current_time: float = time.time() # Calculate time since last check @@ -629,6 +580,8 @@ def should_check_for_auto_update() -> bool: def check_and_update_ytdlp_auto() -> bool: """Perform automatic yt-dlp update check and update if needed.""" + from src.utils.ytsage_config_manager import ConfigManager + try: logger.info("Performing automatic yt-dlp update check...") @@ -659,9 +612,7 @@ def check_and_update_ytdlp_auto() -> bool: if update_yt_dlp(): logger.info("Auto-update completed successfully!") # Update the last check timestamp - config = load_config() - config["last_update_check"] = time.time() - save_config(config) + ConfigManager.set("last_update_check", time.time()) return True else: logger.info("Auto-update failed") @@ -669,9 +620,7 @@ def check_and_update_ytdlp_auto() -> bool: else: logger.info("yt-dlp is already up to date") # Still update the timestamp even if no update was needed - config = load_config() - config["last_update_check"] = time.time() - save_config(config) + ConfigManager.set("last_update_check", time.time()) return True except requests.RequestException as e: diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py index 8937bca..5b5d605 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py @@ -14,9 +14,10 @@ from packaging import version from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout -from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config +from src.core.ytsage_utils import get_ytdlp_version from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL +from src.utils.ytsage_config_manager import ConfigManager from src.utils.ytsage_localization import LocalizationManager # Shorthand for localization @@ -407,9 +408,7 @@ class AutoUpdateThread(QThread): if success: logger.info("AutoUpdateThread: Auto-update completed successfully!") # Update the last check timestamp - config = load_config() - config["last_update_check"] = time.time() - save_config(config) + ConfigManager.set("last_update_check", time.time()) self.update_finished.emit( True, f"Successfully updated yt-dlp from {current_version} to {latest_version}", @@ -420,9 +419,7 @@ class AutoUpdateThread(QThread): else: logger.info("AutoUpdateThread: yt-dlp is already up to date") # Still update the timestamp even if no update was needed - config = load_config() - config["last_update_check"] = time.time() - save_config(config) + ConfigManager.set("last_update_check", time.time()) self.update_finished.emit( True, f"yt-dlp is already up to date (version {current_version})", From 6c9a62f6b806ccdb3ccd7f26e6b884755a6ce1f4 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:58:17 +0200 Subject: [PATCH 035/134] Improve update check with parallel network requests Refactored UpdateCheckThread to use ThreadPoolExecutor for parallel requests to PyPI and GitHub, reducing total wait time. Added error handling and timeouts for faster failure detection, and improved changelog fetching with a fallback message. --- src/gui/ytsage_gui_main.py | 86 ++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 3727b32..342c035 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -54,46 +54,86 @@ from src.utils.ytsage_localization import LocalizationManager, _ from src.utils.ytsage_history_manager import HistoryManager from src.gui.ytsage_stylesheet import StyleSheet +from concurrent.futures import ThreadPoolExecutor, as_completed + class UpdateCheckThread(QThread): + """Background thread for checking application updates with parallel network requests.""" + update_available = Signal(str, str, str) # version, url, changelog + # Reduced timeouts for faster failure detection + PYPI_TIMEOUT = 8 + GITHUB_TIMEOUT = 5 + def __init__(self, current_version): super().__init__() self.current_version = current_version - def run(self): + def _fetch_pypi_version(self) -> tuple[str | None, str | None]: + """Fetch latest version from PyPI. Returns (version, error).""" try: - # Get the latest version info from PyPI (no rate limiting unlike GitHub API) response = requests.get( "https://pypi.org/pypi/ytsage/json", - timeout=10, + timeout=self.PYPI_TIMEOUT, ) response.raise_for_status() - pypi_data = response.json() - latest_version = pypi_data["info"]["version"] + return pypi_data["info"]["version"], None + except requests.Timeout: + return None, "PyPI request timed out" + except requests.RequestException as e: + return None, f"PyPI request failed: {e}" + except Exception as e: + return None, f"Error parsing PyPI response: {e}" - # Compare versions - if version.parse(latest_version) > version.parse(self.current_version): - release_url = "https://github.com/oop7/YTSage/releases/latest" + def _fetch_github_changelog(self) -> str: + """Fetch changelog from GitHub. Returns changelog text or fallback message.""" + fallback = "View the full changelog on the [GitHub Releases](https://github.com/oop7/YTSage/releases) page." + try: + response = requests.get( + "https://api.github.com/repos/oop7/YTSage/releases/latest", + headers={"Accept": "application/vnd.github.v3+json"}, + timeout=self.GITHUB_TIMEOUT, + ) + if response.status_code == 200: + gh_data = response.json() + return gh_data.get("body", fallback) or fallback + return fallback + except Exception: + # Silently fallback if GitHub API fails (rate limiting, network issues, etc.) + return fallback + + def run(self): + """Check for updates using parallel network requests for better performance.""" + try: + # Use ThreadPoolExecutor to make both requests in parallel + # This reduces total wait time from potentially 15s to ~8s max + with ThreadPoolExecutor(max_workers=2) as executor: + # Submit both tasks + pypi_future = executor.submit(self._fetch_pypi_version) + github_future = executor.submit(self._fetch_github_changelog) + + # Get PyPI result (this is required) + latest_version, error = pypi_future.result() - # Try to fetch changelog from GitHub (with fallback if rate-limited) - changelog = "View the full changelog on the [GitHub Releases](https://github.com/oop7/YTSage/releases) page." - try: - gh_response = requests.get( - "https://api.github.com/repos/oop7/YTSage/releases/latest", - headers={"Accept": "application/vnd.github.v3+json"}, - timeout=5, - ) - if gh_response.status_code == 200: - gh_data = gh_response.json() - changelog = gh_data.get("body", changelog) - except Exception: - # Silently fallback to static message if GitHub API fails - pass + if error: + logger.debug(f"Update check failed: {error}") + return - self.update_available.emit(latest_version, release_url, changelog) + if not latest_version: + logger.debug("No version returned from PyPI") + return + + # Compare versions + if version.parse(latest_version) > version.parse(self.current_version): + release_url = "https://github.com/oop7/YTSage/releases/latest" + + # Get GitHub changelog (may already be complete due to parallel execution) + changelog = github_future.result() + + self.update_available.emit(latest_version, release_url, changelog) + except Exception as e: logger.debug(f"Failed to check for updates: {e}") From 66870d062b24a189a733506c6905469ad5148cfc Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:01:45 +0200 Subject: [PATCH 036/134] Centralize file extension constants in ytsage_constants Moved video, audio, and subtitle file extension lists to ytsage_constants.py as centralized frozenset constants. Updated all usages in downloader and GUI modules to reference these constants, reducing duplication and improving maintainability. --- src/core/ytsage_downloader.py | 32 +++++++++++++++++--------------- src/gui/ytsage_gui_main.py | 21 ++++++++++++++------- src/utils/ytsage_constants.py | 24 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/core/ytsage_downloader.py b/src/core/ytsage_downloader.py index 48b99b6..01dbd28 100644 --- a/src/core/ytsage_downloader.py +++ b/src/core/ytsage_downloader.py @@ -12,7 +12,13 @@ from typing import Optional, List, Set from PySide6.QtCore import QObject, QThread, Signal from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS +from src.utils.ytsage_constants import ( + SUBPROCESS_CREATIONFLAGS, + VIDEO_EXTENSIONS, + AUDIO_EXTENSIONS, + SUBTITLE_EXTENSIONS, + MEDIA_EXTENSIONS, +) from src.utils.ytsage_localization import LocalizationManager from src.utils.ytsage_logger import logger @@ -444,10 +450,6 @@ class DownloadThread(QThread): final_file_found = False try: - # Define video/audio extensions - video_audio_extensions = {'.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv', - '.m4a', '.mp3', '.opus', '.flac', '.aac', '.wav', '.ogg'} - # First, check if last_file_path exists and is valid if self.last_file_path: last_path = Path(self.last_file_path) @@ -463,7 +465,7 @@ class DownloadThread(QThread): potential_files = [] # Search in download directory and subdirectories (for playlists) - for ext in video_audio_extensions: + for ext in MEDIA_EXTENSIONS: potential_files.extend(self.path.glob(f'*{ext}')) # Also check subdirectories (for playlist downloads) potential_files.extend(self.path.glob(f'*/*{ext}')) @@ -563,13 +565,13 @@ class DownloadThread(QThread): if is_audio_download or "Downloading audio" in line: self.status_signal.emit(_("download.downloading_audio")) # Video file extensions with likely video content - elif ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: + elif ext in VIDEO_EXTENSIONS: self.status_signal.emit(_("download.downloading_video")) # Audio file extensions - elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: + elif ext in AUDIO_EXTENSIONS: self.status_signal.emit(_("download.downloading_audio")) # Subtitle file extensions - elif ext in [".vtt", ".srt", ".ass", ".ssa"]: + elif ext in SUBTITLE_EXTENSIONS: self.status_signal.emit(_("download.downloading_subtitle")) # Default case else: @@ -704,11 +706,11 @@ class DownloadThread(QThread): # Determine file type based on extension for existing file message ext = Path(filename).suffix.lower() - if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: + if ext in VIDEO_EXTENSIONS: self.status_signal.emit(f"тЪая╕П Video file already exists") - elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: + elif ext in AUDIO_EXTENSIONS: self.status_signal.emit(f"тЪая╕П Audio file already exists") - elif ext in [".vtt", ".srt", ".ass", ".ssa"]: + elif ext in SUBTITLE_EXTENSIONS: self.status_signal.emit(f"тЪая╕П Subtitle file already exists") else: self.status_signal.emit(f"тЪая╕П File already exists") @@ -725,13 +727,13 @@ class DownloadThread(QThread): ext = Path(self.current_filename).suffix.lower() # Video file extensions - if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: + if ext in VIDEO_EXTENSIONS: self.status_signal.emit(_("download.video_completed")) # Audio file extensions - elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: + elif ext in AUDIO_EXTENSIONS: self.status_signal.emit(_("download.audio_completed")) # Subtitle file extensions - elif ext in [".vtt", ".srt", ".ass", ".ssa"]: + elif ext in SUBTITLE_EXTENSIONS: self.status_signal.emit(_("download.subtitle_completed")) # Default case else: diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 342c035..47bd6ed 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -47,7 +47,14 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__ from src.gui.ytsage_gui_format_table import FormatTableMixin from src.gui.ytsage_gui_video_info import VideoInfoMixin from src.gui.ytsage_gui_analysis import AnalysisMixin -from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS +from src.utils.ytsage_constants import ( + ICON_PATH, + SOUND_PATH, + SUBPROCESS_CREATIONFLAGS, + VIDEO_EXTENSIONS, + AUDIO_EXTENSIONS, + SUBTITLE_EXTENSIONS, +) from src.utils.ytsage_logger import logger from src.utils.ytsage_config_manager import ConfigManager from src.utils.ytsage_localization import LocalizationManager, _ @@ -704,13 +711,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): ext = filename.suffix.lower() # Video file extensions - if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: + if ext in VIDEO_EXTENSIONS: self.status_label.setText(_('download.video_completed')) # Audio file extensions - elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: + elif ext in AUDIO_EXTENSIONS: self.status_label.setText(_('download.audio_completed')) # Subtitle file extensions - elif ext in [".vtt", ".srt", ".ass", ".ssa"]: + elif ext in SUBTITLE_EXTENSIONS: self.status_label.setText(_('download.subtitle_completed')) # Default case else: @@ -1109,13 +1116,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): ext = Path(filename).suffix.lower() # Video file extensions - if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: + if ext in VIDEO_EXTENSIONS: self.status_label.setText(_("status.video_file_exists")) # Audio file extensions - elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: + elif ext in AUDIO_EXTENSIONS: self.status_label.setText(_("status.audio_file_exists")) # Subtitle file extensions - elif ext in [".vtt", ".srt", ".ass", ".ssa"]: + elif ext in SUBTITLE_EXTENSIONS: self.status_label.setText(_("status.subtitle_file_exists")) # Default case else: diff --git a/src/utils/ytsage_constants.py b/src/utils/ytsage_constants.py index d1a6a8c..def0a31 100644 --- a/src/utils/ytsage_constants.py +++ b/src/utils/ytsage_constants.py @@ -182,6 +182,30 @@ FFMPEG_ZIP_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essen FFMPEG_7Z_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.ver" FFMPEG_ZIP_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.ver" +# ============================================================================= +# File Extension Constants +# ============================================================================= +# Centralized file extension definitions to avoid duplication across modules +# Use these constants for file type detection throughout the application + +# Video file extensions (container formats that typically contain video) +VIDEO_EXTENSIONS: frozenset[str] = frozenset({ + ".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv" +}) + +# Audio file extensions (audio-only formats) +AUDIO_EXTENSIONS: frozenset[str] = frozenset({ + ".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac" +}) + +# Subtitle file extensions +SUBTITLE_EXTENSIONS: frozenset[str] = frozenset({ + ".vtt", ".srt", ".ass", ".ssa" +}) + +# Combined video and audio extensions (for file search operations) +MEDIA_EXTENSIONS: frozenset[str] = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS + if __name__ == "__main__": # If this file is run directly, print directory information; if imported, create the necessary directories for the application. # for debug, to check os specific variable which can be different based on os. From e43b1bbb2eeb1b31bd0e198b6937f2cfb5c38b88 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:21:24 +0200 Subject: [PATCH 037/134] Optimize format table filtering and rebuilding Refactors the format table logic to build the table once and use row visibility for fast filtering between video and audio formats. Adds internal flags to track table state and format types per row, improving performance and responsiveness when toggling format filters. Also streamlines table population and color-coding logic. --- src/gui/ytsage_gui_format_table.py | 204 +++++++++++++++-------------- 1 file changed, 106 insertions(+), 98 deletions(-) diff --git a/src/gui/ytsage_gui_format_table.py b/src/gui/ytsage_gui_format_table.py index 48c2c6a..bf03cd3 100644 --- a/src/gui/ytsage_gui_format_table.py +++ b/src/gui/ytsage_gui_format_table.py @@ -158,6 +158,8 @@ class FormatTableMixin: # Store format checkboxes and formats self.format_checkboxes = [] self.all_formats = [] + self._row_format_type = [] # Track format type per row: 'video' or 'audio' + self._table_built = False # Track if table has been built with current formats # Set table size policies self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) @@ -173,29 +175,42 @@ class FormatTableMixin: def filter_formats(self) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. - if not hasattr(self, "all_formats"): + if not hasattr(self, "all_formats") or not self.all_formats: return + # Check if we need to rebuild the table (first time or formats changed) + if not self._table_built: + self._build_full_format_table() + return + + # Use row visibility for fast filtering instead of rebuilding table + show_video = hasattr(self, "video_button") and self.video_button.isChecked() # type: ignore[reportAttributeAccessIssue] + show_audio = hasattr(self, "audio_button") and self.audio_button.isChecked() # type: ignore[reportAttributeAccessIssue] + + for row, format_type in enumerate(self._row_format_type): + if format_type == "video": + self.format_table.setRowHidden(row, not show_video) + else: # audio + self.format_table.setRowHidden(row, not show_audio) + + def _build_full_format_table(self) -> None: + """Build the complete format table once with all formats.""" + self = cast("YTSageApp", self) # for autocompletion and type inference. + # Clear current table self.format_table.setRowCount(0) self.format_checkboxes.clear() + self._row_format_type.clear() - # Determine which formats to show - filtered_formats = [] - - if hasattr(self, "video_button") and self.video_button.isChecked(): # type: ignore[reportAttributeAccessIssue] - filtered_formats.extend([f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]) - - if hasattr(self, "audio_button") and self.audio_button.isChecked(): # type: ignore[reportAttributeAccessIssue] - filtered_formats.extend( - [ - f - for f in self.all_formats - if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower()) - and f.get("acodec") != "none" - and f.get("filesize") is not None - ] - ) + # Separate and filter formats + video_formats = [f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None] + audio_formats = [ + f + for f in self.all_formats + if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower()) + and f.get("acodec") != "none" + and f.get("filesize") is not None + ] # Sort formats by quality def get_quality(f): @@ -211,17 +226,23 @@ class FormatTableMixin: else: return f.get("abr", 0) - filtered_formats.sort(key=get_quality, reverse=True) + video_formats.sort(key=get_quality, reverse=True) + audio_formats.sort(key=get_quality, reverse=True) - # Update table with filtered formats - self.format_signals.format_update.emit(filtered_formats) + # Combine: video first, then audio (maintains logical grouping) + all_filtered = [(f, "video") for f in video_formats] + [(f, "audio") for f in audio_formats] - def _update_format_table(self, formats) -> None: + # Build table with format type tracking + self._populate_format_table(all_filtered) + self._table_built = True + + # Apply initial visibility based on current button states + self.filter_formats() + + def _populate_format_table(self, formats_with_types: list) -> None: + """Populate the format table with formats and their types.""" self = cast("YTSageApp", self) # for autocompletion and type inference. - self.format_table.setRowCount(0) - self.format_checkboxes.clear() - is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue] # Configure columns based on mode @@ -252,119 +273,103 @@ class FormatTableMixin: _("formats.hdr"), ] self.format_table.setHorizontalHeaderLabels(header_labels) - - # Ensure all columns are visible - for i in range(2, 9): - self.format_table.setColumnHidden(i, False) - - # Apply responsive column widths for normal mode self._apply_column_widths(header_labels, is_playlist_mode=False) - - for f in formats: + for f, format_type in formats_with_types: row = self.format_table.rowCount() self.format_table.insertRow(row) + self._row_format_type.append(format_type) - # Column 0: Select Checkbox (Always shown) + # Create checkbox widget checkbox = QCheckBox() - checkbox.format_id = str(f.get("format_id", "")) # type: ignore[reportAttributeAccessIssue] - checkbox.is_audio_only = bool((f.get("vcodec") or "none").lower() == "none") # type: ignore[attr-defined] - checkbox.has_audio = bool(f.get("acodec") and f.get("acodec") != "none") # type: ignore[attr-defined] + checkbox.setStyleSheet("QCheckBox { margin-left: 8px; }") + checkbox.format_id = f["format_id"] + checkbox.is_audio_only = f.get("vcodec") == "none" + checkbox.has_audio = f.get("acodec") != "none" checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb)) self.format_checkboxes.append(checkbox) - checkbox_widget = QWidget() - checkbox_widget.setStyleSheet("background-color: transparent;") - checkbox_layout = QHBoxLayout(checkbox_widget) + + # Create a container widget for the checkbox + checkbox_container = QWidget() + checkbox_layout = QHBoxLayout(checkbox_container) checkbox_layout.addWidget(checkbox) checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) checkbox_layout.setContentsMargins(0, 0, 0, 0) - checkbox_layout.setSpacing(0) - self.format_table.setCellWidget(row, 0, checkbox_widget) + self.format_table.setCellWidget(row, 0, checkbox_container) - # Column 1: Quality (Always shown) - quality_text = self.get_quality_label(f) - quality_item = QTableWidgetItem(quality_text) - # Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms) - quality_lower = quality_text.lower() # Make comparison case-insensitive + # Quality label with color coding + quality_label = self.get_quality_label(f) + quality_item = QTableWidgetItem(quality_label) + # Set color based on quality (check multiple language terms) + quality_lower = quality_label.lower() if any(term.lower() in quality_lower for term in ["Best", "├Уptima", "Mejor", "Melhor", "╨Ы╤Г╤З╤И╨╡╨╡", "цЬАф╜│", "Beste", "Meilleure", "рд╕рд░реНрд╡реЛрддреНрддрдо", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "╪з┘Д╪г┘Б╪╢┘Д", "╪г┘Б╪╢┘Д", "цЬАщлШ"]): quality_item.setForeground(QColor("#00ff00")) # Green for best quality - elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "├Бudio Alto", "Audio Alto", "╨Т╤Л╤Б╨╛╨║╨╛╨╡", "щлШц╕Е", "щлШш┤ищЗП", "Hoch", "Haute", "├Йlev├й", "Audio ├йlev├й", "рдЙрдЪреНрдЪ", "рдЙрдЪреНрдЪ рдСрдбрд┐рдпреЛ", "Tinggi", "Audio tinggi", "Y├╝ksek", "Y├╝ksek ses", "Wysoka", "Wysoki", "Wysokie", "Alta", "Audio alto", "╪╣╪з┘Д┘К╪й", "╪╣╪з┘Д┘К", "╪╡┘И╪к ╪╣╪з┘Д┘К", "щлШ", "щлШщЯ│ш│к"]): + elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "├Бudio Alto", "Audio Alto", "╨Т╤Л╤Б╨╛╨║╨╛╨╡", "щлШц╕Е", "щлШш┤ищЗП", "Hoch", "Haute", "├Йlev├й", "Audio ├йlev├й", "рдЙрдЪреНрдЪ", "рдЙрдЪреНрдЪ рдСрдбрд┐рдпреЛ", "Tinggi", "Audio tinggi", "Y├╝ksek", "Y├╝ksek ses", "Wysoka", "Wysoki", "Wysokie", "Audio alto", "╪╣╪з┘Д┘К╪й", "╪╣╪з┘Д┘К", "╪╡┘И╪к ╪╣╪з┘Д┘К", "щлШ", "щлШщЯ│ш│к"]): quality_item.setForeground(QColor("#00cc00")) # Light green for high quality - elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "M├йdia", "├Бudio M├йdio", "Audio Medio", "╨б╤А╨╡╨┤╨╜╨╡╨╡", "ф╕нчнЙ", "Mittel", "Moyenne", "Audio moyen", "рдордзреНрдпрдо", "рдордзреНрдпрдо рдСрдбрд┐рдпреЛ", "Sedang", "Audio sedang", "Orta", "Orta ses", "┼Ъrednia", "┼Ъredni", "┼Ъrednie", "Media", "Audio medio", "┘Е╪к┘И╪│╪╖╪й", "┘Е╪к┘И╪│╪╖", "╪╡┘И╪к ┘Е╪к┘И╪│╪╖", "ф╕н", "ф╕нщЯ│ш│к"]): + elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "M├йdia", "├Бudio M├йdio", "Audio Medio", "╨б╤А╨╡╨┤╨╜╨╡╨╡", "ф╕нчнЙ", "Mittel", "Moyenne", "Audio moyen", "рдордзреНрдпрдо", "рдордзреНрдпрдо рдСрдбрд┐рдпреЛ", "Sedang", "Audio sedang", "Orta", "Orta ses", "┼Ъrednia", "┼Ъredni", "┼Ъrednie", "Audio medio", "┘Е╪к┘И╪│╪╖╪й", "┘Е╪к┘И╪│╪╖", "╪╡┘И╪к ┘Е╪к┘И╪│╪╖", "ф╕н", "ф╕нщЯ│ш│к"]): quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality elif any(term.lower() in quality_lower for term in ["Low", "Baja", "Bajo", "Baixa", "├Бudio Baixo", "Audio Bajo", "╨Э╨╕╨╖╨║╨╛╨╡", "ф╜Ош┤ищЗП", "Niedrig", "Niedriges Audio", "Faible", "Audio faible", "Qualit├й faible", "рдирд┐рдореНрди", "рдирд┐рдореНрди рдСрдбрд┐рдпреЛ", "рдирд┐рдореНрди рдЧреБрдгрд╡рддреНрддрд╛", "Rendah", "Audio rendah", "Kualitas rendah", "D├╝┼Я├╝k", "D├╝┼Я├╝k ses", "D├╝┼Я├╝k kalite", "Niska", "Niski", "Niskie", "Bassa", "Audio basso", "Bassa qualit├а", "┘Е┘Ж╪о┘Б╪╢╪й", "┘Е┘Ж╪о┘Б╪╢", "╪╡┘И╪к ┘Е┘Ж╪о┘Б╪╢", "╪м┘И╪п╪й ┘Е┘Ж╪о┘Б╪╢╪й", "ф╜О", "ф╜ОщЯ│ш│к", "ф╜ОхУБш│к"]): quality_item.setForeground(QColor("#ff5555")) # Red for low quality self.format_table.setItem(row, 1, quality_item) - # --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) --- - - # Column 2: Resolution (Always shown) + # Resolution resolution = f.get("resolution", "N/A") - if f.get("vcodec") == "none": - resolution = _("formats.audio_only_resolution") - self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) - # Column 3: FPS for playlist mode, Extension for normal mode if is_playlist_mode: - # Get FPS for playlist mode + # Column 2 for playlist mode: Resolution + self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) + + # Column 3: FPS (Frame Rate) fps_value = f.get("fps") - if fps_value is not None: - # Format FPS value appropriately - if fps_value >= 1: - fps_text = f"{fps_value:.0f}fps" - else: - fps_text = "N/A" # Very low fps like storyboards + if fps_value is not None and fps_value >= 1: + fps_text = f"{fps_value:.0f}fps" else: fps_text = "N/A" - fps_item = QTableWidgetItem(fps_text) - # Color code based on FPS value if fps_value and fps_value >= 60: - fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps + fps_item.setForeground(QColor("#00ff00")) elif fps_value and fps_value >= 30: - fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps + fps_item.setForeground(QColor("#ffaa00")) elif fps_value and fps_value >= 1: - fps_item.setForeground(QColor("#ff5555")) # Red for low fps + fps_item.setForeground(QColor("#ff5555")) else: - fps_item.setForeground(QColor("#888888")) # Gray for N/A + fps_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 3, fps_item) - - # Column 4: HDR for playlist mode + + # Column 4: HDR if f.get("vcodec") == "none": - # Audio-only formats don't have HDR hdr_text = "N/A" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for N/A + hdr_item.setForeground(QColor("#888888")) else: hdr_value = f.get("dynamic_range") if hdr_value and hdr_value != "SDR": hdr_text = hdr_value hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR + hdr_item.setForeground(QColor("#00ffff")) else: hdr_text = "SDR" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for SDR + hdr_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 4, hdr_item) else: # Extension for normal mode (column 2) self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper())) - # Column 4 in playlist mode, Column 6 in normal mode: Audio Status - needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge + # Audio Status column + needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only")) audio_item = QTableWidgetItem(audio_status) if needs_audio: audio_item.setForeground(QColor("#ffa500")) elif audio_status == _("formats.audio_only"): - audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only - else: # Has Audio (Video+Audio) - audio_item.setForeground(QColor("#00cc00")) # Green for included audio - # Set item for correct column based on mode + audio_item.setForeground(QColor("#cccccc")) + else: + audio_item.setForeground(QColor("#00cc00")) audio_column_index = 5 if is_playlist_mode else 6 self.format_table.setItem(row, audio_column_index, audio_item) - # --- Populate columns only shown in non-playlist mode --- + # Populate columns only shown in non-playlist mode if not is_playlist_mode: # Column 3: Resolution self.format_table.setItem(row, 3, QTableWidgetItem(resolution)) @@ -384,45 +389,47 @@ class FormatTableMixin: # Column 7: FPS (Frame Rate) fps_value = f.get("fps") - if fps_value is not None: - # Format FPS value appropriately - if fps_value >= 1: - fps_text = f"{fps_value:.0f}fps" - else: - fps_text = "N/A" # Very low fps like storyboards + if fps_value is not None and fps_value >= 1: + fps_text = f"{fps_value:.0f}fps" else: fps_text = "N/A" - + fps_item = QTableWidgetItem(fps_text) - # Color code based on FPS value if fps_value and fps_value >= 60: - fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps + fps_item.setForeground(QColor("#00ff00")) elif fps_value and fps_value >= 30: - fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps + fps_item.setForeground(QColor("#ffaa00")) elif fps_value and fps_value >= 1: - fps_item.setForeground(QColor("#ff5555")) # Red for low fps + fps_item.setForeground(QColor("#ff5555")) else: - fps_item.setForeground(QColor("#888888")) # Gray for N/A + fps_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 7, fps_item) - + # Column 8: HDR (Dynamic Range) if f.get("vcodec") == "none": - # Audio-only formats don't have HDR hdr_text = "N/A" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for N/A + hdr_item.setForeground(QColor("#888888")) else: hdr_value = f.get("dynamic_range") if hdr_value and hdr_value != "SDR": hdr_text = hdr_value hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR + hdr_item.setForeground(QColor("#00ffff")) else: hdr_text = "SDR" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for SDR + hdr_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 8, hdr_item) + def _update_format_table(self, formats) -> None: + """Signal handler that triggers a full table rebuild when formats change.""" + self = cast("YTSageApp", self) # for autocompletion and type inference. + + # Mark table as needing rebuild and trigger it + self._table_built = False + self._build_full_format_table() + def handle_checkbox_click(self, clicked_checkbox) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. @@ -446,6 +453,7 @@ class FormatTableMixin: self = cast("YTSageApp", self) # for autocompletion and type inference. self.all_formats = formats + self._table_built = False # Reset flag to trigger rebuild with new formats self.format_signals.format_update.emit(formats) def get_quality_label(self, format_info) -> str: From 1dba5b629ad45a0471223db17993953e6f87f2ea Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:32:02 +0200 Subject: [PATCH 038/134] Increase progress bar precision to 0.01% Changed the progress bar range from 0-100 to 0-10000 for finer granularity (0.01% steps). Updated all progress bar value assignments and scaling logic to match the new range, and set the display format to show percentage. --- src/gui/ytsage_gui_main.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 47bd6ed..c47d159 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -460,6 +460,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Progress section with improved styling progress_layout = QVBoxLayout() self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 10000) # Use 0-10000 range for 0.01% precision + self.progress_bar.setFormat("%p%") # Display as percentage self.progress_bar.setStyleSheet(StyleSheet.PROGRESS_BAR) progress_layout.addWidget(self.progress_bar) @@ -603,7 +605,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Show preparation message self.status_label.setText(_('download.preparing')) - self.progress_bar.setValue(0) + self.progress_bar.setValue(0) # Reset progress (range is 0-10000) self.open_folder_btn.setVisible(False) # Hide the open folder button on new download # Get resolution for filename @@ -703,7 +705,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.toggle_download_controls(True) self.pause_btn.setVisible(False) self.cancel_btn.setVisible(False) - self.progress_bar.setValue(100) + self.progress_bar.setValue(10000) # 100% in 0-10000 range # Set completion message based on the file type of last downloaded file if self.download_thread and self.download_thread.current_filename: @@ -824,9 +826,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def update_progress_bar(self, value) -> None: try: - # Ensure the value is an integer - int_value = int(value) - self.progress_bar.setValue(int_value) + # Scale float percentage (0-100) to progress bar range (0-10000) for precision + scaled_value = int(float(value) * 100) + self.progress_bar.setValue(scaled_value) except Exception as e: logger.exception(f"Progress bar update error: {e}") @@ -1110,7 +1112,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.toggle_download_controls(True) self.pause_btn.setVisible(False) self.cancel_btn.setVisible(False) - self.progress_bar.setValue(100) + self.progress_bar.setValue(10000) # 100% in 0-10000 range # Determine file type based on extension ext = Path(filename).suffix.lower() @@ -1212,7 +1214,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Clear progress/status when controls are re-enabled if enabled: - self.progress_bar.setValue(0) + self.progress_bar.setValue(0) # Reset progress (range is 0-10000) self.status_label.setText(_("status.ready")) self.download_details_label.setText("") # Clear details label From 0c03e3b4ba65f75bc7093d20e4f67ed15bc66695 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:34:12 +0200 Subject: [PATCH 039/134] Add smooth animation to progress bar updates Introduces a QPropertyAnimation for the progress bar to provide a smooth transition effect when updating its value. The animation is triggered for significant changes in progress, improving the user experience with more visually appealing feedback. --- src/gui/ytsage_gui_main.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index c47d159..99b2677 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -7,7 +7,7 @@ from pathlib import Path import markdown import requests from packaging import version -from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl +from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl, QPropertyAnimation, QEasingCurve from PySide6.QtGui import QIcon from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from PySide6.QtWidgets import ( @@ -464,6 +464,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.progress_bar.setFormat("%p%") # Display as percentage self.progress_bar.setStyleSheet(StyleSheet.PROGRESS_BAR) progress_layout.addWidget(self.progress_bar) + + # Setup smooth animation for progress bar + self._progress_animation = QPropertyAnimation(self.progress_bar, b"value") + self._progress_animation.setDuration(150) # 150ms smooth transition + self._progress_animation.setEasingCurve(QEasingCurve.Type.OutCubic) # Add download details label with improved styling self.download_details_label = QLabel() @@ -828,7 +833,19 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): try: # Scale float percentage (0-100) to progress bar range (0-10000) for precision scaled_value = int(float(value) * 100) - self.progress_bar.setValue(scaled_value) + + # Use smooth animation for progress updates + if self._progress_animation.state() == QPropertyAnimation.State.Running: + self._progress_animation.stop() + + current_value = self.progress_bar.value() + # Only animate if there's a meaningful change (avoid micro-animations) + if abs(scaled_value - current_value) > 10: # More than 0.1% change + self._progress_animation.setStartValue(current_value) + self._progress_animation.setEndValue(scaled_value) + self._progress_animation.start() + else: + self.progress_bar.setValue(scaled_value) except Exception as e: logger.exception(f"Progress bar update error: {e}") From f0b05d74f49e7c1e5229ed2df751822debf13b10 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:48:36 +0200 Subject: [PATCH 040/134] Add localized error messages for yt-dlp download failures Introduces new error message keys for download failures with specific return codes and direct command errors in all supported languages. Updates ytsage_downloader.py to use these localized messages and adds the corresponding entries to the localization manager. --- languages/ar.json | 5 ++++- languages/de.json | 5 ++++- languages/en.json | 5 ++++- languages/es.json | 5 ++++- languages/fr.json | 5 ++++- languages/hi.json | 5 ++++- languages/id.json | 5 ++++- languages/it.json | 5 ++++- languages/ja.json | 5 ++++- languages/pl.json | 5 ++++- languages/pt.json | 5 ++++- languages/ru.json | 5 ++++- languages/tr.json | 5 ++++- languages/zh.json | 5 ++++- src/core/ytsage_downloader.py | 8 +++++--- src/utils/ytsage_localization.py | 5 +++++ 16 files changed, 66 insertions(+), 17 deletions(-) diff --git a/languages/ar.json b/languages/ar.json index 3cd4a0e..526b6bb 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -409,7 +409,10 @@ "ytdlp_failed": "╪о╪╖╪г: ┘Б╪┤┘Д yt-dlp: {error}", "parse_failed": "╪о╪╖╪г: ┘Б╪┤┘Д ╪к╪н┘Д┘К┘Д ┘Е╪о╪▒╪м╪з╪к yt-dlp: {error}", "analysis_failed": "╪о╪╖╪г: ┘Б╪┤┘Д ╪з┘Д╪к╪н┘Д┘К┘Д: {error}", - "generic_error": "╪о╪╖╪г: {error}" + "generic_error": "╪о╪╖╪г: {error}", + "download_failed_return_code_conflict": "┘Б╪┤┘Д ╪з┘Д╪к┘Ж╪▓┘К┘Д ╪и╪▒┘Е╪▓ ╪з┘Д╪е╪▒╪м╪з╪╣ {return_code}. ┘В╪п ┘К┘Г┘И┘Ж ╪░┘Д┘Г ╪и╪│╪и╪и ╪к╪╣╪з╪▒╪╢ ╪и┘К┘Ж ╪╣╪п╪й ╪╣┘Е┘Д┘К╪з╪к ╪к╪л╪и┘К╪к ┘Д┘А yt-dlp. ╪м╪▒┘С╪и ╪е╪▓╪з┘Д╪й ╪г┘К ╪к╪л╪и┘К╪к ┘Д┘Ж╪╕╪з┘Е ╪з┘Д╪к╪┤╪║┘К┘Д (┘Е╪л┘Д snap ╪г┘И apt) ╪л┘Е ╪г╪╣╪п ╪к╪┤╪║┘К┘Д ╪з┘Д╪к╪╖╪и┘К┘В.", + "download_failed_return_code": "┘Б╪┤┘Д ╪з┘Д╪к┘Ж╪▓┘К┘Д ╪и╪▒┘Е╪▓ ╪з┘Д╪е╪▒╪м╪з╪╣ {return_code}", + "direct_command_error": "╪о╪╖╪г ┘Б┘К ╪з┘Д╪г┘Е╪▒ ╪з┘Д┘Е╪и╪з╪┤╪▒: {error}" }, "update_dialog": { "title": "╪к╪н╪п┘К╪л ┘Е╪к╪з╪н", diff --git a/languages/de.json b/languages/de.json index 3e9d727..3e0b073 100644 --- a/languages/de.json +++ b/languages/de.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Fehler: yt-dlp fehlgeschlagen: {error}", "parse_failed": "Fehler: Fehler beim Parsen der yt-dlp-Ausgabe: {error}", "analysis_failed": "Fehler: Analyse fehlgeschlagen: {error}", - "generic_error": "Fehler: {error}" + "generic_error": "Fehler: {error}", + "download_failed_return_code_conflict": "Download fehlgeschlagen mit R├╝ckgabecode {return_code}. Dies kann an einem Konflikt mit mehreren yt-dlp-Installationen liegen. Deinstalliere ggf. eine systemweit installierte yt-dlp-Version (z. B. ├╝ber snap oder apt) und starte die Anwendung neu.", + "download_failed_return_code": "Download fehlgeschlagen mit R├╝ckgabecode {return_code}", + "direct_command_error": "Fehler im direkten Befehl: {error}" }, "update_dialog": { "title": "Update verf├╝gbar", diff --git a/languages/en.json b/languages/en.json index 8f7684b..b961214 100644 --- a/languages/en.json +++ b/languages/en.json @@ -410,7 +410,10 @@ "ytdlp_failed": "Error: yt-dlp failed: {error}", "parse_failed": "Error: Failed to parse yt-dlp output: {error}", "analysis_failed": "Error: Analysis failed: {error}", - "generic_error": "Error: {error}" + "generic_error": "Error: {error}", + "download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.", + "download_failed_return_code": "Download failed with return code {return_code}", + "direct_command_error": "Error in direct command: {error}" }, "update_dialog": { "title": "Update Available", diff --git a/languages/es.json b/languages/es.json index 465fcdd..f27c4dd 100644 --- a/languages/es.json +++ b/languages/es.json @@ -392,7 +392,10 @@ "ytdlp_failed": "Error: yt-dlp fall├│: {error}", "parse_failed": "Error: Fall├│ al analizar salida de yt-dlp: {error}", "analysis_failed": "Error: An├бlisis fall├│: {error}", - "generic_error": "Error: {error}" + "generic_error": "Error: {error}", + "download_failed_return_code_conflict": "La descarga fall├│ con el c├│digo de salida {return_code}. Esto puede deberse a un conflicto con varias instalaciones de yt-dlp. Intenta desinstalar cualquier yt-dlp instalado en el sistema (p. ej., mediante snap o apt) y reinicia la aplicaci├│n.", + "download_failed_return_code": "La descarga fall├│ con el c├│digo de salida {return_code}", + "direct_command_error": "Error en el comando directo: {error}" }, "update_dialog": { "title": "Actualizaci├│n disponible", diff --git a/languages/fr.json b/languages/fr.json index 51a2f6b..ff77e2e 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Erreur : yt-dlp a ├йchou├й : {error}", "parse_failed": "Erreur : ├Йchec de l'analyse de la sortie yt-dlp : {error}", "analysis_failed": "Erreur : ├Йchec de l'analyse : {error}", - "generic_error": "Erreur : {error}" + "generic_error": "Erreur : {error}", + "download_failed_return_code_conflict": "T├йl├йchargement ├йchou├й avec le code de retour {return_code}. Cela peut ├кtre d├╗ ├а un conflit entre plusieurs installations de yt-dlp. Essayez de d├йsinstaller toute version install├йe au niveau du syst├иme (par ex. via snap ou apt) puis red├йmarrez lтАЩapplication.", + "download_failed_return_code": "T├йl├йchargement ├йchou├й avec le code de retour {return_code}", + "direct_command_error": "Erreur dans la commande directe : {error}" }, "update_dialog": { "title": "Mise ├а jour disponible", diff --git a/languages/hi.json b/languages/hi.json index bf4360c..1bacff3 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -409,7 +409,10 @@ "ytdlp_failed": "рддреНрд░реБрдЯрд┐: yt-dlp рдЕрд╕рдлрд▓: {error}", "parse_failed": "рддреНрд░реБрдЯрд┐: yt-dlp рдЖрдЙрдЯрдкреБрдЯ рдкрд╛рд░реНрд╕ рдХрд░рдиреЗ рдореЗрдВ рд╡рд┐рдлрд▓: {error}", "analysis_failed": "рддреНрд░реБрдЯрд┐: рд╡рд┐рд╢реНрд▓реЗрд╖рдг рд╡рд┐рдлрд▓: {error}", - "generic_error": "рддреНрд░реБрдЯрд┐: {error}" + "generic_error": "рддреНрд░реБрдЯрд┐: {error}", + "download_failed_return_code_conflict": "рдбрд╛рдЙрдирд▓реЛрдб {return_code} рд░рд┐рдЯрд░реНрди рдХреЛрдб рдХреЗ рд╕рд╛рде рд╡рд┐рдлрд▓ рд╣реБрдЖред рдпрд╣ рдХрдИ yt-dlp рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рдХреЗ рдЯрдХрд░рд╛рд╡ рдХреЗ рдХрд╛рд░рдг рд╣реЛ рд╕рдХрддрд╛ рд╣реИред рдХрд┐рд╕реА рднреА рд╕рд┐рд╕реНрдЯрдо-рдЗрдВрд╕реНрдЯреЙрд▓реНрдб yt-dlp (рдЬреИрд╕реЗ snap рдпрд╛ apt) рдХреЛ рд╣рдЯрд╛рдХрд░ рдРрдк рдХреЛ рд░реАрд╕реНрдЯрд╛рд░реНрдЯ рдХрд░реЗрдВред", + "download_failed_return_code": "рдбрд╛рдЙрдирд▓реЛрдб {return_code} рд░рд┐рдЯрд░реНрди рдХреЛрдб рдХреЗ рд╕рд╛рде рд╡рд┐рдлрд▓ рд╣реБрдЖ", + "direct_command_error": "рд╕реАрдзреЗ рдХрдорд╛рдВрдб рдореЗрдВ рддреНрд░реБрдЯрд┐: {error}" }, "update_dialog": { "title": "рдЕрдкрдбреЗрдЯ рдЙрдкрд▓рдмреНрдз", diff --git a/languages/id.json b/languages/id.json index f1e60f2..46eea59 100644 --- a/languages/id.json +++ b/languages/id.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Kesalahan: yt-dlp gagal: {error}", "parse_failed": "Kesalahan: Gagal mengurai output yt-dlp: {error}", "analysis_failed": "Kesalahan: Analisis gagal: {error}", - "generic_error": "Kesalahan: {error}" + "generic_error": "Kesalahan: {error}", + "download_failed_return_code_conflict": "Unduhan gagal dengan kode pengembalian {return_code}. Ini mungkin karena konflik dengan beberapa instalasi yt-dlp. Coba uninstall yt-dlp yang terpasang di sistem (mis. melalui snap atau apt) lalu mulai ulang aplikasi.", + "download_failed_return_code": "Unduhan gagal dengan kode pengembalian {return_code}", + "direct_command_error": "Error pada perintah langsung: {error}" }, "update_dialog": { "title": "Pembaruan Tersedia", diff --git a/languages/it.json b/languages/it.json index 7cfaf98..dc5f24e 100644 --- a/languages/it.json +++ b/languages/it.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Errore: yt-dlp fallito: {error}", "parse_failed": "Errore: Impossibile analizzare l'output di yt-dlp: {error}", "analysis_failed": "Errore: Analisi fallita: {error}", - "generic_error": "Errore: {error}" + "generic_error": "Errore: {error}", + "download_failed_return_code_conflict": "Download non riuscito con codice di uscita {return_code}. Potrebbe essere dovuto a un conflitto tra pi├╣ installazioni di yt-dlp. Prova a disinstallare eventuali yt-dlp installati a livello di sistema (es. snap o apt) e riavvia lтАЩapp.", + "download_failed_return_code": "Download non riuscito con codice di uscita {return_code}", + "direct_command_error": "Errore nel comando diretto: {error}" }, "update_dialog": { "title": "Aggiornamento disponibile", diff --git a/languages/ja.json b/languages/ja.json index 4b8d4e2..784089b 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -409,7 +409,10 @@ "ytdlp_failed": "уВиуГйуГ╝: yt-dlpуБМхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", "parse_failed": "уВиуГйуГ╝: yt-dlpхЗ║хКЫуБошзгцЮРуБлхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", "analysis_failed": "уВиуГйуГ╝: шзгцЮРуБМхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", - "generic_error": "уВиуГйуГ╝: {error}" + "generic_error": "уВиуГйуГ╝: {error}", + "download_failed_return_code_conflict": "уГАуВжуГ│уГнуГ╝уГЙуБпцИ╗уВКуВ│уГ╝уГЙ {return_code} уБзхд▒цХЧуБЧуБ╛уБЧуБЯуАВшдЗцХ░уБо yt-dlp уВдуГ│уВ╣уГИуГ╝уГлуБочл╢хРИуБМхОЯхЫауБохПпшГ╜цАзуБМуБВуВКуБ╛уБЩуАВуВ╖уВ╣уГЖуГауБлуВдуГ│уВ╣уГИуГ╝уГлуБХуВМуБЯ yt-dlpя╝Иф╛Л: snap уВД aptя╝ЙуВТуВвуГ│уВдуГ│уВ╣уГИуГ╝уГлуБЧуБжуВвуГЧуГкуВТхЖНш╡╖хЛХуБЧуБжуБПуБауБХуБДуАВ", + "download_failed_return_code": "уГАуВжуГ│уГнуГ╝уГЙуБпцИ╗уВКуВ│уГ╝уГЙ {return_code} уБзхд▒цХЧуБЧуБ╛уБЧуБЯ", + "direct_command_error": "чЫ┤цОеуВ│уГЮуГ│уГЙуБоуВиуГйуГ╝: {error}" }, "update_dialog": { "title": "уВвуГГуГЧуГЗуГ╝уГИуБМхИйчФихПпшГ╜уБзуБЩ", diff --git a/languages/pl.json b/languages/pl.json index 8db7dc4..75744bf 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -409,7 +409,10 @@ "ytdlp_failed": "B┼В─Еd: yt-dlp nie powiod┼Вo si─Щ: {error}", "parse_failed": "B┼В─Еd: Nie uda┼Вo si─Щ przeanalizowa─З wyj┼Ыcia yt-dlp: {error}", "analysis_failed": "B┼В─Еd: Analiza nie powiod┼Вa si─Щ: {error}", - "generic_error": "B┼В─Еd: {error}" + "generic_error": "B┼В─Еd: {error}", + "download_failed_return_code_conflict": "Pobieranie nie powiod┼Вo si─Щ z kodem {return_code}. Mo┼╝e to wynika─З z konfliktu wielu instalacji yt-dlp. Spr├│buj odinstalowa─З systemowo zainstalowane yt-dlp (np. przez snap lub apt) i uruchom aplikacj─Щ ponownie.", + "download_failed_return_code": "Pobieranie nie powiod┼Вo si─Щ z kodem {return_code}", + "direct_command_error": "B┼В─Еd w bezpo┼Ыrednim poleceniu: {error}" }, "update_dialog": { "title": "Dost─Щpna aktualizacja", diff --git a/languages/pt.json b/languages/pt.json index 27f3152..16aa7c4 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Erro: yt-dlp falhou: {error}", "parse_failed": "Erro: Falha ao analisar a sa├нda do yt-dlp: {error}", "analysis_failed": "Erro: An├бlise falhou: {error}", - "generic_error": "Erro: {error}" + "generic_error": "Erro: {error}", + "download_failed_return_code_conflict": "O download falhou com o c├│digo de retorno {return_code}. Isso pode ser devido a um conflito com v├бrias instala├з├╡es do yt-dlp. Tente desinstalar qualquer yt-dlp instalado no sistema (ex.: snap ou apt) e reinicie o aplicativo.", + "download_failed_return_code": "O download falhou com o c├│digo de retorno {return_code}", + "direct_command_error": "Erro no comando direto: {error}" }, "update_dialog": { "title": "Atualiza├з├гo Dispon├нvel", diff --git a/languages/ru.json b/languages/ru.json index 1c7448b..83830ec 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -409,7 +409,10 @@ "ytdlp_failed": "╨Ю╤И╨╕╨▒╨║╨░: yt-dlp ╨╖╨░╨▓╨╡╤А╤И╨╕╨╗╤Б╤П ╤Б ╨╛╤И╨╕╨▒╨║╨╛╨╣: {error}", "parse_failed": "╨Ю╤И╨╕╨▒╨║╨░: ╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╤А╨░╨╖╨╛╨▒╤А╨░╤В╤М ╨▓╤Л╨▓╨╛╨┤ yt-dlp: {error}", "analysis_failed": "╨Ю╤И╨╕╨▒╨║╨░: ╨Р╨╜╨░╨╗╨╕╨╖ ╨╜╨╡ ╤Г╨┤╨░╨╗╤Б╤П: {error}", - "generic_error": "╨Ю╤И╨╕╨▒╨║╨░: {error}" + "generic_error": "╨Ю╤И╨╕╨▒╨║╨░: {error}", + "download_failed_return_code_conflict": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╖╨░╨▓╨╡╤А╤И╨╕╨╗╨░╤Б╤М ╨╛╤И╨╕╨▒╨║╨╛╨╣ ╤Б ╨║╨╛╨┤╨╛╨╝ {return_code}. ╨н╤В╨╛ ╨╝╨╛╨╢╨╡╤В ╨▒╤Л╤В╤М ╨╕╨╖-╨╖╨░ ╨║╨╛╨╜╤Д╨╗╨╕╨║╤В╨░ ╨╜╨╡╤Б╨║╨╛╨╗╤М╨║╨╕╤Е ╤Г╤Б╤В╨░╨╜╨╛╨▓╨╛╨║ yt-dlp. ╨Я╨╛╨┐╤А╨╛╨▒╤Г╨╣╤В╨╡ ╤Г╨┤╨░╨╗╨╕╤В╤М ╤Б╨╕╤Б╤В╨╡╨╝╨╜╨╛ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨╗╨╡╨╜╨╜╤Л╨╣ yt-dlp (╨╜╨░╨┐╤А╨╕╨╝╨╡╤А, ╤З╨╡╤А╨╡╨╖ snap ╨╕╨╗╨╕ apt) ╨╕ ╨┐╨╡╤А╨╡╨╖╨░╨┐╤Г╤Б╤В╨╕╤В╨╡ ╨┐╤А╨╕╨╗╨╛╨╢╨╡╨╜╨╕╨╡.", + "download_failed_return_code": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╜╨╡ ╤Г╨┤╨░╨╗╨░╤Б╤М, ╨║╨╛╨┤ {return_code}", + "direct_command_error": "╨Ю╤И╨╕╨▒╨║╨░ ╨▓ ╨┐╤А╤П╨╝╨╛╨╣ ╨║╨╛╨╝╨░╨╜╨┤╨╡: {error}" }, "update_dialog": { "title": "╨Ф╨╛╤Б╤В╤Г╨┐╨╜╨╛ ╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡", diff --git a/languages/tr.json b/languages/tr.json index 906be44..a0aa779 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -409,7 +409,10 @@ "ytdlp_failed": "Hata: yt-dlp ba┼Яar─▒s─▒z oldu: {error}", "parse_failed": "Hata: yt-dlp ├з─▒kt─▒s─▒ ayr─▒┼Яt─▒r─▒lamad─▒: {error}", "analysis_failed": "Hata: Analiz ba┼Яar─▒s─▒z oldu: {error}", - "generic_error": "Hata: {error}" + "generic_error": "Hata: {error}", + "download_failed_return_code_conflict": "─░ndirme {return_code} d├╢n├╝┼Я koduyla ba┼Яar─▒s─▒z oldu. Bunun nedeni birden fazla yt-dlp kurulumunun ├зak─▒┼Яmas─▒ olabilir. Sistem kurulumlu yt-dlp'yi (├╢rn. snap veya apt) kald─▒r─▒p uygulamay─▒ yeniden ba┼Яlatmay─▒ deneyin.", + "download_failed_return_code": "─░ndirme {return_code} d├╢n├╝┼Я koduyla ba┼Яar─▒s─▒z oldu", + "direct_command_error": "Do─Яrudan komutta hata: {error}" }, "update_dialog": { "title": "G├╝ncelleme Mevcut", diff --git a/languages/zh.json b/languages/zh.json index da2e429..812ae47 100644 --- a/languages/zh.json +++ b/languages/zh.json @@ -409,7 +409,10 @@ "ytdlp_failed": "щФЩшппя╝Ъyt-dlpхд▒ш┤ея╝Ъ{error}", "parse_failed": "щФЩшппя╝ЪшзгцЮРyt-dlpш╛УхЗ║хд▒ш┤ея╝Ъ{error}", "analysis_failed": "щФЩшппя╝ЪхИЖцЮРхд▒ш┤ея╝Ъ{error}", - "generic_error": "щФЩшппя╝Ъ{error}" + "generic_error": "щФЩшппя╝Ъ{error}", + "download_failed_return_code_conflict": "ф╕Лш╜╜хд▒ш┤ея╝Мш┐ФхЫЮчаБ {return_code}уАВш┐ЩхПпшГ╜цШпчФ▒ф║ОхнШхЬихдЪф╕к yt-dlp хоЙшгЕхп╝шЗ┤хЖ▓чкБуАВшп╖хН╕ш╜╜ч│╗ч╗Яф╕нхоЙшгЕчЪД yt-dlpя╝Иф╛ЛхжВщАЪш┐З snap цИЦ aptя╝Йя╝МчД╢хРОщЗНхРпх║ФчФиуАВ", + "download_failed_return_code": "ф╕Лш╜╜хд▒ш┤ея╝Мш┐ФхЫЮчаБ {return_code}", + "direct_command_error": "чЫ┤цОехС╜ф╗дхЗ║щФЩя╝Ъ{error}" }, "update_dialog": { "title": "цЬЙхПпчФицЫ┤цЦ░", diff --git a/src/core/ytsage_downloader.py b/src/core/ytsage_downloader.py index 01dbd28..8719b9b 100644 --- a/src/core/ytsage_downloader.py +++ b/src/core/ytsage_downloader.py @@ -511,10 +511,12 @@ class DownloadThread(QThread): # Provide more descriptive error message for possible yt-dlp conflicts if return_code == 1: self.error_signal.emit( - f"Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application." + _("errors.download_failed_return_code_conflict", return_code=return_code) ) else: - self.error_signal.emit(f"Download failed with return code {return_code}") + self.error_signal.emit( + _("errors.download_failed_return_code", return_code=return_code) + ) # Add delay before cleanup to allow file handles to be released time.sleep(1) @@ -522,7 +524,7 @@ class DownloadThread(QThread): except Exception as e: logger.exception(f"Error in direct command: {e}") - self.error_signal.emit(f"Error in direct command: {e}") + self.error_signal.emit(_("errors.direct_command_error", error=str(e))) # Add delay before cleanup to allow file handles to be released time.sleep(1) self.cleanup_partial_files() diff --git a/src/utils/ytsage_localization.py b/src/utils/ytsage_localization.py index 1116fb5..cfedccc 100644 --- a/src/utils/ytsage_localization.py +++ b/src/utils/ytsage_localization.py @@ -108,6 +108,11 @@ class LocalizationManager: }, "formats": { "show_formats": "Show formats:" + }, + "errors": { + "download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.", + "download_failed_return_code": "Download failed with return code {return_code}", + "direct_command_error": "Error in direct command: {error}" } } From 7fbef735317c22057087123a184eea713cbd8c6e Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:49:52 +0200 Subject: [PATCH 041/134] Update Arabic translations for new features Added and updated Arabic localization strings for new features and UI improvements, including FFmpeg and yt-dlp setup dialogs, proxy and cookie status messages, download settings, and error handling. This enhances support for recent application updates and improves user experience for Arabic-speaking users. --- languages/ar.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/languages/ar.json b/languages/ar.json index 526b6bb..e12daae 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -93,7 +93,8 @@ "select_subtitles": "╪з╪о╪к╪▒ ╪з┘Д╪к╪▒╪м┘Е╪з╪к", "filter_languages_placeholder": "╪к╪╡┘Б┘К╪й ╪з┘Д┘Д╪║╪з╪к (┘Е╪л╪з┘Д: ar╪М en)...", "no_subtitles_available": "┘Д╪з ╪к┘И╪м╪п ╪к╪▒╪м┘Е╪з╪к ┘Е╪к╪з╪н╪й", - "matching": "┘Е╪╖╪з╪и┘В╪й" + "matching": "┘Е╪╖╪з╪и┘В╪й", + "ytdlp_log_title": "╪│╪м┘Д yt-dlp" }, "tabs": { "cookies": "╪к╪│╪м┘К┘Д ╪з┘Д╪п╪о┘И┘Д ╪и╪з┘Д┘Г┘И┘Г┘К╪▓", @@ -124,7 +125,10 @@ "browser_selected_title": "╪к┘Е ╪к╪╖╪и┘К┘В ┘Г┘И┘Г┘К╪▓ ╪з┘Д┘Е╪к╪╡┘Б╪н", "browser_applied_message": "╪│┘К╪к┘Е ╪з╪│╪к╪о╪▒╪з╪м ┘Г┘И┘Г┘К╪▓ ╪з┘Д┘Е╪к╪╡┘Б╪н ┘Е┘Ж: {browser}", "cleared_title": "╪к┘Е ┘Е╪│╪н ╪з┘Д┘Г┘И┘Г┘К╪▓", - "cleared_message": "╪к┘Е ┘Е╪│╪н ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д┘Г┘И┘Г┘К╪▓" + "cleared_message": "╪к┘Е ┘Е╪│╪н ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д┘Г┘И┘Г┘К╪▓", + "active_browser": "тЬУ ┘Ж╪┤╪╖: ┘Г┘И┘Г┘К╪▓ ╪з┘Д┘Е╪к╪╡┘Б╪н ({browser})", + "active_file": "тЬУ ┘Ж╪┤╪╖: ┘Е┘Д┘Б ┘Г┘И┘Г┘К╪▓ ({file})", + "none_active": "тЧЛ ┘Д╪з ╪к┘И╪м╪п ┘Г┘И┘Г┘К╪▓ ┘Ж╪┤╪╖╪й" }, "custom_command": { "help_text": "╪г╪п╪о┘Д ╪г┘Е╪▒ yt-dlp ┘Е╪о╪╡╪╡ ╪г╪п┘Ж╪з┘З. ╪│┘К╪к┘Е ╪е╪╢╪з┘Б╪й ╪з┘Д╪▒╪з╪и╪╖ ╪з┘Д╪н╪з┘Д┘К ╪к┘Д┘В╪з╪ж┘К╪з┘Л.

┘Д┘Д╪н╪╡┘И┘Д ╪╣┘Д┘Й ┘В╪з╪ж┘Е╪й ┘Г╪з┘Е┘Д╪й ╪и╪з┘Д╪о┘К╪з╪▒╪з╪к ┘И╪г┘Е╪л┘Д╪й ╪з┘Д╪з╪│╪к╪о╪п╪з┘Е ╪з┘Ж┘В╪▒ ┘З┘Ж╪з ┘Д┘Е╪┤╪з┘З╪п╪й ╪з┘Д┘И╪л╪з╪ж┘В ╪з┘Д╪▒╪│┘Е┘К╪й ┘Д┘А yt-dlp.

┘Е┘Д╪з╪н╪╕╪й: ┘К╪к┘Е ╪з┘Д╪к╪╣╪з┘Е┘Д ┘Е╪╣ ┘Е╪│╪з╪▒ ╪з┘Д╪к┘Ж╪▓┘К┘Д ┘И┘Ж┘Е┘И╪░╪м ╪з╪│┘Е ╪з┘Д┘Е┘Д┘Б ╪к┘Д┘В╪з╪ж┘К╪з┘Л.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз ╪з┘Д╪г┘Е╪▒ ╪з┘Д┘Г╪з┘Е┘Д: {command}", "command_success": "тЬЕ ╪к┘Е ╪к┘Ж┘Б┘К╪░ ╪з┘Д╪г┘Е╪▒ ╪з┘Д┘Е╪о╪╡╪╡ ╪и┘Ж╪м╪з╪н!", "command_failed": "тЭМ ┘Б╪┤┘Д ╪з┘Д╪г┘Е╪▒ ╪и╪▒┘Е╪▓ ╪з┘Д╪о╪▒┘И╪м {code}", - "command_error": "тЭМ ╪о╪╖╪г ┘Б┘К ╪к┘Ж┘Б┘К╪░ ╪з┘Д╪г┘Е╪▒ ╪з┘Д┘Е╪о╪╡╪╡: {error}" + "command_error": "тЭМ ╪о╪╖╪г ┘Б┘К ╪к┘Ж┘Б┘К╪░ ╪з┘Д╪г┘Е╪▒ ╪з┘Д┘Е╪о╪╡╪╡: {error}", + "error_no_url": "тЭМ ╪о╪╖╪г: ┘Д┘Е ┘К╪к┘Е ╪к┘В╪п┘К┘Е ╪╣┘Ж┘И╪з┘Ж URL. ┘К╪▒╪м┘Й ╪е╪п╪о╪з┘Д ╪▒╪з╪и╪╖ ┘Б┘К ╪з┘Д┘Ж╪з┘Б╪░╪й ╪з┘Д╪▒╪ж┘К╪│┘К╪й.", + "error_no_command": "тЭМ ╪о╪╖╪г: ┘Д┘Е ┘К╪к┘Е ╪к┘В╪п┘К┘Е ╪г┘К ╪г┘Е╪▒. ┘К╪▒╪м┘Й ╪е╪п╪о╪з┘Д ┘Е╪╣╪з┘Е┘Д╪з╪к yt-dlp.", + "executing": "ЁЯЪА ╪м╪з╪▒┘Н ╪к┘Ж┘Б┘К╪░ ╪г┘Е╪▒ yt-dlp ┘Е╪о╪╡╪╡", + "url_label": "ЁЯУН ╪з┘Д╪▒╪з╪и╪╖: {url}", + "args_label": "тЪЩя╕П ╪з┘Д┘Е╪╣╪з┘Е┘Д╪з╪к: {command}", + "download_path_label": "ЁЯУБ ┘Е╪│╪з╪▒ ╪з┘Д╪к┘Ж╪▓┘К┘Д: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "┘В┘Е ╪и╪к┘Г┘И┘К┘Ж ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д╪и╪▒┘И┘Г╪│┘К ┘Д┘Д╪к┘Ж╪▓┘К┘Д╪з╪к. ╪з╪к╪▒┘Г┘З ┘Б╪з╪▒╪║╪з┘Л ┘Д┘Д╪з╪к╪╡╪з┘Д ╪з┘Д┘Е╪и╪з╪┤╪▒.", @@ -159,7 +170,15 @@ "invalid_main_url": "╪к┘Ж╪│┘К┘В ╪▒╪з╪и╪╖ ╪з┘Д╪и╪▒┘И┘Г╪│┘К ╪з┘Д╪▒╪ж┘К╪│┘К ╪║┘К╪▒ ╪╡╪з┘Д╪н", "invalid_geo_url": "╪к┘Ж╪│┘К┘В ╪▒╪з╪и╪╖ ╪и╪▒┘И┘Г╪│┘К ╪з┘Д┘Е┘И┘В╪╣ ╪║┘К╪▒ ╪╡╪з┘Д╪н", "main_configured": "╪к┘Е ╪к┘Г┘И┘К┘Ж ╪з┘Д╪и╪▒┘И┘Г╪│┘К ╪з┘Д╪▒╪ж┘К╪│┘К", - "geo_configured": "╪к┘Е ╪к┘Г┘И┘К┘Ж ╪и╪▒┘И┘Г╪│┘К ╪з┘Д┘Е┘И┘В╪╣" + "geo_configured": "╪к┘Е ╪к┘Г┘И┘К┘Ж ╪и╪▒┘И┘Г╪│┘К ╪з┘Д┘Е┘И┘В╪╣", + "set_title": "╪к┘Е ╪к╪╣┘К┘К┘Ж ╪з┘Д┘И┘Г┘К┘Д", + "set_message": "╪к┘Е ╪к╪╣┘К┘К┘Ж ╪з┘Д┘И┘Г┘К┘Д ╪з┘Д╪▒╪ж┘К╪│┘К ┘И╪н┘Б╪╕┘З: {proxy}", + "geo_set_title": "╪к┘Е ╪к╪╣┘К┘К┘Ж ┘И┘Г┘К┘Д ╪м╪║╪▒╪з┘Б┘К", + "geo_set_message": "╪к┘Е ╪к╪╣┘К┘К┘Ж ┘И┘Г┘К┘Д ╪з┘Д╪к╪н┘В┘В ╪з┘Д╪м╪║╪▒╪з┘Б┘К ┘И╪н┘Б╪╕┘З: {proxy}", + "cleared_title": "╪к┘Е ┘Е╪│╪н ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д┘И┘Г┘К┘Д", + "cleared_message": "╪к┘Е ┘Е╪│╪н ╪м┘Е┘К╪╣ ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д┘И┘Г┘К┘Д ┘И╪н┘Б╪╕┘З╪з.", + "saved_main": "╪к┘Е ╪н┘Б╪╕ ╪з┘Д┘И┘Г┘К┘Д ╪з┘Д╪▒╪ж┘К╪│┘К: {proxy}", + "saved_geo": "╪к┘Е ╪н┘Б╪╕ ┘И┘Г┘К┘Д ╪з┘Д┘Е┘И┘В╪╣: {proxy}" }, "download": { "preparing": "╪м╪з╪▒┘К ╪з┘Д╪к╪н╪╢┘К╪▒ ┘Д┘Д╪к┘Ж╪▓┘К┘Д...", @@ -347,7 +366,11 @@ "zero_selected": "╪к┘Е ╪з╪о╪к┘К╪з╪▒ 0", "analyze_first_tooltip": "┘К╪▒╪м┘Й ╪к╪н┘Д┘К┘Д ╪з┘Д┘Б┘К╪п┘К┘И ╪г┘И┘Д╪з┘Л", "audio_mode_disabled": "╪║┘К╪▒ ┘Е╪к╪з╪н ┘Б┘К ┘И╪╢╪╣ ╪з┘Д╪╡┘И╪к ┘Б┘В╪╖", - "select_subtitles_first": "┘К╪▒╪м┘Й ╪к╪н╪п┘К╪п ╪з┘Д╪к╪▒╪м┘Е╪й ╪г┘И┘Д╪з┘Л" + "select_subtitles_first": "┘К╪▒╪м┘Й ╪к╪н╪п┘К╪п ╪з┘Д╪к╪▒╪м┘Е╪й ╪г┘И┘Д╪з┘Л", + "settings_tooltip": "╪з┘Д┘Е╪│╪з╪▒ ╪з┘Д╪н╪з┘Д┘К: {path}\n╪н╪п ╪з┘Д╪│╪▒╪╣╪й: {speed_limit}", + "speed_limit_none": "╪и╪п┘И┘Ж", + "open_folder_error": "╪к╪╣╪░╪▒ ┘Б╪к╪н ╪з┘Д┘Е╪м┘Д╪п: {error}", + "time_range_set": "╪к┘Е ╪к╪╣┘К┘К┘Ж ╪з┘Д┘Е┘В╪╖╪╣: {section}" }, "sponsorblock": { "sponsor": "╪з┘Д╪▒╪з╪╣┘К", @@ -446,7 +469,8 @@ "next_check": "╪з┘Д┘Б╪н╪╡ ╪з┘Д╪к╪з┘Д┘К: {time}", "next_check_error": "╪з┘Д┘Б╪н╪╡ ╪з┘Д╪к╪з┘Д┘К: ╪о╪╖╪г ┘Б┘К ╪з┘Д╪н╪│╪з╪и", "checking": "ЁЯФД ╪м╪з╪▒┘К ╪з┘Д┘Б╪н╪╡...", - "check_now": "ЁЯФН ╪з┘Д╪к╪н┘В┘В ┘Е┘Ж ╪з┘Д╪к╪н╪п┘К╪л╪з╪к ╪з┘Д╪в┘Ж" + "check_now": "ЁЯФН ╪з┘Д╪к╪н┘В┘В ┘Е┘Ж ╪з┘Д╪к╪н╪п┘К╪л╪з╪к ╪з┘Д╪в┘Ж", + "current_version": "╪е╪╡╪п╪з╪▒ yt-dlp ╪з┘Д╪н╪з┘Д┘К: {version}" }, "url_validation": { "empty_url": "┘Д╪з ┘К┘Е┘Г┘Ж ╪г┘Ж ┘К┘Г┘И┘Ж ╪з┘Д╪▒╪з╪и╪╖ ┘Б╪з╪▒╪║┘Л╪з", @@ -477,6 +501,7 @@ "clear_confirm_message": "┘З┘Д ╪г┘Ж╪к ┘Е╪к╪г┘Г╪п╪Я ┘Д╪з ┘К┘Е┘Г┘Ж ╪з┘Д╪к╪▒╪з╪м╪╣ ╪╣┘Ж ┘З╪░╪з.", "no_history": "┘Д╪з ┘К┘И╪м╪п ╪│╪м┘Д ╪и╪╣╪п", "no_history_description": "╪│╪к╪╕┘З╪▒ ╪к┘Ж╪▓┘К┘Д╪з╪к┘Г ┘З┘Ж╪з", + "loading": "╪м╪з╪▒┘Н ╪к╪н┘Е┘К┘Д ╪з┘Д╪│╪м┘Д...", "search_placeholder": "╪и╪н╪л...", "open_location": "┘Б╪к╪н ╪з┘Д┘Е┘И┘В╪╣", "redownload": "╪е╪╣╪з╪п╪й ╪з┘Д╪к┘Ж╪▓┘К┘Д", @@ -495,7 +520,47 @@ "one_entry": "╪к┘Ж╪▓┘К┘Д ┘И╪з╪н╪п", "redownload_confirm_title": "╪е╪╣╪з╪п╪й ╪з┘Д╪к┘Ж╪▓┘К┘Д╪Я", "redownload_confirm_message": "╪е╪╣╪з╪п╪й ╪з┘Д╪к┘Ж╪▓┘К┘Д╪Я\n\n{title}", - "redownload_started": "╪и╪п╪г ╪з┘Д╪к┘Ж╪▓┘К┘Д" + "redownload_started": "╪и╪п╪г ╪з┘Д╪к┘Ж╪▓┘К┘Д", + "no_url_error": "┘Д┘Е ┘К╪к┘Е ╪з┘Д╪╣╪л┘И╪▒ ╪╣┘Д┘Й ╪╣┘Ж┘И╪з┘Ж URL ┘Б┘К ╪│╪м┘Д ╪з┘Д╪к╪з╪▒┘К╪о", + "redownload_failed": "┘Б╪┤┘Д ╪и╪п╪б ╪е╪╣╪з╪п╪й ╪з┘Д╪к┘Ж╪▓┘К┘Д: {error}" + }, + "ffmpeg": { + "installation_title": "╪к╪л╪и┘К╪к FFmpeg", + "installation_message": "┘К╪н╪к╪з╪м YTSage ╪е┘Д┘Й FFmpeg ┘Д┘Е╪╣╪з┘Д╪м╪й ╪з┘Д┘Б┘К╪п┘К┘И┘З╪з╪к.\n\n╪з╪о╪к╪▒ ╪о┘К╪з╪▒ ╪з┘Д╪к╪л╪и┘К╪к ╪г╪п┘Ж╪з┘З:", + "install_button": "╪к╪л╪и┘К╪к FFmpeg", + "manual_guide": "╪з┘Д╪п┘Д┘К┘Д ╪з┘Д┘К╪п┘И┘К", + "installation_failed": "┘И╪з╪м┘З ╪к╪л╪и┘К╪к FFmpeg ┘Е╪┤┘Г┘Д╪й.", + "already_installed": "╪к┘Е ╪к╪л╪и┘К╪к FFmpeg ╪и╪з┘Д┘Б╪╣┘Д!", + "installation_complete": "╪з┘Г╪к┘Е┘Д╪к ╪╣┘Е┘Д┘К╪й ╪з┘Д╪к╪л╪и┘К╪к. ┘К┘Е┘Г┘Ж┘Г ╪е╪║┘Д╪з┘В ┘З╪░╪з ╪з┘Д╪н┘И╪з╪▒ ┘И┘Е╪к╪з╪и╪╣╪й ╪з╪│╪к╪о╪п╪з┘Е YTSage.", + "installing": "╪м╪з╪▒┘Н ╪к╪л╪и┘К╪к FFmpeg... ┘К╪▒╪м┘Й ╪з┘Д╪з┘Ж╪к╪╕╪з╪▒", + "install_success": "╪к┘Е ╪к╪л╪и┘К╪к FFmpeg ╪и┘Ж╪м╪з╪н!", + "installation_complete_close": "╪з┘Г╪к┘Е┘Д╪к ╪╣┘Е┘Д┘К╪й ╪з┘Д╪к╪л╪и┘К╪к. ┘К┘Е┘Г┘Ж┘Г ╪з┘Д╪в┘Ж ╪е╪║┘Д╪з┘В ┘З╪░╪з ╪з┘Д╪н┘И╪з╪▒ ┘И┘Е╪к╪з╪и╪╣╪й ╪з╪│╪к╪о╪п╪з┘Е YTSage.", + "try_manual": "┘К╪▒╪м┘Й ┘Е╪н╪з┘И┘Д╪й ╪з╪│╪к╪о╪п╪з┘Е ╪п┘Д┘К┘Д ╪з┘Д╪к╪л╪и┘К╪к ╪з┘Д┘К╪п┘И┘К ╪и╪п┘Д╪з┘Л ┘Е┘Ж ╪░┘Д┘Г." + }, + "ytdlp_setup": { + "required_title": "╪е╪╣╪п╪з╪п yt-dlp ┘Е╪╖┘Д┘И╪и", + "description": "┘К╪к╪╖┘Д╪и YTSage ┘И╪м┘И╪п yt-dlp ┘Д╪к┘Ж╪▓┘К┘Д ╪з┘Д┘Б┘К╪п┘К┘И┘З╪з╪к.

┘Д┘Е ┘К╪к┘Е ╪з┘Д╪╣╪л┘И╪▒ ╪╣┘Д┘Й yt-dlp ┘Б┘К ╪з┘Д╪п┘Д┘К┘Д ╪з┘Д┘Е╪н┘Д┘К ┘Д┘Д╪к╪╖╪и┘К┘В. ┘К╪н╪к╪з╪м YTSage ╪е┘Д┘Й ╪е╪╣╪п╪з╪п yt-dlp ┘Д┘Ж╪╕╪з┘Е {os_name}.

┘К╪▒╪м┘Й ╪з╪о╪к┘К╪з╪▒ ╪о┘К╪з╪▒ ╪г╪п┘Ж╪з┘З:", + "option_auto": "╪к┘Ж╪▓┘К┘Д ╪к┘Д┘В╪з╪ж┘К┘Л╪з (┘Е┘И╪╡┘Й ╪и┘З)", + "option_manual": "╪з╪о╪к┘К╪з╪▒ ╪з┘Д┘Е╪│╪з╪▒ ┘К╪п┘И┘К┘Л╪з", + "setup_button": "╪е╪╣╪п╪з╪п yt-dlp", + "downloading": "╪м╪з╪▒┘Н ╪к┘Ж╪▓┘К┘Д yt-dlp...", + "success": "╪к┘Е ╪к╪л╪и┘К╪к yt-dlp ╪и┘Ж╪м╪з╪н!", + "error": "╪о╪╖╪г: {error}", + "download_failed_title": "┘Б╪┤┘Д ╪з┘Д╪к┘Ж╪▓┘К┘Д", + "download_failed_message": "┘Б╪┤┘Д ╪к┘Ж╪▓┘К┘Д yt-dlp: {error}", + "select_executable_title": "╪з╪о╪к╪▒ ┘Е┘Д┘Б yt-dlp ╪з┘Д╪к┘Ж┘Б┘К╪░┘К", + "copied_to": "╪к┘Е ┘Ж╪│╪о yt-dlp ╪и┘Ж╪м╪з╪н ╪е┘Д┘Й {path}", + "setup_error_title": "╪о╪╖╪г ┘Б┘К ╪з┘Д╪е╪╣╪п╪з╪п", + "copy_error": "╪о╪╖╪г ╪г╪л┘Ж╪з╪б ┘Ж╪│╪о yt-dlp ╪е┘Д┘Й ┘Е╪м┘Д╪п ╪з┘Д╪к╪╖╪и┘К┘В: {error}", + "invalid_executable_title": "┘Е┘Д┘Б ╪к┘Ж┘Б┘К╪░┘К ╪║┘К╪▒ ╪╡╪з┘Д╪н", + "invalid_executable_message": "╪з┘Д┘Е┘Д┘Б ╪з┘Д┘Е╪н╪п╪п ┘Д╪з ┘К╪и╪п┘И ┘Г╪к┘Ж┘Б┘К╪░ ╪╡╪н┘К╪н ┘Д┘А yt-dlp.", + "verify_error": "╪о╪╖╪г ╪г╪л┘Ж╪з╪б ╪з┘Д╪к╪н┘В┘В ┘Е┘Ж ┘Е┘Д┘Б yt-dlp ╪з┘Д╪к┘Ж┘Б┘К╪░┘К: {error}", + "setup_failed_title": "┘Б╪┤┘Д ╪з┘Д╪е╪╣╪п╪з╪п", + "setup_failed_message": "┘Б╪┤┘Д ╪е╪╣╪п╪з╪п yt-dlp. ┘В╪п ┘Д╪з ╪к╪╣┘Е┘Д ╪и╪╣╪╢ ╪з┘Д┘Е┘К╪▓╪з╪к ╪и╪┤┘Г┘Д ╪╡╪н┘К╪н.", + "success_dialog_title": "╪е╪╣╪п╪з╪п yt-dlp", + "success_dialog_message": "╪к┘Е ╪к┘Г┘И┘К┘Ж yt-dlp ╪и┘Ж╪м╪з╪н ┘Б┘К:\n{path}", + "file_filter_windows": "┘Е┘Д┘Б╪з╪к ╪к┘Ж┘Б┘К╪░┘К╪й (*.exe)", + "file_filter_all": "┘Г┘Д ╪з┘Д┘Е┘Д┘Б╪з╪к (*)" }, "ffmpeg_updater": { "title": "┘Е╪п┘В┘В ╪е╪╡╪п╪з╪▒ FFmpeg", From 180f9e77a445dada54057683db34269cff39d6be Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:49:57 +0200 Subject: [PATCH 042/134] Update and expand German translations Added and updated multiple German translation strings for features including yt-dlp log, cookie and proxy management, custom command errors, download settings, FFmpeg installation, and yt-dlp setup dialogs. This improves localization coverage and user guidance for new and existing features. --- languages/de.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/languages/de.json b/languages/de.json index 3e0b073..2c17bac 100644 --- a/languages/de.json +++ b/languages/de.json @@ -93,7 +93,8 @@ "select_subtitles": "Untertitel ausw├дhlen", "filter_languages_placeholder": "Sprachen filtern (z.B. en, de)...", "no_subtitles_available": "Keine Untertitel verf├╝gbar", - "matching": "passend" + "matching": "passend", + "ytdlp_log_title": "yt-dlp-Protokoll" }, "tabs": { "cookies": "Mit Cookies anmelden", @@ -124,7 +125,10 @@ "browser_selected_title": "Browser-Cookies angewendet", "browser_applied_message": "Browser-Cookies werden extrahiert von: {browser}", "cleared_title": "Cookies gel├╢scht", - "cleared_message": "Cookie-Einstellungen wurden gel├╢scht" + "cleared_message": "Cookie-Einstellungen wurden gel├╢scht", + "active_browser": "тЬУ Aktiv: Browser-Cookies ({browser})", + "active_file": "тЬУ Aktiv: Cookie-Datei ({file})", + "none_active": "тЧЛ Keine Cookies aktiv" }, "custom_command": { "help_text": "Geben Sie Ihren benutzerdefinierten yt-dlp-Befehl unten ein. Die aktuelle URL wird automatisch angeh├дngt.

F├╝r die vollst├дndige Liste der Optionen und Verwendungsbeispiele klicken Sie hier, um die offizielle yt-dlp-Dokumentation anzuzeigen.

Hinweis: Download-Pfad und Dateinamen-Vorlage werden automatisch behandelt.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Vollst├дndiger Befehl: {command}", "command_success": "тЬЕ Benutzerdefinierter Befehl erfolgreich ausgef├╝hrt!", "command_failed": "тЭМ Befehl fehlgeschlagen mit Exit-Code {code}", - "command_error": "тЭМ Fehler beim Ausf├╝hren des benutzerdefinierten Befehls: {error}" + "command_error": "тЭМ Fehler beim Ausf├╝hren des benutzerdefinierten Befehls: {error}", + "error_no_url": "тЭМ Fehler: Keine URL angegeben. Bitte geben Sie im Hauptfenster eine URL ein.", + "error_no_command": "тЭМ Fehler: Kein Befehl angegeben. Bitte yt-dlp-Argumente eingeben.", + "executing": "ЁЯЪА Benutzerdefinierter yt-dlp-Befehl wird ausgef├╝hrt", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argumente: {command}", + "download_path_label": "ЁЯУБ Download-Pfad: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Proxy-Einstellungen f├╝r Downloads konfigurieren. Leer lassen f├╝r direkte Verbindung.", @@ -159,7 +170,15 @@ "invalid_main_url": "Ung├╝ltiges Haupt-Proxy-URL-Format", "invalid_geo_url": "Ung├╝ltiges Geo-Proxy-URL-Format", "main_configured": "Haupt-Proxy konfiguriert", - "geo_configured": "Geo-Proxy konfiguriert" + "geo_configured": "Geo-Proxy konfiguriert", + "set_title": "Proxy gesetzt", + "set_message": "Haupt-Proxy gesetzt und gespeichert: {proxy}", + "geo_set_title": "Geo-Proxy gesetzt", + "geo_set_message": "Geo-Pr├╝f-Proxy gesetzt und gespeichert: {proxy}", + "cleared_title": "Proxy-Einstellungen gel├╢scht", + "cleared_message": "Alle Proxy-Einstellungen wurden gel├╢scht und gespeichert.", + "saved_main": "Gespeicherter Haupt-Proxy: {proxy}", + "saved_geo": "Gespeicherter Geo-Proxy: {proxy}" }, "download": { "preparing": "Download wird vorbereitet...", @@ -347,7 +366,11 @@ "zero_selected": "0 ausgew├дhlt", "analyze_first_tooltip": "Bitte analysieren Sie zuerst das Video", "audio_mode_disabled": "Nicht verf├╝gbar im Nur-Audio-Modus", - "select_subtitles_first": "Bitte w├дhlen Sie zuerst Untertitel aus" + "select_subtitles_first": "Bitte w├дhlen Sie zuerst Untertitel aus", + "settings_tooltip": "Aktueller Pfad: {path}\nGeschwindigkeitslimit: {speed_limit}", + "speed_limit_none": "Keines", + "open_folder_error": "Ordner konnte nicht ge├╢ffnet werden: {error}", + "time_range_set": "Abschnitt gesetzt: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +469,8 @@ "next_check": "N├дchste Pr├╝fung: {time}", "next_check_error": "N├дchste Pr├╝fung: Fehler bei Berechnung", "checking": "ЁЯФД Pr├╝fen...", - "check_now": "ЁЯФН Jetzt nach Updates suchen" + "check_now": "ЁЯФН Jetzt nach Updates suchen", + "current_version": "Aktuelle yt-dlp-Version: {version}" }, "url_validation": { "empty_url": "URL kann nicht leer sein", @@ -477,6 +501,7 @@ "clear_confirm_message": "Sind Sie sicher? Dies kann nicht r├╝ckg├дngig gemacht werden.", "no_history": "Noch kein Verlauf", "no_history_description": "Ihre Downloads werden hier angezeigt", + "loading": "Verlauf wird geladen...", "search_placeholder": "Suchen...", "open_location": "Speicherort ├Цffnen", "redownload": "Erneut Laden", @@ -495,7 +520,47 @@ "one_entry": "1 Download", "redownload_confirm_title": "Erneut Laden?", "redownload_confirm_message": "Erneut herunterladen?\n\n{title}", - "redownload_started": "Download gestartet" + "redownload_started": "Download gestartet", + "no_url_error": "Keine URL im Verlaufseintrag gefunden", + "redownload_failed": "Neuer Download konnte nicht gestartet werden: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpeg-Installation", + "installation_message": "YTSage ben├╢tigt FFmpeg zur Verarbeitung von Videos.\n\nW├дhle unten eine Installationsoption:", + "install_button": "FFmpeg installieren", + "manual_guide": "Manuelle Anleitung", + "installation_failed": "Bei der FFmpeg-Installation ist ein Problem aufgetreten.", + "already_installed": "FFmpeg ist bereits installiert!", + "installation_complete": "Installation abgeschlossen. Sie k├╢nnen diesen Dialog schlie├Яen und YTSage weiter nutzen.", + "installing": "FFmpeg wird installiert... Bitte warten", + "install_success": "FFmpeg wurde erfolgreich installiert!", + "installation_complete_close": "Installation abgeschlossen. Sie k├╢nnen diesen Dialog jetzt schlie├Яen und YTSage weiter nutzen.", + "try_manual": "Bitte versuchen Sie stattdessen die manuelle Installationsanleitung." + }, + "ytdlp_setup": { + "required_title": "yt-dlp-Einrichtung erforderlich", + "description": "YTSage ben├╢tigt yt-dlp zum Herunterladen von Videos.

yt-dlp wurde im lokalen App-Verzeichnis nicht gefunden. YTSage muss yt-dlp f├╝r dein {os_name}-System einrichten.

Bitte w├дhle unten eine Option:", + "option_auto": "Automatisch herunterladen (empfohlen)", + "option_manual": "Pfad manuell ausw├дhlen", + "setup_button": "yt-dlp einrichten", + "downloading": "yt-dlp wird heruntergeladen...", + "success": "yt-dlp wurde erfolgreich installiert!", + "error": "Fehler: {error}", + "download_failed_title": "Download fehlgeschlagen", + "download_failed_message": "yt-dlp konnte nicht heruntergeladen werden: {error}", + "select_executable_title": "yt-dlp-Programm ausw├дhlen", + "copied_to": "yt-dlp erfolgreich nach {path} kopiert", + "setup_error_title": "Einrichtungsfehler", + "copy_error": "Fehler beim Kopieren von yt-dlp ins App-Verzeichnis: {error}", + "invalid_executable_title": "Ung├╝ltige Datei", + "invalid_executable_message": "Die ausgew├дhlte Datei scheint kein g├╝ltiges yt-dlp-Programm zu sein.", + "verify_error": "Fehler beim Pr├╝fen der yt-dlp-Datei: {error}", + "setup_failed_title": "Einrichtung fehlgeschlagen", + "setup_failed_message": "yt-dlp konnte nicht eingerichtet werden. Einige Funktionen funktionieren m├╢glicherweise nicht korrekt.", + "success_dialog_title": "yt-dlp-Einrichtung", + "success_dialog_message": "yt-dlp wurde erfolgreich eingerichtet unter:\n{path}", + "file_filter_windows": "Ausf├╝hrbare Dateien (*.exe)", + "file_filter_all": "Alle Dateien (*)" }, "ffmpeg_updater": { "title": "FFmpeg-Versionspr├╝fer", From d2dd73d34d4b555fb2ada9566ed853f9ea127d7f Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:03 +0200 Subject: [PATCH 043/134] Add new localization strings for setup and status dialogs Introduces additional English localization keys for yt-dlp log, cookie and proxy status, custom command errors, download settings, FFmpeg installation, yt-dlp setup, and improved download history messages. These changes support new UI dialogs and error handling features. --- languages/en.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/languages/en.json b/languages/en.json index b961214..807fd1e 100644 --- a/languages/en.json +++ b/languages/en.json @@ -94,7 +94,8 @@ "filter_languages_placeholder": "Filter languages (e.g., en, es)...", "filter_playlist_placeholder": "Filter videos...", "no_subtitles_available": "No subtitles available", - "matching": "matching" + "matching": "matching", + "ytdlp_log_title": "yt-dlp Log" }, "tabs": { "cookies": "Login with Cookies", @@ -125,7 +126,10 @@ "browser_selected_title": "Browser Cookies Applied", "browser_applied_message": "Browser cookies will be extracted from: {browser}", "cleared_title": "Cookies Cleared", - "cleared_message": "Cookie settings have been cleared" + "cleared_message": "Cookie settings have been cleared", + "active_browser": "тЬУ Active: Browser cookies ({browser})", + "active_file": "тЬУ Active: Cookie file ({file})", + "none_active": "тЧЛ No cookies active" }, "custom_command": { "help_text": "Enter your custom yt-dlp command below. The current URL will be appended automatically.

For the full list of options and usage examples, click here to view the official yt-dlp documentation.

Note: Download path and filename template will be handled automatically.", @@ -138,7 +142,14 @@ "full_command": "ЁЯФз Full command: {command}", "command_success": "тЬЕ Custom command completed successfully!", "command_failed": "тЭМ Command failed with exit code {code}", - "command_error": "тЭМ Error running custom command: {error}" + "command_error": "тЭМ Error running custom command: {error}", + "error_no_url": "тЭМ Error: No URL provided. Please enter a URL in the main window.", + "error_no_command": "тЭМ Error: No command provided. Please enter yt-dlp arguments.", + "executing": "ЁЯЪА Executing custom yt-dlp command", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Arguments: {command}", + "download_path_label": "ЁЯУБ Download path: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Configure proxy settings for downloading. Leave empty to use direct connection.", @@ -160,7 +171,15 @@ "invalid_main_url": "Invalid main proxy URL format", "invalid_geo_url": "Invalid geo proxy URL format", "main_configured": "Main proxy configured", - "geo_configured": "Geo proxy configured" + "geo_configured": "Geo proxy configured", + "set_title": "Proxy Set", + "set_message": "Main proxy set and saved: {proxy}", + "geo_set_title": "Geo Proxy Set", + "geo_set_message": "Geo-verification proxy set and saved: {proxy}", + "cleared_title": "Proxy Settings Cleared", + "cleared_message": "All proxy settings have been cleared and saved.", + "saved_main": "Saved main proxy: {proxy}", + "saved_geo": "Saved geo proxy: {proxy}" }, "download": { "preparing": "Preparing download...", @@ -348,7 +367,11 @@ "zero_selected": "0 selected", "analyze_first_tooltip": "Please analyze the video first", "audio_mode_disabled": "Not available in audio-only mode", - "select_subtitles_first": "Please select subtitles first" + "select_subtitles_first": "Please select subtitles first", + "settings_tooltip": "Current Path: {path}\nSpeed Limit: {speed_limit}", + "speed_limit_none": "None", + "open_folder_error": "Could not open folder: {error}", + "time_range_set": "Section set: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -447,7 +470,8 @@ "next_check": "Next check: {time}", "next_check_error": "Next check: Error calculating", "checking": "ЁЯФД Checking...", - "check_now": "ЁЯФН Check for Updates Now" + "check_now": "ЁЯФН Check for Updates Now", + "current_version": "Current yt-dlp version: {version}" }, "url_validation": { "empty_url": "URL cannot be empty", @@ -478,6 +502,7 @@ "clear_confirm_message": "Are you sure you want to clear all download history? This cannot be undone.", "no_history": "No download history yet", "no_history_description": "Your downloaded videos and audio will appear here", + "loading": "Loading history...", "search_placeholder": "Search history...", "open_location": "Open File Location", "redownload": "Redownload", @@ -496,7 +521,47 @@ "one_entry": "1 download", "redownload_confirm_title": "Redownload Video?", "redownload_confirm_message": "Download this video again using the same settings?\n\n{title}", - "redownload_started": "Redownload started" + "redownload_started": "Redownload started", + "no_url_error": "No URL found in history entry", + "redownload_failed": "Failed to start redownload: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpeg Installation", + "installation_message": "YTSage needs FFmpeg to process videos.\n\nChoose an installation option below:", + "install_button": "Install FFmpeg", + "manual_guide": "Manual Guide", + "installation_failed": "FFmpeg installation encountered an issue.", + "already_installed": "FFmpeg is already installed!", + "installation_complete": "Installation complete. You can close this dialog and continue using YTSage.", + "installing": "Installing FFmpeg... Please wait", + "install_success": "FFmpeg has been installed successfully!", + "installation_complete_close": "Installation complete. You can now close this dialog and continue using YTSage.", + "try_manual": "Please try using the manual installation guide instead." + }, + "ytdlp_setup": { + "required_title": "yt-dlp Setup Required", + "description": "YTSage requires yt-dlp to download videos.

yt-dlp was not found in the app's local directory. YTSage needs to set up yt-dlp for your {os_name} system.

Please choose an option below:", + "option_auto": "Download automatically (Recommended)", + "option_manual": "Select path manually", + "setup_button": "Setup yt-dlp", + "downloading": "Downloading yt-dlp...", + "success": "yt-dlp was successfully installed!", + "error": "Error: {error}", + "download_failed_title": "Download Failed", + "download_failed_message": "Failed to download yt-dlp: {error}", + "select_executable_title": "Select yt-dlp executable", + "copied_to": "yt-dlp successfully copied to {path}", + "setup_error_title": "Setup Error", + "copy_error": "Error copying yt-dlp to app directory: {error}", + "invalid_executable_title": "Invalid Executable", + "invalid_executable_message": "The selected file does not appear to be a valid yt-dlp executable.", + "verify_error": "Error verifying yt-dlp executable: {error}", + "setup_failed_title": "Setup Failed", + "setup_failed_message": "Failed to set up yt-dlp. Some features may not work correctly.", + "success_dialog_title": "yt-dlp Setup", + "success_dialog_message": "yt-dlp has been successfully configured at:\n{path}", + "file_filter_windows": "Executable Files (*.exe)", + "file_filter_all": "All Files (*)" }, "ffmpeg_updater": { "title": "FFmpeg Version Checker", From 53049fc046ac06936ce7e1e16516b12bbc5e62b4 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:08 +0200 Subject: [PATCH 044/134] Add and update Spanish translations for new features Expanded the es.json file with new and updated translations for features related to yt-dlp setup, FFmpeg installation, proxy configuration, download settings, and history management. These changes support new UI elements and error messages, improving localization coverage for recent and upcoming features. --- languages/es.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/languages/es.json b/languages/es.json index f27c4dd..3482bca 100644 --- a/languages/es.json +++ b/languages/es.json @@ -60,7 +60,8 @@ "select_subtitles": "Seleccionar Subt├нtulos", "filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...", "no_subtitles_available": "No hay subt├нtulos disponibles", - "matching": "que coincidan con" + "matching": "que coincidan con", + "ytdlp_log_title": "Registro de yt-dlp" }, "tabs": { "cookies": "Iniciar sesi├│n con Cookies", @@ -91,7 +92,10 @@ "browser_selected_title": "Cookies del Navegador Aplicadas", "browser_applied_message": "Las cookies del navegador se extraer├бn de: {browser}", "cleared_title": "Cookies Borradas", - "cleared_message": "La configuraci├│n de cookies se ha borrado" + "cleared_message": "La configuraci├│n de cookies se ha borrado", + "active_browser": "тЬУ Activas: cookies del navegador ({browser})", + "active_file": "тЬУ Activo: archivo de cookies ({file})", + "none_active": "тЧЛ No hay cookies activas" }, "custom_command": { "help_text": "Ingresa tu comando yt-dlp personalizado a continuaci├│n. La URL actual se a├▒adir├б autom├бticamente.

Para ver la lista completa de opciones y ejemplos de uso, haz clic aqu├н para ver la documentaci├│n oficial de yt-dlp.

Nota: La ruta de descarga y la plantilla de nombre de archivo se manejar├бn autom├бticamente.", @@ -102,7 +106,14 @@ "full_command": "ЁЯФз Comando completo: {command}", "command_failed": "тЭМ El comando fall├│ con c├│digo de salida {code}", "command_success": "тЬЕ ┬бComando completado exitosamente!", - "command_error": "тЭМ Error ejecutando comando: {error}" + "command_error": "тЭМ Error ejecutando comando: {error}", + "error_no_url": "тЭМ Error: No se proporcion├│ URL. Ingresa una URL en la ventana principal.", + "error_no_command": "тЭМ Error: No se proporcion├│ comando. Ingresa argumentos de yt-dlp.", + "executing": "ЁЯЪА Ejecutando comando personalizado de yt-dlp", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argumentos: {command}", + "download_path_label": "ЁЯУБ Ruta de descarga: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Configurar ajustes de proxy para conexiones de red y geo-verificaci├│n.\nEl proxy puede ayudar a evitar restricciones regionales y mejorar el rendimiento de descarga.", @@ -120,7 +131,15 @@ "invalid_main_url": "Formato de URL de proxy principal inv├бlido", "invalid_geo_url": "Formato de URL de geo proxy inv├бlido", "main_configured": "Proxy principal configurado", - "geo_configured": "Geo proxy configurado" + "geo_configured": "Geo proxy configurado", + "set_title": "Proxy establecido", + "set_message": "Proxy principal establecido y guardado: {proxy}", + "geo_set_title": "Proxy geo establecido", + "geo_set_message": "Proxy de verificaci├│n geogr├бfica establecido y guardado: {proxy}", + "cleared_title": "Configuraci├│n de proxy borrada", + "cleared_message": "Se borr├│ y guard├│ toda la configuraci├│n de proxy.", + "saved_main": "Proxy principal guardado: {proxy}", + "saved_geo": "Proxy geo guardado: {proxy}" }, "download": { "preparing": "Preparando descarga...", @@ -330,7 +349,11 @@ "zero_selected": "0 seleccionados", "analyze_first_tooltip": "Por favor analiza el video primero", "audio_mode_disabled": "No disponible en modo solo audio", - "select_subtitles_first": "Por favor selecciona subt├нtulos primero" + "select_subtitles_first": "Por favor selecciona subt├нtulos primero", + "settings_tooltip": "Ruta actual: {path}\nL├нmite de velocidad: {speed_limit}", + "speed_limit_none": "Ninguno", + "open_folder_error": "No se pudo abrir la carpeta: {error}", + "time_range_set": "Secci├│n establecida: {section}" }, "sponsorblock": { "sponsor": "Patrocinador", @@ -429,7 +452,8 @@ "next_check": "Pr├│xima verificaci├│n: {time}", "next_check_error": "Pr├│xima verificaci├│n: Error al calcular", "checking": "ЁЯФД Verificando...", - "check_now": "ЁЯФН Verificar actualizaciones ahora" + "check_now": "ЁЯФН Verificar actualizaciones ahora", + "current_version": "Versi├│n actual de yt-dlp: {version}" }, "url_validation": { "empty_url": "La URL no puede estar vac├нa", @@ -460,6 +484,7 @@ "clear_confirm_message": "┬┐Est├бs seguro de que quieres borrar todo el historial? Esto no se puede deshacer.", "no_history": "A├║n no hay historial", "no_history_description": "Tus descargas aparecer├бn aqu├н", + "loading": "Cargando historial...", "search_placeholder": "Buscar...", "open_location": "Abrir Ubicaci├│n", "redownload": "Descargar de Nuevo", @@ -478,7 +503,47 @@ "one_entry": "1 descarga", "redownload_confirm_title": "┬┐Descargar de Nuevo?", "redownload_confirm_message": "┬┐Descargar nuevamente?\n\n{title}", - "redownload_started": "Descarga iniciada" + "redownload_started": "Descarga iniciada", + "no_url_error": "No se encontr├│ URL en la entrada del historial", + "redownload_failed": "No se pudo iniciar la re-descarga: {error}" + }, + "ffmpeg": { + "installation_title": "Instalaci├│n de FFmpeg", + "installation_message": "YTSage necesita FFmpeg para procesar videos.\n\nElige una opci├│n de instalaci├│n:", + "install_button": "Instalar FFmpeg", + "manual_guide": "Gu├нa manual", + "installation_failed": "La instalaci├│n de FFmpeg encontr├│ un problema.", + "already_installed": "┬бFFmpeg ya est├б instalado!", + "installation_complete": "Instalaci├│n completa. Puedes cerrar este di├бlogo y seguir usando YTSage.", + "installing": "Instalando FFmpeg... Por favor espera", + "install_success": "┬бFFmpeg se instal├│ correctamente!", + "installation_complete_close": "Instalaci├│n completa. Ahora puedes cerrar este di├бlogo y seguir usando YTSage.", + "try_manual": "Intenta usar la gu├нa de instalaci├│n manual en su lugar." + }, + "ytdlp_setup": { + "required_title": "Se requiere configurar yt-dlp", + "description": "YTSage requiere yt-dlp para descargar videos.

No se encontr├│ yt-dlp en el directorio local de la app. YTSage necesita configurar yt-dlp para tu sistema {os_name}.

Elige una opci├│n:", + "option_auto": "Descargar autom├бticamente (recomendado)", + "option_manual": "Seleccionar ruta manualmente", + "setup_button": "Configurar yt-dlp", + "downloading": "Descargando yt-dlp...", + "success": "┬бyt-dlp se instal├│ correctamente!", + "error": "Error: {error}", + "download_failed_title": "Descarga fallida", + "download_failed_message": "No se pudo descargar yt-dlp: {error}", + "select_executable_title": "Seleccionar ejecutable de yt-dlp", + "copied_to": "yt-dlp se copi├│ correctamente a {path}", + "setup_error_title": "Error de configuraci├│n", + "copy_error": "Error al copiar yt-dlp al directorio de la app: {error}", + "invalid_executable_title": "Ejecutable inv├бlido", + "invalid_executable_message": "El archivo seleccionado no parece ser un ejecutable v├бlido de yt-dlp.", + "verify_error": "Error al verificar el ejecutable de yt-dlp: {error}", + "setup_failed_title": "Configuraci├│n fallida", + "setup_failed_message": "No se pudo configurar yt-dlp. Algunas funciones pueden no funcionar correctamente.", + "success_dialog_title": "Configuraci├│n de yt-dlp", + "success_dialog_message": "yt-dlp se configur├│ correctamente en:\n{path}", + "file_filter_windows": "Archivos ejecutables (*.exe)", + "file_filter_all": "Todos los archivos (*)" }, "ffmpeg_updater": { "title": "Comprobador de Versi├│n FFmpeg", From 1102f7fd4e1d47d8518e42bb07232d1ea0b1f8af Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:13 +0200 Subject: [PATCH 045/134] Localize log window title in LogWindow dialog Replaced the hardcoded log window title with a localized string using the translation function. This improves internationalization support for the dialog. --- src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index c25481f..ee159a2 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -31,7 +31,7 @@ from src.core.ytsage_deno import check_deno_installed, get_deno_path class LogWindow(QDialog): def __init__(self, parent=None) -> None: super().__init__(parent) - self.setWindowTitle("yt-dlp Log") + self.setWindowTitle(_("dialogs.ytdlp_log_title")) self.setMinimumSize(700, 500) layout = QVBoxLayout(self) From dd6918eefc38113f490d3d2e6f16e6b34b287c84 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:17 +0200 Subject: [PATCH 046/134] Refactor dialog strings for i18n support Replaces hardcoded status and log messages in CustomOptionsDialog with calls to the translation function (_), enabling internationalization of cookie, proxy, and custom command messages. --- .../ytsage_dialogs_custom.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index e87a3eb..b254f8a 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -675,13 +675,17 @@ class CustomOptionsDialog(QDialog): def _update_cookies_active_status(self) -> None: """Update the status indicator showing if cookies are currently active""" if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option: - self.cookies_active_status.setText(f"тЬУ Active: Browser cookies ({self._parent.browser_cookies_option})") + self.cookies_active_status.setText( + _("cookies.active_browser", browser=self._parent.browser_cookies_option) + ) self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;") elif hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path: - self.cookies_active_status.setText(f"тЬУ Active: Cookie file ({self._parent.cookie_file_path.name})") + self.cookies_active_status.setText( + _("cookies.active_file", file=self._parent.cookie_file_path.name) + ) self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;") else: - self.cookies_active_status.setText("тЧЛ No cookies active") + self.cookies_active_status.setText(_("cookies.none_active")) self.cookies_active_status.setStyleSheet("color: #888888; font-style: italic;") def _initialize_proxy_settings(self) -> None: @@ -854,9 +858,9 @@ class CustomOptionsDialog(QDialog): if saved_main or saved_geo: status_parts = [] if saved_main: - status_parts.append(f"Saved main proxy: {saved_main}") + status_parts.append(_("proxy.saved_main", proxy=saved_main)) if saved_geo: - status_parts.append(f"Saved geo proxy: {saved_geo}") + status_parts.append(_("proxy.saved_geo", proxy=saved_geo)) self.proxy_status.setText(" | ".join(status_parts)) self.proxy_status.setStyleSheet("color: #888888; font-style: italic;") else: @@ -866,10 +870,10 @@ class CustomOptionsDialog(QDialog): issues = [] if main_proxy and not self.validate_proxy_url(main_proxy): - issues.append("Invalid main proxy URL format") + issues.append(_("proxy.invalid_main_url")) if geo_proxy and not self.validate_proxy_url(geo_proxy): - issues.append("Invalid geo proxy URL format") + issues.append(_("proxy.invalid_geo_url")) if issues: self.proxy_status.setText(" | ".join(issues)) @@ -877,9 +881,9 @@ class CustomOptionsDialog(QDialog): else: status_parts = [] if main_proxy: - status_parts.append("Main proxy configured") + status_parts.append(_("proxy.main_configured")) if geo_proxy: - status_parts.append("Geo proxy configured") + status_parts.append(_("proxy.geo_configured")) self.proxy_status.setText(" | ".join(status_parts)) self.proxy_status.setStyleSheet("color: #00cc00; font-style: italic;") @@ -887,24 +891,24 @@ class CustomOptionsDialog(QDialog): def run_custom_command(self) -> None: url = self._parent.url_input.text().strip() if not url: - self.log_output.append("тЭМ Error: No URL provided. Please enter a URL in the main window.") + self.log_output.append(_("custom_command.error_no_url")) return command = self.command_input.toPlainText().strip() if not command: - self.log_output.append("тЭМ Error: No command provided. Please enter yt-dlp arguments.") + self.log_output.append(_("custom_command.error_no_command")) return # Get download path from parent path = self._parent.last_path self.log_output.clear() - self.log_output.append("ЁЯЪА Executing custom yt-dlp command") - self.log_output.append(f"ЁЯУН URL: {url}") - self.log_output.append(f"тЪЩя╕П Arguments: {command}") + self.log_output.append(_("custom_command.executing")) + self.log_output.append(_("custom_command.url_label", url=url)) + self.log_output.append(_("custom_command.args_label", command=command)) if path: - self.log_output.append(f"ЁЯУБ Download path: {path}") - self.log_output.append("=" * 50) + self.log_output.append(_("custom_command.download_path_label", path=path)) + self.log_output.append(_("custom_command.separator")) self.run_btn.setEnabled(False) self.run_btn.setText(_("command.running")) From 5eda759152438fc5a38ea56c0ea95a38f59ef5c8 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:22 +0200 Subject: [PATCH 047/134] Add localization to FFmpeg installation dialog Replaced hardcoded strings in the FFmpeg installation dialog with localized strings using the `_` function. This improves internationalization support and prepares the dialog for translation. --- .../ytsage_dialogs_ffmpeg.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py index af521b1..44eb563 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py @@ -11,6 +11,7 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLa from src.core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed from src.utils.ytsage_constants import ICON_PATH +from src.utils.ytsage_localization import _ class FFmpegInstallThread(QThread): @@ -30,7 +31,7 @@ class FFmpegInstallThread(QThread): class FFmpegCheckDialog(QDialog): def __init__(self, parent=None) -> None: super().__init__(parent) - self.setWindowTitle("FFmpeg Installation") + self.setWindowTitle(_("ffmpeg.installation_title")) self.setMinimumWidth(500) self.setMinimumHeight(280) self.resize(500, 300) @@ -50,13 +51,13 @@ class FFmpegCheckDialog(QDialog): layout.setContentsMargins(20, 20, 20, 20) # Header with title and improved spacing - header_text = QLabel("FFmpeg Installation") + header_text = QLabel(_("ffmpeg.installation_title")) header_text.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;") header_text.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(header_text) # Message - self.message_label = QLabel("YTSage needs FFmpeg to process videos.\n\n" "Choose an installation option below:") + self.message_label = QLabel(_("ffmpeg.installation_message")) self.message_label.setWordWrap(True) self.message_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 10px 0; line-height: 1.4;") self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -93,17 +94,17 @@ class FFmpegCheckDialog(QDialog): button_layout.setSpacing(12) # Install button - self.install_btn = QPushButton("Install FFmpeg") + self.install_btn = QPushButton(_("ffmpeg.install_button")) self.install_btn.clicked.connect(self.start_installation) button_layout.addWidget(self.install_btn) # Manual install button - self.manual_btn = QPushButton("Manual Guide") + self.manual_btn = QPushButton(_("ffmpeg.manual_guide")) self.manual_btn.clicked.connect(lambda: webbrowser.open("https://github.com/oop7/ffmpeg-install-guide")) button_layout.addWidget(self.manual_btn) # Close button - self.close_btn = QPushButton("Close") + self.close_btn = QPushButton(_("buttons.close")) self.close_btn.clicked.connect(self.close) button_layout.addWidget(self.close_btn) @@ -153,15 +154,15 @@ class FFmpegCheckDialog(QDialog): # Check if FFmpeg is already installed if check_ffmpeg_installed(): - self.message_label.setText("FFmpeg is already installed!") - self.progress_label.setText("Installation complete. You can close this dialog and continue using YTSage.") + self.message_label.setText(_("ffmpeg.already_installed")) + self.progress_label.setText(_("ffmpeg.installation_complete")) self.progress_label.show() self.install_btn.hide() self.manual_btn.hide() self.close_btn.setEnabled(True) return - self.message_label.setText("Installing FFmpeg... Please wait") + self.message_label.setText(_("ffmpeg.installing")) self.progress_messages = [] # Clear previous messages self.progress_label.show() @@ -181,13 +182,13 @@ class FFmpegCheckDialog(QDialog): def installation_finished(self, success) -> None: if success: - self.message_label.setText("FFmpeg has been installed successfully!") - self.progress_label.setText("Installation complete. You can now close this dialog and continue using YTSage.") + self.message_label.setText(_("ffmpeg.install_success")) + self.progress_label.setText(_("ffmpeg.installation_complete_close")) self.install_btn.hide() self.manual_btn.hide() else: - self.message_label.setText("FFmpeg installation encountered an issue.") - self.progress_label.setText("Please try using the manual installation guide instead.") + self.message_label.setText(_("ffmpeg.installation_failed")) + self.progress_label.setText(_("ffmpeg.try_manual")) self.install_btn.setEnabled(True) self.manual_btn.setEnabled(True) From 3fbddff279f814b548b1b554775233cf6736a4e6 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:26 +0200 Subject: [PATCH 048/134] Localize yt-dlp setup dialog strings Replaced hardcoded English strings in the YtdlpSetupDialog with calls to the localization function _. This improves internationalization support by allowing all user-facing text in the setup dialog to be translated. --- src/core/ytsage_yt_dlp.py | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/core/ytsage_yt_dlp.py b/src/core/ytsage_yt_dlp.py index 500515a..be11bdf 100644 --- a/src/core/ytsage_yt_dlp.py +++ b/src/core/ytsage_yt_dlp.py @@ -32,6 +32,7 @@ from src.utils.ytsage_constants import ( YTDLP_SHA256_URL, ) from src.core.ytsage_ffmpeg import get_file_sha256 +from src.utils.ytsage_localization import _ # YTDLP_URLS moved to src\utils\ytsage_constants.py # get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py @@ -162,7 +163,7 @@ class YtdlpSetupDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("yt-dlp Setup Required") + self.setWindowTitle(_("ytdlp_setup.required_title")) self.setMinimumWidth(520) self.setMinimumHeight(350) self.resize(520, 380) @@ -253,7 +254,7 @@ class YtdlpSetupDialog(QDialog): layout.setContentsMargins(25, 25, 25, 25) # Header title - title_label = QLabel("yt-dlp Setup Required") + title_label = QLabel(_("ytdlp_setup.required_title")) title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;") title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(title_label) @@ -262,10 +263,7 @@ class YtdlpSetupDialog(QDialog): # os_name logic moved to src\utils\ytsage_constants.py info_label = QLabel( - f"YTSage requires yt-dlp to download videos.

" - f"yt-dlp was not found in the app's local directory. " - f"YTSage needs to set up yt-dlp for your {OS_FULL_NAME} system.

" - f"Please choose an option below:" + _("ytdlp_setup.description", os_name=OS_FULL_NAME) ) info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) info_label.setWordWrap(True) @@ -278,9 +276,9 @@ class YtdlpSetupDialog(QDialog): option_layout.setSpacing(8) option_layout.setContentsMargins(0, 0, 0, 0) - self.auto_radio = QRadioButton("Download automatically (Recommended)") + self.auto_radio = QRadioButton(_("ytdlp_setup.option_auto")) self.auto_radio.setChecked(True) - self.manual_radio = QRadioButton("Select path manually") + self.manual_radio = QRadioButton(_("ytdlp_setup.option_manual")) option_layout.addWidget(self.auto_radio) option_layout.addWidget(self.manual_radio) @@ -326,10 +324,10 @@ class YtdlpSetupDialog(QDialog): button_layout.setSpacing(15) button_layout.setContentsMargins(0, 10, 0, 0) # Add top margin for buttons - self.setup_button = QPushButton("Setup yt-dlp") + self.setup_button = QPushButton(_("ytdlp_setup.setup_button")) self.setup_button.clicked.connect(self.setup_ytdlp) - self.cancel_button = QPushButton("Cancel") + self.cancel_button = QPushButton(_("buttons.cancel")) self.cancel_button.clicked.connect(self.reject) button_layout.addWidget(self.setup_button) @@ -347,7 +345,7 @@ class YtdlpSetupDialog(QDialog): def download_ytdlp(self) -> None: self.progress_bar.setVisible(True) self.progress_bar.setValue(0) - self.status_label.setText("Downloading yt-dlp...") + self.status_label.setText(_("ytdlp_setup.downloading")) self.setup_button.setEnabled(False) self.cancel_button.setEnabled(False) @@ -364,15 +362,15 @@ class YtdlpSetupDialog(QDialog): self.cancel_button.setEnabled(True) if success: - self.status_label.setText("yt-dlp was successfully installed!") + self.status_label.setText(_("ytdlp_setup.success")) self.setup_complete.emit(result) self.accept() else: - self.status_label.setText(f"Error: {result}") + self.status_label.setText(_("ytdlp_setup.error", error=result)) error_dialog = QMessageBox(self) error_dialog.setIcon(QMessageBox.Icon.Critical) - error_dialog.setWindowTitle("Download Failed") - error_dialog.setText(f"Failed to download yt-dlp: {result}") + error_dialog.setWindowTitle(_("ytdlp_setup.download_failed_title")) + error_dialog.setText(_("ytdlp_setup.download_failed_message", error=result)) # Set the window icon to match the main dialog error_dialog.setWindowIcon(self.windowIcon()) error_dialog.setStyleSheet( @@ -401,9 +399,9 @@ class YtdlpSetupDialog(QDialog): def select_ytdlp_path(self) -> None: if OS_NAME == "Windows": - file_filter = "Executable Files (*.exe)" + file_filter = _("ytdlp_setup.file_filter_windows") else: - file_filter = "All Files (*)" + file_filter = _("ytdlp_setup.file_filter_all") # Apply style to QFileDialog file_dialog = QFileDialog(self) @@ -431,7 +429,9 @@ class YtdlpSetupDialog(QDialog): """ ) - file_path, _ = file_dialog.getOpenFileName(self, "Select yt-dlp executable", "", file_filter) + file_path, _ = file_dialog.getOpenFileName( + self, _("ytdlp_setup.select_executable_title"), "", file_filter + ) if file_path: logger.debug(f"User selected file: {file_path}") @@ -465,7 +465,7 @@ class YtdlpSetupDialog(QDialog): logger.debug(f"Permissions set on Unix system") # Return the path of the copied file - self.status_label.setText(f"yt-dlp successfully copied to {target_path}") + self.status_label.setText(_("ytdlp_setup.copied_to", path=target_path)) logger.debug(f"Emitting setup_complete signal with path: {target_path}") self.setup_complete.emit(target_path) self.accept() @@ -473,8 +473,8 @@ class YtdlpSetupDialog(QDialog): logger.debug(f"Error copying file: {copy_error}", exc_info=True) error_dialog = QMessageBox(self) error_dialog.setIcon(QMessageBox.Icon.Critical) - error_dialog.setWindowTitle("Setup Error") - error_dialog.setText(f"Error copying yt-dlp to app directory: {copy_error}") + error_dialog.setWindowTitle(_("ytdlp_setup.setup_error_title")) + error_dialog.setText(_("ytdlp_setup.copy_error", error=copy_error)) error_dialog.setStyleSheet( """ QMessageBox { @@ -502,8 +502,8 @@ class YtdlpSetupDialog(QDialog): logger.debug(f"File verification failed with return code: {result.returncode}") error_dialog = QMessageBox(self) error_dialog.setIcon(QMessageBox.Icon.Warning) - error_dialog.setWindowTitle("Invalid Executable") - error_dialog.setText("The selected file does not appear to be a valid yt-dlp executable.") + error_dialog.setWindowTitle(_("ytdlp_setup.invalid_executable_title")) + error_dialog.setText(_("ytdlp_setup.invalid_executable_message")) error_dialog.setStyleSheet( """ QMessageBox { @@ -531,8 +531,8 @@ class YtdlpSetupDialog(QDialog): logger.debug(f"Exception during verification: {e}", exc_info=True) error_dialog = QMessageBox(self) error_dialog.setIcon(QMessageBox.Icon.Critical) - error_dialog.setWindowTitle("Error") - error_dialog.setText(f"Error verifying yt-dlp executable: {e}") + error_dialog.setWindowTitle(_("main_ui.error_title")) + error_dialog.setText(_("ytdlp_setup.verify_error", error=e)) error_dialog.setStyleSheet( """ QMessageBox { @@ -678,8 +678,8 @@ def setup_ytdlp(parent_widget=None): if parent_widget: error_dialog = QMessageBox(parent_widget) error_dialog.setIcon(QMessageBox.Icon.Warning) - error_dialog.setWindowTitle("Setup Failed") - error_dialog.setText("Failed to set up yt-dlp. Some features may not work correctly.") + error_dialog.setWindowTitle(_("ytdlp_setup.setup_failed_title")) + error_dialog.setText(_("ytdlp_setup.setup_failed_message")) # Set the window icon to match the parent error_dialog.setWindowIcon(parent_widget.windowIcon()) error_dialog.setStyleSheet( From 2d343dfacf43e6275388344dfc584c59af7285f1 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:31 +0200 Subject: [PATCH 049/134] Refactor UI strings to use translation function Replaced hardcoded UI strings and tooltips with calls to the translation function (_) throughout ytsage_gui_main.py. This improves localization support and ensures all user-facing messages are translatable. --- src/gui/ytsage_gui_main.py | 50 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/gui/ytsage_gui_main.py b/src/gui/ytsage_gui_main.py index 99b2677..ffc406d 100644 --- a/src/gui/ytsage_gui_main.py +++ b/src/gui/ytsage_gui_main.py @@ -430,7 +430,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # --- Rename Path Button to Settings Button --- self.settings_button = QPushButton(_("buttons.download_settings")) # Renamed button self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method - self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip + self.settings_button.setToolTip( + _( + "main_ui.settings_tooltip", + path=self.last_path, + speed_limit=_("main_ui.speed_limit_none"), + ) + ) # Update initial tooltip # --- End Settings Button --- self.download_btn = QPushButton(_("buttons.download")) @@ -568,10 +574,16 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Update Tooltip if anything changed if path_changed or limit_changed or format_changed or audio_format_changed: - limit_text = "None" + limit_text = _("main_ui.speed_limit_none") if self.speed_limit_value: limit_text = f"{self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index]}" - self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: {limit_text}") + self.settings_button.setToolTip( + _( + "main_ui.settings_tooltip", + path=self.last_path, + speed_limit=limit_text, + ) + ) def start_download(self) -> None: if self.is_updating_ytdlp: @@ -820,7 +832,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): except Exception as e: logger.exception(f"Error opening download folder: {e}") - QMessageBox.warning(self, "Error", f"Could not open folder: {str(e)}") + QMessageBox.warning(self, _("main_ui.error_title"), _("main_ui.open_folder_error", error=str(e))) def download_error(self, error_message) -> None: self.toggle_download_controls(True) @@ -1069,24 +1081,24 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.info(f"Main proxy set: {self.proxy_url}") QMessageBox.information( self, - "Proxy Set", - f"Main proxy set and saved: {proxy_url}", + _("proxy.set_title"), + _("proxy.set_message", proxy=proxy_url), ) if geo_proxy_url: logger.info(f"Geo-verification proxy set: {self.geo_proxy_url}") QMessageBox.information( self, - "Geo Proxy Set", - f"Geo-verification proxy set and saved: {geo_proxy_url}", + _("proxy.geo_set_title"), + _("proxy.geo_set_message", proxy=geo_proxy_url), ) # Show a combined message if both are cleared if not proxy_url and not geo_proxy_url and (ConfigManager.get("proxy_url") or ConfigManager.get("geo_proxy_url")): QMessageBox.information( self, - "Proxy Settings Cleared", - "All proxy settings have been cleared and saved.", + _("proxy.cleared_title"), + _("proxy.cleared_message"), ) def show_about_dialog(self) -> None: # ADDED METHOD HERE @@ -1119,10 +1131,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Trigger analysis self.analyze_url() else: - QMessageBox.warning(self, "Error", "No URL found in history entry") + QMessageBox.warning(self, _("main_ui.error_title"), _("history.no_url_error")) except Exception as e: logger.error(f"Error handling redownload from history: {e}", exc_info=True) - QMessageBox.warning(self, "Error", f"Failed to start redownload: {str(e)}") + QMessageBox.warning(self, _("main_ui.error_title"), _("history.redownload_failed", error=str(e))) def file_already_exists(self, filename) -> None: """Handle case when file already exists - simplified version""" @@ -1378,8 +1390,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.info(f"Selected cookie file: {self.cookie_file_path}") QMessageBox.information( self, - "Cookie File Selected", - f"Cookie file selected: {self.cookie_file_path}", + _("main_ui.cookie_file_selected_title"), + _("main_ui.cookie_file_selected_message", path=self.cookie_file_path), ) elif browser_cookies: self.browser_cookies_option = browser_cookies @@ -1387,8 +1399,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.info(f"Selected browser cookies: {self.browser_cookies_option}") QMessageBox.information( self, - "Browser Cookies Selected", - f"Browser cookies will be extracted from: {browser_cookies}", + _("main_ui.browser_cookies_selected_title"), + _("main_ui.browser_cookies_selected_message", browser=browser_cookies), ) else: self.cookie_file_path = None # Clear path if dialog accepted but no file selected @@ -1414,7 +1426,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): if self.download_section: self.time_range_btn.setStyleSheet(StyleSheet.TIME_RANGE_BTN_ACTIVE) - self.time_range_btn.setToolTip(f"Section set: {self.download_section}") + self.time_range_btn.setToolTip(_("main_ui.time_range_set", section=self.download_section)) else: # Reset to default style if no section is selected self.download_section = None @@ -1428,8 +1440,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): if yt_dlp_path != "yt-dlp": success_dialog = QMessageBox(self) success_dialog.setIcon(QMessageBox.Icon.Information) - success_dialog.setWindowTitle("yt-dlp Setup") - success_dialog.setText(f"yt-dlp has been successfully configured at:\n{yt_dlp_path}") + success_dialog.setWindowTitle(_("ytdlp_setup.success_dialog_title")) + success_dialog.setText(_("ytdlp_setup.success_dialog_message", path=yt_dlp_path)) success_dialog.setWindowIcon(self.windowIcon()) success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) success_dialog.exec() From 07765a7a196d20a3220062fe918c63bad7e40ab1 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:36 +0200 Subject: [PATCH 050/134] Replace hardcoded strings with translatable keys Updated status and error messages in HistoryDialog to use translation keys instead of hardcoded English strings, improving internationalization support. --- src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 5227656..6c41906 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -435,7 +435,7 @@ class HistoryDialog(QDialog): layout.addWidget(self.status_label) def show_loading_state(self): - self.status_label.setText("Loading history...") + self.status_label.setText(_("history.loading")) self.clear_btn.setEnabled(False) def start_loading_history(self): @@ -517,7 +517,7 @@ class HistoryDialog(QDialog): path = Path(path_str) if not path.exists(): - QMessageBox.warning(self, "Error", f"File not found: {path}") + QMessageBox.warning(self, _("main_ui.error_title"), _("history.file_not_found_message", path=path)) return try: From d985980e88781581e956ace13e1b9bd802d65c51 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:41 +0200 Subject: [PATCH 051/134] Localize current version label in auto-update dialog Replaced hardcoded current version label with a localized string using the translation function in the AutoUpdateSettingsDialog. --- src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index d8ea96d..0d04f29 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -573,7 +573,7 @@ class AutoUpdateSettingsDialog(QDialog): # Update status labels current_version = get_ytdlp_version() - self.current_version_label.setText(f"Current yt-dlp version: {current_version}") + self.current_version_label.setText(_("auto_update.current_version", version=current_version)) last_check = settings["last_check"] if last_check > 0: From 0dffa8fbbdc0877febcbd5b4b9bdac14cbdad978 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:50:49 +0200 Subject: [PATCH 052/134] Add FFmpeg and yt-dlp setup translations Added new translation keys for FFmpeg installation and yt-dlp setup dialogs in French, Hindi, Indonesian, and Chinese. Also updated and expanded several UI strings related to cookies, proxy, download, and history management for improved localization coverage. --- languages/fr.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/hi.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/id.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/zh.json | 56 ++++++++++++++++++++++++++++++--- 4 files changed, 267 insertions(+), 26 deletions(-) diff --git a/languages/fr.json b/languages/fr.json index ff77e2e..f0d45f9 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -93,7 +93,8 @@ "select_subtitles": "S├йlectionner les sous-titres", "filter_languages_placeholder": "Filtrer les langues (ex: en, fr)...", "no_subtitles_available": "Aucun sous-titre disponible", - "matching": "correspondant" + "matching": "correspondant", + "ytdlp_log_title": "Journal yt-dlp" }, "tabs": { "cookies": "Se connecter avec des cookies", @@ -124,7 +125,10 @@ "browser_selected_title": "Cookies du navigateur appliqu├йs", "browser_applied_message": "Les cookies du navigateur seront extraits de : {browser}", "cleared_title": "Cookies effac├йs", - "cleared_message": "Les param├иtres de cookies ont ├йt├й effac├йs" + "cleared_message": "Les param├иtres des cookies ont ├йt├й effac├йs", + "active_browser": "тЬУ Actifs : cookies du navigateur ({browser})", + "active_file": "тЬУ Actif : fichier cookies ({file})", + "none_active": "тЧЛ Aucun cookie actif" }, "custom_command": { "help_text": "Entrez votre commande yt-dlp personnalis├йe ci-dessous. L'URL actuelle sera automatiquement ajout├йe.

Pour la liste compl├иte des options et exemples d'utilisation cliquez ici pour voir la documentation officielle yt-dlp.

Note : Le chemin de t├йl├йchargement et le mod├иle de nom de fichier sont g├йr├йs automatiquement.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Commande compl├иte : {command}", "command_success": "тЬЕ Commande personnalis├йe ex├йcut├йe avec succ├иs !", "command_failed": "тЭМ Commande ├йchou├йe avec le code de sortie {code}", - "command_error": "тЭМ Erreur lors de l'ex├йcution de la commande personnalis├йe : {error}" + "command_error": "тЭМ Erreur lors de l'ex├йcution de la commande personnalis├йe : {error}", + "error_no_url": "тЭМ Erreur : aucune URL fournie. Veuillez saisir une URL dans la fen├кtre principale.", + "error_no_command": "тЭМ Erreur : aucune commande fournie. Veuillez saisir des arguments yt-dlp.", + "executing": "ЁЯЪА Ex├йcution de la commande yt-dlp personnalis├йe", + "url_label": "ЁЯУН URL : {url}", + "args_label": "тЪЩя╕П Arguments : {command}", + "download_path_label": "ЁЯУБ Chemin de t├йl├йchargement : {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Configurer les param├иtres proxy pour les t├йl├йchargements. Laisser vide pour une connexion directe.", @@ -159,7 +170,15 @@ "invalid_main_url": "Format d'URL de proxy principal invalide", "invalid_geo_url": "Format d'URL de proxy g├йographique invalide", "main_configured": "Proxy principal configur├й", - "geo_configured": "Proxy g├йographique configur├й" + "geo_configured": "Proxy g├йographique configur├й", + "set_title": "Proxy d├йfini", + "set_message": "Proxy principal d├йfini et enregistr├й : {proxy}", + "geo_set_title": "Proxy g├йo d├йfini", + "geo_set_message": "Proxy de v├йrification g├йographique d├йfini et enregistr├й : {proxy}", + "cleared_title": "Param├иtres proxy effac├йs", + "cleared_message": "Tous les param├иtres proxy ont ├йt├й effac├йs et enregistr├йs.", + "saved_main": "Proxy principal enregistr├й : {proxy}", + "saved_geo": "Proxy g├йo enregistr├й : {proxy}" }, "download": { "preparing": "Pr├йparation du t├йl├йchargement...", @@ -347,7 +366,11 @@ "zero_selected": "0 s├йlectionn├й", "analyze_first_tooltip": "Veuillez d'abord analyser la vid├йo", "audio_mode_disabled": "Non disponible en mode audio uniquement", - "select_subtitles_first": "Veuillez d'abord s├йlectionner les sous-titres" + "select_subtitles_first": "Veuillez d'abord s├йlectionner les sous-titres", + "settings_tooltip": "Chemin actuel : {path}\nLimite de vitesse : {speed_limit}", + "speed_limit_none": "Aucune", + "open_folder_error": "Impossible dтАЩouvrir le dossier : {error}", + "time_range_set": "Section d├йfinie : {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +469,8 @@ "next_check": "Prochaine v├йrification : {time}", "next_check_error": "Prochaine v├йrification : Erreur de calcul", "checking": "ЁЯФД V├йrification...", - "check_now": "ЁЯФН V├йrifier les mises ├а jour maintenant" + "check_now": "ЁЯФН V├йrifier les mises ├а jour maintenant", + "current_version": "Version actuelle de yt-dlp : {version}" }, "url_validation": { "empty_url": "L'URL ne peut pas ├кtre vide", @@ -477,6 +501,7 @@ "clear_confirm_message": "├Кtes-vous s├╗r? Cela ne peut pas ├кtre annul├й.", "no_history": "Pas encore d'historique", "no_history_description": "Vos t├йl├йchargements appara├оtront ici", + "loading": "Chargement de lтАЩhistorique...", "search_placeholder": "Rechercher...", "open_location": "Ouvrir l'Emplacement", "redownload": "T├йl├йcharger ├а Nouveau", @@ -495,7 +520,47 @@ "one_entry": "1 t├йl├йchargement", "redownload_confirm_title": "T├йl├йcharger ├а Nouveau?", "redownload_confirm_message": "T├йl├йcharger ├а nouveau?\n\n{title}", - "redownload_started": "T├йl├йchargement d├йmarr├й" + "redownload_started": "T├йl├йchargement d├йmarr├й", + "no_url_error": "Aucune URL trouv├йe dans lтАЩentr├йe dтАЩhistorique", + "redownload_failed": "Impossible de relancer le t├йl├йchargement : {error}" + }, + "ffmpeg": { + "installation_title": "Installation de FFmpeg", + "installation_message": "YTSage a besoin de FFmpeg pour traiter les vid├йos.\n\nChoisissez une option dтАЩinstallation ciтАСdessous :", + "install_button": "Installer FFmpeg", + "manual_guide": "Guide manuel", + "installation_failed": "LтАЩinstallation de FFmpeg a rencontr├й un probl├иme.", + "already_installed": "FFmpeg est d├йj├а install├й !", + "installation_complete": "Installation termin├йe. Vous pouvez fermer cette bo├оte de dialogue et continuer ├а utiliser YTSage.", + "installing": "Installation de FFmpeg... Veuillez patienter", + "install_success": "FFmpeg a ├йt├й install├й avec succ├иs !", + "installation_complete_close": "Installation termin├йe. Vous pouvez maintenant fermer cette bo├оte de dialogue et continuer ├а utiliser YTSage.", + "try_manual": "Veuillez essayer dтАЩutiliser le guide dтАЩinstallation manuel ├а la place." + }, + "ytdlp_setup": { + "required_title": "Configuration de yt-dlp requise", + "description": "YTSage n├йcessite yt-dlp pour t├йl├йcharger des vid├йos.

yt-dlp est introuvable dans le r├йpertoire local de lтАЩapplication. YTSage doit configurer yt-dlp pour votre syst├иme {os_name}.

Veuillez choisir une option ciтАСdessous :", + "option_auto": "T├йl├йcharger automatiquement (recommand├й)", + "option_manual": "S├йlectionner le chemin manuellement", + "setup_button": "Configurer yt-dlp", + "downloading": "T├йl├йchargement de yt-dlp...", + "success": "yt-dlp a ├йt├й install├й avec succ├иs !", + "error": "Erreur : {error}", + "download_failed_title": "T├йl├йchargement ├йchou├й", + "download_failed_message": "├Йchec du t├йl├йchargement de yt-dlp : {error}", + "select_executable_title": "S├йlectionner lтАЩex├йcutable yt-dlp", + "copied_to": "yt-dlp copi├й avec succ├иs vers {path}", + "setup_error_title": "Erreur de configuration", + "copy_error": "Erreur lors de la copie de yt-dlp dans le r├йpertoire de lтАЩapplication : {error}", + "invalid_executable_title": "Ex├йcutable invalide", + "invalid_executable_message": "Le fichier s├йlectionn├й ne semble pas ├кtre un ex├йcutable yt-dlp valide.", + "verify_error": "Erreur lors de la v├йrification de lтАЩex├йcutable yt-dlp : {error}", + "setup_failed_title": "Configuration ├йchou├йe", + "setup_failed_message": "La configuration de yt-dlp a ├йchou├й. Certaines fonctionnalit├йs peuvent ne pas fonctionner correctement.", + "success_dialog_title": "Configuration de yt-dlp", + "success_dialog_message": "yt-dlp a ├йt├й configur├й avec succ├иs ├а lтАЩemplacement :\n{path}", + "file_filter_windows": "Fichiers ex├йcutables (*.exe)", + "file_filter_all": "Tous les fichiers (*)" }, "ffmpeg_updater": { "title": "V├йrificateur de Version FFmpeg", diff --git a/languages/hi.json b/languages/hi.json index 1bacff3..97ea90d 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -93,7 +93,8 @@ "select_subtitles": "рдЙрдкрд╢реАрд░реНрд╖рдХ рдЪреБрдиреЗрдВ", "filter_languages_placeholder": "рднрд╛рд╖рд╛рдПрдВ рдлрд╝рд┐рд▓реНрдЯрд░ рдХрд░реЗрдВ (рдЬреИрд╕реЗ: hi, en)...", "no_subtitles_available": "рдХреЛрдИ рдЙрдкрд╢реАрд░реНрд╖рдХ рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВ", - "matching": "рдореЗрд▓ рдЦрд╛рддрд╛" + "matching": "рдореЗрд▓ рдЦрд╛рддрд╛", + "ytdlp_log_title": "yt-dlp рд▓реЙрдЧ" }, "tabs": { "cookies": "рдХреБрдХреАрдЬрд╝ рдХреЗ рд╕рд╛рде рд▓реЙрдЧрд┐рди", @@ -124,7 +125,10 @@ "browser_selected_title": "рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреБрдХреАрдЬрд╝ рд▓рд╛рдЧреВ рдХреА рдЧрдИрдВ", "browser_applied_message": "рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреБрдХреАрдЬрд╝ рдирд┐рдХрд╛рд▓реА рдЬрд╛рдПрдВрдЧреА: {browser}", "cleared_title": "рдХреБрдХреАрдЬрд╝ рд╕рд╛рдлрд╝ рдХреА рдЧрдИрдВ", - "cleared_message": "рдХреБрдХреА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╕рд╛рдлрд╝ рдХрд░ рджреА рдЧрдИ рд╣реИрдВ" + "cleared_message": "рдХреБрдХреА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╕рд╛рдлрд╝ рдХрд░ рджреА рдЧрдИ рд╣реИрдВ", + "active_browser": "тЬУ рд╕рдХреНрд░рд┐рдп: рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреБрдХреАрдЬрд╝ ({browser})", + "active_file": "тЬУ рд╕рдХреНрд░рд┐рдп: рдХреБрдХреА рдлрд╝рд╛рдЗрд▓ ({file})", + "none_active": "тЧЛ рдХреЛрдИ рдХреБрдХреА рд╕рдХреНрд░рд┐рдп рдирд╣реАрдВ" }, "custom_command": { "help_text": "рдиреАрдЪреЗ рдЕрдкрдирд╛ рдХрд╕реНрдЯрдо yt-dlp рдХрдорд╛рдВрдб рджрд░реНрдЬ рдХрд░реЗрдВред рд╡рд░реНрддрдорд╛рди URL рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдЬреЛрдбрд╝рд╛ рдЬрд╛рдПрдЧрд╛ред

рд╡рд┐рдХрд▓реНрдкреЛрдВ рдХреА рдкреВрд░реА рд╕реВрдЪреА рдФрд░ рдЙрдкрдпреЛрдЧ рдХреЗ рдЙрджрд╛рд╣рд░рдгреЛрдВ рдХреЗ рд▓рд┐рдП рдЖрдзрд┐рдХрд╛рд░рд┐рдХ yt-dlp рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рджреЗрдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдпрд╣рд╛рдБ рдХреНрд▓рд┐рдХ рдХрд░реЗрдВред

рдиреЛрдЯ: рдбрд╛рдЙрдирд▓реЛрдб рдкрде рдФрд░ рдлрд╝рд╛рдЗрд▓рдирд╛рдо рдЯреЗрдореНрдкреНрд▓реЗрдЯ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рд╕рдВрднрд╛рд▓реЗ рдЬрд╛рддреЗ рд╣реИрдВред", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз рдкреВрд░рд╛ рдХрдорд╛рдВрдб: {command}", "command_success": "тЬЕ рдХрд╕реНрдЯрдо рдХрдорд╛рдВрдб рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдирд┐рд╖реНрдкрд╛рджрд┐рдд!", "command_failed": "тЭМ рдХрдорд╛рдВрдб рдЕрд╕рдлрд▓, рдПрдЧреНрдЬрд┐рдЯ рдХреЛрдб {code}", - "command_error": "тЭМ рдХрд╕реНрдЯрдо рдХрдорд╛рдВрдб рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд░рдиреЗ рдореЗрдВ рддреНрд░реБрдЯрд┐: {error}" + "command_error": "тЭМ рдХрд╕реНрдЯрдо рдХрдорд╛рдВрдб рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд░рдиреЗ рдореЗрдВ рддреНрд░реБрдЯрд┐: {error}", + "error_no_url": "тЭМ рддреНрд░реБрдЯрд┐: рдХреЛрдИ URL рдирд╣реАрдВ рджрд┐рдпрд╛ рдЧрдпрд╛ред рдХреГрдкрдпрд╛ рдореБрдЦреНрдп рд╡рд┐рдВрдбреЛ рдореЗрдВ URL рджрд░реНрдЬ рдХрд░реЗрдВред", + "error_no_command": "тЭМ рддреНрд░реБрдЯрд┐: рдХреЛрдИ рдХрдорд╛рдВрдб рдирд╣реАрдВ рджрд┐рдпрд╛ рдЧрдпрд╛ред рдХреГрдкрдпрд╛ yt-dlp рдЖрд░реНрдЧреБрдореЗрдВрдЯреНрд╕ рджрд░реНрдЬ рдХрд░реЗрдВред", + "executing": "ЁЯЪА рдХрд╕реНрдЯрдо yt-dlp рдХрдорд╛рдВрдб рдЪрд▓ рд░рд╣рд╛ рд╣реИ", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П рдЖрд░реНрдЧреБрдореЗрдВрдЯреНрд╕: {command}", + "download_path_label": "ЁЯУБ рдбрд╛рдЙрдирд▓реЛрдб рдкрде: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "рдбрд╛рдЙрдирд▓реЛрдб рдХреЗ рд▓рд┐рдП рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВред рдкреНрд░рддреНрдпрдХреНрд╖ рдХрдиреЗрдХреНрд╢рди рдХреЗ рд▓рд┐рдП рдЦрд╛рд▓реА рдЫреЛрдбрд╝реЗрдВред", @@ -159,7 +170,15 @@ "invalid_main_url": "рдЕрдорд╛рдиреНрдп рдореБрдЦреНрдп рдкреНрд░реЙрдХреНрд╕реА URL рдкреНрд░рд╛рд░реВрдк", "invalid_geo_url": "рдЕрдорд╛рдиреНрдп рднреВ-рдкреНрд░реЙрдХреНрд╕реА URL рдкреНрд░рд╛рд░реВрдк", "main_configured": "рдореБрдЦреНрдп рдкреНрд░реЙрдХреНрд╕реА рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХреА рдЧрдИ", - "geo_configured": "рднреВ-рдкреНрд░реЙрдХреНрд╕реА рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХреА рдЧрдИ" + "geo_configured": "рднреВ-рдкреНрд░реЙрдХреНрд╕реА рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХреА рдЧрдИ", + "set_title": "рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯ", + "set_message": "рдореБрдЦреНрдп рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯ рдФрд░ рд╕реЗрд╡ рдХрд┐рдпрд╛ рдЧрдпрд╛: {proxy}", + "geo_set_title": "рдЬрд┐рдпреЛ рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯ", + "geo_set_message": "рдЬрд┐рдпреЛтАСрд╡реЗрд░рд┐рдлрд┐рдХреЗрд╢рди рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯ рдФрд░ рд╕реЗрд╡ рдХрд┐рдпрд╛ рдЧрдпрд╛: {proxy}", + "cleared_title": "рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╕рд╛рдлрд╝ рдХреА рдЧрдИрдВ", + "cleared_message": "рд╕рднреА рдкреНрд░реЙрдХреНрд╕реА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рд╕рд╛рдлрд╝ рдХрд░ рд╕реЗрд╡ рдХрд░ рджреА рдЧрдИрдВред", + "saved_main": "рд╕рд╣реЗрдЬрд╛ рдЧрдпрд╛ рдореБрдЦреНрдп рдкреНрд░реЙрдХреНрд╕реА: {proxy}", + "saved_geo": "рд╕рд╣реЗрдЬрд╛ рдЧрдпрд╛ рдЬрд┐рдпреЛ рдкреНрд░реЙрдХреНрд╕реА: {proxy}" }, "download": { "preparing": "рдбрд╛рдЙрдирд▓реЛрдб рддреИрдпрд╛рд░ рд╣реЛ рд░рд╣рд╛ рд╣реИ...", @@ -347,7 +366,11 @@ "zero_selected": "0 рдЪрдпрдирд┐рдд", "analyze_first_tooltip": "рдХреГрдкрдпрд╛ рдкрд╣рд▓реЗ рд╡реАрдбрд┐рдпреЛ рдХрд╛ рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдХрд░реЗрдВ", "audio_mode_disabled": "рдХреЗрд╡рд▓ рдСрдбрд┐рдпреЛ рдореЛрдб рдореЗрдВ рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВ", - "select_subtitles_first": "рдХреГрдкрдпрд╛ рдкрд╣рд▓реЗ рдЙрдкрд╢реАрд░реНрд╖рдХ рдЪреБрдиреЗрдВ" + "select_subtitles_first": "рдХреГрдкрдпрд╛ рдкрд╣рд▓реЗ рдЙрдкрд╢реАрд░реНрд╖рдХ рдЪреБрдиреЗрдВ", + "settings_tooltip": "рд╡рд░реНрддрдорд╛рди рдкрде: {path}\nрд╕реНрдкреАрдб рд▓рд┐рдорд┐рдЯ: {speed_limit}", + "speed_limit_none": "рдХреЛрдИ рдирд╣реАрдВ", + "open_folder_error": "рдлрд╝реЛрд▓реНрдбрд░ рдирд╣реАрдВ рдЦреБрд▓ рд╕рдХрд╛: {error}", + "time_range_set": "рд╕реЗрдХреНрд╢рди рд╕реЗрдЯ: {section}" }, "sponsorblock": { "sponsor": "рдкреНрд░рд╛рдпреЛрдЬрдХ", @@ -446,7 +469,8 @@ "next_check": "рдЕрдЧрд▓реА рдЬрд╛рдВрдЪ: {time}", "next_check_error": "рдЕрдЧрд▓реА рдЬрд╛рдВрдЪ: рдЧрдгрдирд╛ рддреНрд░реБрдЯрд┐", "checking": "ЁЯФД рдЬрд╛рдВрдЪ рд░рд╣реЗ рд╣реИрдВ...", - "check_now": "ЁЯФН рдЕрднреА рдЕрдкрдбреЗрдЯ рдХреА рдЬрд╛рдВрдЪ рдХрд░реЗрдВ" + "check_now": "ЁЯФН рдЕрднреА рдЕрдкрдбреЗрдЯ рдХреА рдЬрд╛рдВрдЪ рдХрд░реЗрдВ", + "current_version": "рд╡рд░реНрддрдорд╛рди yt-dlp рд╕рдВрд╕реНрдХрд░рдг: {version}" }, "url_validation": { "empty_url": "URL рдЦрд╛рд▓реА рдирд╣реАрдВ рд╣реЛ рд╕рдХрддрд╛", @@ -477,6 +501,7 @@ "clear_confirm_message": "рдХреНрдпрд╛ рдЖрдк рдирд┐рд╢реНрдЪрд┐рдд рд╣реИрдВ? рдЗрд╕реЗ рдкреВрд░реНрд╡рд╡рдд рдирд╣реАрдВ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ред", "no_history": "рдЕрднреА рддрдХ рдХреЛрдИ рдЗрддрд┐рд╣рд╛рд╕ рдирд╣реАрдВ", "no_history_description": "рдЖрдкрдХреЗ рдбрд╛рдЙрдирд▓реЛрдб рдпрд╣рд╛рдВ рджрд┐рдЦрд╛рдИ рджреЗрдВрдЧреЗ", + "loading": "рдЗрддрд┐рд╣рд╛рд╕ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ...", "search_placeholder": "рдЦреЛрдЬреЗрдВ...", "open_location": "рд╕реНрдерд╛рди рдЦреЛрд▓реЗрдВ", "redownload": "рдлрд┐рд░ рд╕реЗ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ", @@ -495,7 +520,47 @@ "one_entry": "1 рдбрд╛рдЙрдирд▓реЛрдб", "redownload_confirm_title": "рдлрд┐рд░ рд╕реЗ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ?", "redownload_confirm_message": "рдлрд┐рд░ рд╕реЗ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ?\n\n{title}", - "redownload_started": "рдбрд╛рдЙрдирд▓реЛрдб рд╢реБрд░реВ рд╣реБрдЖ" + "redownload_started": "рдбрд╛рдЙрдирд▓реЛрдб рд╢реБрд░реВ рд╣реБрдЖ", + "no_url_error": "рд╣рд┐рд╕реНрдЯреНрд░реА рдПрдВрдЯреНрд░реА рдореЗрдВ рдХреЛрдИ URL рдирд╣реАрдВ рдорд┐рд▓рд╛", + "redownload_failed": "рд░реАрдбрд╛рдЙрдирд▓реЛрдб рд╢реБрд░реВ рдХрд░рдиреЗ рдореЗрдВ рд╡рд┐рдлрд▓: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpeg рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди", + "installation_message": "рд╡реАрдбрд┐рдпреЛ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП YTSage рдХреЛ FFmpeg рдЪрд╛рд╣рд┐рдПред\n\nрдиреАрдЪреЗ рдПрдХ рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рд╡рд┐рдХрд▓реНрдк рдЪреБрдиреЗрдВ:", + "install_button": "FFmpeg рдЗрдВрд╕реНрдЯреЙрд▓ рдХрд░реЗрдВ", + "manual_guide": "рдореИрдиреНрдпреБрдЕрд▓ рдЧрд╛рдЗрдб", + "installation_failed": "FFmpeg рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рдореЗрдВ рд╕рдорд╕реНрдпрд╛ рдЖрдИред", + "already_installed": "FFmpeg рдкрд╣рд▓реЗ рд╕реЗ рдЗрдВрд╕реНрдЯреЙрд▓ рд╣реИ!", + "installation_complete": "рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рдкреВрд░рд╛ рд╣реБрдЖред рдЖрдк рдЗрд╕ рдбрд╛рдпрд▓реЙрдЧ рдХреЛ рдмрдВрдж рдХрд░ YTSage рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред", + "installing": "FFmpeg рдЗрдВрд╕реНрдЯреЙрд▓ рд╣реЛ рд░рд╣рд╛ рд╣реИ... рдХреГрдкрдпрд╛ рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗрдВ", + "install_success": "FFmpeg рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдЗрдВрд╕реНрдЯреЙрд▓ рд╣реЛ рдЧрдпрд╛!", + "installation_complete_close": "рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рдкреВрд░рд╛ рд╣реБрдЖред рдЕрдм рдЖрдк рдЗрд╕ рдбрд╛рдпрд▓реЙрдЧ рдХреЛ рдмрдВрдж рдХрд░ YTSage рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред", + "try_manual": "рдХреГрдкрдпрд╛ рдЗрд╕рдХреЗ рдмрдЬрд╛рдп рдореИрдиреНрдпреБрдЕрд▓ рдЗрдВрд╕реНрдЯреЙрд▓реЗрд╢рди рдЧрд╛рдЗрдб рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВред" + }, + "ytdlp_setup": { + "required_title": "yt-dlp рд╕реЗрдЯрдЕрдк рдЖрд╡рд╢реНрдпрдХ", + "description": "рд╡реАрдбрд┐рдпреЛ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП YTSage рдХреЛ yt-dlp рдЪрд╛рд╣рд┐рдПред

рдРрдк рдХреА рд▓реЛрдХрд▓ рдбрд╛рдпрд░реЗрдХреНрдЯрд░реА рдореЗрдВ yt-dlp рдирд╣реАрдВ рдорд┐рд▓рд╛ред YTSage рдХреЛ рдЖрдкрдХреЗ {os_name} рд╕рд┐рд╕реНрдЯрдо рдХреЗ рд▓рд┐рдП yt-dlp рд╕реЗрдЯрдЕрдк рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред

рдХреГрдкрдпрд╛ рдиреАрдЪреЗ рдПрдХ рд╡рд┐рдХрд▓реНрдк рдЪреБрдиреЗрдВ:", + "option_auto": "рдЕрдкрдиреЗ рдЖрдк рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ (рд╕рд┐рдлрд╝рд╛рд░рд┐рд╢ рдХреА рдЧрдИ)", + "option_manual": "рдкрд╛рде рдореИрдиреНрдпреБрдЕрд▓реА рдЪреБрдиреЗрдВ", + "setup_button": "yt-dlp рд╕реЗрдЯрдЕрдк", + "downloading": "yt-dlp рдбрд╛рдЙрдирд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "success": "yt-dlp рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдЗрдВрд╕реНрдЯреЙрд▓ рд╣реЛ рдЧрдпрд╛!", + "error": "рддреНрд░реБрдЯрд┐: {error}", + "download_failed_title": "рдбрд╛рдЙрдирд▓реЛрдб рд╡рд┐рдлрд▓", + "download_failed_message": "yt-dlp рдбрд╛рдЙрдирд▓реЛрдб рдирд╣реАрдВ рд╣реЛ рд╕рдХрд╛: {error}", + "select_executable_title": "yt-dlp executable рдЪреБрдиреЗрдВ", + "copied_to": "yt-dlp рдХреЛ {path} рдкрд░ рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдХреЙрдкреА рдХрд┐рдпрд╛ рдЧрдпрд╛", + "setup_error_title": "рд╕реЗрдЯрдЕрдк рддреНрд░реБрдЯрд┐", + "copy_error": "yt-dlp рдХреЛ рдРрдк рдбрд╛рдпрд░реЗрдХреНрдЯрд░реА рдореЗрдВ рдХреЙрдкреА рдХрд░рдиреЗ рдореЗрдВ рддреНрд░реБрдЯрд┐: {error}", + "invalid_executable_title": "рдЕрдорд╛рдиреНрдп executable", + "invalid_executable_message": "рдЪреБрдиреА рдЧрдИ рдлрд╝рд╛рдЗрд▓ рд╡реИрдз yt-dlp executable рдирд╣реАрдВ рд▓рдЧрддреАред", + "verify_error": "yt-dlp executable рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░рдиреЗ рдореЗрдВ рддреНрд░реБрдЯрд┐: {error}", + "setup_failed_title": "рд╕реЗрдЯрдЕрдк рд╡рд┐рдлрд▓", + "setup_failed_message": "yt-dlp рд╕реЗрдЯрдЕрдк рд╡рд┐рдлрд▓ рд░рд╣рд╛ред рдХреБрдЫ рдлреАрдЪрд░ рд╕рд╣реА рд╕реЗ рдХрд╛рдо рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗред", + "success_dialog_title": "yt-dlp рд╕реЗрдЯрдЕрдк", + "success_dialog_message": "yt-dlp рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдЗрд╕ рд╕реНрдерд╛рди рдкрд░ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рд╣реБрдЖ рд╣реИ:\n{path}", + "file_filter_windows": "Executable рдлрд╝рд╛рдЗрд▓реЗрдВ (*.exe)", + "file_filter_all": "рд╕рднреА рдлрд╝рд╛рдЗрд▓реЗрдВ (*)" }, "ffmpeg_updater": { "title": "FFmpeg рд╕рдВрд╕реНрдХрд░рдг рдЬрд╛рдВрдЪрдХрд░реНрддрд╛", diff --git a/languages/id.json b/languages/id.json index 46eea59..6a76658 100644 --- a/languages/id.json +++ b/languages/id.json @@ -93,7 +93,8 @@ "select_subtitles": "Pilih subtitle", "filter_languages_placeholder": "Filter bahasa (misalnya: id, en)...", "no_subtitles_available": "Tidak ada subtitle yang tersedia", - "matching": "yang cocok" + "matching": "yang cocok", + "ytdlp_log_title": "Log yt-dlp" }, "tabs": { "cookies": "Masuk dengan cookies", @@ -124,7 +125,10 @@ "browser_selected_title": "Cookies Browser Diterapkan", "browser_applied_message": "Cookies browser akan diekstrak dari: {browser}", "cleared_title": "Cookies Dihapus", - "cleared_message": "Pengaturan cookie telah dihapus" + "cleared_message": "Pengaturan cookie telah dihapus", + "active_browser": "тЬУ Aktif: cookie browser ({browser})", + "active_file": "тЬУ Aktif: file cookie ({file})", + "none_active": "тЧЛ Tidak ada cookie aktif" }, "custom_command": { "help_text": "Masukkan perintah yt-dlp khusus Anda di bawah. URL saat ini akan ditambahkan secara otomatis.

Untuk daftar lengkap opsi dan contoh penggunaan klik di sini untuk melihat dokumentasi resmi yt-dlp.

Catatan: Jalur unduhan dan template nama file ditangani secara otomatis.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Perintah lengkap: {command}", "command_success": "тЬЕ Perintah khusus berhasil dijalankan!", "command_failed": "тЭМ Perintah gagal dengan kode keluar {code}", - "command_error": "тЭМ Kesalahan menjalankan perintah khusus: {error}" + "command_error": "тЭМ Kesalahan menjalankan perintah khusus: {error}", + "error_no_url": "тЭМ Error: Tidak ada URL yang diberikan. Masukkan URL di jendela utama.", + "error_no_command": "тЭМ Error: Tidak ada perintah yang diberikan. Masukkan argumen yt-dlp.", + "executing": "ЁЯЪА Menjalankan perintah yt-dlp kustom", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argumen: {command}", + "download_path_label": "ЁЯУБ Lokasi unduhan: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Konfigurasi pengaturan proxy untuk unduhan. Biarkan kosong untuk koneksi langsung.", @@ -159,7 +170,15 @@ "invalid_main_url": "Format URL proxy utama tidak valid", "invalid_geo_url": "Format URL proxy geografis tidak valid", "main_configured": "Proxy utama dikonfigurasi", - "geo_configured": "Proxy geografis dikonfigurasi" + "geo_configured": "Proxy geografis dikonfigurasi", + "set_title": "Proxy diatur", + "set_message": "Proxy utama diatur dan disimpan: {proxy}", + "geo_set_title": "Proxy geo diatur", + "geo_set_message": "Proxy verifikasi geo diatur dan disimpan: {proxy}", + "cleared_title": "Pengaturan proxy dibersihkan", + "cleared_message": "Semua pengaturan proxy telah dibersihkan dan disimpan.", + "saved_main": "Proxy utama tersimpan: {proxy}", + "saved_geo": "Proxy geo tersimpan: {proxy}" }, "download": { "preparing": "Mempersiapkan unduhan...", @@ -347,7 +366,11 @@ "zero_selected": "0 dipilih", "analyze_first_tooltip": "Silakan analisis video terlebih dahulu", "audio_mode_disabled": "Tidak tersedia dalam mode audio saja", - "select_subtitles_first": "Silakan pilih subtitle terlebih dahulu" + "select_subtitles_first": "Silakan pilih subtitle terlebih dahulu", + "settings_tooltip": "Path saat ini: {path}\nBatas kecepatan: {speed_limit}", + "speed_limit_none": "Tidak ada", + "open_folder_error": "Tidak dapat membuka folder: {error}", + "time_range_set": "Bagian disetel: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +469,8 @@ "next_check": "Pemeriksaan berikutnya: {time}", "next_check_error": "Pemeriksaan berikutnya: Kesalahan perhitungan", "checking": "ЁЯФД Memeriksa...", - "check_now": "ЁЯФН Periksa pembaruan sekarang" + "check_now": "ЁЯФН Periksa pembaruan sekarang", + "current_version": "Versi yt-dlp saat ini: {version}" }, "url_validation": { "empty_url": "URL tidak boleh kosong", @@ -477,6 +501,7 @@ "clear_confirm_message": "Apakah Anda yakin? Ini tidak dapat dibatalkan.", "no_history": "Belum ada riwayat", "no_history_description": "Unduhan Anda akan muncul di sini", + "loading": "Memuat riwayat...", "search_placeholder": "Cari...", "open_location": "Buka Lokasi", "redownload": "Unduh Lagi", @@ -495,7 +520,47 @@ "one_entry": "1 unduhan", "redownload_confirm_title": "Unduh Lagi?", "redownload_confirm_message": "Unduh lagi?\n\n{title}", - "redownload_started": "Unduhan dimulai" + "redownload_started": "Unduhan dimulai", + "no_url_error": "Tidak ada URL pada entri riwayat", + "redownload_failed": "Gagal memulai unduh ulang: {error}" + }, + "ffmpeg": { + "installation_title": "Instalasi FFmpeg", + "installation_message": "YTSage membutuhkan FFmpeg untuk memproses video.\n\nPilih opsi instalasi di bawah:", + "install_button": "Instal FFmpeg", + "manual_guide": "Panduan manual", + "installation_failed": "Instalasi FFmpeg mengalami masalah.", + "already_installed": "FFmpeg sudah terinstal!", + "installation_complete": "Instalasi selesai. Anda dapat menutup dialog ini dan melanjutkan menggunakan YTSage.", + "installing": "Menginstal FFmpeg... Harap tunggu", + "install_success": "FFmpeg berhasil diinstal!", + "installation_complete_close": "Instalasi selesai. Anda sekarang dapat menutup dialog ini dan melanjutkan menggunakan YTSage.", + "try_manual": "Silakan coba gunakan panduan instalasi manual sebagai gantinya." + }, + "ytdlp_setup": { + "required_title": "Pengaturan yt-dlp diperlukan", + "description": "YTSage memerlukan yt-dlp untuk mengunduh video.

yt-dlp tidak ditemukan di direktori lokal aplikasi. YTSage perlu menyiapkan yt-dlp untuk sistem {os_name} Anda.

Silakan pilih opsi di bawah:", + "option_auto": "Unduh otomatis (disarankan)", + "option_manual": "Pilih path secara manual", + "setup_button": "Siapkan yt-dlp", + "downloading": "Mengunduh yt-dlp...", + "success": "yt-dlp berhasil diinstal!", + "error": "Error: {error}", + "download_failed_title": "Unduhan gagal", + "download_failed_message": "Gagal mengunduh yt-dlp: {error}", + "select_executable_title": "Pilih executable yt-dlp", + "copied_to": "yt-dlp berhasil disalin ke {path}", + "setup_error_title": "Kesalahan pengaturan", + "copy_error": "Kesalahan menyalin yt-dlp ke direktori aplikasi: {error}", + "invalid_executable_title": "Executable tidak valid", + "invalid_executable_message": "File yang dipilih tidak tampak sebagai executable yt-dlp yang valid.", + "verify_error": "Kesalahan memverifikasi executable yt-dlp: {error}", + "setup_failed_title": "Pengaturan gagal", + "setup_failed_message": "Gagal menyiapkan yt-dlp. Beberapa fitur mungkin tidak berfungsi dengan benar.", + "success_dialog_title": "Pengaturan yt-dlp", + "success_dialog_message": "yt-dlp berhasil dikonfigurasi di:\n{path}", + "file_filter_windows": "File executable (*.exe)", + "file_filter_all": "Semua file (*)" }, "ffmpeg_updater": { "title": "Pemeriksa Versi FFmpeg", diff --git a/languages/zh.json b/languages/zh.json index 812ae47..27498d8 100644 --- a/languages/zh.json +++ b/languages/zh.json @@ -93,7 +93,8 @@ "select_subtitles": "щАЙцЛйхнЧх╣Х", "filter_languages_placeholder": "ш┐Зц╗дшпншиАя╝Иф╛ЛхжВя╝Ъen, zhя╝Й...", "no_subtitles_available": "цЧахПпчФихнЧх╣Х", - "matching": "хМ╣щЕН" + "matching": "хМ╣щЕН", + "ytdlp_log_title": "yt-dlp цЧех┐Ч" }, "tabs": { "cookies": "ф╜┐чФи Cookie чЩ╗х╜Х", @@ -159,7 +160,13 @@ "invalid_main_url": "ф╕╗ф╗гчРЖч╜СхЭАца╝х╝ПцЧацХИ", "invalid_geo_url": "хЬ░чРЖф╗гчРЖч╜СхЭАца╝х╝ПцЧацХИ", "main_configured": "ф╕╗ф╗гчРЖх╖▓щЕНч╜о", - "geo_configured": "хЬ░чРЖф╗гчРЖх╖▓щЕНч╜о" + "geo_configured": "хЬ░чРЖф╗гчРЖх╖▓щЕНч╜о", + "set_title": "х╖▓шо╛ч╜оф╗гчРЖ", + "set_message": "ф╕╗ф╗гчРЖх╖▓шо╛ч╜ох╣╢ф┐ЭхнШ: {proxy}", + "geo_set_title": "х╖▓шо╛ч╜охЬ░чРЖф╗гчРЖ", + "geo_set_message": "хЬ░чРЖщкМшпБф╗гчРЖх╖▓шо╛ч╜ох╣╢ф┐ЭхнШ: {proxy}", + "cleared_title": "ф╗гчРЖшо╛ч╜ох╖▓ц╕ЕщЩд", + "cleared_message": "цЙАцЬЙф╗гчРЖшо╛ч╜ох╖▓ц╕ЕщЩдх╣╢ф┐ЭхнШуАВ" }, "download": { "preparing": "цнгхЬихЗЖхдЗф╕Лш╜╜...", @@ -347,7 +354,11 @@ "zero_selected": "х╖▓щАЙцЛй 0 ф╕к", "analyze_first_tooltip": "шп╖хЕИхИЖцЮРшзЖщвС", "audio_mode_disabled": "хЬич║пщЯ│щвСцибх╝Пф╕Лф╕НхПпчФи", - "select_subtitles_first": "шп╖хЕИщАЙцЛйхнЧх╣Х" + "select_subtitles_first": "шп╖хЕИщАЙцЛйхнЧх╣Х", + "settings_tooltip": "х╜УхЙНш╖пх╛Д: {path}\nщАЯх║жщЩРхИ╢: {speed_limit}", + "speed_limit_none": "цЧа", + "open_folder_error": "цЧац│ХцЙУх╝АцЦЗф╗╢хд╣: {error}", + "time_range_set": "х╖▓шо╛ч╜охМ║щЧ┤: {section}" }, "sponsorblock": { "sponsor": "ш╡ЮхКйхХЖ", @@ -446,7 +457,8 @@ "next_check": "ф╕ЛцмбцгАцЯе: {time}", "next_check_error": "ф╕ЛцмбцгАцЯе: шобчоЧщФЩшпп", "checking": "ЁЯФД цгАцЯеф╕н...", - "check_now": "ЁЯФН члЛхН│цгАцЯецЫ┤цЦ░" + "check_now": "ЁЯФН члЛхН│цгАцЯецЫ┤цЦ░", + "current_version": "х╜УхЙН yt-dlp чЙИцЬмя╝Ъ{version}" }, "url_validation": { "empty_url": "URLф╕НшГ╜ф╕║чй║", @@ -495,7 +507,41 @@ "one_entry": "1 ф╕кф╕Лш╜╜", "redownload_confirm_title": "щЗНцЦ░ф╕Лш╜╜я╝Я", "redownload_confirm_message": "щЗНцЦ░ф╕Лш╜╜я╝Я\n\n{title}", - "redownload_started": "х╖▓х╝АхзЛф╕Лш╜╜" + "redownload_started": "х╖▓х╝АхзЛф╕Лш╜╜", + "no_url_error": "хОЖхП▓шо░х╜Хф╕нцЬкцЙ╛хИ░ URL", + "redownload_failed": "цЧац│Хх╝АхзЛщЗНцЦ░ф╕Лш╜╜: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpeg хоЙшгЕ", + "installation_message": "YTSage щЬАшжБ FFmpeg цЭехдДчРЖшзЖщвСуАВ\n\nшп╖щАЙцЛйф╕ЛщЭвчЪДхоЙшгЕщАЙщб╣я╝Ъ", + "install_button": "хоЙшгЕ FFmpeg", + "manual_guide": "цЙЛхКицМЗхНЧ", + "installation_failed": "FFmpeg хоЙшгЕщБЗхИ░щЧощвШуАВ" + }, + "ytdlp_setup": { + "required_title": "щЬАшжБшо╛ч╜о yt-dlp", + "description": "YTSage щЬАшжБ yt-dlp цЭеф╕Лш╜╜шзЖщвСуАВ

хЬих║ФчФицЬмхЬ░чЫох╜Хф╕нцЬкцЙ╛хИ░ yt-dlpуАВYTSage щЬАшжБф╕║ф╜ачЪД {os_name} ч│╗ч╗Яшо╛ч╜о yt-dlpуАВ

шп╖щАЙцЛйф╕ЛщЭвчЪДщАЙщб╣я╝Ъ", + "option_auto": "шЗкхКиф╕Лш╜╜я╝ИцОишНРя╝Й", + "option_manual": "цЙЛхКищАЙцЛйш╖пх╛Д", + "setup_button": "шо╛ч╜о yt-dlp", + "downloading": "цнгхЬиф╕Лш╜╜ yt-dlp...", + "success": "yt-dlp х╖▓цИРхКЯхоЙшгЕя╝Б", + "error": "щФЩшппя╝Ъ{error}", + "download_failed_title": "ф╕Лш╜╜хд▒ш┤е", + "download_failed_message": "цЧац│Хф╕Лш╜╜ yt-dlpя╝Ъ{error}", + "select_executable_title": "щАЙцЛй yt-dlp хПпцЙзшбМцЦЗф╗╢", + "copied_to": "yt-dlp х╖▓цИРхКЯхдНхИ╢хИ░ {path}", + "setup_error_title": "шо╛ч╜ощФЩшпп", + "copy_error": "х░Ж yt-dlp хдНхИ╢хИ░х║ФчФичЫох╜ХцЧ╢хЗ║щФЩя╝Ъ{error}", + "invalid_executable_title": "цЧацХИчЪДхПпцЙзшбМцЦЗф╗╢", + "invalid_executable_message": "цЙАщАЙцЦЗф╗╢ф╝╝ф╣Оф╕НцШпцЬЙцХИчЪД yt-dlp хПпцЙзшбМцЦЗф╗╢уАВ", + "verify_error": "щкМшпБ yt-dlp хПпцЙзшбМцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ{error}", + "setup_failed_title": "шо╛ч╜охд▒ш┤е", + "setup_failed_message": "цЧац│Хшо╛ч╜о yt-dlpуАВцЯРф║ЫхКЯшГ╜хПпшГ╜цЧац│Хцнгх╕╕х╖еф╜ЬуАВ", + "success_dialog_title": "yt-dlp шо╛ч╜о", + "success_dialog_message": "yt-dlp х╖▓цИРхКЯщЕНч╜охИ░я╝Ъ\n{path}", + "file_filter_windows": "хПпцЙзшбМцЦЗф╗╢ (*.exe)", + "file_filter_all": "цЙАцЬЙцЦЗф╗╢ (*)" }, "ffmpeg_updater": { "title": "FFmpeg чЙИцЬмцгАцЯехЩи", From 065c5c9c36c619212cba6ff1275fc87199fceae2 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:51:00 +0200 Subject: [PATCH 053/134] Add FFmpeg and yt-dlp setup translations Extended it.json, ja.json, pl.json, pt.json, ru.json, and tr.json with new translation keys for FFmpeg installation and yt-dlp setup dialogs, error messages, and status updates. Also added new UI strings for cookies, proxy, download, and history features to improve localization coverage. --- languages/it.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/ja.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/pl.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/pt.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/ru.json | 79 ++++++++++++++++++++++++++++++++++++++++++----- languages/tr.json | 70 ++++++++++++++++++++++++++++++++++++----- 6 files changed, 423 insertions(+), 42 deletions(-) diff --git a/languages/it.json b/languages/it.json index dc5f24e..36055ec 100644 --- a/languages/it.json +++ b/languages/it.json @@ -93,7 +93,8 @@ "select_subtitles": "Seleziona sottotitoli", "filter_languages_placeholder": "Filtra lingue (es: it, en)...", "no_subtitles_available": "Nessun sottotitolo disponibile", - "matching": "corrispondenti" + "matching": "corrispondenti", + "ytdlp_log_title": "Registro yt-dlp" }, "tabs": { "cookies": "Accedi con i cookie", @@ -124,7 +125,10 @@ "browser_selected_title": "Cookie del Browser Applicati", "browser_applied_message": "I cookie del browser verranno estratti da: {browser}", "cleared_title": "Cookie Cancellati", - "cleared_message": "Le impostazioni dei cookie sono state cancellate" + "cleared_message": "Le impostazioni dei cookie sono state cancellate", + "active_browser": "тЬУ Attivi: cookie del browser ({browser})", + "active_file": "тЬУ Attivo: file cookie ({file})", + "none_active": "тЧЛ Nessun cookie attivo" }, "custom_command": { "help_text": "Inserisci un comando yt-dlp personalizzato qui sotto. L'URL corrente verr├а aggiunto automaticamente.

Per l'elenco completo delle opzioni ed esempi di utilizzo clicca qui per vedere la documentazione ufficiale yt-dlp.

Nota: Il percorso di download e il modello del nome file sono gestiti automaticamente.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Comando completo: {command}", "command_success": "тЬЕ Comando personalizzato eseguito con successo!", "command_failed": "тЭМ Comando fallito con codice di uscita {code}", - "command_error": "тЭМ Errore nell'esecuzione del comando personalizzato: {error}" + "command_error": "тЭМ Errore nell'esecuzione del comando personalizzato: {error}", + "error_no_url": "тЭМ Errore: nessun URL fornito. Inserisci un URL nella finestra principale.", + "error_no_command": "тЭМ Errore: nessun comando fornito. Inserisci gli argomenti di yt-dlp.", + "executing": "ЁЯЪА Esecuzione del comando yt-dlp personalizzato", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argomenti: {command}", + "download_path_label": "ЁЯУБ Percorso di download: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Configura le impostazioni proxy per i download. Lascia vuoto per connessione diretta.", @@ -159,7 +170,15 @@ "invalid_main_url": "Formato URL proxy principale non valido", "invalid_geo_url": "Formato URL proxy geo non valido", "main_configured": "Proxy principale configurato", - "geo_configured": "Proxy geo configurato" + "geo_configured": "Proxy geo configurato", + "set_title": "Proxy impostato", + "set_message": "Proxy principale impostato e salvato: {proxy}", + "geo_set_title": "Proxy geo impostato", + "geo_set_message": "Proxy di verifica geo impostato e salvato: {proxy}", + "cleared_title": "Impostazioni proxy cancellate", + "cleared_message": "Tutte le impostazioni proxy sono state cancellate e salvate.", + "saved_main": "Proxy principale salvato: {proxy}", + "saved_geo": "Proxy geo salvato: {proxy}" }, "download": { "preparing": "Preparazione download...", @@ -347,7 +366,11 @@ "zero_selected": "0 selezionati", "analyze_first_tooltip": "Si prega di analizzare prima il video", "audio_mode_disabled": "Non disponibile in modalit├а solo audio", - "select_subtitles_first": "Si prega di selezionare prima i sottotitoli" + "select_subtitles_first": "Si prega di selezionare prima i sottotitoli", + "settings_tooltip": "Percorso corrente: {path}\nLimite velocit├а: {speed_limit}", + "speed_limit_none": "Nessuno", + "open_folder_error": "Impossibile aprire la cartella: {error}", + "time_range_set": "Sezione impostata: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +469,8 @@ "next_check": "Prossimo controllo: {time}", "next_check_error": "Prossimo controllo: Errore di calcolo", "checking": "ЁЯФД Controllo...", - "check_now": "ЁЯФН Controlla aggiornamenti ora" + "check_now": "ЁЯФН Controlla aggiornamenti ora", + "current_version": "Versione corrente di yt-dlp: {version}" }, "url_validation": { "empty_url": "L'URL non pu├▓ essere vuoto", @@ -477,6 +501,7 @@ "clear_confirm_message": "Sei sicuro? Questa azione non pu├▓ essere annullata.", "no_history": "Nessuna cronologia ancora", "no_history_description": "I tuoi download appariranno qui", + "loading": "Caricamento cronologia...", "search_placeholder": "Cerca...", "open_location": "Apri Posizione", "redownload": "Scarica di Nuovo", @@ -495,7 +520,47 @@ "one_entry": "1 download", "redownload_confirm_title": "Scaricare di Nuovo?", "redownload_confirm_message": "Scaricare di nuovo?\n\n{title}", - "redownload_started": "Download avviato" + "redownload_started": "Download avviato", + "no_url_error": "Nessun URL trovato nella voce della cronologia", + "redownload_failed": "Impossibile avviare il download di nuovo: {error}" + }, + "ffmpeg": { + "installation_title": "Installazione di FFmpeg", + "installation_message": "YTSage richiede FFmpeg per elaborare i video.\n\nScegli un'opzione di installazione qui sotto:", + "install_button": "Installa FFmpeg", + "manual_guide": "Guida manuale", + "installation_failed": "L'installazione di FFmpeg ha riscontrato un problema.", + "already_installed": "FFmpeg ├и gi├а installato!", + "installation_complete": "Installazione completata. Puoi chiudere questo dialogo e continuare a usare YTSage.", + "installing": "Installazione di FFmpeg... Attendere", + "install_success": "FFmpeg ├и stato installato con successo!", + "installation_complete_close": "Installazione completata. Ora puoi chiudere questo dialogo e continuare a usare YTSage.", + "try_manual": "Prova invece a usare la guida di installazione manuale." + }, + "ytdlp_setup": { + "required_title": "Configurazione di yt-dlp richiesta", + "description": "YTSage richiede yt-dlp per scaricare video.

yt-dlp non ├и stato trovato nella directory locale dell'app. YTSage deve configurare yt-dlp per il tuo sistema {os_name}.

Scegli un'opzione qui sotto:", + "option_auto": "Scarica automaticamente (consigliato)", + "option_manual": "Seleziona il percorso manualmente", + "setup_button": "Configura yt-dlp", + "downloading": "Download di yt-dlp...", + "success": "yt-dlp ├и stato installato correttamente!", + "error": "Errore: {error}", + "download_failed_title": "Download non riuscito", + "download_failed_message": "Impossibile scaricare yt-dlp: {error}", + "select_executable_title": "Seleziona l'eseguibile yt-dlp", + "copied_to": "yt-dlp copiato correttamente in {path}", + "setup_error_title": "Errore di configurazione", + "copy_error": "Errore durante la copia di yt-dlp nella directory dell'app: {error}", + "invalid_executable_title": "Eseguibile non valido", + "invalid_executable_message": "Il file selezionato non sembra essere un eseguibile yt-dlp valido.", + "verify_error": "Errore durante la verifica dell'eseguibile yt-dlp: {error}", + "setup_failed_title": "Configurazione non riuscita", + "setup_failed_message": "Impossibile configurare yt-dlp. Alcune funzionalit├а potrebbero non funzionare correttamente.", + "success_dialog_title": "Configurazione di yt-dlp", + "success_dialog_message": "yt-dlp ├и stato configurato correttamente in:\n{path}", + "file_filter_windows": "File eseguibili (*.exe)", + "file_filter_all": "Tutti i file (*)" }, "ffmpeg_updater": { "title": "Controllo Versione FFmpeg", diff --git a/languages/ja.json b/languages/ja.json index 784089b..dc2338b 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -93,7 +93,8 @@ "select_subtitles": "хнЧх╣ХуВТщБ╕цКЮ", "filter_languages_placeholder": "шиАшкЮуБзуГХуВгуГлуВ┐ (ф╛Л: ja, en)...", "no_subtitles_available": "хИйчФихПпшГ╜уБкхнЧх╣ХуБМуБВуВКуБ╛уБЫуВУ", - "matching": "ф╕АшЗ┤" + "matching": "ф╕АшЗ┤", + "ytdlp_log_title": "yt-dlpуГнуВ░" }, "tabs": { "cookies": "CookieуБзуГнуВ░уВдуГ│", @@ -124,7 +125,10 @@ "browser_selected_title": "уГЦуГйуВжуВ╢CookieуБМщБйчФиуБХуВМуБ╛уБЧуБЯ", "browser_applied_message": "уГЦуГйуВжуВ╢CookieуБМцК╜хЗ║уБХуВМуБ╛уБЩ: {browser}", "cleared_title": "CookieуБМуВпуГкуВвуБХуВМуБ╛уБЧуБЯ", - "cleared_message": "CookieшинхоЪуБМуВпуГкуВвуБХуВМуБ╛уБЧуБЯ" + "cleared_message": "CookieшинхоЪуБМуВпуГкуВвуБХуВМуБ╛уБЧуБЯ", + "active_browser": "тЬУ цЬЙхК╣: уГЦуГйуВжуВ╢уГ╝уБоCookie ({browser})", + "active_file": "тЬУ цЬЙхК╣: CookieуГХуВбуВдуГл ({file})", + "none_active": "тЧЛ цЬЙхК╣уБкCookieуБпуБВуВКуБ╛уБЫуВУ" }, "custom_command": { "help_text": "ф╗еф╕ЛуБлуВлуВ╣уВ┐уГаyt-dlpуВ│уГЮуГ│уГЙуВТхЕехКЫуБЧуБжуБПуБауБХуБДуАВчП╛хЬиуБоURLуБпшЗкхЛХчЪДуБлш┐╜хКауБХуВМуБ╛уБЩуАВ

уВкуГЧуВ╖уГзуГ│уБохоМхЕиуБкуГкуВ╣уГИуБиф╜┐чФиф╛ЛуБлуБдуБДуБжуБпуАБуБУуБбуВЙуВТуВпуГкуГГуВпуБЧуБжyt-dlpхЕмх╝ПуГЙуВнуГеуГбуГ│уГИуВТхПВчЕзуБЧуБжуБПуБауБХуБДуАВ

ц│и: уГАуВжуГ│уГнуГ╝уГЙуГСуВ╣уБиуГХуВбуВдуГлхРНуГЖуГ│уГЧуГмуГ╝уГИуБпшЗкхЛХчЪДуБлхЗжчРЖуБХуВМуБ╛уБЩуАВ", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз хоМхЕиуБкуВ│уГЮуГ│уГЙ: {command}", "command_success": "тЬЕ уВлуВ╣уВ┐уГауВ│уГЮуГ│уГЙуБМцнгх╕╕уБлхоЯшбМуБХуВМуБ╛уБЧуБЯя╝Б", "command_failed": "тЭМ уВ│уГЮуГ│уГЙуБМч╡Вф║ЖуВ│уГ╝уГЙ{code}уБзхд▒цХЧуБЧуБ╛уБЧуБЯ", - "command_error": "тЭМ уВлуВ╣уВ┐уГауВ│уГЮуГ│уГЙуБохоЯшбМуВиуГйуГ╝: {error}" + "command_error": "тЭМ уВлуВ╣уВ┐уГауВ│уГЮуГ│уГЙуБохоЯшбМуВиуГйуГ╝: {error}", + "error_no_url": "тЭМ уВиуГйуГ╝: URL уБМцМЗхоЪуБХуВМуБжуБДуБ╛уБЫуВУуАВуГбуВдуГ│уВжуВгуГ│уГЙуВжуБз URL уВТхЕехКЫуБЧуБжуБПуБауБХуБДуАВ", + "error_no_command": "тЭМ уВиуГйуГ╝: уВ│уГЮуГ│уГЙуБМцМЗхоЪуБХуВМуБжуБДуБ╛уБЫуВУуАВyt-dlp уБох╝ХцХ░уВТхЕехКЫуБЧуБжуБПуБауБХуБДуАВ", + "executing": "ЁЯЪА уВлуВ╣уВ┐уГа yt-dlp уВ│уГЮуГ│уГЙуВТхоЯшбМф╕н", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П х╝ХцХ░: {command}", + "download_path_label": "ЁЯУБ уГАуВжуГ│уГнуГ╝уГЙхЕИ: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "уГАуВжуГ│уГнуГ╝уГЙчФиуБоуГЧуГнуВнуВ╖шинхоЪуВТцзЛцИРуБЧуБ╛уБЩуАВчЫ┤цОецОеч╢ЪуБоха┤хРИуБпчй║чЩ╜уБоуБ╛уБ╛уБлуБЧуБжуБПуБауБХуБДуАВ", @@ -159,7 +170,15 @@ "invalid_main_url": "уГбуВдуГ│уГЧуГнуВнуВ╖уБоURLх╜вх╝ПуБМчДбхК╣уБзуБЩ", "invalid_geo_url": "хЬ░хЯЯуГЧуГнуВнуВ╖уБоURLх╜вх╝ПуБМчДбхК╣уБзуБЩ", "main_configured": "уГбуВдуГ│уГЧуГнуВнуВ╖уБМшинхоЪуБХуВМуБ╛уБЧуБЯ", - "geo_configured": "хЬ░хЯЯуГЧуГнуВнуВ╖уБМшинхоЪуБХуВМуБ╛уБЧуБЯ" + "geo_configured": "хЬ░хЯЯуГЧуГнуВнуВ╖уБМшинхоЪуБХуВМуБ╛уБЧуБЯ", + "set_title": "уГЧуГнуВнуВ╖уВТшинхоЪуБЧуБ╛уБЧуБЯ", + "set_message": "уГбуВдуГ│уГЧуГнуВнуВ╖уВТшинхоЪуБЧуБжф┐ЭхнШуБЧуБ╛уБЧуБЯ: {proxy}", + "geo_set_title": "уВ╕уВкуГЧуГнуВнуВ╖уВТшинхоЪуБЧуБ╛уБЧуБЯ", + "geo_set_message": "уВ╕уВкцдЬши╝уГЧуГнуВнуВ╖уВТшинхоЪуБЧуБжф┐ЭхнШуБЧуБ╛уБЧуБЯ: {proxy}", + "cleared_title": "уГЧуГнуВнуВ╖шинхоЪуВТуВпуГкуВвуБЧуБ╛уБЧуБЯ", + "cleared_message": "уБЩуБ╣уБжуБоуГЧуГнуВнуВ╖шинхоЪуВТуВпуГкуВвуБЧуБжф┐ЭхнШуБЧуБ╛уБЧуБЯуАВ", + "saved_main": "ф┐ЭхнШуБХуВМуБЯуГбуВдуГ│уГЧуГнуВнуВ╖: {proxy}", + "saved_geo": "ф┐ЭхнШуБХуВМуБЯуВ╕уВкуГЧуГнуВнуВ╖: {proxy}" }, "download": { "preparing": "уГАуВжуГ│уГнуГ╝уГЙуВТц║ЦхВЩф╕н...", @@ -347,7 +366,11 @@ "zero_selected": "0хАЛщБ╕цКЮ", "analyze_first_tooltip": "цЬАхИЭуБлхЛХчФ╗уВТхИЖцЮРуБЧуБжуБПуБауБХуБД", "audio_mode_disabled": "щЯ│хг░уБоуБ┐уГвуГ╝уГЙуБзуБпхИйчФиуБзуБНуБ╛уБЫуВУ", - "select_subtitles_first": "цЬАхИЭуБлхнЧх╣ХуВТщБ╕цКЮуБЧуБжуБПуБауБХуБД" + "select_subtitles_first": "цЬАхИЭуБлхнЧх╣ХуВТщБ╕цКЮуБЧуБжуБПуБауБХуБД", + "settings_tooltip": "чП╛хЬиуБоуГСуВ╣: {path}\nщАЯх║жхИ╢щЩР: {speed_limit}", + "speed_limit_none": "уБкуБЧ", + "open_folder_error": "уГХуВйуГлуГАуГ╝уВТщЦЛуБСуБ╛уБЫуВУуБзуБЧуБЯ: {error}", + "time_range_set": "уВ╗уВпуВ╖уГзуГ│шинхоЪ: {section}" }, "sponsorblock": { "sponsor": "уВ╣уГЭуГ│уВ╡уГ╝", @@ -446,7 +469,8 @@ "next_check": "цмбхЫЮчв║шкН: {time}", "next_check_error": "цмбхЫЮчв║шкН: шиИчоЧуВиуГйуГ╝", "checking": "ЁЯФД чв║шкНф╕н...", - "check_now": "ЁЯФН ф╗КуБЩуБРцЫ┤цЦ░уВТчв║шкН" + "check_now": "ЁЯФН ф╗КуБЩуБРцЫ┤цЦ░уВТчв║шкН", + "current_version": "чП╛хЬиуБоyt-dlpуГРуГ╝уВ╕уГзуГ│: {version}" }, "url_validation": { "empty_url": "URLуВТчй║уБлуБЩуВЛуБУуБиуБпуБзуБНуБ╛уБЫуВУ", @@ -477,6 +501,7 @@ "clear_confirm_message": "цЬмх╜УуБлуВИуВНуБЧуБДуБзуБЩуБЛя╝ЯуБУуБоцУНф╜ЬуБпхПЦуВКц╢ИуБЫуБ╛уБЫуВУуАВ", "no_history": "уБ╛уБах▒ецн┤уБМуБВуВКуБ╛уБЫуВУ", "no_history_description": "уГАуВжуГ│уГнуГ╝уГЙуБМуБУуБУуБлшбичд║уБХуВМуБ╛уБЩ", + "loading": "х▒ецн┤уВТшкнуБ┐ш╛╝уБ┐ф╕н...", "search_placeholder": "цдЬч┤в...", "open_location": "ха┤цЙАуВТщЦЛуБП", "redownload": "хЖНуГАуВжуГ│уГнуГ╝уГЙ", @@ -495,7 +520,47 @@ "one_entry": "1 ф╗╢", "redownload_confirm_title": "хЖНуГАуВжуГ│уГнуГ╝уГЙя╝Я", "redownload_confirm_message": "хЖНуГАуВжуГ│уГнуГ╝уГЙуБЧуБ╛уБЩуБЛя╝Я\n\n{title}", - "redownload_started": "уГАуВжуГ│уГнуГ╝уГЙщЦЛхзЛ" + "redownload_started": "уГАуВжуГ│уГнуГ╝уГЙщЦЛхзЛ", + "no_url_error": "х▒ецн┤уВиуГ│уГИуГкуБлURLуБМуБВуВКуБ╛уБЫуВУ", + "redownload_failed": "хЖНуГАуВжуГ│уГнуГ╝уГЙуБощЦЛхзЛуБлхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpegуБоуВдуГ│уВ╣уГИуГ╝уГл", + "installation_message": "YTSageуБпхЛХчФ╗хЗжчРЖуБлFFmpegуБМх┐ЕшжБуБзуБЩуАВ\n\nф╗еф╕ЛуБоуВдуГ│уВ╣уГИуГ╝уГлуВкуГЧуВ╖уГзуГ│уВТщБ╕цКЮуБЧуБжуБПуБауБХуБД:", + "install_button": "FFmpegуВТуВдуГ│уВ╣уГИуГ╝уГл", + "manual_guide": "цЙЛхЛХуВмуВдуГЙ", + "installation_failed": "FFmpegуБоуВдуГ│уВ╣уГИуГ╝уГлф╕нуБлхХПщбМуБМчЩ║чФЯуБЧуБ╛уБЧуБЯуАВ", + "already_installed": "FFmpegуБпуБЩуБзуБлуВдуГ│уВ╣уГИуГ╝уГлуБХуВМуБжуБДуБ╛уБЩя╝Б", + "installation_complete": "уВдуГ│уВ╣уГИуГ╝уГлхоМф║ЖуАВуБУуБоуГАуВдуВвуГнуВ░уВТщЦЙуБШуБжYTSageуВТч╢ЪуБСуБжф╜┐чФиуБзуБНуБ╛уБЩуАВ", + "installing": "FFmpegуВТуВдуГ│уВ╣уГИуГ╝уГлф╕н... уБКх╛ЕуБбуБПуБауБХуБД", + "install_success": "FFmpegуБМцнгх╕╕уБлуВдуГ│уВ╣уГИуГ╝уГлуБХуВМуБ╛уБЧуБЯя╝Б", + "installation_complete_close": "уВдуГ│уВ╣уГИуГ╝уГлхоМф║ЖуАВф╗КуБЩуБРуБУуБоуГАуВдуВвуГнуВ░уВТщЦЙуБШуБжYTSageуВТч╢ЪуБСуБжф╜┐чФиуБзуБНуБ╛уБЩуАВ", + "try_manual": "ф╗гуВПуВКуБлцЙЛхЛХуВдуГ│уВ╣уГИуГ╝уГлуВмуВдуГЙуВТуБКшйжуБЧуБПуБауБХуБДуАВ" + }, + "ytdlp_setup": { + "required_title": "yt-dlpуБоуВ╗уГГуГИуВвуГГуГЧуБМх┐ЕшжБуБзуБЩ", + "description": "YTSageуБпхЛХчФ╗уБоуГАуВжуГ│уГнуГ╝уГЙуБлyt-dlpуБМх┐ЕшжБуБзуБЩуАВ

уВвуГЧуГкуБоуГнуГ╝уВлуГлуГЗуВгуГмуВпуГИуГкуБлyt-dlpуБМшжЛуБдуБЛуВКуБ╛уБЫуВУуБзуБЧуБЯуАВ{os_name}уВ╖уВ╣уГЖуГачФиуБлyt-dlpуВТуВ╗уГГуГИуВвуГГуГЧуБЩуВЛх┐ЕшжБуБМуБВуВКуБ╛уБЩуАВ

ф╗еф╕ЛуБоуВкуГЧуВ╖уГзуГ│уВТщБ╕цКЮуБЧуБжуБПуБауБХуБД:", + "option_auto": "шЗкхЛХуБзуГАуВжуГ│уГнуГ╝уГЙя╝ИцОихеия╝Й", + "option_manual": "уГСуВ╣уВТцЙЛхЛХуБзщБ╕цКЮ", + "setup_button": "yt-dlpуВТуВ╗уГГуГИуВвуГГуГЧ", + "downloading": "yt-dlpуВТуГАуВжуГ│уГнуГ╝уГЙф╕н...", + "success": "yt-dlpуБМцнгх╕╕уБлуВдуГ│уВ╣уГИуГ╝уГлуБХуВМуБ╛уБЧуБЯя╝Б", + "error": "уВиуГйуГ╝: {error}", + "download_failed_title": "уГАуВжуГ│уГнуГ╝уГЙхд▒цХЧ", + "download_failed_message": "yt-dlpуБоуГАуВжуГ│уГнуГ╝уГЙуБлхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", + "select_executable_title": "yt-dlpхоЯшбМуГХуВбуВдуГлуВТщБ╕цКЮ", + "copied_to": "yt-dlpуВТ{path}уБлуВ│уГФуГ╝уБЧуБ╛уБЧуБЯ", + "setup_error_title": "уВ╗уГГуГИуВвуГГуГЧуВиуГйуГ╝", + "copy_error": "yt-dlpуВТуВвуГЧуГкуГЗуВгуГмуВпуГИуГкуБлуВ│уГФуГ╝уБзуБНуБ╛уБЫуВУ: {error}", + "invalid_executable_title": "чДбхК╣уБкхоЯшбМуГХуВбуВдуГл", + "invalid_executable_message": "щБ╕цКЮуБХуВМуБЯуГХуВбуВдуГлуБпцЬЙхК╣уБкyt-dlpхоЯшбМуГХуВбуВдуГлуБзуБпуБВуВКуБ╛уБЫуВУуАВ", + "verify_error": "yt-dlpхоЯшбМуГХуВбуВдуГлуБоцдЬши╝уБлхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", + "setup_failed_title": "уВ╗уГГуГИуВвуГГуГЧхд▒цХЧ", + "setup_failed_message": "yt-dlpуБоуВ╗уГГуГИуВвуГГуГЧуБлхд▒цХЧуБЧуБ╛уБЧуБЯуАВуБДуБПуБдуБЛуБоцйЯшГ╜уБМцнгуБЧуБПхЛХф╜ЬуБЧуБкуБДхПпшГ╜цАзуБМуБВуВКуБ╛уБЩуАВ", + "success_dialog_title": "yt-dlpуВ╗уГГуГИуВвуГГуГЧ", + "success_dialog_message": "yt-dlpуБошинхоЪуБлцИРхКЯуБЧуБ╛уБЧуБЯ:\n{path}", + "file_filter_windows": "хоЯшбМуГХуВбуВдуГл (*.exe)", + "file_filter_all": "уБЩуБ╣уБжуБоуГХуВбуВдуГл (*)" }, "ffmpeg_updater": { "title": "FFmpegуГРуГ╝уВ╕уГзуГ│уГБуВзуГГуВлуГ╝", diff --git a/languages/pl.json b/languages/pl.json index 75744bf..ca0189b 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -93,7 +93,8 @@ "select_subtitles": "Wybierz napisy", "filter_languages_placeholder": "Filtruj j─Щzyki (np: pl, en)...", "no_subtitles_available": "Brak dost─Щpnych napis├│w", - "matching": "dopasowuj─Еce" + "matching": "dopasowuj─Еce", + "ytdlp_log_title": "Log yt-dlp" }, "tabs": { "cookies": "Zaloguj za pomoc─Е ciasteczek", @@ -124,7 +125,10 @@ "browser_selected_title": "Zastosowano ciasteczka przegl─Еdarki", "browser_applied_message": "Ciasteczka przegl─Еdarki zostan─Е wyodr─Щbnione z: {browser}", "cleared_title": "Wyczyszczono ciasteczka", - "cleared_message": "Ustawienia ciasteczek zosta┼Вy wyczyszczone" + "cleared_message": "Ustawienia ciasteczek zosta┼Вy wyczyszczone", + "active_browser": "тЬУ Aktywne: ciasteczka przegl─Еdarki ({browser})", + "active_file": "тЬУ Aktywny: plik ciasteczek ({file})", + "none_active": "тЧЛ Brak aktywnych ciasteczek" }, "custom_command": { "help_text": "Wprowad┼║ niestandardowe polecenie yt-dlp poni┼╝ej. Aktualny URL zostanie automatycznie dodany.

Aby uzyska─З pe┼Вn─Е list─Щ opcji i przyk┼Вad├│w u┼╝ycia kliknij tutaj, aby zobaczy─З oficjaln─Е dokumentacj─Щ yt-dlp.

Uwaga: ┼Ъcie┼╝ka pobierania i szablon nazwy pliku s─Е obs┼Вugiwane automatycznie.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Pe┼Вne polecenie: {command}", "command_success": "тЬЕ Polecenie niestandardowe wykonane pomy┼Ыlnie!", "command_failed": "тЭМ Polecenie nie powiod┼Вo si─Щ z kodem wyj┼Ыcia {code}", - "command_error": "тЭМ B┼В─Еd wykonywania polecenia niestandardowego: {error}" + "command_error": "тЭМ B┼В─Еd wykonywania polecenia niestandardowego: {error}", + "error_no_url": "тЭМ B┼В─Еd: nie podano URL. Wprowad┼║ URL w g┼В├│wnym oknie.", + "error_no_command": "тЭМ B┼В─Еd: nie podano polecenia. Wprowad┼║ argumenty yt-dlp.", + "executing": "ЁЯЪА Uruchamianie niestandardowego polecenia yt-dlp", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argumenty: {command}", + "download_path_label": "ЁЯУБ ┼Ъcie┼╝ka pobierania: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Skonfiguruj ustawienia proxy dla pobierania. Pozostaw puste dla bezpo┼Ыredniego po┼В─Еczenia.", @@ -159,7 +170,15 @@ "invalid_main_url": "Nieprawid┼Вowy format URL g┼В├│wnego proxy", "invalid_geo_url": "Nieprawid┼Вowy format URL proxy geograficznego", "main_configured": "G┼В├│wny proxy skonfigurowany", - "geo_configured": "Proxy geograficzny skonfigurowany" + "geo_configured": "Proxy geograficzny skonfigurowany", + "set_title": "Ustawiono proxy", + "set_message": "G┼В├│wne proxy ustawione i zapisane: {proxy}", + "geo_set_title": "Ustawiono proxy geo", + "geo_set_message": "Proxy weryfikacji geo ustawione i zapisane: {proxy}", + "cleared_title": "Wyczyszczono ustawienia proxy", + "cleared_message": "Wszystkie ustawienia proxy zosta┼Вy wyczyszczone i zapisane.", + "saved_main": "Zapisany g┼В├│wny proxy: {proxy}", + "saved_geo": "Zapisany proxy geo: {proxy}" }, "download": { "preparing": "Przygotowywanie pobierania...", @@ -347,7 +366,11 @@ "zero_selected": "0 wybranych", "analyze_first_tooltip": "Najpierw przeanalizuj wideo", "audio_mode_disabled": "Niedost─Щpne w trybie tylko audio", - "select_subtitles_first": "Najpierw wybierz napisy" + "select_subtitles_first": "Najpierw wybierz napisy", + "settings_tooltip": "Bie┼╝─Еca ┼Ыcie┼╝ka: {path}\nLimit pr─Щdko┼Ыci: {speed_limit}", + "speed_limit_none": "Brak", + "open_folder_error": "Nie mo┼╝na otworzy─З folderu: {error}", + "time_range_set": "Ustawiono sekcj─Щ: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +469,8 @@ "next_check": "Nast─Щpne sprawdzenie: {time}", "next_check_error": "Nast─Щpne sprawdzenie: B┼В─Еd obliczania", "checking": "ЁЯФД Sprawdzanie...", - "check_now": "ЁЯФН Sprawd┼║ aktualizacje teraz" + "check_now": "ЁЯФН Sprawd┼║ aktualizacje teraz", + "current_version": "Bie┼╝─Еca wersja yt-dlp: {version}" }, "url_validation": { "empty_url": "URL nie mo┼╝e by─З pusty", @@ -477,6 +501,7 @@ "clear_confirm_message": "Czy jeste┼Ы pewien? Nie mo┼╝na tego cofn─Е─З.", "no_history": "Brak historii", "no_history_description": "Twoje pobrane pliki pojawi─Е si─Щ tutaj", + "loading": "┼Бadowanie historii...", "search_placeholder": "Szukaj...", "open_location": "Otw├│rz Lokalizacj─Щ", "redownload": "Pobierz Ponownie", @@ -495,7 +520,47 @@ "one_entry": "1 pobranie", "redownload_confirm_title": "Pobra─З Ponownie?", "redownload_confirm_message": "Pobra─З ponownie?\n\n{title}", - "redownload_started": "Rozpocz─Щto pobieranie" + "redownload_started": "Rozpocz─Щto pobieranie", + "no_url_error": "Nie znaleziono URL w wpisie historii", + "redownload_failed": "Nie uda┼Вo si─Щ rozpocz─Е─З ponownego pobierania: {error}" + }, + "ffmpeg": { + "installation_title": "Instalacja FFmpeg", + "installation_message": "YTSage wymaga FFmpeg do przetwarzania wideo.\n\nWybierz opcj─Щ instalacji poni┼╝ej:", + "install_button": "Zainstaluj FFmpeg", + "manual_guide": "Instrukcja r─Щczna", + "installation_failed": "Instalacja FFmpeg napotka┼Вa problem.", + "already_installed": "FFmpeg jest ju┼╝ zainstalowany!", + "installation_complete": "Instalacja zako┼Дczona. Mo┼╝esz zamkn─Е─З to okno i kontynuowa─З u┼╝ywanie YTSage.", + "installing": "Instalowanie FFmpeg... Prosz─Щ czeka─З", + "install_success": "FFmpeg zosta┼В pomy┼Ыlnie zainstalowany!", + "installation_complete_close": "Instalacja zako┼Дczona. Mo┼╝esz teraz zamkn─Е─З to okno i kontynuowa─З u┼╝ywanie YTSage.", + "try_manual": "Spr├│buj u┼╝y─З instrukcji instalacji r─Щcznej." + }, + "ytdlp_setup": { + "required_title": "Wymagana konfiguracja yt-dlp", + "description": "YTSage wymaga yt-dlp do pobierania wideo.

Nie znaleziono yt-dlp w lokalnym katalogu aplikacji. YTSage musi skonfigurowa─З yt-dlp dla systemu {os_name}.

Wybierz opcj─Щ poni┼╝ej:", + "option_auto": "Pobierz automatycznie (zalecane)", + "option_manual": "Wybierz ┼Ыcie┼╝k─Щ r─Щcznie", + "setup_button": "Skonfiguruj yt-dlp", + "downloading": "Pobieranie yt-dlp...", + "success": "yt-dlp zosta┼В pomy┼Ыlnie zainstalowany!", + "error": "B┼В─Еd: {error}", + "download_failed_title": "Pobieranie nieudane", + "download_failed_message": "Nie uda┼Вo si─Щ pobra─З yt-dlp: {error}", + "select_executable_title": "Wybierz plik wykonywalny yt-dlp", + "copied_to": "yt-dlp skopiowano pomy┼Ыlnie do {path}", + "setup_error_title": "B┼В─Еd konfiguracji", + "copy_error": "B┼В─Еd podczas kopiowania yt-dlp do katalogu aplikacji: {error}", + "invalid_executable_title": "Nieprawid┼Вowy plik wykonywalny", + "invalid_executable_message": "Wybrany plik nie wygl─Еda na prawid┼Вowy plik wykonywalny yt-dlp.", + "verify_error": "B┼В─Еd podczas weryfikacji pliku yt-dlp: {error}", + "setup_failed_title": "Konfiguracja nieudana", + "setup_failed_message": "Nie uda┼Вo si─Щ skonfigurowa─З yt-dlp. Niekt├│re funkcje mog─Е dzia┼Вa─З nieprawid┼Вowo.", + "success_dialog_title": "Konfiguracja yt-dlp", + "success_dialog_message": "yt-dlp zosta┼В pomy┼Ыlnie skonfigurowany w:\n{path}", + "file_filter_windows": "Pliki wykonywalne (*.exe)", + "file_filter_all": "Wszystkie pliki (*)" }, "ffmpeg_updater": { "title": "Sprawdzanie Wersji FFmpeg", diff --git a/languages/pt.json b/languages/pt.json index 16aa7c4..df2f491 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -93,7 +93,8 @@ "select_subtitles": "Selecionar Legendas", "filter_languages_placeholder": "Filtrar idiomas (ex., en, pt)...", "no_subtitles_available": "Nenhuma legenda dispon├нvel", - "matching": "correspondendo" + "matching": "correspondendo", + "ytdlp_log_title": "Log do yt-dlp" }, "tabs": { "cookies": "Entrar com Cookies", @@ -124,7 +125,10 @@ "browser_selected_title": "Cookies do Navegador Aplicados", "browser_applied_message": "Os cookies do navegador ser├гo extra├нdos de: {browser}", "cleared_title": "Cookies Limpos", - "cleared_message": "As configura├з├╡es de cookies foram limpas" + "cleared_message": "As configura├з├╡es de cookies foram limpas", + "active_browser": "тЬУ Ativos: cookies do navegador ({browser})", + "active_file": "тЬУ Ativo: arquivo de cookies ({file})", + "none_active": "тЧЛ Nenhum cookie ativo" }, "custom_command": { "help_text": "Digite seu comando yt-dlp personalizado abaixo. A URL atual ser├б anexada automaticamente.

Para a lista completa de op├з├╡es e exemplos de uso, clique aqui para ver a documenta├з├гo oficial do yt-dlp.

Nota: O caminho de download e modelo de nome de arquivo ser├гo tratados automaticamente.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Comando completo: {command}", "command_success": "тЬЕ Comando personalizado executado com sucesso!", "command_failed": "тЭМ Comando falhou com c├│digo de sa├нda {code}", - "command_error": "тЭМ Erro ao executar comando personalizado: {error}" + "command_error": "тЭМ Erro ao executar comando personalizado: {error}", + "error_no_url": "тЭМ Erro: nenhuma URL fornecida. Insira uma URL na janela principal.", + "error_no_command": "тЭМ Erro: nenhum comando fornecido. Insira os argumentos do yt-dlp.", + "executing": "ЁЯЪА Executando comando personalizado do yt-dlp", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Argumentos: {command}", + "download_path_label": "ЁЯУБ Caminho de download: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "Configure as defini├з├╡es de proxy para download. Deixe vazio para usar conex├гo direta.", @@ -159,7 +170,15 @@ "invalid_main_url": "Formato de URL do proxy principal inv├бlido", "invalid_geo_url": "Formato de URL do proxy geogr├бfico inv├бlido", "main_configured": "Proxy principal configurado", - "geo_configured": "Proxy geogr├бfico configurado" + "geo_configured": "Proxy geogr├бfico configurado", + "set_title": "Proxy definido", + "set_message": "Proxy principal definido e salvo: {proxy}", + "geo_set_title": "Proxy geo definido", + "geo_set_message": "Proxy de verifica├з├гo geo definido e salvo: {proxy}", + "cleared_title": "Configura├з├╡es de proxy limpas", + "cleared_message": "Todas as configura├з├╡es de proxy foram limpas e salvas.", + "saved_main": "Proxy principal salvo: {proxy}", + "saved_geo": "Proxy geo salvo: {proxy}" }, "download": { "preparing": "Preparando download...", @@ -347,7 +366,11 @@ "zero_selected": "0 selecionados", "analyze_first_tooltip": "Por favor analise o v├нdeo primeiro", "audio_mode_disabled": "N├гo dispon├нvel no modo apenas ├бudio", - "select_subtitles_first": "Por favor selecione legendas primeiro" + "select_subtitles_first": "Por favor selecione legendas primeiro", + "settings_tooltip": "Caminho atual: {path}\nLimite de velocidade: {speed_limit}", + "speed_limit_none": "Nenhum", + "open_folder_error": "N├гo foi poss├нvel abrir a pasta: {error}", + "time_range_set": "Se├з├гo definida: {section}" }, "sponsorblock": { "sponsor": "Patrocinador", @@ -446,7 +469,8 @@ "next_check": "Pr├│xima verifica├з├гo: {time}", "next_check_error": "Pr├│xima verifica├з├гo: Erro ao calcular", "checking": "ЁЯФД Verificando...", - "check_now": "ЁЯФН Verificar atualiza├з├╡es agora" + "check_now": "ЁЯФН Verificar atualiza├з├╡es agora", + "current_version": "Vers├гo atual do yt-dlp: {version}" }, "url_validation": { "empty_url": "O URL n├гo pode estar vazio", @@ -477,6 +501,7 @@ "clear_confirm_message": "Tem certeza? Isto n├гo pode ser desfeito.", "no_history": "Nenhum hist├│rico ainda", "no_history_description": "Seus downloads aparecer├гo aqui", + "loading": "Carregando hist├│rico...", "search_placeholder": "Pesquisar...", "open_location": "Abrir Local", "redownload": "Baixar Novamente", @@ -495,7 +520,47 @@ "one_entry": "1 download", "redownload_confirm_title": "Baixar Novamente?", "redownload_confirm_message": "Baixar novamente?\n\n{title}", - "redownload_started": "Download iniciado" + "redownload_started": "Download iniciado", + "no_url_error": "Nenhuma URL encontrada no item do hist├│rico", + "redownload_failed": "Falha ao iniciar o novo download: {error}" + }, + "ffmpeg": { + "installation_title": "Instala├з├гo do FFmpeg", + "installation_message": "O YTSage precisa do FFmpeg para processar v├нdeos.\n\nEscolha uma op├з├гo de instala├з├гo abaixo:", + "install_button": "Instalar FFmpeg", + "manual_guide": "Guia manual", + "installation_failed": "A instala├з├гo do FFmpeg encontrou um problema.", + "already_installed": "O FFmpeg j├б est├б instalado!", + "installation_complete": "Instala├з├гo conclu├нda. Voc├к pode fechar este di├бlogo e continuar usando o YTSage.", + "installing": "Instalando FFmpeg... Aguarde", + "install_success": "FFmpeg instalado com sucesso!", + "installation_complete_close": "Instala├з├гo conclu├нda. Agora voc├к pode fechar este di├бlogo e continuar usando o YTSage.", + "try_manual": "Tente usar o guia de instala├з├гo manual." + }, + "ytdlp_setup": { + "required_title": "Configura├з├гo do yt-dlp necess├бria", + "description": "O YTSage precisa do yt-dlp para baixar v├нdeos.

O yt-dlp n├гo foi encontrado no diret├│rio local do aplicativo. O YTSage precisa configurar o yt-dlp para o seu sistema {os_name}.

Escolha uma op├з├гo abaixo:", + "option_auto": "Baixar automaticamente (recomendado)", + "option_manual": "Selecionar caminho manualmente", + "setup_button": "Configurar yt-dlp", + "downloading": "Baixando yt-dlp...", + "success": "yt-dlp foi instalado com sucesso!", + "error": "Erro: {error}", + "download_failed_title": "Falha no download", + "download_failed_message": "Falha ao baixar yt-dlp: {error}", + "select_executable_title": "Selecionar execut├бvel do yt-dlp", + "copied_to": "yt-dlp copiado com sucesso para {path}", + "setup_error_title": "Erro de configura├з├гo", + "copy_error": "Erro ao copiar yt-dlp para o diret├│rio do aplicativo: {error}", + "invalid_executable_title": "Execut├бvel inv├бlido", + "invalid_executable_message": "O arquivo selecionado n├гo parece ser um execut├бvel v├бlido do yt-dlp.", + "verify_error": "Erro ao verificar o execut├бvel do yt-dlp: {error}", + "setup_failed_title": "Configura├з├гo falhou", + "setup_failed_message": "Falha ao configurar o yt-dlp. Alguns recursos podem n├гo funcionar corretamente.", + "success_dialog_title": "Configura├з├гo do yt-dlp", + "success_dialog_message": "yt-dlp foi configurado com sucesso em:\n{path}", + "file_filter_windows": "Arquivos execut├бveis (*.exe)", + "file_filter_all": "Todos os arquivos (*)" }, "ffmpeg_updater": { "title": "Verificador de Vers├гo FFmpeg", diff --git a/languages/ru.json b/languages/ru.json index 83830ec..2888c4e 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -93,7 +93,8 @@ "select_subtitles": "╨Т╤Л╨▒╤А╨░╤В╤М ╤Б╤Г╨▒╤В╨╕╤В╤А╤Л", "filter_languages_placeholder": "╨д╨╕╨╗╤М╤В╤А ╤П╨╖╤Л╨║╨╛╨▓ (╨╜╨░╨┐╤А╨╕╨╝╨╡╤А, en, ru)...", "no_subtitles_available": "╨б╤Г╨▒╤В╨╕╤В╤А╤Л ╨╜╨╡╨┤╨╛╤Б╤В╤Г╨┐╨╜╤Л", - "matching": "╤Б╨╛╨╛╤В╨▓╨╡╤В╤Б╤В╨▓╤Г╤О╤Й╨╕╨╡" + "matching": "╤Б╨╛╨╛╤В╨▓╨╡╤В╤Б╤В╨▓╤Г╤О╤Й╨╕╨╡", + "ytdlp_log_title": "╨Ц╤Г╤А╨╜╨░╨╗ yt-dlp" }, "tabs": { "cookies": "╨Т╨╛╨╣╤В╨╕ ╤З╨╡╤А╨╡╨╖ Cookie", @@ -124,7 +125,10 @@ "browser_selected_title": "Cookie ╨▒╤А╨░╤Г╨╖╨╡╤А╨░ ╨┐╤А╨╕╨╝╨╡╨╜╨╡╨╜╤Л", "browser_applied_message": "Cookie ╨▒╤А╨░╤Г╨╖╨╡╤А╨░ ╨▒╤Г╨┤╤Г╤В ╨╕╨╖╨▓╨╗╨╡╤З╨╡╨╜╤Л ╨╕╨╖: {browser}", "cleared_title": "Cookie ╨╛╤З╨╕╤Й╨╡╨╜╤Л", - "cleared_message": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨╕ cookie ╨▒╤Л╨╗╨╕ ╨╛╤З╨╕╤Й╨╡╨╜╤Л" + "cleared_message": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨╕ cookie ╨▒╤Л╨╗╨╕ ╨╛╤З╨╕╤Й╨╡╨╜╤Л", + "active_browser": "тЬУ ╨Р╨║╤В╨╕╨▓╨╜╤Л: cookie ╨▒╤А╨░╤Г╨╖╨╡╤А╨░ ({browser})", + "active_file": "тЬУ ╨Р╨║╤В╨╕╨▓╨╡╨╜: ╤Д╨░╨╣╨╗ cookie ({file})", + "none_active": "тЧЛ ╨Э╨╡╤В ╨░╨║╤В╨╕╨▓╨╜╤Л╤Е cookie" }, "custom_command": { "help_text": "╨Т╨▓╨╡╨┤╨╕╤В╨╡ ╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤М╤Б╨║╤Г╤О ╨║╨╛╨╝╨░╨╜╨┤╤Г yt-dlp ╨╜╨╕╨╢╨╡. ╨в╨╡╨║╤Г╤Й╨╕╨╣ URL ╨▒╤Г╨┤╨╡╤В ╨┤╨╛╨▒╨░╨▓╨╗╨╡╨╜ ╨░╨▓╤В╨╛╨╝╨░╤В╨╕╤З╨╡╤Б╨║╨╕.

╨Ф╨╗╤П ╨┐╨╛╨╗╨╜╨╛╨│╨╛ ╤Б╨┐╨╕╤Б╨║╨░ ╨╛╨┐╤Ж╨╕╨╣ ╨╕ ╨┐╤А╨╕╨╝╨╡╤А╨╛╨▓ ╨╕╤Б╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╨╜╨╕╤П, ╨╜╨░╨╢╨╝╨╕╤В╨╡ ╨╖╨┤╨╡╤Б╤М ╨┤╨╗╤П ╨┐╤А╨╛╤Б╨╝╨╛╤В╤А╨░ ╨╛╤Д╨╕╤Ж╨╕╨░╨╗╤М╨╜╨╛╨╣ ╨┤╨╛╨║╤Г╨╝╨╡╨╜╤В╨░╤Ж╨╕╨╕ yt-dlp.

╨Я╤А╨╕╨╝╨╡╤З╨░╨╜╨╕╨╡: ╨Я╤Г╤В╤М ╨╖╨░╨│╤А╤Г╨╖╨║╨╕ ╨╕ ╤И╨░╨▒╨╗╨╛╨╜ ╨╕╨╝╨╡╨╜╨╕ ╤Д╨░╨╣╨╗╨░ ╨▒╤Г╨┤╤Г╤В ╨╛╨▒╤А╨░╨▒╨╛╤В╨░╨╜╤Л ╨░╨▓╤В╨╛╨╝╨░╤В╨╕╤З╨╡╤Б╨║╨╕.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз ╨Я╨╛╨╗╨╜╨░╤П ╨║╨╛╨╝╨░╨╜╨┤╨░: {command}", "command_success": "тЬЕ ╨Я╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤М╤Б╨║╨░╤П ╨║╨╛╨╝╨░╨╜╨┤╨░ ╨▓╤Л╨┐╨╛╨╗╨╜╨╡╨╜╨░ ╤Г╤Б╨┐╨╡╤И╨╜╨╛!", "command_failed": "тЭМ ╨Ъ╨╛╨╝╨░╨╜╨┤╨░ ╨╖╨░╨▓╨╡╤А╤И╨╕╨╗╨░╤Б╤М ╤Б ╨║╨╛╨┤╨╛╨╝ ╨╛╤И╨╕╨▒╨║╨╕ {code}", - "command_error": "тЭМ ╨Ю╤И╨╕╨▒╨║╨░ ╨▓╤Л╨┐╨╛╨╗╨╜╨╡╨╜╨╕╤П ╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤М╤Б╨║╨╛╨╣ ╨║╨╛╨╝╨░╨╜╨┤╤Л: {error}" + "command_error": "тЭМ ╨Ю╤И╨╕╨▒╨║╨░ ╨▓╤Л╨┐╨╛╨╗╨╜╨╡╨╜╨╕╤П ╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤М╤Б╨║╨╛╨╣ ╨║╨╛╨╝╨░╨╜╨┤╤Л: {error}", + "error_no_url": "тЭМ ╨Ю╤И╨╕╨▒╨║╨░: URL ╨╜╨╡ ╤Г╨║╨░╨╖╨░╨╜. ╨Т╨▓╨╡╨┤╨╕╤В╨╡ URL ╨▓ ╨│╨╗╨░╨▓╨╜╨╛╨╝ ╨╛╨║╨╜╨╡.", + "error_no_command": "тЭМ ╨Ю╤И╨╕╨▒╨║╨░: ╨║╨╛╨╝╨░╨╜╨┤╨░ ╨╜╨╡ ╤Г╨║╨░╨╖╨░╨╜╨░. ╨Т╨▓╨╡╨┤╨╕╤В╨╡ ╨░╤А╨│╤Г╨╝╨╡╨╜╤В╤Л yt-dlp.", + "executing": "ЁЯЪА ╨Т╤Л╨┐╨╛╨╗╨╜╤П╨╡╤В╤Б╤П ╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╨╡╨╗╤М╤Б╨║╨░╤П ╨║╨╛╨╝╨░╨╜╨┤╨░ yt-dlp", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П ╨Р╤А╨│╤Г╨╝╨╡╨╜╤В╤Л: {command}", + "download_path_label": "ЁЯУБ ╨Я╤Г╤В╤М ╨╖╨░╨│╤А╤Г╨╖╨║╨╕: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "╨Э╨░╤Б╤В╤А╨╛╨╣╤В╨╡ ╨┐╨░╤А╨░╨╝╨╡╤В╤А╤Л ╨┐╤А╨╛╨║╤Б╨╕ ╨┤╨╗╤П ╨╖╨░╨│╤А╤Г╨╖╨║╨╕. ╨Ю╤Б╤В╨░╨▓╤М╤В╨╡ ╨┐╤Г╤Б╤В╤Л╨╝ ╨┤╨╗╤П ╨┐╤А╤П╨╝╨╛╨│╨╛ ╨┐╨╛╨┤╨║╨╗╤О╤З╨╡╨╜╨╕╤П.", @@ -159,7 +170,15 @@ "invalid_main_url": "╨Э╨╡╨▓╨╡╤А╨╜╤Л╨╣ ╤Д╨╛╤А╨╝╨░╤В URL ╨╛╤Б╨╜╨╛╨▓╨╜╨╛╨│╨╛ ╨┐╤А╨╛╨║╤Б╨╕", "invalid_geo_url": "╨Э╨╡╨▓╨╡╤А╨╜╤Л╨╣ ╤Д╨╛╤А╨╝╨░╤В URL ╨│╨╡╨╛-╨┐╤А╨╛╨║╤Б╨╕", "main_configured": "╨Ю╤Б╨╜╨╛╨▓╨╜╨╛╨╣ ╨┐╤А╨╛╨║╤Б╨╕ ╨╜╨░╤Б╤В╤А╨╛╨╡╨╜", - "geo_configured": "╨У╨╡╨╛-╨┐╤А╨╛╨║╤Б╨╕ ╨╜╨░╤Б╤В╤А╨╛╨╡╨╜" + "geo_configured": "╨У╨╡╨╛-╨┐╤А╨╛╨║╤Б╨╕ ╨╜╨░╤Б╤В╤А╨╛╨╡╨╜", + "set_title": "╨Я╤А╨╛╨║╤Б╨╕ ╨╖╨░╨┤╨░╨╜", + "set_message": "╨Ю╤Б╨╜╨╛╨▓╨╜╨╛╨╣ ╨┐╤А╨╛╨║╤Б╨╕ ╨╖╨░╨┤╨░╨╜ ╨╕ ╤Б╨╛╤Е╤А╨░╨╜╤С╨╜: {proxy}", + "geo_set_title": "╨У╨╡╨╛тАС╨┐╤А╨╛╨║╤Б╨╕ ╨╖╨░╨┤╨░╨╜", + "geo_set_message": "╨Я╤А╨╛╨║╤Б╨╕ ╨┤╨╗╤П ╨│╨╡╨╛тАС╨┐╤А╨╛╨▓╨╡╤А╨║╨╕ ╨╖╨░╨┤╨░╨╜ ╨╕ ╤Б╨╛╤Е╤А╨░╨╜╤С╨╜: {proxy}", + "cleared_title": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨╕ ╨┐╤А╨╛╨║╤Б╨╕ ╨╛╤З╨╕╤Й╨╡╨╜╤Л", + "cleared_message": "╨Т╤Б╨╡ ╨╜╨░╤Б╤В╤А╨╛╨╣╨║╨╕ ╨┐╤А╨╛╨║╤Б╨╕ ╨╛╤З╨╕╤Й╨╡╨╜╤Л ╨╕ ╤Б╨╛╤Е╤А╨░╨╜╨╡╨╜╤Л.", + "saved_main": "╨б╨╛╤Е╤А╨░╨╜╤С╨╜ ╨╛╤Б╨╜╨╛╨▓╨╜╨╛╨╣ ╨┐╤А╨╛╨║╤Б╨╕: {proxy}", + "saved_geo": "╨б╨╛╤Е╤А╨░╨╜╤С╨╜ ╨│╨╡╨╛тАС╨┐╤А╨╛╨║╤Б╨╕: {proxy}" }, "download": { "preparing": "╨Я╨╛╨┤╨│╨╛╤В╨╛╨▓╨║╨░ ╨╖╨░╨│╤А╤Г╨╖╨║╨╕...", @@ -347,7 +366,11 @@ "zero_selected": "0 ╨▓╤Л╨▒╤А╨░╨╜╨╛", "analyze_first_tooltip": "╨Я╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░, ╤Б╨╜╨░╤З╨░╨╗╨░ ╨┐╤А╨╛╨░╨╜╨░╨╗╨╕╨╖╨╕╤А╤Г╨╣╤В╨╡ ╨▓╨╕╨┤╨╡╨╛", "audio_mode_disabled": "╨Э╨╡╨┤╨╛╤Б╤В╤Г╨┐╨╜╨╛ ╨▓ ╤А╨╡╨╢╨╕╨╝╨╡ ╤В╨╛╨╗╤М╨║╨╛ ╨░╤Г╨┤╨╕╨╛", - "select_subtitles_first": "╨Я╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░, ╤Б╨╜╨░╤З╨░╨╗╨░ ╨▓╤Л╨▒╨╡╤А╨╕╤В╨╡ ╤Б╤Г╨▒╤В╨╕╤В╤А╤Л" + "select_subtitles_first": "╨Я╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░, ╤Б╨╜╨░╤З╨░╨╗╨░ ╨▓╤Л╨▒╨╡╤А╨╕╤В╨╡ ╤Б╤Г╨▒╤В╨╕╤В╤А╤Л", + "settings_tooltip": "╨в╨╡╨║╤Г╤Й╨╕╨╣ ╨┐╤Г╤В╤М: {path}\n╨Ы╨╕╨╝╨╕╤В ╤Б╨║╨╛╤А╨╛╤Б╤В╨╕: {speed_limit}", + "speed_limit_none": "╨Э╨╡╤В", + "open_folder_error": "╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨╛╤В╨║╤А╤Л╤В╤М ╨┐╨░╨┐╨║╤Г: {error}", + "time_range_set": "╨г╤Б╤В╨░╨╜╨╛╨▓╨╗╨╡╨╜ ╤Г╤З╨░╤Б╤В╨╛╨║: {section}" }, "sponsorblock": { "sponsor": "╨б╨┐╨╛╨╜╤Б╨╛╤А", @@ -446,7 +469,8 @@ "next_check": "╨б╨╗╨╡╨┤╤Г╤О╤Й╨░╤П ╨┐╤А╨╛╨▓╨╡╤А╨║╨░: {time}", "next_check_error": "╨б╨╗╨╡╨┤╤Г╤О╤Й╨░╤П ╨┐╤А╨╛╨▓╨╡╤А╨║╨░: ╨Ю╤И╨╕╨▒╨║╨░ ╤А╨░╤Б╤З╨╡╤В╨░", "checking": "ЁЯФД ╨Я╤А╨╛╨▓╨╡╤А╨║╨░...", - "check_now": "ЁЯФН ╨Я╤А╨╛╨▓╨╡╤А╨╕╤В╤М ╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П ╤Б╨╡╨╣╤З╨░╤Б" + "check_now": "ЁЯФН ╨Я╤А╨╛╨▓╨╡╤А╨╕╤В╤М ╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П ╤Б╨╡╨╣╤З╨░╤Б", + "current_version": "╨в╨╡╨║╤Г╤Й╨░╤П ╨▓╨╡╤А╤Б╨╕╤П yt-dlp: {version}" }, "url_validation": { "empty_url": "URL ╨╜╨╡ ╨╝╨╛╨╢╨╡╤В ╨▒╤Л╤В╤М ╨┐╤Г╤Б╤В╤Л╨╝", @@ -477,6 +501,7 @@ "clear_confirm_message": "╨Т╤Л ╤Г╨▓╨╡╤А╨╡╨╜╤Л? ╨н╤В╨╛ ╨╜╨╡╨╗╤М╨╖╤П ╨╛╤В╨╝╨╡╨╜╨╕╤В╤М.", "no_history": "╨Ш╤Б╤В╨╛╤А╨╕╤П ╨┐╨╛╨║╨░ ╨┐╤Г╤Б╤В╨░", "no_history_description": "╨Ч╨░╨│╤А╤Г╨╖╨║╨╕ ╨┐╨╛╤П╨▓╤П╤В╤Б╤П ╨╖╨┤╨╡╤Б╤М", + "loading": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╕╤Б╤В╨╛╤А╨╕╨╕...", "search_placeholder": "╨Я╨╛╨╕╤Б╨║...", "open_location": "╨Ю╤В╨║╤А╤Л╤В╤М ╨Я╨░╨┐╨║╤Г", "redownload": "╨Ч╨░╨│╤А╤Г╨╖╨╕╤В╤М ╨б╨╜╨╛╨▓╨░", @@ -495,7 +520,47 @@ "one_entry": "1 ╨╖╨░╨│╤А╤Г╨╖╨║╨░", "redownload_confirm_title": "╨Ч╨░╨│╤А╤Г╨╖╨╕╤В╤М ╨б╨╜╨╛╨▓╨░?", "redownload_confirm_message": "╨Ч╨░╨│╤А╤Г╨╖╨╕╤В╤М ╤Б╨╜╨╛╨▓╨░?\n\n{title}", - "redownload_started": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╜╨░╤З╨░╤В╨░" + "redownload_started": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╜╨░╤З╨░╤В╨░", + "no_url_error": "╨Т ╨╖╨░╨┐╨╕╤Б╨╕ ╨╕╤Б╤В╨╛╤А╨╕╨╕ ╨╜╨╡ ╨╜╨░╨╣╨┤╨╡╨╜ URL", + "redownload_failed": "╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨╖╨░╨┐╤Г╤Б╤В╨╕╤В╤М ╨┐╨╛╨▓╤В╨╛╤А╨╜╤Г╤О ╨╖╨░╨│╤А╤Г╨╖╨║╤Г: {error}" + }, + "ffmpeg": { + "installation_title": "╨г╤Б╤В╨░╨╜╨╛╨▓╨║╨░ FFmpeg", + "installation_message": "YTSage ╤В╤А╨╡╨▒╤Г╨╡╤В╤Б╤П FFmpeg ╨┤╨╗╤П ╨╛╨▒╤А╨░╨▒╨╛╤В╨║╨╕ ╨▓╨╕╨┤╨╡╨╛.\n\n╨Т╤Л╨▒╨╡╤А╨╕╤В╨╡ ╨▓╨░╤А╨╕╨░╨╜╤В ╤Г╤Б╤В╨░╨╜╨╛╨▓╨║╨╕ ╨╜╨╕╨╢╨╡:", + "install_button": "╨г╤Б╤В╨░╨╜╨╛╨▓╨╕╤В╤М FFmpeg", + "manual_guide": "╨а╤Г╨║╨╛╨▓╨╛╨┤╤Б╤В╨▓╨╛ ╨▓╤А╤Г╤З╨╜╤Г╤О", + "installation_failed": "╨Я╤А╨╕ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨║╨╡ FFmpeg ╨▓╨╛╨╖╨╜╨╕╨║╨╗╨░ ╨┐╤А╨╛╨▒╨╗╨╡╨╝╨░.", + "already_installed": "FFmpeg ╤Г╨╢╨╡ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨╗╨╡╨╜!", + "installation_complete": "╨г╤Б╤В╨░╨╜╨╛╨▓╨║╨░ ╨╖╨░╨▓╨╡╤А╤И╨╡╨╜╨░. ╨Т╤Л ╨╝╨╛╨╢╨╡╤В╨╡ ╨╖╨░╨║╤А╤Л╤В╤М ╤Н╤В╨╛ ╨╛╨║╨╜╨╛ ╨╕ ╨┐╤А╨╛╨┤╨╛╨╗╨╢╨╕╤В╤М ╨╕╤Б╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╤М YTSage.", + "installing": "╨г╤Б╤В╨░╨╜╨╛╨▓╨║╨░ FFmpeg... ╨Я╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░, ╨┐╨╛╨┤╨╛╨╢╨┤╨╕╤В╨╡", + "install_success": "FFmpeg ╤Г╤Б╨┐╨╡╤И╨╜╨╛ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨╗╨╡╨╜!", + "installation_complete_close": "╨г╤Б╤В╨░╨╜╨╛╨▓╨║╨░ ╨╖╨░╨▓╨╡╤А╤И╨╡╨╜╨░. ╨в╨╡╨┐╨╡╤А╤М ╨▓╤Л ╨╝╨╛╨╢╨╡╤В╨╡ ╨╖╨░╨║╤А╤Л╤В╤М ╤Н╤В╨╛ ╨╛╨║╨╜╨╛ ╨╕ ╨┐╤А╨╛╨┤╨╛╨╗╨╢╨╕╤В╤М ╨╕╤Б╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╤М YTSage.", + "try_manual": "╨Я╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░, ╨┐╨╛╨┐╤А╨╛╨▒╤Г╨╣╤В╨╡ ╨▓╨╛╤Б╨┐╨╛╨╗╤М╨╖╨╛╨▓╨░╤В╤М╤Б╤П ╤А╤Г╨║╨╛╨▓╨╛╨┤╤Б╤В╨▓╨╛╨╝ ╨┐╨╛ ╤А╤Г╤З╨╜╨╛╨╣ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨║╨╡." + }, + "ytdlp_setup": { + "required_title": "╨в╤А╨╡╨▒╤Г╨╡╤В╤Б╤П ╨╜╨░╤Б╤В╤А╨╛╨╣╨║╨░ yt-dlp", + "description": "YTSage ╤В╤А╨╡╨▒╤Г╨╡╤В yt-dlp ╨┤╨╗╤П ╨╖╨░╨│╤А╤Г╨╖╨║╨╕ ╨▓╨╕╨┤╨╡╨╛.

yt-dlp ╨╜╨╡ ╨╜╨░╨╣╨┤╨╡╨╜ ╨▓ ╨╗╨╛╨║╨░╨╗╤М╨╜╨╛╨╝ ╨║╨░╤В╨░╨╗╨╛╨│╨╡ ╨┐╤А╨╕╨╗╨╛╨╢╨╡╨╜╨╕╤П. YTSage ╨╜╤Г╨╢╨╜╨╛ ╨╜╨░╤Б╤В╤А╨╛╨╕╤В╤М yt-dlp ╨┤╨╗╤П ╨▓╨░╤И╨╡╨╣ ╤Б╨╕╤Б╤В╨╡╨╝╤Л {os_name}.

╨Т╤Л╨▒╨╡╤А╨╕╤В╨╡ ╨▓╨░╤А╨╕╨░╨╜╤В ╨╜╨╕╨╢╨╡:", + "option_auto": "╨б╨║╨░╤З╨░╤В╤М ╨░╨▓╤В╨╛╨╝╨░╤В╨╕╤З╨╡╤Б╨║╨╕ (╤А╨╡╨║╨╛╨╝╨╡╨╜╨┤╤Г╨╡╤В╤Б╤П)", + "option_manual": "╨Т╤Л╨▒╤А╨░╤В╤М ╨┐╤Г╤В╤М ╨▓╤А╤Г╤З╨╜╤Г╤О", + "setup_button": "╨Э╨░╤Б╤В╤А╨╛╨╕╤В╤М yt-dlp", + "downloading": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ yt-dlp...", + "success": "yt-dlp ╤Г╤Б╨┐╨╡╤И╨╜╨╛ ╤Г╤Б╤В╨░╨╜╨╛╨▓╨╗╨╡╨╜!", + "error": "╨Ю╤И╨╕╨▒╨║╨░: {error}", + "download_failed_title": "╨б╨▒╨╛╨╣ ╨╖╨░╨│╤А╤Г╨╖╨║╨╕", + "download_failed_message": "╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╤Б╨║╨░╤З╨░╤В╤М yt-dlp: {error}", + "select_executable_title": "╨Т╤Л╨▒╨╡╤А╨╕╤В╨╡ ╨╕╤Б╨┐╨╛╨╗╨╜╤П╨╡╨╝╤Л╨╣ ╤Д╨░╨╣╨╗ yt-dlp", + "copied_to": "yt-dlp ╤Г╤Б╨┐╨╡╤И╨╜╨╛ ╤Б╨║╨╛╨┐╨╕╤А╨╛╨▓╨░╨╜ ╨▓ {path}", + "setup_error_title": "╨Ю╤И╨╕╨▒╨║╨░ ╨╜╨░╤Б╤В╤А╨╛╨╣╨║╨╕", + "copy_error": "╨Ю╤И╨╕╨▒╨║╨░ ╨┐╤А╨╕ ╨║╨╛╨┐╨╕╤А╨╛╨▓╨░╨╜╨╕╨╕ yt-dlp ╨▓ ╨║╨░╤В╨░╨╗╨╛╨│ ╨┐╤А╨╕╨╗╨╛╨╢╨╡╨╜╨╕╤П: {error}", + "invalid_executable_title": "╨Э╨╡╨┤╨╛╨┐╤Г╤Б╤В╨╕╨╝╤Л╨╣ ╨╕╤Б╨┐╨╛╨╗╨╜╤П╨╡╨╝╤Л╨╣ ╤Д╨░╨╣╨╗", + "invalid_executable_message": "╨Т╤Л╨▒╤А╨░╨╜╨╜╤Л╨╣ ╤Д╨░╨╣╨╗ ╨╜╨╡ ╤П╨▓╨╗╤П╨╡╤В╤Б╤П ╨║╨╛╤А╤А╨╡╨║╤В╨╜╤Л╨╝ ╨╕╤Б╨┐╨╛╨╗╨╜╤П╨╡╨╝╤Л╨╝ ╤Д╨░╨╣╨╗╨╛╨╝ yt-dlp.", + "verify_error": "╨Ю╤И╨╕╨▒╨║╨░ ╨┐╤А╨╕ ╨┐╤А╨╛╨▓╨╡╤А╨║╨╡ ╨╕╤Б╨┐╨╛╨╗╨╜╤П╨╡╨╝╨╛╨│╨╛ ╤Д╨░╨╣╨╗╨░ yt-dlp: {error}", + "setup_failed_title": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨╜╨╡ ╤Г╨┤╨░╨╗╨░╤Б╤М", + "setup_failed_message": "╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨╜╨░╤Б╤В╤А╨╛╨╕╤В╤М yt-dlp. ╨Э╨╡╨║╨╛╤В╨╛╤А╤Л╨╡ ╤Д╤Г╨╜╨║╤Ж╨╕╨╕ ╨╝╨╛╨│╤Г╤В ╤А╨░╨▒╨╛╤В╨░╤В╤М ╨╜╨╡╨║╨╛╤А╤А╨╡╨║╤В╨╜╨╛.", + "success_dialog_title": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨░ yt-dlp", + "success_dialog_message": "yt-dlp ╤Г╤Б╨┐╨╡╤И╨╜╨╛ ╨╜╨░╤Б╤В╤А╨╛╨╡╨╜ ╨┐╨╛ ╨┐╤Г╤В╨╕:\n{path}", + "file_filter_windows": "╨Ш╤Б╨┐╨╛╨╗╨╜╤П╨╡╨╝╤Л╨╡ ╤Д╨░╨╣╨╗╤Л (*.exe)", + "file_filter_all": "╨Т╤Б╨╡ ╤Д╨░╨╣╨╗╤Л (*)" }, "ffmpeg_updater": { "title": "╨Я╤А╨╛╨▓╨╡╤А╨║╨░ ╨▓╨╡╤А╤Б╨╕╨╕ FFmpeg", diff --git a/languages/tr.json b/languages/tr.json index a0aa779..6d1edc6 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -93,7 +93,8 @@ "select_subtitles": "Altyaz─▒ se├з", "filter_languages_placeholder": "Dilleri filtrele (├╢rn: tr, en)...", "no_subtitles_available": "Altyaz─▒ mevcut de─Яil", - "matching": "e┼Яle┼Яen" + "matching": "e┼Яle┼Яen", + "ytdlp_log_title": "yt-dlp G├╝nl├╝─Я├╝" }, "tabs": { "cookies": "├Зerezlerle giri┼Я yap", @@ -124,7 +125,10 @@ "browser_selected_title": "Taray─▒c─▒ ├Зerezleri Uyguland─▒", "browser_applied_message": "Taray─▒c─▒ ├зerezleri ┼Яuradan ├з─▒kar─▒lacak: {browser}", "cleared_title": "├Зerezler Temizlendi", - "cleared_message": "├Зerez ayarlar─▒ temizlendi" + "cleared_message": "├Зerez ayarlar─▒ temizlendi", + "active_browser": "тЬУ Etkin: Taray─▒c─▒ ├зerezleri ({browser})", + "active_file": "тЬУ Etkin: ├Зerez dosyas─▒ ({file})", + "none_active": "тЧЛ Etkin ├зerez yok" }, "custom_command": { "help_text": "├Цzel yt-dlp komutunuzu a┼Яa─Я─▒ya girin. Mevcut URL otomatik olarak eklenecektir.

Se├зeneklerin tam listesi ve kullan─▒m ├╢rnekleri i├зin resmi yt-dlp belgelerini g├╢rmek i├зin buraya t─▒klay─▒n.

Not: ─░ndirme yolu ve dosya ad─▒ ┼Яablonu otomatik olarak i┼Яlenir.", @@ -137,7 +141,14 @@ "full_command": "ЁЯФз Tam komut: {command}", "command_success": "тЬЕ ├Цzel komut ba┼Яar─▒yla ├зal─▒┼Яt─▒r─▒ld─▒!", "command_failed": "тЭМ Komut {code} ├з─▒k─▒┼Я koduyla ba┼Яar─▒s─▒z oldu", - "command_error": "тЭМ ├Цzel komut ├зal─▒┼Яt─▒rma hatas─▒: {error}" + "command_error": "тЭМ ├Цzel komut ├зal─▒┼Яt─▒r─▒l─▒rken hata: {error}", + "error_no_url": "тЭМ Hata: URL sa─Яlanmad─▒. L├╝tfen ana pencerede bir URL girin.", + "error_no_command": "тЭМ Hata: Komut sa─Яlanmad─▒. L├╝tfen yt-dlp arg├╝manlar─▒n─▒ girin.", + "executing": "ЁЯЪА ├Цzel yt-dlp komutu ├зal─▒┼Яt─▒r─▒l─▒yor", + "url_label": "ЁЯУН URL: {url}", + "args_label": "тЪЩя╕П Arg├╝manlar: {command}", + "download_path_label": "ЁЯУБ ─░ndirme yolu: {path}", + "separator": "==================================================" }, "proxy": { "help_text": "─░ndirmeler i├зin proxy ayarlar─▒n─▒ yap─▒land─▒r─▒n. Do─Яrudan ba─Яlant─▒ i├зin bo┼Я b─▒rak─▒n.", @@ -159,7 +170,13 @@ "invalid_main_url": "Ge├зersiz ana proxy URL format─▒", "invalid_geo_url": "Ge├зersiz co─Яrafi proxy URL format─▒", "main_configured": "Ana proxy yap─▒land─▒r─▒ld─▒", - "geo_configured": "Co─Яrafi proxy yap─▒land─▒r─▒ld─▒" + "geo_configured": "Co─Яrafi proxy yap─▒land─▒r─▒ld─▒", + "set_title": "Proxy ayarland─▒", + "set_message": "Ana proxy ayarland─▒ ve kaydedildi: {proxy}", + "geo_set_title": "Co─Яrafi proxy ayarland─▒", + "geo_set_message": "Co─Яrafi do─Яrulama proxy'si ayarland─▒ ve kaydedildi: {proxy}", + "cleared_title": "Proxy ayarlar─▒ temizlendi", + "cleared_message": "T├╝m proxy ayarlar─▒ temizlendi ve kaydedildi." }, "download": { "preparing": "─░ndirme haz─▒rlan─▒yor...", @@ -347,7 +364,11 @@ "zero_selected": "0 se├зildi", "analyze_first_tooltip": "L├╝tfen ├╢nce videoyu analiz edin", "audio_mode_disabled": "Yaln─▒zca ses modunda kullan─▒lamaz", - "select_subtitles_first": "L├╝tfen ├╢nce altyaz─▒lar─▒ se├зin" + "select_subtitles_first": "L├╝tfen ├╢nce altyaz─▒lar─▒ se├зin", + "settings_tooltip": "Mevcut Yol: {path}\nH─▒z S─▒n─▒r─▒: {speed_limit}", + "speed_limit_none": "Yok", + "open_folder_error": "Klas├╢r a├з─▒lamad─▒: {error}", + "time_range_set": "B├╢l├╝m ayarland─▒: {section}" }, "sponsorblock": { "sponsor": "Sponsor", @@ -446,7 +467,8 @@ "next_check": "Sonraki kontrol: {time}", "next_check_error": "Sonraki kontrol: Hesaplama hatas─▒", "checking": "ЁЯФД Kontrol ediliyor...", - "check_now": "ЁЯФН ┼Юimdi g├╝ncellemeleri kontrol et" + "check_now": "ЁЯФН ┼Юimdi g├╝ncellemeleri kontrol et", + "current_version": "Mevcut yt-dlp s├╝r├╝m├╝: {version}" }, "url_validation": { "empty_url": "URL bo┼Я olamaz", @@ -495,7 +517,41 @@ "one_entry": "1 indirme", "redownload_confirm_title": "Tekrar ─░ndir?", "redownload_confirm_message": "Tekrar indir?\n\n{title}", - "redownload_started": "─░ndirme ba┼Яlad─▒" + "redownload_started": "─░ndirme ba┼Яlad─▒", + "no_url_error": "Ge├зmi┼Я kayd─▒nda URL bulunamad─▒", + "redownload_failed": "Yeniden indirme ba┼Яlat─▒lamad─▒: {error}" + }, + "ffmpeg": { + "installation_title": "FFmpeg Kurulumu", + "installation_message": "YTSage, videolar─▒ i┼Яlemek i├зin FFmpeg'e ihtiya├з duyar.\n\nA┼Яa─Я─▒dan bir kurulum se├зene─Яi se├зin:", + "install_button": "FFmpeg Kur", + "manual_guide": "Manuel K─▒lavuz", + "installation_failed": "FFmpeg kurulumu bir sorunla kar┼Я─▒la┼Яt─▒." + }, + "ytdlp_setup": { + "required_title": "yt-dlp Kurulumu Gerekli", + "description": "YTSage, video indirmek i├зin yt-dlp'ye ihtiya├з duyar.

yt-dlp uygulaman─▒n yerel dizininde bulunamad─▒. YTSage'in {os_name} sisteminiz i├зin yt-dlp'yi kurmas─▒ gerekiyor.

L├╝tfen a┼Яa─Я─▒dan bir se├зenek se├зin:", + "option_auto": "Otomatik indir (├Цnerilen)", + "option_manual": "Yolu manuel se├з", + "setup_button": "yt-dlp Kur", + "downloading": "yt-dlp indiriliyor...", + "success": "yt-dlp ba┼Яar─▒yla y├╝klendi!", + "error": "Hata: {error}", + "download_failed_title": "─░ndirme Ba┼Яar─▒s─▒z", + "download_failed_message": "yt-dlp indirilemedi: {error}", + "select_executable_title": "yt-dlp ├зal─▒┼Яt─▒r─▒labilir dosyas─▒n─▒ se├з", + "copied_to": "yt-dlp ba┼Яar─▒yla {path} konumuna kopyaland─▒", + "setup_error_title": "Kurulum Hatas─▒", + "copy_error": "yt-dlp uygulama dizinine kopyalan─▒rken hata: {error}", + "invalid_executable_title": "Ge├зersiz ├Зal─▒┼Яt─▒r─▒labilir", + "invalid_executable_message": "Se├зilen dosya ge├зerli bir yt-dlp ├зal─▒┼Яt─▒r─▒labilir dosyas─▒ de─Яil.", + "verify_error": "yt-dlp ├зal─▒┼Яt─▒r─▒labilir dosyas─▒ do─Яrulan─▒rken hata: {error}", + "setup_failed_title": "Kurulum Ba┼Яar─▒s─▒z", + "setup_failed_message": "yt-dlp kurulumu ba┼Яar─▒s─▒z oldu. Baz─▒ ├╢zellikler d├╝zg├╝n ├зal─▒┼Яmayabilir.", + "success_dialog_title": "yt-dlp Kurulumu", + "success_dialog_message": "yt-dlp ba┼Яar─▒yla ┼Яu konuma yap─▒land─▒r─▒ld─▒:\n{path}", + "file_filter_windows": "├Зal─▒┼Яt─▒r─▒labilir Dosyalar (*.exe)", + "file_filter_all": "T├╝m Dosyalar (*)" }, "ffmpeg_updater": { "title": "FFmpeg S├╝r├╝m Denet├зisi", From 99d3b6ae17014cedb0839fd89ca0c46bcbeaf02f Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:02:08 +0200 Subject: [PATCH 054/134] Update CI/CD README with Flatpak and macOS details Added Flatpak bundle to the list of Linux artifacts and clarified that the macOS zipped application bundle is for arm64 architecture. --- .github/CI_CD_README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/CI_CD_README.md b/.github/CI_CD_README.md index 3bd04d7..e7e31ab 100644 --- a/.github/CI_CD_README.md +++ b/.github/CI_CD_README.md @@ -48,9 +48,10 @@ The workflow creates the following files based on the platform: - `YTSage-v{version}-{arch}.AppImage` - AppImage portable (x86_64, aarch64) - `YTSage-v{version}-{arch}.rpm` - RPM package - `YTSage-v{version}-{arch}.deb` - Debian package +- `YTSage-v{version}-{arch}.flatpak` - Flatpak bundle #### macOS -- `YTSage-v{version}-{arch}.app.zip` - Zipped application bundle (x64, arm64) +- `YTSage-v{version}-{arch}.app.zip` - Zipped application bundle arm64 - `YTSage-v{version}-{arch}.dmg` - Disk image installer ## Workflow Features @@ -63,7 +64,7 @@ The workflow creates the following files based on the platform: ### Manual Versioning - Version is strictly controlled by the input you provide at runtime. - No longer dependent on git tags, reducing accidental releases. - + ### Caching - Python dependencies and virtual environments are cached to speed up builds. From acda70150f8df3c5b65693143c7474aeaa23b8b5 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:03:01 +0200 Subject: [PATCH 055/134] Move language files to ytsage/languages directory Renamed and relocated all language JSON files from the root 'languages' directory to 'ytsage/languages' for improved project organization. --- {languages => ytsage/languages}/ar.json | 0 {languages => ytsage/languages}/de.json | 0 {languages => ytsage/languages}/en.json | 0 {languages => ytsage/languages}/es.json | 0 {languages => ytsage/languages}/fr.json | 0 {languages => ytsage/languages}/hi.json | 0 {languages => ytsage/languages}/id.json | 0 {languages => ytsage/languages}/it.json | 0 {languages => ytsage/languages}/ja.json | 0 {languages => ytsage/languages}/pl.json | 0 {languages => ytsage/languages}/pt.json | 0 {languages => ytsage/languages}/ru.json | 0 {languages => ytsage/languages}/tr.json | 0 {languages => ytsage/languages}/zh.json | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename {languages => ytsage/languages}/ar.json (100%) rename {languages => ytsage/languages}/de.json (100%) rename {languages => ytsage/languages}/en.json (100%) rename {languages => ytsage/languages}/es.json (100%) rename {languages => ytsage/languages}/fr.json (100%) rename {languages => ytsage/languages}/hi.json (100%) rename {languages => ytsage/languages}/id.json (100%) rename {languages => ytsage/languages}/it.json (100%) rename {languages => ytsage/languages}/ja.json (100%) rename {languages => ytsage/languages}/pl.json (100%) rename {languages => ytsage/languages}/pt.json (100%) rename {languages => ytsage/languages}/ru.json (100%) rename {languages => ytsage/languages}/tr.json (100%) rename {languages => ytsage/languages}/zh.json (100%) diff --git a/languages/ar.json b/ytsage/languages/ar.json similarity index 100% rename from languages/ar.json rename to ytsage/languages/ar.json diff --git a/languages/de.json b/ytsage/languages/de.json similarity index 100% rename from languages/de.json rename to ytsage/languages/de.json diff --git a/languages/en.json b/ytsage/languages/en.json similarity index 100% rename from languages/en.json rename to ytsage/languages/en.json diff --git a/languages/es.json b/ytsage/languages/es.json similarity index 100% rename from languages/es.json rename to ytsage/languages/es.json diff --git a/languages/fr.json b/ytsage/languages/fr.json similarity index 100% rename from languages/fr.json rename to ytsage/languages/fr.json diff --git a/languages/hi.json b/ytsage/languages/hi.json similarity index 100% rename from languages/hi.json rename to ytsage/languages/hi.json diff --git a/languages/id.json b/ytsage/languages/id.json similarity index 100% rename from languages/id.json rename to ytsage/languages/id.json diff --git a/languages/it.json b/ytsage/languages/it.json similarity index 100% rename from languages/it.json rename to ytsage/languages/it.json diff --git a/languages/ja.json b/ytsage/languages/ja.json similarity index 100% rename from languages/ja.json rename to ytsage/languages/ja.json diff --git a/languages/pl.json b/ytsage/languages/pl.json similarity index 100% rename from languages/pl.json rename to ytsage/languages/pl.json diff --git a/languages/pt.json b/ytsage/languages/pt.json similarity index 100% rename from languages/pt.json rename to ytsage/languages/pt.json diff --git a/languages/ru.json b/ytsage/languages/ru.json similarity index 100% rename from languages/ru.json rename to ytsage/languages/ru.json diff --git a/languages/tr.json b/ytsage/languages/tr.json similarity index 100% rename from languages/tr.json rename to ytsage/languages/tr.json diff --git a/languages/zh.json b/ytsage/languages/zh.json similarity index 100% rename from languages/zh.json rename to ytsage/languages/zh.json From f9eace71e0e0d0066ca002883f88a55f66622d1f Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:04:08 +0200 Subject: [PATCH 056/134] Reorganize asset and branding file structure Moved icons, screenshots, SVGs, and sound files to new locations under 'branding' and 'ytsage/assets' directories for improved project organization. --- {assets/branding => branding}/icons/YTSage.ico | Bin {assets/branding => branding}/icons/icon.icns | Bin {assets/branding => branding}/icons/icon.png | Bin .../screenshots/Custom-Option.png | Bin .../screenshots/Download-Settings.png | Bin .../screenshots/audio_format.png | Bin {assets/branding => branding}/screenshots/main.png | Bin .../branding => branding}/screenshots/playlist.png | Bin .../branding => branding}/svg/ytsage-wordmark.svg | 0 {assets => ytsage/assets}/Icon/icon.png | Bin {assets => ytsage/assets}/sound/notification.mp3 | Bin 11 files changed, 0 insertions(+), 0 deletions(-) rename {assets/branding => branding}/icons/YTSage.ico (100%) rename {assets/branding => branding}/icons/icon.icns (100%) rename {assets/branding => branding}/icons/icon.png (100%) rename {assets/branding => branding}/screenshots/Custom-Option.png (100%) rename {assets/branding => branding}/screenshots/Download-Settings.png (100%) rename {assets/branding => branding}/screenshots/audio_format.png (100%) rename {assets/branding => branding}/screenshots/main.png (100%) rename {assets/branding => branding}/screenshots/playlist.png (100%) rename {assets/branding => branding}/svg/ytsage-wordmark.svg (100%) rename {assets => ytsage/assets}/Icon/icon.png (100%) rename {assets => ytsage/assets}/sound/notification.mp3 (100%) diff --git a/assets/branding/icons/YTSage.ico b/branding/icons/YTSage.ico similarity index 100% rename from assets/branding/icons/YTSage.ico rename to branding/icons/YTSage.ico diff --git a/assets/branding/icons/icon.icns b/branding/icons/icon.icns similarity index 100% rename from assets/branding/icons/icon.icns rename to branding/icons/icon.icns diff --git a/assets/branding/icons/icon.png b/branding/icons/icon.png similarity index 100% rename from assets/branding/icons/icon.png rename to branding/icons/icon.png diff --git a/assets/branding/screenshots/Custom-Option.png b/branding/screenshots/Custom-Option.png similarity index 100% rename from assets/branding/screenshots/Custom-Option.png rename to branding/screenshots/Custom-Option.png diff --git a/assets/branding/screenshots/Download-Settings.png b/branding/screenshots/Download-Settings.png similarity index 100% rename from assets/branding/screenshots/Download-Settings.png rename to branding/screenshots/Download-Settings.png diff --git a/assets/branding/screenshots/audio_format.png b/branding/screenshots/audio_format.png similarity index 100% rename from assets/branding/screenshots/audio_format.png rename to branding/screenshots/audio_format.png diff --git a/assets/branding/screenshots/main.png b/branding/screenshots/main.png similarity index 100% rename from assets/branding/screenshots/main.png rename to branding/screenshots/main.png diff --git a/assets/branding/screenshots/playlist.png b/branding/screenshots/playlist.png similarity index 100% rename from assets/branding/screenshots/playlist.png rename to branding/screenshots/playlist.png diff --git a/assets/branding/svg/ytsage-wordmark.svg b/branding/svg/ytsage-wordmark.svg similarity index 100% rename from assets/branding/svg/ytsage-wordmark.svg rename to branding/svg/ytsage-wordmark.svg diff --git a/assets/Icon/icon.png b/ytsage/assets/Icon/icon.png similarity index 100% rename from assets/Icon/icon.png rename to ytsage/assets/Icon/icon.png diff --git a/assets/sound/notification.mp3 b/ytsage/assets/sound/notification.mp3 similarity index 100% rename from assets/sound/notification.mp3 rename to ytsage/assets/sound/notification.mp3 From 2aac706ef785480a11da5ba92285929542a30a83 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:04:31 +0200 Subject: [PATCH 057/134] Migrate dependencies to pyproject.toml Replaced requirements.txt with pyproject.toml to modernize project configuration and dependency management using PEP 621 standards. This change improves compatibility with modern Python packaging tools. --- pyproject.toml | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 7 ----- 2 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..323bcdf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,68 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "ytsage" +version = "4.9.7" +description = "Modern YouTube downloader with a clean PySide6 interface." +authors = [ + { name = "oop7", email = "oop7_support@proton.me" }, +] +dependencies = [ + "PySide6>=6.10.1", + "requests>=2.32.5", + "pillow>=12.0.0", + "packaging>=25.0", + "markdown>=3.10", + "loguru>=0.7.3", + "setuptools>=80.9.0", +] +requires-python = ">=3.10,<3.15" +readme = "README.md" +license = "MIT" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + 'Programming Language :: Python :: 3.14', + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Desktop Environment", + "Environment :: X11 Applications :: Qt", + "Environment :: Win32 (MS Windows)", + "Environment :: MacOS X" +] + +keywords = ["youtube", "downloader", "video", "audio", "PySide6", "yt-dlp", "GUI"] + + +[project.scripts] +ytsage = "ytsage.main:main" + +[project.urls] +Homepage = "https://github.com/oop7/YTSage" +Bug-Tracker = "https://github.com/oop7/YTSage/issues" +Reddit = "https://www.reddit.com/r/NO-N_A_M_E/" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["ytsage*"] + +[tool.setuptools.package-data] +ytsage = [ + "assets/Icon/icon.png", + "assets/sound/notification.mp3", + "languages/*.json", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 550f74e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -PySide6>=6.10.1 -requests>=2.32.5 -pillow>=12.0.0 -packaging>=25.0 -markdown>=3.10 -loguru>=0.7.3 -setuptools>=80.9.0 \ No newline at end of file From 32b2b544f478c0a0cb8a95dc3e09c77021730547 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:04:39 +0200 Subject: [PATCH 058/134] Add build_release.py for automated release process Introduces a script to automate the production build process, including backing up and modifying the README for PyPI, cleaning previous build artifacts, building the wheel, and restoring the original README. This streamlines and standardizes the release workflow. --- build_release.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 build_release.py diff --git a/build_release.py b/build_release.py new file mode 100644 index 0000000..25277aa --- /dev/null +++ b/build_release.py @@ -0,0 +1,64 @@ +import os +import shutil +import re +import subprocess +import sys + +def build_release(): + print("Starting Production Build...") + + # 1. Backup local README + if os.path.exists("README.md"): + shutil.copy2("README.md", "README.md.bak") + print("Backed up README.md") + + try: + # 2. Prepare PyPI README + with open("README.md", "r", encoding="utf-8") as f: + content = f.read() + + # Base URL for assets + base_url = "https://github.com/oop7/YTSage/raw/main/branding/" + + # Function to fix paths matches by regex + def path_fixer(match): + full_match = match.group(0) + # Convert backslashes to forward slashes for URL compatibility + fixed = full_match.replace("\\", "/") + # Prepend the absolute URL + return fixed.replace("branding/", base_url) + + # Regex: matches "branding" followed by backslash or slash, then characters until quote, closing paren, or space + # This captures paths like: branding\screenshots\main.png + pattern = r"branding[\\/][^\"')\s]+" + new_content = re.sub(pattern, path_fixer, content) + + with open("README.md", "w", encoding="utf-8") as f: + f.write(new_content) + + print("Modified README.md for PyPI (Absolute URLs)") + + # 3. Clean previous builds + for folder in ["dist", "build", "ytsage.egg-info"]: + if os.path.exists(folder): + shutil.rmtree(folder) + + # 4. Run Build + print("Building Wheel...") + subprocess.check_call([sys.executable, "-m", "build"]) + + except Exception as e: + print(f"Error during build: {e}") + sys.exit(1) + + finally: + # 5. Restore README + if os.path.exists("README.md.bak"): + # Force move back, overwriting the modified one + shutil.move("README.md.bak", "README.md") + print("Restored original README.md") + + print("Build Complete. Artifacts are in the /dist folder.") + +if __name__ == "__main__": + build_release() From 2e5e40cbf500d0f0ea6cdc945979c2582e43e644 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:05:05 +0200 Subject: [PATCH 059/134] Update README for new directory structure and usage Updated image paths and installation instructions to reflect the new project structure. The documentation now references the 'branding' directory instead of 'assets/branding', and installation uses 'pip install .' with module execution via 'python -m ytsage.main'. The project tree was revised to match the reorganized source and asset directories. --- README.md | 97 +++++++++++++++++-------------------------------------- 1 file changed, 30 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index bf4a712..bc9e4af 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-ytsage-wordmark -YTSage Interface +ytsage-wordmark +YTSage Interface [![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://badge.fury.io/py/ytsage) [![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) @@ -114,19 +114,19 @@ cd YTSage #### тЪб With uv ```bash -uv pip install -r requirements.txt +uv pip install . ``` #### ЁЯУж Or with standard pip ```bash -pip install -r requirements.txt +pip install . ``` ### 3. Run the Application ```bash -python main.py +python -m ytsage.main ``` @@ -137,16 +137,16 @@ python main.py
- - + + - - + + @@ -344,47 +344,23 @@ YTSage/ тФВ тФВ тФВтФАтФА build-windows.yml # Windows build workflow | | тФФтФАтФА release-all.yml # Master release workflow тФВ тФФтФАтФА ЁЯУД CI_CD_README.md # CI/CD documentation -тФЬтФАтФА ЁЯУБ assets/ # Static assets and resources -тФВ тФЬтФАтФА ЁЯУБ branding/ # Branding assets -тФВ тФВ тФЬтФАтФА ЁЯУБ icons/ # Application icons -тФВ тФВ тФВ тФЬтФАтФА icon.icns # macOS icon -тФВ тФВ тФВ тФЬтФАтФА icon.png # PNG icon -тФВ тФВ тФВ тФФтФАтФА YTSage.ico # Windows icon -тФВ тФВ тФЬтФАтФА ЁЯУБ screenshots/ # Screenshots for documentation -тФВ тФВ тФВ тФЬтФАтФА audio_format.png -тФВ тФВ тФВ тФЬтФАтФА Custom-Option.png -тФВ тФВ тФВ тФЬтФАтФА Download-Settings.png -тФВ тФВ тФВ тФЬтФАтФА playlist.png -тФВ тФВ тФВ тФФтФАтФА main.png -тФВ тФВ тФФтФАтФА ЁЯУБ svg/ # SVG assets -тФВ тФВ тФФтФАтФА ytsage-wordmark.svg -тФВ тФВ тФФтФАтФА ytsage-wordmark.svg -тФВ тФЬтФАтФА ЁЯУБ Icon/ # Legacy icon directory -тФВ тФВ тФФтФАтФА icon.png -тФВ тФФтФАтФА ЁЯУБ sound/ # Audio files -тФВ тФФтФАтФА notification.mp3 -тФЬтФАтФА ЁЯУБ languages/ # Localization files -тФВ тФЬтФАтФА ЁЯУД ar.json # Arabic translation -тФВ тФЬтФАтФА ЁЯУД de.json # German translation -тФВ тФЬтФАтФА ЁЯУД en.json # English translation -тФВ тФЬтФАтФА ЁЯУД es.json # Spanish translation -тФВ тФЬтФАтФА ЁЯУД fr.json # French translation -тФВ тФЬтФАтФА ЁЯУД hi.json # Hindi translation -тФВ тФЬтФАтФА ЁЯУД id.json # Indonesian translation -тФВ тФЬтФАтФА ЁЯУД it.json # Italian translation -тФВ тФЬтФАтФА ЁЯУД ja.json # Japanese translation -тФВ тФЬтФАтФА ЁЯУД pl.json # Polish translation -тФВ тФЬтФАтФА ЁЯУД pt.json # Portuguese translation -тФВ тФЬтФАтФА ЁЯУД ru.json # Russian translation -тФВ тФЬтФАтФА ЁЯУД tr.json # Turkish translation -тФВ тФФтФАтФА ЁЯУД zh.json # Chinese translation +тФЬтФАтФА ЁЯУБ branding/ # Branding assets (Screenshots, SVGs) +тФВ тФЬтФАтФА ЁЯУБ icons/ # Application icons +тФВ тФЬтФАтФА ЁЯУБ screenshots/ # Screenshots for documentation +тФВ тФФтФАтФА ЁЯУБ svg/ # SVG assets тФЬтФАтФА ЁЯУД LICENSE # License file -тФЬтФАтФА ЁЯУД main.py # Application entry point +тФЬтФАтФА ЁЯУД pyproject.toml # Project metadata and dependencies тФЬтФАтФА ЁЯУД README.md # Project documentation -тФЬтФАтФА ЁЯУД .gitignore # Git ignore rules -тФЬтФАтФА ЁЯУД requirements.txt # Python dependencies -тФФтФАтФА ЁЯУБ src/ # Source code - | +тФЬтФАтФА ЁЯУД requirements.txt # Python dependencies (dev) +тФФтФАтФА ЁЯУБ ytsage/ # Source package + тФЬтФАтФА ЁЯУБ assets/ # Runtime assets + тФВ тФЬтФАтФА ЁЯУБ Icon/ # Application icons + тФВ тФФтФАтФА ЁЯУБ sound/ # Audio files + тФЬтФАтФА ЁЯУБ languages/ # Localization files + тФВ тФЬтФАтФА ЁЯУД ar.json # Arabic translation + тФВ тФЬтФАтФА ЁЯУД de.json # German translation + тФВ тФЬтФАтФА ЁЯУД en.json # English translation + тФВ тФФтФАтФА ... # Other languages тФЬтФАтФА ЁЯУБ core/ # Core business logic тФВ тФЬтФАтФА ЁЯУД __init__.py # Core package init тФВ тФЬтФАтФА ЁЯУД ytsage_deno.py # Deno integration @@ -394,27 +370,14 @@ YTSage/ тФВ тФФтФАтФА ЁЯУД ytsage_yt_dlp.py # yt-dlp integration тФЬтФАтФА ЁЯУБ gui/ # User interface components тФВ тФЬтФАтФА ЁЯУД __init__.py # GUI package init - тФВ тФЬтФАтФА ЁЯУД ytsage_gui_format_table.py # Format table functionality тФВ тФЬтФАтФА ЁЯУД ytsage_gui_main.py # Main application window - тФВ тФЬтФАтФА ЁЯУД ytsage_gui_video_info.py # Video information display - | тФЬтФАтФА ЁЯУД ytsage_stylesheet.py # Stylesheet definitions тФВ тФФтФАтФА ЁЯУБ ytsage_gui_dialogs/ # Dialog classes - тФВ тФЬтФАтФА ЁЯУД __init__.py # Dialogs package init - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_base.py # Basic dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_custom.py # Custom functionality dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_ffmpeg.py # FFmpeg-related dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_history.py # History dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_selection.py # Selection dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_settings.py # Settings dialogs - тФВ тФЬтФАтФА ЁЯУД ytsage_dialogs_update.py # Update dialogs - тФВ тФФтФАтФА ЁЯУД ytsage_dialogs_updater.py # Updater dialogs - тФФтФАтФА ЁЯУБ utils/ # Utility modules - тФЬтФАтФА ЁЯУД __init__.py # Utils package init - тФЬтФАтФА ЁЯУД ytsage_config_manager.py # Configuration management - тФЬтФАтФА ЁЯУД ytsage_constants.py # Application constants - тФЬтФАтФА ЁЯУД ytsage_history_manager.py # History management - тФЬтФАтФА ЁЯУД ytsage_localization.py # Localization utilities - тФФтФАтФА ЁЯУД ytsage_logger.py # Logging utilities + тФЬтФАтФА ЁЯУБ utils/ # Utility modules + тФВ тФЬтФАтФА ЁЯУД __init__.py # Utils package init + тФВ тФЬтФАтФА ЁЯУД ytsage_config_manager.py # Configuration management + тФВ тФФтФАтФА ЁЯУД ytsage_logger.py # Logging utilities + тФЬтФАтФА ЁЯУД __init__.py # Package entry point + тФФтФАтФА ЁЯУД main.py # Main execution script ``` From a0df6b273fba1543882d463908d813fd31fa1b6e Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:05:34 +0200 Subject: [PATCH 060/134] Update Linux build workflow for PEP 517 and cx_Freeze Switch pip install to use PEP 517 (pyproject.toml) instead of requirements.txt and update cache key accordingly. Refactor cx_Freeze setup to use new asset paths, add entry point script, and adjust include_files for correct packaging structure. --- .github/workflows/build-linux.yml | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 845593a..bb05ae1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -49,7 +49,7 @@ jobs: path: | venv ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- @@ -67,7 +67,7 @@ jobs: python -m venv venv source venv/bin/activate python -m pip install --upgrade pip - pip install --no-cache-dir -r requirements.txt + pip install --no-cache-dir . pip install --no-cache-dir cx_Freeze - name: Prepare build variables @@ -82,6 +82,13 @@ jobs: - name: Create cx_Freeze setup script (Linux) shell: bash run: | + # Create entry point script + cat > ytsage_entry.py <<'ENTRY' + from ytsage.main import main + if __name__ == "__main__": + main() + ENTRY + cat > setup_cxfreeze.py <<'PY' import os import sys @@ -92,18 +99,18 @@ jobs: # Prepare include_files list include_files_list = [ - ("src", "src"), - ("assets/branding/icons", "lib/assets/branding/icons"), - ("assets/Icon", "lib/assets/Icon"), - ("assets/sound", "lib/assets/sound"), - ("languages", "lib/languages"), + ("ytsage/assets/Icon", "lib/assets/Icon"), + ("ytsage/assets/sound", "lib/assets/sound"), + ("ytsage/languages", "lib/languages"), + ("branding/icons", "lib/assets/branding/icons"), ("ytsage.desktop", "share/applications/ytsage.desktop"), - ("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"), + ("branding/icons/icon.png", "share/pixmaps/ytsage.png"), ] build_exe_options = dict( optimize=2, packages=[ + "ytsage", "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", @@ -143,9 +150,9 @@ jobs: include_files=include_files_list, # Bundle all dependencies - avoid system library references bin_includes=[], - bin_excludes=[], - # Important: include system libs to avoid external dependencies - replace_paths=[("*", "")], + bin_excludesytsage_entry.py", + target_name="ytsage", + icon="*", "")], ) executables = [ From de7d7965f286a9c718998ae5ef47e4a7563a99d2 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:05:38 +0200 Subject: [PATCH 061/134] Update macOS build workflow for pyproject and new structure Switches dependency installation from requirements.txt to pyproject.toml and updates pip cache key accordingly. Refactors cx_Freeze setup to use a new entry point, adjusts included files and paths to match the updated project structure, and updates icon file references. --- .github/workflows/build-macos.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 9a578c5..3621e82 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -50,7 +50,7 @@ jobs: venv ~/.cache/pip ~/Library/Caches/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- @@ -60,7 +60,7 @@ jobs: python -m venv venv source venv/bin/activate python -m pip install --upgrade pip - pip install --no-cache-dir -r requirements.txt + pip install --no-cache-dir . pip install --no-cache-dir cx_Freeze dmgbuild - name: Prepare build variables @@ -80,6 +80,13 @@ jobs: - name: Create cx_Freeze setup script shell: bash run: | + # Create entry point script + cat > ytsage_entry.py <<'ENTRY' + from ytsage.main import main + if __name__ == "__main__": + main() + ENTRY + cat > setup_cxfreeze.py <<'PY' import os from cx_Freeze import setup, Executable @@ -89,6 +96,7 @@ jobs: build_exe_options = dict( optimize=2, packages=[ + "ytsage", "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", @@ -126,19 +134,18 @@ jobs: "tests", ], include_files=[ - ("src", "src"), - ("assets/branding/icons", "lib/assets/branding/icons"), - ("assets/Icon", "lib/assets/Icon"), - ("assets/sound", "lib/assets/sound"), - ("languages", "lib/languages"), + ("ytsage/assets/Icon", "lib/assets/Icon"), + ("ytsage/assets/sound", "lib/assets/sound"), + ("ytsage/languages", "lib/languages"), + ("branding/icons", "lib/assets/branding/icons"), ], ) executables = [ Executable( - script="main.py", + script="ytsage_entry.py", target_name=f"YTSage-v{version}", - icon="assets/branding/icons/icon.icns", + icon="branding/icons/icon.icns", ) ] @@ -149,7 +156,7 @@ jobs: options={ "build_exe": build_exe_options, "bdist_mac": { - "iconfile": "assets/branding/icons/icon.icns", + "iconfile": "branding/icons/icon.icns", "bundle_name": f"YTSage-v{version}", }, "bdist_dmg": { From 7efcfaa85bf7f6a6ca8ab2fcc96fdbcdfdea1c4d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:05:44 +0200 Subject: [PATCH 062/134] Update Windows build workflow for pyproject and asset paths Switch pip cache and install to use pyproject.toml instead of requirements.txt. Update cx_Freeze setup to use a new entry point, adjust included packages and asset paths to reflect new project structure, and update icon path. --- .github/workflows/build-windows.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index e7966da..6d2dba4 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -49,7 +49,7 @@ jobs: path: | venv ~\AppData\Local\pip\Cache - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- @@ -59,7 +59,7 @@ jobs: python -m venv venv .\venv\Scripts\Activate.ps1 python -m pip install --upgrade pip - pip install --no-cache-dir -r requirements.txt + pip install --no-cache-dir . pip install --no-cache-dir cx_Freeze - name: Prepare build variables @@ -72,6 +72,14 @@ jobs: - name: Create cx_Freeze setup script shell: powershell run: | + # Create entry point script + $entryContent = @' + from ytsage.main import main + if __name__ == "__main__": + main() + '@ + Set-Content -Path "ytsage_entry.py" -Value $entryContent + # Create setup script for cx_Freeze New-Item -Path "setup_cxfreeze.py" -ItemType File -Force Add-Content -Path "setup_cxfreeze.py" -Value "import os" @@ -82,6 +90,7 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value "build_exe_options = dict(" Add-Content -Path "setup_cxfreeze.py" -Value " optimize=2," Add-Content -Path "setup_cxfreeze.py" -Value " packages=[" + Add-Content -Path "setup_cxfreeze.py" -Value ' "ytsage",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtCore",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtGui",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWidgets",' @@ -119,20 +128,19 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "tests",' Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " include_files=[" - Add-Content -Path "setup_cxfreeze.py" -Value ' ("src", "src"),' - Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/branding/icons", "lib/assets/branding/icons"),' - Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/Icon", "lib/assets/Icon"),' - Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/sound", "lib/assets/sound"),' - Add-Content -Path "setup_cxfreeze.py" -Value ' ("languages", "lib/languages"),' + Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/Icon", "lib/assets/Icon"),' + Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/sound", "lib/assets/sound"),' + Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/languages", "lib/languages"),' + Add-Content -Path "setup_cxfreeze.py" -Value ' ("branding/icons", "lib/assets/branding/icons"),' Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value ")" Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value "executables = [" Add-Content -Path "setup_cxfreeze.py" -Value " Executable(" - Add-Content -Path "setup_cxfreeze.py" -Value ' script="main.py",' + Add-Content -Path "setup_cxfreeze.py" -Value ' script="ytsage_entry.py",' Add-Content -Path "setup_cxfreeze.py" -Value ' target_name=f"YTSage-v{version}.exe",' Add-Content -Path "setup_cxfreeze.py" -Value ' base="gui",' - Add-Content -Path "setup_cxfreeze.py" -Value ' icon="assets/branding/icons/YTSage.ico",' + Add-Content -Path "setup_cxfreeze.py" -Value ' icon="branding/icons/YTSage.ico",' Add-Content -Path "setup_cxfreeze.py" -Value " )" Add-Content -Path "setup_cxfreeze.py" -Value "]" Add-Content -Path "setup_cxfreeze.py" -Value "" From ad5f304d4e30eef69a04b42e7950c44901b8bbda Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:05:55 +0200 Subject: [PATCH 063/134] Move main.py to ytsage/ and update imports Renamed main.py to ytsage/main.py and updated relative imports to reflect the new module structure. --- main.py => ytsage/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename main.py => ytsage/main.py (85%) diff --git a/main.py b/ytsage/main.py similarity index 85% rename from main.py rename to ytsage/main.py index ce49b44..6427578 100644 --- a/main.py +++ b/ytsage/main.py @@ -2,8 +2,8 @@ import sys from PySide6.QtWidgets import QApplication, QMessageBox -from src.utils.ytsage_logger import logger -from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main +from .utils.ytsage_logger import logger +from .gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main def show_error_dialog(message): From ca6b53715e1296c9884e0950caee2fe4c11d645c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:06:41 +0200 Subject: [PATCH 064/134] Refactor utils module structure and update imports Moved utility modules from src/utils/ to ytsage/utils/ and updated all relative imports accordingly. Adjusted docstring usage examples and fixed a path reference in LocalizationManager to reflect the new directory structure. --- {src => ytsage}/utils/__init__.py | 0 {src => ytsage}/utils/ytsage_config_manager.py | 6 +++--- {src => ytsage}/utils/ytsage_constants.py | 2 +- {src => ytsage}/utils/ytsage_history_manager.py | 6 +++--- {src => ytsage}/utils/ytsage_localization.py | 6 +++--- {src => ytsage}/utils/ytsage_logger.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) rename {src => ytsage}/utils/__init__.py (100%) rename {src => ytsage}/utils/ytsage_config_manager.py (97%) rename {src => ytsage}/utils/ytsage_constants.py (99%) rename {src => ytsage}/utils/ytsage_history_manager.py (98%) rename {src => ytsage}/utils/ytsage_localization.py (98%) rename {src => ytsage}/utils/ytsage_logger.py (95%) diff --git a/src/utils/__init__.py b/ytsage/utils/__init__.py similarity index 100% rename from src/utils/__init__.py rename to ytsage/utils/__init__.py diff --git a/src/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py similarity index 97% rename from src/utils/ytsage_config_manager.py rename to ytsage/utils/ytsage_config_manager.py index 0ff3182..c4b5d3d 100644 --- a/src/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -20,7 +20,7 @@ Features Usage ----- -from src.utils.ytsage_config_manager import ConfigManager +from .ytsage_config_manager import ConfigManager # Load settings (auto-loads if not already loaded) download_path = ConfigManager.get("download_path") @@ -54,8 +54,8 @@ import threading from pathlib import Path from typing import Any, Dict, Optional -from src.utils.ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR -from src.utils.ytsage_logger import logger +from .ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR +from .ytsage_logger import logger class ConfigManager: diff --git a/src/utils/ytsage_constants.py b/ytsage/utils/ytsage_constants.py similarity index 99% rename from src/utils/ytsage_constants.py rename to ytsage/utils/ytsage_constants.py index def0a31..af80163 100644 --- a/src/utils/ytsage_constants.py +++ b/ytsage/utils/ytsage_constants.py @@ -70,7 +70,7 @@ def get_asset_path(asset_relative_path: str) -> Path: # Fallback to relative path (for development environment) current_file = Path(__file__) - # Go up from src/utils to ytsage root, then to asset + # Go up from utils to ytsage root, then to asset ytsage_root = current_file.parent.parent.parent asset_path = ytsage_root / asset_relative_path diff --git a/src/utils/ytsage_history_manager.py b/ytsage/utils/ytsage_history_manager.py similarity index 98% rename from src/utils/ytsage_history_manager.py rename to ytsage/utils/ytsage_history_manager.py index 63e84e2..d373581 100644 --- a/src/utils/ytsage_history_manager.py +++ b/ytsage/utils/ytsage_history_manager.py @@ -14,7 +14,7 @@ Features Usage ----- -from src.utils.ytsage_history_manager import HistoryManager +from .ytsage_history_manager import HistoryManager # Add a download to history HistoryManager.add_entry(...) @@ -38,8 +38,8 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional -from src.utils.ytsage_constants import APP_HISTORY_FILE, APP_DATA_DIR -from src.utils.ytsage_logger import logger +from .ytsage_constants import APP_HISTORY_FILE, APP_DATA_DIR +from .ytsage_logger import logger class HistoryManager: diff --git a/src/utils/ytsage_localization.py b/ytsage/utils/ytsage_localization.py similarity index 98% rename from src/utils/ytsage_localization.py rename to ytsage/utils/ytsage_localization.py index cfedccc..199c3ad 100644 --- a/src/utils/ytsage_localization.py +++ b/ytsage/utils/ytsage_localization.py @@ -15,7 +15,7 @@ Features Usage ----- -from src.utils.ytsage_localization import LocalizationManager +from .ytsage_localization import LocalizationManager # Get localized text text = LocalizationManager.get_text("download.ready") @@ -33,7 +33,7 @@ import threading from pathlib import Path from typing import Any, Dict -from src.utils.ytsage_logger import logger +from .ytsage_logger import logger class LocalizationManager: @@ -46,7 +46,7 @@ class LocalizationManager: _lock = threading.RLock() _current_language = "en" _languages: Dict[str, Dict[str, Any]] = {} - _languages_dir = Path(__file__).parent.parent.parent / "languages" + _languages_dir = Path(__file__).parent.parent / "languages" # Fallback English strings embedded in code _fallback_strings = { diff --git a/src/utils/ytsage_logger.py b/ytsage/utils/ytsage_logger.py similarity index 95% rename from src/utils/ytsage_logger.py rename to ytsage/utils/ytsage_logger.py index a066454..5444ae1 100644 --- a/src/utils/ytsage_logger.py +++ b/ytsage/utils/ytsage_logger.py @@ -9,7 +9,7 @@ import sys from loguru import logger -from src.utils.ytsage_constants import APP_LOG_DIR, IS_FROZEN +from .ytsage_constants import APP_LOG_DIR, IS_FROZEN # Separate configs for each handler CONSOLE_CONFIG = { From fa3dc0236fb7d33ed0a96d8aca267edb2d2370ad Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:07:03 +0200 Subject: [PATCH 065/134] Refactor core module imports and update paths Renamed 'src/core' to 'ytsage/core' and updated all internal imports to use relative paths. This improves package structure and ensures correct module resolution after the directory move. --- {src => ytsage}/core/__init__.py | 0 {src => ytsage}/core/ytsage_deno.py | 8 +++---- {src => ytsage}/core/ytsage_downloader.py | 8 +++---- {src => ytsage}/core/ytsage_ffmpeg.py | 4 ++-- {src => ytsage}/core/ytsage_utils.py | 28 +++++++++++------------ {src => ytsage}/core/ytsage_yt_dlp.py | 8 +++---- 6 files changed, 28 insertions(+), 28 deletions(-) rename {src => ytsage}/core/__init__.py (100%) rename {src => ytsage}/core/ytsage_deno.py (99%) rename {src => ytsage}/core/ytsage_downloader.py (99%) rename {src => ytsage}/core/ytsage_ffmpeg.py (99%) rename {src => ytsage}/core/ytsage_utils.py (97%) rename {src => ytsage}/core/ytsage_yt_dlp.py (99%) diff --git a/src/core/__init__.py b/ytsage/core/__init__.py similarity index 100% rename from src/core/__init__.py rename to ytsage/core/__init__.py diff --git a/src/core/ytsage_deno.py b/ytsage/core/ytsage_deno.py similarity index 99% rename from src/core/ytsage_deno.py rename to ytsage/core/ytsage_deno.py index 4d76e0d..2e5edc4 100644 --- a/src/core/ytsage_deno.py +++ b/ytsage/core/ytsage_deno.py @@ -19,9 +19,9 @@ from PySide6.QtWidgets import ( QVBoxLayout, ) -from src.utils.ytsage_logger import logger -from src.utils.ytsage_localization import _ -from src.utils.ytsage_constants import ( +from ..utils.ytsage_logger import logger +from ..utils.ytsage_localization import _ +from ..utils.ytsage_constants import ( APP_BIN_DIR, ICON_PATH, OS_FULL_NAME, @@ -31,7 +31,7 @@ from src.utils.ytsage_constants import ( DENO_DOWNLOAD_URL, DENO_SHA256_URL, ) -from src.core.ytsage_ffmpeg import get_file_sha256 +from .ytsage_ffmpeg import get_file_sha256 def verify_deno_sha256(file_path: Path, sha256_url: str) -> bool: diff --git a/src/core/ytsage_downloader.py b/ytsage/core/ytsage_downloader.py similarity index 99% rename from src/core/ytsage_downloader.py rename to ytsage/core/ytsage_downloader.py index 8719b9b..be7ed05 100644 --- a/src/core/ytsage_downloader.py +++ b/ytsage/core/ytsage_downloader.py @@ -11,16 +11,16 @@ from typing import Optional, List, Set from PySide6.QtCore import QObject, QThread, Signal -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.utils.ytsage_constants import ( +from .ytsage_yt_dlp import get_yt_dlp_path +from ..utils.ytsage_constants import ( SUBPROCESS_CREATIONFLAGS, VIDEO_EXTENSIONS, AUDIO_EXTENSIONS, SUBTITLE_EXTENSIONS, MEDIA_EXTENSIONS, ) -from src.utils.ytsage_localization import LocalizationManager -from src.utils.ytsage_logger import logger +from ..utils.ytsage_localization import LocalizationManager +from ..utils.ytsage_logger import logger # Shorthand for localization _ = LocalizationManager.get_text diff --git a/src/core/ytsage_ffmpeg.py b/ytsage/core/ytsage_ffmpeg.py similarity index 99% rename from src/core/ytsage_ffmpeg.py rename to ytsage/core/ytsage_ffmpeg.py index deb1073..6e7cf22 100644 --- a/src/core/ytsage_ffmpeg.py +++ b/ytsage/core/ytsage_ffmpeg.py @@ -7,8 +7,8 @@ from pathlib import Path import requests -from src.utils.ytsage_logger import logger -from src.utils.ytsage_constants import ( +from ..utils.ytsage_logger import logger +from ..utils.ytsage_constants import ( FFMPEG_7Z_DOWNLOAD_URL, FFMPEG_7Z_SHA256_URL, FFMPEG_ZIP_DOWNLOAD_URL, diff --git a/src/core/ytsage_utils.py b/ytsage/core/ytsage_utils.py similarity index 97% rename from src/core/ytsage_utils.py rename to ytsage/core/ytsage_utils.py index 6303d69..73ee865 100644 --- a/src/core/ytsage_utils.py +++ b/ytsage/core/ytsage_utils.py @@ -11,9 +11,9 @@ from typing import Any, Dict, Optional, Union import requests from packaging import version -from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.utils.ytsage_constants import ( +from .ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path +from .ytsage_yt_dlp import get_yt_dlp_path +from ..utils.ytsage_constants import ( APP_CONFIG_FILE, OS_NAME, SUBPROCESS_CREATIONFLAGS, @@ -21,8 +21,8 @@ from src.utils.ytsage_constants import ( YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL, ) -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger +from ..utils.ytsage_localization import _ +from ..utils.ytsage_logger import logger try: from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError @@ -107,7 +107,7 @@ def update_version_cache(tool_name: str, version_info: str, path: Optional[str], def load_version_cache_from_config() -> None: """Load cached version info from config file.""" - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager try: cached_versions = ConfigManager.get("cached_versions") or {} @@ -121,7 +121,7 @@ def load_version_cache_from_config() -> None: def save_version_cache_to_config() -> None: """Save version cache to config file.""" - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager try: ConfigManager.set("cached_versions", _version_cache.copy()) @@ -179,7 +179,7 @@ def get_ffmpeg_version_cached() -> str: def get_deno_version_cached() -> str: """Get Deno version with caching support.""" try: - from src.core.ytsage_deno import get_deno_path + from .ytsage_deno import get_deno_path current_path = get_deno_path() @@ -190,7 +190,7 @@ def get_deno_version_cached() -> str: return cached_version # Get fresh version info - from src.core.ytsage_deno import get_deno_version_direct + from .ytsage_deno import get_deno_version_direct version_info = get_deno_version_direct(current_path) # Update cache @@ -215,7 +215,7 @@ def refresh_version_cache(force=False) -> bool: update_version_cache("ffmpeg", version_info, "ffmpeg", force_save=True) # Refresh Deno - from src.core.ytsage_deno import get_deno_path, get_deno_version_direct + from .ytsage_deno import get_deno_path, get_deno_version_direct deno_path = get_deno_path() version_info = get_deno_version_direct(deno_path) update_version_cache("deno", version_info, deno_path, force_save=True) @@ -550,7 +550,7 @@ def update_yt_dlp() -> bool: def should_check_for_auto_update() -> bool: """Check if auto-update should be performed based on user settings.""" - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager try: # Check if auto-update is enabled @@ -580,7 +580,7 @@ def should_check_for_auto_update() -> bool: def check_and_update_ytdlp_auto() -> bool: """Perform automatic yt-dlp update check and update if needed.""" - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager try: logger.info("Performing automatic yt-dlp update check...") @@ -637,7 +637,7 @@ def check_and_update_ytdlp_auto() -> bool: def get_auto_update_settings() -> Dict[str, Any]: """Get current auto-update settings from config.""" - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager enabled: Optional[bool] = ConfigManager.get("auto_update_ytdlp") frequency: Optional[str] = ConfigManager.get("auto_update_frequency") @@ -653,7 +653,7 @@ def get_auto_update_settings() -> Dict[str, Any]: def update_auto_update_settings(enabled: bool, frequency: str) -> bool: """Update auto-update settings in config.""" try: - from src.utils.ytsage_config_manager import ConfigManager + from ..utils.ytsage_config_manager import ConfigManager ConfigManager.set("auto_update_ytdlp", enabled) ConfigManager.set("auto_update_frequency", frequency) diff --git a/src/core/ytsage_yt_dlp.py b/ytsage/core/ytsage_yt_dlp.py similarity index 99% rename from src/core/ytsage_yt_dlp.py rename to ytsage/core/ytsage_yt_dlp.py index be11bdf..06efdb7 100644 --- a/src/core/ytsage_yt_dlp.py +++ b/ytsage/core/ytsage_yt_dlp.py @@ -20,8 +20,8 @@ from PySide6.QtWidgets import ( QWidget, ) -from src.utils.ytsage_logger import logger -from src.utils.ytsage_constants import ( +from ..utils.ytsage_logger import logger +from ..utils.ytsage_constants import ( APP_BIN_DIR, ICON_PATH, OS_FULL_NAME, @@ -31,8 +31,8 @@ from src.utils.ytsage_constants import ( YTDLP_DOWNLOAD_URL, YTDLP_SHA256_URL, ) -from src.core.ytsage_ffmpeg import get_file_sha256 -from src.utils.ytsage_localization import _ +from .ytsage_ffmpeg import get_file_sha256 +from ..utils.ytsage_localization import _ # YTDLP_URLS moved to src\utils\ytsage_constants.py # get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py From 4fb9815ac6abd18b775aaec3634d7f0a7e60c7a2 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:07:29 +0200 Subject: [PATCH 066/134] Rename package directory from src to ytsage Moved __init__.py from src/ to ytsage/ to reflect the new package structure. This change helps clarify the package namespace. --- {src => ytsage}/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src => ytsage}/__init__.py (100%) diff --git a/src/__init__.py b/ytsage/__init__.py similarity index 100% rename from src/__init__.py rename to ytsage/__init__.py From 96dadd7e5b149e22423cc8bd950dd86042a6ee5c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:07:51 +0200 Subject: [PATCH 067/134] Refactor GUI package structure and update imports Moved all files from src/gui/ to ytsage/gui/ and updated import statements to use relative imports within the new package structure. This improves modularity and prepares the codebase for distribution as a proper Python package. --- {src => ytsage}/gui/__init__.py | 0 {src => ytsage}/gui/ytsage_gui_analysis.py | 12 ++++---- .../gui/ytsage_gui_dialogs/__init__.py | 16 +++++----- .../ytsage_gui_dialogs/ytsage_dialogs_base.py | 14 ++++----- .../ytsage_dialogs_custom.py | 16 +++++----- .../ytsage_dialogs_ffmpeg.py | 6 ++-- .../ytsage_dialogs_history.py | 10 +++---- .../ytsage_dialogs_selection.py | 2 +- .../ytsage_dialogs_settings.py | 6 ++-- .../ytsage_dialogs_update.py | 14 ++++----- .../ytsage_dialogs_updater.py | 18 +++++------ .../gui/ytsage_gui_format_table.py | 4 +-- {src => ytsage}/gui/ytsage_gui_main.py | 30 +++++++++---------- {src => ytsage}/gui/ytsage_gui_video_info.py | 8 ++--- {src => ytsage}/gui/ytsage_stylesheet.py | 0 15 files changed, 78 insertions(+), 78 deletions(-) rename {src => ytsage}/gui/__init__.py (100%) rename {src => ytsage}/gui/ytsage_gui_analysis.py (97%) rename {src => ytsage}/gui/ytsage_gui_dialogs/__init__.py (63%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py (97%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py (98%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py (97%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py (98%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py (99%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py (99%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py (97%) rename {src => ytsage}/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py (98%) rename {src => ytsage}/gui/ytsage_gui_format_table.py (99%) rename {src => ytsage}/gui/ytsage_gui_main.py (98%) rename {src => ytsage}/gui/ytsage_gui_video_info.py (98%) rename {src => ytsage}/gui/ytsage_stylesheet.py (100%) diff --git a/src/gui/__init__.py b/ytsage/gui/__init__.py similarity index 100% rename from src/gui/__init__.py rename to ytsage/gui/__init__.py diff --git a/src/gui/ytsage_gui_analysis.py b/ytsage/gui/ytsage_gui_analysis.py similarity index 97% rename from src/gui/ytsage_gui_analysis.py rename to ytsage/gui/ytsage_gui_analysis.py index b1bcaa2..0a37739 100644 --- a/src/gui/ytsage_gui_analysis.py +++ b/ytsage/gui/ytsage_gui_analysis.py @@ -5,14 +5,14 @@ import subprocess from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal from PySide6.QtWidgets import QMessageBox -from src.core.ytsage_utils import validate_video_url, parse_yt_dlp_error -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger +from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error +from ..core.ytsage_yt_dlp import get_yt_dlp_path +from ..utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS +from ..utils.ytsage_localization import _ +from ..utils.ytsage_logger import logger if TYPE_CHECKING: - from src.gui.ytsage_gui_main import YTSageApp + from .ytsage_gui_main import YTSageApp class AnalysisThread(QThread): diff --git a/src/gui/ytsage_gui_dialogs/__init__.py b/ytsage/gui/ytsage_gui_dialogs/__init__.py similarity index 63% rename from src/gui/ytsage_gui_dialogs/__init__.py rename to ytsage/gui/ytsage_gui_dialogs/__init__.py index c0e8b9f..8bae87b 100644 --- a/src/gui/ytsage_gui_dialogs/__init__.py +++ b/ytsage/gui/ytsage_gui_dialogs/__init__.py @@ -12,18 +12,18 @@ This package contains all dialog classes organized by functionality: """ # Re-export all dialog classes for backward compatibility -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_history import HistoryDialog -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import ( +from .ytsage_dialogs_base import AboutDialog, LogWindow +from .ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog +from .ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread +from .ytsage_dialogs_history import HistoryDialog +from .ytsage_dialogs_selection import ( PlaylistSelectionDialog, SponsorBlockCategoryDialog, SubtitleSelectionDialog, ) -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget +from .ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog +from .ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog +from .ytsage_dialogs_updater import UpdaterTabWidget __all__ = [ # Base dialogs diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py similarity index 97% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index ee159a2..1f5ad26 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -19,13 +19,13 @@ from PySide6.QtWidgets import ( QWidget, ) -from src import __version__ as APP_VERSION -from src.utils.ytsage_localization import _ +from ... import __version__ as APP_VERSION +from ...utils.ytsage_localization import _ -from src.core.ytsage_ffmpeg import get_ffmpeg_path -from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache -from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path -from src.core.ytsage_deno import check_deno_installed, get_deno_path +from ...core.ytsage_ffmpeg import get_ffmpeg_path +from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache +from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path +from ...core.ytsage_deno import check_deno_installed, get_deno_path class LogWindow(QDialog): @@ -459,7 +459,7 @@ class AboutDialog(QDialog): # Only show path if it's not the fallback "deno" and the file exists if deno_path and deno_path != "deno": from pathlib import Path - from src.utils.ytsage_constants import DENO_APP_BIN_PATH + from ...utils.ytsage_constants import DENO_APP_BIN_PATH # Check if the path is our managed binary if Path(deno_path).resolve() == DENO_APP_BIN_PATH.resolve(): deno_path_text = deno_path diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py similarity index 98% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index b254f8a..49de1fd 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -29,16 +29,16 @@ from PySide6.QtWidgets import ( QWidget, ) -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.core.ytsage_utils import update_auto_update_settings -from src.utils.ytsage_constants import YTDLP_DOCS_URL -from src.utils.ytsage_config_manager import ConfigManager -from src.utils.ytsage_localization import LocalizationManager, _ -from src.utils.ytsage_logger import logger -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget +from ...core.ytsage_yt_dlp import get_yt_dlp_path +from ...core.ytsage_utils import update_auto_update_settings +from ...utils.ytsage_constants import YTDLP_DOCS_URL +from ...utils.ytsage_config_manager import ConfigManager +from ...utils.ytsage_localization import LocalizationManager, _ +from ...utils.ytsage_logger import logger +from .ytsage_dialogs_updater import UpdaterTabWidget if TYPE_CHECKING: - from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import) + from ..ytsage_gui_main import YTSageApp # only for type hints (no runtime import) class CommandWorker(QObject): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py similarity index 97% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py index 44eb563..99b79f9 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py @@ -9,9 +9,9 @@ from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QIcon from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout -from src.core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed -from src.utils.ytsage_constants import ICON_PATH -from src.utils.ytsage_localization import _ +from ...core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed +from ...utils.ytsage_constants import ICON_PATH +from ...utils.ytsage_localization import _ class FFmpegInstallThread(QThread): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py similarity index 98% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py index 6c41906..b3987f2 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_history.py @@ -37,13 +37,13 @@ from PySide6.QtWidgets import ( QApplication ) -from src.utils.ytsage_history_manager import HistoryManager -from src.utils.ytsage_constants import APP_THUMBNAILS_DIR, SUBPROCESS_CREATIONFLAGS -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger +from ...utils.ytsage_history_manager import HistoryManager +from ...utils.ytsage_constants import APP_THUMBNAILS_DIR, SUBPROCESS_CREATIONFLAGS +from ...utils.ytsage_localization import _ +from ...utils.ytsage_logger import logger if TYPE_CHECKING: - from src.gui.ytsage_gui_main import YTSageApp + from ..ytsage_gui_main import YTSageApp class HistoryLoaderThread(QThread): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py similarity index 99% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py index bead261..1990bd2 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_selection.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from src.utils.ytsage_localization import _ +from ...utils.ytsage_localization import _ class SubtitleSelectionDialog(QDialog): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py similarity index 99% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index 0d04f29..3b68fd7 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -27,9 +27,9 @@ from PySide6.QtWidgets import ( QVBoxLayout, ) -from src.utils.ytsage_logger import logger -from src.utils.ytsage_localization import _ -from src.utils.ytsage_config_manager import ConfigManager +from ...utils.ytsage_logger import logger +from ...utils.ytsage_localization import _ +from ...utils.ytsage_config_manager import ConfigManager class DownloadSettingsDialog(QDialog): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py similarity index 97% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py index 5b5d605..1ceca4e 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_update.py @@ -14,16 +14,16 @@ from packaging import version from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout -from src.core.ytsage_utils import get_ytdlp_version -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL -from src.utils.ytsage_config_manager import ConfigManager -from src.utils.ytsage_localization import LocalizationManager +from ...core.ytsage_utils import get_ytdlp_version +from ...core.ytsage_yt_dlp import get_yt_dlp_path +from ...utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL +from ...utils.ytsage_config_manager import ConfigManager +from ...utils.ytsage_localization import LocalizationManager # Shorthand for localization _ = LocalizationManager.get_text -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger +from ...utils.ytsage_localization import _ +from ...utils.ytsage_logger import logger class VersionCheckThread(QThread): diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py similarity index 98% rename from src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py rename to ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index 06853cd..e73a2e8 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -23,25 +23,25 @@ from PySide6.QtWidgets import ( QWidget, ) -from src.core.ytsage_utils import ( +from ...core.ytsage_utils import ( get_auto_update_settings, get_ffmpeg_version_direct, update_auto_update_settings, ) -from src.core.ytsage_yt_dlp import get_yt_dlp_path -from src.core.ytsage_deno import check_deno_update, upgrade_deno -from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import YTDLPUpdateDialog -from src.utils.ytsage_config_manager import ConfigManager -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger -from src.utils.ytsage_constants import ( +from ...core.ytsage_yt_dlp import get_yt_dlp_path +from ...core.ytsage_deno import check_deno_update, upgrade_deno +from .ytsage_dialogs_update import YTDLPUpdateDialog +from ...utils.ytsage_config_manager import ConfigManager +from ...utils.ytsage_localization import _ +from ...utils.ytsage_logger import logger +from ...utils.ytsage_constants import ( FFMPEG_7Z_VERSION_URL, OS_NAME, SUBPROCESS_CREATIONFLAGS, ) if TYPE_CHECKING: - from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog + from .ytsage_dialogs_custom import CustomOptionsDialog # Helper functions for FFmpeg version checking (copied from removed ytsage_ffmpeg_updater.py) diff --git a/src/gui/ytsage_gui_format_table.py b/ytsage/gui/ytsage_gui_format_table.py similarity index 99% rename from src/gui/ytsage_gui_format_table.py rename to ytsage/gui/ytsage_gui_format_table.py index bf03cd3..b3794ac 100644 --- a/src/gui/ytsage_gui_format_table.py +++ b/ytsage/gui/ytsage_gui_format_table.py @@ -4,10 +4,10 @@ from PySide6.QtCore import QObject, Qt, Signal from PySide6.QtGui import QColor, QFontMetrics from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget -from src.utils.ytsage_localization import _ +from ..utils.ytsage_localization import _ if TYPE_CHECKING: - from src.gui.ytsage_gui_main import YTSageApp + from .ytsage_gui_main import YTSageApp class FormatSignals(QObject): diff --git a/src/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py similarity index 98% rename from src/gui/ytsage_gui_main.py rename to ytsage/gui/ytsage_gui_main.py index ffc406d..8b6c728 100644 --- a/src/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -28,12 +28,12 @@ from PySide6.QtWidgets import ( QWidget, ) -from src import __version__ as APP_VERSION -from src.core.ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes -from src.core.ytsage_utils import check_ffmpeg, load_saved_path, parse_yt_dlp_error, save_path, should_check_for_auto_update, validate_video_url -from src.core.ytsage_yt_dlp import get_yt_dlp_path, setup_ytdlp # Import the new yt-dlp functions -from src.core.ytsage_deno import get_deno_path, setup_deno # Import the new Deno functions -from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py +from .. import __version__ as APP_VERSION +from ..core.ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes +from ..core.ytsage_utils import check_ffmpeg, load_saved_path, parse_yt_dlp_error, save_path, should_check_for_auto_update, validate_video_url +from ..core.ytsage_yt_dlp import get_yt_dlp_path, setup_ytdlp # Import the new yt-dlp functions +from ..core.ytsage_deno import get_deno_path, setup_deno # Import the new Deno functions +from .ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py AboutDialog, AutoUpdateThread, CustomOptionsDialog, @@ -44,10 +44,10 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__ TimeRangeDialog, YTDLPUpdateDialog, ) -from src.gui.ytsage_gui_format_table import FormatTableMixin -from src.gui.ytsage_gui_video_info import VideoInfoMixin -from src.gui.ytsage_gui_analysis import AnalysisMixin -from src.utils.ytsage_constants import ( +from .ytsage_gui_format_table import FormatTableMixin +from .ytsage_gui_video_info import VideoInfoMixin +from .ytsage_gui_analysis import AnalysisMixin +from ..utils.ytsage_constants import ( ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS, @@ -55,11 +55,11 @@ from src.utils.ytsage_constants import ( AUDIO_EXTENSIONS, SUBTITLE_EXTENSIONS, ) -from src.utils.ytsage_logger import logger -from src.utils.ytsage_config_manager import ConfigManager -from src.utils.ytsage_localization import LocalizationManager, _ -from src.utils.ytsage_history_manager import HistoryManager -from src.gui.ytsage_stylesheet import StyleSheet +from ..utils.ytsage_logger import logger +from ..utils.ytsage_config_manager import ConfigManager +from ..utils.ytsage_localization import LocalizationManager, _ +from ..utils.ytsage_history_manager import HistoryManager +from .ytsage_stylesheet import StyleSheet from concurrent.futures import ThreadPoolExecutor, as_completed diff --git a/src/gui/ytsage_gui_video_info.py b/ytsage/gui/ytsage_gui_video_info.py similarity index 98% rename from src/gui/ytsage_gui_video_info.py rename to ytsage/gui/ytsage_gui_video_info.py index 8ab17f8..f081136 100644 --- a/src/gui/ytsage_gui_video_info.py +++ b/ytsage/gui/ytsage_gui_video_info.py @@ -10,15 +10,15 @@ from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py +from .ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py SponsorBlockCategoryDialog, SubtitleSelectionDialog, ) -from src.utils.ytsage_localization import _ -from src.utils.ytsage_logger import logger +from ..utils.ytsage_localization import _ +from ..utils.ytsage_logger import logger if TYPE_CHECKING: - from src.gui.ytsage_gui_main import YTSageApp + from .ytsage_gui_main import YTSageApp class ThumbnailDownloadThread(QThread): diff --git a/src/gui/ytsage_stylesheet.py b/ytsage/gui/ytsage_stylesheet.py similarity index 100% rename from src/gui/ytsage_stylesheet.py rename to ytsage/gui/ytsage_stylesheet.py From 9a1fb33996bc596584ed37780781448e3c784ef5 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:11:11 +0200 Subject: [PATCH 068/134] Update macOS artifact naming in CI/CD docs Clarified the naming convention for macOS build artifacts by specifying 'arm64' in the filenames for the zipped app bundle and disk image installer. --- .github/CI_CD_README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CI_CD_README.md b/.github/CI_CD_README.md index e7e31ab..12ac0fd 100644 --- a/.github/CI_CD_README.md +++ b/.github/CI_CD_README.md @@ -51,8 +51,8 @@ The workflow creates the following files based on the platform: - `YTSage-v{version}-{arch}.flatpak` - Flatpak bundle #### macOS -- `YTSage-v{version}-{arch}.app.zip` - Zipped application bundle arm64 -- `YTSage-v{version}-{arch}.dmg` - Disk image installer +- `YTSage-v{version}-arm64.app.zip` - Zipped application bundle +- `YTSage-v{version}-arm64.dmg` - Disk image installer ## Workflow Features From c15d615b0698cbfce3cc54ed1ce00895aa84bb5f Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:20:01 +0200 Subject: [PATCH 069/134] Add MANIFEST.in to include project files in package Adds a MANIFEST.in file to ensure README.md, LICENSE, pyproject.toml, and all files in ytsage/assets and ytsage/languages are included in the package distribution. --- MANIFEST.in | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bd19fb6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include README.md +include LICENSE +include pyproject.toml +recursive-include ytsage/assets * +recursive-include ytsage/languages * From c8ff4849fec79b48fd274426c35d19b8a1868d41 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:22:58 +0200 Subject: [PATCH 070/134] Add module docstring to utils package Introduced a descriptive docstring in ytsage/utils/__init__.py to clarify the purpose of the utilities package and its contents. --- ytsage/utils/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ytsage/utils/__init__.py b/ytsage/utils/__init__.py index e69de29..3bf8475 100644 --- a/ytsage/utils/__init__.py +++ b/ytsage/utils/__init__.py @@ -0,0 +1,5 @@ +""" +Utility modules for YTSage. + +This package contains shared utilities, constants, logging, and configuration management. +""" From 2e42d26f44d5f92ded1accce1e85a485e72d5ee9 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:29:01 +0200 Subject: [PATCH 071/134] Bump version to 5.0.0b2 Update the __version__ string in ytsage/__init__.py to 5.0.0b2 to reflect the latest changes. --- ytsage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ytsage/__init__.py b/ytsage/__init__.py index cc62bb6..eba6ea6 100644 --- a/ytsage/__init__.py +++ b/ytsage/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "5.0.0b" +__version__ = "5.0.0b2" __author__ = "oop7" From 38f08543d9e81a1ebf20c25d54c65afb0eead0e8 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:45:54 +0200 Subject: [PATCH 072/134] Fix build config for Linux workflow Corrected script path to 'ytsage_entry.py', updated icon path, and fixed configuration for including system libraries and binary excludes in the build-linux.yml workflow. --- .github/workflows/build-linux.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index bb05ae1..ba1e06f 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -150,16 +150,16 @@ jobs: include_files=include_files_list, # Bundle all dependencies - avoid system library references bin_includes=[], - bin_excludesytsage_entry.py", - target_name="ytsage", - icon="*", "")], + bin_excludes=[], + # Important: include system libs to avoid external dependencies + replace_paths=[("*", "")], ) executables = [ Executable( - script="main.py", + script="ytsage_entry.py", target_name="ytsage", - icon="assets/branding/icons/icon.png", + icon="branding/icons/icon.png", ) ] From 387621cb243326462cb5c4d53629b9b695929486 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:54:00 +0200 Subject: [PATCH 073/134] Update icon path in build scripts Changed the icon installation path from assets/branding/icons/icon.png to branding/icons/icon.png in Linux packaging steps to reflect updated directory structure. --- .github/workflows/build-linux.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index ba1e06f..f03692b 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -263,7 +263,7 @@ jobs: # Install desktop file and icon install -m 0644 ${workspace_root}/ytsage.desktop %{buildroot}/usr/share/applications/ytsage.desktop - install -m 0644 ${workspace_root}/assets/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png + install -m 0644 ${workspace_root}/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png %files /opt/ytsage @@ -343,7 +343,7 @@ jobs: # Desktop file and icon install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop" - install -m 0644 assets/branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png" + install -m 0644 branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png" # Control file cat > "$pkgroot/DEBIAN/control" < Date: Sun, 25 Jan 2026 15:53:17 +0200 Subject: [PATCH 074/134] Include PySide6.QtNetwork in build workflows Moved PySide6.QtNetwork from the excludes to the includes list in build workflows for Linux, macOS, and Windows. This ensures the QtNetwork module is bundled during packaging. --- .github/workflows/build-linux.yml | 2 +- .github/workflows/build-macos.yml | 2 +- .github/workflows/build-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index f03692b..f06de53 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -115,6 +115,7 @@ jobs: "PySide6.QtGui", "PySide6.QtWidgets", "PySide6.QtMultimedia", + "PySide6.QtNetwork", "requests", "PIL", "packaging", @@ -124,7 +125,6 @@ jobs: ], excludes=[ "PySide6.QtBluetooth", - "PySide6.QtNetwork", "PySide6.QtOpenGL", "PySide6.QtPrintSupport", "PySide6.QtSvg", diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 3621e82..e07755e 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -101,6 +101,7 @@ jobs: "PySide6.QtGui", "PySide6.QtWidgets", "PySide6.QtMultimedia", + "PySide6.QtNetwork", "requests", "PIL", "packaging", @@ -110,7 +111,6 @@ jobs: ], excludes=[ "PySide6.QtBluetooth", - "PySide6.QtNetwork", "PySide6.QtOpenGL", "PySide6.QtPrintSupport", "PySide6.QtSvg", diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 6d2dba4..ca70cbc 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -95,6 +95,7 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtGui",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWidgets",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtMultimedia",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtNetwork",' Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",' Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",' @@ -104,7 +105,6 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " excludes=[" Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtBluetooth",' - Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtNetwork",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtOpenGL",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtPrintSupport",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSvg",' From ffe64acbebce4d578d1dee3960f33d254ec9c3a3 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:40:00 +0200 Subject: [PATCH 075/134] Capture yt-dlp errors and show better messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize an error buffer and collect yt-dlp "ERROR:" output lines during direct command runs. When the process exits nonтАСzero, emit a more informative error using the last two captured error lines (falling back to the generic return-code message if none were captured). Also ensure the buffer is present before appending and keep the existing delay/cleanup flow. --- ytsage/core/ytsage_downloader.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ytsage/core/ytsage_downloader.py b/ytsage/core/ytsage_downloader.py index be7ed05..a1b1252 100644 --- a/ytsage/core/ytsage_downloader.py +++ b/ytsage/core/ytsage_downloader.py @@ -385,7 +385,9 @@ class DownloadThread(QThread): def _run_direct_command(self) -> None: """Run yt-dlp as a direct command line process instead of using Python API.""" try: + self.error_lines = [] # Initialize error capture list cmd: List[str] = self._build_yt_dlp_command() + cmd_str: str = " ".join(shlex.quote(str(arg)) for arg in cmd) logger.debug(f"Executing command: {cmd_str}") @@ -508,16 +510,19 @@ class DownloadThread(QThread): if self.cancelled: self.status_signal.emit(_("download.cancelled")) else: - # Provide more descriptive error message for possible yt-dlp conflicts - if return_code == 1: + # Provide informative error message based on captured output + if self.error_lines: + # Use the captured error lines (last 2 for context) + error_msg = "\n".join(self.error_lines[-2:]) self.error_signal.emit( - _("errors.download_failed_return_code_conflict", return_code=return_code) + _("errors.ytdlp_failed", error=error_msg) ) else: + # Fallback to generic return code error self.error_signal.emit( _("errors.download_failed_return_code", return_code=return_code) ) - + # Add delay before cleanup to allow file handles to be released time.sleep(1) self.cleanup_partial_files() @@ -534,6 +539,11 @@ class DownloadThread(QThread): line = line.strip() # logger.info(f"yt-dlp: {line}") # Log all output - OPTIONALLY UNCOMMENT FOR VERBOSE DEBUG + # Capture error lines + if "ERROR:" in line: + if hasattr(self, 'error_lines'): + self.error_lines.append(line) + # Extract filename when the destination line appears # Use a slightly more robust regex looking for the start of the line dest_match = re.search(r"^\[download\] Destination:\s*(.*)", line) From 2d42be1a0c1ed70eceae5f2f07692820d6c1c3af Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:21:41 +0200 Subject: [PATCH 076/134] Add animated fade for status and controls Introduce smooth UI transitions by adding opacity-based animations. Import QGraphicsOpacityEffect and add animate_widget_fade_in, animate_widget_fade_out, and set_status_message_animated to cross-fade status text and fade buttons in/out. Update signal handlers and download flow to use the new animated status setter and fade methods instead of direct setText/setVisible calls. Keep animation references on widgets to avoid premature GC and remove effects after fade-out. --- ytsage/gui/ytsage_gui_main.py | 119 ++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 8b6c728..411d7de 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -26,6 +26,7 @@ from PySide6.QtWidgets import ( QTextEdit, QVBoxLayout, QWidget, + QGraphicsOpacityEffect, ) from .. import __version__ as APP_VERSION @@ -505,7 +506,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Connect signals self.signals.update_formats.connect(self.update_format_table) - self.signals.update_status.connect(self.status_label.setText) + self.signals.update_status.connect(self.set_status_message_animated) self.signals.update_progress.connect(self.update_progress_bar) # Connect new signals @@ -698,7 +699,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Connect signals self.download_thread.progress_signal.connect(self.update_progress_bar) - self.download_thread.status_signal.connect(self.status_label.setText) + self.download_thread.status_signal.connect(self.set_status_message_animated) self.download_thread.update_details.connect(self.download_details_label.setText) self.download_thread.finished_signal.connect(self.download_finished) self.download_thread.error_signal.connect(self.download_error) @@ -710,8 +711,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Show pause/cancel buttons self.pause_btn.setText(_("buttons.pause")) - self.pause_btn.setVisible(True) - self.cancel_btn.setVisible(True) + self.animate_widget_fade_in(self.pause_btn) + self.animate_widget_fade_in(self.cancel_btn) # Start download thread self.current_download = self.download_thread @@ -720,8 +721,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def download_finished(self) -> None: self.toggle_download_controls(True) - self.pause_btn.setVisible(False) - self.cancel_btn.setVisible(False) + self.animate_widget_fade_out(self.pause_btn) + self.animate_widget_fade_out(self.cancel_btn) self.progress_bar.setValue(10000) # 100% in 0-10000 range # Set completion message based on the file type of last downloaded file @@ -731,19 +732,19 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Video file extensions if ext in VIDEO_EXTENSIONS: - self.status_label.setText(_('download.video_completed')) + self.set_status_message_animated(_('download.video_completed')) # Audio file extensions elif ext in AUDIO_EXTENSIONS: - self.status_label.setText(_('download.audio_completed')) + self.set_status_message_animated(_('download.audio_completed')) # Subtitle file extensions elif ext in SUBTITLE_EXTENSIONS: - self.status_label.setText(_('download.subtitle_completed')) + self.set_status_message_animated(_('download.subtitle_completed')) # Default case else: - self.status_label.setText(_('download.completed')) + self.set_status_message_animated(_('download.completed')) # Show the open folder button - self.open_folder_btn.setVisible(True) + self.animate_widget_fade_in(self.open_folder_btn) # Save to history try: @@ -1409,7 +1410,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def cancel_download(self) -> None: if self.current_download: self.current_download.cancelled = True - self.status_label.setText(_("status.cancelling")) # Set status directly + self.set_status_message_animated(_("status.cancelling")) # Set status directly self.download_details_label.setText("") # Clear details label on cancellation def show_ffmpeg_dialog(self) -> None: @@ -1458,4 +1459,98 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) success_dialog.exec() + def animate_widget_fade_in(self, widget: QWidget, duration: int = 300) -> None: + """Fade in a widget using opacity animation.""" + if widget.isVisible(): + return + + # Setup opacity effect if not present + effect = QGraphicsOpacityEffect(widget) + widget.setGraphicsEffect(effect) + + # Determine start and end values + start_val = 0.0 + end_val = 1.0 + + # Setup animation + anim = QPropertyAnimation(effect, b"opacity", widget) + anim.setDuration(duration) + anim.setStartValue(start_val) + anim.setEndValue(end_val) + anim.setEasingCurve(QEasingCurve.Type.OutQuad) + + # Show widget before animation + widget.setVisible(True) + anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + # Keep reference to avoid garbage collection + widget._fade_anim = anim + + def animate_widget_fade_out(self, widget: QWidget, duration: int = 300) -> None: + """Fade out a widget and then hide it.""" + if not widget.isVisible(): + return + + effect = QGraphicsOpacityEffect(widget) + widget.setGraphicsEffect(effect) + + anim = QPropertyAnimation(effect, b"opacity", widget) + anim.setDuration(duration) + anim.setStartValue(1.0) + anim.setEndValue(0.0) + anim.setEasingCurve(QEasingCurve.Type.InQuad) + + def on_finished(): + widget.setVisible(False) + widget.setGraphicsEffect(None) # Remove effect to restore normal painting + + anim.finished.connect(on_finished) + anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + widget._fade_out_anim = anim # Keep reference + + def set_status_message_animated(self, message: str) -> None: + """Update status label with a cross-fade animation.""" + if self.status_label.text() == message: + return + + # If previous animation is running, stop it + if hasattr(self.status_label, '_status_anim'): + try: + if self.status_label._status_anim.state() == QPropertyAnimation.State.Running: + self.status_label._status_anim.stop() + except RuntimeError: + pass # Animation object already deleted + + # Create effect if needed + effect = self.status_label.graphicsEffect() + if not effect or not isinstance(effect, QGraphicsOpacityEffect): + effect = QGraphicsOpacityEffect(self.status_label) + self.status_label.setGraphicsEffect(effect) + + # 1. Fade OUT + anim1 = QPropertyAnimation(effect, b"opacity", self.status_label) + anim1.setDuration(150) + anim1.setStartValue(1.0) + anim1.setEndValue(0.0) + anim1.setEasingCurve(QEasingCurve.Type.OutQuad) + + # 2. Change Text & Fade IN + anim2 = QPropertyAnimation(effect, b"opacity", self.status_label) + anim2.setDuration(150) + anim2.setStartValue(0.0) + anim2.setEndValue(1.0) + anim2.setEasingCurve(QEasingCurve.Type.InQuad) + + def on_fade_out_finished(): + self.status_label.setText(message) + anim2.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + anim1.finished.connect(on_fade_out_finished) + anim1.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + # Store ref + self.status_label._status_anim = anim1 + self.status_label._status_anim2 = anim2 + From 621d47d7012a4304ef46f5d6d2fc4051cddd87e0 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:26:47 +0200 Subject: [PATCH 077/134] Hide and fade-in format table on build Make the format table invisible while it's populated and filtered to avoid visual artifacts, then animate its appearance with animate_widget_fade_in after initial filtering. This ensures a smoother UI when the table is first built. --- ytsage/gui/ytsage_gui_format_table.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ytsage/gui/ytsage_gui_format_table.py b/ytsage/gui/ytsage_gui_format_table.py index b3794ac..2462297 100644 --- a/ytsage/gui/ytsage_gui_format_table.py +++ b/ytsage/gui/ytsage_gui_format_table.py @@ -233,11 +233,15 @@ class FormatTableMixin: all_filtered = [(f, "video") for f in video_formats] + [(f, "audio") for f in audio_formats] # Build table with format type tracking + self.format_table.setVisible(False) self._populate_format_table(all_filtered) self._table_built = True # Apply initial visibility based on current button states self.filter_formats() + + # Animate table appearance + self.animate_widget_fade_in(self.format_table) def _populate_format_table(self, formats_with_types: list) -> None: """Populate the format table with formats and their types.""" From f9a9dc4879ffd61f58d1a202867f925cee98290d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:26:57 +0200 Subject: [PATCH 078/134] Animate widget visibility and fix fades Use animated show/hide for playlist widgets and harden fade animations. Replace direct setVisible signal handlers with lambdas that call set_widget_visible_animated, and add set_widget_visible_animated to route to fade-in/out. Fade methods now stop opposing animations, reuse existing QGraphicsOpacityEffect when present, preserve current opacity as start values, and set effect opacity when creating a new effect. Also catch possible RuntimeError when stopping animations and keep animation references on widgets to avoid premature GC. --- ytsage/gui/ytsage_gui_main.py | 51 ++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 411d7de..43748ca 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -510,10 +510,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.signals.update_progress.connect(self.update_progress_bar) # Connect new signals - self.signals.playlist_info_label_visible.connect(self.playlist_info_label.setVisible) + self.signals.playlist_info_label_visible.connect(lambda v: self.set_widget_visible_animated(self.playlist_info_label, v)) self.signals.playlist_info_label_text.connect(self.playlist_info_label.setText) self.signals.selected_subs_label_text.connect(self.selected_subs_label.setText) - self.signals.playlist_select_btn_visible.connect(self.playlist_select_btn.setVisible) + self.signals.playlist_select_btn_visible.connect(lambda v: self.set_widget_visible_animated(self.playlist_select_btn, v)) self.signals.playlist_select_btn_text.connect(self.playlist_select_btn.setText) # Disable analysis-dependent controls until video is analyzed @@ -1461,15 +1461,25 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def animate_widget_fade_in(self, widget: QWidget, duration: int = 300) -> None: """Fade in a widget using opacity animation.""" - if widget.isVisible(): + # Stop fade out if running + if hasattr(widget, '_fade_out_anim'): + try: + if widget._fade_out_anim.state() == QPropertyAnimation.State.Running: + widget._fade_out_anim.stop() + except RuntimeError: + pass + + if widget.isVisible() and widget.graphicsEffect() is None: return # Setup opacity effect if not present - effect = QGraphicsOpacityEffect(widget) - widget.setGraphicsEffect(effect) + effect = widget.graphicsEffect() + if not effect or not isinstance(effect, QGraphicsOpacityEffect): + effect = QGraphicsOpacityEffect(widget) + widget.setGraphicsEffect(effect) - # Determine start and end values - start_val = 0.0 + # Determine start value (current opacity if previously animating) + start_val = effect.opacity() if widget.isVisible() else 0.0 end_val = 1.0 # Setup animation @@ -1488,15 +1498,28 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def animate_widget_fade_out(self, widget: QWidget, duration: int = 300) -> None: """Fade out a widget and then hide it.""" + # Stop fade in if running + if hasattr(widget, '_fade_anim'): + try: + if widget._fade_anim.state() == QPropertyAnimation.State.Running: + widget._fade_anim.stop() + except RuntimeError: + pass + if not widget.isVisible(): return - effect = QGraphicsOpacityEffect(widget) - widget.setGraphicsEffect(effect) + effect = widget.graphicsEffect() + if not effect or not isinstance(effect, QGraphicsOpacityEffect): + effect = QGraphicsOpacityEffect(widget) + widget.setGraphicsEffect(effect) + effect.setOpacity(1.0) + + start_val = effect.opacity() anim = QPropertyAnimation(effect, b"opacity", widget) anim.setDuration(duration) - anim.setStartValue(1.0) + anim.setStartValue(start_val) anim.setEndValue(0.0) anim.setEasingCurve(QEasingCurve.Type.InQuad) @@ -1509,6 +1532,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): widget._fade_out_anim = anim # Keep reference + def set_widget_visible_animated(self, widget: QWidget, visible: bool) -> None: + """Toggle widget visibility with fade animation.""" + if visible: + self.animate_widget_fade_in(widget) + else: + self.animate_widget_fade_out(widget) + + def set_status_message_animated(self, message: str) -> None: """Update status label with a cross-fade animation.""" if self.status_label.text() == message: From 091c6c68c19442c785d47f02d827399a8da98cd3 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:27:07 +0200 Subject: [PATCH 079/134] Fade in video thumbnail on load Hide the thumbnail QLabel before assigning the pixmap and trigger animate_widget_fade_in to smoothly fade the thumbnail into view. Keeps existing exception logging for thumbnail processing. --- ytsage/gui/ytsage_gui_video_info.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ytsage/gui/ytsage_gui_video_info.py b/ytsage/gui/ytsage_gui_video_info.py index f081136..aa3327b 100644 --- a/ytsage/gui/ytsage_gui_video_info.py +++ b/ytsage/gui/ytsage_gui_video_info.py @@ -410,7 +410,12 @@ class VideoInfoMixin: image.save(img_byte_arr, format="PNG") pixmap = QPixmap() pixmap.loadFromData(img_byte_arr.getvalue()) + + # Fade in the thumbnail + self.thumbnail_label.setVisible(False) self.thumbnail_label.setPixmap(pixmap) + self.animate_widget_fade_in(self.thumbnail_label) + except Exception as e: logger.exception(f"Error processing thumbnail image: {e}") From 36eff02b18c712d50259fdbf0236842a6a13a74c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:36:00 +0200 Subject: [PATCH 080/134] Add widget shake animation for invalid input Introduce a shake animation to visually indicate invalid inputs and wire it into validation/error flows. Changes: import QPoint; add animate_widget_shake(QWidget) implementation that uses QPropertyAnimation on widget.pos with keyframes; call the animation when URL/path/format validations fail and replace some direct status_label updates with set_status_message_animated. Updated files: ytsage/gui/ytsage_gui_main.py and ytsage/gui/ytsage_gui_analysis.py. --- ytsage/gui/ytsage_gui_analysis.py | 4 +++ ytsage/gui/ytsage_gui_main.py | 46 +++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/ytsage/gui/ytsage_gui_analysis.py b/ytsage/gui/ytsage_gui_analysis.py index 0a37739..46604ce 100644 --- a/ytsage/gui/ytsage_gui_analysis.py +++ b/ytsage/gui/ytsage_gui_analysis.py @@ -271,12 +271,16 @@ class AnalysisMixin: url = self.url_input.text().strip() if not url: self.signals.update_status.emit(_("main_ui.invalid_url_or_enter")) + if hasattr(self, "animate_widget_shake"): + self.animate_widget_shake(self.url_input) return # Validate URL before processing is_valid, error_message = validate_video_url(url) if not is_valid: QMessageBox.warning(self, _("main_ui.error_title"), error_message) + if hasattr(self, "animate_widget_shake"): + self.animate_widget_shake(self.url_input) return # Cancel any existing analysis thread diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 43748ca..b48cd6f 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -7,7 +7,7 @@ from pathlib import Path import markdown import requests from packaging import version -from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl, QPropertyAnimation, QEasingCurve +from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer, Slot, QThread, Signal, QUrl, QPropertyAnimation, QEasingCurve, QPoint from PySide6.QtGui import QIcon from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from PySide6.QtWidgets import ( @@ -598,11 +598,15 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): if not url or not path: # More specific error message if path is missing if not path: - self.status_label.setText(_('download.please_set_path')) + self.set_status_message_animated(_('download.please_set_path')) + self.animate_widget_shake(self.settings_button) elif not url: - self.status_label.setText(_('download.please_enter_url')) + self.set_status_message_animated(_('download.please_enter_url')) + self.animate_widget_shake(self.url_input) else: - self.status_label.setText(_('download.please_enter_url_and_path')) + self.set_status_message_animated(_('download.please_enter_url_and_path')) + self.animate_widget_shake(self.url_input) + self.animate_widget_shake(self.settings_button) return # --- End Path Change --- @@ -610,12 +614,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): is_valid, error_message = validate_video_url(url) if not is_valid: QMessageBox.warning(self, _("main_ui.error_title"), error_message) + self.animate_widget_shake(self.url_input) return # Get selected format selected_format = self.get_selected_format() if not selected_format: - self.status_label.setText(_('download.please_select_format')) + self.set_status_message_animated(_('download.please_select_format')) + self.animate_widget_shake(self.format_table) return format_id = selected_format["format_id"] is_audio_only = bool(selected_format.get("is_audio_only")) @@ -1539,6 +1545,36 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): else: self.animate_widget_fade_out(widget) + def animate_widget_shake(self, widget: QWidget) -> None: + """Shake a widget left and right to indicate an error or invalid input.""" + # Stop shake if running + if hasattr(widget, '_shake_anim'): + try: + if widget._shake_anim.state() == QPropertyAnimation.State.Running: + return + except RuntimeError: + pass + + # Use current position as baseline + pos = widget.pos() + x = pos.x() + y = pos.y() + + anim = QPropertyAnimation(widget, b"pos", widget) + anim.setDuration(300) + anim.setLoopCount(1) + + # Create keyframes for shake effect + anim.setKeyValueAt(0, QPoint(x, y)) + anim.setKeyValueAt(0.2, QPoint(x - 5, y)) + anim.setKeyValueAt(0.4, QPoint(x + 5, y)) + anim.setKeyValueAt(0.6, QPoint(x - 5, y)) + anim.setKeyValueAt(0.8, QPoint(x + 5, y)) + anim.setKeyValueAt(1.0, QPoint(x, y)) + + widget._shake_anim = anim + anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + def set_status_message_animated(self, message: str) -> None: """Update status label with a cross-fade animation.""" From 7890a947029c26076d307a7c78adb3aecee6e973 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:51:36 +0200 Subject: [PATCH 081/134] Add pressed padding to QPushButton styles Update ytsage/gui/ytsage_stylesheet.py to add explicit padding for various QPushButton:pressed rules (and QPushButton:checked:pressed) across multiple style blocks. These changes unify the visual spacing and touch feedback when buttons are pressed, ensuring consistent pressed-state layout and appearance. --- ytsage/gui/ytsage_stylesheet.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ytsage/gui/ytsage_stylesheet.py b/ytsage/gui/ytsage_stylesheet.py index 465a8ad..c1f7841 100644 --- a/ytsage/gui/ytsage_stylesheet.py +++ b/ytsage/gui/ytsage_stylesheet.py @@ -32,6 +32,7 @@ class StyleSheet: } QPushButton:pressed { background-color: #800000; + padding: 10px 13px 6px 17px; } QPushButton:disabled { background-color: #3d3d3d; @@ -141,6 +142,7 @@ class StyleSheet: } QPushButton:pressed { background-color: #1a1d1e; + padding: 11px 18px 7px 22px; } """ @@ -159,6 +161,7 @@ class StyleSheet: } QPushButton:pressed { background-color: #800000; + padding: 11px 18px 7px 22px; } QPushButton:disabled { background-color: #3d3d3d; @@ -181,6 +184,10 @@ class StyleSheet: background-color: #2a2d36; border-color: #a50000; } + QPushButton:pressed { + background-color: #1d1e22; + padding: 8px 10px 4px 12px; + } """ FORMAT_TOGGLE_BUTTON = """ @@ -201,6 +208,14 @@ class StyleSheet: QPushButton:checked:hover { background-color: #a50000; } + QPushButton:pressed { + background-color: #151619; + padding: 10px 13px 6px 17px; + } + QPushButton:checked:pressed { + background-color: #800000; + padding: 10px 13px 6px 17px; + } """ CHECKBOX = """ @@ -266,6 +281,7 @@ class StyleSheet: } QPushButton:pressed { background-color: #1a1d1e; + padding: 4px 0px 0px 4px; } """ From 79a85cf707f7fa5601cbfbb3b4f46afe10990b4d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:23:32 +0200 Subject: [PATCH 082/134] Run dialogs with blurred screenshot overlay Add a safe blurred-overlay flow for modal dialogs by implementing run_dialog_with_blur and _apply_blur_to_pixmap on YTSageApp. The new flow captures a window screenshot, generates a blurred/dimmed pixmap via a QGraphicsScene + QGraphicsBlurEffect, shows it in an overlay QLabel with a fade-in, runs the dialog, and then removes the overlay. Replace many direct dialog.exec() calls with run_dialog_with_blur(...) across the main GUI (download settings, about, history, playlist selection, custom options, cookie login, ffmpeg check, time range, setup success dialogs, etc.). Also add imports required for pixmap/graphics handling. Make animate_widget_fade_in usage safer by checking for the method before calling it and falling back to setVisible(True) for format table and thumbnails to avoid QPainter/graphics-effect conflicts. Small comment/cleanup in fade-in/out methods. --- ytsage/gui/ytsage_gui_format_table.py | 5 +- ytsage/gui/ytsage_gui_main.py | 104 +++++++++++++++++++++++--- ytsage/gui/ytsage_gui_video_info.py | 10 ++- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/ytsage/gui/ytsage_gui_format_table.py b/ytsage/gui/ytsage_gui_format_table.py index 2462297..cd3f029 100644 --- a/ytsage/gui/ytsage_gui_format_table.py +++ b/ytsage/gui/ytsage_gui_format_table.py @@ -241,7 +241,10 @@ class FormatTableMixin: self.filter_formats() # Animate table appearance - self.animate_widget_fade_in(self.format_table) + if hasattr(self, "animate_widget_fade_in"): + self.animate_widget_fade_in(self.format_table) + else: + self.format_table.setVisible(True) def _populate_format_table(self, formats_with_types: list) -> None: """Populate the format table with formats and their types.""" diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index b48cd6f..5b1c7bb 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -27,7 +27,11 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, QGraphicsOpacityEffect, + QGraphicsBlurEffect, + QGraphicsScene, + QGraphicsPixmapItem, ) +from PySide6.QtGui import QIcon, QPixmap, QPainter, QBrush, QColor from .. import __version__ as APP_VERSION from ..core.ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes @@ -531,7 +535,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def show_download_settings_dialog(self) -> None: # Renamed method dialog = DownloadSettingsDialog(self.last_path, self.speed_limit_value, self.speed_limit_unit_index, self) - if dialog.exec(): + if self.run_dialog_with_blur(dialog): # Update Path new_path = dialog.get_selected_path() path_changed = False @@ -978,7 +982,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Style the dialog with improved theme matching msg.setStyleSheet(StyleSheet.UPDATE_DIALOG_MAIN) - msg.show() + self.run_dialog_with_blur(msg) def open_release_page(self, url): webbrowser.open(url) @@ -1070,7 +1074,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def show_custom_options(self) -> None: dialog = CustomOptionsDialog(self) - if dialog.exec(): + if self.run_dialog_with_blur(dialog): # Handle proxy options proxy_url = dialog.get_proxy_url() geo_proxy_url = dialog.get_geo_proxy_url() @@ -1110,13 +1114,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def show_about_dialog(self) -> None: # ADDED METHOD HERE dialog = AboutDialog(self) - dialog.exec() + self.run_dialog_with_blur(dialog) def show_history_dialog(self) -> None: """Show the download history dialog.""" dialog = HistoryDialog(self) dialog.redownload_requested.connect(self.handle_redownload_from_history) - dialog.exec() + self.run_dialog_with_blur(dialog) def handle_redownload_from_history(self, entry: dict) -> None: """Handle redownload request from history dialog.""" @@ -1208,7 +1212,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): dialog = PlaylistSelectionDialog(self.playlist_entries, self.selected_playlist_items, self) - if dialog.exec(): + if self.run_dialog_with_blur(dialog): self.selected_playlist_items = dialog.get_selected_items_string() logger.info(f"Playlist items selected: {self.selected_playlist_items}") @@ -1381,12 +1385,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def show_custom_command(self) -> None: dialog = CustomOptionsDialog(self) dialog.tab_widget.setCurrentIndex(1) # Select the Custom Command tab - dialog.exec() + self.run_dialog_with_blur(dialog) def show_cookie_login_dialog(self) -> None: dialog = CustomOptionsDialog(self) dialog.tab_widget.setCurrentIndex(0) # Select the Cookie Login tab - if dialog.exec(): + if self.run_dialog_with_blur(dialog): # Handle cookies cookie_path = dialog.get_cookie_file_path() browser_cookies = dialog.get_browser_cookies_option() @@ -1421,12 +1425,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def show_ffmpeg_dialog(self) -> None: dialog = FFmpegCheckDialog(self) - dialog.exec() + self.run_dialog_with_blur(dialog) # Add method for showing time range dialog def show_time_range_dialog(self) -> None: dialog = TimeRangeDialog(self) - if dialog.exec(): + if self.run_dialog_with_blur(dialog): # Store the time range settings self.download_section = dialog.get_download_sections() self.force_keyframes = dialog.get_force_keyframes() @@ -1451,7 +1455,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): success_dialog.setText(_("ytdlp_setup.success_dialog_message", path=yt_dlp_path)) success_dialog.setWindowIcon(self.windowIcon()) success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) - success_dialog.exec() + self.run_dialog_with_blur(success_dialog) def show_deno_setup_dialog(self) -> None: """Show the Deno setup dialog to configure Deno""" @@ -1463,10 +1467,15 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): success_dialog.setText(f"{_('deno.success')}\n{deno_path}") success_dialog.setWindowIcon(self.windowIcon()) success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG) - success_dialog.exec() + self.run_dialog_with_blur(success_dialog) def animate_widget_fade_in(self, widget: QWidget, duration: int = 300) -> None: """Fade in a widget using opacity animation.""" + # Check if the main window has a graphics effect (blur) active. + # If so, animating a child widget with another effect causes QPainter conflicts. + # NOTE: With the new Screenshot Overlay method, 'self.graphicsEffect()' on the MainWindow isn't used. + # However, we should still be careful if the widget itself already has an effect. + # Stop fade out if running if hasattr(widget, '_fade_out_anim'): try: @@ -1504,6 +1513,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): def animate_widget_fade_out(self, widget: QWidget, duration: int = 300) -> None: """Fade out a widget and then hide it.""" + # Stop fade in if running if hasattr(widget, '_fade_anim'): try: @@ -1620,4 +1630,74 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.status_label._status_anim = anim1 self.status_label._status_anim2 = anim2 + def run_dialog_with_blur(self, dialog: QDialog) -> int: + """Run a dialog with a static background screenshot blur to avoid QPainter conflicts.""" + + # 1. Capture the current state of the window (screenshot) + pixmap = self.grab() + + # 2. Create the blur using a Graphics Scene method (much safer than QGraphicsBlurEffect on live widget) + # However, for simplicity and performance with PySide6, we can just apply a blur to the image + # or use a simplified overlay. + # Let's manually blur the pixmap or use a simpler transparent overlay if blur is too heavy manually. + # Actually, using QGraphicsBlurEffect on a temporary QGraphicsScene rendering to a pixmap is a valid way + # to generate a single blurred frame. + + blurred_pixmap = self._apply_blur_to_pixmap(pixmap, radius=10) + + # 3. Create an overlay widget that covers the Main Window + overlay = QLabel(self) + overlay.setPixmap(blurred_pixmap) + overlay.setGeometry(0, 0, self.width(), self.height()) + overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) # Block mouse + overlay.show() + + # Animate overlay Fade In + opacity_effect = QGraphicsOpacityEffect(overlay) + overlay.setGraphicsEffect(opacity_effect) + + anim = QPropertyAnimation(opacity_effect, b"opacity", overlay) + anim.setDuration(250) + anim.setStartValue(0.0) + anim.setEndValue(1.0) + anim.setEasingCurve(QEasingCurve.Type.OutQuad) + anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + # 4. Run the dialog + # Ensure dialog is on top + result = dialog.exec() + + # 5. Remove overlay + overlay.deleteLater() + + return result + + def _apply_blur_to_pixmap(self, source_pixmap: QPixmap, radius: int) -> QPixmap: + """Helper to create a blurred version of a pixmap.""" + if source_pixmap.isNull(): + return source_pixmap + + # Create a QGraphicsScene to apply the effect to the pixmap + scene = QGraphicsScene() + item = QGraphicsPixmapItem(source_pixmap) + scene.addItem(item) + + blur = QGraphicsBlurEffect() + blur.setBlurRadius(radius) + item.setGraphicsEffect(blur) + + # Render back to pixmap + result = source_pixmap.copy() # Match size/format + result.fill(Qt.GlobalColor.transparent) + + with QPainter(result) as painter: + scene.render(painter, target=source_pixmap.rect().toRectF(), source=source_pixmap.rect().toRectF()) + + # Optional: Add a dark tint for 'dimming' effect + painter.setBrush(QBrush(QColor(0, 0, 0, 100))) # 100/255 opacity black + painter.setPen(Qt.PenStyle.NoPen) + painter.drawRect(source_pixmap.rect()) + + return result + diff --git a/ytsage/gui/ytsage_gui_video_info.py b/ytsage/gui/ytsage_gui_video_info.py index aa3327b..253cc96 100644 --- a/ytsage/gui/ytsage_gui_video_info.py +++ b/ytsage/gui/ytsage_gui_video_info.py @@ -310,7 +310,7 @@ class VideoInfoMixin: # removed extra logic for mapping to main_windows merge_checkbox = getattr(self, "merge_subs_checkbox", None) - if dialog.exec(): # If user clicks OK + if self.run_dialog_with_blur(dialog): # If user clicks OK self.selected_subtitles = dialog.get_selected_subtitles() logger.info(f"Selected subtitles: {self.selected_subtitles}") # Update UI to reflect selection @@ -356,7 +356,7 @@ class VideoInfoMixin: dialog = SponsorBlockCategoryDialog(dialog_categories, self) - if dialog.exec(): + if self.run_dialog_with_blur(dialog): self.selected_sponsorblock_categories = dialog.get_selected_categories() logger.info(f"SponsorBlock categories selected: {self.selected_sponsorblock_categories}") self._update_sponsorblock_display() @@ -414,7 +414,11 @@ class VideoInfoMixin: # Fade in the thumbnail self.thumbnail_label.setVisible(False) self.thumbnail_label.setPixmap(pixmap) - self.animate_widget_fade_in(self.thumbnail_label) + + if hasattr(self, "animate_widget_fade_in"): + self.animate_widget_fade_in(self.thumbnail_label) + else: + self.thumbnail_label.setVisible(True) except Exception as e: logger.exception(f"Error processing thumbnail image: {e}") From 2935eb01c1750cf2ba5da8b493df8e11909a33e8 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:30:54 +0200 Subject: [PATCH 083/134] Add smooth tab widget with fade transitions Introduce SmoothTabWidget and a FadingStackedWidget to provide cross-fade transitions between tabs. Added ytsage/gui/ytsage_smooth_tab_widget.py which implements a QTabBar + FadingStackedWidget (default 300ms fade) and overlay-based fade logic. Updated ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py to use SmoothTabWidget instead of QTabWidget and adjusted the stylesheet selector to target the new content frame (#tabContent) so existing styles continue to apply. --- .../ytsage_dialogs_custom.py | 5 +- ytsage/gui/ytsage_smooth_tab_widget.py | 123 ++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 ytsage/gui/ytsage_smooth_tab_widget.py diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index 49de1fd..5cd798c 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -29,6 +29,7 @@ from PySide6.QtWidgets import ( QWidget, ) +from ..ytsage_smooth_tab_widget import SmoothTabWidget from ...core.ytsage_yt_dlp import get_yt_dlp_path from ...core.ytsage_utils import update_auto_update_settings from ...utils.ytsage_constants import YTDLP_DOCS_URL @@ -115,7 +116,7 @@ class CustomOptionsDialog(QDialog): layout = QVBoxLayout(self) # Create tab widget to organize content - self.tab_widget = QTabWidget() + self.tab_widget = SmoothTabWidget() layout.addWidget(self.tab_widget) # === Cookies Tab === @@ -542,7 +543,7 @@ class CustomOptionsDialog(QDialog): QDialog { background-color: #15181b; } - QTabWidget::pane { + QFrame#tabContent { border: 1px solid #3d3d3d; background-color: #15181b; } diff --git a/ytsage/gui/ytsage_smooth_tab_widget.py b/ytsage/gui/ytsage_smooth_tab_widget.py new file mode 100644 index 0000000..375b2ba --- /dev/null +++ b/ytsage/gui/ytsage_smooth_tab_widget.py @@ -0,0 +1,123 @@ +from PySide6.QtCore import QEasingCurve, QPropertyAnimation, Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import ( + QFrame, + QGraphicsOpacityEffect, + QLabel, + QStackedWidget, + QTabBar, + QVBoxLayout, + QWidget, +) + +class FadingStackedWidget(QStackedWidget): + """ + A QStackedWidget that cross-fades between widgets. + """ + def __init__(self, parent=None): + super().__init__(parent) + self.fade_duration = 300 + self.fade_easing = QEasingCurve.Type.OutQuad + + def setCurrentIndex(self, index): + curr_index = self.currentIndex() + if index == curr_index: + return + + widget = self.widget(index) + curr_widget = self.widget(curr_index) + + # If widget isn't visible or valid, just swap + if not self.isVisible() or not curr_widget: + super().setCurrentIndex(index) + return + + # 1. Capture the current view (the "old" tab) + # Use grab() for simplicity and reliability in PySide6 + pixmap = self.grab() + + # 2. Create an overlay label to hold this "old" view + overlay = QLabel(self) + overlay.setPixmap(pixmap) + overlay.setGeometry(0, 0, self.width(), self.height()) + overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # Don't block clicks + overlay.show() + + # 3. Switch the actual stack to the "new" view + super().setCurrentIndex(index) + + # CRITICAL: Ensure overlay stays on top of the new widget + overlay.raise_() + + # 4. Fade OUT the overlay, revealing the new view + effect = QGraphicsOpacityEffect(overlay) + overlay.setGraphicsEffect(effect) + + + anim = QPropertyAnimation(effect, b"opacity", overlay) + anim.setDuration(self.fade_duration) + anim.setStartValue(1.0) + anim.setEndValue(0.0) + anim.setEasingCurve(self.fade_easing) + + # Cleanup when done + anim.finished.connect(lambda: self._cleanup(overlay)) + + # Keep reference to prevent GC + self._active_anim = anim + anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) + + def _cleanup(self, overlay): + overlay.hide() + overlay.deleteLater() + + +class SmoothTabWidget(QWidget): + """ + A unified Widget that behaves like a QTabWidget but uses smooth fading transitions. + Includes a QTabBar and a FadingStackedWidget. + """ + def __init__(self, parent=None): + super().__init__(parent) + + # Main Layout + self.layout = QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + + # Tab Bar + self.tab_bar = QTabBar(self) + self.tab_bar.setDrawBase(False) # We draw border on content instead + self.tab_bar.currentChanged.connect(self.set_current_index) + self.layout.addWidget(self.tab_bar) + + # Content Area (Frame) - Mimics QTabWidget::pane + self.content_frame = QFrame(self) + self.content_frame.setObjectName("tabContent") + + # Layout inside the content frame + self.content_layout = QVBoxLayout(self.content_frame) + self.content_layout.setContentsMargins(0, 0, 0, 0) + self.content_layout.setSpacing(0) + + # The Stack + self.stack = FadingStackedWidget(self.content_frame) + self.content_layout.addWidget(self.stack) + + self.layout.addWidget(self.content_frame) + + def addTab(self, widget, label): + """Add a tab with the given widget and label.""" + self.stack.addWidget(widget) + self.tab_bar.addTab(label) + + def set_current_index(self, index): + """Slot to handle tab bar clicks.""" + self.tab_bar.setCurrentIndex(index) + self.stack.setCurrentIndex(index) + + def currentWidget(self): + return self.stack.currentWidget() + + def currentIndex(self): + return self.stack.currentIndex() From 6c98d729ea205df4e4884752317bed68ed30f323 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:17:46 +0200 Subject: [PATCH 084/134] Optimize Windows builds and trim dist files Add build/time optimizations and post-build trimming for Windows CI. Enables silent cxfreeze output and adds zip_include_packages (PySide6, shiboken6, requests, PIL, packaging) so selected packages are included in the zip; excludes additional stdlib modules (pydoc, doctest, email) to reduce bundle size. Switches build invocation to python -OO for extra optimization and adds dedicated PowerShell steps (for both standard and FFmpeg builds) that remove screenshots, PDB debug files, unused Qt translations/plugins and extra Qt DLLs (Qt6Web*/Qt6Pdf*) to shrink artifacts and remove unnecessary files before packaging. --- .github/workflows/build-windows.yml | 91 ++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index ca70cbc..e656bdc 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -89,6 +89,7 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value "build_exe_options = dict(" Add-Content -Path "setup_cxfreeze.py" -Value " optimize=2," + Add-Content -Path "setup_cxfreeze.py" -Value " silent=True," Add-Content -Path "setup_cxfreeze.py" -Value " packages=[" Add-Content -Path "setup_cxfreeze.py" -Value ' "ytsage",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtCore",' @@ -103,6 +104,13 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "loguru",' Add-Content -Path "setup_cxfreeze.py" -Value ' "setuptools",' Add-Content -Path "setup_cxfreeze.py" -Value " ]," + Add-Content -Path "setup_cxfreeze.py" -Value " zip_include_packages=[" + Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "shiboken6",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",' + Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " excludes=[" Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtBluetooth",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtOpenGL",' @@ -124,6 +132,9 @@ jobs: Add-Content -Path "setup_cxfreeze.py" -Value ' "tkinter",' Add-Content -Path "setup_cxfreeze.py" -Value ' "yt_dlp",' Add-Content -Path "setup_cxfreeze.py" -Value ' "unittest",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "pydoc",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "doctest",' + Add-Content -Path "setup_cxfreeze.py" -Value ' "email",' Add-Content -Path "setup_cxfreeze.py" -Value ' "test",' Add-Content -Path "setup_cxfreeze.py" -Value ' "tests",' Add-Content -Path "setup_cxfreeze.py" -Value " ]," @@ -164,13 +175,39 @@ jobs: if (Test-Path "build\bdist.*") { Remove-Item "build\bdist.*" -Recurse -Force } if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force } - # Build executable using setup script - python setup_cxfreeze.py build_exe --build-exe "dist\YTSage" - - # Remove screenshots folder to reduce build size - if (Test-Path "dist\YTSage\lib\assets\branding\screenshots") { - Remove-Item "dist\YTSage\lib\assets\branding\screenshots" -Recurse -Force - Write-Host "Removed screenshots folder from standard build" + # Build executable using setup script with extra optimization + python -OO setup_cxfreeze.py build_exe --build-exe "dist\YTSage" + + - name: Trim unnecessary files from Standard build + shell: powershell + run: | + $distDir = "dist\YTSage" + if (Test-Path $distDir) { + # Remove screenshots + if (Test-Path "$distDir\lib\assets\branding\screenshots") { + Remove-Item "$distDir\lib\assets\branding\screenshots" -Recurse -Force + Write-Host "Removed screenshots folder" + } + # Remove debug PDB files + Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force + Write-Host "Removed PDB debug files" + # Remove unused Qt translations (if not needed for multilingual support) + if (Test-Path "$distDir\PySide6\translations") { + Remove-Item "$distDir\PySide6\translations" -Recurse -Force + Write-Host "Removed Qt translations" + } + # Remove unused Qt plugins (based on your excludes; adjust if needed) + $unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml") + foreach ($plugin in $unusedPlugins) { + if (Test-Path "$distDir\PySide6\plugins\$plugin") { + Remove-Item "$distDir\PySide6\plugins\$plugin" -Recurse -Force + Write-Host "Removed unused plugin: $plugin" + } + } + # Remove any duplicate or unnecessary DLLs (e.g., web-related) + Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Web*" | Remove-Item -Force + Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Pdf*" | Remove-Item -Force + Write-Host "Removed additional unnecessary Qt files" } - name: Setup FFmpeg for bundle @@ -232,13 +269,7 @@ jobs: (Get-Content "setup_cxfreeze.py") -replace 'YTSage-v\{version\}\.exe', 'YTSage-v{version}-ffmpeg.exe' | Set-Content "setup_cxfreeze_ffmpeg.py" # Build executable with FFmpeg using setup script - python setup_cxfreeze_ffmpeg.py build_exe --build-exe "dist\YTSage-FFmpeg" - - # Remove screenshots folder to reduce build size - if (Test-Path "dist\YTSage-FFmpeg\lib\assets\branding\screenshots") { - Remove-Item "dist\YTSage-FFmpeg\lib\assets\branding\screenshots" -Recurse -Force - Write-Host "Removed screenshots folder from FFmpeg build" - } + python -OO setup_cxfreeze_ffmpeg.py build_exe --build-exe "dist\YTSage-FFmpeg" # Copy FFmpeg binaries into dist folder post-build (more reliable than CLI include) # Note: ffplay.exe is excluded as it's not needed by the application @@ -257,6 +288,38 @@ jobs: } } } + + - name: Trim unnecessary files from FFmpeg build + shell: powershell + run: | + $distDir = "dist\YTSage-FFmpeg" + if (Test-Path $distDir) { + # Remove screenshots + if (Test-Path "$distDir\lib\assets\branding\screenshots") { + Remove-Item "$distDir\lib\assets\branding\screenshots" -Recurse -Force + Write-Host "Removed screenshots folder" + } + # Remove debug PDB files + Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force + Write-Host "Removed PDB debug files" + # Remove unused Qt translations (if not needed for multilingual support) + if (Test-Path "$distDir\PySide6\translations") { + Remove-Item "$distDir\PySide6\translations" -Recurse -Force + Write-Host "Removed Qt translations" + } + # Remove unused Qt plugins (based on your excludes; adjust if needed) + $unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml") + foreach ($plugin in $unusedPlugins) { + if (Test-Path "$distDir\PySide6\plugins\$plugin") { + Remove-Item "$distDir\PySide6\plugins\$plugin" -Recurse -Force + Write-Host "Removed unused plugin: $plugin" + } + } + # Remove any duplicate or unnecessary DLLs (e.g., web-related) + Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Web*" | Remove-Item -Force + Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Pdf*" | Remove-Item -Force + Write-Host "Removed additional unnecessary Qt files" + } - name: Package and prepare release artifacts (ZIPs) shell: powershell From bf9e11d08f13892b7129b16e4b67b2f77613b879 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:41:19 +0200 Subject: [PATCH 085/134] macOS build: trim .app bundle and packaging tweaks Improve macOS CI packaging by trimming unnecessary files from the built .app and adjusting build options. Changes include: - Enable cx_Freeze silent build and run Python with -OO for bdist_mac. - Add zip_include_packages to ensure key packages are bundled inside zip distributions. - Exclude additional stdlib modules (pydoc, doctest, email) to reduce size. - New workflow step to locate the generated .app and remove screenshots, Qt translations, unused PySide6 plugins and other Qt artifacts (Qt6Web*, Qt6Pdf*), with safer path detection and logging. - Remove noisy post-build directory listings and simplify DMG handling/log messages; make .app and .dmg discovery more robust. These changes aim to produce smaller, cleaner artifacts and more reliable CI packaging for macOS builds. --- .github/workflows/build-macos.yml | 87 +++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index e07755e..8ba0b6a 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -95,6 +95,7 @@ jobs: build_exe_options = dict( optimize=2, + silent=True, packages=[ "ytsage", "PySide6.QtCore", @@ -109,6 +110,13 @@ jobs: "loguru", "setuptools", ], + zip_include_packages=[ + "PySide6", + "shiboken6", + "requests", + "PIL", + "packaging", + ], excludes=[ "PySide6.QtBluetooth", "PySide6.QtOpenGL", @@ -132,6 +140,9 @@ jobs: "unittest", "test", "tests", + "pydoc", + "doctest", + "email", ], include_files=[ ("ytsage/assets/Icon", "lib/assets/Icon"), @@ -162,7 +173,6 @@ jobs: "bdist_dmg": { "volume_label": f"YTSage v{version}", "applications_shortcut": True, - # Sensible defaults; can be customized later if desired "format": "UDZO", "filesystem": "HFS+", "default_view": "icon-view", @@ -176,31 +186,67 @@ jobs: shell: bash run: | source venv/bin/activate - python setup_cxfreeze.py bdist_mac - # Locate the built .app (cx_Freeze may place it under build/ or dist/) + python -OO setup_cxfreeze.py bdist_mac + + - name: Trim unnecessary files from App bundle + shell: bash + run: | + version="${{ steps.get_version.outputs.VERSION }}" + + # Locate the built .app app_path="" - for cand in "dist/YTSage-v${VERSION}.app" "build/dist/YTSage-v${VERSION}.app" "build/YTSage-v${VERSION}.app"; do + for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do if [ -d "$cand" ]; then app_path="$cand"; break; fi done if [ -z "$app_path" ]; then app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) fi - if [ -n "$app_path" ] && [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then - rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" - echo "Removed screenshots folder from .app bundle at $app_path" + + if [ -n "$app_path" ]; then + echo "Processing App Bundle at: $app_path" + + # 1. Remove screenshots + if [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then + rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" + echo "Removed screenshots folder" + fi + + # 2. Remove unused Qt translations + # In macOS .app, Qt libs might be in Contents/MacOS/PySide6 or similar + # Search for translations folder within the app bundle + translations=$(find "$app_path" -type d -name "translations" | grep "PySide6" || true) + if [ -n "$translations" ]; then + rm -rf "$translations" + echo "Removed Qt translations at $translations" + fi + + # 3. Remove unused Qt plugins (matching Windows logic) + plugins_dir=$(find "$app_path" -type d -name "plugins" | grep "PySide6" | head -n1 || true) + if [ -n "$plugins_dir" ]; then + echo "Cleaning plugins in $plugins_dir" + for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do + if [ -d "$plugins_dir/$plugin" ]; then + rm -rf "$plugins_dir/$plugin" + echo "Removed plugin: $plugin" + fi + done + fi + + # 4. Remove other unused components if found + find "$app_path" -name "Qt6Web*" -delete + find "$app_path" -name "Qt6Pdf*" -delete + echo "Cleaned additional Qt files" + + else + echo "Error: Could not find .app bundle to trim!" + exit 1 fi - echo "Post-bdist_mac directory listing:" - echo "-- dist --"; ls -lah dist || true - echo "-- build --"; ls -lah build || true - name: Build DMG (bdist_dmg) shell: bash run: | source venv/bin/activate python setup_cxfreeze.py bdist_dmg - echo "Post-bdist_dmg directory listing:" - echo "-- dist --"; ls -lah dist || true - echo "-- build --"; ls -lah build || true - name: Package and prepare release artifacts (.app.zip and .dmg) shell: bash @@ -209,7 +255,7 @@ jobs: echo "Preparing artifacts for version: $version" mkdir -p artifacts - # Find the .app bundle (preferring versioned name) + # Find the .app bundle app_path="" for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do if [ -d "$cand" ]; then app_path="$cand"; break; fi @@ -217,21 +263,17 @@ jobs: if [ -z "$app_path" ]; then app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) fi + if [ -n "$app_path" ] && [ -d "$app_path" ]; then app_base="$(basename "$app_path")" app_parent="$(dirname "$app_path")" - # Ensure screenshots folder is not shipped - if [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then - rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" - echo "Removed screenshots folder from .app bundle at $app_path" - fi (cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base") echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" else echo "Warning: .app bundle not found in dist/ or build/" fi - # Try to find the generated DMG in dist or build + # Try to find the generated DMG dmg_src="" for cand in "dist/YTSage-v${version}.dmg"; do if [ -f "$cand" ]; then dmg_src="$cand"; break; fi @@ -241,11 +283,10 @@ jobs: fi if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then - # Rename the DMG to a consistent name in artifacts cp "$dmg_src" "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" - echo "Copied DMG to artifacts from $dmg_src -> artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" + echo "Copied DMG to artifacts" else - echo "Warning: No DMG found in dist/ or build/" + echo "Warning: No DMG found" fi echo "Final artifacts:" From cc3a00e1e09dd170b7470a886d15932e513624da Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:46:37 +0200 Subject: [PATCH 086/134] Enhance Linux build packaging and cleanup Adjust build-linux CI to produce smaller, self-contained AppImages: enable silent build output, add zip_include_packages so key libs (PySide6, shiboken6, requests, PIL, packaging) are packaged, and add extra excludes (pydoc, doctest, email). Use python -OO for optimized bytecode. Expand post-build trimming to remove screenshots, Qt translations, unused PySide6 plugins and other Qt artifacts (Qt6Web*, Qt6Pdf*) with informative logging. These changes reduce package size and avoid shipping unnecessary runtime components. --- .github/workflows/build-linux.yml | 56 +++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index f06de53..1a4991d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -109,6 +109,7 @@ jobs: build_exe_options = dict( optimize=2, + silent=True, packages=[ "ytsage", "PySide6.QtCore", @@ -123,6 +124,13 @@ jobs: "loguru", "setuptools", ], + zip_include_packages=[ + "PySide6", + "shiboken6", + "requests", + "PIL", + "packaging", + ], excludes=[ "PySide6.QtBluetooth", "PySide6.QtOpenGL", @@ -146,6 +154,9 @@ jobs: "unittest", "test", "tests", + "pydoc", + "doctest", + "email", ], include_files=include_files_list, # Bundle all dependencies - avoid system library references @@ -200,14 +211,47 @@ jobs: set -e source venv/bin/activate - # Ensure a clean build and build_exe first - python setup_cxfreeze.py build_exe + # Ensure a clean build and build_exe first with -OO + python -OO setup_cxfreeze.py build_exe - # Remove screenshots to reduce size before packaging + # Clean unnecessary files from build directory before packaging build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true) - if [ -n "$build_dir" ] && [ -d "$build_dir/lib/assets/branding/screenshots" ]; then - rm -rf "$build_dir/lib/assets/branding/screenshots" - echo "Removed screenshots folder from $build_dir" + + if [ -n "$build_dir" ]; then + echo "Trimming files in $build_dir..." + + # Remove screenshots + if [ -d "$build_dir/lib/assets/branding/screenshots" ]; then + rm -rf "$build_dir/lib/assets/branding/screenshots" + echo "Removed screenshots folder" + fi + + # Remove unused Qt translations + # In Linux builds, translations are often in lib/PySide6/translations or similar + translations=$(find "$build_dir" -type d -name "translations" | grep "PySide6" || true) + if [ -n "$translations" ]; then + rm -rf "$translations" + echo "Removed Qt translations at $translations" + fi + + # Remove unused Qt plugins (matching Windows logic) + plugins_dir=$(find "$build_dir" -type d -name "plugins" | grep "PySide6" | head -n1 || true) + if [ -n "$plugins_dir" ]; then + for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do + if [ -d "$plugins_dir/$plugin" ]; then + rm -rf "$plugins_dir/$plugin" + echo "Removed plugin: $plugin" + fi + done + fi + + # Remove other unused components if found + find "$build_dir" -name "Qt6Web*" -delete + find "$build_dir" -name "Qt6Pdf*" -delete + echo "Cleaned additional Qt files" + + else + echo "Warning: Build directory not found, cannot trim files" fi # Build AppImage From dab35a1a3a2f283cd84b84af208c8714408cec3b Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:04:30 +0200 Subject: [PATCH 087/134] Prune PySide6 files in Windows build workflow Refactor Windows build cleanup to use a dedicated $libDir and more aggressively prune unused PySide6/Qt files. Replaces direct $distDir references with $libDir for lib content and removes screenshots, PDBs, translations, many plugin folders, specific image/icon plugins (qpdf/qsvg/qsvgicon), platform input contexts, and common bulky Qt DLL patterns. Also removes several build tools/unused Python modules (setuptools, wheel, pkg_resources, _distutils_hack, curses, _pyrepl). Applied the same changes to both YTSage and YTSage-FFmpeg packaging steps to reduce bundle size and noise, and improved log messages for removed items. --- .github/workflows/build-windows.yml | 114 ++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index e656bdc..51af56a 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -182,32 +182,65 @@ jobs: shell: powershell run: | $distDir = "dist\YTSage" + $libDir = "$distDir\lib" if (Test-Path $distDir) { # Remove screenshots - if (Test-Path "$distDir\lib\assets\branding\screenshots") { - Remove-Item "$distDir\lib\assets\branding\screenshots" -Recurse -Force + if (Test-Path "$libDir\assets\branding\screenshots") { + Remove-Item "$libDir\assets\branding\screenshots" -Recurse -Force Write-Host "Removed screenshots folder" } # Remove debug PDB files Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force Write-Host "Removed PDB debug files" - # Remove unused Qt translations (if not needed for multilingual support) - if (Test-Path "$distDir\PySide6\translations") { - Remove-Item "$distDir\PySide6\translations" -Recurse -Force + + # Remove unused Qt translations (entire folder) + if (Test-Path "$libDir\PySide6\translations") { + Remove-Item "$libDir\PySide6\translations" -Recurse -Force Write-Host "Removed Qt translations" } + # Remove unused Qt plugins (based on your excludes; adjust if needed) + # Note: qpdf and qsvg plugins are removed specifically $unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml") foreach ($plugin in $unusedPlugins) { - if (Test-Path "$distDir\PySide6\plugins\$plugin") { - Remove-Item "$distDir\PySide6\plugins\$plugin" -Recurse -Force - Write-Host "Removed unused plugin: $plugin" + if (Test-Path "$libDir\PySide6\plugins\$plugin") { + Remove-Item "$libDir\PySide6\plugins\$plugin" -Recurse -Force + Write-Host "Removed unused plugin folder: $plugin" } } - # Remove any duplicate or unnecessary DLLs (e.g., web-related) - Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Web*" | Remove-Item -Force - Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Pdf*" | Remove-Item -Force - Write-Host "Removed additional unnecessary Qt files" + # Explicitly remove virtualkeyboard input context if it exists + if (Test-Path "$libDir\PySide6\plugins\platforminputcontexts") { + Remove-Item "$libDir\PySide6\plugins\platforminputcontexts" -Recurse -Force + Write-Host "Removed plugin: platforminputcontexts" + } + # Explicitly remove specific image formats + foreach ($fmt in @("qpdf.dll", "qsvg.dll")) { + if (Test-Path "$libDir\PySide6\plugins\imageformats\$fmt") { + Remove-Item "$libDir\PySide6\plugins\imageformats\$fmt" -Force + Write-Host "Removed plugin: $fmt" + } + } + # Explicitly remove specific icon engines + if (Test-Path "$libDir\PySide6\plugins\iconengines\qsvgicon.dll") { + Remove-Item "$libDir\PySide6\plugins\iconengines\qsvgicon.dll" -Force + Write-Host "Removed plugin: qsvgicon.dll" + } + + # Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib) + $bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*") + foreach ($pattern in $bloatDlls) { + Get-ChildItem -Path $libDir -Filter $pattern | Remove-Item -Force + } + Write-Host "Removed unnecessary Qt DLLs" + + # Remove build tools and unused python modules + $uselessFolders = @("setuptools", "wheel", "pkg_resources", "_distutils_hack", "curses", "_pyrepl") + foreach ($folder in $uselessFolders) { + if (Test-Path "$libDir\$folder") { + Remove-Item "$libDir\$folder" -Recurse -Force + Write-Host "Removed unused module: $folder" + } + } } - name: Setup FFmpeg for bundle @@ -293,32 +326,65 @@ jobs: shell: powershell run: | $distDir = "dist\YTSage-FFmpeg" + $libDir = "$distDir\lib" if (Test-Path $distDir) { # Remove screenshots - if (Test-Path "$distDir\lib\assets\branding\screenshots") { - Remove-Item "$distDir\lib\assets\branding\screenshots" -Recurse -Force + if (Test-Path "$libDir\assets\branding\screenshots") { + Remove-Item "$libDir\assets\branding\screenshots" -Recurse -Force Write-Host "Removed screenshots folder" } # Remove debug PDB files Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force Write-Host "Removed PDB debug files" - # Remove unused Qt translations (if not needed for multilingual support) - if (Test-Path "$distDir\PySide6\translations") { - Remove-Item "$distDir\PySide6\translations" -Recurse -Force + + # Remove unused Qt translations (entire folder) + if (Test-Path "$libDir\PySide6\translations") { + Remove-Item "$libDir\PySide6\translations" -Recurse -Force Write-Host "Removed Qt translations" } + # Remove unused Qt plugins (based on your excludes; adjust if needed) + # Note: qpdf and qsvg plugins are removed specifically $unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml") foreach ($plugin in $unusedPlugins) { - if (Test-Path "$distDir\PySide6\plugins\$plugin") { - Remove-Item "$distDir\PySide6\plugins\$plugin" -Recurse -Force - Write-Host "Removed unused plugin: $plugin" + if (Test-Path "$libDir\PySide6\plugins\$plugin") { + Remove-Item "$libDir\PySide6\plugins\$plugin" -Recurse -Force + Write-Host "Removed unused plugin folder: $plugin" } } - # Remove any duplicate or unnecessary DLLs (e.g., web-related) - Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Web*" | Remove-Item -Force - Get-ChildItem -Path $distDir -Recurse -Filter "Qt6Pdf*" | Remove-Item -Force - Write-Host "Removed additional unnecessary Qt files" + # Explicitly remove virtualkeyboard input context if it exists + if (Test-Path "$libDir\PySide6\plugins\platforminputcontexts") { + Remove-Item "$libDir\PySide6\plugins\platforminputcontexts" -Recurse -Force + Write-Host "Removed plugin: platforminputcontexts" + } + # Explicitly remove specific image formats + foreach ($fmt in @("qpdf.dll", "qsvg.dll")) { + if (Test-Path "$libDir\PySide6\plugins\imageformats\$fmt") { + Remove-Item "$libDir\PySide6\plugins\imageformats\$fmt" -Force + Write-Host "Removed plugin: $fmt" + } + } + # Explicitly remove specific icon engines + if (Test-Path "$libDir\PySide6\plugins\iconengines\qsvgicon.dll") { + Remove-Item "$libDir\PySide6\plugins\iconengines\qsvgicon.dll" -Force + Write-Host "Removed plugin: qsvgicon.dll" + } + + # Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib) + $bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*") + foreach ($pattern in $bloatDlls) { + Get-ChildItem -Path $libDir -Filter $pattern | Remove-Item -Force + } + Write-Host "Removed unnecessary Qt DLLs" + + # Remove build tools and unused python modules + $uselessFolders = @("setuptools", "wheel", "pkg_resources", "_distutils_hack", "curses", "_pyrepl") + foreach ($folder in $uselessFolders) { + if (Test-Path "$libDir\$folder") { + Remove-Item "$libDir\$folder" -Recurse -Force + Write-Host "Removed unused module: $folder" + } + } } - name: Package and prepare release artifacts (ZIPs) From 3ea5fedfa3cc2620a80a38dbcae42c09d92b018c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:20:09 +0200 Subject: [PATCH 088/134] Remove Qt DLLs from PySide6 and suppress errors Add -ErrorAction SilentlyContinue to Get-ChildItem calls and add a check for a lib\PySide6 folder to remove matching Qt DLLs there as well. This prevents failures when no files match the patterns and ensures duplicate Qt DLLs packaged under PySide6 are cleaned up. Applied to both DLL-cleanup sections of the Windows build workflow. --- .github/workflows/build-windows.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 51af56a..42a554e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -229,7 +229,12 @@ jobs: # Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib) $bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*") foreach ($pattern in $bloatDlls) { - Get-ChildItem -Path $libDir -Filter $pattern | Remove-Item -Force + # Check in root lib + Get-ChildItem -Path $libDir -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force + # Check in PySide6 folder + if (Test-Path "$libDir\PySide6") { + Get-ChildItem -Path "$libDir\PySide6" -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force + } } Write-Host "Removed unnecessary Qt DLLs" @@ -373,7 +378,12 @@ jobs: # Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib) $bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*") foreach ($pattern in $bloatDlls) { - Get-ChildItem -Path $libDir -Filter $pattern | Remove-Item -Force + # Check in root lib + Get-ChildItem -Path $libDir -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force + # Check in PySide6 folder + if (Test-Path "$libDir\PySide6") { + Get-ChildItem -Path "$libDir\PySide6" -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force + } } Write-Host "Removed unnecessary Qt DLLs" From 536594a56e01bc2173ecf4326ffcdda2951fbbdd Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:32:38 +0200 Subject: [PATCH 089/134] Improve macOS app bundle trimming script Enhance the .github workflow that trims macOS .app bundles by robustly detecting the library directory (with fallbacks) and performing deeper cleanup. Removes screenshots, Qt translations, many Qt plugin folders, specific image/icon libs, bulk Qt6 libraries, and several Python build/tool modules; adapts for macOS dylib naming. Adds logging and a warning if the lib directory cannot be determined to avoid accidental deletes. --- .github/workflows/build-macos.yml | 84 +++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 8ba0b6a..263a9d4 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -205,38 +205,68 @@ jobs: if [ -n "$app_path" ]; then echo "Processing App Bundle at: $app_path" - # 1. Remove screenshots - if [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then - rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" + # Determine Lib directory (standard location vs customized) + # We look for 'os.py' or 'ytsage' folder to be sure + lib_dir=$(find "$app_path/Contents" -type d -name "ytsage" | head -n1 | xargs dirname) + + if [ -z "$lib_dir" ]; then + # Fallback standard cx_Freeze locations + if [ -d "$app_path/Contents/MacOS/lib" ]; then + lib_dir="$app_path/Contents/MacOS/lib" + elif [ -d "$app_path/Contents/Resources/lib" ]; then + lib_dir="$app_path/Contents/Resources/lib" + fi + fi + + echo "Detected Lib Directory: $lib_dir" + + if [ -d "$lib_dir" ]; then + # 1. Remove screenshots + rm -rf "$lib_dir/assets/branding/screenshots" echo "Removed screenshots folder" - fi - - # 2. Remove unused Qt translations - # In macOS .app, Qt libs might be in Contents/MacOS/PySide6 or similar - # Search for translations folder within the app bundle - translations=$(find "$app_path" -type d -name "translations" | grep "PySide6" || true) - if [ -n "$translations" ]; then - rm -rf "$translations" - echo "Removed Qt translations at $translations" - fi - - # 3. Remove unused Qt plugins (matching Windows logic) - plugins_dir=$(find "$app_path" -type d -name "plugins" | grep "PySide6" | head -n1 || true) - if [ -n "$plugins_dir" ]; then - echo "Cleaning plugins in $plugins_dir" + + # 2. Remove unused Qt translations + rm -rf "$lib_dir/PySide6/translations" + echo "Removed Qt translations" + + # 3. Remove unused Qt plugins + plugins_dir="$lib_dir/PySide6/plugins" for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do - if [ -d "$plugins_dir/$plugin" ]; then - rm -rf "$plugins_dir/$plugin" - echo "Removed plugin: $plugin" - fi + if [ -d "$plugins_dir/$plugin" ]; then + rm -rf "$plugins_dir/$plugin" + echo "Removed plugin folder: $plugin" + fi done + + # 3.1. Explicitly remove specific input contexts and image formats + rm -rf "$plugins_dir/platforminputcontexts" + # Note: macOS libs usually have .dylib extension or no extension in bundles, or .so for python modules + # We try removing common variants + rm -f "$plugins_dir/imageformats/libqpdf.dylib" "$plugins_dir/imageformats/libqsvg.dylib" + rm -f "$plugins_dir/imageformats/qpdf.dylib" "$plugins_dir/imageformats/qsvg.dylib" + rm -f "$plugins_dir/iconengines/libqsvgicon.dylib" "$plugins_dir/iconengines/qsvgicon.dylib" + + # 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs) + # Targeting root lib and PySide6 internal lib + # Patterns match both .so, .dylib, and .framework style naming + echo "Removing bloat Qt libraries..." + find "$lib_dir" -name "Qt6Web*" -delete + find "$lib_dir" -name "Qt6Pdf*" -delete + find "$lib_dir" -name "Qt6Qml*" -delete + find "$lib_dir" -name "Qt6Quick*" -delete + find "$lib_dir" -name "Qt6VirtualKeyboard*" -delete + find "$lib_dir" -name "Qt6OpenGL*" -delete + + # 5. Remove build tools and unused python modules + for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do + rm -rf "$lib_dir/$tool" + echo "Removed build tool/module: $tool" + done + + else + echo "Warning: Library directory could not be precisely determined. Skipping deep trim." fi - # 4. Remove other unused components if found - find "$app_path" -name "Qt6Web*" -delete - find "$app_path" -name "Qt6Pdf*" -delete - echo "Cleaned additional Qt files" - else echo "Error: Could not find .app bundle to trim!" exit 1 From ce4a034224063238294d85a8eb562a3201621ae4 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:40:48 +0200 Subject: [PATCH 090/134] Refactor Linux build trimming and cleanup Consolidate and harden post-build cleanup by targeting the build's lib directory (lib_dir) instead of running broad finds across build_dir. Removes screenshots, PySide6 translations, specific Qt plugins and plugin files, and various Qt6 shared libraries (Web, Pdf, Qml, Quick, VirtualKeyboard, OpenGL) to reduce bundle size. Also removes common build tools/modules (setuptools, wheel, pkg_resources, _distutils_hack, curses, _pyrepl) and adds explicit logging and a warning if lib_dir is missing. Simplifies and centralizes cleanup logic for more predictable and efficient trimming of Linux artifacts. --- .github/workflows/build-linux.yml | 73 +++++++++++++++++++------------ 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 1a4991d..57601af 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -219,37 +219,54 @@ jobs: if [ -n "$build_dir" ]; then echo "Trimming files in $build_dir..." + lib_dir="$build_dir/lib" - # Remove screenshots - if [ -d "$build_dir/lib/assets/branding/screenshots" ]; then - rm -rf "$build_dir/lib/assets/branding/screenshots" - echo "Removed screenshots folder" - fi - - # Remove unused Qt translations - # In Linux builds, translations are often in lib/PySide6/translations or similar - translations=$(find "$build_dir" -type d -name "translations" | grep "PySide6" || true) - if [ -n "$translations" ]; then - rm -rf "$translations" - echo "Removed Qt translations at $translations" - fi - - # Remove unused Qt plugins (matching Windows logic) - plugins_dir=$(find "$build_dir" -type d -name "plugins" | grep "PySide6" | head -n1 || true) - if [ -n "$plugins_dir" ]; then - for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do - if [ -d "$plugins_dir/$plugin" ]; then - rm -rf "$plugins_dir/$plugin" - echo "Removed plugin: $plugin" - fi + if [ -d "$lib_dir" ]; then + # 1. Remove screenshots + if [ -d "$lib_dir/assets/branding/screenshots" ]; then + rm -rf "$lib_dir/assets/branding/screenshots" + echo "Removed screenshots folder" + fi + + # 2. Remove unused Qt translations + if [ -d "$lib_dir/PySide6/translations" ]; then + rm -rf "$lib_dir/PySide6/translations" + echo "Removed Qt translations" + fi + + # 3. Remove unused Qt plugins + plugins_dir="$lib_dir/PySide6/plugins" + if [ -d "$plugins_dir" ]; then + for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do + if [ -d "$plugins_dir/$plugin" ]; then + rm -rf "$plugins_dir/$plugin" + echo "Removed plugin: $plugin" + fi + done + # Specific cleanups (input contexts, imageformats) + rm -rf "$plugins_dir/platforminputcontexts" + rm -f "$plugins_dir/imageformats/libqpdf.so" "$plugins_dir/imageformats/libqsvg.so" + rm -f "$plugins_dir/iconengines/libqsvgicon.so" + fi + + # 4. Remove bloat shared libraries (Recursively finds in lib/ and lib/PySide6/) + # Matches libQt6Qml.so.6, Qt6Qml.abi3.so, etc. + echo "Removing bloat Qt libraries..." + find "$lib_dir" -name "*Qt6Web*" -delete + find "$lib_dir" -name "*Qt6Pdf*" -delete + find "$lib_dir" -name "*Qt6Qml*" -delete + find "$lib_dir" -name "*Qt6Quick*" -delete + find "$lib_dir" -name "*Qt6VirtualKeyboard*" -delete + find "$lib_dir" -name "*Qt6OpenGL*" -delete + + # 5. Remove build tools and unused python modules + for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do + rm -rf "$lib_dir/$tool" + echo "Removed build tool/module: $tool" done + else + echo "Warning: lib directory not found inside build_dir" fi - - # Remove other unused components if found - find "$build_dir" -name "Qt6Web*" -delete - find "$build_dir" -name "Qt6Pdf*" -delete - echo "Cleaned additional Qt files" - else echo "Warning: Build directory not found, cannot trim files" fi From c11a4a0280790a76c90ced6a56b347757e5e260e Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:58:23 +0200 Subject: [PATCH 091/134] Enhance macOS build lib cleanup Update the macOS build workflow to robustly locate and clean multiple library directories inside the .app bundle. Instead of a single lib path, the script now finds all 'lib' dirs containing PySide6 or ytsage and iterates over them. Cleanup expanded to handle alternate PySide6 paths (PySide6/Qt/translations, PySide6/Qt/plugins), more plugin/imageformat variants (.dylib/.so), additional Qt library name patterns (Qt6*, framework-style names) and Qt6DBus, and removal of common build tools/modules. Adds logging and a warning when no library dirs are found. --- .github/workflows/build-macos.yml | 119 ++++++++++++++++-------------- 1 file changed, 63 insertions(+), 56 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 263a9d4..a0b16e2 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -206,66 +206,73 @@ jobs: echo "Processing App Bundle at: $app_path" # Determine Lib directory (standard location vs customized) - # We look for 'os.py' or 'ytsage' folder to be sure - lib_dir=$(find "$app_path/Contents" -type d -name "ytsage" | head -n1 | xargs dirname) + # We look for all 'lib' directories that contain 'PySide6' or 'ytsage' to be thorough + # resulting in potentially multiple lib dirs to clean (e.g. MacOS/lib and Resources/lib) + lib_dirs=$(find "$app_path/Contents" -type d -name "lib" | while read -r d; do if [ -d "$d/ytsage" ] || [ -d "$d/PySide6" ]; then echo "$d"; fi; done) - if [ -z "$lib_dir" ]; then - # Fallback standard cx_Freeze locations - if [ -d "$app_path/Contents/MacOS/lib" ]; then - lib_dir="$app_path/Contents/MacOS/lib" - elif [ -d "$app_path/Contents/Resources/lib" ]; then - lib_dir="$app_path/Contents/Resources/lib" - fi + if [ -z "$lib_dirs" ]; then + echo "Warning: No valid Library directory found. Skipping deep trim." fi - echo "Detected Lib Directory: $lib_dir" - - if [ -d "$lib_dir" ]; then - # 1. Remove screenshots - rm -rf "$lib_dir/assets/branding/screenshots" - echo "Removed screenshots folder" + for lib_dir in $lib_dirs; do + echo "Processing Lib Directory: $lib_dir" - # 2. Remove unused Qt translations - rm -rf "$lib_dir/PySide6/translations" - echo "Removed Qt translations" - - # 3. Remove unused Qt plugins - plugins_dir="$lib_dir/PySide6/plugins" - for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do - if [ -d "$plugins_dir/$plugin" ]; then - rm -rf "$plugins_dir/$plugin" - echo "Removed plugin folder: $plugin" - fi - done - - # 3.1. Explicitly remove specific input contexts and image formats - rm -rf "$plugins_dir/platforminputcontexts" - # Note: macOS libs usually have .dylib extension or no extension in bundles, or .so for python modules - # We try removing common variants - rm -f "$plugins_dir/imageformats/libqpdf.dylib" "$plugins_dir/imageformats/libqsvg.dylib" - rm -f "$plugins_dir/imageformats/qpdf.dylib" "$plugins_dir/imageformats/qsvg.dylib" - rm -f "$plugins_dir/iconengines/libqsvgicon.dylib" "$plugins_dir/iconengines/qsvgicon.dylib" - - # 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs) - # Targeting root lib and PySide6 internal lib - # Patterns match both .so, .dylib, and .framework style naming - echo "Removing bloat Qt libraries..." - find "$lib_dir" -name "Qt6Web*" -delete - find "$lib_dir" -name "Qt6Pdf*" -delete - find "$lib_dir" -name "Qt6Qml*" -delete - find "$lib_dir" -name "Qt6Quick*" -delete - find "$lib_dir" -name "Qt6VirtualKeyboard*" -delete - find "$lib_dir" -name "Qt6OpenGL*" -delete - - # 5. Remove build tools and unused python modules - for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do - rm -rf "$lib_dir/$tool" - echo "Removed build tool/module: $tool" - done - - else - echo "Warning: Library directory could not be precisely determined. Skipping deep trim." - fi + # 1. Remove screenshots + rm -rf "$lib_dir/assets/branding/screenshots" + echo "Removed screenshots folder" + + # 2. Remove unused Qt translations (Path varies: PySide6/translations or PySide6/Qt/translations) + rm -rf "$lib_dir/PySide6/translations" + rm -rf "$lib_dir/PySide6/Qt/translations" + echo "Removed Qt translations" + + # 3. Remove unused Qt plugins + # Path varies: PySide6/plugins or PySide6/Qt/plugins + for plugins_base in "$lib_dir/PySide6/plugins" "$lib_dir/PySide6/Qt/plugins"; do + if [ -d "$plugins_base" ]; then + for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do + if [ -d "$plugins_base/$plugin" ]; then + rm -rf "$plugins_base/$plugin" + echo "Removed plugin folder: $plugin from $plugins_base" + fi + done + + # 3.1. Explicitly remove specific input contexts and image formats + rm -rf "$plugins_base/platforminputcontexts" + rm -f "$plugins_base/imageformats/libqpdf.dylib" "$plugins_base/imageformats/libqsvg.dylib" + rm -f "$plugins_base/imageformats/libqpdf.so" "$plugins_base/imageformats/libqsvg.so" # just in case + rm -f "$plugins_base/iconengines/libqsvgicon.dylib" + fi + done + + # 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs/frameworks) + # On macOS, these often appear as files with no extension (e.g. 'QtQml') or .dylib + # We remove both variants and also the 'Qt6' prefixed ones just in case + echo "Removing bloat Qt libraries..." + + # Standard patterns (Linux/Windows style) + find "$lib_dir" -name "*Qt6Web*" -delete + find "$lib_dir" -name "*Qt6Pdf*" -delete + find "$lib_dir" -name "*Qt6Qml*" -delete + find "$lib_dir" -name "*Qt6Quick*" -delete + find "$lib_dir" -name "*Qt6VirtualKeyboard*" -delete + find "$lib_dir" -name "*Qt6OpenGL*" -delete + find "$lib_dir" -name "*Qt6DBus*" -delete + + # macOS Framework style (No '6' in name usually, e.g. QtQml, QtQuick) + # Be careful not to delete QtQuickWidgets if used (though usually safe if Quick is gone) + # We use known specific lists to avoid over-deleting + for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do + find "$lib_dir" -maxdepth 1 -name "$bloat" -delete + find "$lib_dir" -maxdepth 1 -name "${bloat}.*" -delete # Matches versions or extensions + done + + # 5. Remove build tools and unused python modules + for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do + rm -rf "$lib_dir/$tool" + echo "Removed build tool/module: $tool" + done + done else echo "Error: Could not find .app bundle to trim!" From ac11cbd7bb994c8942dcab2f43d2f6c845f1e416 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:14:59 +0200 Subject: [PATCH 092/134] Simplify macOS .app lib trimming workflow Replace complex multi-dir detection with a single find+while loop to identify library dirs and verify Python lib layout (asyncio/PySide6/ytsage). Consolidate cleanup steps: recursively remove PySide6 translations, normalize plugin path detection and remove specific plugin folders/files via find, and simplify removal of unwanted Qt modules using broader find patterns. Remove some verbose checks and add a final sanity listing of remaining lib contents. These changes make the trimming more robust to different .app layouts and reduce duplicate logic. --- .github/workflows/build-macos.yml | 110 +++++++++++++----------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index a0b16e2..4775823 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -205,75 +205,59 @@ jobs: if [ -n "$app_path" ]; then echo "Processing App Bundle at: $app_path" - # Determine Lib directory (standard location vs customized) - # We look for all 'lib' directories that contain 'PySide6' or 'ytsage' to be thorough - # resulting in potentially multiple lib dirs to clean (e.g. MacOS/lib and Resources/lib) - lib_dirs=$(find "$app_path/Contents" -type d -name "lib" | while read -r d; do if [ -d "$d/ytsage" ] || [ -d "$d/PySide6" ]; then echo "$d"; fi; done) - - if [ -z "$lib_dirs" ]; then - echo "Warning: No valid Library directory found. Skipping deep trim." - fi - - for lib_dir in $lib_dirs; do - echo "Processing Lib Directory: $lib_dir" - - # 1. Remove screenshots - rm -rf "$lib_dir/assets/branding/screenshots" - echo "Removed screenshots folder" - - # 2. Remove unused Qt translations (Path varies: PySide6/translations or PySide6/Qt/translations) - rm -rf "$lib_dir/PySide6/translations" - rm -rf "$lib_dir/PySide6/Qt/translations" - echo "Removed Qt translations" - - # 3. Remove unused Qt plugins - # Path varies: PySide6/plugins or PySide6/Qt/plugins - for plugins_base in "$lib_dir/PySide6/plugins" "$lib_dir/PySide6/Qt/plugins"; do - if [ -d "$plugins_base" ]; then + # Simplified discovery of library directories + # We search for 'lib' folders and then verify if they look like the python library folder + find "$app_path/Contents" -type d -name "lib" | while read -r lib_dir; do + # Check if this lib dir is a python library dir (contains asyncio or PySide6 or ytsage) + if [ -d "$lib_dir/asyncio" ] || [ -d "$lib_dir/PySide6" ] || [ -d "$lib_dir/ytsage" ]; then + echo "Cleaning Library Directory: $lib_dir" + + # 1. Remove screenshots + rm -rf "$lib_dir/assets/branding/screenshots" + + # 2. Remove unused Qt translations (Recursively find 'translations' folder inside PySide6) + find "$lib_dir" -type d -path "*/PySide6/*/translations" -exec rm -rf {} + 2>/dev/null || true + find "$lib_dir" -type d -path "*/PySide6/translations" -exec rm -rf {} + 2>/dev/null || true + + # 3. Remove unused Qt plugins + # Determine plugins path + plugins_path="" + if [ -d "$lib_dir/PySide6/plugins" ]; then plugins_path="$lib_dir/PySide6/plugins"; fi + if [ -d "$lib_dir/PySide6/Qt/plugins" ]; then plugins_path="$lib_dir/PySide6/Qt/plugins"; fi + + if [ -n "$plugins_path" ]; then + echo "Found plugins at: $plugins_path" for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do - if [ -d "$plugins_base/$plugin" ]; then - rm -rf "$plugins_base/$plugin" - echo "Removed plugin folder: $plugin from $plugins_base" - fi + rm -rf "$plugins_path/$plugin" done - # 3.1. Explicitly remove specific input contexts and image formats - rm -rf "$plugins_base/platforminputcontexts" - rm -f "$plugins_base/imageformats/libqpdf.dylib" "$plugins_base/imageformats/libqsvg.dylib" - rm -f "$plugins_base/imageformats/libqpdf.so" "$plugins_base/imageformats/libqsvg.so" # just in case - rm -f "$plugins_base/iconengines/libqsvgicon.dylib" + # Specific cleanups + rm -rf "$plugins_path/platforminputcontexts" + find "$plugins_path" -name "libqpdf.*" -delete + find "$plugins_path" -name "libqsvg.*" -delete + find "$plugins_path" -name "libqsvgicon.*" -delete fi - done - - # 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs/frameworks) - # On macOS, these often appear as files with no extension (e.g. 'QtQml') or .dylib - # We remove both variants and also the 'Qt6' prefixed ones just in case - echo "Removing bloat Qt libraries..." - - # Standard patterns (Linux/Windows style) - find "$lib_dir" -name "*Qt6Web*" -delete - find "$lib_dir" -name "*Qt6Pdf*" -delete - find "$lib_dir" -name "*Qt6Qml*" -delete - find "$lib_dir" -name "*Qt6Quick*" -delete - find "$lib_dir" -name "*Qt6VirtualKeyboard*" -delete - find "$lib_dir" -name "*Qt6OpenGL*" -delete - find "$lib_dir" -name "*Qt6DBus*" -delete - - # macOS Framework style (No '6' in name usually, e.g. QtQml, QtQuick) - # Be careful not to delete QtQuickWidgets if used (though usually safe if Quick is gone) - # We use known specific lists to avoid over-deleting - for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do - find "$lib_dir" -maxdepth 1 -name "$bloat" -delete - find "$lib_dir" -maxdepth 1 -name "${bloat}.*" -delete # Matches versions or extensions - done - - # 5. Remove build tools and unused python modules - for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do - rm -rf "$lib_dir/$tool" - echo "Removed build tool/module: $tool" - done + + # 4. Remove bloat Qt libraries / Frameworks + # Iterate specifically over unwanted Qt modules + # Matches both 'QtQml' (file) and 'QtQml.abi3.so' etc + for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do + find "$lib_dir" -maxdepth 2 -name "$bloat" -delete + find "$lib_dir" -maxdepth 2 -name "${bloat}.*" -delete + find "$lib_dir" -maxdepth 2 -name "*$bloat*" -delete + done + + # 5. Remove build tools and unused python modules + for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do + rm -rf "$lib_dir/$tool" + done + fi done + # Final sanity verification + echo "Trim complete. Remaining contents of lib (first 2 levels):" + find "$app_path/Contents" -type d -name "lib" -exec ls -R {} \; | head -n 50 || true + else echo "Error: Could not find .app bundle to trim!" exit 1 From 5d9054154cdada36329b61321b60002b1350362e Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:28:27 +0200 Subject: [PATCH 093/134] Manually create DMG and ZIP artifacts Remove the separate bdist_dmg build step and instead package the generated .app in-place: create a .app.zip and build a UDZO .dmg using a temporary dmg_stage (copy .app, add Applications symlink, run hdiutil). Also remove the previous fallback that searched for prebuilt DMGs and change missing .app handling to error+exit. Artifacts are written to artifacts/YTSage-v${version}-${ARCH_SUFFIX}.{app.zip,dmg}. --- .github/workflows/build-macos.yml | 38 ++++++++++++------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 4775823..2237ff8 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -263,12 +263,6 @@ jobs: exit 1 fi - - name: Build DMG (bdist_dmg) - shell: bash - run: | - source venv/bin/activate - python setup_cxfreeze.py bdist_dmg - - name: Package and prepare release artifacts (.app.zip and .dmg) shell: bash run: | @@ -288,26 +282,24 @@ jobs: if [ -n "$app_path" ] && [ -d "$app_path" ]; then app_base="$(basename "$app_path")" app_parent="$(dirname "$app_path")" + + # Create ZIP (cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base") echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" + + # Create DMG (Manual) + echo "Creating DMG from $app_path..." + mkdir -p dmg_stage + cp -R "$app_path" "dmg_stage/" + ln -s /Applications "dmg_stage/Applications" + + hdiutil create -volname "YTSage v${version}" -srcfolder "dmg_stage" -ov -format UDZO "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" + echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" + + rm -rf dmg_stage else - echo "Warning: .app bundle not found in dist/ or build/" - fi - - # Try to find the generated DMG - dmg_src="" - for cand in "dist/YTSage-v${version}.dmg"; do - if [ -f "$cand" ]; then dmg_src="$cand"; break; fi - done - if [ -z "$dmg_src" ]; then - dmg_src=$(ls dist/*.dmg build/*.dmg build/dist/*.dmg 2>/dev/null | head -n1 || true) - fi - - if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then - cp "$dmg_src" "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" - echo "Copied DMG to artifacts" - else - echo "Warning: No DMG found" + echo "Error: .app bundle not found in dist/ or build/" + exit 1 fi echo "Final artifacts:" From ad63cdcca13cf1b5cdff25ad37b2af19492c05d7 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:44:51 +0200 Subject: [PATCH 094/134] Build AppImage manually in CI workflow Replace the cx_Freeze bdist_appimage step with an explicit manual AppImage build to avoid cx_Freeze rebuilding and untrimming artifacts. The workflow now downloads appimagetool, prepares an AppDir (AppDir/usr/bin), copies the trimmed build contents (preserving cx_Freeze relative paths), installs desktop/icon metadata, creates an AppRun symlink, and runs appimagetool with ARCH=x86_64 and --appimage-extract-and-run to avoid FUSE issues in CI. The rest of the job (e.g. RPM build) remains unchanged. --- .github/workflows/build-linux.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 57601af..efb1737 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -271,8 +271,31 @@ jobs: echo "Warning: Build directory not found, cannot trim files" fi - # Build AppImage - python setup_cxfreeze.py bdist_appimage + # Build AppImage manually to avoid cx_Freeze rebuilding (and untrimming) the artifacts + echo "Creating AppImage manually..." + mkdir -p dist + + # Download appimagetool + wget -q -O appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + chmod +x appimagetool + + # Setup AppDir structure + rm -rf AppDir + mkdir -p AppDir/usr/bin + + # Copy trimmed build content (preserving relative paths for cx_Freeze libs) + cp -r "$build_dir"/* AppDir/usr/bin/ + + # Setup metadata + cp ytsage.desktop AppDir/ + cp branding/icons/icon.png AppDir/ytsage.png + + # Create AppRun symlink + ln -s usr/bin/ytsage AppDir/AppRun + + # Build AppImage (using --appimage-extract-and-run to avoid FUSE issues in CI) + # We explicitly set ARCH for appimagetool + ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "dist/ytsage-v${version}.AppImage" # Build RPM manually with proper spec file version="${{ steps.get_version.outputs.VERSION }}" From cfccbceb8e33e1dd3a025bb88b9bb8502dee229d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:01:25 +0200 Subject: [PATCH 095/134] Adjust AppImage AppDir layout and AppRun script Update the Linux CI AppImage build to copy the cx_Freeze output directly into AppDir root (so 'lib' sits next to the 'ytsage' binary) to avoid ModuleNotFoundError: encodings. Fix the .desktop Exec path, add .DirIcon, and replace the previous symlink with an AppRun script that sets LD_LIBRARY_PATH and execs the bundled binary. --- .github/workflows/build-linux.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index efb1737..0811f77 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -279,19 +279,31 @@ jobs: wget -q -O appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool - # Setup AppDir structure + # Setup AppDir structure (Root layout matches cx_Freeze build output exactly) rm -rf AppDir - mkdir -p AppDir/usr/bin + mkdir -p AppDir - # Copy trimmed build content (preserving relative paths for cx_Freeze libs) - cp -r "$build_dir"/* AppDir/usr/bin/ + # Copy trimmed build content DIRECTLY to AppDir root + # This keeps 'lib' adjacent to 'ytsage', preventing 'ModuleNotFoundError: encodings' + cp -r "$build_dir"/* AppDir/ # Setup metadata cp ytsage.desktop AppDir/ + # Fix Exec path for AppImage context (binary is in root, not /usr/bin) + sed -i 's|Exec=/usr/bin/ytsage|Exec=ytsage|g' AppDir/ytsage.desktop cp branding/icons/icon.png AppDir/ytsage.png + # .DirIcon for file managers + cp branding/icons/icon.png AppDir/.DirIcon - # Create AppRun symlink - ln -s usr/bin/ytsage AppDir/AppRun + # Create AppRun as a script to correctly set environment + cat > AppDir/AppRun <<'APPRUN' + #!/bin/sh + SELF=$(readlink -f "$0") + HERE=$(dirname "$SELF") + export LD_LIBRARY_PATH="$HERE/lib:$LD_LIBRARY_PATH" + exec "$HERE/ytsage" "$@" + APPRUN + chmod +x AppDir/AppRun # Build AppImage (using --appimage-extract-and-run to avoid FUSE issues in CI) # We explicitly set ARCH for appimagetool From 030b37c50590226824e84203f95fb474fedd5d34 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:19:44 +0200 Subject: [PATCH 096/134] Adjust PySide6 components and plugin cleanup Add PySide6.QtDBus and PySide6.QtSvg to the list of required modules and remove PySide6.QtSvg from the secondary module list. Stop removing the svg plugin, platforminputcontexts and svg-related image/icon libraries during the cleanup pass (only libqpdf.so is still removed). These changes ensure QtSvg and D-Bus components are preserved in the Linux build and prevent stripping SVG-related functionality. --- .github/workflows/build-linux.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0811f77..489c5be 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -117,6 +117,8 @@ jobs: "PySide6.QtWidgets", "PySide6.QtMultimedia", "PySide6.QtNetwork", + "PySide6.QtDBus", + "PySide6.QtSvg", "requests", "PIL", "packaging", @@ -135,7 +137,6 @@ jobs: "PySide6.QtBluetooth", "PySide6.QtOpenGL", "PySide6.QtPrintSupport", - "PySide6.QtSvg", "PySide6.QtTest", "PySide6.QtXml", "PySide6.QtSql", @@ -237,16 +238,14 @@ jobs: # 3. Remove unused Qt plugins plugins_dir="$lib_dir/PySide6/plugins" if [ -d "$plugins_dir" ]; then - for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do + for plugin in designer pdf sql help qml quick webengine bluetooth opengl printsupport test xml; do if [ -d "$plugins_dir/$plugin" ]; then rm -rf "$plugins_dir/$plugin" echo "Removed plugin: $plugin" fi done - # Specific cleanups (input contexts, imageformats) - rm -rf "$plugins_dir/platforminputcontexts" - rm -f "$plugins_dir/imageformats/libqpdf.so" "$plugins_dir/imageformats/libqsvg.so" - rm -f "$plugins_dir/iconengines/libqsvgicon.so" + # Specific cleanups (imageformats) + rm -f "$plugins_dir/imageformats/libqpdf.so" fi # 4. Remove bloat shared libraries (Recursively finds in lib/ and lib/PySide6/) From b776b3efa94e1b3b0248127ca0bcdb3ea61d6bb3 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:41:54 +0200 Subject: [PATCH 097/134] Keep QtDBus and related plugins in macOS build Add PySide6.QtDBus to the list of included PySide6 modules and stop removing DBus-related artifacts during the macOS packaging step. The changes remove deletions of the platforminputcontexts plugin and libqpdf/libqsvg/libqsvgicon files, and remove QtDBus from the list of Qt modules treated as bloat. This preserves D-Bus support and related SVG/PDF rendering plugins required at runtime. --- .github/workflows/build-macos.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 2237ff8..9ad8988 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -103,6 +103,7 @@ jobs: "PySide6.QtWidgets", "PySide6.QtMultimedia", "PySide6.QtNetwork", + "PySide6.QtDBus", "requests", "PIL", "packaging", @@ -232,16 +233,13 @@ jobs: done # Specific cleanups - rm -rf "$plugins_path/platforminputcontexts" find "$plugins_path" -name "libqpdf.*" -delete - find "$plugins_path" -name "libqsvg.*" -delete - find "$plugins_path" -name "libqsvgicon.*" -delete fi # 4. Remove bloat Qt libraries / Frameworks # Iterate specifically over unwanted Qt modules # Matches both 'QtQml' (file) and 'QtQml.abi3.so' etc - for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do + for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets; do find "$lib_dir" -maxdepth 2 -name "$bloat" -delete find "$lib_dir" -maxdepth 2 -name "${bloat}.*" -delete find "$lib_dir" -maxdepth 2 -name "*$bloat*" -delete From 4f5e30e6cf71f584081cd19bb7d88b679272d9de Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:41:39 +0200 Subject: [PATCH 098/134] Stream Deno upgrade output and add GUI thread Add streaming progress support to Deno upgrades and hook it into the GUI. upgrade_deno() now accepts an optional progress_callback and uses subprocess.Popen to read stdout line-by-line (line-buffered, utf-8, errors replaced), collecting output and invoking the callback as lines arrive. Added DenoUpdateThread (QThread) with finished/progress/error signals and replaced the previous background thread usage in the updater UI with this QThread. GUI slots strip ANSI escapes, truncate long messages, and update status text; buttons are re-enabled on finish/error. Improved logging and error handling for upgrade failures. --- ytsage/core/ytsage_deno.py | 42 +++++++++--- .../ytsage_dialogs_updater.py | 68 ++++++++++++++----- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/ytsage/core/ytsage_deno.py b/ytsage/core/ytsage_deno.py index 2e5edc4..e18c5a9 100644 --- a/ytsage/core/ytsage_deno.py +++ b/ytsage/core/ytsage_deno.py @@ -683,10 +683,13 @@ def compare_deno_versions(current: str, latest: str) -> bool: return False -def upgrade_deno() -> tuple[bool, str]: +def upgrade_deno(progress_callback=None) -> tuple[bool, str]: """ Upgrade Deno to the latest version using 'deno upgrade' command. + Args: + progress_callback: Optional function to call with output lines for progress tracking + Returns: tuple: (success: bool, output: str) - Success status and command output """ @@ -700,23 +703,42 @@ def upgrade_deno() -> tuple[bool, str]: logger.info(f"Upgrading Deno using: {deno_path}") - # Run deno upgrade command - result = subprocess.run( + # Run deno upgrade command with output capturing + process = subprocess.Popen( [str(deno_path), "upgrade"], - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, - timeout=300, # 5 minutes timeout - creationflags=SUBPROCESS_CREATIONFLAGS + creationflags=SUBPROCESS_CREATIONFLAGS, + bufsize=1, # Line buffered + encoding='utf-8', + errors='replace' ) - output = result.stdout + result.stderr + full_output = [] - if result.returncode == 0: + # Read output line by line as it is generated + while True: + line = process.stdout.readline() + if not line and process.poll() is not None: + break + + if line: + line_str = line.strip() + if line_str: + full_output.append(line_str) + logger.debug(f"Deno upgrade output: {line_str}") + if progress_callback: + progress_callback(line_str) + + return_code = process.poll() + output = "\n".join(full_output) + + if return_code == 0: logger.info("Deno upgrade successful") - logger.debug(f"Upgrade output: {output}") return True, output else: - logger.error(f"Deno upgrade failed with code {result.returncode}") + logger.error(f"Deno upgrade failed with code {return_code}") logger.error(f"Output: {output}") return False, output diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index e73a2e8..39a29c7 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -183,6 +183,21 @@ class DenoCheckThread(QThread): self.error.emit(str(e)) +class DenoUpdateThread(QThread): + finished = Signal(bool, str) + progress = Signal(str) + error = Signal(str) + + def run(self): + try: + success, output = upgrade_deno(progress_callback=self.progress.emit) + self.finished.emit(success, output) + except Exception as e: + logger.exception(f"Error updating Deno: {e}") + self.error.emit(str(e)) + + + class UpdaterTabWidget(QWidget): """Widget for the Updater tab in Custom Options dialog.""" @@ -930,24 +945,41 @@ class UpdaterTabWidget(QWidget): "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" ) - # Run update in background thread - def update_thread(): - try: - success, output = upgrade_deno() - - # Update UI in main thread - self.deno_check_button.setEnabled(True) - self.deno_update_button.setEnabled(True) - self._handle_deno_update_result(success, output) - - except Exception as e: - logger.exception(f"Error updating Deno: {e}") - self.deno_check_button.setEnabled(True) - self.deno_update_button.setEnabled(True) - self._handle_deno_update_result(False, str(e)) - - thread = threading.Thread(target=update_thread, daemon=True) - thread.start() + # Use QThread for updates with progress reporting + self.deno_update_thread = DenoUpdateThread() + self.deno_update_thread.progress.connect(self._on_deno_update_progress) + self.deno_update_thread.finished.connect(self._on_deno_update_finished) + self.deno_update_thread.error.connect(self._on_deno_update_error) + self.deno_update_thread.start() + + @Slot(str) + def _on_deno_update_progress(self, message: str) -> None: + """Handle Deno update progress messages.""" + # Clean up message for display + # Strip ANSI escape codes (colors) + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + display_msg = ansi_escape.sub('', message).strip() + + if not display_msg: + return + + # If message is too long, truncate it + if len(display_msg) > 70: + display_msg = display_msg[:67] + "..." + + self.deno_status_label.setText(display_msg) + + @Slot(bool, str) + def _on_deno_update_finished(self, success: bool, output: str) -> None: + self.deno_check_button.setEnabled(True) + self.deno_update_button.setEnabled(True) + self._handle_deno_update_result(success, output) + + @Slot(str) + def _on_deno_update_error(self, error: str) -> None: + self.deno_check_button.setEnabled(True) + self.deno_update_button.setEnabled(True) + self._handle_deno_update_result(False, error) def _handle_deno_update_result(self, success: bool, output: str) -> None: """Handle Deno update completion.""" From 0b0e3add3c37030f47f9356abc605c5cff227ed0 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:51:05 +0200 Subject: [PATCH 099/134] Add Deno integration check and system info thread Introduce check_ytdlp_deno_integration() in ytsage_yt_dlp.py to detect Deno integration by running `yt-dlp --verbose` and scanning debug output for JS runtimes containing "deno" (with timeout and logging/fallback). Add SystemInfoThread (QThread) in ytsage_dialogs_base.py to collect yt-dlp, ffmpeg and deno presence, versions, paths, cache timestamps and integration status in the background, emitting the gathered info via info_ready. AboutDialog now starts the thread in update_system_info() and populates the UI asynchronously via _populate_system_info(), showing a small "+ yt-dlp" integration indicator next to the Deno status when detected. Refactors usage of version/cache lookups to use the thread-provided info dictionary. --- ytsage/core/ytsage_yt_dlp.py | 40 ++++++++ .../ytsage_gui_dialogs/ytsage_dialogs_base.py | 93 ++++++++++++++----- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/ytsage/core/ytsage_yt_dlp.py b/ytsage/core/ytsage_yt_dlp.py index 06efdb7..e9902c3 100644 --- a/ytsage/core/ytsage_yt_dlp.py +++ b/ytsage/core/ytsage_yt_dlp.py @@ -712,3 +712,43 @@ def setup_ytdlp(parent_widget=None): # User cancelled or setup failed, return the fallback command logger.debug("Returning fallback command 'yt-dlp'") return "yt-dlp" + + +def check_ytdlp_deno_integration() -> bool: + """ + Check if yt-dlp is integrated with Deno by running 'yt-dlp --verbose'. + Returns: + bool: True if Deno is detected in JS runtimes, False otherwise + """ + try: + ytdlp_path = get_yt_dlp_path() + if not ytdlp_path or ytdlp_path == "yt-dlp": + return False + + # Run yt-dlp --verbose to check JS runtimes + # We use a dummy URL or just --verbose with no URL (which might error but should print debug info) + # However, yt-dlp might not print debug info if no URL is provided and it errors out immediately with "usage". + # But per user example: "yt-dlp.exe: error: You must provide at least one URL." comes AFTER debug info. + + result = subprocess.run( + [str(ytdlp_path), "--verbose"], + capture_output=True, + text=True, + timeout=10, + creationflags=SUBPROCESS_CREATIONFLAGS + ) + + # Check stderr for "[debug] JS runtimes: deno" + output = result.stderr + if "[debug] JS runtimes:" in output and "deno" in output: + # Find the line + for line in output.splitlines(): + if "[debug] JS runtimes:" in line and "deno" in line: + logger.info(f"Deno integration detected: {line.strip()}") + return True + + return False + + except Exception as e: + logger.warning(f"Failed to check yt-dlp Deno integration: {e}") + return False diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index 1f5ad26..6ed900c 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -24,10 +24,55 @@ from ...utils.ytsage_localization import _ from ...core.ytsage_ffmpeg import get_ffmpeg_path from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache -from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path +from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path, check_ytdlp_deno_integration from ...core.ytsage_deno import check_deno_installed, get_deno_path +class SystemInfoThread(QThread): + """Background thread to gather system information.""" + info_ready = Signal(dict) + + def run(self): + info = {} + + # yt-dlp Status + ytdlp_found = check_ytdlp_installed() + info['ytdlp_found'] = ytdlp_found + info['ytdlp_version'] = get_ytdlp_version() + info['ytdlp_path'] = get_yt_dlp_path() if ytdlp_found else None + + # yt-dlp cache status + ytdlp_cache = _version_cache.get("ytdlp", {}) + info['ytdlp_last_check'] = ytdlp_cache.get("last_check", 0) + + # FFmpeg Status + ffmpeg_found = check_ffmpeg() + info['ffmpeg_found'] = ffmpeg_found + info['ffmpeg_version'] = get_ffmpeg_version() if ffmpeg_found else _('about.not_available') + info['ffmpeg_path'] = get_ffmpeg_path() if ffmpeg_found else None + + # FFmpeg cache status + ffmpeg_cache = _version_cache.get("ffmpeg", {}) + info['ffmpeg_last_check'] = ffmpeg_cache.get("last_check", 0) + + # Deno Status + deno_found = check_deno_installed() + info['deno_found'] = deno_found + info['deno_version'] = get_deno_version() if deno_found else _('about.not_available') + info['deno_path'] = get_deno_path() if deno_found else None + + # Deno cache status + deno_cache = _version_cache.get("deno", {}) + info['deno_last_check'] = deno_cache.get("last_check", 0) + + # Check integration with yt-dlp if both are present + info['integration_status'] = False + if deno_found and ytdlp_found: + info['integration_status'] = check_ytdlp_deno_integration() + + self.info_ready.emit(info) + + class LogWindow(QDialog): def __init__(self, parent=None) -> None: super().__init__(parent) @@ -378,7 +423,13 @@ class AboutDialog(QDialog): return item_widget def update_system_info(self) -> None: - """Update the system information display with compact layout.""" + """Start background thread to gather system info.""" + self.info_thread = SystemInfoThread() + self.info_thread.info_ready.connect(self._populate_system_info) + self.info_thread.start() + + def _populate_system_info(self, info: dict) -> None: + """Populate UI with gathered info.""" # Clear existing items for i in reversed(range(self.status_container.count())): child = self.status_container.itemAt(i).widget() @@ -386,19 +437,18 @@ class AboutDialog(QDialog): child.deleteLater() # yt-dlp Status - compact version with path - ytdlp_found = check_ytdlp_installed() + ytdlp_found = info['ytdlp_found'] ytdlp_status_text = ( f"{_('about.detected')}" if ytdlp_found else f"{_('about.missing')}" ) - ytdlp_version = get_ytdlp_version() + ytdlp_version = info['ytdlp_version'] # Get yt-dlp path - ytdlp_path = get_yt_dlp_path() if ytdlp_found else None + ytdlp_path = info['ytdlp_path'] ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None # Simplified cache status - ytdlp_cache = _version_cache.get("ytdlp", {}) - last_check = ytdlp_cache.get("last_check", 0) + last_check = info['ytdlp_last_check'] cache_status = "" if last_check > 0: cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") @@ -414,23 +464,19 @@ class AboutDialog(QDialog): self.status_container.addWidget(ytdlp_item) # FFmpeg Status - compact version with path - ffmpeg_found = check_ffmpeg() + ffmpeg_found = info['ffmpeg_found'] ffmpeg_status_text = ( f"{_('about.detected')}" if ffmpeg_found else f"{_('about.missing')}" ) - ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else _('about.not_available') + ffmpeg_version = info['ffmpeg_version'] # Get FFmpeg path - ffmpeg_path_text = None - if ffmpeg_found: - ffmpeg_path = get_ffmpeg_path() - ffmpeg_path_text = ffmpeg_path if ffmpeg_path else None + ffmpeg_path_text = info['ffmpeg_path'] # Simplified cache status for FFmpeg - ffmpeg_cache = _version_cache.get("ffmpeg", {}) - last_check = ffmpeg_cache.get("last_check", 0) + last_check = info['ffmpeg_last_check'] cache_status = "" if last_check > 0 and ffmpeg_found: cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") @@ -446,16 +492,16 @@ class AboutDialog(QDialog): self.status_container.addWidget(ffmpeg_item) # Deno Status - compact version with path (only show path if in app bin directory) - deno_found = check_deno_installed() + deno_found = info['deno_found'] deno_status_text = ( f"{_('about.detected')}" if deno_found else f"{_('about.missing')}" ) - deno_version = get_deno_version() if deno_found else _('about.not_available') + deno_version = info['deno_version'] # Get Deno path - only show if in app bin directory deno_path_text = None if deno_found: - deno_path = get_deno_path() + deno_path = info['deno_path'] # Only show path if it's not the fallback "deno" and the file exists if deno_path and deno_path != "deno": from pathlib import Path @@ -465,22 +511,27 @@ class AboutDialog(QDialog): deno_path_text = deno_path # Simplified cache status for Deno - deno_cache = _version_cache.get("deno", {}) - last_check = deno_cache.get("last_check", 0) + last_check = info['deno_last_check'] cache_status = "" if last_check > 0 and deno_found: cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_status = f" ({cache_time})" + + # Check integration with yt-dlp if both are present + integration_status = "" + if info.get('integration_status', False): + integration_status = f" + yt-dlp" deno_item = self._create_status_item( "ЁЯжХ", "Deno", deno_status_text, - deno_version + cache_status, + deno_version + cache_status + integration_status, deno_path_text, ) self.status_container.addWidget(deno_item) + def refresh_version_info(self) -> None: """Refresh version information manually.""" self.refresh_btn.setText(_('about.refreshing')) From 49dde844817937017f5bc7920515f26ff418ab02 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:17:30 +0200 Subject: [PATCH 100/134] Add progress signals and fake progress timer Emit progress updates during URL analysis and simulate gradual progress for long-running yt-dlp operations. Added QTimer import and a progress_update Signal on AnalysisThread, with emits at multiple analysis milestones (e.g. 15, 30, 60, 70, 85, 90, 92, 95, 100). In AnalysisMixin introduced _analysis_timer and _fake_progress, connected thread progress to _handle_analysis_progress, and implemented a _update_fake_progress timer that slowly advances the progress bar between real updates. Timers are stopped on real progress, completion or error and the UI progress is reset appropriately to improve UX during long extraction steps. --- ytsage/gui/ytsage_gui_analysis.py | 68 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/ytsage/gui/ytsage_gui_analysis.py b/ytsage/gui/ytsage_gui_analysis.py index 46604ce..8abf141 100644 --- a/ytsage/gui/ytsage_gui_analysis.py +++ b/ytsage/gui/ytsage_gui_analysis.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast import json import subprocess -from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal +from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal, QTimer from PySide6.QtWidgets import QMessageBox from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error @@ -22,6 +22,7 @@ class AnalysisThread(QThread): """ # Signals for status updates status_update = Signal(str) + progress_update = Signal(int) # Signal for progress bar updates # Signals for playlist UI playlist_info_visible = Signal(bool) @@ -63,6 +64,7 @@ class AnalysisThread(QThread): """Main thread execution - performs URL analysis.""" try: self.status_update.emit(_("main_ui.analyzing_extracting_basic")) + self.progress_update.emit(15) url = self.url # Clean up the URL to handle both playlist and video URLs @@ -107,6 +109,7 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_extracting_ytdlp")) + self.progress_update.emit(30) # This will trigger fake progress in UI # Build command for basic info extraction cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url] @@ -156,6 +159,7 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_processing_data")) + self.progress_update.emit(60) # Prepare result data result_data: Dict[str, Any] = { @@ -184,6 +188,7 @@ class AnalysisThread(QThread): # Fetch full info for the first video to get formats self.status_update.emit(_("main_ui.analyzing_fetching_first_video")) + self.progress_update.emit(70) first_video_entry = playlist_entries[0] first_video_url = first_video_entry.get("url") @@ -235,20 +240,24 @@ class AnalysisThread(QThread): return self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp")) + self.progress_update.emit(85) result_data["all_formats"] = video_info.get("formats", []) # Get thumbnail URL self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp")) + self.progress_update.emit(90) playlist_info = result_data.get("playlist_info") or {} thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail") result_data["thumbnail_url"] = thumbnail_url # Handle subtitles self.status_update.emit(_("main_ui.analyzing_processing_subtitles_ytdlp")) + self.progress_update.emit(92) result_data["available_subtitles"] = video_info.get("subtitles", {}) result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {}) self.status_update.emit(_("main_ui.analyzing_updating_table")) + self.progress_update.emit(95) # Emit all results at once self.analysis_complete.emit(result_data) @@ -259,6 +268,8 @@ class AnalysisMixin: # Track the current analysis thread _analysis_thread: Optional[AnalysisThread] = None + _analysis_timer: Optional[QTimer] = None + _fake_progress: int = 0 def analyze_url(self) -> None: """Start URL analysis in a background thread.""" @@ -293,11 +304,18 @@ class AnalysisMixin: self.toggle_analysis_dependent_controls(enabled=False) self.signals.update_status.emit(_("main_ui.analyzing_preparing")) + self.signals.update_progress.emit(0) # Reset progress self.is_analyzing = True + + # Stop any existing timer + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None # Create and configure the analysis thread self._analysis_thread = AnalysisThread( url=url, + # ... arguments will be filled by ... usage below ... cookie_file_path=self.cookie_file_path, browser_cookies_option=self.browser_cookies_option, proxy_url=self.proxy_url, @@ -307,6 +325,7 @@ class AnalysisMixin: # Connect signals to handlers self._analysis_thread.status_update.connect(self.signals.update_status.emit) + self._analysis_thread.progress_update.connect(self._handle_analysis_progress) self._analysis_thread.playlist_info_visible.connect(self.signals.playlist_info_label_visible.emit) self._analysis_thread.playlist_info_text.connect(self.signals.playlist_info_label_text.emit) self._analysis_thread.playlist_select_btn_visible.connect(self.signals.playlist_select_btn_visible.emit) @@ -318,10 +337,53 @@ class AnalysisMixin: # Start the thread self._analysis_thread.start() + def _handle_analysis_progress(self, value: int) -> None: + """Handle progress updates from analysis thread.""" + self = cast("YTSageApp", self) + + # Stop fake timer if running on any real update + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + + self.signals.update_progress.emit(value) + + # If we hit the extraction phase (30%), start fake progress + if value == 30: + self._fake_progress = 30 + self._analysis_timer = QTimer(self) + self._analysis_timer.timeout.connect(self._update_fake_progress) + self._analysis_timer.start(200) # Every 200ms + + def _update_fake_progress(self) -> None: + """Increment progress bar slowly during long operations.""" + self = cast("YTSageApp", self) + + # Asymptotically approach 85% + if self._fake_progress < 85: + # Slow down as we get higher + increment = 1 + if self._fake_progress > 60: + if self._fake_progress % 3 == 0: # Slower + increment = 1 + else: + increment = 0 + + if increment > 0: + self._fake_progress += increment + self.signals.update_progress.emit(self._fake_progress) + def _on_analysis_complete(self, result_data: Dict[str, Any]) -> None: """Handle successful analysis completion - runs in main thread.""" self = cast("YTSageApp", self) + # Stop fake timer + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + + self.signals.update_progress.emit(100) + # Update instance variables with results (safe - we're in main thread) self.is_playlist = result_data["is_playlist"] self.playlist_info = result_data["playlist_info"] @@ -362,6 +424,10 @@ class AnalysisMixin: def _on_analysis_error(self, error_message: str) -> None: """Handle analysis error - runs in main thread.""" self = cast("YTSageApp", self) + if self._analysis_timer: + self._analysis_timer.stop() + self._analysis_timer = None + self.signals.update_progress.emit(0) self.signals.update_status.emit(error_message) def _on_analysis_finished(self) -> None: From e86704c5cdbaa558ec4dd6f2caa1607e56a855b3 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:17:44 +0200 Subject: [PATCH 101/134] Simplify analyzing status messages Update English UI strings in ytsage/languages/en.json to remove progress percentages and the repeated "Analyzing" prefix, replacing them with concise action messages (e.g., "Extracting basic info...", "Loading thumbnail..."). This makes status messages shorter and more consistent across regular and ytdlp-related entries without changing functionality. --- ytsage/languages/en.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index 807fd1e..9281bb3 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -346,21 +346,21 @@ "browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}", "error_no_format_info": "Error: No format information available.", "error_extract_info": "Error: Could not extract basic video information. Please check your link.", - "analyzing_preparing": "Analyzing (0%)... Preparing request", - "analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info", - "analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info", - "analyzing_processing_video": "Analyzing (45%)... Processing video data", - "analyzing_processing_formats": "Analyzing (60%)... Processing formats", - "analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail", - "analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles", - "analyzing_updating_table": "Analyzing (95%)... Updating format table", + "analyzing_preparing": "Preparing request...", + "analyzing_extracting_basic": "Extracting basic info...", + "analyzing_extracting_detailed": "Extracting detailed info...", + "analyzing_processing_video": "Processing video data...", + "analyzing_processing_formats": "Processing formats...", + "analyzing_loading_thumbnail": "Loading thumbnail...", + "analyzing_processing_subtitles": "Processing subtitles...", + "analyzing_updating_table": "Updating format table...", "analysis_complete": "Analysis complete!", - "analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info", - "analyzing_fetching_first_video": "Analyzing... Fetching formats for first video", - "analyzing_processing_data": "Analyzing (60%)... Processing data", - "analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats", - "analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail", - "analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles", + "analyzing_extracting_ytdlp": "Extracting info...", + "analyzing_fetching_first_video": "Fetching formats for first video...", + "analyzing_processing_data": "Processing data...", + "analyzing_processing_formats_ytdlp": "Processing formats...", + "analyzing_loading_thumbnail_ytdlp": "Loading thumbnail...", + "analyzing_processing_subtitles_ytdlp": "Processing subtitles...", "select_subtitles": "Select Subtitles...", "sponsorblock_categories": "SponsorBlock Categories...", "invalid_url_or_enter": "Invalid URL or please enter a URL.", From cd9e410ea327b5e750866610814c27652f0ad6e7 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:20:41 +0200 Subject: [PATCH 102/134] Simplify analysis status messages in locales Standardize and simplify analysis/status strings across multiple language files by removing percent-based prefixes and verbose phrasing, adding concise messages with trailing ellipses. This updates localization for ar, de, es, fr, hi, id, it, ja, pl, pt, ru, tr, and zh (ytsage/languages/*.json) to make progress messages cleaner and consistent for both regular and ytdlp-related steps. --- ytsage/languages/ar.json | 28 ++++++++++++++-------------- ytsage/languages/de.json | 28 ++++++++++++++-------------- ytsage/languages/es.json | 28 ++++++++++++++-------------- ytsage/languages/fr.json | 28 ++++++++++++++-------------- ytsage/languages/hi.json | 28 ++++++++++++++-------------- ytsage/languages/id.json | 28 ++++++++++++++-------------- ytsage/languages/it.json | 28 ++++++++++++++-------------- ytsage/languages/ja.json | 28 ++++++++++++++-------------- ytsage/languages/pl.json | 28 ++++++++++++++-------------- ytsage/languages/pt.json | 28 ++++++++++++++-------------- ytsage/languages/ru.json | 28 ++++++++++++++-------------- ytsage/languages/tr.json | 28 ++++++++++++++-------------- ytsage/languages/zh.json | 28 ++++++++++++++-------------- 13 files changed, 182 insertions(+), 182 deletions(-) diff --git a/ytsage/languages/ar.json b/ytsage/languages/ar.json index e12daae..4160c53 100644 --- a/ytsage/languages/ar.json +++ b/ytsage/languages/ar.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "╪│┘К╪к┘Е ╪з╪│╪к╪о╪▒╪з╪м ┘Г┘И┘Г┘К╪▓ ╪з┘Д┘Е╪к╪╡┘Б╪н ┘Е┘Ж: {browser}", "error_no_format_info": "╪о╪╖╪г: ┘Д╪з ╪к┘И╪м╪п ┘Е╪╣┘Д┘И┘Е╪з╪к ╪к┘Ж╪│┘К┘В ┘Е╪к╪з╪н╪й.", "error_extract_info": "╪о╪╖╪г: ╪к╪╣╪░╪▒ ╪з╪│╪к╪о╪▒╪з╪м ┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д┘Б┘К╪п┘К┘И ╪з┘Д╪г╪│╪з╪│┘К╪й. ╪к╪н┘В┘В ┘Е┘Ж ╪з┘Д╪▒╪з╪и╪╖.", - "analyzing_preparing": "╪з┘Д╪к╪н┘Д┘К┘Д (0%)... ╪м╪з╪▒┘К ╪з┘Д╪к╪н╪╢┘К╪▒ ┘Д┘Д╪╖┘Д╪и", - "analyzing_extracting_basic": "╪з┘Д╪к╪н┘Д┘К┘Д (15%)... ╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д╪г╪│╪з╪│┘К╪й", - "analyzing_extracting_detailed": "╪з┘Д╪к╪н┘Д┘К┘Д (30%)... ╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д╪к┘Б╪╡┘К┘Д┘К╪й", - "analyzing_processing_video": "╪з┘Д╪к╪н┘Д┘К┘Д (45%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪и┘К╪з┘Ж╪з╪к ╪з┘Д┘Б┘К╪п┘К┘И", - "analyzing_processing_formats": "╪з┘Д╪к╪н┘Д┘К┘Д (60%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к", - "analyzing_loading_thumbnail": "╪з┘Д╪к╪н┘Д┘К┘Д (75%)... ╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ╪з┘Д╪╡┘И╪▒╪й ╪з┘Д┘Е╪╡╪║╪▒╪й", - "analyzing_processing_subtitles": "╪з┘Д╪к╪н┘Д┘К┘Д (85%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к╪▒╪м┘Е╪з╪к", - "analyzing_updating_table": "╪з┘Д╪к╪н┘Д┘К┘Д (95%)... ╪м╪з╪▒┘К ╪к╪н╪п┘К╪л ╪м╪п┘И┘Д ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к", + "analyzing_preparing": "╪м╪з╪▒┘К ╪з┘Д╪к╪н╪╢┘К╪▒ ┘Д┘Д╪╖┘Д╪и...", + "analyzing_extracting_basic": "╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д╪г╪│╪з╪│┘К╪й...", + "analyzing_extracting_detailed": "╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д╪к┘Б╪╡┘К┘Д┘К╪й...", + "analyzing_processing_video": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪и┘К╪з┘Ж╪з╪к ╪з┘Д┘Б┘К╪п┘К┘И...", + "analyzing_processing_formats": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к...", + "analyzing_loading_thumbnail": "╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ╪з┘Д╪╡┘И╪▒╪й ╪з┘Д┘Е╪╡╪║╪▒╪й...", + "analyzing_processing_subtitles": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к╪▒╪м┘Е╪з╪к...", + "analyzing_updating_table": "╪м╪з╪▒┘К ╪к╪н╪п┘К╪л ╪м╪п┘И┘Д ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к...", "analysis_complete": "╪з┘Г╪к┘Е┘Д ╪з┘Д╪к╪н┘Д┘К┘Д!", - "analyzing_extracting_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (30%)... ╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к", - "analyzing_fetching_first_video": "╪к╪н┘Д┘К┘Д... ╪м╪з╪▒┘К ╪м┘Д╪и ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к ┘Д┘Д┘Б┘К╪п┘К┘И ╪з┘Д╪г┘И┘Д", - "analyzing_processing_data": "╪з┘Д╪к╪н┘Д┘К┘Д (60%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪и┘К╪з┘Ж╪з╪к", - "analyzing_processing_formats_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (75%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к", - "analyzing_loading_thumbnail_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (85%)... ╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ╪з┘Д╪╡┘И╪▒╪й ╪з┘Д┘Е╪╡╪║╪▒╪й", - "analyzing_processing_subtitles_ytdlp": "╪з┘Д╪к╪н┘Д┘К┘Д (90%)... ╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к╪▒╪м┘Е╪з╪к", + "analyzing_extracting_ytdlp": "╪м╪з╪▒┘К ╪з╪│╪к╪о╪▒╪з╪м ╪з┘Д┘Е╪╣┘Д┘И┘Е╪з╪к...", + "analyzing_fetching_first_video": "╪м╪з╪▒┘К ╪м┘Д╪и ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к ┘Д┘Д┘Б┘К╪п┘К┘И ╪з┘Д╪г┘И┘Д...", + "analyzing_processing_data": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪и┘К╪з┘Ж╪з╪к...", + "analyzing_processing_formats_ytdlp": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к┘Ж╪│┘К┘В╪з╪к...", + "analyzing_loading_thumbnail_ytdlp": "╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ╪з┘Д╪╡┘И╪▒╪й ╪з┘Д┘Е╪╡╪║╪▒╪й...", + "analyzing_processing_subtitles_ytdlp": "╪м╪з╪▒┘К ┘Е╪╣╪з┘Д╪м╪й ╪з┘Д╪к╪▒╪м┘Е╪з╪к...", "select_subtitles": "╪з╪о╪к╪▒ ╪з┘Д╪к╪▒╪м┘Е╪з╪к...", "sponsorblock_categories": "┘Б╪ж╪з╪к SponsorBlock...", "invalid_url_or_enter": "╪╣┘Ж┘И╪з┘Ж URL ╪║┘К╪▒ ╪╡╪з┘Д╪н ╪г┘И ╪з┘Д╪▒╪м╪з╪б ╪е╪п╪о╪з┘Д ╪╣┘Ж┘И╪з┘Ж URL.", diff --git a/ytsage/languages/de.json b/ytsage/languages/de.json index 2c17bac..eff03ee 100644 --- a/ytsage/languages/de.json +++ b/ytsage/languages/de.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Browser-Cookies werden aus folgendem Browser extrahiert: {browser}", "error_no_format_info": "Fehler: Keine Formatinformationen verf├╝gbar.", "error_extract_info": "Fehler: Grundlegende Videoinformationen konnten nicht extrahiert werden. Bitte ├╝berpr├╝fen Sie Ihren Link.", - "analyzing_preparing": "Analysiere (0%)... Anfrage wird vorbereitet", - "analyzing_extracting_basic": "Analysiere (15%)... Grundlegende Informationen werden extrahiert", - "analyzing_extracting_detailed": "Analysiere (30%)... Detaillierte Informationen werden extrahiert", - "analyzing_processing_video": "Analysiere (45%)... Videodaten werden verarbeitet", - "analyzing_processing_formats": "Analysiere (60%)... Formate werden verarbeitet", - "analyzing_loading_thumbnail": "Analysiere (75%)... Thumbnail wird geladen", - "analyzing_processing_subtitles": "Analysiere (85%)... Untertitel werden verarbeitet", - "analyzing_updating_table": "Analysiere (95%)... Formattabelle wird aktualisiert", + "analyzing_preparing": "Anfrage wird vorbereitet...", + "analyzing_extracting_basic": "Grundlegende Informationen werden extrahiert...", + "analyzing_extracting_detailed": "Detaillierte Informationen werden extrahiert...", + "analyzing_processing_video": "Videodaten werden verarbeitet...", + "analyzing_processing_formats": "Formate werden verarbeitet...", + "analyzing_loading_thumbnail": "Thumbnail wird geladen...", + "analyzing_processing_subtitles": "Untertitel werden verarbeitet...", + "analyzing_updating_table": "Formattabelle wird aktualisiert...", "analysis_complete": "Analyse abgeschlossen!", - "analyzing_extracting_ytdlp": "Analysiere (30%)... Informationen werden extrahiert", - "analyzing_fetching_first_video": "Analysiere... Formate f├╝r das erste Video werden abgerufen", - "analyzing_processing_data": "Analysiere (60%)... Daten werden verarbeitet", - "analyzing_processing_formats_ytdlp": "Analysiere (75%)... Formate werden verarbeitet", - "analyzing_loading_thumbnail_ytdlp": "Analysiere (85%)... Thumbnail wird geladen", - "analyzing_processing_subtitles_ytdlp": "Analysiere (90%)... Untertitel werden verarbeitet", + "analyzing_extracting_ytdlp": "Informationen werden extrahiert...", + "analyzing_fetching_first_video": "Formate f├╝r das erste Video werden abgerufen...", + "analyzing_processing_data": "Daten werden verarbeitet...", + "analyzing_processing_formats_ytdlp": "Formate werden verarbeitet...", + "analyzing_loading_thumbnail_ytdlp": "Thumbnail wird geladen...", + "analyzing_processing_subtitles_ytdlp": "Untertitel werden verarbeitet...", "select_subtitles": "Untertitel ausw├дhlen...", "sponsorblock_categories": "SponsorBlock-Kategorien...", "invalid_url_or_enter": "Ung├╝ltige URL oder bitte geben Sie eine URL ein.", diff --git a/ytsage/languages/es.json b/ytsage/languages/es.json index 3482bca..1e4b5e2 100644 --- a/ytsage/languages/es.json +++ b/ytsage/languages/es.json @@ -328,21 +328,21 @@ "browser_cookies_selected_message": "Las cookies del navegador ser├бn extra├нdas de: {browser}", "error_no_format_info": "Error: No hay informaci├│n de formato disponible.", "error_extract_info": "Error: No se pudo extraer la informaci├│n b├бsica del video. Por favor verifica tu enlace.", - "analyzing_preparing": "Analizando (0%)... Preparando solicitud", - "analyzing_extracting_basic": "Analizando (15%)... Extrayendo informaci├│n b├бsica", - "analyzing_extracting_detailed": "Analizando (30%)... Extrayendo informaci├│n detallada", - "analyzing_processing_video": "Analizando (45%)... Procesando datos de video", - "analyzing_processing_formats": "Analizando (60%)... Procesando formatos", - "analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura", - "analyzing_processing_subtitles": "Analizando (85%)... Procesando subt├нtulos", - "analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos", + "analyzing_preparing": "Preparando solicitud...", + "analyzing_extracting_basic": "Extrayendo informaci├│n b├бsica...", + "analyzing_extracting_detailed": "Extrayendo informaci├│n detallada...", + "analyzing_processing_video": "Procesando datos de video...", + "analyzing_processing_formats": "Procesando formatos...", + "analyzing_loading_thumbnail": "Cargando miniatura...", + "analyzing_processing_subtitles": "Procesando subt├нtulos...", + "analyzing_updating_table": "Actualizando tabla de formatos...", "analysis_complete": "┬бAn├бlisis completo!", - "analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo informaci├│n", - "analyzing_fetching_first_video": "Analizando... Obteniendo formatos del primer video", - "analyzing_processing_data": "Analizando (60%)... Procesando datos", - "analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos", - "analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura", - "analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subt├нtulos", + "analyzing_extracting_ytdlp": "Extrayendo informaci├│n...", + "analyzing_fetching_first_video": "Obteniendo formatos del primer video...", + "analyzing_processing_data": "Procesando datos...", + "analyzing_processing_formats_ytdlp": "Procesando formatos...", + "analyzing_loading_thumbnail_ytdlp": "Cargando miniatura...", + "analyzing_processing_subtitles_ytdlp": "Procesando subt├нtulos...", "select_subtitles": "Seleccionar Subt├нtulos...", "sponsorblock_categories": "Categor├нas SponsorBlock...", "invalid_url_or_enter": "URL inv├бlida o por favor ingresa una URL.", diff --git a/ytsage/languages/fr.json b/ytsage/languages/fr.json index f0d45f9..8954a3a 100644 --- a/ytsage/languages/fr.json +++ b/ytsage/languages/fr.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Les cookies du navigateur seront extraits depuis : {browser}", "error_no_format_info": "Erreur : Aucune information de format disponible.", "error_extract_info": "Erreur : Impossible d'extraire les informations de base de la vid├йo. Veuillez v├йrifier votre lien.", - "analyzing_preparing": "Analyse (0%)... Pr├йparation de la requ├кte", - "analyzing_extracting_basic": "Analyse (15%)... Extraction des informations de base", - "analyzing_extracting_detailed": "Analyse (30%)... Extraction des informations d├йtaill├йes", - "analyzing_processing_video": "Analyse (45%)... Traitement des donn├йes vid├йo", - "analyzing_processing_formats": "Analyse (60%)... Traitement des formats", - "analyzing_loading_thumbnail": "Analyse (75%)... Chargement de la miniature", - "analyzing_processing_subtitles": "Analyse (85%)... Traitement des sous-titres", - "analyzing_updating_table": "Analyse (95%)... Mise ├а jour du tableau des formats", + "analyzing_preparing": "Pr├йparation de la requ├кte...", + "analyzing_extracting_basic": "Extraction des informations de base...", + "analyzing_extracting_detailed": "Extraction des informations d├йtaill├йes...", + "analyzing_processing_video": "Traitement des donn├йes vid├йo...", + "analyzing_processing_formats": "Traitement des formats...", + "analyzing_loading_thumbnail": "Chargement de la miniature...", + "analyzing_processing_subtitles": "Traitement des sous-titres...", + "analyzing_updating_table": "Mise ├а jour du tableau des formats...", "analysis_complete": "Analyse termin├йe !", - "analyzing_extracting_ytdlp": "Analyse (30%)... Extraction d'informations", - "analyzing_fetching_first_video": "Analyse... R├йcup├йration des formats pour la premi├иre vid├йo", - "analyzing_processing_data": "Analyse (60%)... Traitement des donn├йes", - "analyzing_processing_formats_ytdlp": "Analyse (75%)... Traitement des formats", - "analyzing_loading_thumbnail_ytdlp": "Analyse (85%)... Chargement de la miniature", - "analyzing_processing_subtitles_ytdlp": "Analyse (90%)... Traitement des sous-titres", + "analyzing_extracting_ytdlp": "Extraction d'informations...", + "analyzing_fetching_first_video": "R├йcup├йration des formats pour la premi├иre vid├йo...", + "analyzing_processing_data": "Traitement des donn├йes...", + "analyzing_processing_formats_ytdlp": "Traitement des formats...", + "analyzing_loading_thumbnail_ytdlp": "Chargement de la miniature...", + "analyzing_processing_subtitles_ytdlp": "Traitement des sous-titres...", "select_subtitles": "S├йlectionner les sous-titres...", "sponsorblock_categories": "Cat├йgories SponsorBlock...", "invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.", diff --git a/ytsage/languages/hi.json b/ytsage/languages/hi.json index 97ea90d..18de247 100644 --- a/ytsage/languages/hi.json +++ b/ytsage/languages/hi.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреБрдХреАрдЬрд╝ рдирд┐рдХрд╛рд▓реА рдЬрд╛рдПрдВрдЧреА: {browser}", "error_no_format_info": "рддреНрд░реБрдЯрд┐: рдХреЛрдИ рдкреНрд░рд╛рд░реВрдк рдЬрд╛рдирдХрд╛рд░реА рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВред", "error_extract_info": "рддреНрд░реБрдЯрд┐: рдмреБрдирд┐рдпрд╛рджреА рд╡реАрдбрд┐рдпреЛ рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓рдиреЗ рдореЗрдВ рдЕрд╕рдорд░реНрдеред рдХреГрдкрдпрд╛ рдЕрдкрдирд╛ рд▓рд┐рдВрдХ рдЬрд╛рдВрдЪреЗрдВред", - "analyzing_preparing": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (0%)... рдЕрдиреБрд░реЛрдз рддреИрдпрд╛рд░ рд╣реЛ рд░рд╣рд╛ рд╣реИ", - "analyzing_extracting_basic": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (15%)... рдмреБрдирд┐рдпрд╛рджреА рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ", - "analyzing_extracting_detailed": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (30%)... рд╡рд┐рд╕реНрддреГрдд рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ", - "analyzing_processing_video": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (45%)... рд╡реАрдбрд┐рдпреЛ рдбреЗрдЯрд╛ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣рд╛ рд╣реИ", - "analyzing_processing_formats": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (60%)... рдкреНрд░рд╛рд░реВрдк рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ", - "analyzing_loading_thumbnail": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (75%)... рдердВрдмрдиреЗрд▓ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ", - "analyzing_processing_subtitles": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (85%)... рдЙрдкрд╢реАрд░реНрд╖рдХ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ", - "analyzing_updating_table": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (95%)... рдкреНрд░рд╛рд░реВрдк рддрд╛рд▓рд┐рдХрд╛ рдЕрдкрдбреЗрдЯ рд╣реЛ рд░рд╣реА рд╣реИ", + "analyzing_preparing": "рдЕрдиреБрд░реЛрдз рддреИрдпрд╛рд░ рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "analyzing_extracting_basic": "рдмреБрдирд┐рдпрд╛рджреА рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ...", + "analyzing_extracting_detailed": "рд╡рд┐рд╕реНрддреГрдд рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ...", + "analyzing_processing_video": "рд╡реАрдбрд┐рдпреЛ рдбреЗрдЯрд╛ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "analyzing_processing_formats": "рдкреНрд░рд╛рд░реВрдк рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ...", + "analyzing_loading_thumbnail": "рдердВрдмрдиреЗрд▓ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "analyzing_processing_subtitles": "рдЙрдкрд╢реАрд░реНрд╖рдХ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ...", + "analyzing_updating_table": "рдкреНрд░рд╛рд░реВрдк рддрд╛рд▓рд┐рдХрд╛ рдЕрдкрдбреЗрдЯ рд╣реЛ рд░рд╣реА рд╣реИ...", "analysis_complete": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдкреВрд░реНрдг!", - "analyzing_extracting_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (30%)... рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ", - "analyzing_fetching_first_video": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдХрд░ рд░рд╣рд╛ рд╣реИ... рдкрд╣рд▓реЗ рд╡реАрдбрд┐рдпреЛ рдХреЗ рд▓рд┐рдП рдкреНрд░рд╛рд░реВрдк рдкреНрд░рд╛рдкреНрдд рдХрд░ рд░рд╣рд╛ рд╣реИ", - "analyzing_processing_data": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (60%)... рдбреЗрдЯрд╛ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣рд╛ рд╣реИ", - "analyzing_processing_formats_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (75%)... рдкреНрд░рд╛рд░реВрдк рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ", - "analyzing_loading_thumbnail_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (85%)... рдердВрдмрдиреЗрд▓ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ", - "analyzing_processing_subtitles_ytdlp": "рд╡рд┐рд╢реНрд▓реЗрд╖рдг (90%)... рдЙрдкрд╢реАрд░реНрд╖рдХ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ", + "analyzing_extracting_ytdlp": "рдЬрд╛рдирдХрд╛рд░реА рдирд┐рдХрд╛рд▓реА рдЬрд╛ рд░рд╣реА рд╣реИ...", + "analyzing_fetching_first_video": "рдкрд╣рд▓реЗ рд╡реАрдбрд┐рдпреЛ рдХреЗ рд▓рд┐рдП рдкреНрд░рд╛рд░реВрдк рдкреНрд░рд╛рдкреНрдд рдХрд░ рд░рд╣рд╛ рд╣реИ...", + "analyzing_processing_data": "рдбреЗрдЯрд╛ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "analyzing_processing_formats_ytdlp": "рдкреНрд░рд╛рд░реВрдк рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ...", + "analyzing_loading_thumbnail_ytdlp": "рдердВрдмрдиреЗрд▓ рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ...", + "analyzing_processing_subtitles_ytdlp": "рдЙрдкрд╢реАрд░реНрд╖рдХ рдкреНрд░реЛрд╕реЗрд╕ рд╣реЛ рд░рд╣реЗ рд╣реИрдВ...", "select_subtitles": "рдЙрдкрд╢реАрд░реНрд╖рдХ рдЪреБрдиреЗрдВ...", "sponsorblock_categories": "SponsorBlock рд╢реНрд░реЗрдгрд┐рдпрд╛рдВ...", "invalid_url_or_enter": "рдЕрдорд╛рдиреНрдп URL рдпрд╛ рдХреГрдкрдпрд╛ URL рджрд░реНрдЬ рдХрд░реЗрдВред", diff --git a/ytsage/languages/id.json b/ytsage/languages/id.json index 6a76658..8f41c52 100644 --- a/ytsage/languages/id.json +++ b/ytsage/languages/id.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Cookies browser akan diekstrak dari: {browser}", "error_no_format_info": "Kesalahan: Tidak ada informasi format yang tersedia.", "error_extract_info": "Kesalahan: Tidak dapat mengekstrak informasi video dasar. Silakan periksa tautan Anda.", - "analyzing_preparing": "Menganalisis (0%)... Mempersiapkan permintaan", - "analyzing_extracting_basic": "Menganalisis (15%)... Mengekstrak informasi dasar", - "analyzing_extracting_detailed": "Menganalisis (30%)... Mengekstrak informasi detail", - "analyzing_processing_video": "Menganalisis (45%)... Memproses data video", - "analyzing_processing_formats": "Menganalisis (60%)... Memproses format", - "analyzing_loading_thumbnail": "Menganalisis (75%)... Memuat thumbnail", - "analyzing_processing_subtitles": "Menganalisis (85%)... Memproses subtitle", - "analyzing_updating_table": "Menganalisis (95%)... Memperbarui tabel format", + "analyzing_preparing": "Mempersiapkan permintaan...", + "analyzing_extracting_basic": "Mengekstrak informasi dasar...", + "analyzing_extracting_detailed": "Mengekstrak informasi detail...", + "analyzing_processing_video": "Memproses data video...", + "analyzing_processing_formats": "Memproses format...", + "analyzing_loading_thumbnail": "Memuat thumbnail...", + "analyzing_processing_subtitles": "Memproses subtitle...", + "analyzing_updating_table": "Memperbarui tabel format...", "analysis_complete": "Analisis selesai!", - "analyzing_extracting_ytdlp": "Menganalisis (30%)... Mengekstrak informasi", - "analyzing_fetching_first_video": "Menganalisis... Mengambil format untuk video pertama", - "analyzing_processing_data": "Menganalisis (60%)... Memproses data", - "analyzing_processing_formats_ytdlp": "Menganalisis (75%)... Memproses format", - "analyzing_loading_thumbnail_ytdlp": "Menganalisis (85%)... Memuat thumbnail", - "analyzing_processing_subtitles_ytdlp": "Menganalisis (90%)... Memproses subtitle", + "analyzing_extracting_ytdlp": "Mengekstrak informasi...", + "analyzing_fetching_first_video": "Mengambil format untuk video pertama...", + "analyzing_processing_data": "Memproses data...", + "analyzing_processing_formats_ytdlp": "Memproses format...", + "analyzing_loading_thumbnail_ytdlp": "Memuat thumbnail...", + "analyzing_processing_subtitles_ytdlp": "Memproses subtitle...", "select_subtitles": "Pilih subtitle...", "sponsorblock_categories": "Kategori SponsorBlock...", "invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.", diff --git a/ytsage/languages/it.json b/ytsage/languages/it.json index 36055ec..8b482ec 100644 --- a/ytsage/languages/it.json +++ b/ytsage/languages/it.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "I cookie del browser verranno estratti da: {browser}", "error_no_format_info": "Errore: Nessuna informazione formato disponibile.", "error_extract_info": "Errore: Impossibile estrarre informazioni base del video. Controlla il tuo link.", - "analyzing_preparing": "Analisi (0%)... Preparazione richiesta", - "analyzing_extracting_basic": "Analisi (15%)... Estrazione informazioni base", - "analyzing_extracting_detailed": "Analisi (30%)... Estrazione informazioni dettagliate", - "analyzing_processing_video": "Analisi (45%)... Elaborazione dati video", - "analyzing_processing_formats": "Analisi (60%)... Elaborazione formati", - "analyzing_loading_thumbnail": "Analisi (75%)... Caricamento miniatura", - "analyzing_processing_subtitles": "Analisi (85%)... Elaborazione sottotitoli", - "analyzing_updating_table": "Analisi (95%)... Aggiornamento tabella formati", + "analyzing_preparing": "Preparazione richiesta...", + "analyzing_extracting_basic": "Estrazione informazioni base...", + "analyzing_extracting_detailed": "Estrazione informazioni dettagliate...", + "analyzing_processing_video": "Elaborazione dati video...", + "analyzing_processing_formats": "Elaborazione formati...", + "analyzing_loading_thumbnail": "Caricamento miniatura...", + "analyzing_processing_subtitles": "Elaborazione sottotitoli...", + "analyzing_updating_table": "Aggiornamento tabella formati...", "analysis_complete": "Analisi completata!", - "analyzing_extracting_ytdlp": "Analisi (30%)... Estrazione informazioni", - "analyzing_fetching_first_video": "Analisi... Recupero dei formati per il primo video", - "analyzing_processing_data": "Analisi (60%)... Elaborazione dati", - "analyzing_processing_formats_ytdlp": "Analisi (75%)... Elaborazione formati", - "analyzing_loading_thumbnail_ytdlp": "Analisi (85%)... Caricamento miniatura", - "analyzing_processing_subtitles_ytdlp": "Analisi (90%)... Elaborazione sottotitoli", + "analyzing_extracting_ytdlp": "Estrazione informazioni...", + "analyzing_fetching_first_video": "Recupero dei formati per il primo video...", + "analyzing_processing_data": "Elaborazione dati...", + "analyzing_processing_formats_ytdlp": "Elaborazione formati...", + "analyzing_loading_thumbnail_ytdlp": "Caricamento miniatura...", + "analyzing_processing_subtitles_ytdlp": "Elaborazione sottotitoli...", "select_subtitles": "Seleziona sottotitoli...", "sponsorblock_categories": "Categorie SponsorBlock...", "invalid_url_or_enter": "URL non valido o inserisci un URL.", diff --git a/ytsage/languages/ja.json b/ytsage/languages/ja.json index dc2338b..c8d04e2 100644 --- a/ytsage/languages/ja.json +++ b/ytsage/languages/ja.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "уГЦуГйуВжуВ╢CookieуБМцК╜хЗ║уБХуВМуБ╛уБЩ: {browser}", "error_no_format_info": "уВиуГйуГ╝: хИйчФихПпшГ╜уБкуГХуВйуГ╝уГЮуГГуГИцГЕха▒уБМуБВуВКуБ╛уБЫуВУуАВ", "error_extract_info": "уВиуГйуГ╝: хЯ║цЬмчЪДуБкхЛХчФ╗цГЕха▒уВТцК╜хЗ║уБзуБНуБ╛уБЫуВУуБзуБЧуБЯуАВуГкуГ│уВпуВТчв║шкНуБЧуБжуБПуБауБХуБДуАВ", - "analyzing_preparing": "шзгцЮРф╕н (0%)... уГкуВпуВиуВ╣уГИуВТц║ЦхВЩф╕н", - "analyzing_extracting_basic": "шзгцЮРф╕н (15%)... хЯ║цЬмцГЕха▒уВТцК╜хЗ║ф╕н", - "analyzing_extracting_detailed": "шзгцЮРф╕н (30%)... шй│ч┤░цГЕха▒уВТцК╜хЗ║ф╕н", - "analyzing_processing_video": "шзгцЮРф╕н (45%)... хЛХчФ╗уГЗуГ╝уВ┐уВТхЗжчРЖф╕н", - "analyzing_processing_formats": "шзгцЮРф╕н (60%)... уГХуВйуГ╝уГЮуГГуГИуВТхЗжчРЖф╕н", - "analyzing_loading_thumbnail": "шзгцЮРф╕н (75%)... уВ╡уГауГНуВдуГлуВТшкнуБ┐ш╛╝уБ┐ф╕н", - "analyzing_processing_subtitles": "шзгцЮРф╕н (85%)... хнЧх╣ХуВТхЗжчРЖф╕н", - "analyzing_updating_table": "шзгцЮРф╕н (95%)... уГХуВйуГ╝уГЮуГГуГИуГЖуГ╝уГЦуГлуВТцЫ┤цЦ░ф╕н", + "analyzing_preparing": "уГкуВпуВиуВ╣уГИуВТц║ЦхВЩф╕н...", + "analyzing_extracting_basic": "хЯ║цЬмцГЕха▒уВТцК╜хЗ║ф╕н...", + "analyzing_extracting_detailed": "шй│ч┤░цГЕха▒уВТцК╜хЗ║ф╕н...", + "analyzing_processing_video": "хЛХчФ╗уГЗуГ╝уВ┐уВТхЗжчРЖф╕н...", + "analyzing_processing_formats": "уГХуВйуГ╝уГЮуГГуГИуВТхЗжчРЖф╕н...", + "analyzing_loading_thumbnail": "уВ╡уГауГНуВдуГлуВТшкнуБ┐ш╛╝уБ┐ф╕н...", + "analyzing_processing_subtitles": "хнЧх╣ХуВТхЗжчРЖф╕н...", + "analyzing_updating_table": "уГХуВйуГ╝уГЮуГГуГИуГЖуГ╝уГЦуГлуВТцЫ┤цЦ░ф╕н...", "analysis_complete": "шзгцЮРхоМф║Жя╝Б", - "analyzing_extracting_ytdlp": "шзгцЮРф╕н (30%)... цГЕха▒уВТцК╜хЗ║ф╕н", - "analyzing_fetching_first_video": "шзгцЮРф╕н... цЬАхИЭуБоуГУуГЗуВкуБох╜вх╝ПуВТхПЦх╛ЧуБЧуБжуБДуБ╛уБЩ", - "analyzing_processing_data": "шзгцЮРф╕н (60%)... уГЗуГ╝уВ┐уВТхЗжчРЖф╕н", - "analyzing_processing_formats_ytdlp": "шзгцЮРф╕н (75%)... уГХуВйуГ╝уГЮуГГуГИуВТхЗжчРЖф╕н", - "analyzing_loading_thumbnail_ytdlp": "шзгцЮРф╕н (85%)... уВ╡уГауГНуВдуГлуВТшкнуБ┐ш╛╝уБ┐ф╕н", - "analyzing_processing_subtitles_ytdlp": "шзгцЮРф╕н (90%)... хнЧх╣ХуВТхЗжчРЖф╕н", + "analyzing_extracting_ytdlp": "цГЕха▒уВТцК╜хЗ║ф╕н...", + "analyzing_fetching_first_video": "цЬАхИЭуБоуГУуГЗуВкуБох╜вх╝ПуВТхПЦх╛ЧуБЧуБжуБДуБ╛уБЩ...", + "analyzing_processing_data": "уГЗуГ╝уВ┐уВТхЗжчРЖф╕н...", + "analyzing_processing_formats_ytdlp": "уГХуВйуГ╝уГЮуГГуГИуВТхЗжчРЖф╕н...", + "analyzing_loading_thumbnail_ytdlp": "уВ╡уГауГНуВдуГлуВТшкнуБ┐ш╛╝уБ┐ф╕н...", + "analyzing_processing_subtitles_ytdlp": "хнЧх╣ХуВТхЗжчРЖф╕н...", "select_subtitles": "хнЧх╣ХуВТщБ╕цКЮ...", "sponsorblock_categories": "SponsorBlockуВлуГЖуВ┤уГк...", "invalid_url_or_enter": "чДбхК╣уБкURLуБ╛уБЯуБпURLуВТхЕехКЫуБЧуБжуБПуБауБХуБДуАВ", diff --git a/ytsage/languages/pl.json b/ytsage/languages/pl.json index ca0189b..0a5a500 100644 --- a/ytsage/languages/pl.json +++ b/ytsage/languages/pl.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Ciasteczka przegl─Еdarki zostan─Е wyodr─Щbnione z: {browser}", "error_no_format_info": "B┼В─Еd: Brak dost─Щpnych informacji o formacie.", "error_extract_info": "B┼В─Еd: Nie mo┼╝na wyodr─Щbni─З podstawowych informacji o wideo. Sprawd┼║ sw├│j link.", - "analyzing_preparing": "Analizowanie (0%)... Przygotowywanie ┼╝─Еdania", - "analyzing_extracting_basic": "Analizowanie (15%)... Wyodr─Щbnianie podstawowych informacji", - "analyzing_extracting_detailed": "Analizowanie (30%)... Wyodr─Щbnianie szczeg├│┼Вowych informacji", - "analyzing_processing_video": "Analizowanie (45%)... Przetwarzanie danych wideo", - "analyzing_processing_formats": "Analizowanie (60%)... Przetwarzanie format├│w", - "analyzing_loading_thumbnail": "Analizowanie (75%)... ┼Бadowanie miniatury", - "analyzing_processing_subtitles": "Analizowanie (85%)... Przetwarzanie napis├│w", - "analyzing_updating_table": "Analizowanie (95%)... Aktualizowanie tabeli format├│w", + "analyzing_preparing": "Przygotowywanie ┼╝─Еdania...", + "analyzing_extracting_basic": "Wyodr─Щbnianie podstawowych informacji...", + "analyzing_extracting_detailed": "Wyodr─Щbnianie szczeg├│┼Вowych informacji...", + "analyzing_processing_video": "Przetwarzanie danych wideo...", + "analyzing_processing_formats": "Przetwarzanie format├│w...", + "analyzing_loading_thumbnail": "┼Бadowanie miniatury...", + "analyzing_processing_subtitles": "Przetwarzanie napis├│w...", + "analyzing_updating_table": "Aktualizowanie tabeli format├│w...", "analysis_complete": "Analiza zako┼Дczona!", - "analyzing_fetching_first_video": "Analizowanie... Pobieranie format├│w dla pierwszego filmu", - "analyzing_extracting_ytdlp": "Analizowanie (30%)... Wyodr─Щbnianie informacji", - "analyzing_processing_data": "Analizowanie (60%)... Przetwarzanie danych", - "analyzing_processing_formats_ytdlp": "Analizowanie (75%)... Przetwarzanie format├│w", - "analyzing_loading_thumbnail_ytdlp": "Analizowanie (85%)... ┼Бadowanie miniatury", - "analyzing_processing_subtitles_ytdlp": "Analizowanie (90%)... Przetwarzanie napis├│w", + "analyzing_fetching_first_video": "Pobieranie format├│w dla pierwszego filmu...", + "analyzing_extracting_ytdlp": "Wyodr─Щbnianie informacji...", + "analyzing_processing_data": "Przetwarzanie danych...", + "analyzing_processing_formats_ytdlp": "Przetwarzanie format├│w...", + "analyzing_loading_thumbnail_ytdlp": "┼Бadowanie miniatury...", + "analyzing_processing_subtitles_ytdlp": "Przetwarzanie napis├│w...", "select_subtitles": "Wybierz napisy...", "sponsorblock_categories": "Kategorie SponsorBlock...", "invalid_url_or_enter": "Nieprawid┼Вowy URL lub wprowad┼║ URL.", diff --git a/ytsage/languages/pt.json b/ytsage/languages/pt.json index df2f491..2fde332 100644 --- a/ytsage/languages/pt.json +++ b/ytsage/languages/pt.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Os cookies do navegador ser├гo extra├нdos de: {browser}", "error_no_format_info": "Erro: Nenhuma informa├з├гo de formato dispon├нvel.", "error_extract_info": "Erro: N├гo foi poss├нvel extrair informa├з├╡es b├бsicas do v├нdeo. Verifique seu link.", - "analyzing_preparing": "Analisando (0%)... Preparando solicita├з├гo", - "analyzing_extracting_basic": "Analisando (15%)... Extraindo informa├з├╡es b├бsicas", - "analyzing_extracting_detailed": "Analisando (30%)... Extraindo informa├з├╡es detalhadas", - "analyzing_processing_video": "Analisando (45%)... Processando dados do v├нdeo", - "analyzing_processing_formats": "Analisando (60%)... Processando formatos", - "analyzing_loading_thumbnail": "Analisando (75%)... Carregando miniatura", - "analyzing_processing_subtitles": "Analisando (85%)... Processando legendas", - "analyzing_updating_table": "Analisando (95%)... Atualizando tabela de formatos", + "analyzing_preparing": "Preparando solicita├з├гo...", + "analyzing_extracting_basic": "Extraindo informa├з├╡es b├бsicas...", + "analyzing_extracting_detailed": "Extraindo informa├з├╡es detalhadas...", + "analyzing_processing_video": "Processando dados do v├нdeo...", + "analyzing_processing_formats": "Processando formatos...", + "analyzing_loading_thumbnail": "Carregando miniatura...", + "analyzing_processing_subtitles": "Processando legendas...", + "analyzing_updating_table": "Atualizando tabela de formatos...", "analysis_complete": "An├бlise completa!", - "analyzing_fetching_first_video": "Analisando... Obtendo formatos do primeiro v├нdeo", - "analyzing_extracting_ytdlp": "Analisando (30%)... Extraindo informa├з├╡es", - "analyzing_processing_data": "Analisando (60%)... Processando dados", - "analyzing_processing_formats_ytdlp": "Analisando (75%)... Processando formatos", - "analyzing_loading_thumbnail_ytdlp": "Analisando (85%)... Carregando miniatura", - "analyzing_processing_subtitles_ytdlp": "Analisando (90%)... Processando legendas", + "analyzing_fetching_first_video": "Obtendo formatos do primeiro v├нdeo...", + "analyzing_extracting_ytdlp": "Extraindo informa├з├╡es...", + "analyzing_processing_data": "Processando dados...", + "analyzing_processing_formats_ytdlp": "Processando formatos...", + "analyzing_loading_thumbnail_ytdlp": "Carregando miniatura...", + "analyzing_processing_subtitles_ytdlp": "Processando legendas...", "select_subtitles": "Selecionar Legendas...", "sponsorblock_categories": "Categorias SponsorBlock...", "invalid_url_or_enter": "URL inv├бlido ou por favor insira um URL.", diff --git a/ytsage/languages/ru.json b/ytsage/languages/ru.json index 2888c4e..e88540e 100644 --- a/ytsage/languages/ru.json +++ b/ytsage/languages/ru.json @@ -345,21 +345,21 @@ "browser_cookies_selected_message": "Cookie ╨▒╤А╨░╤Г╨╖╨╡╤А╨░ ╨▒╤Г╨┤╤Г╤В ╨╕╨╖╨▓╨╗╨╡╤З╨╡╨╜╤Л ╨╕╨╖: {browser}", "error_no_format_info": "╨Ю╤И╨╕╨▒╨║╨░: ╨Ш╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╤П ╨╛ ╤Д╨╛╤А╨╝╨░╤В╨╡ ╨╜╨╡╨┤╨╛╤Б╤В╤Г╨┐╨╜╨░.", "error_extract_info": "╨Ю╤И╨╕╨▒╨║╨░: ╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨╕╨╖╨▓╨╗╨╡╤З╤М ╨▒╨░╨╖╨╛╨▓╤Г╤О ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╤О ╨╛ ╨▓╨╕╨┤╨╡╨╛. ╨Я╤А╨╛╨▓╨╡╤А╤М╤В╨╡ ╨▓╨░╤И╤Г ╤Б╤Б╤Л╨╗╨║╤Г.", - "analyzing_preparing": "╨Р╨╜╨░╨╗╨╕╨╖ (0%)... ╨Я╨╛╨┤╨│╨╛╤В╨╛╨▓╨║╨░ ╨╖╨░╨┐╤А╨╛╤Б╨░", - "analyzing_extracting_basic": "╨Р╨╜╨░╨╗╨╕╨╖ (15%)... ╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨▒╨░╨╖╨╛╨▓╨╛╨╣ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕", - "analyzing_extracting_detailed": "╨Р╨╜╨░╨╗╨╕╨╖ (30%)... ╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨┐╨╛╨┤╤А╨╛╨▒╨╜╨╛╨╣ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕", - "analyzing_processing_video": "╨Р╨╜╨░╨╗╨╕╨╖ (45%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╨┤╨░╨╜╨╜╤Л╤Е ╨▓╨╕╨┤╨╡╨╛", - "analyzing_processing_formats": "╨Р╨╜╨░╨╗╨╕╨╖ (60%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓", - "analyzing_loading_thumbnail": "╨Р╨╜╨░╨╗╨╕╨╖ (75%)... ╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╝╨╕╨╜╨╕╨░╤В╤О╤А╤Л", - "analyzing_processing_subtitles": "╨Р╨╜╨░╨╗╨╕╨╖ (85%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Б╤Г╨▒╤В╨╕╤В╤А╨╛╨▓", - "analyzing_updating_table": "╨Р╨╜╨░╨╗╨╕╨╖ (95%)... ╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡ ╤В╨░╨▒╨╗╨╕╤Ж╤Л ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓", + "analyzing_preparing": "╨Я╨╛╨┤╨│╨╛╤В╨╛╨▓╨║╨░ ╨╖╨░╨┐╤А╨╛╤Б╨░...", + "analyzing_extracting_basic": "╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨▒╨░╨╖╨╛╨▓╨╛╨╣ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕...", + "analyzing_extracting_detailed": "╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨┐╨╛╨┤╤А╨╛╨▒╨╜╨╛╨╣ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕...", + "analyzing_processing_video": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╨┤╨░╨╜╨╜╤Л╤Е ╨▓╨╕╨┤╨╡╨╛...", + "analyzing_processing_formats": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓...", + "analyzing_loading_thumbnail": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╝╨╕╨╜╨╕╨░╤В╤О╤А╤Л...", + "analyzing_processing_subtitles": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Б╤Г╨▒╤В╨╕╤В╤А╨╛╨▓...", + "analyzing_updating_table": "╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡ ╤В╨░╨▒╨╗╨╕╤Ж╤Л ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓...", "analysis_complete": "╨Р╨╜╨░╨╗╨╕╨╖ ╨╖╨░╨▓╨╡╤А╤И╨╡╨╜!", - "analyzing_fetching_first_video": "╨Р╨╜╨░╨╗╨╕╨╖... ╨Я╨╛╨╗╤Г╤З╨╡╨╜╨╕╨╡ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓ ╨┤╨╗╤П ╨┐╨╡╤А╨▓╨╛╨│╨╛ ╨▓╨╕╨┤╨╡╨╛", - "analyzing_extracting_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (30%)... ╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕", - "analyzing_processing_data": "╨Р╨╜╨░╨╗╨╕╨╖ (60%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╨┤╨░╨╜╨╜╤Л╤Е", - "analyzing_processing_formats_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (75%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓", - "analyzing_loading_thumbnail_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (85%)... ╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╝╨╕╨╜╨╕╨░╤В╤О╤А╤Л", - "analyzing_processing_subtitles_ytdlp": "╨Р╨╜╨░╨╗╨╕╨╖ (90%)... ╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Б╤Г╨▒╤В╨╕╤В╤А╨╛╨▓", + "analyzing_fetching_first_video": "╨Я╨╛╨╗╤Г╤З╨╡╨╜╨╕╨╡ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓ ╨┤╨╗╤П ╨┐╨╡╤А╨▓╨╛╨│╨╛ ╨▓╨╕╨┤╨╡╨╛...", + "analyzing_extracting_ytdlp": "╨Ш╨╖╨▓╨╗╨╡╤З╨╡╨╜╨╕╨╡ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕...", + "analyzing_processing_data": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╨┤╨░╨╜╨╜╤Л╤Е...", + "analyzing_processing_formats_ytdlp": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Д╨╛╤А╨╝╨░╤В╨╛╨▓...", + "analyzing_loading_thumbnail_ytdlp": "╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╨╝╨╕╨╜╨╕╨░╤В╤О╤А╤Л...", + "analyzing_processing_subtitles_ytdlp": "╨Ю╨▒╤А╨░╨▒╨╛╤В╨║╨░ ╤Б╤Г╨▒╤В╨╕╤В╤А╨╛╨▓...", "select_subtitles": "╨Т╤Л╨▒╤А╨░╤В╤М ╤Б╤Г╨▒╤В╨╕╤В╤А╤Л...", "sponsorblock_categories": "╨Ъ╨░╤В╨╡╨│╨╛╤А╨╕╨╕ SponsorBlock...", "invalid_url_or_enter": "╨Э╨╡╨▓╨╡╤А╨╜╤Л╨╣ URL ╨╕╨╗╨╕ ╨┐╨╛╨╢╨░╨╗╤Г╨╣╤Б╤В╨░ ╨▓╨▓╨╡╨┤╨╕╤В╨╡ URL.", diff --git a/ytsage/languages/tr.json b/ytsage/languages/tr.json index 6d1edc6..2e6701f 100644 --- a/ytsage/languages/tr.json +++ b/ytsage/languages/tr.json @@ -343,21 +343,21 @@ "browser_cookies_selected_message": "Taray─▒c─▒ ├зerezleri ┼Яuradan ├з─▒kar─▒lacak: {browser}", "error_no_format_info": "Hata: Format bilgisi mevcut de─Яil.", "error_extract_info": "Hata: Temel video bilgileri ├з─▒kar─▒lamad─▒. L├╝tfen ba─Яlant─▒n─▒z─▒ kontrol edin.", - "analyzing_preparing": "Analiz ediliyor (0%)... ─░stek haz─▒rlan─▒yor", - "analyzing_extracting_basic": "Analiz ediliyor (15%)... Temel bilgiler ├з─▒kar─▒l─▒yor", - "analyzing_extracting_detailed": "Analiz ediliyor (30%)... Ayr─▒nt─▒l─▒ bilgiler ├з─▒kar─▒l─▒yor", - "analyzing_processing_video": "Analiz ediliyor (45%)... Video verisi i┼Яleniyor", - "analyzing_processing_formats": "Analiz ediliyor (60%)... Formatlar i┼Яleniyor", - "analyzing_loading_thumbnail": "Analiz ediliyor (75%)... K├╝├з├╝k resim y├╝kleniyor", - "analyzing_processing_subtitles": "Analiz ediliyor (85%)... Altyaz─▒lar i┼Яleniyor", - "analyzing_updating_table": "Analiz ediliyor (95%)... Format tablosu g├╝ncelleniyor", + "analyzing_preparing": "─░stek haz─▒rlan─▒yor...", + "analyzing_extracting_basic": "Temel bilgiler ├з─▒kar─▒l─▒yor...", + "analyzing_extracting_detailed": "Ayr─▒nt─▒l─▒ bilgiler ├з─▒kar─▒l─▒yor...", + "analyzing_processing_video": "Video verisi i┼Яleniyor...", + "analyzing_processing_formats": "Formatlar i┼Яleniyor...", + "analyzing_loading_thumbnail": "K├╝├з├╝k resim y├╝kleniyor...", + "analyzing_processing_subtitles": "Altyaz─▒lar i┼Яleniyor...", + "analyzing_updating_table": "Format tablosu g├╝ncelleniyor...", "analysis_complete": "Analiz tamamland─▒!", - "analyzing_fetching_first_video": "Analiz ediliyor... ─░lk video i├зin formatlar al─▒n─▒yor", - "analyzing_extracting_ytdlp": "Analiz ediliyor (30%)... Bilgiler ├з─▒kar─▒l─▒yor", - "analyzing_processing_data": "Analiz ediliyor (60%)... Veriler i┼Яleniyor", - "analyzing_processing_formats_ytdlp": "Analiz ediliyor (75%)... Formatlar i┼Яleniyor", - "analyzing_loading_thumbnail_ytdlp": "Analiz ediliyor (85%)... K├╝├з├╝k resim y├╝kleniyor", - "analyzing_processing_subtitles_ytdlp": "Analiz ediliyor (90%)... Altyaz─▒lar i┼Яleniyor", + "analyzing_fetching_first_video": "─░lk video i├зin formatlar al─▒n─▒yor...", + "analyzing_extracting_ytdlp": "Bilgiler ├з─▒kar─▒l─▒yor...", + "analyzing_processing_data": "Veriler i┼Яleniyor...", + "analyzing_processing_formats_ytdlp": "Formatlar i┼Яleniyor...", + "analyzing_loading_thumbnail_ytdlp": "K├╝├з├╝k resim y├╝kleniyor...", + "analyzing_processing_subtitles_ytdlp": "Altyaz─▒lar i┼Яleniyor...", "select_subtitles": "Altyaz─▒ se├з...", "sponsorblock_categories": "SponsorBlock kategorileri...", "invalid_url_or_enter": "Ge├зersiz URL veya l├╝tfen bir URL girin.", diff --git a/ytsage/languages/zh.json b/ytsage/languages/zh.json index 27498d8..3d0b3a4 100644 --- a/ytsage/languages/zh.json +++ b/ytsage/languages/zh.json @@ -333,21 +333,21 @@ "browser_cookies_selected_message": "х░Жф╗Оф╗еф╕Лц╡ПшзИхЩицПРхПЦ Cookieя╝Ъ{browser}", "error_no_format_info": "щФЩшппя╝ЪцЧахПпчФица╝х╝Пф┐бцБпуАВ", "error_extract_info": "щФЩшппя╝ЪцЧац│ХцПРхПЦхЯ║цЬмшзЖщвСф┐бцБпуАВшп╖цгАцЯецВичЪДщУ╛цОеуАВ", - "analyzing_preparing": "хИЖцЮРф╕н (0%)... цнгхЬихЗЖхдЗшп╖ц▒В", - "analyzing_extracting_basic": "хИЖцЮРф╕н (15%)... цнгхЬицПРхПЦхЯ║цЬмф┐бцБп", - "analyzing_extracting_detailed": "хИЖцЮРф╕н (30%)... цнгхЬицПРхПЦшпжч╗Жф┐бцБп", - "analyzing_processing_video": "хИЖцЮРф╕н (45%)... цнгхЬихдДчРЖшзЖщвСцХ░цНо", - "analyzing_processing_formats": "хИЖцЮРф╕н (60%)... цнгхЬихдДчРЖца╝х╝П", - "analyzing_loading_thumbnail": "хИЖцЮРф╕н (75%)... цнгхЬихКаш╜╜ч╝йчХехЫ╛", - "analyzing_processing_subtitles": "хИЖцЮРф╕н (85%)... цнгхЬихдДчРЖхнЧх╣Х", - "analyzing_updating_table": "хИЖцЮРф╕н (95%)... цнгхЬицЫ┤цЦ░ца╝х╝Пшби", + "analyzing_preparing": "цнгхЬихЗЖхдЗшп╖ц▒В...", + "analyzing_extracting_basic": "цнгхЬицПРхПЦхЯ║цЬмф┐бцБп...", + "analyzing_extracting_detailed": "цнгхЬицПРхПЦшпжч╗Жф┐бцБп...", + "analyzing_processing_video": "цнгхЬихдДчРЖшзЖщвСцХ░цНо...", + "analyzing_processing_formats": "цнгхЬихдДчРЖца╝х╝П...", + "analyzing_loading_thumbnail": "цнгхЬихКаш╜╜ч╝йчХехЫ╛...", + "analyzing_processing_subtitles": "цнгхЬихдДчРЖхнЧх╣Х...", + "analyzing_updating_table": "цнгхЬицЫ┤цЦ░ца╝х╝Пшби...", "analysis_complete": "хИЖцЮРхоМцИРя╝Б", - "analyzing_fetching_first_video": "хИЖцЮРф╕н... цнгхЬишО╖хПЦчммф╕Аф╕кшзЖщвСчЪДца╝х╝П", - "analyzing_extracting_ytdlp": "хИЖцЮРф╕н (30%)... цПРхПЦф┐бцБп", - "analyzing_processing_data": "хИЖцЮРф╕н (60%)... цнгхЬихдДчРЖцХ░цНо", - "analyzing_processing_formats_ytdlp": "хИЖцЮРф╕н (75%)... цнгхЬихдДчРЖца╝х╝П", - "analyzing_loading_thumbnail_ytdlp": "хИЖцЮРф╕н (85%)... цнгхЬихКаш╜╜ч╝йчХехЫ╛", - "analyzing_processing_subtitles_ytdlp": "хИЖцЮРф╕н (90%)... цнгхЬихдДчРЖхнЧх╣Х", + "analyzing_fetching_first_video": "цнгхЬишО╖хПЦчммф╕Аф╕кшзЖщвСчЪДца╝х╝П...", + "analyzing_extracting_ytdlp": "цПРхПЦф┐бцБп...", + "analyzing_processing_data": "цнгхЬихдДчРЖцХ░цНо...", + "analyzing_processing_formats_ytdlp": "цнгхЬихдДчРЖца╝х╝П...", + "analyzing_loading_thumbnail_ytdlp": "цнгхЬихКаш╜╜ч╝йчХехЫ╛...", + "analyzing_processing_subtitles_ytdlp": "цнгхЬихдДчРЖхнЧх╣Х...", "select_subtitles": "щАЙцЛйхнЧх╣Х...", "sponsorblock_categories": "SponsorBlock хИЖч▒╗...", "invalid_url_or_enter": "цЧацХИчЪДURLцИЦшп╖ш╛УхЕеURLуАВ", From a65f0702acd8a67bee6d5a5efb659640c152b6ba Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:32:39 +0200 Subject: [PATCH 103/134] Bump version to 5.0.0b3 Update package version in ytsage/__init__.py from 5.0.0b2 to 5.0.0b3 to reflect a new beta release. --- ytsage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ytsage/__init__.py b/ytsage/__init__.py index eba6ea6..ec46976 100644 --- a/ytsage/__init__.py +++ b/ytsage/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "5.0.0b2" +__version__ = "5.0.0b3" __author__ = "oop7" From ed1469898c4cac6ca6b9070811b9407c6fecae49 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:28:06 +0200 Subject: [PATCH 104/134] Add configurable output filename format Introduce a new filename_format setting and UI to control yt-dlp output templates. ConfigManager now includes a default filename_format (%(title)s_%(resolution)s.%(ext)s). The DownloadSettingsDialog exposes a text input and help text for the format and saves the value to ConfigManager. DownloadThread now accepts a filename_format argument and uses it when building output templates for single videos and playlists. YTSageApp reads the config and passes the filename format into the download thread. Added corresponding English language strings. --- ytsage/core/ytsage_downloader.py | 9 ++++-- .../ytsage_dialogs_settings.py | 28 +++++++++++++++++++ ytsage/gui/ytsage_gui_main.py | 4 +++ ytsage/languages/en.json | 2 ++ ytsage/utils/ytsage_config_manager.py | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ytsage/core/ytsage_downloader.py b/ytsage/core/ytsage_downloader.py index a1b1252..0647152 100644 --- a/ytsage/core/ytsage_downloader.py +++ b/ytsage/core/ytsage_downloader.py @@ -72,6 +72,7 @@ class DownloadThread(QThread): preferred_output_format="mp4", force_audio_format=False, preferred_audio_format="best", + filename_format=None, ) -> None: super().__init__() self.url = url @@ -99,6 +100,7 @@ class DownloadThread(QThread): self.preferred_output_format = preferred_output_format self.force_audio_format = force_audio_format self.preferred_audio_format = preferred_audio_format + self.filename_format = filename_format self.paused: bool = False self.cancelled: bool = False self.process: Optional[subprocess.Popen] = None @@ -274,11 +276,14 @@ class DownloadThread(QThread): # Use string concatenation instead of Path.joinpath to avoid Path object issues base_path: str = self.path.as_posix() + # Determine the filename part of the template + filename_part = self.filename_format if self.filename_format else "%(title)s_%(resolution)s.%(ext)s" + if self.is_playlist: # Create output template with playlist subfolder - output_template: str = f"{base_path}/%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s" + output_template: str = f"{base_path}/%(playlist_title)s/{filename_part}" else: - output_template: str = f"{base_path}/%(title)s_%(resolution)s.%(ext)s" + output_template: str = f"{base_path}/{filename_part}" cmd.extend(["-o", str(output_template)]) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index 3b68fd7..535f73a 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -280,6 +280,25 @@ class DownloadSettingsDialog(QDialog): audio_format_group_box.setLayout(audio_format_layout) layout.addWidget(audio_format_group_box) + # --- Filename Format Section --- + filename_format_group_box = QGroupBox(_("settings.filename_format")) + filename_layout = QVBoxLayout() + + # Load current filename format from ConfigManager + self.filename_format_value = ConfigManager.get("filename_format") or "%(title)s_%(resolution)s.%(ext)s" + + self.filename_format_input = QLineEdit(self.filename_format_value) + self.filename_format_input.setPlaceholderText("%(title)s_%(resolution)s.%(ext)s") + filename_layout.addWidget(self.filename_format_input) + + filename_help_label = QLabel(_("settings.filename_format_help")) + filename_help_label.setWordWrap(True) + filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") + filename_layout.addWidget(filename_help_label) + + filename_format_group_box.setLayout(filename_layout) + layout.addWidget(filename_format_group_box) + # Dialog buttons (OK/Cancel) button_box = QDialogButtonBox() ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole) @@ -332,6 +351,10 @@ class DownloadSettingsDialog(QDialog): audio_format_map = {0: "best", 1: "aac", 2: "mp3", 3: "flac", 4: "wav", 5: "opus", 6: "m4a", 7: "vorbis"} return audio_format_map.get(self.audio_format_combo.currentIndex(), "best") + def get_filename_format(self) -> str: + """Returns the filename format string.""" + return self.filename_format_input.text().strip() + def _create_styled_message_box(self, icon, title, text) -> QMessageBox: """Create a styled QMessageBox that matches the app theme.""" msg_box = QMessageBox(self) @@ -382,6 +405,11 @@ class DownloadSettingsDialog(QDialog): ConfigManager.set("force_audio_format", force_audio_format) ConfigManager.set("preferred_audio_format", preferred_audio_format) + # Save filename format + filename_format = self.get_filename_format() + if filename_format: + ConfigManager.set("filename_format", filename_format) + QMessageBox.information( self, _("settings.settings_saved_title"), diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 5b1c7bb..3a55ec9 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -678,6 +678,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.warning(f"Thumbnail download failed: {e}", exc_info=True) # Optionally inform the user, but don't stop the main download + # Get filename format from config + filename_format = ConfigManager.get("filename_format") + # Create download thread with resolution in output template self.download_thread = DownloadThread( url=url, @@ -705,6 +708,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): preferred_output_format=self.preferred_output_format, # Pass preferred format force_audio_format=self.force_audio_format, # Pass force audio format setting preferred_audio_format=self.preferred_audio_format, # Pass preferred audio format + filename_format=filename_format, # Pass the filename format ) # Connect signals diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index 9281bb3..fef8ddc 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -318,6 +318,8 @@ "format_mkv": "MKV (Feature-rich)", "audio_format_settings": "Audio Format Settings", "force_audio_format": "Force audio format for audio-only downloads", + "filename_format": "Output Filename Format", + "filename_format_help": "Available variables: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Standard yt-dlp output template syntax is supported.", "preferred_audio_format": "Preferred audio format:", "force_audio_format_help": "When enabled, audio-only downloads will be converted to your preferred format. This only applies when downloading audio formats.", "audio_format_best": "Best (No conversion)", diff --git a/ytsage/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py index c4b5d3d..919d546 100644 --- a/ytsage/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -90,6 +90,7 @@ class ConfigManager: "preferred_output_format": "mp4", "force_audio_format": False, "preferred_audio_format": "best", + "filename_format": "%(title)s_%(resolution)s.%(ext)s", "cached_versions": { "ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, From 656a77f94382b2d4fdfa17ee6c7dc68b4ce62aac Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:29:42 +0200 Subject: [PATCH 105/134] Document Output Filename Format option Add a README entry describing the new Output Filename Format setting under Download Settings, showing example variables such as %(title)s, %(uploader)s, and %(resolution)s so users can customize downloaded filenames. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bc9e4af..bf58777 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ python -m ytsage.main c. Create a file named `cookies.txt` and paste the cookies into it d. Select the `cookies.txt` file in the app - **Save Download Path:** Save the default download path for future downloads. Available in **Download Settings тЖТ Download Path**. +- **Output Filename Format:** Customize the output filename format using variables like `%(title)s`, `%(uploader)s`, `%(resolution)s`, etc. Available in **Download Settings тЖТ Filename Format**. - **Updater Tab:** Unified tab in Custom Options for managing all updates: - **yt-dlp Updates:** Check and update yt-dlp to the latest version, with release channel selection (Stable/Nightly) - **FFmpeg Version Checker:** Check your FFmpeg version with direct links to installation guides From 1fbc3a43c2ec7c421bca36258d4570bd23c88f0a Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:37:26 +0200 Subject: [PATCH 106/134] Add Reset button for filename format Add a Reset button next to the filename format input so users can restore the default pattern. The filename input and reset button are grouped in a QHBoxLayout; the button is fixed width (70) and sets the field back to the default %(title)s_%(resolution)s.%(ext)s. Also add the corresponding i18n key (buttons.reset -> "Reset") to en.json. --- .../ytsage_gui_dialogs/ytsage_dialogs_settings.py | 12 +++++++++++- ytsage/languages/en.json | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index 535f73a..7798cfc 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -287,9 +287,19 @@ class DownloadSettingsDialog(QDialog): # Load current filename format from ConfigManager self.filename_format_value = ConfigManager.get("filename_format") or "%(title)s_%(resolution)s.%(ext)s" + # Input and Reset Button Layout + filename_input_layout = QHBoxLayout() + self.filename_format_input = QLineEdit(self.filename_format_value) self.filename_format_input.setPlaceholderText("%(title)s_%(resolution)s.%(ext)s") - filename_layout.addWidget(self.filename_format_input) + filename_input_layout.addWidget(self.filename_format_input) + + self.reset_format_button = QPushButton(_("buttons.reset")) + self.reset_format_button.setFixedWidth(70) + self.reset_format_button.clicked.connect(lambda: self.filename_format_input.setText("%(title)s_%(resolution)s.%(ext)s")) + filename_input_layout.addWidget(self.reset_format_button) + + filename_layout.addLayout(filename_input_layout) filename_help_label = QLabel(_("settings.filename_format_help")) filename_help_label.setWordWrap(True) diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index fef8ddc..fe74363 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -82,7 +82,8 @@ "select_defaults": "Select Defaults", "select_all": "Select All", "deselect_all": "Deselect All", - "open_folder": "Open folder location" + "open_folder": "Open folder location", + "reset": "Reset" }, "dialogs": { "custom_options": "Custom Options", From b1d003453784b088dce76b6d8926ec6c1a93883d Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:40:37 +0200 Subject: [PATCH 107/134] Increase help label font size to 11px Bump help text font-size from 10px to 11px in DownloadSettingsDialog for improved readability. Updated style for force format, audio format, and filename format help labels in ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index 7798cfc..ea91901 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -228,7 +228,7 @@ class DownloadSettingsDialog(QDialog): # Help text help_label = QLabel(_("settings.force_format_help")) help_label.setWordWrap(True) - help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") + help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") output_format_layout.addWidget(help_label) output_format_group_box.setLayout(output_format_layout) @@ -274,7 +274,7 @@ class DownloadSettingsDialog(QDialog): # Help text for audio format audio_help_label = QLabel(_("settings.force_audio_format_help")) audio_help_label.setWordWrap(True) - audio_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") + audio_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") audio_format_layout.addWidget(audio_help_label) audio_format_group_box.setLayout(audio_format_layout) @@ -303,7 +303,7 @@ class DownloadSettingsDialog(QDialog): filename_help_label = QLabel(_("settings.filename_format_help")) filename_help_label.setWordWrap(True) - filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") + filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;") filename_layout.addWidget(filename_help_label) filename_format_group_box.setLayout(filename_layout) From 40069c039a747a5c5eca9f4f061ea6957beca2c6 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:48:11 +0200 Subject: [PATCH 108/134] Clarify README UI labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update README text to reflect UI label changes: 'Force Output Format' now points to 'Download Settings тЖТ Output Format Settings', and 'Download History' instructs users to click the 'History' button. Improves accuracy of guidance in the docs. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf58777..1add7e4 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,9 @@ python -m ytsage.main - **FFmpeg/yt-dlp/Deno Detection:** Automatically detect FFmpeg/yt-dlp/Deno path and version. You can use this option by clicking on about button. - **Trim Video:** Download only specific parts of a video by specifying time ranges (HH:MM:SS format) - **Proxy Support:** Use a proxy server for downloads (e.g., `http://:`) -- **Force Output Format:** Force video downloads in a specific container format (e.g., `mp4`, `webm`, `mkv`). Available in **Download Settings тЖТ Audio Format Settings**. +- **Force Output Format:** Force video downloads in a specific container format (e.g., `mp4`, `webm`, `mkv`). Available in **Download Settings тЖТ Output Format Settings**. - **Audio Format Conversion:** Convert audio-only downloads to preferred formats (`AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, or `Best`). Ideal for video editing software like DaVinci Resolve. Available in **Download Settings тЖТ Audio Format Settings**. -- **Download History:** View past downloads with thumbnails and statuses. You can use this option by clicking on download settings button. +- **Download History:** View past downloads with thumbnails and statuses. You can use this option by clicking on the **History** button. From 351504ade93abf1a99b5ea62977d472b44ade23b Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:55:10 +0200 Subject: [PATCH 109/134] Add reset and filename_format translations Add a "reset" button label and new "filename_format" + "filename_format_help" entries to multiple locale JSON files (ar, de, es, fr, hi, id, it, ja, pl, pt, ru, tr, zh). The help text documents supported yt-dlp output template variables (%(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s) so the UI can accept a custom output filename format. --- ytsage/languages/ar.json | 5 ++++- ytsage/languages/de.json | 5 ++++- ytsage/languages/es.json | 5 ++++- ytsage/languages/fr.json | 5 ++++- ytsage/languages/hi.json | 5 ++++- ytsage/languages/id.json | 5 ++++- ytsage/languages/it.json | 5 ++++- ytsage/languages/ja.json | 5 ++++- ytsage/languages/pl.json | 5 ++++- ytsage/languages/pt.json | 5 ++++- ytsage/languages/ru.json | 5 ++++- ytsage/languages/tr.json | 5 ++++- ytsage/languages/zh.json | 5 ++++- 13 files changed, 52 insertions(+), 13 deletions(-) diff --git a/ytsage/languages/ar.json b/ytsage/languages/ar.json index 4160c53..6363bc7 100644 --- a/ytsage/languages/ar.json +++ b/ytsage/languages/ar.json @@ -57,6 +57,7 @@ "audio_only_resolution": "╪╡┘И╪к ┘Б┘В╪╖" }, "buttons": { + "reset": "╪е╪╣╪з╪п╪й ╪к╪╣┘К┘К┘Ж", "download": "╪к┘Ж╪▓┘К┘Д", "pause": "╪е┘К┘В╪з┘Б ┘Е╪д┘В╪к", "resume": "╪з╪│╪к╪ж┘Ж╪з┘Б", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (╪║┘К╪▒ ┘Е╪╢╪║┘И╪╖)", "audio_format_opus": "Opus (┘Б╪╣╪з┘Д)", "audio_format_m4a": "M4A (╪г╪и┘Д)", - "audio_format_vorbis": "Vorbis (┘Е┘Б╪к┘И╪н)" + "audio_format_vorbis": "Vorbis (┘Е┘Б╪к┘И╪н)", + "filename_format": "╪к┘Ж╪│┘К┘В ╪з╪│┘Е ╪з┘Д┘Е┘Д┘Б ╪з┘Д┘Ж╪з╪к╪м", + "filename_format_help": "╪з┘Д┘Е╪к╪║┘К╪▒╪з╪к ╪з┘Д┘Е╪к╪з╪н╪й: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. ┘К╪к┘Е ╪п╪╣┘Е ╪╡┘К╪║╪й ┘В╪з┘Д╪и ╪е╪о╪▒╪з╪м yt-dlp ╪з┘Д┘В┘К╪з╪│┘К╪й." }, "main_ui": { "url_placeholder": "╪г╪п╪о┘Д ╪▒╪з╪и╪╖ ┘Б┘К╪п┘К┘И ┘К┘И╪к┘К┘И╪и ╪г┘И ┘В╪з╪ж┘Е╪й ╪к╪┤╪║┘К┘Д", diff --git a/ytsage/languages/de.json b/ytsage/languages/de.json index eff03ee..e9a5a24 100644 --- a/ytsage/languages/de.json +++ b/ytsage/languages/de.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Nur Audio" }, "buttons": { + "reset": "Zur├╝cksetzen", "download": "Herunterladen", "pause": "Pausieren", "resume": "Fortsetzen", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (Unkomprimiert)", "audio_format_opus": "Opus (Effizient)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Offen)" + "audio_format_vorbis": "Vorbis (Offen)", + "filename_format": "Ausgabe-Dateinamenformat", + "filename_format_help": "Verf├╝gbare Variablen: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Die Standard-yt-dlp-Ausgabevorlagensyntax wird unterst├╝tzt." }, "main_ui": { "url_placeholder": "YouTube-Video- oder Playlist-URL eingeben", diff --git a/ytsage/languages/es.json b/ytsage/languages/es.json index 1e4b5e2..64b2ff6 100644 --- a/ytsage/languages/es.json +++ b/ytsage/languages/es.json @@ -25,6 +25,7 @@ "ready": "Listo" }, "buttons": { + "reset": "Restablecer", "download": "Descargar", "pause": "Pausar", "resume": "Reanudar", @@ -309,7 +310,9 @@ "audio_format_wav": "WAV (Sin comprimir)", "audio_format_opus": "Opus (Eficiente)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Abierto)" + "audio_format_vorbis": "Vorbis (Abierto)", + "filename_format": "Formato de nombre de archivo", + "filename_format_help": "Variables disponibles: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Se admite la sintaxis est├бndar de plantilla de salida de yt-dlp." }, "main_ui": { "url_placeholder": "Ingresa URL de video o lista de YouTube", diff --git a/ytsage/languages/fr.json b/ytsage/languages/fr.json index 8954a3a..b50f688 100644 --- a/ytsage/languages/fr.json +++ b/ytsage/languages/fr.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Audio uniquement" }, "buttons": { + "reset": "R├йinitialiser", "download": "T├йl├йcharger", "pause": "Pause", "resume": "Reprendre", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (Non compress├й)", "audio_format_opus": "Opus (Efficace)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Ouvert)" + "audio_format_vorbis": "Vorbis (Ouvert)", + "filename_format": "Format du nom de fichier", + "filename_format_help": "Variables disponibles : %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. La syntaxe standard des mod├иles de sortie yt-dlp est prise en charge." }, "main_ui": { "url_placeholder": "Entrer l'URL de la vid├йo ou de la playlist YouTube", diff --git a/ytsage/languages/hi.json b/ytsage/languages/hi.json index 18de247..0ccd1ab 100644 --- a/ytsage/languages/hi.json +++ b/ytsage/languages/hi.json @@ -57,6 +57,7 @@ "audio_only_resolution": "рдХреЗрд╡рд▓ рдСрдбрд┐рдпреЛ" }, "buttons": { + "reset": "рд░реАрд╕реЗрдЯ", "download": "рдбрд╛рдЙрдирд▓реЛрдб", "pause": "рд░реЛрдХреЗрдВ", "resume": "рдЬрд╛рд░реА рд░рдЦреЗрдВ", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (рдЕрд╕рдВрдХреБрдЪрд┐рдд)", "audio_format_opus": "Opus (рдХреБрд╢рд▓)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (рдЦреБрд▓рд╛)" + "audio_format_vorbis": "Vorbis (рдЦреБрд▓рд╛)", + "filename_format": "рдЖрдЙрдЯрдкреБрдЯ рдлрд╝рд╛рдЗрд▓рдирд╛рдо рдкреНрд░рд╛рд░реВрдк", + "filename_format_help": "рдЙрдкрд▓рдмреНрдз рдЪрд░: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. рдорд╛рдирдХ yt-dlp рдЖрдЙрдЯрдкреБрдЯ рдЯреЗрдореНрдкреНрд▓реЗрдЯ рд╕рд┐рдВрдЯреИрдХреНрд╕ рд╕рдорд░реНрдерд┐рдд рд╣реИ." }, "main_ui": { "url_placeholder": "YouTube рд╡реАрдбрд┐рдпреЛ рдпрд╛ рдкреНрд▓реЗрд▓рд┐рд╕реНрдЯ URL рджрд░реНрдЬ рдХрд░реЗрдВ", diff --git a/ytsage/languages/id.json b/ytsage/languages/id.json index 8f41c52..f1d5249 100644 --- a/ytsage/languages/id.json +++ b/ytsage/languages/id.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Audio saja" }, "buttons": { + "reset": "Atur Ulang", "download": "Unduh", "pause": "Jeda", "resume": "Lanjutkan", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (Tidak terkompresi)", "audio_format_opus": "Opus (Efisien)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Terbuka)" + "audio_format_vorbis": "Vorbis (Terbuka)", + "filename_format": "Format Nama File Keluaran", + "filename_format_help": "Variabel yang tersedia: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Sintaks templat keluaran standar yt-dlp didukung." }, "main_ui": { "url_placeholder": "Masukkan URL video atau playlist YouTube", diff --git a/ytsage/languages/it.json b/ytsage/languages/it.json index 8b482ec..ab15d67 100644 --- a/ytsage/languages/it.json +++ b/ytsage/languages/it.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Solo audio" }, "buttons": { + "reset": "Reimposta", "download": "Scarica", "pause": "Pausa", "resume": "Riprendi", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (Non compresso)", "audio_format_opus": "Opus (Efficiente)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Aperto)" + "audio_format_vorbis": "Vorbis (Aperto)", + "filename_format": "Formato nome file output", + "filename_format_help": "Variabili disponibili: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. ├И supportata la sintassi standard del modello di output di yt-dlp." }, "main_ui": { "url_placeholder": "Inserisci URL video YouTube o playlist", diff --git a/ytsage/languages/ja.json b/ytsage/languages/ja.json index c8d04e2..79fd54d 100644 --- a/ytsage/languages/ja.json +++ b/ytsage/languages/ja.json @@ -57,6 +57,7 @@ "audio_only_resolution": "щЯ│хг░уБоуБ┐" }, "buttons": { + "reset": "уГкуВ╗уГГуГИ", "download": "уГАуВжуГ│уГнуГ╝уГЙ", "pause": "ф╕АцЩВхБЬцнв", "resume": "хЖНщЦЛ", @@ -326,7 +327,9 @@ "audio_format_wav": "WAVя╝ИщЭЮхЬзч╕оя╝Й", "audio_format_opus": "Opusя╝ИхК╣чОЗчЪДя╝Й", "audio_format_m4a": "M4Aя╝ИAppleя╝Й", - "audio_format_vorbis": "Vorbisя╝ИуВкуГ╝уГЧуГ│я╝Й" + "audio_format_vorbis": "Vorbisя╝ИуВкуГ╝уГЧуГ│я╝Й", + "filename_format": "хЗ║хКЫуГХуВбуВдуГлхРНуБох╜вх╝П", + "filename_format_help": "хИйчФихПпшГ╜уБкхдЙцХ░: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)sуАВциЩц║ЦуБоyt-dlpхЗ║хКЫуГЖуГ│уГЧуГмуГ╝уГИцзЛцЦЗуБМуВ╡уГЭуГ╝уГИуБХуВМуБжуБДуБ╛уБЩуАВ" }, "main_ui": { "url_placeholder": "YouTubeуБохЛХчФ╗URLуБ╛уБЯуБпуГЧуГмуВдуГкуВ╣уГИURLуВТхЕехКЫ", diff --git a/ytsage/languages/pl.json b/ytsage/languages/pl.json index 0a5a500..12623bd 100644 --- a/ytsage/languages/pl.json +++ b/ytsage/languages/pl.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Tylko d┼║wi─Щk" }, "buttons": { + "reset": "Zresetuj", "download": "Pobierz", "pause": "Wstrzymaj", "resume": "Wzn├│w", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (Nieskompresowany)", "audio_format_opus": "Opus (Wydajny)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Otwarty)" + "audio_format_vorbis": "Vorbis (Otwarty)", + "filename_format": "Format nazwy pliku", + "filename_format_help": "Dost─Щpne zmienne: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Obs┼Вugiwana jest standardowa sk┼Вadnia szablonu wyj┼Ыciowego yt-dlp." }, "main_ui": { "url_placeholder": "Wprowad┼║ URL wideo YouTube lub playlisty", diff --git a/ytsage/languages/pt.json b/ytsage/languages/pt.json index 2fde332..e6aedef 100644 --- a/ytsage/languages/pt.json +++ b/ytsage/languages/pt.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Apenas ├бudio" }, "buttons": { + "reset": "Redefinir", "download": "Baixar", "pause": "Pausar", "resume": "Retomar", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (N├гo comprimido)", "audio_format_opus": "Opus (Eficiente)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (Aberto)" + "audio_format_vorbis": "Vorbis (Aberto)", + "filename_format": "Formato de nome de arquivo", + "filename_format_help": "Vari├бveis dispon├нveis: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. A sintaxe padr├гo de modelo de sa├нda do yt-dlp ├й suportada." }, "main_ui": { "url_placeholder": "Digite a URL do v├нdeo ou playlist do YouTube", diff --git a/ytsage/languages/ru.json b/ytsage/languages/ru.json index e88540e..cb8c2e2 100644 --- a/ytsage/languages/ru.json +++ b/ytsage/languages/ru.json @@ -57,6 +57,7 @@ "audio_only_resolution": "╨в╨╛╨╗╤М╨║╨╛ ╨░╤Г╨┤╨╕╨╛" }, "buttons": { + "reset": "╨б╨▒╤А╨╛╤Б", "download": "╨б╨║╨░╤З╨░╤В╤М", "pause": "╨Я╨░╤Г╨╖╨░", "resume": "╨Я╤А╨╛╨┤╨╛╨╗╨╢╨╕╤В╤М", @@ -326,7 +327,9 @@ "audio_format_wav": "WAV (╨Э╨╡╤Б╨╢╨░╤В╤Л╨╣)", "audio_format_opus": "Opus (╨н╤Д╤Д╨╡╨║╤В╨╕╨▓╨╜╤Л╨╣)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (╨Ю╤В╨║╤А╤Л╤В╤Л╨╣)" + "audio_format_vorbis": "Vorbis (╨Ю╤В╨║╤А╤Л╤В╤Л╨╣)", + "filename_format": "╨д╨╛╤А╨╝╨░╤В ╨╕╨╝╨╡╨╜╨╕ ╤Д╨░╨╣╨╗╨░", + "filename_format_help": "╨Ф╨╛╤Б╤В╤Г╨┐╨╜╤Л╨╡ ╨┐╨╡╤А╨╡╨╝╨╡╨╜╨╜╤Л╨╡: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. ╨Я╨╛╨┤╨┤╨╡╤А╨╢╨╕╨▓╨░╨╡╤В╤Б╤П ╤Б╤В╨░╨╜╨┤╨░╤А╤В╨╜╤Л╨╣ ╤Б╨╕╨╜╤В╨░╨║╤Б╨╕╤Б ╤И╨░╨▒╨╗╨╛╨╜╨░ ╨▓╤Л╨▓╨╛╨┤╨░ yt-dlp." }, "main_ui": { "url_placeholder": "╨Т╨▓╨╡╨┤╨╕╤В╨╡ URL ╨▓╨╕╨┤╨╡╨╛ ╨╕╨╗╨╕ ╨┐╨╗╨╡╨╣╨╗╨╕╤Б╤В╨░ YouTube", diff --git a/ytsage/languages/tr.json b/ytsage/languages/tr.json index 2e6701f..e8abda5 100644 --- a/ytsage/languages/tr.json +++ b/ytsage/languages/tr.json @@ -57,6 +57,7 @@ "audio_only_resolution": "Sadece ses" }, "buttons": { + "reset": "S─▒f─▒rla", "download": "─░ndir", "pause": "Duraklat", "resume": "Devam Et", @@ -324,7 +325,9 @@ "audio_format_wav": "WAV (S─▒k─▒┼Яt─▒r─▒lmam─▒┼Я)", "audio_format_opus": "Opus (Verimli)", "audio_format_m4a": "M4A (Apple)", - "audio_format_vorbis": "Vorbis (A├з─▒k)" + "audio_format_vorbis": "Vorbis (A├з─▒k)", + "filename_format": "├З─▒kt─▒ Dosya Ad─▒ Format─▒", + "filename_format_help": "Kullan─▒labilir de─Яi┼Яkenler: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Standart yt-dlp ├з─▒kt─▒ ┼Яablonu s├╢zdizimi desteklenir." }, "main_ui": { "url_placeholder": "YouTube video veya oynatma listesi URL'sini girin", diff --git a/ytsage/languages/zh.json b/ytsage/languages/zh.json index 3d0b3a4..f16979b 100644 --- a/ytsage/languages/zh.json +++ b/ytsage/languages/zh.json @@ -57,6 +57,7 @@ "audio_only_resolution": "ф╗ЕщЯ│щвС" }, "buttons": { + "reset": "щЗНч╜о", "download": "ф╕Лш╜╜", "pause": "цЪВхБЬ", "resume": "цБвхдН", @@ -314,7 +315,9 @@ "audio_format_wav": "WAVя╝ИцЬкхОЛч╝йя╝Й", "audio_format_opus": "Opusя╝ИщлШцХИя╝Й", "audio_format_m4a": "M4Aя╝ИAppleя╝Й", - "audio_format_vorbis": "Vorbisя╝Их╝АцФ╛я╝Й" + "audio_format_vorbis": "Vorbisя╝Их╝АцФ╛я╝Й", + "filename_format": "ш╛УхЗ║цЦЗф╗╢хРНца╝х╝П", + "filename_format_help": "хПпчФихПШщЗП: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)sуАВцФпцМБцаЗхЗЖчЪД yt-dlp ш╛УхЗ║цибцЭ┐шпнц│ХуАВ" }, "main_ui": { "url_placeholder": "ш╛УхЕе YouTube шзЖщвСцИЦцТнцФ╛хИЧшбич╜СхЭА", From c8ecdf8eb64e0cdd41a3398dbc9f40e55d9209d7 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:03:39 +0200 Subject: [PATCH 110/134] Fix PyPI badge link and add platform badge Update README badges: change the PyPI badge URL to the official PyPI project page (pypi.org) and add a 'Supported Platforms' badge indicating cross-platform support (linked to releases). This improves badge accuracy and highlights platform compatibility in the project README. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1add7e4..2f52658 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ ytsage-wordmark YTSage Interface -[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://badge.fury.io/py/ytsage) +[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) [![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) [![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) [![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) +[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-4b5563?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) **A modern YouTube downloader with a clean PySide6 interface.** Download videos in any quality, extract audio, fetch subtitles, and more. From 9f1381d27e4a2d57c9464a23bff6a015c40f1a1a Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:01:37 +0200 Subject: [PATCH 111/134] Update format table directly from analysis Stop storing result_data["all_formats"] on the mixin and instead call update_format_table(result_data["all_formats"]). Replaces the previous filter_formats() call so the format table is populated directly from the fresh analysis result, avoiding reliance on a separate self.all_formats attribute. --- ytsage/gui/ytsage_gui_analysis.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ytsage/gui/ytsage_gui_analysis.py b/ytsage/gui/ytsage_gui_analysis.py index 8abf141..58b6308 100644 --- a/ytsage/gui/ytsage_gui_analysis.py +++ b/ytsage/gui/ytsage_gui_analysis.py @@ -389,7 +389,6 @@ class AnalysisMixin: self.playlist_info = result_data["playlist_info"] self.playlist_entries = result_data["playlist_entries"] self.video_info = result_data["video_info"] - self.all_formats = result_data["all_formats"] self.available_subtitles = result_data["available_subtitles"] self.available_automatic_subtitles = result_data["available_automatic_subtitles"] self.selected_playlist_items = None @@ -413,7 +412,7 @@ class AnalysisMixin: # Update format table self.video_button.setChecked(True) self.audio_button.setChecked(False) - self.filter_formats() + self.update_format_table(result_data["all_formats"]) self.signals.update_status.emit(_("main_ui.analysis_complete")) From 3d5dfd6d4858ec67212bd7958f7329d2121ce48a Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:13:47 +0200 Subject: [PATCH 112/134] Add Inno Setup installers and build step Add Inno Setup compilation to the Windows CI: the workflow now installs Inno Setup via choco, adjusts PATH, and runs iscc to compile two installers (standard and -ffmpeg) using /DMyAppVersion,/DMyAppExeName and /DSourceDir, placing outputs into repo/artifacts and failing the job on non-zero exit. Also add two new Inno Setup scripts in setup-scripts/: Setup-windows.iss and Setup-windows-ffmpeg.iss. The scripts include metadata, multi-language support, file/icon/registry/run entries, and Pascal code to create or update the user's ytsage_config.json language setting; the -ffmpeg script adds an optional task to append the app folder to the user's PATH. --- .github/workflows/build-windows.yml | 33 ++++++ setup-scripts/Setup-windows-ffmpeg.iss | 151 +++++++++++++++++++++++++ setup-scripts/Setup-windows.iss | 129 +++++++++++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 setup-scripts/Setup-windows-ffmpeg.iss create mode 100644 setup-scripts/Setup-windows.iss diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 42a554e..7b205b2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -449,6 +449,39 @@ jobs: Write-Host " Artifacts directory not found!" } + - name: Create Installer (Inno Setup) + shell: powershell + run: | + $version = "${{ steps.get_version.outputs.VERSION }}" + + # Install Inno Setup + choco install innosetup --no-progress + + # Add matches generally C:\Program Files (x86)\Inno Setup 6 + $env:Path = "C:\Program Files (x86)\Inno Setup 6;$env:Path" + + # Compile + $exeName = "YTSage-v$version.exe" + Write-Host "Compiling installer for $exeName..." + + # Note: Setup-windows.iss is in setup-scripts/ + # OutputDir is ..\artifacts (relative to script) -> repo/artifacts + # SourceDir is ..\dist\YTSage (relative to script) -> repo/dist/YTSage + + iscc /DMyAppVersion="$version" /DMyAppExeName="$exeName" /DSourceDir="..\dist\YTSage" "setup-scripts\Setup-windows.iss" + + # Compile FFmpeg Installer + $exeNameFFmpeg = "YTSage-v$version-ffmpeg.exe" + Write-Host "Compiling FFmpeg installer for $exeNameFFmpeg..." + iscc /DMyAppVersion="$version" /DMyAppExeName="$exeNameFFmpeg" /DSourceDir="..\dist\YTSage-FFmpeg" "setup-scripts\Setup-windows-ffmpeg.iss" + + if ($LASTEXITCODE -eq 0) { + Write-Host "Installer compilation successful." + } else { + Write-Host "Installer compilation failed." + exit 1 + } + - name: Create draft release uses: softprops/action-gh-release@v2 with: diff --git a/setup-scripts/Setup-windows-ffmpeg.iss b/setup-scripts/Setup-windows-ffmpeg.iss new file mode 100644 index 0000000..9ad8a10 --- /dev/null +++ b/setup-scripts/Setup-windows-ffmpeg.iss @@ -0,0 +1,151 @@ +; Script generated by the Inno Setup Script Wizard. + +#define MyAppName "YTSage" +#ifndef MyAppVersion + #define MyAppVersion "0.0.0" +#endif +#define MyAppPublisher "oop7" +#define MyAppURL "https://github.com/oop7/YTSage/" +#ifndef MyAppExeName + #define MyAppExeName "YTSage-ffmpeg.exe" +#endif +#ifndef SourceDir + #define SourceDir "..\dist\YTSage-FFmpeg" +#endif + +[Setup] +AppId={{56997322-2A3A-4338-AEF1-C3C8BB28AC4F} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +UninstallDisplayIcon={app}\{#MyAppExeName} +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +DisableProgramGroupPage=yes +LicenseFile=..\LICENSE +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputBaseFilename=YTSage-v{#MyAppVersion}-ffmpeg-Setup +SolidCompression=yes +WizardStyle=modern +OutputDir=..\artifacts + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl" +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" +Name: "french"; MessagesFile: "compiler:Languages\French.isl" +Name: "german"; MessagesFile: "compiler:Languages\German.isl" +Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" +Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" +Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" +Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" +Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; +Name: "addtopath"; Description: "Add ffmpeg to user PATH environment variable"; GroupDescription: "Additional options:"; Flags: unchecked + +[Files] +Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Registry] +Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}') + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; + ParamExpanded: string; +begin + // Expand the setup constants like {app} from Param + ParamExpanded := ExpandConstant(Param); + if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then + begin + Result := True; + exit; + end; + // Look for the path with leading and trailing semicolon + Result := Pos(';' + UpperCase(ParamExpanded) + ';', ';' + UpperCase(OrigPath) + ';') = 0; + if Result = True then + Result := Pos(';' + UpperCase(ParamExpanded) + '\;', ';' + UpperCase(OrigPath) + ';') = 0; +end; + +procedure UpdateLanguageConfig; +var + LanguageCode: string; + ConfigPath: string; + FileContent: AnsiString; + JsonContent: string; + LangPattern: string; + P_Start, P_End: Integer; +begin + if ActiveLanguage = 'english' then LanguageCode := 'en' + else if ActiveLanguage = 'arabic' then LanguageCode := 'ar' + else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br' + else if ActiveLanguage = 'french' then LanguageCode := 'fr' + else if ActiveLanguage = 'german' then LanguageCode := 'de' + else if ActiveLanguage = 'italian' then LanguageCode := 'it' + else if ActiveLanguage = 'japanese' then LanguageCode := 'ja' + else if ActiveLanguage = 'polish' then LanguageCode := 'pl' + else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt' + else if ActiveLanguage = 'russian' then LanguageCode := 'ru' + else if ActiveLanguage = 'spanish' then LanguageCode := 'es' + else if ActiveLanguage = 'turkish' then LanguageCode := 'tr' + else LanguageCode := 'en'; + + ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json'); + + if not DirExists(ExtractFilePath(ConfigPath)) then + ForceDirectories(ExtractFilePath(ConfigPath)); + + if FileExists(ConfigPath) then + begin + if LoadStringFromFile(ConfigPath, FileContent) then + begin + JsonContent := String(FileContent); + LangPattern := '"language": "'; + P_Start := Pos(LangPattern, JsonContent); + if P_Start > 0 then + begin + P_Start := P_Start + Length(LangPattern); + P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent))); + if P_End > 0 then + begin + Delete(JsonContent, P_Start, P_End - 1); + Insert(LanguageCode, JsonContent, P_Start); + SaveStringToFile(ConfigPath, AnsiString(JsonContent), False); + end; + end; + end; + end + else + begin + JsonContent := '{' + #13#10 + + ' "language": "' + LanguageCode + '"' + #13#10 + + '}'; + SaveStringToFile(ConfigPath, AnsiString(JsonContent), False); + end; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + UpdateLanguageConfig(); + end; +end; diff --git a/setup-scripts/Setup-windows.iss b/setup-scripts/Setup-windows.iss new file mode 100644 index 0000000..1f1228c --- /dev/null +++ b/setup-scripts/Setup-windows.iss @@ -0,0 +1,129 @@ +; Script generated by the Inno Setup Script Wizard. + +#define MyAppName "YTSage" +#ifndef MyAppVersion + #define MyAppVersion "0.0.0" +#endif +#define MyAppPublisher "oop7" +#define MyAppURL "https://github.com/oop7/YTSage/" +#ifndef MyAppExeName + #define MyAppExeName "YTSage.exe" +#endif +#ifndef SourceDir + #define SourceDir "..\dist\YTSage" +#endif + +[Setup] +AppId={{AE618DBF-DD56-462D-9C09-2C2B7A41B201} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +UninstallDisplayIcon={app}\{#MyAppExeName} +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +DisableProgramGroupPage=yes +LicenseFile=..\LICENSE +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputBaseFilename=YTSage-v{#MyAppVersion}-Setup +SolidCompression=yes +WizardStyle=modern +OutputDir=..\artifacts + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl" +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" +Name: "french"; MessagesFile: "compiler:Languages\French.isl" +Name: "german"; MessagesFile: "compiler:Languages\German.isl" +Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" +Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" +Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" +Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" +Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; + +[Files] +Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +procedure UpdateLanguageConfig; +var + LanguageCode: string; + ConfigPath: string; + FileContent: AnsiString; + JsonContent: string; + LangPattern: string; + P_Start, P_End: Integer; +begin + if ActiveLanguage = 'english' then LanguageCode := 'en' + else if ActiveLanguage = 'arabic' then LanguageCode := 'ar' + else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br' + else if ActiveLanguage = 'french' then LanguageCode := 'fr' + else if ActiveLanguage = 'german' then LanguageCode := 'de' + else if ActiveLanguage = 'italian' then LanguageCode := 'it' + else if ActiveLanguage = 'japanese' then LanguageCode := 'ja' + else if ActiveLanguage = 'polish' then LanguageCode := 'pl' + else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt' + else if ActiveLanguage = 'russian' then LanguageCode := 'ru' + else if ActiveLanguage = 'spanish' then LanguageCode := 'es' + else if ActiveLanguage = 'turkish' then LanguageCode := 'tr' + else LanguageCode := 'en'; + + ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json'); + + if not DirExists(ExtractFilePath(ConfigPath)) then + ForceDirectories(ExtractFilePath(ConfigPath)); + + if FileExists(ConfigPath) then + begin + if LoadStringFromFile(ConfigPath, FileContent) then + begin + JsonContent := String(FileContent); + LangPattern := '"language": "'; + P_Start := Pos(LangPattern, JsonContent); + if P_Start > 0 then + begin + P_Start := P_Start + Length(LangPattern); + P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent))); + if P_End > 0 then + begin + Delete(JsonContent, P_Start, P_End - 1); + Insert(LanguageCode, JsonContent, P_Start); + SaveStringToFile(ConfigPath, AnsiString(JsonContent), False); + end; + end; + end; + end + else + begin + JsonContent := '{' + #13#10 + + ' "language": "' + LanguageCode + '"' + #13#10 + + '}'; + SaveStringToFile(ConfigPath, AnsiString(JsonContent), False); + end; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + UpdateLanguageConfig(); + end; +end; From d2bc08e93777c02cb4a198fec09c4bd8dcdaa853 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:15:07 +0200 Subject: [PATCH 113/134] Bump package version to 5.0.0b4 Update __version__ in ytsage/__init__.py from 5.0.0b3 to 5.0.0b4 to mark the next beta release. --- ytsage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ytsage/__init__.py b/ytsage/__init__.py index ec46976..ebe747e 100644 --- a/ytsage/__init__.py +++ b/ytsage/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "5.0.0b3" +__version__ = "5.0.0b4" __author__ = "oop7" From d250c1b05f3f32fe83001846b117d16cd1449979 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:41:13 +0200 Subject: [PATCH 114/134] Add opt-in beta update UI and checks Introduce an opt-in beta updates feature: add a styled "Receive Beta Updates" checkbox in the Updater UI with i18n strings, load/save logic in the custom options dialog, and a default config key (check_beta_updates=false). Add _fetch_github_beta_version to the update thread to query GitHub releases (selecting the highest tag) and emit update_available for newer beta builds. When beta checking is enabled, the thread will perform the GitHub beta check and return early to avoid conflicting with the PyPI check. This enables users to opt into preview releases safely. --- .../ytsage_dialogs_custom.py | 6 +++ .../ytsage_dialogs_updater.py | 42 +++++++++++++++ ytsage/gui/ytsage_gui_main.py | 54 +++++++++++++++++++ ytsage/languages/en.json | 2 + ytsage/utils/ytsage_config_manager.py | 1 + 5 files changed, 105 insertions(+) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index 5cd798c..e8f3d28 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -966,6 +966,12 @@ class CustomOptionsDialog(QDialog): logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}") result = update_auto_update_settings(enabled, frequency) logger.info(f"Auto-update settings save result: {result}") + + # Save beta update setting + beta_enabled = self.updater_tab.get_beta_update_setting() + ConfigManager.set("check_beta_updates", beta_enabled) + logger.info(f"Saved beta updates setting: {beta_enabled}") + except Exception as e: logger.exception(f"Error saving auto-update settings: {e}") diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index 39a29c7..eca6651 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -441,6 +441,40 @@ class UpdaterTabWidget(QWidget): deno_layout.addLayout(deno_button_layout) layout.addWidget(deno_group) + + # === App Updates Section === + app_update_group = QGroupBox(_("settings.app_updates_title")) + app_update_layout = QVBoxLayout() + + self.beta_updates_checkbox = QCheckBox(_("settings.check_beta_updates")) + self.beta_updates_checkbox.setStyleSheet( + """ + QCheckBox { + color: #ffffff; + spacing: 5px; + padding: 3px; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + border-radius: 9px; + } + QCheckBox::indicator:unchecked { + border: 2px solid #666666; + background: #1d1e22; + border-radius: 9px; + } + QCheckBox::indicator:checked { + border: 2px solid #c90000; + background: #c90000; + border-radius: 9px; + } + """ + ) + app_update_layout.addWidget(self.beta_updates_checkbox) + + app_update_group.setLayout(app_update_layout) + layout.addWidget(app_update_group) # === yt-dlp Release Channel Section === ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel")) @@ -618,6 +652,10 @@ class UpdaterTabWidget(QWidget): # Set checkbox self.auto_update_enabled.setChecked(auto_settings["enabled"]) + # Load beta setting + beta_enabled = ConfigManager.get("check_beta_updates") or False + self.beta_updates_checkbox.setChecked(beta_enabled) + # Set current selection based on saved settings current_frequency = auto_settings["frequency"] if current_frequency == "startup": @@ -655,6 +693,10 @@ class UpdaterTabWidget(QWidget): frequency = "weekly" return enabled, frequency + + def get_beta_update_setting(self) -> bool: + """Returns the beta update setting from the dialog.""" + return self.beta_updates_checkbox.isChecked() def _on_channel_changed(self, checked: bool) -> None: """Handle channel selection change.""" diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 3a55ec9..250338c 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -116,9 +116,63 @@ class UpdateCheckThread(QThread): # Silently fallback if GitHub API fails (rate limiting, network issues, etc.) return fallback + def _fetch_github_beta_version(self) -> tuple[str | None, str | None, str | None]: + """Fetch latest version code from GitHub releases (including betas). Returns (version, tag, changelog).""" + try: + response = requests.get( + "https://api.github.com/repos/oop7/YTSage/releases", + headers={"Accept": "application/vnd.github.v3+json"}, + timeout=self.GITHUB_TIMEOUT, + ) + if response.status_code != 200: + logger.debug(f"GitHub Releases API returned {response.status_code}") + return None, None, None + + releases = response.json() + if not releases: + return None, None, None + + latest_release = None + highest_ver = version.parse("0.0.0") + + for rel in releases: + tag = rel.get("tag_name", "") + ver_str = tag.lstrip("v") + try: + v = version.parse(ver_str) + if v > highest_ver: + highest_ver = v + latest_release = rel + except Exception: + continue + + if latest_release: + return str(highest_ver), latest_release.get("tag_name"), latest_release.get("body") + return None, None, None + + except Exception as e: + logger.debug(f"GitHub beta check error: {e}") + return None, None, None + def run(self): """Check for updates using parallel network requests for better performance.""" try: + # Check for beta updates if enabled + check_beta = ConfigManager.get("check_beta_updates") + + if check_beta: + latest_ver_str, tag, changelog = self._fetch_github_beta_version() + + if latest_ver_str and version.parse(latest_ver_str) > version.parse(self.current_version): + release_url = f"https://github.com/oop7/YTSage/releases/tag/{tag}" + if not changelog: + changelog = "View the full changelog on GitHub." + self.update_available.emit(latest_ver_str, release_url, changelog) + # Return if beta check completes (whether update found or not), + # effectively skipping PyPI check if beta is enabled. + # This ensures we don't downgrade or conflict. + return + # Use ThreadPoolExecutor to make both requests in parallel # This reduces total wait time from potentially 15s to ~8s max with ThreadPoolExecutor(max_workers=2) as executor: diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index fe74363..f9c59a0 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -298,6 +298,8 @@ "ytdlp_channel_switched": "тЬЕ Successfully switched to {channel} channel!", "ytdlp_channel_switch_failed": "тЭМ Failed to switch channel: {error}", "ytdlp_current_channel": "Current channel: {channel}", + "app_updates_title": "YTSage Updates", + "check_beta_updates": "Receive Beta Updates", "auto_update_title": "Auto-Update Settings", "auto_update_header": "ЁЯФД Auto-Update Settings", "auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.", diff --git a/ytsage/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py index 919d546..4c58d58 100644 --- a/ytsage/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -83,6 +83,7 @@ class ConfigManager: "geo_proxy_url": None, "auto_update_ytdlp": True, "auto_update_frequency": "daily", + "check_beta_updates": False, "last_update_check": 0, "language": "en", "ytdlp_channel": "stable", From 04e043c68972298a087658383a7171ec8471cbe8 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:43:24 +0200 Subject: [PATCH 115/134] Add app update strings to translations Add 'app_updates_title' and 'check_beta_updates' translation keys to language files (ar, de, es, fr, hi, id, it, ja, pl, pt, ru, tr, zh). These localized strings support the app updates settings UI (including an option to receive beta updates) and keep translations consistent with the existing auto-update-related entries. --- ytsage/languages/ar.json | 2 ++ ytsage/languages/de.json | 2 ++ ytsage/languages/es.json | 2 ++ ytsage/languages/fr.json | 2 ++ ytsage/languages/hi.json | 2 ++ ytsage/languages/id.json | 2 ++ ytsage/languages/it.json | 2 ++ ytsage/languages/ja.json | 2 ++ ytsage/languages/pl.json | 2 ++ ytsage/languages/pt.json | 2 ++ ytsage/languages/ru.json | 2 ++ ytsage/languages/tr.json | 2 ++ ytsage/languages/zh.json | 2 ++ 13 files changed, 26 insertions(+) diff --git a/ytsage/languages/ar.json b/ytsage/languages/ar.json index 6363bc7..42a6135 100644 --- a/ytsage/languages/ar.json +++ b/ytsage/languages/ar.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ ╪к┘Е ╪з┘Д╪к╪и╪п┘К┘Д ╪и┘Ж╪м╪з╪н ╪е┘Д┘Й ┘В┘Ж╪з╪й {channel}!", "ytdlp_channel_switch_failed": "тЭМ ┘Б╪┤┘Д ╪к╪и╪п┘К┘Д ╪з┘Д┘В┘Ж╪з╪й: {error}", "ytdlp_current_channel": "╪з┘Д┘В┘Ж╪з╪й ╪з┘Д╪н╪з┘Д┘К╪й: {channel}", + "app_updates_title": "╪к╪н╪п┘К╪л╪з╪к YTSage", + "check_beta_updates": "╪к┘Д┘В┘К ╪к╪н╪п┘К╪л╪з╪к ╪к╪м╪▒┘К╪и┘К╪й (Beta)", "auto_update_title": "╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д╪к╪н╪п┘К╪л╪з╪к ╪з┘Д╪к┘Д┘В╪з╪ж┘К╪й", "auto_update_header": "ЁЯФД ╪е╪╣╪п╪з╪п╪з╪к ╪з┘Д╪к╪н╪п┘К╪л╪з╪к ╪з┘Д╪к┘Д┘В╪з╪ж┘К╪й", "auto_update_description": "┘В┘Е ╪и╪к┘Г┘И┘К┘Ж ╪з┘Д╪к╪н╪п┘К╪л╪з╪к ╪з┘Д╪к┘Д┘В╪з╪ж┘К╪й ┘Д┘А yt-dlp ┘Д╪╢┘Е╪з┘Ж ╪з┘Д╪н╪╡┘И┘Д ╪╣┘Д┘Й ╪г╪н╪п╪л ╪з┘Д┘Е┘К╪▓╪з╪к ┘И╪е╪╡┘Д╪з╪н╪з╪к ╪з┘Д╪г╪о╪╖╪з╪б.", diff --git a/ytsage/languages/de.json b/ytsage/languages/de.json index e9a5a24..41b6817 100644 --- a/ytsage/languages/de.json +++ b/ytsage/languages/de.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Erfolgreich zu {channel}-Kanal gewechselt!", "ytdlp_channel_switch_failed": "тЭМ Kanalwechsel fehlgeschlagen: {error}", "ytdlp_current_channel": "Aktueller Kanal: {channel}", + "app_updates_title": "YTSage-Updates", + "check_beta_updates": "Beta-Updates erhalten", "auto_update_title": "Auto-Update-Einstellungen", "auto_update_header": "ЁЯФД Auto-Update-Einstellungen", "auto_update_description": "Konfigurieren Sie automatische Updates f├╝r yt-dlp, um sicherzustellen, dass Sie immer die neuesten Funktionen und Fehlerbehebungen haben.", diff --git a/ytsage/languages/es.json b/ytsage/languages/es.json index 64b2ff6..cf2436d 100644 --- a/ytsage/languages/es.json +++ b/ytsage/languages/es.json @@ -280,6 +280,8 @@ "ytdlp_channel_switched": "тЬЕ ┬бCambiado exitosamente al canal {channel}!", "ytdlp_channel_switch_failed": "тЭМ Error al cambiar de canal: {error}", "ytdlp_current_channel": "Canal actual: {channel}", + "app_updates_title": "Actualizaciones de YTSage", + "check_beta_updates": "Recibir actualizaciones beta", "auto_update_title": "Configuraci├│n de Auto-Actualizaci├│n", "auto_update_header": "ЁЯФД Configuraci├│n de Auto-Actualizaci├│n", "auto_update_description": "Configura las actualizaciones autom├бticas de yt-dlp para asegurar que siempre tengas las ├║ltimas funciones y correcciones de errores.", diff --git a/ytsage/languages/fr.json b/ytsage/languages/fr.json index b50f688..6d38e3c 100644 --- a/ytsage/languages/fr.json +++ b/ytsage/languages/fr.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Passage r├йussi au canal {channel} !", "ytdlp_channel_switch_failed": "тЭМ ├Йchec du changement de canal : {error}", "ytdlp_current_channel": "Canal actuel : {channel}", + "app_updates_title": "Mises ├а jour YTSage", + "check_beta_updates": "Recevoir les mises ├а jour b├кta", "auto_update_title": "Param├иtres de mise ├а jour automatique", "auto_update_header": "ЁЯФД Param├иtres de mise ├а jour automatique", "auto_update_description": "Configurez les mises ├а jour automatiques pour yt-dlp afin de vous assurer d'avoir toujours les derni├иres fonctionnalit├йs et corrections de bogues.", diff --git a/ytsage/languages/hi.json b/ytsage/languages/hi.json index 0ccd1ab..a202285 100644 --- a/ytsage/languages/hi.json +++ b/ytsage/languages/hi.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ {channel} рдЪреИрдирд▓ рдкрд░ рд╕реНрд╡рд┐рдЪ рдХрд┐рдпрд╛ рдЧрдпрд╛!", "ytdlp_channel_switch_failed": "тЭМ рдЪреИрдирд▓ рд╕реНрд╡рд┐рдЪ рдХрд░рдирд╛ рд╡рд┐рдлрд▓: {error}", "ytdlp_current_channel": "рд╡рд░реНрддрдорд╛рди рдЪреИрдирд▓: {channel}", + "app_updates_title": "YTSage рдЕрдкрдбреЗрдЯ", + "check_beta_updates": "рдмреАрдЯрд╛ рдЕрдкрдбреЗрдЯ рдкреНрд░рд╛рдкреНрдд рдХрд░реЗрдВ", "auto_update_title": "рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдЕрдкрдбреЗрдЯ рд╕реЗрдЯрд┐рдВрдЧреНрд╕", "auto_update_header": "ЁЯФД рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдЕрдкрдбреЗрдЯ рд╕реЗрдЯрд┐рдВрдЧреНрд╕", "auto_update_description": "yt-dlp рдХреЗ рд▓рд┐рдП рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдЕрдкрдбреЗрдЯ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВ рддрд╛рдХрд┐ рдЖрдкрдХреЗ рдкрд╛рд╕ рд╣рдореЗрд╢рд╛ рдирд╡реАрдирддрдо рд╕реБрд╡рд┐рдзрд╛рдПрдВ рдФрд░ рдмрдЧ рдлрд┐рдХреНрд╕ рд╣реЛрдВред", diff --git a/ytsage/languages/id.json b/ytsage/languages/id.json index f1d5249..ce23cc2 100644 --- a/ytsage/languages/id.json +++ b/ytsage/languages/id.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Berhasil beralih ke saluran {channel}!", "ytdlp_channel_switch_failed": "тЭМ Gagal beralih saluran: {error}", "ytdlp_current_channel": "Saluran saat ini: {channel}", + "app_updates_title": "Pembaruan YTSage", + "check_beta_updates": "Terima Pembaruan Beta", "auto_update_title": "Pengaturan pembaruan otomatis", "auto_update_header": "ЁЯФД Pengaturan pembaruan otomatis", "auto_update_description": "Konfigurasi pembaruan otomatis untuk yt-dlp untuk memastikan Anda selalu memiliki fitur dan perbaikan bug terbaru.", diff --git a/ytsage/languages/it.json b/ytsage/languages/it.json index ab15d67..5343b0d 100644 --- a/ytsage/languages/it.json +++ b/ytsage/languages/it.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Passaggio al canale {channel} riuscito!", "ytdlp_channel_switch_failed": "тЭМ Impossibile cambiare canale: {error}", "ytdlp_current_channel": "Canale attuale: {channel}", + "app_updates_title": "Aggiornamenti YTSage", + "check_beta_updates": "Ricevi aggiornamenti beta", "auto_update_title": "Impostazioni aggiornamenti automatici", "auto_update_header": "ЁЯФД Impostazioni aggiornamenti automatici", "auto_update_description": "Configura gli aggiornamenti automatici per yt-dlp per garantire le ultime funzionalit├а e correzioni bug.", diff --git a/ytsage/languages/ja.json b/ytsage/languages/ja.json index 79fd54d..0dfe15f 100644 --- a/ytsage/languages/ja.json +++ b/ytsage/languages/ja.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ {channel}уГБуГгуГ│уГНуГлуБ╕уБохИЗуВКцЫ┐уБИуБлцИРхКЯуБЧуБ╛уБЧуБЯя╝Б", "ytdlp_channel_switch_failed": "тЭМ уГБуГгуГ│уГНуГлуБохИЗуВКцЫ┐уБИуБлхд▒цХЧуБЧуБ╛уБЧуБЯ: {error}", "ytdlp_current_channel": "чП╛хЬиуБоуГБуГгуГ│уГНуГл: {channel}", + "app_updates_title": "YTSageуБоцЫ┤цЦ░", + "check_beta_updates": "уГЩуГ╝уВ┐чЙИуБоцЫ┤цЦ░уВТхПЧуБСхПЦуВЛ", "auto_update_title": "шЗкхЛХцЫ┤цЦ░шинхоЪ", "auto_update_header": "ЁЯФД шЗкхЛХцЫ┤цЦ░шинхоЪ", "auto_update_description": "yt-dlpуБошЗкхЛХцЫ┤цЦ░уВТшинхоЪуБЧуБжуАБцЬАцЦ░уБоцйЯшГ╜уБиуГРуВ░ф┐оцнгуВТчв║хоЯуБлхЕецЙЛуБЧуБжуБПуБауБХуБДуАВ", diff --git a/ytsage/languages/pl.json b/ytsage/languages/pl.json index 12623bd..4388c01 100644 --- a/ytsage/languages/pl.json +++ b/ytsage/languages/pl.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Pomy┼Ыlnie prze┼В─Еczono na kana┼В {channel}!", "ytdlp_channel_switch_failed": "тЭМ Nie uda┼Вo si─Щ prze┼В─Еczy─З kana┼Вu: {error}", "ytdlp_current_channel": "Aktualny kana┼В: {channel}", + "app_updates_title": "Aktualizacje YTSage", + "check_beta_updates": "Otrzymuj aktualizacje beta", "auto_update_title": "Ustawienia automatycznych aktualizacji", "auto_update_header": "ЁЯФД Ustawienia automatycznych aktualizacji", "auto_update_description": "Skonfiguruj automatyczne aktualizacje dla yt-dlp, aby zapewni─З najnowsze funkcje i poprawki b┼В─Щd├│w.", diff --git a/ytsage/languages/pt.json b/ytsage/languages/pt.json index e6aedef..eaf25cc 100644 --- a/ytsage/languages/pt.json +++ b/ytsage/languages/pt.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ Mudado com sucesso para o canal {channel}!", "ytdlp_channel_switch_failed": "тЭМ Falha ao trocar de canal: {error}", "ytdlp_current_channel": "Canal atual: {channel}", + "app_updates_title": "Atualiza├з├╡es do YTSage", + "check_beta_updates": "Receber atualiza├з├╡es beta", "auto_update_title": "Configura├з├╡es de Auto-Atualiza├з├гo", "auto_update_header": "ЁЯФД Configura├з├╡es de Auto-Atualiza├з├гo", "auto_update_description": "Configure atualiza├з├╡es autom├бticas para o yt-dlp para garantir que voc├к sempre tenha os recursos mais recentes e corre├з├╡es de bugs.", diff --git a/ytsage/languages/ru.json b/ytsage/languages/ru.json index cb8c2e2..9da7f1e 100644 --- a/ytsage/languages/ru.json +++ b/ytsage/languages/ru.json @@ -297,6 +297,8 @@ "ytdlp_channel_switched": "тЬЕ ╨г╤Б╨┐╨╡╤И╨╜╨╛ ╨┐╨╡╤А╨╡╨║╨╗╤О╤З╨╡╨╜╨╛ ╨╜╨░ ╨║╨░╨╜╨░╨╗ {channel}!", "ytdlp_channel_switch_failed": "тЭМ ╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨┐╨╡╤А╨╡╨║╨╗╤О╤З╨╕╤В╤М ╨║╨░╨╜╨░╨╗: {error}", "ytdlp_current_channel": "╨в╨╡╨║╤Г╤Й╨╕╨╣ ╨║╨░╨╜╨░╨╗: {channel}", + "app_updates_title": "╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П YTSage", + "check_beta_updates": "╨Я╨╛╨╗╤Г╤З╨░╤В╤М ╨▒╨╡╤В╨░-╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П", "auto_update_title": "╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨╕ ╨░╨▓╤В╨╛╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П", "auto_update_header": "ЁЯФД ╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨╕ ╨░╨▓╤В╨╛╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П", "auto_update_description": "╨Э╨░╤Б╤В╤А╨╛╨╣╤В╨╡ ╨░╨▓╤В╨╛╨╝╨░╤В╨╕╤З╨╡╤Б╨║╨╕╨╡ ╨╛╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╤П ╨┤╨╗╤П yt-dlp, ╤З╤В╨╛╨▒╤Л ╨▓╤Б╨╡╨│╨┤╨░ ╨╕╨╝╨╡╤В╤М ╨┐╨╛╤Б╨╗╨╡╨┤╨╜╨╕╨╡ ╤Д╤Г╨╜╨║╤Ж╨╕╨╕ ╨╕ ╨╕╤Б╨┐╤А╨░╨▓╨╗╨╡╨╜╨╕╤П ╨╛╤И╨╕╨▒╨╛╨║.", diff --git a/ytsage/languages/tr.json b/ytsage/languages/tr.json index e8abda5..aa38369 100644 --- a/ytsage/languages/tr.json +++ b/ytsage/languages/tr.json @@ -295,6 +295,8 @@ "ytdlp_channel_switched": "тЬЕ Ba┼Яar─▒yla {channel} kanal─▒na ge├зildi!", "ytdlp_channel_switch_failed": "тЭМ Kanal de─Яi┼Яtirilemedi: {error}", "ytdlp_current_channel": "Mevcut kanal: {channel}", + "app_updates_title": "YTSage G├╝ncellemeleri", + "check_beta_updates": "Beta G├╝ncellemelerini Al", "auto_update_title": "Otomatik g├╝ncelleme ayarlar─▒", "auto_update_header": "ЁЯФД Otomatik g├╝ncelleme ayarlar─▒", "auto_update_description": "En son ├╢zelliklere ve hata d├╝zeltmelerine sahip olmak i├зin yt-dlp otomatik g├╝ncellemelerini yap─▒land─▒r─▒n.", diff --git a/ytsage/languages/zh.json b/ytsage/languages/zh.json index f16979b..3f4128f 100644 --- a/ytsage/languages/zh.json +++ b/ytsage/languages/zh.json @@ -285,6 +285,8 @@ "ytdlp_channel_switched": "тЬЕ цИРхКЯхИЗцНвхИ░ {channel} ц╕ащБУя╝Б", "ytdlp_channel_switch_failed": "тЭМ хИЗцНвц╕ащБУхд▒ш┤ея╝Ъ{error}", "ytdlp_current_channel": "х╜УхЙНц╕ащБУя╝Ъ{channel}", + "app_updates_title": "YTSage цЫ┤цЦ░", + "check_beta_updates": "цОецФ╢ц╡ЛшпХчЙИцЫ┤цЦ░", "auto_update_title": "шЗкхКицЫ┤цЦ░шо╛ч╜о", "auto_update_header": "ЁЯФД шЗкхКицЫ┤цЦ░шо╛ч╜о", "auto_update_description": "ф╕║ yt-dlp щЕНч╜ошЗкхКицЫ┤цЦ░я╝Мф╗ечбоф┐ЭцВихзЛч╗ИцЛецЬЙцЬАцЦ░хКЯшГ╜хТМщФЩшппф┐охдНуАВ", From 96cade32267268822f374eac948959fd641adc81 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:39:18 +0200 Subject: [PATCH 116/134] Add PyPI build workflow and docs Introduce a new GitHub Actions workflow (build-pypi.yml) to build Python Wheel and Source Distribution and upload them as release assets. Integrate the PyPI job into the release-all workflow and update .github/CI_CD_README.md to document the PyPI artifacts and the new build-pypi.yml entry. The workflow uses Python 3.13, installs build tooling, runs build_release.py, and uploads dist/* via softprops/action-gh-release. --- .github/CI_CD_README.md | 10 +++++-- .github/workflows/build-pypi.yml | 49 +++++++++++++++++++++++++++++++ .github/workflows/release-all.yml | 6 ++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-pypi.yml diff --git a/.github/CI_CD_README.md b/.github/CI_CD_README.md index 12ac0fd..e68969d 100644 --- a/.github/CI_CD_README.md +++ b/.github/CI_CD_README.md @@ -8,8 +8,8 @@ This repository uses GitHub Actions to automatically build and release YTSage fo The workflows are triggered manually via the GitHub Actions "Workflow Dispatch" interface. This allows you to specify the version number explicitly (e.g., `1.0.0`) at runtime. ### Workflows -- **Create All Releases** (`release-all.yml`): The master workflow. Triggering this will automatically run the Windows, Linux, and macOS builds in parallel with the version you provide. -- **Platform Specific**: You can also trigger `Build Windows Release`, `Build Linux Release`, or `Build macOS Release` individually if you only need updates for one OS. +- **Create All Releases** (`release-all.yml`): The master workflow. Triggering this will automatically run the Windows, Linux, macOS, and PyPI builds in parallel with the version you provide. +- **Platform Specific**: You can also trigger `Build Windows Release`, `Build Linux Release`, `Build macOS Release`, or `Build PyPI Package` individually. ### Build Process 1. **Setup**: Uses Python 3.13 on all platforms @@ -54,8 +54,13 @@ The workflow creates the following files based on the platform: - `YTSage-v{version}-arm64.app.zip` - Zipped application bundle - `YTSage-v{version}-arm64.dmg` - Disk image installer +#### PyPI +- `ytsage-{version}-py3-none-any.whl` - Python Wheel +- `ytsage-{version}.tar.gz` - Source Distribution + ## Workflow Features +- **PyPI**: Standard Python build system (Wheel & Source) ### Multi-Platform Support - **Windows**: Uses PowerShell scripts with cx_Freeze - **Linux**: Uses Bash scripts with cx_Freeze, creates AppImage, RPM, and DEB @@ -91,6 +96,7 @@ The workflow files are located in `.github/workflows/`: - `release-all.yml` - Master workflow that orchestrates the others - `build-windows.yml` - Windows builds logic - `build-linux.yml` - Linux builds logic +- `build-pypi.yml` - PyPI build logic - `build-macos.yml` - macOS builds logic ### Key Configuration Options diff --git a/.github/workflows/build-pypi.yml b/.github/workflows/build-pypi.yml new file mode 100644 index 0000000..274fb34 --- /dev/null +++ b/.github/workflows/build-pypi.yml @@ -0,0 +1,49 @@ +name: Build PyPI Package + +on: + workflow_dispatch: + inputs: + version: + description: 'Version name for the release (e.g., 1.0.0)' + required: true + type: string + workflow_call: + inputs: + version: + description: 'Version name for the release (e.g., 1.0.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + build-pypi: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build PyPI package + run: | + python build_release.py + + - name: Upload Release Assets + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ inputs.version || github.event.inputs.version }} + files: | + dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml index c46dd44..306bbda 100644 --- a/.github/workflows/release-all.yml +++ b/.github/workflows/release-all.yml @@ -29,3 +29,9 @@ jobs: with: version: ${{ inputs.version }} secrets: inherit + + release-pypi: + uses: ./.github/workflows/build-pypi.yml + with: + version: ${{ inputs.version }} + secrets: inherit From f33122fecdd5e8a0e58ba5ca289734ad58c74865 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:50:03 +0200 Subject: [PATCH 117/134] Add Open Logs button and simplify bug template Add an 'Open Logs' button to the About dialog that opens the app log folder via QDesktopServices/QUrl, ensures the log directory exists, and logs warnings/errors. Include minimal styling and a handler (open_logs_folder) that uses APP_LOG_DIR and the logger. Also update settings dialog imports to include QUrl/QDesktopServices and APP_LOG_DIR. Simplify the bug report template to instruct users to open logs from the About dialog and attach ytsage.log / ytsage_error.log. --- .github/ISSUE_TEMPLATE/ЁЯРЫ-bug-report.md | 12 ++--- .../ytsage_gui_dialogs/ytsage_dialogs_base.py | 47 ++++++++++++++++++- .../ytsage_dialogs_settings.py | 4 +- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ЁЯРЫ-bug-report.md b/.github/ISSUE_TEMPLATE/ЁЯРЫ-bug-report.md index 2710703..80d2c7a 100644 --- a/.github/ISSUE_TEMPLATE/ЁЯРЫ-bug-report.md +++ b/.github/ISSUE_TEMPLATE/ЁЯРЫ-bug-report.md @@ -45,14 +45,10 @@ assignees: '' Attach screenshots (for GUI issues) or terminal logs (for CLI errors). To collect log files: - 1. Go to the logs folder: - - Windows: %LOCALAPPDATA%\YTSage\logs - - macOS: ~/Library/Application Support/YTSage/logs - - Linux: ~/.local/share/YTSage/logs - 2. Delete all files in that folder - 3. Open the app and reproduce the issue - 4. Go back to the logs folder - you should find two new log files (ytsage.log, ytsage_errors.log) - 5. Attach those log files to this issue + 1. Open YTSage and reproduce the issue. + 2. Click the **About** button. + 3. Click **Logs** (ЁЯУВ) to open the logs folder. + 4. Attach `ytsage.log` and `ytsage_error.log`. Use ``` to format logs: --> diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index 6ed900c..48c8b76 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -5,7 +5,8 @@ Contains basic utility dialogs like LogWindow and AboutDialog. from datetime import datetime -from PySide6.QtCore import Qt, QThread, QTimer, Signal +from PySide6.QtCore import Qt, QThread, QTimer, Signal, QUrl +from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, @@ -21,6 +22,8 @@ from PySide6.QtWidgets import ( from ... import __version__ as APP_VERSION from ...utils.ytsage_localization import _ +from ...utils.ytsage_logger import logger +from ...utils.ytsage_constants import APP_LOG_DIR from ...core.ytsage_ffmpeg import get_ffmpeg_path from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache @@ -288,6 +291,34 @@ class AboutDialog(QDialog): # Add stretch to push refresh button to the right header_layout.addStretch() + # Create logs button (minimal) + self.logs_btn = QPushButton(_("about.open_logs")) # Expected to be small text or icon + self.logs_btn.setToolTip(_("about.logs_tooltip")) + self.logs_btn.setCursor(Qt.CursorShape.PointingHandCursor) + self.logs_btn.setStyleSheet( + """ + QPushButton { + padding: 1px 6px; + background-color: transparent; + border: 1px solid #333; + border-radius: 4px; + color: #888888; + font-size: 10px; + margin-right: 8px; + } + QPushButton:hover { + color: #ffffff; + border-color: #555; + background-color: rgba(255, 255, 255, 0.05); + } + QPushButton:pressed { + background-color: rgba(255, 255, 255, 0.1); + } + """ + ) + self.logs_btn.clicked.connect(self.open_logs_folder) + header_layout.addWidget(self.logs_btn) + # Create refresh button self.refresh_btn = QPushButton(_("about.refresh")) self.refresh_btn.setFixedSize(16, 16) @@ -532,6 +563,20 @@ class AboutDialog(QDialog): self.status_container.addWidget(deno_item) + def open_logs_folder(self): + """Open the application logs folder in the system file explorer.""" + try: + if not APP_LOG_DIR.exists(): + logger.warning(f"Log directory does not exist: {APP_LOG_DIR}") + APP_LOG_DIR.mkdir(parents=True, exist_ok=True) + + log_url = QUrl.fromLocalFile(str(APP_LOG_DIR)) + QDesktopServices.openUrl(log_url) + except Exception as e: + logger.error(f"Failed to open log folder: {e}") + # Minimal error feedback since this is about dialog + self.logs_btn.setToolTip(f"Error: {str(e)}") + def refresh_version_info(self) -> None: """Refresh version information manually.""" self.refresh_btn.setText(_('about.refreshing')) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index ea91901..0f69956 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -9,7 +9,8 @@ from datetime import datetime import requests from packaging import version as version_parser -from PySide6.QtCore import Qt, QTimer +from PySide6.QtCore import Qt, QTimer, QUrl +from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import ( QButtonGroup, QCheckBox, @@ -30,6 +31,7 @@ from PySide6.QtWidgets import ( from ...utils.ytsage_logger import logger from ...utils.ytsage_localization import _ from ...utils.ytsage_config_manager import ConfigManager +from ...utils.ytsage_constants import APP_LOG_DIR class DownloadSettingsDialog(QDialog): From 79f2613cf782956c10a8f467ac0ea717a11634ea Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:50:22 +0200 Subject: [PATCH 118/134] Add open_logs strings to localizations Add open_logs and logs_tooltip localization entries across multiple language JSON files (ar, de, en, es, fr, hi, id, it, ja, pl, pt, ru, tr, zh) so the About/System Info UI can expose an action to open the application logs folder. Also add an "about" section to ytsage/utils/ytsage_localization.py with default entries (open_logs, logs_tooltip, refresh). This wires the logs-folder UI text into the localization system. --- ytsage/languages/ar.json | 2 ++ ytsage/languages/de.json | 2 ++ ytsage/languages/en.json | 2 ++ ytsage/languages/es.json | 2 ++ ytsage/languages/fr.json | 2 ++ ytsage/languages/hi.json | 2 ++ ytsage/languages/id.json | 2 ++ ytsage/languages/it.json | 2 ++ ytsage/languages/ja.json | 2 ++ ytsage/languages/pl.json | 2 ++ ytsage/languages/pt.json | 2 ++ ytsage/languages/ru.json | 2 ++ ytsage/languages/tr.json | 2 ++ ytsage/languages/zh.json | 2 ++ ytsage/utils/ytsage_localization.py | 5 +++++ 15 files changed, 33 insertions(+) diff --git a/ytsage/languages/ar.json b/ytsage/languages/ar.json index 42a6135..810da58 100644 --- a/ytsage/languages/ar.json +++ b/ytsage/languages/ar.json @@ -245,6 +245,8 @@ "system_info": "┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д┘Ж╪╕╪з┘Е", "loading": "ЁЯФД ╪м╪з╪▒┘К ╪к╪н┘Е┘К┘Д ┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д┘Ж╪╕╪з┘Е...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ ╪з┘Д╪│╪м┘Д╪з╪к", + "logs_tooltip": "┘Б╪к╪н ┘Е╪м┘Д╪п ╪│╪м┘Д╪з╪к ╪з┘Д╪к╪╖╪и┘К┘В", "refreshing": "ЁЯФД ╪м╪з╪▒┘К ╪з┘Д╪к╪н╪п┘К╪л...", "refresh_failed": "┘Б╪┤┘Д ╪з┘Д╪к╪н╪п┘К╪л", "refresh_failed_message": "╪к╪╣╪░╪▒ ╪к╪н╪п┘К╪л ┘Е╪╣┘Д┘И┘Е╪з╪к ╪з┘Д╪е╪╡╪п╪з╪▒.", diff --git a/ytsage/languages/de.json b/ytsage/languages/de.json index 41b6817..af55e32 100644 --- a/ytsage/languages/de.json +++ b/ytsage/languages/de.json @@ -245,6 +245,8 @@ "system_info": "Systeminformationen", "loading": "ЁЯФД Systeminformationen werden geladen...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Logs", + "logs_tooltip": "Anwendungs-Protokollordner ├╢ffnen", "refreshing": "ЁЯФД Wird aktualisiert...", "refresh_failed": "Aktualisierung fehlgeschlagen", "refresh_failed_message": "Versionsinformationen konnten nicht aktualisiert werden.", diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index f9c59a0..b275ac4 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -246,6 +246,8 @@ "system_info": "System Information", "loading": "ЁЯФД Loading system information...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Logs", + "logs_tooltip": "Open application logs folder", "refreshing": "ЁЯФД Refreshing...", "refresh_failed": "Refresh Failed", "refresh_failed_message": "Could not refresh version information.", diff --git a/ytsage/languages/es.json b/ytsage/languages/es.json index cf2436d..aa05646 100644 --- a/ytsage/languages/es.json +++ b/ytsage/languages/es.json @@ -205,6 +205,8 @@ "system_info": "Informaci├│n del Sistema", "loading": "ЁЯФД Cargando informaci├│n del sistema...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Registros", + "logs_tooltip": "Abrir carpeta de registros de la aplicaci├│n", "refreshing": "ЁЯФД Actualizando...", "refresh_failed": "Actualizaci├│n Fallida", "refresh_failed_message": "No se pudo actualizar la informaci├│n de versi├│n.", diff --git a/ytsage/languages/fr.json b/ytsage/languages/fr.json index 6d38e3c..75bae7a 100644 --- a/ytsage/languages/fr.json +++ b/ytsage/languages/fr.json @@ -245,6 +245,8 @@ "system_info": "Informations syst├иme", "loading": "ЁЯФД Chargement des informations syst├иme...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Journaux", + "logs_tooltip": "Ouvrir le dossier des journaux d'application", "refreshing": "ЁЯФД Actualisation...", "refresh_failed": "├Йchec de l'actualisation", "refresh_failed_message": "Impossible d'actualiser les informations de version.", diff --git a/ytsage/languages/hi.json b/ytsage/languages/hi.json index a202285..aa258fc 100644 --- a/ytsage/languages/hi.json +++ b/ytsage/languages/hi.json @@ -245,6 +245,8 @@ "system_info": "рд╕рд┐рд╕реНрдЯрдо рдЬрд╛рдирдХрд╛рд░реА", "loading": "ЁЯФД рд╕рд┐рд╕реНрдЯрдо рдЬрд╛рдирдХрд╛рд░реА рд▓реЛрдб рд╣реЛ рд░рд╣реА рд╣реИ...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ рд▓реЙрдЧреНрд╕", + "logs_tooltip": "рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓реЙрдЧ рдлрд╝реЛрд▓реНрдбрд░ рдЦреЛрд▓реЗрдВ", "refreshing": "ЁЯФД рд░рд┐рдлреНрд░реЗрд╢ рд╣реЛ рд░рд╣рд╛ рд╣реИ...", "refresh_failed": "рд░рд┐рдлреНрд░реЗрд╢ рдЕрд╕рдлрд▓", "refresh_failed_message": "рд╕рдВрд╕реНрдХрд░рдг рдЬрд╛рдирдХрд╛рд░реА рд░рд┐рдлреНрд░реЗрд╢ рдХрд░рдиреЗ рдореЗрдВ рдЕрд╕рдорд░реНрдеред", diff --git a/ytsage/languages/id.json b/ytsage/languages/id.json index ce23cc2..ef68be8 100644 --- a/ytsage/languages/id.json +++ b/ytsage/languages/id.json @@ -245,6 +245,8 @@ "system_info": "Informasi sistem", "loading": "ЁЯФД Memuat informasi sistem...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Log", + "logs_tooltip": "Buka folder log aplikasi", "refreshing": "ЁЯФД Menyegarkan...", "refresh_failed": "Gagal menyegarkan", "refresh_failed_message": "Tidak dapat menyegarkan informasi versi.", diff --git a/ytsage/languages/it.json b/ytsage/languages/it.json index 5343b0d..2e5e647 100644 --- a/ytsage/languages/it.json +++ b/ytsage/languages/it.json @@ -245,6 +245,8 @@ "system_info": "Informazioni sistema", "loading": "ЁЯФД Caricamento informazioni sistema...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Log", + "logs_tooltip": "Apri cartella log applicazione", "refreshing": "ЁЯФД Aggiornamento...", "refresh_failed": "Aggiornamento fallito", "refresh_failed_message": "Impossibile aggiornare le informazioni sulla versione.", diff --git a/ytsage/languages/ja.json b/ytsage/languages/ja.json index 0dfe15f..4a71ee5 100644 --- a/ytsage/languages/ja.json +++ b/ytsage/languages/ja.json @@ -245,6 +245,8 @@ "system_info": "уВ╖уВ╣уГЖуГацГЕха▒", "loading": "ЁЯФД уВ╖уВ╣уГЖуГацГЕха▒уВТшкнуБ┐ш╛╝уБ┐ф╕н...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ уГнуВ░", + "logs_tooltip": "уВвуГЧуГкуВ▒уГ╝уВ╖уГзуГ│уБоуГнуВ░уГХуВйуГлуГАуВТщЦЛуБП", "refreshing": "ЁЯФД цЫ┤цЦ░ф╕н...", "refresh_failed": "цЫ┤цЦ░уБлхд▒цХЧуБЧуБ╛уБЧуБЯ", "refresh_failed_message": "уГРуГ╝уВ╕уГзуГ│цГЕха▒уВТцЫ┤цЦ░уБзуБНуБ╛уБЫуВУуБзуБЧуБЯуАВ", diff --git a/ytsage/languages/pl.json b/ytsage/languages/pl.json index 4388c01..5b38dec 100644 --- a/ytsage/languages/pl.json +++ b/ytsage/languages/pl.json @@ -245,6 +245,8 @@ "system_info": "Informacje systemowe", "loading": "ЁЯФД ┼Бadowanie informacji systemowych...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Logi", + "logs_tooltip": "Otw├│rz folder log├│w aplikacji", "refreshing": "ЁЯФД Od┼Ыwie┼╝anie...", "refresh_failed": "Od┼Ыwie┼╝anie nie powiod┼Вo si─Щ", "refresh_failed_message": "Nie mo┼╝na od┼Ыwie┼╝y─З informacji o wersji.", diff --git a/ytsage/languages/pt.json b/ytsage/languages/pt.json index eaf25cc..da43811 100644 --- a/ytsage/languages/pt.json +++ b/ytsage/languages/pt.json @@ -245,6 +245,8 @@ "system_info": "Informa├з├╡es do Sistema", "loading": "ЁЯФД Carregando informa├з├╡es do sistema...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Logs", + "logs_tooltip": "Abrir pasta de logs da aplica├з├гo", "refreshing": "ЁЯФД Atualizando...", "refresh_failed": "Atualiza├з├гo Falhou", "refresh_failed_message": "N├гo foi poss├нvel atualizar as informa├з├╡es de vers├гo.", diff --git a/ytsage/languages/ru.json b/ytsage/languages/ru.json index 9da7f1e..5fcaf7b 100644 --- a/ytsage/languages/ru.json +++ b/ytsage/languages/ru.json @@ -245,6 +245,8 @@ "system_info": "╨б╨╕╤Б╤В╨╡╨╝╨╜╨░╤П ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╤П", "loading": "ЁЯФД ╨Ч╨░╨│╤А╤Г╨╖╨║╨░ ╤Б╨╕╤Б╤В╨╡╨╝╨╜╨╛╨╣ ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╨╕...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ ╨Ы╨╛╨│╨╕", + "logs_tooltip": "╨Ю╤В╨║╤А╤Л╤В╤М ╨┐╨░╨┐╨║╤Г ╤Б ╨╗╨╛╨│╨░╨╝╨╕ ╨┐╤А╨╕╨╗╨╛╨╢╨╡╨╜╨╕╤П", "refreshing": "ЁЯФД ╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡...", "refresh_failed": "╨Ю╨▒╨╜╨╛╨▓╨╗╨╡╨╜╨╕╨╡ ╨╜╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М", "refresh_failed_message": "╨Э╨╡ ╤Г╨┤╨░╨╗╨╛╤Б╤М ╨╛╨▒╨╜╨╛╨▓╨╕╤В╤М ╨╕╨╜╤Д╨╛╤А╨╝╨░╤Ж╨╕╤О ╨╛ ╨▓╨╡╤А╤Б╨╕╨╕.", diff --git a/ytsage/languages/tr.json b/ytsage/languages/tr.json index aa38369..fd3138a 100644 --- a/ytsage/languages/tr.json +++ b/ytsage/languages/tr.json @@ -243,6 +243,8 @@ "system_info": "Sistem bilgileri", "loading": "ЁЯФД Sistem bilgileri y├╝kleniyor...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ Kay─▒tlar", + "logs_tooltip": "Uygulama kay─▒t klas├╢r├╝n├╝ a├з", "refreshing": "ЁЯФД Yenileniyor...", "refresh_failed": "Yenileme ba┼Яar─▒s─▒z", "refresh_failed_message": "S├╝r├╝m bilgileri yenilenemedi.", diff --git a/ytsage/languages/zh.json b/ytsage/languages/zh.json index 3f4128f..3480e70 100644 --- a/ytsage/languages/zh.json +++ b/ytsage/languages/zh.json @@ -233,6 +233,8 @@ "system_info": "ч│╗ч╗Яф┐бцБп", "loading": "ЁЯФД цнгхЬихКаш╜╜ч│╗ч╗Яф┐бцБп...", "refresh": "ЁЯФД", + "open_logs": "ЁЯУВ цЧех┐Ч", + "logs_tooltip": "цЙУх╝Ах║ФчФичиЛх║ПцЧех┐ЧцЦЗф╗╢хд╣", "refreshing": "ЁЯФД цнгхЬихИ╖цЦ░...", "refresh_failed": "хИ╖цЦ░хд▒ш┤е", "refresh_failed_message": "цЧац│ХхИ╖цЦ░чЙИцЬмф┐бцБпуАВ", diff --git a/ytsage/utils/ytsage_localization.py b/ytsage/utils/ytsage_localization.py index 199c3ad..579cd9e 100644 --- a/ytsage/utils/ytsage_localization.py +++ b/ytsage/utils/ytsage_localization.py @@ -113,6 +113,11 @@ class LocalizationManager: "download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.", "download_failed_return_code": "Download failed with return code {return_code}", "direct_command_error": "Error in direct command: {error}" + }, + "about": { + "open_logs": "ЁЯУВ Logs", + "logs_tooltip": "Open application logs folder", + "refresh": "ЁЯФД" } } From c4e09cf4333c913abd730b7055d9852da5f19736 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:54:18 +0200 Subject: [PATCH 119/134] About dialog: adjust loading text and style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip the ЁЯФД emoji from the localized loading string to avoid rendering issues and use the cleaned text for the QLabel. Tweak the label styling by changing the color from #cccccc to #888888 and removing the italic font-style for a more consistent appearance. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index 48c8b76..dc39a48 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -366,14 +366,15 @@ class AboutDialog(QDialog): def _show_loading_message(self) -> None: """Show a compact loading message while system information is being gathered.""" - loading_label = QLabel(_("about.loading")) + # Strip the emoji from the localized string to avoid rendering issues + loading_text = _("about.loading").replace("ЁЯФД", "").strip() + loading_label = QLabel(loading_text) loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_label.setStyleSheet( """ QLabel { - color: #cccccc; + color: #888888; font-size: 11px; - font-style: italic; padding: 10px; } """ From 201d2fc8b69bcaea495124a30c42a72f07179ef3 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:11:33 +0200 Subject: [PATCH 120/134] Add Chinese, Hindi, Indonesian language files Add unofficial translation files for Chinese Simplified, Hindi and Indonesian under setup-scripts/Languages/Unofficial. Also update Setup-windows.iss and Setup-windows-ffmpeg.iss to incorporate related installer changes. --- .../Unofficial/ChineseSimplified.isl | 418 ++++++++++++++++++ setup-scripts/Languages/Unofficial/Hindi.islu | 336 ++++++++++++++ .../Languages/Unofficial/Indonesian.isl | 350 +++++++++++++++ setup-scripts/Setup-windows-ffmpeg.iss | 6 + setup-scripts/Setup-windows.iss | 6 + 5 files changed, 1116 insertions(+) create mode 100644 setup-scripts/Languages/Unofficial/ChineseSimplified.isl create mode 100644 setup-scripts/Languages/Unofficial/Hindi.islu create mode 100644 setup-scripts/Languages/Unofficial/Indonesian.isl diff --git a/setup-scripts/Languages/Unofficial/ChineseSimplified.isl b/setup-scripts/Languages/Unofficial/ChineseSimplified.isl new file mode 100644 index 0000000..d6a11c4 --- /dev/null +++ b/setup-scripts/Languages/Unofficial/ChineseSimplified.isl @@ -0,0 +1,418 @@ +; *** Inno Setup version 6.5.0+ Chinese Simplified messages *** +; +; To download user-contributed translations of this file, go to: +; https://jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). +; +; Maintained by Zhenghan Yang +; Email: 847320916@QQ.com +; Translation based on network resource +; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation +; + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=чоАф╜Уф╕нцЦЗ +; If Language Name display incorrect, uncomment next line +; LanguageName=<7B80><4F53><4E2D><6587> +; About LanguageID, to reference link: +; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c +LanguageID=$0804 +; About CodePage, to reference link: +; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers +LanguageCodePage=936 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +;DialogFontName= +;DialogFontSize=9 +;DialogFontBaseScaleWidth=7 +;DialogFontBaseScaleHeight=15 +;WelcomeFontName=Segoe UI +;WelcomeFontSize=14 + +[Messages] + +; *** х║ФчФичиЛх║ПцаЗщвШ +SetupAppTitle=хоЙшгЕ +SetupWindowTitle=хоЙшгЕ - %1 +UninstallAppTitle=хН╕ш╜╜ +UninstallAppFullTitle=%1 хН╕ш╜╜ + +; *** Misc. common +InformationTitle=ф┐бцБп +ConfirmTitle=чбошод +ErrorTitle=щФЩшпп + +; *** SetupLdr messages +SetupLdrStartupMessage=чО░хЬих░ЖхоЙшгЕ %1уАВцВицГ│шжБч╗зч╗нхРЧя╝Я +LdrCannotCreateTemp=цЧац│ХхИЫх╗║ф╕┤цЧ╢цЦЗф╗╢уАВхоЙшгЕчиЛх║Пх╖▓ф╕нцнв +LdrCannotExecTemp=цЧац│ХцЙзшбМф╕┤цЧ╢чЫох╜Хф╕нчЪДцЦЗф╗╢уАВхоЙшгЕчиЛх║Пх╖▓ф╕нцнв +HelpTextNote= + +; *** хРпхКищФЩшппц╢ИцБп +LastErrorMessage=%1уАВ%n%nщФЩшпп %2: %3 +SetupFileMissing=хоЙшгЕчЫох╜Хф╕нч╝║х░СцЦЗф╗╢ %1уАВшп╖ф┐оцнгш┐Щф╕кщЧощвШцИЦшАЕшО╖хПЦчиЛх║ПчЪДцЦ░хЙпцЬмуАВ +SetupFileCorrupt=хоЙшгЕцЦЗф╗╢х╖▓цНЯхЭПуАВшп╖шО╖хПЦчиЛх║ПчЪДцЦ░хЙпцЬмуАВ +SetupFileCorruptOrWrongVer=хоЙшгЕцЦЗф╗╢х╖▓цНЯхЭПя╝МцИЦцШпф╕Ош┐Щф╕кхоЙшгЕчиЛх║ПчЪДчЙИцЬмф╕НхЕ╝хо╣уАВшп╖ф┐оцнгш┐Щф╕кщЧощвШцИЦшО╖хПЦцЦ░чЪДчиЛх║ПхЙпцЬмуАВ +InvalidParameter=цЧацХИчЪДхС╜ф╗дшбМхПВцХ░я╝Ъ%n%n%1 +SetupAlreadyRunning=хоЙшгЕчиЛх║ПцнгхЬиш┐РшбМуАВ +WindowsVersionNotSupported=цндчиЛх║Пф╕НцФпцМБх╜УхЙНшобчоЧцЬ║ш┐РшбМчЪД Windows чЙИцЬмуАВ +WindowsServicePackRequired=цндчиЛх║ПщЬАшжБ %1 цЬНхКбхМЕ %2 цИЦцЫ┤щлШчЙИцЬмуАВ +NotOnThisPlatform=цндчиЛх║Пф╕НшГ╜хЬи %1 ф╕Кш┐РшбМуАВ +OnlyOnThisPlatform=цндчиЛх║ПхПкшГ╜хЬи %1 ф╕Кш┐РшбМуАВ +OnlyOnTheseArchitectures=цндчиЛх║ПхПкшГ╜хоЙшгЕхИ░ф╕║ф╕ЛхИЧхдДчРЖхЩицЮ╢цЮДшо╛шобчЪД Windows чЙИцЬмф╕ня╝Ъ%n%n%1 +WinVersionTooLowError=цндчиЛх║ПщЬАшжБ %1 чЙИцЬм %2 цИЦцЫ┤щлШуАВ +WinVersionTooHighError=цндчиЛх║Пф╕НшГ╜хоЙшгЕф║О %1 чЙИцЬм %2 цИЦцЫ┤щлШуАВ +AdminPrivilegesRequired=хЬихоЙшгЕцндчиЛх║ПцЧ╢цВих┐Ещб╗ф╗ечобчРЖхСШш║лф╗╜чЩ╗х╜ХуАВ +PowerUserPrivilegesRequired=хЬихоЙшгЕцндчиЛх║ПцЧ╢цВих┐Ещб╗ф╗ечобчРЖхСШш║лф╗╜цИЦцЬЙцЭГщЩРчЪДчФицИ╖ч╗Дш║лф╗╜чЩ╗х╜ХуАВ +SetupAppRunningError=хоЙшгЕчиЛх║ПхПСчО░ %1 х╜УхЙНцнгхЬиш┐РшбМуАВ%n%nшп╖хЕИхЕ│щЧнцнгхЬиш┐РшбМчЪДчиЛх║Пя╝МчД╢хРОчВ╣хЗ╗тАЬчбохоЪтАЭч╗зч╗ня╝МцИЦчВ╣хЗ╗тАЬхПЦц╢ИтАЭщААхЗ║уАВ +UninstallAppRunningError=хН╕ш╜╜чиЛх║ПхПСчО░ %1 х╜УхЙНцнгхЬиш┐РшбМуАВ%n%nшп╖хЕИхЕ│щЧнцнгхЬиш┐РшбМчЪДчиЛх║Пя╝МчД╢хРОчВ╣хЗ╗тАЬчбохоЪтАЭч╗зч╗ня╝МцИЦчВ╣хЗ╗тАЬхПЦц╢ИтАЭщААхЗ║уАВ + +; *** хРпхКищЧощвШ +PrivilegesRequiredOverrideTitle=щАЙцЛйхоЙшгЕчиЛх║Пцибх╝П +PrivilegesRequiredOverrideInstruction=щАЙцЛйхоЙшгЕцибх╝П +PrivilegesRequiredOverrideText1=%1 хПпф╗еф╕║цЙАцЬЙчФицИ╖хоЙшгЕ(щЬАшжБчобчРЖхСШцЭГщЩР)я╝МцИЦф╗Еф╕║цВихоЙшгЕуАВ +PrivilegesRequiredOverrideText2=%1 хПпф╗еф╗Еф╕║цВихоЙшгЕя╝МцИЦф╕║цЙАцЬЙчФицИ╖хоЙшгЕ(щЬАшжБчобчРЖхСШцЭГщЩР)уАВ +PrivilegesRequiredOverrideAllUsers=ф╕║цЙАцЬЙчФицИ╖хоЙшгЕ(&A) +PrivilegesRequiredOverrideAllUsersRecommended=ф╕║цЙАцЬЙчФицИ╖хоЙшгЕ(&A) (х╗║шоощАЙщб╣) +PrivilegesRequiredOverrideCurrentUser=ф╗Еф╕║цИСхоЙшгЕ(&M) +PrivilegesRequiredOverrideCurrentUserRecommended=ф╗Еф╕║цИСхоЙшгЕ(&M) (х╗║шоощАЙщб╣) + +; *** хЕ╢ф╗ЦщФЩшпп +ErrorCreatingDir=хоЙшгЕчиЛх║ПцЧац│ХхИЫх╗║чЫох╜ХтАЬ%1тАЭ +ErrorTooManyFilesInDir=цЧац│ХхЬичЫох╜ХтАЬ%1тАЭф╕нхИЫх╗║цЦЗф╗╢я╝МхЫаф╕║щЗМщЭвхМЕхРлхдкхдЪцЦЗф╗╢ + +; *** хоЙшгЕчиЛх║ПхЕмхЕ▒ц╢ИцБп +ExitSetupTitle=щААхЗ║хоЙшгЕчиЛх║П +ExitSetupMessage=хоЙшгЕчиЛх║Пх░ЪцЬкхоМцИРуАВхжВцЮЬчО░хЬищААхЗ║я╝Мх░Жф╕Нф╝ЪхоЙшгЕшпечиЛх║ПуАВ%n%nцВиф╣ЛхРОхПпф╗ехЖНцмбш┐РшбМхоЙшгЕчиЛх║ПхоМцИРхоЙшгЕуАВ%n%nчО░хЬищААхЗ║хоЙшгЕчиЛх║ПхРЧя╝Я +AboutSetupMenuItem=хЕ│ф║ОхоЙшгЕчиЛх║П(&A)... +AboutSetupTitle=хЕ│ф║ОхоЙшгЕчиЛх║П +AboutSetupMessage=%1 чЙИцЬм %2%n%3%n%n%1 ф╕╗щб╡я╝Ъ%n%4 +AboutSetupNote= +TranslatorNote=чоАф╜Уф╕нцЦЗч┐╗шпСчФ▒Kira(847320916@qq.com)ч╗┤цКдуАВщб╣чЫохЬ░хЭАя╝Ъhttps://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation + +; *** цМЙщТо +ButtonBack=< ф╕Кф╕Ацне(&B) +ButtonNext=ф╕Лф╕Ацне(&N) > +ButtonInstall=хоЙшгЕ(&I) +ButtonOK=чбохоЪ +ButtonCancel=хПЦц╢И +ButtonYes=цШп(&Y) +ButtonYesToAll=хЕицШп(&A) +ButtonNo=хРж(&N) +ButtonNoToAll=хЕихРж(&O) +ButtonFinish=хоМцИР(&F) +ButtonBrowse=ц╡ПшзИ(&B)... +ButtonWizardBrowse=ц╡ПшзИ(&R)... +ButtonNewFolder=цЦ░х╗║цЦЗф╗╢хд╣(&M) + +; *** тАЬщАЙцЛйшпншиАтАЭхп╣шпЭцбЖц╢ИцБп +SelectLanguageTitle=щАЙцЛйхоЙшгЕшпншиА +SelectLanguageLabel=щАЙцЛйхоЙшгЕцЧ╢ф╜┐чФичЪДшпншиАуАВ + +; *** хЕмхЕ▒хРСхп╝цЦЗхнЧ +ClickNext=чВ╣хЗ╗тАЬф╕Лф╕АцнетАЭч╗зч╗ня╝МцИЦчВ╣хЗ╗тАЬхПЦц╢ИтАЭщААхЗ║хоЙшгЕчиЛх║ПуАВ +BeveledLabel= +BrowseDialogTitle=ц╡ПшзИцЦЗф╗╢хд╣ +BrowseDialogLabel=хЬиф╕ЛщЭвчЪДхИЧшбиф╕нщАЙцЛйф╕Аф╕кцЦЗф╗╢хд╣я╝МчД╢хРОчВ╣хЗ╗тАЬчбохоЪтАЭуАВ +NewFolderName=цЦ░х╗║цЦЗф╗╢хд╣ + +; *** тАЬцмвш┐ОтАЭхРСхп╝щб╡ +WelcomeLabel1=цмвш┐Оф╜┐чФи [name] хоЙшгЕхРСхп╝ +WelcomeLabel2=чО░хЬих░ЖхоЙшгЕ [name/ver] хИ░цВичЪДчФ╡шДСф╕нуАВ%n%nх╗║шооцВихЬич╗зч╗нхоЙшгЕхЙНхЕ│щЧнцЙАцЬЙхЕ╢ф╗Цх║ФчФичиЛх║ПуАВ + +; *** тАЬхпЖчаБтАЭхРСхп╝щб╡ +WizardPassword=хпЖчаБ +PasswordLabel1=ш┐Щф╕кхоЙшгЕчиЛх║ПцЬЙхпЖчаБф┐ЭцКдуАВ +PasswordLabel3=шп╖ш╛УхЕехпЖчаБя╝МчД╢хРОчВ╣хЗ╗тАЬф╕Лф╕АцнетАЭч╗зч╗нуАВхпЖчаБхМ║хИЖхдзх░ПхЖЩуАВ +PasswordEditLabel=хпЖчаБ(&P)я╝Ъ +IncorrectPassword=цВиш╛УхЕечЪДхпЖчаБф╕Нцнгчбоя╝Мшп╖щЗНцЦ░ш╛УхЕеуАВ + +; *** тАЬшо╕хПпхНПшоотАЭхРСхп╝щб╡ +WizardLicense=шо╕хПпхНПшоо +LicenseLabel=шп╖хЬич╗зч╗нхоЙшгЕхЙНщШЕшп╗ф╗еф╕ЛщЗНшжБф┐бцБпуАВ +LicenseLabel3=шп╖ф╗Фч╗ЖщШЕшп╗ф╕ЛхИЧшо╕хПпхНПшооуАВхЬич╗зч╗нхоЙшгЕхЙНцВих┐Ещб╗хРМцДПш┐Щф║ЫхНПшооцЭбцм╛уАВ +LicenseAccepted=цИСхРМцДПцндхНПшоо(&A) +LicenseNotAccepted=цИСф╕НхРМцДПцндхНПшоо(&D) + +; *** тАЬф┐бцБптАЭхРСхп╝щб╡ +WizardInfoBefore=ф┐бцБп +InfoBeforeLabel=шп╖хЬич╗зч╗нхоЙшгЕхЙНщШЕшп╗ф╗еф╕ЛщЗНшжБф┐бцБпуАВ +InfoBeforeClickLabel=хЗЖхдЗхе╜ч╗зч╗нхоЙшгЕхРОя╝МчВ╣хЗ╗тАЬф╕Лф╕АцнетАЭуАВ +WizardInfoAfter=ф┐бцБп +InfoAfterLabel=шп╖хЬич╗зч╗нхоЙшгЕхЙНщШЕшп╗ф╗еф╕ЛщЗНшжБф┐бцБпуАВ +InfoAfterClickLabel=хЗЖхдЗхе╜ч╗зч╗нхоЙшгЕхРОя╝МчВ╣хЗ╗тАЬф╕Лф╕АцнетАЭуАВ + +; *** тАЬчФицИ╖ф┐бцБптАЭхРСхп╝щб╡ +WizardUserInfo=чФицИ╖ф┐бцБп +UserInfoDesc=шп╖ш╛УхЕецВичЪДф┐бцБпуАВ +UserInfoName=чФицИ╖хРН(&U)я╝Ъ +UserInfoOrg=ч╗Дч╗З(&O)я╝Ъ +UserInfoSerial=х║ПхИЧхП╖(&S)я╝Ъ +UserInfoNameRequired=цВих┐Ещб╗ш╛УхЕечФицИ╖хРНуАВ + +; *** тАЬщАЙцЛйчЫоцаЗчЫох╜ХтАЭхРСхп╝щб╡ +WizardSelectDir=щАЙцЛйчЫоцаЗф╜Нч╜о +SelectDirDesc=цВицГ│х░Ж [name] хоЙшгЕхЬихУкщЗМя╝Я +SelectDirLabel3=хоЙшгЕчиЛх║Пх░ЖхоЙшгЕ [name] хИ░ф╕ЛщЭвчЪДцЦЗф╗╢хд╣ф╕нуАВ +SelectDirBrowseLabel=чВ╣хЗ╗тАЬф╕Лф╕АцнетАЭч╗зч╗нуАВхжВцЮЬцВицГ│щАЙцЛйхЕ╢ф╗ЦцЦЗф╗╢хд╣я╝МчВ╣хЗ╗тАЬц╡ПшзИтАЭуАВ +DiskSpaceGBLabel=шЗ│х░СщЬАшжБцЬЙ [gb] GB чЪДхПпчФичгБчЫШчй║щЧ┤уАВ +DiskSpaceMBLabel=шЗ│х░СщЬАшжБцЬЙ [mb] MB чЪДхПпчФичгБчЫШчй║щЧ┤уАВ +CannotInstallToNetworkDrive=хоЙшгЕчиЛх║ПцЧац│ХхоЙшгЕхИ░ф╕Аф╕кч╜Сч╗Ьщй▒хКихЩиуАВ +CannotInstallToUNCPath=хоЙшгЕчиЛх║ПцЧац│ХхоЙшгЕхИ░ф╕Аф╕к UNC ш╖пх╛ДуАВ +InvalidPath=цВих┐Ещб╗ш╛УхЕеф╕Аф╕кх╕жщй▒хКихЩихН╖цаЗчЪДхоМцХ┤ш╖пх╛Дя╝Мф╛ЛхжВя╝Ъ%n%nC:\APP%n%nцИЦUNCш╖пх╛Дя╝Ъ%n%n\\server\share +InvalidDrive=цВищАЙхоЪчЪДщй▒хКихЩицИЦ UNC хЕ▒ф║лф╕НхнШхЬицИЦф╕НшГ╜шо┐щЧоуАВшп╖щАЙцЛйхЕ╢ф╗Цф╜Нч╜оуАВ +DiskSpaceWarningTitle=чгБчЫШчй║щЧ┤ф╕Нш╢│ +DiskSpaceWarning=хоЙшгЕчиЛх║ПшЗ│х░СщЬАшжБ %1 KB чЪДхПпчФичй║щЧ┤цЙНшГ╜хоЙшгЕя╝Мф╜ЖщАЙхоЪщй▒хКихЩихПкцЬЙ %2 KB чЪДхПпчФичй║щЧ┤уАВ%n%nцВиф╕АхоЪшжБч╗зч╗нхРЧя╝Я +DirNameTooLong=цЦЗф╗╢хд╣хРНчз░цИЦш╖пх╛ДхдкщХ┐уАВ +InvalidDirName=цЦЗф╗╢хд╣хРНчз░цЧацХИуАВ +BadDirName32=цЦЗф╗╢хд╣хРНчз░ф╕НшГ╜хМЕхРлф╕ЛхИЧф╗╗ф╜ХхнЧчмжя╝Ъ%n%n%1 +DirExistsTitle=цЦЗф╗╢хд╣х╖▓хнШхЬи +DirExists=цЦЗф╗╢хд╣я╝Ъ%n%n%1%n%nх╖▓ч╗ПхнШхЬиуАВцВиф╕АхоЪшжБхоЙшгЕхИ░ш┐Щф╕кцЦЗф╗╢хд╣ф╕нхРЧя╝Я +DirDoesntExistTitle=цЦЗф╗╢хд╣ф╕НхнШхЬи +DirDoesntExist=цЦЗф╗╢хд╣я╝Ъ%n%n%1%n%nф╕НхнШхЬиуАВцВицГ│шжБхИЫх╗║цндцЦЗф╗╢хд╣хРЧя╝Я + +; *** тАЬщАЙцЛйч╗Дф╗╢тАЭхРСхп╝щб╡ +WizardSelectComponents=щАЙцЛйч╗Дф╗╢ +SelectComponentsDesc=цВицГ│хоЙшгЕхУкф║ЫчиЛх║Пч╗Дф╗╢я╝Я +SelectComponentsLabel2=щАЙф╕нцВицГ│хоЙшгЕчЪДч╗Дф╗╢я╝ЫхПЦц╢ИцВиф╕НцГ│хоЙшгЕчЪДч╗Дф╗╢уАВчД╢хРОчВ╣хЗ╗тАЬф╕Лф╕АцнетАЭч╗зч╗нуАВ +FullInstallation=хоМхЕихоЙшгЕ +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=чоАц┤БхоЙшгЕ +CustomInstallation=шЗкхоЪф╣ЙхоЙшгЕ +NoUninstallWarningTitle=ч╗Дф╗╢х╖▓хнШхЬи +NoUninstallWarning=хоЙшгЕчиЛх║ПцгАц╡ЛхИ░ф╕ЛхИЧч╗Дф╗╢х╖▓хоЙшгЕхЬицВичЪДчФ╡шДСф╕ня╝Ъ%n%n%1%n%nхПЦц╢ИщАЙф╕нш┐Щф║Ыч╗Дф╗╢ф╕Нф╝ЪхН╕ш╜╜хоГф╗муАВ%n%nчбохоЪшжБч╗зч╗нхРЧя╝Я +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceGBLabel=х╜УхЙНщАЙцЛйчЪДч╗Дф╗╢щЬАшжБшЗ│х░С [gb] GB чЪДчгБчЫШчй║щЧ┤уАВ +ComponentsDiskSpaceMBLabel=х╜УхЙНщАЙцЛйчЪДч╗Дф╗╢щЬАшжБшЗ│х░С [mb] MB чЪДчгБчЫШчй║щЧ┤уАВ + +; *** тАЬщАЙцЛйщЩДхКаф╗╗хКбтАЭхРСхп╝щб╡ +WizardSelectTasks=щАЙцЛйщЩДхКаф╗╗хКб +SelectTasksDesc=цВицГ│шжБхоЙшгЕчиЛх║ПцЙзшбМхУкф║ЫщЩДхКаф╗╗хКбя╝Я +SelectTasksLabel2=щАЙцЛйцВицГ│шжБхоЙшгЕчиЛх║ПхЬихоЙшгЕ [name] цЧ╢цЙзшбМчЪДщЩДхКаф╗╗хКбя╝МчД╢хРОчВ╣хЗ╗тАЬф╕Лф╕АцнетАЭуАВ + +; *** тАЬщАЙцЛйх╝АхзЛшПЬхНХцЦЗф╗╢хд╣тАЭхРСхп╝щб╡ +WizardSelectProgramGroup=щАЙцЛйх╝АхзЛшПЬхНХцЦЗф╗╢хд╣ +SelectStartMenuFolderDesc=хоЙшгЕчиЛх║Пх║ФшпехЬихУкщЗМцФ╛ч╜очиЛх║ПчЪДх┐лцН╖цЦ╣х╝Пя╝Я +SelectStartMenuFolderLabel3=хоЙшгЕчиЛх║Пх░ЖхЬиф╕ЛхИЧтАЬх╝АхзЛтАЭшПЬхНХцЦЗф╗╢хд╣ф╕нхИЫх╗║чиЛх║ПчЪДх┐лцН╖цЦ╣х╝ПуАВ +SelectStartMenuFolderBrowseLabel=чВ╣хЗ╗тАЬф╕Лф╕АцнетАЭч╗зч╗нуАВхжВцЮЬцВицГ│щАЙцЛйхЕ╢ф╗ЦцЦЗф╗╢хд╣я╝МчВ╣хЗ╗тАЬц╡ПшзИтАЭуАВ +MustEnterGroupName=цВих┐Ещб╗ш╛УхЕеф╕Аф╕кцЦЗф╗╢хд╣хРНуАВ +GroupNameTooLong=цЦЗф╗╢хд╣хРНцИЦш╖пх╛ДхдкщХ┐уАВ +InvalidGroupName=цЧацХИчЪДцЦЗф╗╢хд╣хРНхнЧуАВ +BadGroupName=цЦЗф╗╢хд╣хРНф╕НшГ╜хМЕхРлф╕ЛхИЧф╗╗ф╜ХхнЧчмжя╝Ъ%n%n%1 +NoProgramGroupCheck2=ф╕НхИЫх╗║х╝АхзЛшПЬхНХцЦЗф╗╢хд╣(&D) + +; *** тАЬхЗЖхдЗхоЙшгЕтАЭхРСхп╝щб╡ +WizardReady=хЗЖхдЗхоЙшгЕ +ReadyLabel1=хоЙшгЕчиЛх║ПхЗЖхдЗх░▒ч╗кя╝МчО░хЬихПпф╗ех╝АхзЛхоЙшгЕ [name] хИ░цВичЪДчФ╡шДСуАВ +ReadyLabel2a=чВ╣хЗ╗тАЬхоЙшгЕтАЭч╗зч╗нцндхоЙшгЕчиЛх║ПуАВхжВцЮЬцВицГ│щЗНцЦ░шАГшЩСцИЦф┐оцФ╣ф╗╗ф╜Хшо╛ч╜оя╝МчВ╣хЗ╗тАЬф╕Кф╕АцнетАЭуАВ +ReadyLabel2b=чВ╣хЗ╗тАЬхоЙшгЕтАЭч╗зч╗нцндхоЙшгЕчиЛх║ПуАВ +ReadyMemoUserInfo=чФицИ╖ф┐бцБпя╝Ъ +ReadyMemoDir=чЫоцаЗф╜Нч╜оя╝Ъ +ReadyMemoType=хоЙшгЕч▒╗хЮЛя╝Ъ +ReadyMemoComponents=х╖▓щАЙцЛйч╗Дф╗╢я╝Ъ +ReadyMemoGroup=х╝АхзЛшПЬхНХцЦЗф╗╢хд╣я╝Ъ +ReadyMemoTasks=щЩДхКаф╗╗хКбя╝Ъ + +; *** TExtractionWizardPage хРСхп╝щб╡щЭвф╕О ExtractArchive +ExtractingLabel=цнгхЬишзгхОЛцЦЗф╗╢... +ButtonStopExtraction=хБЬцнвшзгхОЛ(&S) +StopExtraction=цВичбохоЪшжБхБЬцнвшзгхОЛхРЧя╝Я +ErrorExtractionAborted=шзгхОЛх╖▓ф╕нцнв +ErrorExtractionFailed=шзгхОЛхд▒ш┤ея╝Ъ%1 + +; *** хОЛч╝йцЦЗф╗╢шзгхОЛхд▒ш┤ешпжцГЕ +ArchiveIncorrectPassword=хОЛч╝йцЦЗф╗╢хпЖчаБф╕Нцнгчбо +ArchiveIsCorrupted=хОЛч╝йцЦЗф╗╢х╖▓цНЯхЭП +ArchiveUnsupportedFormat=ф╕НцФпцМБчЪДхОЛч╝йцЦЗф╗╢ца╝х╝П + +; *** TDownloadWizardPage хРСхп╝щб╡щЭвхТМ DownloadTemporaryFile +DownloadingLabel2=цнгхЬиф╕Лш╜╜цЦЗф╗╢... +ButtonStopDownload=хБЬцнвф╕Лш╜╜(&S) +StopDownload=цВичбохоЪшжБхБЬцнвф╕Лш╜╜хРЧя╝Я +ErrorDownloadAborted=ф╕Лш╜╜х╖▓ф╕нцнв +ErrorDownloadFailed=ф╕Лш╜╜хд▒ш┤ея╝Ъ%1 %2 +ErrorDownloadSizeFailed=шО╖хПЦф╕Лш╜╜хдзх░Пхд▒ш┤ея╝Ъ%1 %2 +ErrorProgress=цЧацХИчЪДш┐Ых║жя╝Ъ%1 / %2 +ErrorFileSize=цЦЗф╗╢хдзх░ПщФЩшппя╝ЪщвДцЬЯ %1я╝МхоЮщЩЕ %2 + +; *** тАЬцнгхЬихЗЖхдЗхоЙшгЕтАЭхРСхп╝щб╡ +WizardPreparing=цнгхЬихЗЖхдЗхоЙшгЕ +PreparingDesc=хоЙшгЕчиЛх║ПцнгхЬихЗЖхдЗхоЙшгЕ [name] хИ░цВичЪДчФ╡шДСуАВ +PreviousInstallNotCompleted=хЕИхЙНчЪДчиЛх║ПхоЙшгЕцИЦхН╕ш╜╜цЬкхоМцИРя╝МцВищЬАшжБщЗНхРпцВичЪДчФ╡шДСф╗ехоМцИРуАВ%n%nхЬищЗНхРпчФ╡шДСхРОя╝МхЖНцмбш┐РшбМхоЙшгЕчиЛх║Пф╗ехоМцИР [name] чЪДхоЙшгЕуАВ +CannotContinue=хоЙшгЕчиЛх║Пф╕НшГ╜ч╗зч╗нуАВшп╖чВ╣хЗ╗тАЬхПЦц╢ИтАЭщААхЗ║уАВ +ApplicationsFound=ф╗еф╕Лх║ФчФичиЛх║ПцнгхЬиф╜┐чФих░ЖчФ▒хоЙшгЕчиЛх║ПцЫ┤цЦ░чЪДцЦЗф╗╢уАВх╗║шооцВихЕБшо╕хоЙшгЕчиЛх║ПшЗкхКихЕ│щЧнш┐Щф║Ых║ФчФичиЛх║ПуАВ +ApplicationsFound2=ф╗еф╕Лх║ФчФичиЛх║ПцнгхЬиф╜┐чФих░ЖчФ▒хоЙшгЕчиЛх║ПцЫ┤цЦ░чЪДцЦЗф╗╢уАВх╗║шооцВихЕБшо╕хоЙшгЕчиЛх║ПшЗкхКихЕ│щЧнш┐Щф║Ых║ФчФичиЛх║ПуАВхоЙшгЕхоМцИРхРОя╝МхоЙшгЕчиЛх║Пх░Жх░ЭшпХщЗНцЦ░хРпхКиш┐Щф║Ых║ФчФичиЛх║ПуАВ +CloseApplications=шЗкхКихЕ│щЧнх║ФчФичиЛх║П(&A) +DontCloseApplications=ф╕НшжБхЕ│щЧнх║ФчФичиЛх║П(&D) +ErrorCloseApplications=хоЙшгЕчиЛх║ПцЧац│ХшЗкхКихЕ│щЧнцЙАцЬЙх║ФчФичиЛх║ПуАВх╗║шооцВихЬич╗зч╗нф╣ЛхЙНя╝МхЕ│щЧнцЙАцЬЙхЬиф╜┐чФищЬАшжБчФ▒хоЙшгЕчиЛх║ПцЫ┤цЦ░чЪДцЦЗф╗╢чЪДх║ФчФичиЛх║ПуАВ +PrepareToInstallNeedsRestart=хоЙшгЕчиЛх║Пх┐Ещб╗щЗНхРпцВичЪДшобчоЧцЬ║уАВшобчоЧцЬ║щЗНхРпхРОя╝Мшп╖хЖНцмбш┐РшбМхоЙшгЕчиЛх║Пф╗ехоМцИР [name] чЪДхоЙшгЕуАВ%n%nцШпхРжчлЛхН│щЗНцЦ░хРпхКия╝Я + +; *** тАЬцнгхЬихоЙшгЕтАЭхРСхп╝щб╡ +WizardInstalling=цнгхЬихоЙшгЕ +InstallingLabel=хоЙшгЕчиЛх║ПцнгхЬихоЙшгЕ [name] хИ░цВичЪДчФ╡шДСя╝Мшп╖чиНхАЩуАВ + +; *** тАЬхоЙшгЕхоМцИРтАЭхРСхп╝щб╡ +FinishedHeadingLabel=[name] хоЙшгЕхоМцИР +FinishedLabelNoIcons=хоЙшгЕчиЛх║Пх╖▓хЬицВичЪДчФ╡шДСф╕нхоЙшгЕф║Ж [name]уАВ +FinishedLabel=хоЙшгЕчиЛх║Пх╖▓хЬицВичЪДчФ╡шДСф╕нхоЙшгЕф║Ж [name]уАВцВихПпф╗ещАЪш┐Зх╖▓хоЙшгЕчЪДх┐лцН╖цЦ╣х╝Пш┐РшбМцндх║ФчФичиЛх║ПуАВ +ClickFinish=чВ╣хЗ╗тАЬхоМцИРтАЭщААхЗ║хоЙшгЕчиЛх║ПуАВ +FinishedRestartLabel=ф╕║хоМцИР [name] чЪДхоЙшгЕя╝МхоЙшгЕчиЛх║Пх┐Ещб╗щЗНцЦ░хРпхКицВичЪДчФ╡шДСуАВшжБчлЛхН│щЗНхРпхРЧя╝Я +FinishedRestartMessage=ф╕║хоМцИР [name] чЪДхоЙшгЕя╝МхоЙшгЕчиЛх║Пх┐Ещб╗щЗНцЦ░хРпхКицВичЪДчФ╡шДСуАВ%n%nшжБчлЛхН│щЗНхРпхРЧя╝Я +ShowReadmeCheck=цШпя╝МцИСцГ│цЯещШЕшЗкш┐░цЦЗф╗╢ +YesRadio=цШпя╝МчлЛхН│щЗНхРпчФ╡шДС(&Y) +NoRadio=хРжя╝МчиНхРОщЗНхРпчФ╡шДС(&N) +; used for example as 'Run MyProg.exe' +RunEntryExec=ш┐РшбМ %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=цЯещШЕ %1 + +; *** тАЬхоЙшгЕчиЛх║ПщЬАшжБф╕Лф╕Ах╝ачгБчЫШтАЭцПРчд║ +ChangeDiskTitle=хоЙшгЕчиЛх║ПщЬАшжБф╕Лф╕Ах╝ачгБчЫШ +SelectDiskLabel2=шп╖цПТхЕечгБчЫШ %1 х╣╢чВ╣хЗ╗тАЬчбохоЪтАЭуАВ%n%nхжВцЮЬш┐Щф╕кчгБчЫШф╕нчЪДцЦЗф╗╢хПпф╗ехЬиф╕ЛхИЧцЦЗф╗╢хд╣ф╣ЛхдЦчЪДцЦЗф╗╢хд╣ф╕нцЙ╛хИ░я╝Мшп╖ш╛УхЕецнгчбочЪДш╖пх╛ДцИЦчВ╣хЗ╗тАЬц╡ПшзИтАЭуАВ +PathLabel=ш╖пх╛Д(&P)я╝Ъ +FileNotInDir2=тАЬ%2тАЭф╕нцЙ╛ф╕НхИ░цЦЗф╗╢тАЬ%1тАЭуАВшп╖цПТхЕецнгчбочЪДчгБчЫШцИЦщАЙцЛйхЕ╢ф╗ЦцЦЗф╗╢хд╣уАВ +SelectDirectoryLabel=шп╖цМЗхоЪф╕Лф╕Ах╝ачгБчЫШчЪДф╜Нч╜оуАВ + +; *** хоЙшгЕщШ╢цо╡ц╢ИцБп +SetupAborted=хоЙшгЕчиЛх║ПцЬкхоМцИРхоЙшгЕуАВ%n%nшп╖ф┐оцнгш┐Щф╕кщЧощвШх╣╢щЗНцЦ░ш┐РшбМхоЙшгЕчиЛх║ПуАВ +AbortRetryIgnoreSelectAction=щАЙцЛйцУНф╜Ь +AbortRetryIgnoreRetry=щЗНшпХ(&T) +AbortRetryIgnoreIgnore=х┐╜чХещФЩшппх╣╢ч╗зч╗н(&I) +AbortRetryIgnoreCancel=хЕ│щЧнхоЙшгЕчиЛх║П +RetryCancelSelectAction=щАЙцЛйцУНф╜Ь +RetryCancelRetry=щЗНшпХ(&T) +RetryCancelCancel=хПЦц╢И(&C) + +; *** хоЙшгЕчК╢цАБц╢ИцБп +StatusClosingApplications=цнгхЬихЕ│щЧнх║ФчФичиЛх║П... +StatusCreateDirs=цнгхЬихИЫх╗║чЫох╜Х... +StatusExtractFiles=цнгхЬицПРхПЦцЦЗф╗╢... +StatusDownloadFiles=цнгхЬиф╕Лш╜╜цЦЗф╗╢... +StatusCreateIcons=цнгхЬихИЫх╗║х┐лцН╖цЦ╣х╝П... +StatusCreateIniEntries=цнгхЬихИЫх╗║ INI цЭбчЫо... +StatusCreateRegistryEntries=цнгхЬихИЫх╗║ц│ихЖМшбицЭбчЫо... +StatusRegisterFiles=цнгхЬиц│ихЖМцЦЗф╗╢... +StatusSavingUninstall=цнгхЬиф┐ЭхнШхН╕ш╜╜ф┐бцБп... +StatusRunProgram=цнгхЬихоМцИРхоЙшгЕ... +StatusRestartingApplications=цнгхЬищЗНхРпх║ФчФичиЛх║П... +StatusRollback=цнгхЬицТдщФАцЫ┤цФ╣... + +; *** хЕ╢ф╗ЦщФЩшпп +ErrorInternal2=хЖЕщГищФЩшппя╝Ъ%1 +ErrorFunctionFailedNoCode=%1 хд▒ш┤е +ErrorFunctionFailed=%1 хд▒ш┤ея╝ЫщФЩшппф╗гчаБ %2 +ErrorFunctionFailedWithMessage=%1 хд▒ш┤ея╝ЫщФЩшппф╗гчаБ %2.%n%3 +ErrorExecutingProgram=цЧац│ХцЙзшбМцЦЗф╗╢я╝Ъ%n%1 + +; *** ц│ихЖМшбищФЩшпп +ErrorRegOpenKey=цЙУх╝Ац│ихЖМшбищб╣цЧ╢хЗ║щФЩя╝Ъ%n%1\%2 +ErrorRegCreateKey=хИЫх╗║ц│ихЖМшбищб╣цЧ╢хЗ║щФЩя╝Ъ%n%1\%2 +ErrorRegWriteKey=хЖЩхЕец│ихЖМшбищб╣цЧ╢хЗ║щФЩя╝Ъ%n%1\%2 + +; *** INI щФЩшпп +ErrorIniEntry=хЬицЦЗф╗╢тАЬ%1тАЭф╕нхИЫх╗║ INI цЭбчЫоцЧ╢хЗ║щФЩуАВ + +; *** цЦЗф╗╢хдНхИ╢щФЩшпп +FileAbortRetryIgnoreSkipNotRecommended=ш╖│ш┐ЗцндцЦЗф╗╢(&S) (ф╕НцОишНР) +FileAbortRetryIgnoreIgnoreNotRecommended=х┐╜чХещФЩшппх╣╢ч╗зч╗н(&I) (ф╕НцОишНР) +SourceIsCorrupted=ц║РцЦЗф╗╢х╖▓цНЯхЭП +SourceDoesntExist=ц║РцЦЗф╗╢тАЬ%1тАЭф╕НхнШхЬи +SourceVerificationFailed=ц║РцЦЗф╗╢щкМшпБхд▒ш┤е: %1 +VerificationSignatureDoesntExist=чн╛хРНцЦЗф╗╢тАЬ%1тАЭф╕НхнШхЬи +VerificationSignatureInvalid=чн╛хРНцЦЗф╗╢тАЬ%1тАЭцЧацХИ +VerificationKeyNotFound=чн╛хРНцЦЗф╗╢тАЬ%1тАЭф╜┐чФиф║ЖцЬкчЯехпЖщТе +VerificationFileNameIncorrect=цЦЗф╗╢хРНф╕Нцнгчбо +VerificationFileTagIncorrect=цЦЗф╗╢цаЗчн╛ф╕Нцнгчбо +VerificationFileSizeIncorrect=цЦЗф╗╢хдзх░Пф╕Нцнгчбо +VerificationFileHashIncorrect=цЦЗф╗╢хУИх╕МхА╝ф╕Нцнгчбо +ExistingFileReadOnly2=цЧац│ХцЫ┐цНвчО░цЬЙцЦЗф╗╢я╝МхоГцШпхПкшп╗чЪДуАВ +ExistingFileReadOnlyRetry=чз╗щЩдхПкшп╗х▒ЮцАзх╣╢щЗНшпХ(&R) +ExistingFileReadOnlyKeepExisting=ф┐ЭчХЩчО░цЬЙцЦЗф╗╢(&K) +ErrorReadingExistingDest=х░ЭшпХшп╗хПЦчО░цЬЙцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +FileExistsSelectAction=щАЙцЛйцУНф╜Ь +FileExists2=цЦЗф╗╢х╖▓ч╗ПхнШхЬиуАВ +FileExistsOverwriteExisting=шжЖчЫЦх╖▓хнШхЬичЪДцЦЗф╗╢(&O) +FileExistsKeepExisting=ф┐ЭчХЩчО░цЬЙчЪДцЦЗф╗╢(&K) +FileExistsOverwriteOrKeepAll=ф╕║цЙАцЬЙхЖ▓чкБцЦЗф╗╢цЙзшбМцндцУНф╜Ь(&D) +ExistingFileNewerSelectAction=щАЙцЛйцУНф╜Ь +ExistingFileNewer2=чО░цЬЙчЪДцЦЗф╗╢цпФхоЙшгЕчиЛх║Пх░ЖшжБхоЙшгЕчЪДцЦЗф╗╢ш┐ШшжБцЦ░уАВ +ExistingFileNewerOverwriteExisting=шжЖчЫЦх╖▓хнШхЬичЪДцЦЗф╗╢(&O) +ExistingFileNewerKeepExisting=ф┐ЭчХЩчО░цЬЙчЪДцЦЗф╗╢(&K) (цОишНР) +ExistingFileNewerOverwriteOrKeepAll=ф╕║цЙАцЬЙхЖ▓чкБцЦЗф╗╢цЙзшбМцндцУНф╜Ь(&D) +ErrorChangingAttr=х░ЭшпХцЫ┤цФ╣ф╕ЛхИЧчО░цЬЙцЦЗф╗╢чЪДх▒ЮцАзцЧ╢хЗ║щФЩя╝Ъ +ErrorCreatingTemp=х░ЭшпХхЬичЫоцаЗчЫох╜ХхИЫх╗║цЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorReadingSource=х░ЭшпХшп╗хПЦф╕ЛхИЧц║РцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorCopying=х░ЭшпХхдНхИ╢ф╕ЛхИЧцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorDownloading=ф╕Лш╜╜цЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorExtracting=шзгхОЛхОЛч╝йцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorReplacingExistingFile=х░ЭшпХцЫ┐цНвчО░цЬЙцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorRestartReplace=щЗНхРпх╣╢цЫ┐цНвхд▒ш┤ея╝Ъ +ErrorRenamingTemp=х░ЭшпХщЗНхС╜хРНф╕ЛхИЧчЫоцаЗчЫох╜Хф╕нчЪДф╕Аф╕кцЦЗф╗╢цЧ╢хЗ║щФЩя╝Ъ +ErrorRegisterServer=цЧац│Хц│ихЖМ DLL/OCXя╝Ъ%1 +ErrorRegSvr32Failed=RegSvr32 хд▒ш┤ея╝ЫщААхЗ║ф╗гчаБ %1 +ErrorRegisterTypeLib=цЧац│Хц│ихЖМч▒╗х║Уя╝Ъ%1 + +; *** хН╕ш╜╜цШ╛чд║хРНхнЧцаЗшо░ +; used for example as 'My Program (32-bit)' +UninstallDisplayNameMark=%1 (%2) +; used for example as 'My Program (32-bit, All users)' +UninstallDisplayNameMarks=%1 (%2, %3) +UninstallDisplayNameMark32Bit=32 ф╜Н +UninstallDisplayNameMark64Bit=64 ф╜Н +UninstallDisplayNameMarkAllUsers=цЙАцЬЙчФицИ╖ +UninstallDisplayNameMarkCurrentUser=х╜УхЙНчФицИ╖ + +; *** хоЙшгЕхРОщФЩшпп +ErrorOpeningReadme=х░ЭшпХцЙУх╝АшЗкш┐░цЦЗф╗╢цЧ╢хЗ║щФЩуАВ +ErrorRestartingComputer=хоЙшгЕчиЛх║ПцЧац│ХщЗНхРпчФ╡шДСя╝Мшп╖цЙЛхКищЗНхРпуАВ + +; *** хН╕ш╜╜ц╢ИцБп +UninstallNotFound=цЦЗф╗╢тАЬ%1тАЭф╕НхнШхЬиуАВцЧац│ХхН╕ш╜╜уАВ +UninstallOpenError=цЦЗф╗╢тАЬ%1тАЭф╕НшГ╜швлцЙУх╝АуАВцЧац│ХхН╕ш╜╜уАВ +UninstallUnsupportedVer=цндчЙИцЬмчЪДхН╕ш╜╜чиЛх║ПцЧац│ХшпЖхИлхН╕ш╜╜цЧех┐ЧцЦЗф╗╢тАЬ%1тАЭчЪДца╝х╝ПуАВцЧац│ХхН╕ш╜╜ +UninstallUnknownEntry=хН╕ш╜╜цЧех┐Чф╕нщБЗхИ░ф╕Аф╕кцЬкчЯецЭбчЫо (%1) +ConfirmUninstall=цВичбошодшжБхоМхЕичз╗щЩд %1 хПКхЕ╢цЙАцЬЙч╗Дф╗╢хРЧя╝Я +UninstallOnlyOnWin64=ф╗ЕхЕБшо╕хЬи 64 ф╜Н Windows ф╕нхН╕ш╜╜цндчиЛх║ПуАВ +OnlyAdminCanUninstall=ф╗Еф╜┐чФичобчРЖхСШцЭГщЩРчЪДчФицИ╖шГ╜хоМцИРцндхН╕ш╜╜уАВ +UninstallStatusLabel=цнгхЬиф╗ОцВичЪДчФ╡шДСф╕нчз╗щЩд %1я╝Мшп╖чиНхАЩуАВ +UninstalledAll=х╖▓щб║хИйф╗ОцВичЪДчФ╡шДСф╕нчз╗щЩд %1уАВ +UninstalledMost=%1 хН╕ш╜╜хоМцИРуАВ%n%nцЬЙщГихИЖхЖЕхо╣цЬкшГ╜швлхИащЩдя╝Мф╜ЖцВихПпф╗ецЙЛхКихИащЩдхоГф╗муАВ +UninstalledAndNeedsRestart=ф╕║хоМцИР %1 чЪДхН╕ш╜╜я╝МщЬАшжБщЗНхРпцВичЪДчФ╡шДСуАВ%n%nчлЛхН│щЗНхРпчФ╡шДСхРЧя╝Я +UninstallDataCorrupted=цЦЗф╗╢тАЬ%1тАЭх╖▓цНЯхЭПуАВцЧац│ХхН╕ш╜╜ + +; *** хН╕ш╜╜чК╢цАБц╢ИцБп +ConfirmDeleteSharedFileTitle=хИащЩдхЕ▒ф║лчЪДцЦЗф╗╢хРЧя╝Я +ConfirmDeleteSharedFile2=ч│╗ч╗Яшбичд║ф╕ЛхИЧхЕ▒ф║лчЪДцЦЗф╗╢х╖▓ф╕НцЬЙхЕ╢ф╗ЦчиЛх║Пф╜┐чФиуАВцВих╕МцЬЫхН╕ш╜╜чиЛх║ПхИащЩдш┐Щф║ЫхЕ▒ф║лчЪДцЦЗф╗╢хРЧя╝Я%n%nхжВцЮЬхИащЩдш┐Щф║ЫцЦЗф╗╢я╝Мф╜Жф╗НцЬЙчиЛх║ПхЬиф╜┐чФиш┐Щф║ЫцЦЗф╗╢я╝МхИЩш┐Щф║ЫчиЛх║ПхПпшГ╜хЗ║чО░х╝Вх╕╕уАВхжВцЮЬцВиф╕НшГ╜чбохоЪя╝Мшп╖щАЙцЛйтАЬхРжтАЭя╝МхЬич│╗ч╗Яф╕нф┐ЭчХЩш┐Щф║ЫцЦЗф╗╢ф╗ехЕНх╝ХхПСщЧощвШуАВ +SharedFileNameLabel=цЦЗф╗╢хРНя╝Ъ +SharedFileLocationLabel=ф╜Нч╜оя╝Ъ +WizardUninstalling=хН╕ш╜╜чК╢цАБ +StatusUninstalling=цнгхЬихН╕ш╜╜ %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp=цнгхЬихоЙшгЕ %1уАВ +ShutdownBlockReasonUninstallingApp=цнгхЬихН╕ш╜╜ %1уАВ + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 чЙИцЬм %2 +AdditionalIcons=щЩДхКах┐лцН╖цЦ╣х╝Пя╝Ъ +CreateDesktopIcon=хИЫх╗║цбМщЭвх┐лцН╖цЦ╣х╝П(&D) +CreateQuickLaunchIcon=хИЫх╗║х┐лщАЯхРпхКицаПх┐лцН╖цЦ╣х╝П(&Q) +ProgramOnTheWeb=%1 ч╜СчлЩ +UninstallProgram=хН╕ш╜╜ %1 +LaunchProgram=ш┐РшбМ %1 +AssocFileExtension=х░Ж %2 цЦЗф╗╢цЙйх▒ХхРНф╕О %1 х╗║члЛхЕ│шБФ(&A) +AssocingFileExtension=цнгхЬих░Ж %2 цЦЗф╗╢цЙйх▒ХхРНф╕О %1 х╗║члЛхЕ│шБФ... +AutoStartProgramGroupDescription=хРпхКия╝Ъ +AutoStartProgram=шЗкхКихРпхКи %1 +AddonHostProgramNotFound=цВищАЙцЛйчЪДцЦЗф╗╢хд╣ф╕нцЧац│ХцЙ╛хИ░ %1уАВ%n%nцВишжБч╗зч╗нхРЧя╝Я diff --git a/setup-scripts/Languages/Unofficial/Hindi.islu b/setup-scripts/Languages/Unofficial/Hindi.islu new file mode 100644 index 0000000..5c4f946 --- /dev/null +++ b/setup-scripts/Languages/Unofficial/Hindi.islu @@ -0,0 +1,336 @@ +я╗┐ ; *** Inno Setup version 5.5.3+ Hindi messages *** +; Translated by Him Prasad Gautam [ drishtibachak at gmail.com ] +; To download user-contributed translations of this file, go to: +; http://www.jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=<0939><093F><0902><0926><0940> +LanguageID=$0439 +LanguageCodePage=0 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +;DialogFontName= +;DialogFontSize=10 +;WelcomeFontName= +WelcomeFontSize=12 +;TitleFontName= +TitleFontSize=35 +;CopyrightFontName= +CopyrightFontSize=9 + +[Messages] + +; *** Application titles +SetupAppTitle=рд╕реНрдерд╛рдкрдирд╛ +SetupWindowTitle=рд╕реНрдерд╛рдкрдирд╛ - %1 +UninstallAppTitle=рдирд┐рд╕реНрдХрд╛рд╕рди +UninstallAppFullTitle=%1 рдХрд┐ рдирд┐рд╕реНрдХрд╛рд╕рди + +; *** Misc. common +InformationTitle=рд╕реБрдЪрдирд╛ +ConfirmTitle=рдкреБрд╖реНрдЯрд┐рдХрд░рдг +ErrorTitle=рддреНрд░реБрдЯреА + +; *** SetupLdr messages +SetupLdrStartupMessage=рдЗрд╕ рд╕реЗ %1 рдЖрдкрдХрд┐ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╣реЛрдЧрд╛. рдХреНрдпрд╛ рдЖрдк рдЖрдЧреЗ рдмреЭрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +LdrCannotCreateTemp=рдЕрд╕реНрдерд╛рдИ реЮрд╛рдЗрд▓ рдирд╣реА рдмрдирд╛ рдкрд╛ рд░рд╣рд╛. рд╕реНрдерд╛рдкрдирд╛ рдХреЛ рдмрд┐рдЪ рдореЗрдВ рд╣реА рд░реЛрдХрдирд╛ рдкреЬрд╛. +LdrCannotExecTemp=рдЕрд╕реНрдерд╛рдИ рдлреЛрд▓реНрдбрд░ рдореЗрдВ рд╕реЗ реЮрд╛рдЗрд▓ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдирд╣реА рдХрд░ рдкрд╛рдпрд╛. рд╕реНрдерд╛рдкрдирд╛ рдХреЛ рдмрд┐рдЪ рдореЗрдВ рд╣реА рд░реЛрдХрдирд╛ рдкреЬрд╛. + +; *** Startup error messages +LastErrorMessage=%1.%n%nрддреНрд░реБрдЯреА %2: %3 +SetupFileMissing=реЮрд╛рдЗрд▓ %1 рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛ рдореЗрдВ рдирд╣реА рд╣реИ. рдХреГрдкрдпрд╛ рдпрд╛ рддреЛ рд╕рдорд╕реНрдпрд╛ рдХрд╛ рдирд┐рджрд╛рди рдХреАрдЬрд┐рдпреЗ рдпрд╛ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреА рдирдИ рдкреНрд░рддрд┐ рд▓рд╛рдЗрдП. +SetupFileCorrupt=рд╕реНрдерд╛рдкрдирд╛ рдлрд╛рдЗрд▓ рдореЗрдВ рддреНрд░реБрдЯреА рд╣реИ. рдХреГрдкрдпрд╛ рдирдИ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреА рдкреНрд░рддрд┐ рд▓рд╛рдЗрдП. +SetupFileCorruptOrWrongVer=рд╕реНрдерд╛рдкрдирд╛ рдлрд╛рдЗрд▓ рдореЗрдВ рддреНрд░реБрдЯреА рд╣реИ рдпрд╛ рддреЛ рдЕрд▓рдЧ рдкреНрд░рдХрд╛рд░ рдХрд┐ рд╣реИ. рдХреГрдкрдпрд╛ рд╕рдорд╕реНрдпрд╛-рдирд┐рджрд╛рди рдХрд░реЗ рдпрд╛ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреА рдирдИ рдкреНрд░рддрд┐ рд▓рд╛рдЗрдП. +InvalidParameter=рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рд╡реИрдз рдирд╣реА рд╣реИ. +SetupAlreadyRunning=рд╕реНрдерд╛рдкрдирд╛ рддреЛ рдкрд╣рд▓реЗ рд╕реЗ рд╣рд┐ рдЪрд▓ рд░рд╣рд╛ рд╣реИ +WindowsVersionNotSupported=рдЗрд╕ рд╕реЗ [name/ver] рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╣реЛрдЧрд╛.%n%nрдпреЗ рдмрд╣реЗрддрд░ рд╣реЛрдЧрд╛ рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣реЗрд▓реЗ рдЖрдк рдЕрдиреНрдп рд╕рднреА рдХрд╛рд░реНрдпрдХреНрд░рдо рд╣рд╛рд▓ рддреБрд░рдд рдХреЗ рд▓рд┐рдП рдмрдВрдз рдХрд░ рджреЗ. +WindowsServicePackRequired=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЛ %1 Service Pack %2 рдпрд╛ рдкрд┐рдЫрд▓рд╛ рд╕рдВрд╕реНрдХрд░рдг рдЪрд╛рд╣рд┐рдП. +NotOnThisPlatform=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо %1 рдкреЗ рдирд╣реА рдЪрд▓реЗрдЧрд╛. +OnlyOnThisPlatform=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЗрд╡рд▓ %1 рдкреЗ рд╣реА рдЪрд▓реЗрдЧрд╛. +OnlyOnTheseArchitectures=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЗрд╡рд▓ рдЗрди рдкреНрд░реЛрд╕реЗрд╕рд░ :%n%n%1 рд╕реЗ рдЕрдиреБрд░реВрдк рд╡рд┐рдиреНрдбреЛреЫ рдкреНрд▓реЗрдЯрдлреЙрд░реНрдо рдкреЗ рд╣реА рдЪрд▓реЗрдЧрд╛. +MissingWOW64APIs=рдЖрдкрдХрд╛ рд╡рд┐рдВрдбреЛ рдкреНрд▓реЗрдЯрдлреЙрд░реНрдо 64-bit рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╕рдорд░реНрдерди рдирд╣реА рдХрд░рддрд╛. рдХреГрдкрдпрд╛ рд╕рд░реНрд╡рд┐рд╕ рдкреИрдХ %1 рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░реЗ. +WinVersionTooLowError=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдЪрд▓рдиреЗ рдХреЗ рд▓рд┐рдП %1 рд╕рдВрд╕реНрдХрд░рдг %2 рдпрд╛ рдЙрд╕ рд╕реЗ рдкрд┐рдЫрд▓рд╛ рдЪрд╛рд╣рд┐рдП. +WinVersionTooHighError=рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдирд╣реА рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ %1 рд╕рдВрд╕реНрдХрд░рдг %2 рдпрд╛ рдкрд┐рдЫрд▓рд╛ рдкреЗ. +AdminPrivilegesRequired=рдЕрдЧрд░ рдЖрдк рдкреНрд░рд╢рд╛рд╕рдХ рдЦрд╛рддреЗ рд╕реЗ рдЖрд░рдореНрдн рдХрд░реЗ рддреЛ рд╣реА рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░ рдкрд╛рдУрдЧреЗ. +PowerUserPrivilegesRequired=рдЖрдк рдкреНрд░рд╢рд╛рд╕рдХ рдЦрд╛рддреЗ рдпрд╛ рд╢рдХреНрддрд┐-рдкреНрд░рдпреЛрдЧ рдХрд░реНрддрд╛ рд╕рдореВрд╣ рдХреЗ рдЦрд╛рддреЗ рд╕реЗ рдЖрд░рдореНрдн рдХрд░реЗ рддреЛ рд╣реА рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░ рдкрд╛рдУрдЧреЗ. +SetupAppRunningError=рд╕реНрдерд╛рдкрдирд╛ рдиреЗ рдкрдХреЬрд╛ рдХреА %1 рд╣рд╛рд▓ рдЪрд╛рд▓реВ рд╣реИ..%n%n рдХреГрдкрдпрд╛ рдЙрд╕реЗ рдмрдВрдз рдХрд░реЗ рдЕрднреА, рдФрд░ рдмрд╛рдж рдореЗрдВ рдЖрдЧреЗ рдмрдврдиреЗ рд╡рд╛рд╕реНрддреЗ рдареАрдХ рдпрд╛ рдирд┐рдХрд▓ рдЬрд╛рдиреЗ рд╡рд╛рд╕реНрддреЗ рд░рджреНрдж рдХрд░реЗрдБ рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. +UninstallAppRunningError=рдирд┐рд╕реНрдХрд╛рд╕рди рдХреЛ рдпреЗ рдЬреНрдЮрд╛рдд рд╣реБрдЖ рдХреА %1 рдЕрднреА рдЪрд╛рд▓реВ рд╣реИ.%n%n рдХреГрдкрдпрд╛ рдЙрд╕реЗ рдмрдВрдз рдХрд░реЗ рдФрд░ рдлрд┐рд░ рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рдареАрдХ рдпрд╛ рдмрд╛рд╣рд░ рдЬрд╛рдиреЗ рдХреЗ рд▓рд┐рдП рд░рджреНрдж рдХрд░реЗрдБ рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. + +; *** Misc. errors +ErrorCreatingDir=рд╕реНрдерд╛рдкрдирд╛ "%1" рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛ рдмрдирд╛рдиреЗ рдореЗрдВ рд╡рд┐рдлрд▓ рд░рд╣рд╛ +ErrorTooManyFilesInDir=%1 рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛ рдореЗрдВ рдмрд╣реБрдд рдлрд╛рдЗрд▓ рдореМрдЬреВрдж рд╣реЛрдиреЗ рдХреЗ рд╡рдЬрд╣ рд╕реЗ рд╕реНрдерд╛рдкрдирд╛ рдлрд╛рдЗрд▓ рдмрдирд╛рдиреЗ рдореЗрдВ рд╡рд┐рдлрд▓ рд░рд╣рд╛. + +; *** Setup common messages +ExitSetupTitle=рд╕реНрдерд╛рдкрдирд╛ рдХрд┐ рдмрд╣рд┐рд░реНрдЧрдорди +ExitSetupMessage=рд╕реНрдерд╛рдкрдирд╛ рдХрд┐ рдХрд╛рд░реНрдп рдкреВрд░реНрдг рдирд╣реА рд╣реБрдЖ, рдпрджрд┐ рдЖрдк рдЕрднреА рдмрд╛рд╣рд░ рдЬрд╛рдиреЗ рдХрд┐ рдИрд░рд╛рдзрд╛ рдХрд░реЗрдВрдЧреЗ рддреЛ рдХрд╛рд░реНрдпрдХреНрд░рдо рд╕рд╣рд┐ рдврдВрдЧ рд╕реЗ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдирд╣реА рд╣реЛрдЧрд╛.%n%nрдЖрдк рдХрд┐рд╕реА рдУрд░ рд╡рдХреНрдд рдлрд┐рд░ рд╕реЗ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░ рд╕рдХрддреЗ рд╣реЛ.%n%nрдХреНрдпрд╛ рдмрд╛рд╣рд░ рдЬрд╛рдП? +AboutSetupMenuItem=рд╕реНрдерд╛рдкрдирд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ... +AboutSetupTitle=рд╕реНрдерд╛рдкрдирд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ +AboutSetupMessage=%1 рд╕рдВрд╕реНрдХрд░рдг %2%n%3%n%n%1 рдЧреГрд╣ рдкреГрд╖реНрда:%n%4 +AboutSetupNote= +TranslatorNote= рдпрд╣ рд╣рд┐рдиреНрджреА рдореЗрдВ рдЕрдиреБрд╡рд╛рдж рдХрд┐ рдХрд╛рд░реНрдп рд╣рд┐рдо рдкреНрд░рд╕рд╛рдж рдЧреМрддрдо рдиреЗ рдХрд┐рдпрд╛ рд╣реИ. + +; *** Buttons +ButtonBack=< &рдкрд┐рдЫреЗ рд╣рдЯреЛ +ButtonNext=&рдЖрдЧреЗ рдмрдвреЛ > +ButtonInstall=&рдЕрдзрд┐рд╖реНрдард╛рдкрди +ButtonOK=&рдареАрдХ +ButtonCancel=&рд░рджреНрдж рдХрд░реЗрдБ +ButtonYes=&рд╣рд╛рдБ +ButtonYesToAll=&рд╕рднреА рдХреЗ рд▓рд┐рдП рд╣рд╛рдБ +ButtonNo=&рдирд╣реА +ButtonNoToAll=рд╕&рднреА рдХреЗ рд▓рд┐рдП рдирд╣реА +ButtonFinish=&рд╕рдорд╛рдкреНрдд +ButtonBrowse=&рдмреНрд░рд╛рдЙреЫ... +ButtonWizardBrowse=&рдмреНрд░рд╛рдЙреЫ... +ButtonNewFolder=&рдирдпрд╛ рдлреЛрд▓реНрдбрд░ рдмрдирд╛рдП + +; "Select Language" dialog messages +SelectLanguageTitle=рд╕реНрдерд╛рдкрдирд╛ рднрд╛рд╖рд╛ рдЪрдпрди +SelectLanguageLabel=рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХреЗ рджрд░рдореНрдпрд╛рди рдЗрд╕реНрддреЗрдорд╛рд▓ рд╣реЛрдиреЗ рд╡рд╛рд▓реА рднрд╛рд╖рд╛ рдЪрдпрди рдХрд░реЗ: + +; *** Common wizard text +ClickNext=рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рдЖрдЧреЗ рдмрдвреЛ рджрдмрд╛рдП, рдпрд╛ рдмрд╛рд╣рд░ рдЬрд╛рдиреЗ рдХреЗ рд╡рд╛рд╕реНрддреЗ рд░рджреНрдж рдХрд░реЗрдБ рджрдмрд╛рдП. +BeveledLabel= рд╕реМрдЬрдиреНрдпрдГ рд╣рд┐рдо рдкреНрд░рд╕рд╛рдж рдЧреМрддрдо +BrowseDialogTitle=рдлреЛрд▓реНрдбрд░ рдХреЗ рд▓рд┐рдП рдмреНрд░рд╛рдЙреЫ рдХрд░реЗ +BrowseDialogLabel=рдиреАрдЪреЗ рдХреА рд╕реБрдЪреА рдореЗрдВ рд╕реЗ рдПрдХ рдлреЛрд▓реНрдбрд░ рдЪрдпрди рдХрд░рдХреЗ рдареАрдХ рджрдмрд╛рдП. +NewFolderName=рдирдпрд╛ рдлреЛрд▓реНрдбрд░ + +; "Welcome" wizard page +WelcomeLabel1=рдпрд╣ [name] рдХрд┐ рд╕реНрдерд╛рдкрдирд╛ рд╣реЛ рд░рд╣реА рд╕рдорд╛рд░реЛрд╣ рдореЗрдВ рдЖрдкрдХрд╛ рд╕реНрд╡рд╛рдЧрдд рд╣реИ +WelcomeLabel2=рдЗрд╕ рд╕реЗ [name/ver] рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╣реЛрдЧрд╛.%n%nрдпреЗ рдмрд╣реЗрддрд░ рд╣реЛрдЧрд╛ рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЖрдк рдЕрдиреНрдп рд╕рднреА рдЦреБрд▓рд┐ рд╣реБрдИ рдХрд╛рд░реНрдпрдХреНрд░рдо рд╣рд╛рд▓ рдХреЗ рд▓рд┐рдП рдмрдВрдз рдХрд░ рджреЗ. + +; "Password" wizard page +WizardPassword=рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж +PasswordLabel1=рдпреЗ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж рд╕реЗ рд▓реЛрдХ рд╣реИ. +PasswordLabel3=рдХреГрдкрдпрд╛ рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж рд▓рд┐рдЦреЗрдВ рдФрд░ рдмрд╛рдж рдореЗрдВ 'рдЖрдЧреЗ рдмрдвреЛ' рдмрдЯрди рджрдмрд╛рдП. рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж case-рд╕рдореНрд╡реЗрджрдирд╕реАрд▓ рд╣реИ. +PasswordEditLabel=рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж: +IncorrectPassword=рдЖрдкрдиреЗ рд▓рд┐рдЦрд╛ рд╣реБрдЖ рдЦреБрдкрд┐рдпрд╛рд╢рдмреНрдж рдЧрд▓рдд рд╣реИ. рдХреГрдкрдпрд╛ рдлрд┐рд░ рд╕реЗ рдХреЛрд╢рд┐рд╢ рдХрд░реЗ. + +; "License Agreement" wizard page +WizardLicense=рдЗрдЬрд╛рдЬрдд рдХрд░рд╛рд░ +LicenseLabel=рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣реЗрд▓реЗ рдпреЗ рдорд╣рддреНрд╡рдкреВрд░реНрдг рд╕реВрдЪрдирд╛рдП рдкрдвреЗ. +LicenseLabel3=рдпреЗ рдЗрдЬрд╛рдЬрдд рдХрд░рд╛рд░ рдкрдвреЗ. рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣реЗрд▓реЗ рдЖрдкрдХреЛ рдЗрд╕рдХреА рд╢рд░реНрддреЛрдВ рдХреЛ рдорд╛рдирдирд╛ рд╣реА рд╣реЛрдЧрд╛. +LicenseAccepted=рд╣рд╛рдБ рдореБрдЭреЗ рдпреЗ рдХрд░рд╛рд░рдирд╛рдорд╛ рдХрдмреВрд▓ рд╣реИ. +LicenseNotAccepted=рдирд╣реА рдореБрдЭреЗ рдпреЗ рдХрд░рд╛рд░рдирд╛рдорд╛ рдХрдмреВрд▓ рдирд╣реА рд╣реИ. + +; "Information" wizard pages +WizardInfoBefore=рд╕реБрдЪрдирд╛ +InfoBeforeLabel=рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣реЗрд▓реЗ рдпреЗ рдорд╣рддреНрд╡рдкреВрд░реНрдг рд╕реВрдЪрдирд╛рдП рдкрдвреЗ. +InfoBeforeClickLabel=рдЬрдм рдЖрдк рддрдпрд╛рд░ рд╣реЛ, 'рдЖрдЧреЗ рдмрдвреЛ' рдмрдЯрди рджрдмрд╛рдП. +WizardInfoAfter=рд╕реБрдЪрдирд╛ +InfoAfterLabel=рдЖрдЧреЗ рдмрдврдиреЗ рд╕реЗ рдкрд╣реЗрд▓реЗ рдпреЗ рдорд╣рддреНрд╡рдкреВрд░реНрдг рд╕реВрдЪрдирд╛рдП рдкрдвреЗ. +InfoAfterClickLabel=рдЬрдм рдЖрдк рддрдпрд╛рд░ рд╣реЛ, 'рдЖрдЧреЗ рдмрдвреЛ' рдмрдЯрди рджрдмрд╛рдП. + +; "User Information" wizard page +WizardUserInfo=рдкреНрд░рдпреЛрдЧ рдХрд░реНрддрд╛ рдХреА рдЬрд╛рдирдХрд╛рд░реА +UserInfoDesc=рдХреГрдкрдпрд╛ рдЖрдкрдХреА рдЬрд╛рдирдХрд╛рд░реА рдЕрдВрджрд░ рдбрд╛рд▓реЗ. +UserInfoName=рдкреНрд░рдпреЛрдЧ рдХрд░реНрддрд╛ рдХрд╛ рдирд╛рдо: +UserInfoOrg=рд╕рдВрд╕реНрдерд╛: +UserInfoSerial=рдХреНрд░рдорд╛рдЩреНрдХ +UserInfoNameRequired=рдЖрдкрдХреЛ рдирд╛рдо рддреЛ рдбрд╛рд▓рдирд╛ рд╣реА рд╣реЛрдЧрд╛. + +; "Select Destination Location" wizard page +WizardSelectDir=рд▓рдХреНрд╖реНрдп рдкрде рдЪрдпрди рдХрд░реЗ +SelectDirDesc=[name] рдХреЛ рдХрд┐рдзрд░ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдирд╛ рд╣реИ? +SelectDirLabel3=рд╕реНрдерд╛рдкрдирд╛ [name] рдХреЛ рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдлреЛрд▓реНрдбрд░ рдореЗрдВ рдбрд╛рд▓реЗрдЧрд╛. +SelectDirBrowseLabel=рдЖрдЧреЗ рдмрдврдиреЗ рд╡рд╛рд╕реНрддреЗ рдЖрдЧреЗ рдмрдвреЛ рджрдмрд╛рдП. рдпрджрд┐ рдЕрдиреНрдп рдлреЛрд▓реНрдбрд░ рдЪрдпрди рдХрд░рдирд╛ рд╣реИ рддреЛ рдмреНрд░рд╛рдЙреЫ рджрдмрд╛рдП. +DiskSpaceMBLabel=рдХрдорд╕реЗрдХрдо [mb] MB рдЬрд┐рддрдиреА рдЬрдЧрд╣ рддреЛ рдЬрд░реВрд░реА рд╣реЛрдЧреА. +CannotInstallToNetworkDrive=рд╢реНрдерд╛рдкрдирд╛ рдиреЗ рдиреЗрдЯрд╡рд░реНрдХ рдбреНрд░рд╛рдЗрдн рдирд╣рд┐ рд░рдЦ рдкрд╛рдпрд╛. +CannotInstallToUNCPath=рд╕реНрдерд╛рдкрдирд╛ рдиреЗ UNC path рдирд╣рд┐ рд░рдЦ рдкрд╛рдпрд╛. +InvalidPath=рдЖрдкрдХреЛ рдбреНрд░рд╛рдЗрд╡ рдЕрдХреНрд╖рд░ рдХреЗ рд╕рд╛рде рдкреВрд░реНрдг рдкрде рджреЗрдирд╛ рд╣реЛрдЧрд╛ рдЙрджрд╛рд╣рд░рдг:%n%nC:\APP%n%n рдпрд╛ рддреЛ UNC рд░рд╛рд╕реНрддрд╛ рдпрд╣ рд░реВрдк рдореЗрдВ:%n%n\\server\share +InvalidDrive=рдЬреЛ drive рдпрд╛ UNC share рдЖрдкрдиреЗ рдЪрдпрди рдХреА рд╣реИ рдЙрд╕ рдореЗ рд╣рдо рдкрд╣реБрдБрдЪ рдирд╣реА рдХрд░ рдкрд╛ рд░рд╣реЗ рдХреГрдкрдпрд╛ рдЕрдиреНрдп рдЪрдпрди рдХрд░реЗ. +DiskSpaceWarningTitle=рдЬрд░реВрд░реА рдЬрдЧрд╣ рдирд╣реА рд╣реИ. +DiskSpaceWarning=рд╕реНрдерд╛рдкрдирд╛ рдХрдо рд╕реЗ рдХрдо %1 KB рдЬрдЧрд╣ рдордВрдЧрддрд╛ рд╣реИ, рд▓реЗрдХрд┐рди рдЪрдпрдирд┐рдд рдбреНрд░рд╛рдЗрд╡ рдореЗрдВ рддреЛ рдХреЗрд╡рд▓ %2 KB рд╣реА рдореМрдЬреВрдж рд╣реИ.%n%nрдХреНрдпрд╛ рдЖрдк рдлрд┐рд░ рднреА рдЖрдЧреЗ рдмреЭрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реЛ? +DirNameTooLong=рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рдпрд╛ рдкрде рдмрд╣реЛрдд рд▓рдВрдмрд╛ рд╣реИ. +InvalidDirName=рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рд╡реИрдз рдирд╣реА рд╣реИ. +BadDirName32=рдлреЛрд▓реНрдбрд░ рдирд╛рдо рдореЗрдВ рдпреЗ рдЕрдХреНрд╖рд░ рдирд╣реА рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд░ рд╕рдХрддреЗ:%n%n%1 +DirExistsTitle=рдлреЛрд▓реНрдбрд░ рдореМрдЬреВрдж рд╣реИ +DirExists=рдлреЛрд▓реНрдбрд░:%n%n%1%n%nрдкрд╣реЗрд▓реЗ рд╕реЗ рд╣реА рдореМрдЬреВрдж рд╣реИ, рдХреНрдпрд╛ рдЖрдк рдлрд┐рд░ рднреА рдЙрд╕рдореЗ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +DirDoesntExistTitle=рдлреЛрд▓реНрдбрд░ рдореМрдЬреВрдж рдирд╣реА рд╣реИ +DirDoesntExist=рдлреЛрд▓реНрдбрд░:%n%n%1%n%nрдореМрдЬреВрдж рдирд╣реА рд╣реИ. рдХреНрдпрд╛ рдЖрдк рдпреЗ рдлреЛрд▓реНрдбрд░ рдмрдирд╛рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? + +; "Select Components" wizard page +WizardSelectComponents=рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдБ рдкрд╕рдВрдж рдХрд░реЗ. +SelectComponentsDesc=рдХреЛрдирд╕реЗ рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдБ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдиреЗ рд╣реИ? +SelectComponentsLabel2=рдЬреЛ рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдБ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдирд╛ рд╣реИ, рдЙрдиреНрд╣реЗрдВ рдЪрдпрди рдХрд░реЗ; рдЬрд┐рдиреНрд╣реЗрдВ рдирд╣реА рдХрд░рдирд╛ рд╣реЛ рддреЛ рдЙрдиреНрд╣реЗрдВ рд╕рд╛рдл рдХрд░реЗ. рдЬрдм рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рддрдпрд╛рд░ рд╣реЛ рддреЛ рдЖрдЧреЗ рдмрдвреЛ рджрдмрд╛рдП. +FullInstallation=рд╕рдореНрдкреВрд░реНрдг рдЕрдзрд┐рд╖реНрдард╛рдкрди +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=рдордЬрдмреБрдд рдЕрдзрд┐рд╖реНрдард╛рдкрди +CustomInstallation=рд░рд┐рд╡рд╛рдЬреА рдЕрдзрд┐рд╖реНрдард╛рдкрди. +NoUninstallWarningTitle=рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдБ рдореМрдЬреВрдж рд╣реИ. +NoUninstallWarning=рд╕реНрдерд╛рдкрдирд╛ рдХреЛ рдпреЗ рдЬреНрдЮрд╛рдд рд╣реБрдЖ рд╣реИ рдХреА рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдБ рдкрд╣реЗрд▓реЗ рд╕реЗ рд╣реА рдореЛрдЬреВрдж рд╣реИ.:%n%n%1%n%nрдЗрдиреНрд╣реЗрдВ рдбреА-рдЪрдпрди рдХрд░рдиреЗ рд╕реЗ рд╡реЗ рдирд┐рд╕реНрдХрд╛рд╕рди рдирд╣реА рд╣реЛрдЧреЗ.%n%nрдХреНрдпрд╛ рдЖрдк рдРрд╕реЗ рд╣реА рдЖрдЧреЗ рдмреЭрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceMBLabel=рдЗрд╕ рдЪрдпрди рдХреЗ рд╕рд╛рде рд╕реНрдерд╛рдкрдирд╛ рд╡рд╛рд╕реНрддреЗ [mb] MB рдЬрдЧрд╣ рдЪрд╛рд╣рд┐рдП. + +; "Select Additional Tasks" wizard page +WizardSelectTasks=рдЕрддрд┐рд░рд┐рдХреНрдд рдХрд╛рдо рдЪрдпрди рдХрд░реЗ. +SelectTasksDesc=рдХреЛрди рд╕реЗ рдЕрддрд┐рд░рд┐рдХреНрдд рдХрд╛рдо рдХрд░рдиреЗ рд╣реИ? +SelectTasksLabel2=[name] рдХреЛ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рддреЗ рд╡рдХреНрдд рдЬреЛ рдЕрддрд┐рд░рд┐рдХреНрдд рдХрд╛рдо рдХрд░рдиреЗ рд╣реИ рдЙрдиреНрд╣реЗрдВ рдЪрдпрди рдХрд░реЗ рдФрд░ рдмрд╛рдж рдореЗрдВ рдЖрдЧреЗ рдмрдвреЛ рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. + +; "Select Start Menu Folder" wizard page +WizardSelectProgramGroup=рд╕реБрд░реБ рдореЗрдиреВ рдлреЛрд▓реНрдбрд░ рдЪрдпрди рдХрд░реЗ. +SelectStartMenuFolderDesc=рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЗ рдЫреЛрдЯреАрд░рд╛рд╕реНрддрд╛ рдХрд┐рдзрд░ рд░рдЦрдиреЗ рд╣реИ? +SelectStartMenuFolderLabel3=рд╕реНрдерд╛рдкрдирд╛ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЗ рдЫреЛрдЯреАрд░рд╛рд╕реНрддрд╛ рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рд╕реБрд░реБ-рдореЗрдиреВ рдлреЛрд▓реНрдбрд░ рдореЗрдВ рдбрд╛рд▓реЗрдЧрд╛. +SelectStartMenuFolderBrowseLabel=рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рдЖрдЧреЗ рдмрдвреЛ рджрдмрд╛рдП. рдпрджрд┐ рдЕрд▓рдЧ рдлреЛрд▓реНрдбрд░ рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдирд╛ рд╣реИ рддреЛ Browse рджрдмрд╛рдП. +MustEnterGroupName=рдЖрдкрдХреЛ рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рддреЛ рдбрд╛рд▓рдирд╛ рд╣реА рд╣реЛрдЧрд╛. +GroupNameTooLong=рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рдпрд╛ рдкрде рдмрд╣реБрдд рд▓рдВрдмрд╛ рд╣реИ. +InvalidGroupName=рдлреЛрд▓реНрдбрд░ рдХрд╛ рдирд╛рдо рд╡реИрдз рдирд╣реА рд╣реИ. +BadGroupName=рдлреЛрд▓реНрдбрд░ рдирд╛рдо рдореЗрдВ рдпреЗ рд╡рд╛рд▓реЗ рдЕрдХреНрд╖рд░ рдирд╣реА рдбрд╛рд▓ рд╕рдХрддреЗ:%n%n%1 +NoProgramGroupCheck2=рд╕реБрд░реБ рдореЗрдиреВ рдлреЛрд▓реНрдбрд░ рдирд╣реА рдмрдирд╛рдирд╛ рд╣реИ. + +; "Ready to Install" wizard page +WizardReady=рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХреЗ рд▓рд┐рдП рддрдпрд╛рд░ +ReadyLabel1=рд╕реНрдерд╛рдкрдирд╛ рдЕрдм [name] рдХреЛ рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рддрдпрд╛рд░ рд╣реИ. +ReadyLabel2a=рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рдЕрдзрд┐рд╖реНрдард╛рдкрди рджрдмрд╛рдП, рдЕрдЧрд░ рдХреЛрдИ рдмрджрд▓рд╛рд╡ рдХрд░рдирд╛ рд╣реИ рддреЛ рдкрд┐рдЫреЗ рд╣рдЯреЛ рджрдмрд╛рдП. +ReadyLabel2b=рдЕрдзрд┐рд╖реНрдард╛рдкрди рдореЗрдВ рдЖрдЧреЗ рдмрдврдиреЗ рдХреЗ рд▓рд┐рдП рдЕрдзрд┐рд╖реНрдард╛рдкрди рджрдмрд╛рдП. +ReadyMemoUserInfo=рдкреНрд░рдпреЛрдЧ рдХрд░реНрддрд╛ рдХреА рд╕реВрдЪрдирд╛рдП: +ReadyMemoDir=рд▓рдХреНрд╖реНрдп рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛: +ReadyMemoType=рд╕реНрдерд╛рдкрдирд╛ рдХрд╛ рдкреНрд░рдХрд╛рд░: +ReadyMemoComponents=рдЪрдпрди рдХрд┐рдпреЗ рд╕рд╣рдпреЛрдЧрд┐рдпреЛрдВ: +ReadyMemoGroup=рд╕реБрд░реБ рдореЗрдиреВ рдлреЛрд▓реНрдбрд░: +ReadyMemoTasks=рдЕрддрд┐рд░рд┐рдХреНрдд рдХрд╛рдо: + +; "Preparing to Install" wizard page +WizardPreparing=рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХреЗ рд▓рд┐рдП рддреИрдпрд╛рд░реА рдХрд░ рд░рд╣рд╛ рд╣реИ. +PreparingDesc=рд╕реНрдерд╛рдкрдирд╛ [name] рдХреЛ рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рдбрд╛рд▓рдиреЗ рдХреА рддреИрдпрд╛рд░реАрдХрд░ рд░рд╣рд╛ рд╣реИ. +PreviousInstallNotCompleted=рдкрд┐рдЫрд▓реЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХрд╛ рдкреНрд░рддрд┐рд╕реНрдерд╛рдкрди / рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╕рд╣реА рдврдВрдЧ рд╕реЗ рдкреВрд░рд╛ рдирд╣реА рд╣реБрдЖ рдерд╛. рдЖрдкрдХреЛ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдирд╛ рдкрдбреЗрдЧрд╛.%n%nрдХрд▓реНрдкрдпрдиреНрддреНрд░ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдиреЗ рдкрд╢реНрдЪрд╛рдд рдЖрдк рдлрд┐рд░ рд╕реЗ [name] рдХрд╛ рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╢реБрд░реВ рдХрд░реЗ. +CannotContinue=рд╕реНрдерд╛рдкрдирд╛ рдЖрдЧреЗ рдирд╣реА рдмреЭ рд╕рдХрддрд╛, рдХреГрдкрдпрд╛ рд░рджреНрдж рдХрд░реЗрдБ рдмрдЯрди рджрдмрд╛рдПрдБ. +ApplicationsFound=рдирд┐рдЪреЗ рд╡рд╛рд▓реА рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛ рдиреЗ рд╕реНрдерд╛рдкрдирд╛ рджреНрд╡рд╛рд░рд╛ рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛ рдЬрд╛рдиреЗ рд╡рд╛рд▓рд╛ рдлрд╛рдЗрд▓реЛрдВ рдХреЛ рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд┐рдпрд╛ рд╣реИ. рдЖрдкрдХреЛ рдпрд╣ рдорд╕рд╡рд░рд╛ рджрд┐рдпрд╛ рдЬрд╛ рддрд╛ рд╣реИ рдХрд┐ рдЖрдк рд╕реНрдерд╛рдкрдирд╛ рдХреЛ рдпрд╣ рдЕрдиреБрдкреНрд░рдпреЛрдЧреМрдВ рдХрд┐ рдЦреБрдж рд╣реА рдмрдиреНрдж рдХрд░рдиреЗ рдХрд┐ рдЗрдЬрд╛рдЬрдд рдкреНрд░рджрд╛рди рдХрд░реЗрдВ. +ApplicationsFound2=рдпрд╣ рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛ рдиреЗ рд╕реНрдерд╛рдкрдирд╛ рджреНрд╡рд╛рд░рд╛ рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛ рдЬрд╛рдиреЗ рд╡рд╛рд▓рд╛ рдлрд╛рдЗрд▓реЛрдВ рдХреЛ рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд┐рдпрд╛ рд╣реИ. рдЖрдкрдХреЛ рдпрд╣ рдорд╕рд╡рд░рд╛ рджрд┐рдпрд╛ рдЬрд╛ рддрд╛ рд╣реИ рдХрд┐ рдЖрдк рд╕реНрдерд╛рдкрдирд╛ рдХреЛ рдпрд╣ рдЕрдиреБрдкреНрд░рдпреЛрдЧреМрдВ рдХреЛ рдЦреБрдж рд╣реА рдмрдиреНрдж рдХрд░рдиреЗ рдХрд┐ рдЗрдЬрд╛рдЬрдд рдкреНрд░рджрд╛рди рдХрд░реЗрдВ. рдЕрдзрд┐рд╖реНрдард╛рдкрди рдЦрддрдо рд╣реЛрдиреЗ рдХреЗ рд╡рд╛рдж, рд╕реНрдерд╛рдкрдирд╛ рдпрд╣ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХреЛрдВ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдиреЗ рдХрд┐ рдХреЛрд╕рд┐рд╕ рдХрд░реЗрдЧрд╛. +CloseApplications=&рдЦреБрдж рд╣рд┐ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХреЛрдВ рдмрдиреНрдж рдХрд░реЗрдВ +DontCloseApplications=рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХреЛрдВ рдмрдиреНрдж &рдирд╣рд┐ рдХрд░реЗрдВ +ErrorCloseApplications=рд╕реНрдерд╛рдкрдирд╛ рдЦреБрдж рд╣рд┐ рд╕рднреА рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛрдВ рдХреЛ рдмрдиреНрдж рдирд╣рд┐ рдХрд░ рд╕рдХрд╛. рдЖрдкрдХреЛ рдпрд╣ рдорд╕рд╡рд░рд╛ рджрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рдХрд┐ рд╕реНрдерд╛рдкрдирд╛ рдиреЗ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рд╡рд╛рд▓реА рдлрд╛рдЗрд▓реЛрдВ рдХреЛ рдЗрд╕реНрддрдорд╛рд▓ рдХрд░ рд░рд╣реЗ рдЕрдиреБрдкреНрд░рдпреЛрдЧреМрдВ рдХреЛ рдЖрдЧреЗ рдмрдвреНрдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЖрдк рдЦреБрдж рд╣реА рдмрдиреНрдж рдХрд░реЗрдВ. + +; "Installing" wizard page +WizardInstalling=рдЕрдзрд┐рд╖реНрдард╛рдкрди рд╣реЛ рд░рд╣рд╛ рд╣реИ. +InstallingLabel=рдЬрдм рддрдХ рд╕реНрдерд╛рдкрдирд╛ рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ [name] рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░рддрд╛ рд╣реИ, рдЙрд╕ рд╡рдЦреНрдд рддрдХ рдХреГрдкрдпрд╛ рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗ. + +; "Setup Completed" wizard page +FinishedHeadingLabel=[name] рд╕реНрдерд╛рдкрдирд╛ рдХрд┐ рдХрд╛рд░реНрдп рдкреВрд░рд╛ рд╣реЛ рд░рд╣рд╛ рд╣реИ. +FinishedLabelNoIcons=рд╕реНрдерд╛рдкрдирд╛ рдиреЗ [name] рдХреЛ рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░ рджрд┐рдпрд╛ рд╣реИ. +FinishedLabel=рд╕реНрдерд╛рдкрдирд╛ рдиреЗ [name] рдЖрдкрдХреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░рдореЗрдВ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХрд░ рджрд┐рдпрд╛ рд╣реИ. рдЖрдк рдЙрдкрдпреБрдХреНрдд рдкреНрд░рддрд┐рдорд╛ рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░ рдХреЗ рдХрднреА рднреА рдпреЗ рдХрд╛рд░реНрдпрдХреНрд░рдо рд╢реБрд░реВ рдХрд░ рд╕рдХрддреЗ рд╣реИ. +ClickFinish=рд╕реНрдерд╛рдкрдирд╛ рд╕реЗ рдмрд╛рд╣рд░ рдирд┐рдХрд▓рдиреЗ рд╡рд╛рд╕реНрддреЗ рд╕рдорд╛рдкреНрдд рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. +FinishedRestartLabel=[name] рдХрд╛ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдкреВрд░рд╛ рдХрд░рдиреЗ рд╡рд╛рд╕реНрддреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдирд╛ рдмреЗрд╣рдж рдЬрд░реВрд░реА рд╣реИ. %n%nрдХреНрдпрд╛ рдЖрдк рдЕрднреА рд░рд┐рд╕реБрд░реБ рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +FinishedRestartMessage=[name] рдХрд╛ рдЕрдзрд┐рд╖реНрдард╛рдкрди рдкреВрд░рд╛ рдХрд░рдиреЗ рд╣реЗрддреБ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдирд╛ рдмреЗрд╣рдж рдЬрд░реВрд░реА рд╣реИ.%n%nрдХреНрдпрд╛ рдЖрдк рдЕрднреА рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +ShowReadmeCheck=рд╣рд╛рдБ рдореБрдЭреЗ рд╣рдореЗрдВ рдкрдвреЛ file рджреЗрдЦрдиреА рд╣реИ. +YesRadio=&рд╣рд╛рдБ, рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░ рджреЛ. +NoRadio=&рдирд╣реА рдореИ рдЕрдкрдирд╛ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рд╕реНрд╡рдпрдВ рдмрд╛рдж рдореЗрдВ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░реВрдБрдЧрд╛. +; used for example as 'Run MyProg.exe' +RunEntryExec=рд░рди %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=рджреЗрдЦреЗ %1 + +; "Setup Needs the Next Disk" stuff +ChangeDiskTitle=рд╕реНрдерд╛рдкрдирд╛ рдХреЗ рд▓рд┐рдП рдЕрдЧрд▓реА рдбрд┐рд╕реНрдХ рдЪрд╛рд╣рд┐рдП. +SelectDiskLabel2=рдХреГрдкрдпрд╛ рдбрд┐рд╕реНрдХ %1 рдбрд╛рд▓рдХреЗ рдареАрдХ рджрдмрд╛рдП.%n%nрдпрджрд┐ рдЗрд╕ рдбрд┐рд╕реНрдХ рдХреА рдлрд╛рдЗрд▓ рдирд╣реА рдорд┐рд▓рддреА рддреЛ рд╕рд╣реА рдкрде рдмрддрд╛рдП рдпрд╛ рдмреНрд░рд╛рдЙреЫ рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. +PathLabel=рдкрде: +FileNotInDir2=рдлрд╛рдЗрд▓ "%1" рдХреЛ "%2" рдореЗрдВ рдвреБрдв рдирд╣реА рдкрд╛рдП. рдХреГрдкрдпрд╛ рд╕рд╣реА рдбрд┐рд╕реНрдХ рдбрд╛рд▓реЗ рдпрд╛ рдЕрд▓рдЧ рдлреЛрд▓реНрдбрд░ рдЪрдпрди рдХрд░реЗ. +SelectDirectoryLabel=рдЕрдЧрд▓реА рдбрд┐рд╕реНрдХ рдХрд╛ рдкрддрд╛ рдмрддрд╛рдП. + +; *** Installation phase messages +SetupAborted=рд╕реНрдерд╛рдкрдирд╛ рдкреВрд░рд╛ рдирд╣реА рд╣реЛ рдкрд╛рдпрд╛.%n%nрдХреГрдкрдпрд╛ рддреНрд░реБрдЯреА рдареАрдХ рдХрд░реЗ рдФрд░ рдлрд┐рд░ рд╕реЗ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗ. +EntryAbortRetryIgnore=рдлрд┐рд░ рд╕реЗ рдкреНрд░рдпрд╛рд╕ рдХрд░рдиреЗ рд╡рд╛рд╕реНрддреЗ Retry рджрдмрд╛рдП, рдпрджрд┐ рдРрд╕реЗ рд╣реА рдЖрдЧреЗ рдмреЭрдирд╛ рд╣реИ рддреЛ Ignore рджрдмрд╛рдП, рдпрд╛ рддреЛ рд╕реНрдерд╛рдкрдирд╛ рд░рджреНрдж рдХрд░реЗрдБ рдХрд░рдиреЗ рд╡рд╛рд╕реНрддреЗ Abort рджрдмрд╛рдП. + +; *** Installation status messages +StatusClosingApplications=рдЕрдиреБрдкреНрд░рдпреЛрдЧрдХреЛрдВ рдмрдиреНрдж рдХрд┐рдпрд╛ рдЬрд╛ рд░рд╣рд╛ рд╣реИ. +StatusCreateDirs=рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛ рдмрдирд╛ рд░рд╣рд╛ рд╣реИ... +StatusExtractFiles=рдлрд╛рдЗрд▓ рдЙрддреНрдЦрдирди рдХрд░ рд░рд╣рд╛ рд╣реИ... +StatusCreateIcons=рдЫреЛрдЯреАрд░рд╛рд╕реНрддрд╛ рдмрдирд╛ рд░рд╣рд╛ рд╣реИ... +StatusCreateIniEntries=INI рдПрдВрдЯреНрд░реА рдмрдирд╛ рд░рд╣рд╛ рд╣реИ... +StatusCreateRegistryEntries=рдкрдЮреНрдЬреАрдХрд╛ рдПрдВрдЯреНрд░реА рдмрдирд╛ рд░рд╣рд╛ рд╣реИ... +StatusRegisterFiles=рдлрд╛рдЗрд▓ рдкрдЮреНрдЬрд┐рдХреГрдд рдХрд░ рд░рд╣рд╛ рд╣реИ... +StatusSavingUninstall=рдирд┐рд╕реНрдХрд╛рд╕рди рдХреА рд╕реБрдЪрдирд╛рдП рдмрдЪрддрдХрд░ рд░рд╣рд╛ рд╣реИ... +StatusRunProgram=рдЕрдзрд┐рд╖реНрдард╛рдкрди рдкреВрд░рд╛ рдХрд░ рд░рд╣рд╛ рд╣реИ... +StatusRestartingApplications=рдЕрдиреБрдкреНрд░рдпреЛрдЧрдХреЛрдВ рдХрд┐ рдлрд┐рд░ рд╕реБрд░реБрд╡рд╛рдд +StatusRollback=рдмрджрд▓рд╛рд╡реЛрдВ рдХреЛ рдкрд┐рдЫреЗ рд╣рдЯреНрдиреЗ рдХрд┐ рдХрд╛рдо рдХрд░ рд░рд╣рд╛ рд╣реИ... + +; *** Misc. errors +ErrorInternal2=рдЖрдВрддрд░рд┐рдХ рддреНрд░реБрдЯреА: %1 +ErrorFunctionFailedNoCode=%1 рд╡рд┐рдлрд▓ +ErrorFunctionFailed=%1 рд╡рд┐рдлрд▓; рдХреЛрдб %2 +ErrorFunctionFailedWithMessage=%1 рд╡рд┐рдлрд▓; рдХреЛрдб %2.%n%3 +ErrorExecutingProgram=рдлрд╛рдЗрд▓ рдХреЛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдирд╣реА рдХрд░ рдкрд╛ рд░рд╣рд╛:%n%1 + +; *** Registry errors +ErrorRegOpenKey=рдкрдЮреНрдЬреАрдХрд╛ рдХреБрдЮреНрдЬреА рдЦреЛрд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА:%n%1\%2 +ErrorRegCreateKey=рдкрдЮреНрдЬреАрдХрд╛ рдХреБрдЮреНрдЬреА рдмрдирд╛рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА:%n%1\%2 +ErrorRegWriteKey=рдкрдЮреНрдЬреАрдХрд╛ рдХреБрдЮреНрдЬреА рдореЗрдВ рд▓рд┐рдЦрддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА:%n%1\%2 + +; *** INI errors +ErrorIniEntry=реЮрд╛рдЗрд▓ "%1" рдореЗрдВ INI рдПрдВрдЯреНрд░реА рдбрд╛рд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА. + +; *** File copying errors +FileAbortRetryIgnore=рдлрд┐рд░ рд╕реЗ рдкреНрд░рдпрд╛рд╕ рдХрд░рдиреЗ рд╣реЗрддреБ Retry рдмрдЯрди рджрдмрд╛рдП, рдпрджрд┐ рдРрд╕реЗ рд╣реА рдЖрдЧреЗ рдмреЭрдирд╛ рд╣реИ рддреЛ Ignore рджрдмрд╛рдП(рд╣рдо рдРрд╕рд╛ рд╕реБрдЬрд╛рд╡ рдирд╣реА рджреЗрддреЗ),рдпрд╛ рддреЛ Abort рджрдмрд╛рдП рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХреЛ рд░рджреНрдж рдХрд░реЗрдБ рдХрд░рдиреЗ рд╣реЗрддреБ. +FileAbortRetryIgnore2=рдлрд┐рд░ рд╕реЗ рдкреНрд░рдпрд╛рд╕ рдХрд░рдиреЗ рд╣реЗрддреБ Retry рдмрдЯрди рджрдмрд╛рдП, рдпрджрд┐ рдРрд╕реЗ рд╣реА рдЖрдЧреЗ рдмреЭрдирд╛ рд╣реИ рддреЛ Ignore рджрдмрд╛рдП(рд╣рдо рдРрд╕рд╛ рд╕реБрдЬрд╛рд╡ рдирд╣реА рджреЗрддреЗ),рдпрд╛ рддреЛ Abort рджрдмрд╛рдП рдЕрдзрд┐рд╖реНрдард╛рдкрди рдХреЛ рд░рджреНрдж рдХрд░реЗрдБ рдХрд░рдиреЗ рд╣реЗрддреБ. +SourceIsCorrupted=рд╢реНрд░реЛрдд реЮрд╛рдЗрд▓ рдореЗрдВ рдЧрдбрдмрдб рд╣реИ. +SourceDoesntExist=рд╢реНрд░реЛрдд рдлрд╛рдЗрд▓ "%1" рдореМрдЬреВрдж рд╣реА рдирд╣реА рд╣реИ. +ExistingFileReadOnly=рдореМрдЬрджрд╛ реЮрд╛рдЗрд▓ рд╕рд┐рд░реНрдл-рдкрдвреЛ рд╣реИ.%n%n рдЖрдк Retry рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ, рдЙрд╕рдХрд╛ рд╕рд┐рд░реНрдл-рдкрдвреЛ attribute рд╣рдЯрд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдФрд░ рдлрд┐рд░ рджреЛрдмрд╛рд░рд╛ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗ. рдпрджрд┐ рдЗрд╕ рдлрд╛рдЗрд▓ рдХреЛ рдЫреЛреЬ рджреЗрдирд╛ рд╣реИ рддреЛ Ignore, рдФрд░ рдпрджрд┐ рдЕрдзрд┐рд╖реНрдард╛рдкрди рд░рджреНрдж рдХрд░реЗрдБ рдХрд░рдирд╛ рд╣реИ рддреЛ Abort рдмрдЯрди рджрдмрд╛рдП. +ErrorReadingExistingDest=рдореМрдЬрджрд╛ рдлрд╛рдЗрд▓ рдХреЛ рдкрдврддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА: +FileExists=рдлрд╛рдЗрд▓ рдкрд╣реЗрд▓реЗ рд╕реЗ рдореМрдЬреВрдж рд╣реИ.%n%nрдХреНрдпрд╛ рдЖрдк рдЙрд╕рдХреЛ рдУрд╡рд░-рд░рд╛рдИрдЯ рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реЛ? +ExistingFileNewer=рдореМрдЬреВрджрд╛ рдлрд╛рдЗрд▓ рд╕реНрдерд╛рдкрдирд╛ реЮрд╛рдЗрд▓ рд╕реЗ рдирдИ рд╣реИ. рд╣рдорд╛рд░рд╛ рд╕реБрдЬрд╛рд╡ рд╣реИ рдХреА рдЖрдк рдЗрд╕реЗ рд░рдЦреЗ.%n%nрдХреНрдпрд╛ рдЖрдк реЮрд╛рдЗрд▓ рдХреЛ рд░рдЦрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ? +ErrorChangingAttr=рдореМрдЬреВрджрд╛ рдлрд╛рдЗрд▓ рдХреЗ рдПрдЯреНрд░реАрдмреНрдпреВрдЯ рдмрджрд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА: +ErrorCreatingTemp=реЮрд╛рдЗрд▓ рдмрдирд╛рддреЗ рд╡рдЦреНрдд рддреНрд░реБрдЯреА: +ErrorReadingSource=рд╢реНрд░реЛрдд рдлрд╛рдЗрд▓ рдЦреЛрд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА: +ErrorCopying=реЮрд╛рдЗрд▓ рдкреНрд░рддрд┐ рдХрд░рдиреЗ рдХрд╛ рдкреНрд░рдпрд╛рд╕ рдХрд░рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА: +ErrorReplacingExistingFile=рдореМрдЬреВрдж рдлрд╛рдЗрд▓ рдХреЛ рдкреНрд░рддрд┐рд╕реНрдерд╛рдкрдирд╛ рдХрд░рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА: +ErrorRestartReplace=рдкреНрд░рддрд┐рд╕реНрдерд╛рдкрди рдХрд┐ рдлрд┐рд░ рд╕реЗ рд╕реБрд░реБрд╡рд╛рдд рд╡рд┐рдлрд▓ рд░рд╣рд╛: +ErrorRenamingTemp=рд╕рдЩреНрдЧреНрд░рд╣рд┐рдХрд╛ рдореЗрдВ рдлрд╛рдЗрд▓ рдХрд╛ рдирд╛рдо рдмрджрд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА рд╣реБрдИ: +ErrorRegisterServer=рдЗрд╕ рдХреЛ рдкрдЮреНрдЬрд┐рдХреГрдд рдирд╣реА рдХрд░ рдкрд╛ рд░рд╣рд╛ DLL/OCX: %1 +ErrorRegSvr32Failed=RegSvr32 рдЕрд╕рдлрд▓ рд╣реЛ рдЧрдпреА, рдмрд╛рд╣рд░ рдЬрд╛рдиреЗ рдХреЛрдб %1 рдХреЗ рд╕рд╛рде +ErrorRegisterTypeLib=рдЗрд╕ рдЯрд╛рдЗрдк рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХреЛ рдкрдВрдЬреАрдХреГрдд рдирд╣реА рдХрд░ рдкрд╛ рд░рд╣рд╛: %1 + +; *** Post-installation errors +ErrorOpeningReadme=рдореБрдЭреЗ рдкрдвреЛ реЮрд╛рдЗрд▓ рдЦреЛрд▓рддреЗ рд╡рдХреНрдд рддреНрд░реБрдЯреА рд╣реБрдИ. +ErrorRestartingComputer=рд╕реНрдерд╛рдкрдирд╛ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдХреЛ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдиреЗ рдореЗрдВ рдЕрд╕рдлрд▓ рд░рд╣рд╛. рдХреГрдкрдпрд╛ рдЖрдк рд╣реА рдЗрд╕рдХреЛ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░реЗ. + +; *** Uninstaller messages +UninstallNotFound=рдлрд╛рдЗрд▓ "%1" рдореМрдЬреВрдж рд╣реА рдирд╣реА рд╣реИ. рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд░рдирд╛ рдЕрд╕рдВрднрд╡. +UninstallOpenError=реЮрд╛рдЗрд▓ "%1" рдЦреБрд▓ рдирд╣реА рд░рд╣реА. рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд░рдирд╛ рдЕрд╕рдВрднрд╡. +UninstallUnsupportedVer=рдирд┐рд╕реНрдХрд╛рд╕рди рд▓реЛрдЧ реЮрд╛рдЗрд▓ "%1" рдЬрд┐рд╕ рдлреЛрд░реНрдореЗрдЯ рдореЗрдВ рд╣реИ рдЙрд╕реЗ рд╣рдо рдкрд╣рдЪрд╛рди рдирд╣реА рдкрд╛ рд░рд╣реЗ. рдЖрдЧреЗ рдмреЭрдирд╛ рдирд╛рдореБрдордХрд┐рди. +UninstallUnknownEntry=рдирд┐рд╕реНрдХрд╛рд╕рди рд▓реЛрдЧ рдореЗрдВ рдПрдХ рдЕрдЬреНрдЮрд╛рдд рдкреНрд░рд╡рд┐рд╖реНрдЯреА (%1)рдорд┐рд▓реА. +ConfirmUninstall=рдХреНрдпрд╛ рдкрдХреНрдХрд╛ рдЖрдк %1 рдХреЛ рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реЛ? +UninstallOnlyOnWin64=рдХреЗрд╡рд▓ 64-bit Windows рд╕реЗ рд╣реА рдЗрд╕реЗ рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ. +OnlyAdminCanUninstall=рдХреЗрд╡рд▓ рдкреНрд░рд╢рд╛рд╕рдХ рдЦрд╛рддреЛрдВ рд╕реЗ рд╣реА рдЗрд╕реЗ рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ.. +UninstallStatusLabel=рдЬрдм рддрдХ %1 рдирд╣реА рд╣рдбреНрддрд╛, рдзреИрд░реНрдп рд░рдЦреЗ. +UninstalledAll=%1 рд╕рдлрд▓рддрд╛рдкреВрд░реНрд╡рдХ рдирд┐рд╕реНрдХрд╛рд╕рди рд╣реБрдЖ. +UninstalledMost=%1 рдирд┐рд╕реНрдХрд╛рд╕рди рдкреВрд░рд╛ рд╣реБрдЖ.%n%nрдХреБрдЫ рддрддреНрд╡реЛрдВ рдХреЛ рдирд┐рдХрд╛рд▓ рдирд╣реА рдкрд╛рдП рд▓реЗрдХрд┐рди рдЖрдк рдЙрдиреНрд╣реЗрдВ рдЕрдкрдирд┐ рддрд░рд╣ рд╕реЗ рд╣рдЯрд╛ рд╕рдХрддреЗ рд╣реЛ. +UninstalledAndNeedsRestart=%1 рдХрд╛ рдирд┐рд╕реНрдХрд╛рд╕рди рдкреВрд░рд╛ рдХрд░рдиреЗ рд╡рд╛рд╕реНрддреЗ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдХреЛ рдлрд┐рд░ рд╕реБрд░реБ рдХрд░рдирд╛ рдЬрд░реВрд░реА рд╣реИ.%n%nрдХреНрдпрд╛ рдЕрднреА рдлрд┐рд░ рд╕реБрд░реБ рдХрд░реЗ? +UninstallDataCorrupted=%1 реЮрд╛рдЗрд▓ рдореЗрдВ рддреНрд░реБрдЯреА. рдирд┐рд╕реНрдХрд╛рд╕рди рдирд╛рдореБрдордХрд┐рди. + +; *** Uninstallation phase messages +ConfirmDeleteSharedFileTitle=рдХреНрдпрд╛ рд╢реЗрд░реЗрдб-рдлрд╛рдЗрд▓ рдХреЛ рдирд┐рдХрд╛рд▓ рджреЗрдирд╛ рд╣реИ? +ConfirmDeleteSharedFile2=рдкреНрд░рдгрд╛рд▓реА рд╕реЗ рдпреЗ рдЬреНрдЮрд╛рдд рд╣реЛрддрд╛ рд╣реИ рдХреА рдирд┐рдореНрдирд▓рд┐рдЦрд┐рддреА рд╢реЗрд░реЗрдб-реЮрд╛рдЗрд▓ рдЕрдм рдЖрдЧреЗ рдЗрд╕реНрддреЗрдорд╛рд▓ рдореЗрдВ рдирд╣реА рдЖрдПрдЧреА. рдХреНрдпрд╛ рдЖрдк рдЙрдиреНрд╣реЗрдВ рднреА рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ?%n%n рдпрджрд┐ рдХреЛрдИ рдЕрдиреНрдп рдХрд╛рд░реНрдпрдХреНрд░рдо рдЗрди рдлрд╛рдЗрд▓ рдкреЗ рдЖрдзрд╛рд░рд┐рдд рд╣реИ рддреЛ рд╡реЛ рд╢рд╛рдпрдж рдЗрдиреНрд╣реЗрдВ рдирд┐рдХрд╛рд▓ рджреЗрдиреЗ рдкрд░ рдврдВрдЧ рд╕реЗ рдХрд╛рдо рдирд╛ рднреА рдХрд░реЗ. рдпрджрд┐ рдЖрдк рдлреИрд╕рд▓рд╛ рдирд╣реА рдХрд░ рдкрд╛ рд░рд╣реЗ рддреЛ 'рдирд╣реА' рдкреЗ рдХреНрд▓рд┐рдХ рдХрд░реЗ. рдЗрди рдлрд╛рдЗрд▓ рдХреЛ рдХрд▓реНрдкрдпрдиреНрддреНрд░ рдореЗрдВ рдкреЬреЗ рд░рд╣реЗрдиреЗ рджреЛрдЧреЗ рддреЛ рднреА рдХреЛрдИ рдиреБрдХрд╕рд╛рди рдирд╣реА рд╣реЛрдЧрд╛. +SharedFileNameLabel=рдлрд╛рдЗрд▓ рдирд╛рдо: +SharedFileLocationLabel=рдкрддрд╛: +WizardUninstalling=рдирд┐рд╕реНрдХрд╛рд╕рди рд╕реНрдерд┐рддрд┐ +StatusUninstalling=рдирд┐рд╕реНрдХрд╛рд╕рди рд╣реЛ рд░рд╣рд╛ рд╣реИ %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp= %1 рдХрд┐ рдЕрдзрд┐рд╖реНрдардЖрдкрди рд╣реЛ рд░рд╣реА рд╣реИ. +ShutdownBlockReasonUninstallingApp=%1 рдХрд┐ рдирд┐рд╕реНрдХрд╛рд╕рди рд╣реЛ рд░рд╣реА рд╣реИ. + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 рд╕рдВрд╕реНрдХрд░рдг %2 +AdditionalIcons=рдЕрддрд┐рд░рд┐рдХреНрдд рдкреНрд░рддрд┐рдорд╛: +CreateDesktopIcon=рдбреЗрд╕реНрдХрдЯреЙрдк рдкреНрд░рддрд┐рдорд╛ рдмрдирд╛рдП +CreateQuickLaunchIcon=рдЬрд▓реНрджрд┐ рдЪрд▓реЛ рдкреНрд░рддрд┐рдорд╛ рдмрдирд╛рдП +ProgramOnTheWeb=%1 рдЗрдиреНрдЯрд░рдиреЗрдЯ рдкреЗ +UninstallProgram=рдирд┐рд╕реНрдХрд╛рд╕рди рдХрд░реЗ %1 +LaunchProgram=рд▓реЛрдВрдЪ рдХрд░реЗ %1 +AssocFileExtension=%1 рдХреЛ %2 реЮрд╛рдЗрд▓ рдПрдХреНрд╕рдЯреЗрдВрд╢рди рдХреЗ рд╕рд╛рде рдЖрдмрджреНрдз рдХрд░реЗ +AssocingFileExtension=%1 рдХреЛ %2 реЮрд╛рдЗрд▓ рдПрдХреНрд╕рдЯреЗрдВрд╢рди рдХреЗ рд╕рд╛рде рдЖрдмрджреНрдз рдХрд░ рд░рд╣рд╛ рд╣реИ.... +AutoStartProgramGroupDescription=рд╕реБрд░реБрд╡рд╛рдд +AutoStartProgram=%1 рдХреЛ %2 реЮрд╛рдЗрд▓ рдПрдХреНрд╕рдЯреЗрдВрд╢рди рдХреЗ рд╕рд╛рде рдЖрдмрджреНрдз рдХрд░ рд░рд╣рд╛ рд╣реИ.... +AddonHostProgramNotFound=рдЖрдкрдиреЗ рдЪрдпрди рдХрд┐рдпрд╛ рд╣реБрдЖрдлреЛрд▓реНрдбрд░ рдореЗрдВ %1 рдирд╣реА рдорд┐рд▓рд╛. %n%nрдХреНрдпрд╛ рдЖрдк рдХрд┐рд╕рд┐ рд╣рд╛рд▓рдд рдореЗрдВ рдпрд╕ рдХрд┐ рдирд┐рд░рдиреНрддрд░рддрд╛ рд░рдЦреНрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИ ? diff --git a/setup-scripts/Languages/Unofficial/Indonesian.isl b/setup-scripts/Languages/Unofficial/Indonesian.isl new file mode 100644 index 0000000..322b392 --- /dev/null +++ b/setup-scripts/Languages/Unofficial/Indonesian.isl @@ -0,0 +1,350 @@ +; *** Inno Setup version 6.4.0+ Indonesian messages *** +; +; Untuk mengunduh terjemahan kontribusi-pengguna dari berkas ini, buka: +; http://www.jrsoftware.org/files/istrans/ +; +; Alih bahasa oleh: MozaikTM (mozaik.tm@gmail.com) + +[LangOptions] +LanguageName=Bahasa Indonesia +LanguageID=$0421 +LanguageCodePage=0 + +[Messages] +SetupAppTitle=Instalasi +SetupWindowTitle=Instalasi - %1 +UninstallAppTitle=Pelepas +UninstallAppFullTitle=Pelepasan %1 + +InformationTitle=Informasi +ConfirmTitle=Konfirmasi +ErrorTitle=Galat + +SetupLdrStartupMessage=Kami akan memasang %1. Teruskan? +LdrCannotCreateTemp=Tidak dapat membuat berkas sementara. Batal memasang +LdrCannotExecTemp=Tidak dapat menjalankan berkas di direktori sementara. Batal memasang + +LastErrorMessage=%1.%n%nGalat %2: %3 +SetupFileMissing=Berkas %1 hilang dari direktori instalasi. Silakan koreksi masalah atau dapatkan salinan program yang baru. +SetupFileCorrupt=Berkas pemandu telah rusak. Silakan dapatkan salinan program yang baru. +SetupFileCorruptOrWrongVer=Berkas pemandu telah rusak, atau tidak cocok dengan versi pemandu ini. Silakan koreksi masalah atau dapatkan salinan program yang baru. +InvalidParameter=Parameter tak sah terdapat pada baris perintah: %n%n%1 +SetupAlreadyRunning=Pemandu sudah berjalan. +WindowsVersionNotSupported=Program ini tidak mendukung versi Windows yang berjalan pada komputer Anda. +WindowsServicePackRequired=Program ini memerlukan %1 Service Pack %2 atau yang terbaru. +NotOnThisPlatform=Program ini tidak akan berjalan pada %1. +OnlyOnThisPlatform=Program ini harus dijalankan pada %1. +OnlyOnTheseArchitectures=Program ini hanya bisa dipasang pada versi Windows yang didesain untuk arsitektur prosesor berikut:%n%n%1 +WinVersionTooLowError=Program ini memerlukan %1 versi %2 atau yang terbaru. +WinVersionTooHighError=Program ini tidak dapat dipasang pada %1 versi %2 atau yang terbaru. +AdminPrivilegesRequired=Anda harus masuk sebagai seorang administrator saat memasang program ini. +PowerUserPrivilegesRequired=Anda harus masuk sebagai seorang administrator atau anggota grup Power Users saat memasang program ini. +SetupAppRunningError=Kami mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar. +UninstallAppRunningError=Pelepas mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar. + +;Inno6 +PrivilegesRequiredOverrideTitle=Pilih Mode Instalasi +PrivilegesRequiredOverrideInstruction=Pilih mode instalasi +PrivilegesRequiredOverrideText1=%1 bisa dipasang untuk semua pengguna (perlu izin administratif), atau hanya Anda. +PrivilegesRequiredOverrideText2=%1 bisa dipasang hanya untuk Anda, atau semua pengguna (perlu izin administratif). +PrivilegesRequiredOverrideAllUsers=Pasang untuk &semua pengguna +PrivilegesRequiredOverrideAllUsersRecommended=Pasang untuk &semua pengguna (disarankan) +PrivilegesRequiredOverrideCurrentUser=Pasang &hanya untuk saya +PrivilegesRequiredOverrideCurrentUserRecommended=Pasang &hanya untuk saya (disarankan) +;Inno6 + +ErrorCreatingDir=Kami tidak dapat membuat direktori "%1" +ErrorTooManyFilesInDir=Tidak dapat membuat berkas di direktori "%1" karena berisi terlalu banyak berkas + +ExitSetupTitle=Keluar Pemandu +ExitSetupMessage=Instalasi tidak lengkap. Bila Anda keluar sekarang, program takkan terpasang.%n%nAnda bisa menjalankan Pemandu lagi lain kali untuk melengkapinya.%n%nKeluar? +AboutSetupMenuItem=&Tentang Pemandu... +AboutSetupTitle=Tentang Pemandu +AboutSetupMessage=%1 versi %2%n%3%n%n%1 laman beranda:%n%4 +AboutSetupNote= +TranslatorNote= + +ButtonBack=&Kembali +ButtonNext=&Maju +ButtonInstall=&Pasang +ButtonOK=Oke +ButtonCancel=Batal +ButtonYes=&Ya +ButtonYesToAll=Y&a semuanya +ButtonNo=&Tidak +ButtonNoToAll=T&idak semuanya +ButtonFinish=&Selesai +ButtonBrowse=&Cari... +ButtonWizardBrowse=C&ari... +ButtonNewFolder=&Buat Map Baru + +SelectLanguageTitle=Pilih Bahasa Pemandu +SelectLanguageLabel=Pilih bahasa untuk digunakan ketika memasang. + +ClickNext=Klik Maju untuk meneruskan, atau Batal untuk keluar. +BeveledLabel= +BrowseDialogTitle=Cari Map +BrowseDialogLabel=Pilih map dari daftar berikut, lalu klik OK. +NewFolderName=Map Baru + +WelcomeLabel1=Selamat datang di Pemandu Instalasi [name] +WelcomeLabel2=Kami akan memasang [name/ver] pada komputer Anda.%n%nDisarankan untuk menutup semua aplikasi lainnya sebelum meneruskan. + +WizardPassword=Kata Sandi +PasswordLabel1=Instalasi ini dilindungi kata sandi. +PasswordLabel3=Silakan masukkan kata sandi, lalu klik Maju untuk meneruskan. Kata sandi bersifat sensitif-kapitalisasi. +PasswordEditLabel=&Kata Sandi: +IncorrectPassword=Kata sandi yang Anda masukkan salah. Silakan coba lagi. + +WizardLicense=Kesepakatan Lisensi +LicenseLabel=Silakan baca informasi berikut sebelum meneruskan. +LicenseLabel3=Silakan baca Kesepakatan Lisensi berikut. Anda harus setuju dengan syarat dari kesepakatan ini sebelum meneruskan instalasi. +LicenseAccepted=Saya &setujui kesepakatan ini +LicenseNotAccepted=Saya &tidak setuju kesepakatan ini + +WizardInfoBefore=Informasi +InfoBeforeLabel=Silakan baca informasi penting berikut sebelum meneruskan. +InfoBeforeClickLabel=Saat Anda siap meneruskan instalasi, klik Maju. +WizardInfoAfter=Informasi +InfoAfterLabel=Silakan baca informasi penting berikut sebelum meneruskan. +InfoAfterClickLabel=Saat Anda siap meneruskan instalasi, klik Maju. + +WizardUserInfo=Informasi Pengguna +UserInfoDesc=Silakan masukkan informasi Anda. +UserInfoName=&Nama Pengguna: +UserInfoOrg=&Organisasi: +UserInfoSerial=&Nomor Seri: +UserInfoNameRequired=Wajib memasukkan nama. + +WizardSelectDir=Pilih Lokasi Tujuan +SelectDirDesc=Di manakah [name] sebaiknya dipasang? +SelectDirLabel3=Kami akan memasang [name] ke dalam map berikut. +SelectDirBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari. +;Inno6 +DiskSpaceGBLabel=Diperlukan sedikitnya [gb] GB ruang bebas. +;Inno6 +DiskSpaceMBLabel=Diperlukan sedikitnya [mb] MB ruang bebas. +CannotInstallToNetworkDrive=Kami tidak bisa memasang ke diska jaringan. +CannotInstallToUNCPath=Kami tidak bisa memasang ke alamat UNC. +InvalidPath=Anda wajib memasukkan alamat lengkap dengan huruf diska; contohnya:%n%nC:\APP%n%natau alamat UNC dalam bentuk:%n%n\\server\share +InvalidDrive=Diska atau alamat UNC yang Anda pilih tidak ada atau tidak dapat diakses. Silakan pilih yang lain. +DiskSpaceWarningTitle=Ruang Bebas Tidak Cukup +DiskSpaceWarning=Kami memerlukan sedikitnya %1 KB ruang bebas untuk memasang, namun diska yang Anda pilih hanya memiliki %2 KB tersedia.%n%nMaju terus? +DirNameTooLong=Nama map atau alamat terlalu panjang. +InvalidDirName=Nama map tidak sah. +BadDirName32=Nama map dilarang berisi karakter-karakter berikut:%n%n%1 +DirExistsTitle=Map Sudah Ada +DirExists=Map:%n%n%1%n%nsudah ada. Tetap pasang di map tersebut? +DirDoesntExistTitle=Map Tidak Ada +DirDoesntExist=Map:%n%n%1%n%ntidak ada. Buat map? + +WizardSelectComponents=Pilih Komponen +SelectComponentsDesc=Komponen mana sajakah yang sebaiknya dipasang? +SelectComponentsLabel2=Centang komponen yang Anda inginkan; hapus centang dari komponen yang tidak Anda inginkan. Klik Maju saat Anda siap meneruskan. +FullInstallation=Instalasi penuh +CompactInstallation=Instalasi padat +CustomInstallation=Instalasi kustom +NoUninstallWarningTitle=Komponen Terpasang +NoUninstallWarning=Kami mendeteksi bahwa komponen berikut telah terpasang pada komputer Anda:%n%n%1%n%nMembatalkan pilihan atas komponen berikut bukan berarti melepasnya.%n%nMaju terus? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +;Inno6 +ComponentsDiskSpaceGBLabel=Pilihan saat ini memerlukan sedikitnya [gb] GB ruang bebas. +;Inno6 +ComponentsDiskSpaceMBLabel=Pilihan saat ini memerlukan sedikitnya [mb] MB ruang bebas. + +WizardSelectTasks=Pilih Tugas Tambahan +SelectTasksDesc=Tugas tambahan mana sajakah yang sebaiknya dijalankan? +SelectTasksLabel2=Pilih tugas tambahan yang Anda ingin kami jalankan ketika memasang [name], lalu klik Maju. + +WizardSelectProgramGroup=Pilih Map Menu Start +SelectStartMenuFolderDesc=Di manakah sebaiknya kami letakkan pintasan program? +SelectStartMenuFolderLabel3=Kami akan membuat pintasan program di map Menu Start berikut. +SelectStartMenuFolderBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari. +MustEnterGroupName=Anda wajib memasukkan nama map. +GroupNameTooLong=Nama map atau alamat terlalu panjang. +InvalidGroupName=Nama map tidak sah. +BadGroupName=Nama map dilarang berisi karakter-karakter berikut:%n%n%1 +NoProgramGroupCheck2=&Jangan buat map Menu Start + +WizardReady=Siap Memasang +ReadyLabel1=Kami siap untuk memulai instalasi [name] pada komputer Anda. +ReadyLabel2a=Klik Pasang untuk meneruskan instalasi, atau klik Kembali bila Anda ingin menilik atau mengubah setelan. +ReadyLabel2b=Klik Pasang untuk meneruskan instalasi. +ReadyMemoUserInfo=Informasi pengguna: +ReadyMemoDir=Lokasi tujuan: +ReadyMemoType=Tipe instalasi: +ReadyMemoComponents=Komponen terpilih: +ReadyMemoGroup=Map Menu Start: +ReadyMemoTasks=Tugas Tambahan: + +;Inno6 +DownloadingLabel=Mengunduh berkas tambahan... +ButtonStopDownload=&Setop Unduhan +StopDownload=Anda yakin ingin berhenti mengunduh? +ErrorDownloadAborted=Unduhan dibatalkan +ErrorDownloadFailed=Gagal mengunduh: %1 %2 +ErrorDownloadSizeFailed=Gagal mendapatkan ukuran: %1 %2 +ErrorFileHash1=Ceksum berkas gagal: %1 +ErrorFileHash2=Ceksum berkas tidak sah: seharusnya %1, yang kami dapatkan %2 +ErrorProgress=Langkah tidak sah: %1 dari %2 +ErrorFileSize=Ukuran berkas tidak sah: seharusnya %1, yang kami dapatkan %2 + +; *** TExtractionWizardPage wizard page and Extract7ZipArchive +ExtractionLabel=Mengektrasi berkas tambahan... +ButtonStopExtraction=&Hentikan ekstrasi +StopExtraction=Anda yakin ingin menghentikan ekstrasi? +ErrorExtractionAborted=Ekstrasi dibatalkan +ErrorExtractionFailed=Ekstraksi gagal: %1 +;Inno6 + +WizardPreparing=Bersiap Memasang +PreparingDesc=Kami sedang bersiap memasang [name] pada komputer Anda. +PreviousInstallNotCompleted=Instalasi/pelepasan dari program sebelumnya tidak lengkap. Anda perlu memulai ulang komputer untuk melengkapinya nanti.%n%nSetelah itu, jalankan Pemandu kembali untuk melengkapi instalasi [name]. +CannotContinue=Kami tidak bisa meneruskan. Klik Batal untuk keluar. +ApplicationsFound=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis. +ApplicationsFound2=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis. Selengkapnya memasang, kami akan berusaha memulai ulang aplikasi-aplikasi tersebut. +CloseApplications=&Otomatis tutup aplikasi +DontCloseApplications=&Jangan tutup aplikasi +ErrorCloseApplications=Kami tidak dapat menutup semua aplikasi secara otomatis. Disarankan agar Anda menutup semua aplikasi yang memakai berkas-berkas yang perlu kami perbarui sebelum meneruskan. +;Inno6 +PrepareToInstallNeedsRestart=Kami perlu memulai ulang komputer Anda. Setelah itu, jalankan Pemandu kembali untuk melengkapi pemasangan [name].%n%nMulai ulang sekarang? +;Inno6 + +WizardInstalling=Memasang +InstallingLabel=Silakan tunggu selagi kami memasang [name] pada komputer Anda. + +FinishedHeadingLabel=Mengakhiri Instalasi [name] +FinishedLabelNoIcons=Kami telah selesai memasang [name] pada komputer Anda. +FinishedLabel=Kami telah selesai memasang [name] pada komputer Anda. Aplikasi tersebut bisa dijalankan dengan cara memilih pintasan yang terpasang. +ClickFinish=Klik Selesai untuk menutup instalasi. +FinishedRestartLabel=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda. Lakukan sekarang? +FinishedRestartMessage=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda.%n%nLakukan sekarang? +ShowReadmeCheck=Ya, saya ingin melihat berkas README +YesRadio=&Ya, mulai ulang komputer sekarang +NoRadio=&Tidak, saya akan memulai ulang komputer nanti +RunEntryExec=Jalankan %1 +RunEntryShellExec=Lihat %1 + +ChangeDiskTitle=Kami Memerlukan Diska Sambungan +SelectDiskLabel2=Silakan masukkan Diska %1 dan klik OK.%n%nBila berkas-berkas di dalam diska ini dapat ditemukan di map lain selain yang ditampilkan di bawah, masukkan alamat yang benar atau klik Cari. +PathLabel=&Alamat: +FileNotInDir2=Berkas "%1" tidak dapat ditemukan di "%2". Silakan masukkan diska yang benar atau pilih map lain. +SelectDirectoryLabel=Silakan tentukan lokasi diska berikutnya. + +SetupAborted=Instalasi tidak lengkap.%n%nSilakan koreksi masalah dan jalankan Pemandu kembali. +;Inno6 +AbortRetryIgnoreSelectAction=Pilih tindakan +AbortRetryIgnoreRetry=&Coba lagi +AbortRetryIgnoreIgnore=&Abaikan galat dan teruskan +AbortRetryIgnoreCancel=Batalkan pemasangan +;Inno6 + +StatusClosingApplications=Menutup aplikasi... +StatusCreateDirs=Membuat direktori... +StatusExtractFiles=Mengekstrak berkas... +StatusCreateIcons=Membuat pintasan... +StatusCreateIniEntries=Membuat catatan INI... +StatusCreateRegistryEntries=Membuat catatan Registry... +StatusRegisterFiles=Meregistrasi berkas... +StatusSavingUninstall=Menyimpan informasi pelepas... +StatusRunProgram=Mengakhiri instalasi... +StatusRestartingApplications=Menjalankan ulang aplikasi... +StatusRollback=Membatalkan perubahan... + +ErrorInternal2=Galat internal: %1 +ErrorFunctionFailedNoCode=%1 gagal +ErrorFunctionFailed=%1 gagal; kode %2 +ErrorFunctionFailedWithMessage=%1 gagal; kode %2.%n%3 +ErrorExecutingProgram=Tidak dapat mengeksekusi berkas:%n%1 + +ErrorRegOpenKey=Galat membuka kunci Registry:%n%1\%2 +ErrorRegCreateKey=Galat membuat kunci Registry:%n%1\%2 +ErrorRegWriteKey=Galat menulis kunci Registry:%n%1\%2 + +ErrorIniEntry=Galat membuat catatan INI dalam berkas "%1". + +;Inno6 +FileAbortRetryIgnoreSkipNotRecommended=&Lewati berkas ini (tidak disarankan) +FileAbortRetryIgnoreIgnoreNotRecommended=&Abaikan galat dan teruskan (tidak disarankan) +;Inno6 + +SourceIsCorrupted=Berkas asal telah rusak +SourceDoesntExist=Berkas asal "%1" tidak ada + +;Inno6 +ExistingFileReadOnly2=Berkas yang sudah ada tidak bisa ditimpa karena telah ditandai hanya-baca. +ExistingFileReadOnlyRetry=&Hapus atribut hanya-baca dan coba lagi +ExistingFileReadOnlyKeepExisting=&Pertahankan berkas yang sudah ada +ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada: +FileExistsSelectAction=Pilih tindakan +FileExists2=Berkas sudah ada. +FileExistsOverwriteExisting=&Timpa berkas yang sudah ada +FileExistsKeepExisting=&Pertahankan berkas yang sudah ada +FileExistsOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya +ExistingFileNewerSelectAction=Pilih tindakan +ExistingFileNewer2=Berkas yang sudah ada lebih baru dari yang akan kami coba pasang. +ExistingFileNewerOverwriteExisting=&Timpa berkas yang sudah ada +ExistingFileNewerKeepExisting=&Pertahankan berkas yang sudah ada (disarankan) +ExistingFileNewerOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya +;Inno6 +ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada: +ErrorChangingAttr=Terjadi galat saat berusaha mengubah atribusi berkas yang sudah ada: +ErrorCreatingTemp=Terjadi galat saat berusaha membuat berkas di direktori tujuan: +ErrorReadingSource=Terjadi galat saat berusaha membaca berkas asal: +ErrorCopying=Terjadi galat saat berusaha menyalin berkas: +ErrorReplacingExistingFile=Terjadi galat saat berusaha menimpa berkas yang sudah ada: +ErrorRestartReplace=RestartReplace gagal: +ErrorRenamingTemp=Terjadi galat saat berusaha mengubah nama berkas di direktori tujuan: +ErrorRegisterServer=Tidak dapat meregistrasi DLL/OCX: %1 +ErrorRegSvr32Failed=RegSvr32 gagal dengan kode akhir %1 +ErrorRegisterTypeLib=Tidak dapat meregistrasi berkas referensi: %1 + +;Inno6 +UninstallDisplayNameMark=%1 (%2) +UninstallDisplayNameMarks=%1 (%2, %3) +UninstallDisplayNameMark32Bit=32-bita +UninstallDisplayNameMark64Bit=64-bita +UninstallDisplayNameMarkAllUsers=Semua pengguna +UninstallDisplayNameMarkCurrentUser=Pengguna saat ini +;Inno6 + +ErrorOpeningReadme=Terjadi galat saat berusaha membuka berkas README. +ErrorRestartingComputer=Kami gagal memulai ulang komputer. Silakan lakukan secara manual. + +UninstallNotFound=Berkas "%1" tidak ada. Tidak bisa melepas +UninstallOpenError=Berkas "%1" tidak dapat dibuka. Tidak bisa melepas +UninstallUnsupportedVer=Berkas catatan pelepas "%1" tidak dalam format yang kami kenali. Tidak bisa melepas +UninstallUnknownEntry=Entri tak dikenal (%1) ditemukan dalam catatan pelepas +ConfirmUninstall=Anda yakin ingin melepas %1 beserta semua komponennya? +UninstallOnlyOnWin64=Instalasi ini hanya bisa dilepas pada Windows 64-bita. +OnlyAdminCanUninstall=Instalasi ini hanya bisa dilepas oleh pengguna dengan izin administratif. +UninstallStatusLabel=Silakan tunggu selagi %1 dihapus dari komputer Anda. +UninstalledAll=%1 berhasil dihapus dari komputer Anda. +UninstalledMost=Selesai melepas %1.%n%nBeberapa elemen tidak dapat dihapus. Anda bisa menghapusnya secara manual. +UninstalledAndNeedsRestart=Untuk melengkapi pelepasan %1, komputer Anda perlu dimulai ulang.%n%nMulai ulang sekarang? +UninstallDataCorrupted=Berkas "%1" rusak. Tidak bisa melepas + +ConfirmDeleteSharedFileTitle=Hapus Berkas Bersama? +ConfirmDeleteSharedFile2=Sistem mengindikasi bahwa berkas bersama di bawah ini tidak lagi dipakai oleh program mana pun. Apa Anda ingin agar kami menghapusnya?%n%nBila masih ada program yang memakainya dan berkas ini dihapus, program tersebut dapat tidak berfungsi dengan semestinya. Bila Anda ragu, pilih No. Membiarkan berkas ini pada sistem Anda takkan membahayakan. +SharedFileNameLabel=Nama berkas: +SharedFileLocationLabel=Lokasi: +WizardUninstalling=Status Pelepasan +StatusUninstalling=Melepas %1... + +ShutdownBlockReasonInstallingApp=Memasang %1. +ShutdownBlockReasonUninstallingApp=Melepas %1. + +[CustomMessages] +NameAndVersion=%1 versi %2 +AdditionalIcons=Pintasan tambahan: +CreateDesktopIcon=Buat pintasan &desktop +CreateQuickLaunchIcon=Buat pintasan Pelontar &Cepat +ProgramOnTheWeb=%1 di Web +UninstallProgram=Lepas %1 +LaunchProgram=Jalankan %1 +AssocFileExtension=&Kaitkan %1 dengan ekstensi berkas %2 +AssocingFileExtension=Mengaitkan %1 dengan ekstensi berkas %2... +AutoStartProgramGroupDescription=Startup: +AutoStartProgram=Otomatis jalankan %1 +AddonHostProgramNotFound=%1 tidak dapat ditemukan di map yang Anda pilih.%n%nMaju terus? diff --git a/setup-scripts/Setup-windows-ffmpeg.iss b/setup-scripts/Setup-windows-ffmpeg.iss index 9ad8a10..b51fc23 100644 --- a/setup-scripts/Setup-windows-ffmpeg.iss +++ b/setup-scripts/Setup-windows-ffmpeg.iss @@ -47,6 +47,9 @@ Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" +Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl" +Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu" +Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; @@ -106,6 +109,9 @@ begin else if ActiveLanguage = 'russian' then LanguageCode := 'ru' else if ActiveLanguage = 'spanish' then LanguageCode := 'es' else if ActiveLanguage = 'turkish' then LanguageCode := 'tr' + else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh' + else if ActiveLanguage = 'hindi' then LanguageCode := 'hi' + else if ActiveLanguage = 'indonesian' then LanguageCode := 'id' else LanguageCode := 'en'; ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json'); diff --git a/setup-scripts/Setup-windows.iss b/setup-scripts/Setup-windows.iss index 1f1228c..49ef544 100644 --- a/setup-scripts/Setup-windows.iss +++ b/setup-scripts/Setup-windows.iss @@ -47,6 +47,9 @@ Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" +Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl" +Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu" +Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; @@ -84,6 +87,9 @@ begin else if ActiveLanguage = 'russian' then LanguageCode := 'ru' else if ActiveLanguage = 'spanish' then LanguageCode := 'es' else if ActiveLanguage = 'turkish' then LanguageCode := 'tr' + else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh' + else if ActiveLanguage = 'hindi' then LanguageCode := 'hi' + else if ActiveLanguage = 'indonesian' then LanguageCode := 'id' else LanguageCode := 'en'; ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json'); From a2c16bf7931e88616e3f0795821f425bcfee632c Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:33:25 +0200 Subject: [PATCH 121/134] Add Windows installer artifacts to CI docs Update CI_CD_README.md to list the Windows installer outputs produced by the workflow: `YTSage-v{version}-Setup.exe` (standard installer) and `YTSage-v{version}-ffmpeg-Setup.exe` (FFmpeg bundle installer). This documents that the pipeline generates both portable zips and installer executables for Windows. --- .github/CI_CD_README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CI_CD_README.md b/.github/CI_CD_README.md index e68969d..93cad84 100644 --- a/.github/CI_CD_README.md +++ b/.github/CI_CD_README.md @@ -43,6 +43,8 @@ The workflow creates the following files based on the platform: #### Windows - `YTSage-v{version}-portable.zip` - Standard portable version - `YTSage-v{version}-ffmpeg-portable.zip` - FFmpeg bundle portable +- `YTSage-v{version}-Setup.exe` - Standard installer +- `YTSage-v{version}-ffmpeg-Setup.exe` - FFmpeg bundle installer #### Linux - `YTSage-v{version}-{arch}.AppImage` - AppImage portable (x86_64, aarch64) From a3abd3309f7156f0535f9b37bce480953f931b19 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:34:44 +0200 Subject: [PATCH 122/134] Bump ytsage version to 5.0.0b5 Update __version__ in ytsage/__init__.py from 5.0.0b4 to 5.0.0b5 to mark the next beta release. --- ytsage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ytsage/__init__.py b/ytsage/__init__.py index ebe747e..b44f066 100644 --- a/ytsage/__init__.py +++ b/ytsage/__init__.py @@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader A modern, user-friendly YouTube video downloader built with PySide6. """ -__version__ = "5.0.0b4" +__version__ = "5.0.0b5" __author__ = "oop7" From 7490f1939b055d6b7381d6141e2c0e0a2fd81a13 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:43:38 +0200 Subject: [PATCH 123/134] Update build-pypi.yml to set draft and prerelease options for GitHub release --- .github/workflows/build-pypi.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-pypi.yml b/.github/workflows/build-pypi.yml index 274fb34..7554805 100644 --- a/.github/workflows/build-pypi.yml +++ b/.github/workflows/build-pypi.yml @@ -43,6 +43,9 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: v${{ inputs.version || github.event.inputs.version }} + name: YTSage v${{ inputs.version || github.event.inputs.version }} + draft: true + prerelease: false files: | dist/* env: From 026bd45c32c6775c67eaf343bff89f3cfcb6a596 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:45:35 +0200 Subject: [PATCH 124/134] Bump project version to 5.0.0b5 Update pyproject.toml to change the project version from 4.9.7 to 5.0.0b5, marking a beta/pre-release for the upcoming 5.0 release cycle. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 323bcdf..5ead99f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ytsage" -version = "4.9.7" +version = "5.0.0b5" description = "Modern YouTube downloader with a clean PySide6 interface." authors = [ { name = "oop7", email = "oop7_support@proton.me" }, From 6b6394b3b6c167c00e4bde75b993037ef04cad77 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:02:54 +0200 Subject: [PATCH 125/134] Zero-pad PyPI version to match GitHub tags Normalize PyPI's version string (e.g. "2026.2.21") to the zero-padded GitHub tag format (e.g. "2026.02.21") before building the update target. The code splits a three-part version and pads the month/day components with two digits so stable@ matches yt-dlp GitHub tagging. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index eca6651..8f8e4bb 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -746,6 +746,11 @@ class UpdaterTabWidget(QWidget): response.raise_for_status() latest_tag = response.json()["info"]["version"] if latest_tag: + # PyPI returns version without zero-padding (e.g. "2026.2.21") + # but yt-dlp GitHub tags are zero-padded (e.g. "2026.02.21") + parts = latest_tag.split(".") + if len(parts) == 3: + latest_tag = f"{parts[0]}.{int(parts[1]):02d}.{int(parts[2]):02d}" update_target = f"stable@{latest_tag}" logger.info(f"Latest stable version tag: {latest_tag}") else: From 0d8bf820ecb1a5b7c565ddd63030e7e6bceeca21 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:14:37 +0200 Subject: [PATCH 126/134] Add installation and update instructions to README Add collapsible (details/summary) sections with step-by-step installation and upgrade instructions: pip upgrade for updating, Windows installer/portable/FFmpeg options, Linux packages (deb/rpm/AppImage/Flatpak) with example commands, and macOS DMG/zip steps plus a troubleshooting note. Improves clarity and discoverability of install/update procedures for users. --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 2f52658..1026660 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,15 @@ Install YTSage from PyPI: pip install ytsage ``` +
+ЁЯФД Update an existing installation + +```bash +pip install --upgrade ytsage +``` + +
+ Then launch the app: ```bash @@ -82,6 +91,14 @@ ytsage | ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable version, no installation required | | ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable with FFmpeg, zipped | +
+ЁЯЫая╕П Installation Steps + +1. **EXE Installer (`.exe`)**: Double-click the file and follow the setup wizard. +2. **Portable Version (`.zip`)**: Extract the archive to your desired location and run `ytsage.exe`. +3. **FFmpeg Bundled**: Choose the FFmpeg bundled versions if you don't have FFmpeg installed on your system. +
+ #### ЁЯРз Linux | Format | Description | @@ -91,6 +108,29 @@ ytsage | ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM package | | ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle | +
+ЁЯЫая╕П Installation Steps + +- **DEB (`.deb`)**: + ```bash + sudo dpkg -i ytsage_*.deb + sudo apt-get install -f # Fix missing dependencies if any + ``` +- **RPM (`.rpm`)**: + ```bash + sudo rpm -i ytsage-*.rpm + ``` +- **AppImage (`.AppImage`)**: + ```bash + chmod +x YTSage-*.AppImage + ./YTSage-*.AppImage + ``` +- **Flatpak**: Follow instructions on Flathub or run: + ```bash + flatpak install flathub io.github.oop7.ytsage + ``` +
+ #### ЁЯНО macOS | Format | Description | @@ -98,6 +138,15 @@ ytsage | ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Zipped application for Apple Silicon | | ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Disk image installer for Apple Silicon | +
+ЁЯЫая╕П Installation Steps + +- **DMG Installer (`.dmg`)**: Double-click to mount, then drag `YTSage.app` into your Applications folder. +- **App Archive (`.zip`)**: Extract the zip and move `YTSage.app` to your Applications folder. + +*Note: If you encounter an "App is damaged" error, see the [macOS troubleshooting section](#troubleshooting) below.* +
+ > [ЁЯСЙ Download Latest Release](https://github.com/oop7/YTSage/releases/latest)
From 51d43e38bf74b2659939caeed271625d37475c84 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:16:25 +0200 Subject: [PATCH 127/134] Add download link and remove duplicate Insert a 'Download Latest Release' callout under the Pre-built Executables section and remove a duplicate link later in the README so there's a single prominent release download link. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1026660..65affdb 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ ytsage ### ЁЯУж Pre-built Executables +> [ЁЯСЙ Download Latest Release](https://github.com/oop7/YTSage/releases/latest) + #### ЁЯкЯ Windows | Format | Description | @@ -147,8 +149,6 @@ ytsage *Note: If you encounter an "App is damaged" error, see the [macOS troubleshooting section](#troubleshooting) below.*
-> [ЁЯСЙ Download Latest Release](https://github.com/oop7/YTSage/releases/latest) -
ЁЯЫая╕П Manual Installation from Source From 3de27938d094889a744985c4ba2f6bb304653240 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:18:28 +0200 Subject: [PATCH 128/134] Add separator and update manual install heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert a horizontal rule before the Manual Installation section and update the section summary emoji/text from 'ЁЯЫая╕П Manual Installation from Source' to 'ЁЯТ╗ Manual Installation from Source' for improved readability and consistency in the README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65affdb..1b231d2 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,10 @@ ytsage *Note: If you encounter an "App is damaged" error, see the [macOS troubleshooting section](#troubleshooting) below.*
+--- +
-ЁЯЫая╕П Manual Installation from Source +ЁЯТ╗ Manual Installation from Source ### 1. Clone the Repository From c34f33cc7a60cc7732a27d862c87cad5aafccc45 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:45:06 +0200 Subject: [PATCH 129/134] Add GitHub Sponsors badge to README Adds a GitHub Sponsors badge (via shields.io) to the README, linking to https://github.com/sponsors/oop7 to promote project sponsorship. Minor documentation/branding enhancement. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1b231d2..24ebecf 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) [![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-4b5563?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=ea4aaa&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7) **A modern YouTube downloader with a clean PySide6 interface.** Download videos in any quality, extract audio, fetch subtitles, and more. From 4775f827a0d9f63303b9f82a6db932155f6518be Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:45:21 +0200 Subject: [PATCH 130/134] Add sponsor link to About dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a styled 'тЭдя╕П Sponsor' GitHub Sponsors link to the About dialog (ytsage_dialogs_base.py). The change creates a QLabel with an external href to the sponsor page, enables openExternalLinks, and appends it to the info layout so the About dialog displays a sponsor call-to-action. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index dc39a48..168c4fd 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -240,6 +240,11 @@ class AboutDialog(QDialog): repo_label.setOpenExternalLinks(True) info_layout.addWidget(repo_label) + sponsor_link = 'тЭдя╕П Sponsor' + sponsor_label = QLabel(sponsor_link) + sponsor_label.setOpenExternalLinks(True) + info_layout.addWidget(sponsor_label) + # Center the info layout info_container = QHBoxLayout() info_container.addStretch() From 1cfb106ccc026b1fd6463820c11ed74d5a59ae74 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:48:42 +0200 Subject: [PATCH 131/134] Reorder badges in README.md Move the PyPI version and License (MIT) badges down in the README header to improve grouping and visual flow of project badges (now placed after the GitHub Stars badge). --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24ebecf..04db3c6 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ ytsage-wordmark YTSage Interface -[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) -[![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) [![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) [![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) +[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) +[![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-4b5563?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=ea4aaa&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7) From 86e0121a58962e6de5f294e11db77c04b6dd6962 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:52:02 +0200 Subject: [PATCH 132/134] Adjust README badges' colors and order Reorder and restyle README badges for consistent visual grouping. Several badges (downloads, license, supported platforms) were changed to a dark-gray color (1f2937) and grouped together, while GitHub Stars, PyPI version, and Sponsors badges were highlighted in red (c90000). This is a cosmetic update to improve badge contrast and layout. --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 04db3c6..97c6387 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ YTSage Interface [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) -[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) -[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=4b5563&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) -[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) -[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) -[![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) -[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-4b5563?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=ea4aaa&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7) +[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage) +[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) +[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) +[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) + +[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) +[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7) **A modern YouTube downloader with a clean PySide6 interface.** Download videos in any quality, extract audio, fetch subtitles, and more. From 7bf4eec9fa04bbbac17a14a43e2a3947aaf0b3d1 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:53:14 +0200 Subject: [PATCH 133/134] Remove extra blank line in README badges Delete an unintended blank line between badge entries in README.md to tighten badge layout and clean up formatting. No functional changes. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 97c6387..8998cc6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT) [![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases) - [![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) [![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7) From a98dad0a33de362d787c546b54b94aeba16de8eb Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:00:03 +0200 Subject: [PATCH 134/134] Simplify link styling in About dialog Update AboutDialog link markup to remove explicit font-size and font-weight styles and to unify link color. The author and repo links no longer set font-size, and the sponsor link color was changed from #ea4aaa to #c90000 (and its font-size/weight removed) for consistent appearance across links. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py index 168c4fd..767684d 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_base.py @@ -226,21 +226,21 @@ class AboutDialog(QDialog): info_layout = QHBoxLayout() info_layout.setSpacing(15) - author_link = 'oop7' + author_link = 'oop7' author_label = QLabel( f"{_('about.author', author=author_link)}" ) author_label.setOpenExternalLinks(True) info_layout.addWidget(author_label) - repo_link = 'YTSage' + repo_link = 'YTSage' repo_label = QLabel( f"{_('about.github', repo=repo_link)}" ) repo_label.setOpenExternalLinks(True) info_layout.addWidget(repo_label) - sponsor_link = 'тЭдя╕П Sponsor' + sponsor_link = 'тЭдя╕П Sponsor' sponsor_label = QLabel(sponsor_link) sponsor_label.setOpenExternalLinks(True) info_layout.addWidget(sponsor_label)
Download SettingsPlaylist DownloadDownload SettingsPlaylist Download
Download Settings Playlist Download
Audio Format Selection with Save ThumbnailCustom OptionsAudio Format Selection with Save ThumbnailCustom Options
Audio Format