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] 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",