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.
This commit is contained in:
oop7
2026-02-08 17:41:13 +02:00
parent d2bc08e937
commit d250c1b05f
5 changed files with 105 additions and 0 deletions
@@ -966,6 +966,12 @@ class CustomOptionsDialog(QDialog):
logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}") logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}")
result = update_auto_update_settings(enabled, frequency) result = update_auto_update_settings(enabled, frequency)
logger.info(f"Auto-update settings save result: {result}") 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: except Exception as e:
logger.exception(f"Error saving auto-update settings: {e}") logger.exception(f"Error saving auto-update settings: {e}")
@@ -442,6 +442,40 @@ class UpdaterTabWidget(QWidget):
layout.addWidget(deno_group) 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 === # === yt-dlp Release Channel Section ===
ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel")) ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel"))
ytdlp_channel_layout = QVBoxLayout() ytdlp_channel_layout = QVBoxLayout()
@@ -618,6 +652,10 @@ class UpdaterTabWidget(QWidget):
# Set checkbox # Set checkbox
self.auto_update_enabled.setChecked(auto_settings["enabled"]) 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 # Set current selection based on saved settings
current_frequency = auto_settings["frequency"] current_frequency = auto_settings["frequency"]
if current_frequency == "startup": if current_frequency == "startup":
@@ -656,6 +694,10 @@ class UpdaterTabWidget(QWidget):
return enabled, frequency 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: def _on_channel_changed(self, checked: bool) -> None:
"""Handle channel selection change.""" """Handle channel selection change."""
if not checked: if not checked:
+54
View File
@@ -116,9 +116,63 @@ class UpdateCheckThread(QThread):
# Silently fallback if GitHub API fails (rate limiting, network issues, etc.) # Silently fallback if GitHub API fails (rate limiting, network issues, etc.)
return fallback 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): def run(self):
"""Check for updates using parallel network requests for better performance.""" """Check for updates using parallel network requests for better performance."""
try: 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 # Use ThreadPoolExecutor to make both requests in parallel
# This reduces total wait time from potentially 15s to ~8s max # This reduces total wait time from potentially 15s to ~8s max
with ThreadPoolExecutor(max_workers=2) as executor: with ThreadPoolExecutor(max_workers=2) as executor:
+2
View File
@@ -298,6 +298,8 @@
"ytdlp_channel_switched": "✅ Successfully switched to {channel} channel!", "ytdlp_channel_switched": "✅ Successfully switched to {channel} channel!",
"ytdlp_channel_switch_failed": "❌ Failed to switch channel: {error}", "ytdlp_channel_switch_failed": "❌ Failed to switch channel: {error}",
"ytdlp_current_channel": "Current channel: {channel}", "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_title": "Auto-Update Settings",
"auto_update_header": "🔄 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.", "auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.",
+1
View File
@@ -83,6 +83,7 @@ class ConfigManager:
"geo_proxy_url": None, "geo_proxy_url": None,
"auto_update_ytdlp": True, "auto_update_ytdlp": True,
"auto_update_frequency": "daily", "auto_update_frequency": "daily",
"check_beta_updates": False,
"last_update_check": 0, "last_update_check": 0,
"language": "en", "language": "en",
"ytdlp_channel": "stable", "ytdlp_channel": "stable",