Skip to content

Commit 8cf2392

Browse files
committed
feat(updater): implement auto-update system with GitHub Releases integration
- Add comprehensive auto-update functionality using GitHub Releases - Include update checking on app start and manual check option - Implement update download, extraction, and installation process - Add version comparison and skip version functionality - Create update dialog UI with update options - Add translation support for update related strings - Update version management and changelog documentation
1 parent c91b854 commit 8cf2392

9 files changed

Lines changed: 584 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.3.0]
6+
- **Auto-Update**: Implemented a comprehensive auto-update system using GitHub Releases
7+
58
## [1.2.7]
69
- **Availability Tracking**: Implemented smart file availability tracking:
710
- **Scanner**: Updated scan logic to mark missing videos/folders as unavailable instead of removing them.

config_manager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class ConfigManager:
1818
'General': {
1919
'language': 'en',
2020
'show_preview_popup': 'True',
21+
'check_updates_on_start': 'True',
22+
'skip_version': '',
2123
},
2224
'Paths': {
2325
'paths': '',
@@ -132,6 +134,30 @@ def get_selected_tag_ids(self) -> set:
132134
return set()
133135
return set()
134136

137+
# --- Update settings ---
138+
139+
def get_check_updates_on_start(self) -> bool:
140+
config = self._read_config()
141+
return config.getboolean('General', 'check_updates_on_start', fallback=True)
142+
143+
def set_check_updates_on_start(self, value: bool):
144+
config = self._read_config()
145+
if 'General' not in config:
146+
config['General'] = {}
147+
config['General']['check_updates_on_start'] = str(value)
148+
self._write_config(config)
149+
150+
def get_skip_version(self) -> str:
151+
config = self._read_config()
152+
return config.get('General', 'skip_version', fallback='')
153+
154+
def set_skip_version(self, version: str):
155+
config = self._read_config()
156+
if 'General' not in config:
157+
config['General'] = {}
158+
config['General']['skip_version'] = version
159+
self._write_config(config)
160+
135161
# --- Path settings ---
136162

137163
def get_library_paths(self) -> str:

main.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ def __init__(self):
231231
if hasattr(self, 'tag_filter_active'):
232232
self.tag_filter_btn.setChecked(self.tag_filter_active)
233233

234+
# Auto-update cleanup and check
235+
from update_app import cleanup_update_artifacts
236+
cleanup_update_artifacts()
237+
if self.config.get_check_updates_on_start():
238+
QTimer.singleShot(5000, self._check_for_update)
239+
234240
def keyPressEvent(self, event: QKeyEvent):
235241
action = self.hotkey_manager.get_action(event)
236242

@@ -623,9 +629,14 @@ def create_menu_bar(self):
623629
reload_styles_action.triggered.connect(self.reload_styles)
624630
view_menu.addAction(reload_styles_action)
625631

626-
# [Help] Menu
627632
help_menu = menubar.addMenu(tr('menu.help'))
628633

634+
check_updates_action = QAction(self.icons.get('upload', QIcon()), tr('menu.check_updates'), self)
635+
check_updates_action.triggered.connect(lambda: self._check_for_update(force=True))
636+
help_menu.addAction(check_updates_action)
637+
638+
help_menu.addSeparator()
639+
629640
about_action = QAction(self.icons.get('menu_about', QIcon()), tr('menu.about'), self)
630641
about_action.triggered.connect(self.show_about)
631642
help_menu.addAction(about_action)
@@ -692,6 +703,76 @@ def show_about(self):
692703
dialog = AboutDialog(self)
693704
dialog.exec()
694705

706+
def _check_for_update(self, force=False):
707+
"""Check for app updates. If force=True, always show result."""
708+
try:
709+
from update_app import check_for_update, get_current_version
710+
update_info = check_for_update()
711+
712+
if update_info:
713+
# Skip if user chose to skip this version (unless forced)
714+
if not force:
715+
skip = self.config.get_skip_version()
716+
if skip and skip == update_info['latest']:
717+
return
718+
719+
from update_dialog import UpdateDialog
720+
dialog = UpdateDialog(self, update_info)
721+
dialog.exec()
722+
723+
if dialog.result_action == UpdateDialog.UPDATE_NOW:
724+
self._do_app_update(update_info)
725+
elif dialog.result_action == UpdateDialog.SKIP_VERSION:
726+
self.config.set_skip_version(update_info['latest'])
727+
elif force:
728+
current = get_current_version()
729+
QMessageBox.information(
730+
self,
731+
tr('updater.title'),
732+
tr('updater.no_updates', version=current)
733+
)
734+
except Exception as e:
735+
if force:
736+
QMessageBox.warning(
737+
self,
738+
tr('updater.title'),
739+
tr('updater.error', error=str(e))
740+
)
741+
742+
def _do_app_update(self, update_info: dict):
743+
"""Download update and launch updater script."""
744+
try:
745+
from update_app import download_update, create_updater_script, launch_updater_and_exit
746+
747+
self.info_label.setText(tr('updater.downloading'))
748+
QApplication.processEvents()
749+
750+
# Download
751+
zip_path = download_update(update_info['url'])
752+
753+
self.info_label.setText(tr('updater.extracting'))
754+
QApplication.processEvents()
755+
756+
# Create bat script
757+
bat_path = create_updater_script(zip_path, update_info['latest'])
758+
759+
self.info_label.setText(tr('updater.success'))
760+
QApplication.processEvents()
761+
762+
# Save state before exit
763+
self.save_window_state()
764+
765+
# Launch updater and quit
766+
launch_updater_and_exit(bat_path)
767+
QApplication.quit()
768+
769+
except Exception as e:
770+
QMessageBox.critical(
771+
self,
772+
tr('updater.title'),
773+
tr('updater.error', error=str(e))
774+
)
775+
695776
def save_progress(self, position_sec, file_path):
696777
"""Save playback progress."""
697778
# logging.debug(f"save_progress called for {file_path}")

resources/translations/en.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"screenshot": "Take Screenshot",
2020
"frame_step": "Next Frame",
2121
"frame_back": "Previous Frame",
22-
"show_markers": "Show Bookmarks"
22+
"show_markers": "Show Bookmarks",
23+
"check_updates": "Check for Updates..."
2324
},
2425
"library": {
2526
"search_placeholder": "Search...",
@@ -131,7 +132,8 @@
131132
"deess_tooltip": "De-esser",
132133
"mono": "Mono",
133134
"mono_tooltip": "Mono Mode"
134-
}
135+
},
136+
"filter_tags": "Filter by tags"
135137
},
136138
"status": {
137139
"ready": "Ready",
@@ -260,6 +262,7 @@
260262
"scan_existing_videos": " • Videos: {count}",
261263
"scan_searching": "🔍 Searching for video courses...",
262264
"scan_found_folders": " Found folders: {count} ({time} sec)",
265+
"availability_check": "🔍 Checking availability...",
263266
"process_title": "PROCESSING VIDEO COURSES",
264267
"process_progress": "[{current}/{total}] ({percent}%) {name}...",
265268
"process_info": " {info}",
@@ -355,5 +358,22 @@
355358
"watched_time": "Watched",
356359
"progress": "Progress",
357360
"subtitle": "Total videos: {total}"
361+
},
362+
"updater": {
363+
"title": "Application Update",
364+
"checking": "Checking for updates...",
365+
"available": "New version available: {version}",
366+
"current_version": "Current version: {version}",
367+
"new_version": "New version: {version}",
368+
"changelog": "What's new:",
369+
"update_now": "Update and Restart",
370+
"later": "Later",
371+
"skip_version": "Skip this version",
372+
"downloading": "Downloading update...",
373+
"extracting": "Installing update...",
374+
"success": "Update downloaded. Application restarting...",
375+
"no_updates": "You have the latest version ({version})",
376+
"error": "Update error: {error}",
377+
"check_updates_auto": "Check for updates on startup"
358378
}
359379
}

