From 45d3cdbad47e707ea4afcb84eed10d53d566965b Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:49:36 +0300 Subject: [PATCH 1/6] Default generic_mode to True; preserve explicit False Set the default generic_mode to True in the config manager and update GUI initialization to distinguish between a missing config and an explicit False. Replace usages of `ConfigManager.get(... ) or False` with a None check so that an explicit False value is respected. Changes made in ytsage/utils/ytsage_config_manager.py and GUI initializers in ytsage/gui/ytsage_gui_main.py and ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py. --- ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py | 3 ++- ytsage/gui/ytsage_gui_main.py | 5 ++++- ytsage/utils/ytsage_config_manager.py | 2 +- 3 files changed, 7 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 59d6e6b..e24822f 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -255,7 +255,8 @@ class DownloadSettingsDialog(QDialog): generic_mode_group_box = QGroupBox(_("settings.generic_mode")) generic_mode_layout = QVBoxLayout() - self.generic_mode_enabled = ConfigManager.get("generic_mode") or False + generic_val = ConfigManager.get("generic_mode") + self.generic_mode_enabled = generic_val if generic_val is not None else True self.generic_mode_checkbox = QCheckBox(_("settings.enable_generic_mode")) self.generic_mode_checkbox.setChecked(self.generic_mode_enabled) diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 7743700..2263e6f 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -259,7 +259,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.force_audio_format = ConfigManager.get("force_audio_format") or False self.preferred_audio_format = ConfigManager.get("preferred_audio_format") or "best" self.audio_normalization = ConfigManager.get("audio_normalization") or False - self.generic_mode_enabled = ConfigManager.get("generic_mode") or False + + generic_val = ConfigManager.get("generic_mode") + self.generic_mode_enabled = generic_val if generic_val is not None else True + # Track if video analysis is completed self.analysis_completed = False diff --git a/ytsage/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py index fcbe92f..3f73532 100644 --- a/ytsage/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -71,7 +71,7 @@ class ConfigManager: _settings: Dict[str, Any] = {} _default_config: Dict[str, Any] = { "download_path": str(USER_HOME_DIR / "Downloads"), - "generic_mode": False, + "generic_mode": True, "speed_limit_value": None, "speed_limit_unit_index": 0, "cookie_source": "browser", # "browser" or "file" From ec18262bba23a2b6b47375d353edd93d4f5e5696 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:54:03 +0300 Subject: [PATCH 2/6] Bump version to 5.2.0b Update package version from 5.0.10b to 5.2.0b in pyproject.toml and ytsage/__init__.py to reflect the new beta release and keep metadata in sync. --- pyproject.toml | 2 +- ytsage/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22ed937..481af62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ytsage" -version = "5.0.10b" +version = "5.2.0b" description = "Modern YouTube downloader with a clean PySide6 interface." authors = [ { name = "oop7", email = "oop7_support@proton.me" }, diff --git a/ytsage/__init__.py b/ytsage/__init__.py index f8faf4e..67d4aed 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.10b" +__version__ = "5.2.0b" __author__ = "oop7" From 8c9966000e89c3397d50ef4fe4b21989bd15a854 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:59:48 +0300 Subject: [PATCH 3/6] Improve save_path handling and use ConfigManager Normalize path input to string, ensure parent directories are created (mkdir with parents=True), and check writability using the normalized path. Replace manual JSON file write with ConfigManager.set("download_path", ...) and add import for ConfigManager. Adjust logging to use logger.error for failures and replace logger.exception where appropriate. These changes improve robustness when saving the download path and centralize config persistence. --- ytsage/core/ytsage_utils.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ytsage/core/ytsage_utils.py b/ytsage/core/ytsage_utils.py index 86cffea..78a096f 100644 --- a/ytsage/core/ytsage_utils.py +++ b/ytsage/core/ytsage_utils.py @@ -402,25 +402,26 @@ def load_saved_path(main_window_instance: Any) -> None: main_window_instance.last_path = tempfile.gettempdir() +from ..utils.ytsage_config_manager import ConfigManager + def save_path(main_window_instance: Any, path: Union[str, Path]) -> bool: """Save download path with enhanced error handling.""" try: # Verify the path is valid and writable - if not Path(path).exists(): + path_str = str(path) + if not Path(path_str).exists(): try: - Path(path).mkdir(exist_ok=True) + Path(path_str).mkdir(parents=True, exist_ok=True) except Exception as e: - logger.exception(f"Error creating directory: {e}") + logger.error(f"Error creating directory: {e}") return False - if not os.access(path, os.W_OK): - logger.info("Path is not writable") + if not os.access(path_str, os.W_OK): + logger.error("Path is not writable") return False - # Save the config - config = {"download_path": path} - with open(APP_CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(config, f, ensure_ascii=False) + # Save the config using ConfigManager + ConfigManager.set("download_path", path_str) return True except Exception as e: From 72f9098a7095b97e1ed40593e1358fa589538110 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:19:40 +0300 Subject: [PATCH 4/6] Persist and restore main window state Restore window geometry/state on startup and save them on exit. Adds YTSageApp._load_window_state() and calls it during UI init; saves Base64-encoded geometry/state via ConfigManager when closing. Adds default config keys "window_geometry" and "window_state". Includes error handling and debug logging to avoid failures if stored values are invalid. --- ytsage/gui/ytsage_gui_main.py | 24 ++++++++++++++++++++++++ ytsage/utils/ytsage_config_manager.py | 2 ++ 2 files changed, 26 insertions(+) diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 2263e6f..d25273b 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -272,6 +272,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.player.setAudioOutput(self.audio_output) self.init_ui() + self._load_window_state() # Defer heavy start-up tasks to ensure UI renders immediately QTimer.singleShot(100, self._perform_startup_checks) @@ -290,6 +291,22 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Initialize UI state based on current mode self.handle_mode_change() + def _load_window_state(self): + """Restore previous window geometry and state from config.""" + from PySide6.QtCore import QByteArray + geo_b64 = ConfigManager.get("window_geometry") + if geo_b64: + try: + self.restoreGeometry(QByteArray.fromBase64(geo_b64.encode("ascii"))) + except Exception as e: + logger.debug(f"Failed to restore geometry: {e}") + + state_b64 = ConfigManager.get("window_state") + if state_b64: + try: + self.restoreState(QByteArray.fromBase64(state_b64.encode("ascii"))) + except Exception as e: + logger.debug(f"Failed to restore state: {e}") def _perform_startup_checks(self): """Perform potentially blocking startup checks after UI is shown.""" @@ -1194,6 +1211,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): self.current_download.terminate() self.current_download.wait(1000) # Wait for termination + # Save the window size and state + try: + ConfigManager.set("window_geometry", self.saveGeometry().toBase64().data().decode("ascii")) + ConfigManager.set("window_state", self.saveState().toBase64().data().decode("ascii")) + except Exception as w_e: + logger.debug(f"Failed to save window state: {w_e}") + logger.info("Application closing...") event.accept() except Exception as e: diff --git a/ytsage/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py index 3f73532..05e61d2 100644 --- a/ytsage/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -96,6 +96,8 @@ class ConfigManager: "preferred_audio_format": "best", "audio_normalization": False, "filename_format": "%(title)s_%(resolution)s_[%(id)s].%(ext)s", + "window_geometry": None, + "window_state": None, "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 dac298a5241776eb6a7d0049129a7d9b0c8a6f01 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:46:30 +0300 Subject: [PATCH 5/6] Show audio codec details in format table Highlight AC3/EAC3 surround-sound formats in the GUI format table and include channel/bitrate details where available. Also relax the audio-only filter so formats without filesize can still be shown. --- ytsage/gui/ytsage_gui_format_table.py | 83 ++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/ytsage/gui/ytsage_gui_format_table.py b/ytsage/gui/ytsage_gui_format_table.py index 5b2dea0..651ff1f 100644 --- a/ytsage/gui/ytsage_gui_format_table.py +++ b/ytsage/gui/ytsage_gui_format_table.py @@ -22,6 +22,34 @@ class FormatTableMixin: text_width = font_metrics.horizontalAdvance(label) return max(text_width + padding, min_width) + def _get_audio_codec_display(self, format_info: dict) -> str: + """ + Generate a display string for audio codec information. + Highlights EAC3/AC3 and other surround sound formats with channel info. + """ + acodec = format_info.get("acodec") + if not acodec or acodec == "none": + return "N/A" + + channels = format_info.get("audio_channels") + abr = format_info.get("abr") + + # Build codec string + codec_str = str(acodec) + + # Add channel info for surround sound formats + if channels: + codec_str += f" ({channels}ch)" + + # Add bitrate if available + if abr and isinstance(abr, (int, float)): + if abr >= 1000: + codec_str += f" {abr/1000:.1f}Mbps" + else: + codec_str += f" {int(abr)}kbps" + + return codec_str + def _apply_column_widths(self, header_labels: list[str], is_playlist_mode: bool = False) -> None: """Apply responsive column widths to format table.""" self = cast("YTSageApp", self) @@ -209,7 +237,8 @@ class FormatTableMixin: for f in self.all_formats if (f.get("vcodec") == "none" or "audio only" in str(f.get("format_note") or "").lower()) and f.get("acodec") != "none" - and f.get("filesize") is not None + # Removed filesize requirement - many audio-only formats (including EAC3/AC3) may not have filesize in yt-dlp output + # Include all audio formats to detect EAC3/AC3 and other surround sound codecs ] # Sort formats by quality @@ -399,14 +428,41 @@ class FormatTableMixin: # 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) + acodec = f.get("acodec") + + # Enhanced audio status with support for surround sound formats (EAC3/AC3) if needs_audio: - audio_item.setForeground(QColor("#ffa500")) - elif audio_status == _("formats.audio_only"): - audio_item.setForeground(QColor("#cccccc")) + audio_status = _("formats.will_merge_audio") + audio_color = QColor("#ffa500") + elif f.get("vcodec") != "none": + # Video format with audio + if acodec in ["ac3", "eac3"]: + # Highlight surround sound codecs + channels = f.get("audio_channels", "") + if channels: + audio_status = f"{_('formats.has_audio')} - {acodec.upper()} {channels}ch" + else: + audio_status = f"{_('formats.has_audio')} - {acodec.upper()}" + audio_color = QColor("#00ccff") # Cyan for surround sound + else: + audio_status = _("formats.has_audio") + audio_color = QColor("#00cc00") else: - audio_item.setForeground(QColor("#00cc00")) + # Audio-only format + if acodec in ["ac3", "eac3"]: + # Highlight surround sound audio-only + channels = f.get("audio_channels", "") + if channels: + audio_status = f"{acodec.upper()} {channels}ch" + else: + audio_status = acodec.upper() + audio_color = QColor("#00ccff") # Cyan for surround sound + else: + audio_status = _("formats.audio_only") + audio_color = QColor("#cccccc") + + audio_item = QTableWidgetItem(audio_status) + audio_item.setForeground(audio_color) audio_column_index = 5 if is_playlist_mode else 6 self.format_table.setItem(row, audio_column_index, audio_item) @@ -421,11 +477,22 @@ class FormatTableMixin: # Column 5: Codec if f.get("vcodec") == "none": + # Audio-only format - display codec with channel info codec = str(f.get("acodec") or "N/A") + # Add audio channel information if available (e.g., for 5.1 detection) + channels = f.get("audio_channels") + if channels: + codec += f" ({channels}ch)" else: + # Video format - display video codec and audio codec if present codec = str(f.get("vcodec") or "N/A") if f.get("acodec") != "none": - codec += f" / {str(f.get('acodec') or 'N/A')}" + acodec = str(f.get("acodec") or "N/A") + # Add audio channel info for video with audio + channels = f.get("audio_channels") + if channels: + acodec += f" ({channels}ch)" + codec += f" / {acodec}" self.format_table.setItem(row, 5, QTableWidgetItem(codec)) # Column 7: FPS (Frame Rate) From bc3e7fd4fd251f5042b20484c302a2389998de14 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:33:12 +0300 Subject: [PATCH 6/6] Persist speed limit settings to config Speed limit settings (value and unit) are now loaded from config on app initialization and saved to config when updated. This ensures the user's speed limit preferences persist across app restarts. --- ytsage/gui/ytsage_gui_main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index d25273b..4958f50 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -249,8 +249,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Initialize proxy settings from config self.proxy_url = ConfigManager.get("proxy_url") self.geo_proxy_url = ConfigManager.get("geo_proxy_url") - self.speed_limit_value = None # Store speed limit value - self.speed_limit_unit_index = 0 # Store speed limit unit index (0: KB/s, 1: MB/s) + # Initialize speed limit settings from config + self.speed_limit_value = ConfigManager.get("speed_limit_value") # Store speed limit value + self.speed_limit_unit_index = ConfigManager.get("speed_limit_unit_index") or 0 # Store speed limit unit index (0: KB/s, 1: MB/s) self.download_section = None self.force_keyframes = False # Initialize output format settings @@ -679,6 +680,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.info( f"Speed limit updated to: {self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index] if self.speed_limit_value else 'None'}" ) + # Save speed limit settings to config + ConfigManager.set("speed_limit_value", self.speed_limit_value) + ConfigManager.set("speed_limit_unit_index", self.speed_limit_unit_index) # Update Output Format Settings new_force_format = dialog.get_force_format_enabled()