Add auto-update feature with Gitea release checking

- Add UpdateChecker class to query Gitea API for latest releases
- Show update dialog with release notes when new version available
- Open browser to release page for download (handles large files)
- Allow users to skip specific versions or defer updates
- Add "Check for Updates Now" button in settings
- Check automatically on startup (respects 24-hour interval)
- Pre-configured for repo.anhonesthost.net/streamer-tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 17:40:13 -08:00
parent 89819f5d1b
commit b7ab57f21f
7 changed files with 425 additions and 4 deletions

View File

@@ -2,10 +2,13 @@
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFileDialog, QMessageBox
QPushButton, QLabel, QFileDialog, QMessageBox,
QDialog, QTextEdit, QCheckBox
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtCore import Qt, QThread, Signal, QTimer
from PySide6.QtGui import QFont
import webbrowser
from datetime import datetime
from pathlib import Path
import sys
@@ -66,6 +69,90 @@ class EngineStartThread(QThread):
self.finished.emit(False, f"Error initializing engine: {e}")
class UpdateDialog(QDialog):
"""Dialog showing available update information."""
def __init__(self, parent, current_version: str, release_info):
"""
Initialize the update dialog.
Args:
parent: Parent window
current_version: Current application version
release_info: ReleaseInfo object with update details
"""
super().__init__(parent)
self.release_info = release_info
self.skip_version = False
self.setWindowTitle("Update Available")
self.setModal(True)
self.setMinimumSize(500, 400)
self.resize(550, 450)
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
self.setLayout(layout)
# Title
title_label = QLabel("Update Available!")
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
# Version info
version_label = QLabel(f"Current: {current_version} \u2192 New: {release_info.version}")
version_font = QFont()
version_font.setPointSize(12)
version_label.setFont(version_font)
layout.addWidget(version_label)
# Release notes
notes_label = QLabel("Release Notes:")
notes_label.setStyleSheet("font-weight: bold; margin-top: 10px;")
layout.addWidget(notes_label)
self.notes_text = QTextEdit()
self.notes_text.setReadOnly(True)
self.notes_text.setPlainText(release_info.release_notes or "No release notes available.")
layout.addWidget(self.notes_text)
# Skip version checkbox
self.skip_checkbox = QCheckBox(f"Skip version {release_info.version}")
self.skip_checkbox.setToolTip("Don't show this update again")
layout.addWidget(self.skip_checkbox)
# Buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
self.later_button = QPushButton("Remind Me Later")
self.later_button.clicked.connect(self._on_later)
button_layout.addWidget(self.later_button)
self.download_button = QPushButton("Download Update")
self.download_button.setStyleSheet("background-color: #2ecc71; color: white; font-weight: bold;")
self.download_button.clicked.connect(self._on_download)
button_layout.addWidget(self.download_button)
layout.addLayout(button_layout)
def _on_later(self):
"""Handle 'Remind Me Later' button click."""
self.skip_version = self.skip_checkbox.isChecked()
self.reject()
def _on_download(self):
"""Handle 'Download Update' button click."""
self.skip_version = self.skip_checkbox.isChecked()
if self.release_info.download_url:
webbrowser.open(self.release_info.download_url)
self.accept()
class MainWindow(QMainWindow):
"""Main application window using PySide6."""
@@ -136,6 +223,9 @@ class MainWindow(QMainWindow):
# Initialize components (in background)
self._initialize_components()
# Schedule update check 3 seconds after startup (non-blocking)
QTimer.singleShot(3000, self._startup_update_check)
def _update_splash(self, message: str):
"""Update splash screen message if it exists."""
if self.splash_screen:
@@ -836,3 +926,98 @@ class MainWindow(QMainWindow):
self.engine_start_thread.wait()
event.accept()
def _startup_update_check(self):
"""Check for updates on startup (called via QTimer)."""
# Only check if auto_check is enabled
if not self.config.get('updates.auto_check', True):
return
# Check if enough time has passed since last check
last_check_str = self.config.get('updates.last_check', '')
check_interval = self.config.get('updates.check_interval_hours', 24)
if last_check_str:
try:
last_check = datetime.fromisoformat(last_check_str)
hours_since_check = (datetime.now() - last_check).total_seconds() / 3600
if hours_since_check < check_interval:
print(f"Skipping update check - last checked {hours_since_check:.1f} hours ago")
return
except (ValueError, TypeError):
pass # Invalid date format, proceed with check
# Perform async update check
self._check_for_updates(show_no_update_message=False)
def _check_for_updates(self, show_no_update_message: bool = True):
"""
Check for updates.
Args:
show_no_update_message: Whether to show a message if no update is available
"""
from client.update_checker import UpdateChecker
gitea_url = self.config.get('updates.gitea_url', 'https://repo.anhonesthost.net')
owner = self.config.get('updates.owner', 'streamer-tools')
repo = self.config.get('updates.repo', 'local-transcription')
if not gitea_url or not owner or not repo:
if show_no_update_message:
QMessageBox.warning(self, "Update Check", "Update checking is not configured.")
return
checker = UpdateChecker(
current_version=__version__,
gitea_url=gitea_url,
owner=owner,
repo=repo
)
def on_update_check_complete(release_info, error):
# Update last check time
self.config.set('updates.last_check', datetime.now().isoformat())
if error:
print(f"Update check failed: {error}")
if show_no_update_message:
QMessageBox.warning(self, "Update Check Failed", f"Could not check for updates:\n{error}")
return
if release_info:
# Check if this version is skipped
skipped_versions = self.config.get('updates.skipped_versions', [])
if release_info.version in skipped_versions:
print(f"Skipping update notification for version {release_info.version} (user skipped)")
return
# Show update dialog on main thread
QTimer.singleShot(0, lambda: self._show_update_dialog(release_info))
else:
if show_no_update_message:
QMessageBox.information(
self,
"No Updates",
f"You are running the latest version ({__version__})."
)
# Run check in background thread
checker.check_for_update_async(on_update_check_complete)
def _show_update_dialog(self, release_info):
"""
Show the update dialog.
Args:
release_info: ReleaseInfo object with update details
"""
dialog = UpdateDialog(self, __version__, release_info)
dialog.exec()
# If user chose to skip this version, save it
if dialog.skip_version:
skipped_versions = self.config.get('updates.skipped_versions', [])
if release_info.version not in skipped_versions:
skipped_versions.append(release_info.version)
self.config.set('updates.skipped_versions', skipped_versions)