resources/translations/ru.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"screenshot": "Сделать скриншот",
2020
"frame_step": "Следующий кадр",
2121
"frame_back": "Предыдущий кадр",
22-
"show_markers": "Показать закладки"
22+
"show_markers": "Показать закладки",
23+
"check_updates": "Проверить обновления..."
2324
},
2425
"library": {
2526
"search_placeholder": "Поиск...",
@@ -131,7 +132,8 @@
131132
"deess_tooltip": "Де-эссер",
132133
"mono": "Моно",
133134
"mono_tooltip": "Моно режим"
134-
}
135+
},
136+
"filter_tags": "Фильтр по тегам"
135137
},
136138
"status": {
137139
"error": "Ошибка",
@@ -260,6 +262,7 @@
260262
"scan_existing_videos": " • Видео: {count}",
261263
"scan_searching": "🔍 Поиск видеокурсов...",
262264
"scan_found_folders": " Найдено папок: {count} ({time} сек)",
265+
"availability_check": "🔍 Проверка доступности...",
263266
"process_title": "ОБРАБОТКА ВИДЕОКУРСОВ",
264267
"process_progress": "[{current}/{total}] ({percent}%) {name}...",
265268
"process_info": " {info}",
@@ -355,5 +358,22 @@
355358
"watched_time": "Просмотрено",
356359
"progress": "Прогресс",
357360
"subtitle": "Всего видео: {total}"
361+
},
362+
"updater": {
363+
"title": "Обновление приложения",
364+
"checking": "Проверка обновлений...",
365+
"available": "Доступна новая версия: {version}",
366+
"current_version": "Текущая версия: {version}",
367+
"new_version": "Новая версия: {version}",
368+
"changelog": "Что нового:",
369+
"update_now": "Обновить и перезапустить",
370+
"later": "Позже",
371+
"skip_version": "Пропустить эту версию",
372+
"downloading": "Загрузка обновления...",
373+
"extracting": "Установка обновления...",
374+
"success": "Обновление загружено. Приложение перезапускается...",
375+
"no_updates": "У вас установлена последняя версия ({version})",
376+
"error": "Ошибка обновления: {error}",
377+
"check_updates_auto": "Проверять обновления при запуске"
358378
}
359379
}

resources/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.7
1+
1.3.0

settings_dialog.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from PyQt6.QtWidgets import (
99
QDialog, QVBoxLayout, QGroupBox, QTreeWidget, QTreeWidgetItem, QPushButton,
1010
QHBoxLayout, QFileDialog, QStyle, QMessageBox, QLabel, QProgressBar, QTextEdit,
11-
QFrame
11+
QFrame, QCheckBox
1212
)
1313
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QThread
1414
from PyQt6.QtGui import QTextCursor, QIcon
@@ -91,8 +91,17 @@ def setup_ui(self):
9191
self.ffmpeg_btn = QPushButton(tr('settings.ffmpeg_checking'))
9292
self.ffmpeg_btn.clicked.connect(self.update_ffmpeg)
9393
deps_layout.addWidget(self.ffmpeg_btn)
94+
95+
self.app_update_btn = QPushButton(tr('updater.checking'))
96+
self.app_update_btn.clicked.connect(self.update_app)
97+
deps_layout.addWidget(self.app_update_btn)
9498

9599
deps_layout.addStretch()
100+
101+
self.auto_update_chk = QCheckBox(tr('updater.check_updates_auto'))
102+
self.auto_update_chk.setChecked(True)
103+
deps_layout.addWidget(self.auto_update_chk)
104+
96105
deps_group.setLayout(deps_layout)
97106

98107
storage_group = QGroupBox(tr('settings.storage_group'))
@@ -117,6 +126,7 @@ def setup_ui(self):
117126

118127
QTimer.singleShot(100, self.check_libmpv_version)
119128
QTimer.singleShot(200, self.check_ffmpeg_version)
129+
QTimer.singleShot(300, self.check_app_version)
120130

121131
save_btn = QPushButton(tr('settings.save'))
122132
save_btn.setIcon(self.icons.get('save', QIcon()))
@@ -243,6 +253,8 @@ def load_current_settings(self):
243253
self.pathslist.addTopLevelItem(item)
244254
self._validate_path(item)
245255

256+
self.auto_update_chk.setChecked(self.config.get_check_updates_on_start())
257+
246258
def _validate_path(self, item):
247259
path = item.text(0)
248260
if os.path.exists(path):
@@ -266,6 +278,7 @@ def save_settings(self):
266278

267279
self.config.set_library_paths(paths)
268280
self.config.set_excluded_library_paths(excluded_paths)
281+
self.config.set_check_updates_on_start(self.auto_update_chk.isChecked())
269282

270283
self.accept()
271284

@@ -406,3 +419,34 @@ def do_update():
406419
dialog.exec()
407420

408421
self.check_ffmpeg_version()
422+
423+
def check_app_version(self):
424+
"""Check if a newer app version is available on GitHub."""
425+
try:
426+
from update_app import get_current_version, get_latest_release, compare_versions
427+
current = get_current_version()
428+
release = get_latest_release()
429+
430+
if release:
431+
latest = release['tag'].lstrip('v')
432+
if compare_versions(current, latest):
433+
self.app_update_btn.setText(f" SP Video Courses Player ({current}{latest})")
434+
self.app_update_btn.setIcon(self.icons.get('upload', QIcon()))
435+
self.app_update_btn.setToolTip(tr('updater.available', version=latest))
436+
else:
437+
self.app_update_btn.setText(f" SP Video Courses Player ({current})")
438+
self.app_update_btn.setIcon(self.icons.get('check', QIcon()))
439+
self.app_update_btn.setToolTip(tr('updater.no_updates', version=current))
440+
else:
441+
self.app_update_btn.setText(f" SP Video Courses Player ({current})")
442+
self.app_update_btn.setIcon(self.icons.get('check', QIcon()))
443+
self.app_update_btn.setToolTip(tr('updater.no_updates', version=current))
444+
except Exception as e:
445+
self.app_update_btn.setText(" SP Video Courses Player")
446+
self.app_update_btn.setIcon(self.icons.get('fail', QIcon()))
447+
self.app_update_btn.setToolTip(str(e))
448+
449+
def update_app(self):
450+
"""Trigger app update via parent window."""
451+
if self.parent() and hasattr(self.parent(), '_check_for_update'):
452+
self.parent()._check_for_update(force=True)

0 commit comments

Comments
 (0)