View File

@@ -528,6 +528,27 @@ class SettingsDialog(QDialog):
remote_group.setLayout(remote_layout)
content_layout.addWidget(remote_group)
# Updates Group
updates_group = QGroupBox("Software Updates")
updates_layout = QFormLayout()
updates_layout.setSpacing(10)
self.update_auto_check = QCheckBox("Enable")
self.update_auto_check.setToolTip(
"Automatically check for updates when the application starts"
)
updates_layout.addRow("Check on Startup:", self.update_auto_check)
self.check_updates_button = QPushButton("Check for Updates Now")
self.check_updates_button.setToolTip(
"Manually check for available updates"
)
self.check_updates_button.clicked.connect(self._check_for_updates_now)
updates_layout.addRow("", self.check_updates_button)
updates_group.setLayout(updates_layout)
content_layout.addWidget(updates_group)
# Add stretch to push everything to the top
content_layout.addStretch()
@@ -779,6 +800,9 @@ class SettingsDialog(QDialog):
self.remote_api_key_input.setText(self.config.get('remote_processing.api_key', ''))
self.remote_fallback_check.setChecked(self.config.get('remote_processing.fallback_to_local', True))
# Update settings
self.update_auto_check.setChecked(self.config.get('updates.auto_check', True))
def _save_settings(self):
"""Save settings to config."""
try:
@@ -851,6 +875,9 @@ class SettingsDialog(QDialog):
self.config.set('remote_processing.api_key', self.remote_api_key_input.text())
self.config.set('remote_processing.fallback_to_local', self.remote_fallback_check.isChecked())
# Update settings
self.config.set('updates.auto_check', self.update_auto_check.isChecked())
# Call save callback (which will show the success message)
if self.on_save:
self.on_save()
@@ -864,3 +891,54 @@ class SettingsDialog(QDialog):
QMessageBox.critical(self, "Invalid Input", f"Please check your input values:\n{e}")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save settings:\n{e}")
def _check_for_updates_now(self):
"""Manually check for updates."""
from version import __version__
from client.update_checker import UpdateChecker
# Get settings from config (hardcoded defaults)
gitea_url = self.config.get('updates.gitea_url', 'https://repo.anhonesthost.net')
owner = self.config.get('updates.owner', 'streamer-tools')
repo = self.config.get('updates.repo', 'local-transcription')
# Disable button during check
self.check_updates_button.setEnabled(False)
self.check_updates_button.setText("Checking...")
try:
checker = UpdateChecker(
current_version=__version__,
gitea_url=gitea_url,
owner=owner,
repo=repo
)
release_info, error = checker.check_for_update()
if error:
QMessageBox.warning(self, "Update Check Failed", f"Could not check for updates:\n{error}")
elif release_info:
# Update available
msg = QMessageBox(self)
msg.setWindowTitle("Update Available")
msg.setIcon(QMessageBox.Information)
msg.setText(f"A new version is available!\n\nCurrent: {__version__}\nNew: {release_info.version}")
if release_info.release_notes:
msg.setDetailedText(release_info.release_notes)
msg.setStandardButtons(QMessageBox.Ok)
msg.exec()
else:
QMessageBox.information(
self,
"No Updates",
f"You are running the latest version ({__version__})."
)
except Exception as e:
QMessageBox.critical(self, "Error", f"Error checking for updates:\n{e}")
finally:
# Re-enable button
self.check_updates_button.setEnabled(True)
self.check_updates_button.setText("Check for Updates Now")