From cf9185e9e7cd83e599093b4b6789c5efdf560e98 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:06:41 +0200 Subject: [PATCH 01/24] feat: Enhance accent color options in settings dialog and save functionality --- src/interface/dialogs/settings.py | 36 +++++++++++++++++++++++++++++-- src/utils/config/settings.py | 7 ++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index 6b6a25e..811c4bc 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -1,9 +1,11 @@ from interface.utils.tabhelper import create_tab +from interface.dialogs.theme import _accent_selection_color from utils.config.settings import save_settings from interface.utils.svghelper import svg_icon from utils.logging.logs import consoleLog from PySide6.QtCore import Qt, QSize from utils.data.state import state +from PySide6.QtGui import QColor from PySide6 import QtWidgets from PySide6.QtWidgets import ( QLineEdit, @@ -13,9 +15,10 @@ from PySide6.QtWidgets import ( QDialog, QLabel, QHBoxLayout, - QSpinBox, QSlider, QCheckBox, + QSpinBox, QSlider, QCheckBox, QFileDialog, QComboBox, + QColorDialog ) import platform @@ -91,9 +94,31 @@ def settings_dialog(self): transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked)) # Accent Color - accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color (requires restart): ", width=180, height=30) + accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color: ", width=180, height=30) + accent_color_button = QPushButton("") + accent_color_button.setFixedSize(21, 21) + accent_color_button.setCursor(Qt.CursorShape.PointingHandCursor) + + def update_accent_button_color(color_str): + c = color_str.strip() + accent_color_button.setStyleSheet( + f"QPushButton {{ background-color: {c if c else 'transparent'}; border: 1px solid palette(mid); border-radius: 3px; }}" + ) + + def browse_accent_color(): + current = accent_color_input.text().strip() + initial = QColor(current) if current else QColor() + color = QColorDialog.getColor(initial, dialog, "Select Accent Color") + if color.isValid(): + accent_color_input.setText(color.name()) + + accent_color_input.textChanged.connect(update_accent_button_color) + accent_color_button.clicked.connect(browse_accent_color) accent_color_input.setPlaceholderText("e.g. #fca7d7") accent_color_input.setText(state.accent_color) + update_accent_button_color(state.accent_color) + accent_color_container.layout().insertWidget(1, accent_color_button) + # API URL Widget api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30) @@ -299,6 +324,13 @@ def settings_dialog(self): image_position=image_position_combo.currentText(), accent_color=accent_color_input.text().strip() ) + color = _accent_selection_color() + if color: + self.tracker_list.setStyleSheet( + f"QComboBox QAbstractItemView::item:selected {{ background: {color}; }}" + ) + else: + self.tracker_list.setStyleSheet("") cancel_btn.clicked.connect(dialog.reject) layout.addWidget(cancel_btn) diff --git a/src/utils/config/settings.py b/src/utils/config/settings.py index 09df0da..115fd7a 100644 --- a/src/utils/config/settings.py +++ b/src/utils/config/settings.py @@ -2,6 +2,9 @@ from network.libtorrent_int import update_settings from utils.config.config import create_config from utils.logging.logs import consoleLog from utils.data.state import state +import qdarktheme +import platform + @@ -36,6 +39,10 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee state.image_position = image_position if accent_color is not None: state.accent_color = accent_color + custom_colors = {"primary": state.accent_color} if state.accent_color else {} + if state.window_transparency and platform.system() != "Windows": + custom_colors["background"] = "#00000000" + qdarktheme.setup_theme("auto", custom_colors=custom_colors if custom_colors else None) update_settings() # Update LibTorrent Session Settings consoleLog("Saved Settings") From cc51f57e183c868862f249a1901fd6403b22f0d4 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:41:32 +0200 Subject: [PATCH 02/24] fix: Add locks, logging refactor, and bugfixes Improve thread-safety, logging, and various bug fixes across the app. Key changes: - Add and use state.downloads_lock around accesses to state.active_downloads in multiple modules (UI, libtorrent, misc, direct downloads) to prevent race conditions. - Prevent concurrent dl_status_loop runs using a non-blocking _loop_lock and ensure proper iteration over a snapshot of downloads. - Add metadata timeout when waiting for torrent metadata and fix free-space check logic to remove or reject downloads appropriately. - Refactor download log persistence: add _load_downloads and _save_downloads helpers and centralize JSON read/write logic in utils/logging/logs.py. - Optimize DirectDownloadStatus speed window by switching to deque and using popleft to trim older samples. - Trim console log widget to 500 lines to avoid unbounded growth. - Add small state fields for custom image positioning (x/y and enabled flag). - Fix Steamrip scraper caching behavior and make Uztracker return an empty list on fetch failures. - Add defensive checks in run_download to handle missing links. These changes focus on stability and correctness for concurrent download handling and durable logging. --- src/data/sources/steamrip.py | 5 +- src/data/sources/uztracker.py | 2 +- src/interface/dialogs/contextmenu.py | 21 +++-- src/interface/dialogs/downloadmodel.py | 6 +- src/interface/gui.py | 9 ++ src/network/direct_download/__init__.py | 10 ++- src/network/direct_download/status.py | 8 +- src/network/libtorrent_int.py | 96 +++++++++++--------- src/network/libtorrent_misc.py | 25 ++++-- src/utils/data/state.py | 3 + src/utils/logging/logs.py | 113 +++++++----------------- src/utils/network/download.py | 6 ++ 12 files changed, 149 insertions(+), 155 deletions(-) diff --git a/src/data/sources/steamrip.py b/src/data/sources/steamrip.py index 71de40e..387d860 100644 --- a/src/data/sources/steamrip.py +++ b/src/data/sources/steamrip.py @@ -50,6 +50,9 @@ class SteamripScraper: for i in range(len(names)): ret.append({"title" : names[i], "url" : links[i]}) + self.cache["data"] = ret + self.cache["last_fetched"] = current_time + return ret def scrape_steamrip_game_downloads(self, gamelink): @@ -72,8 +75,6 @@ class SteamripScraper: if len(link) != 1: ret.append(link) - self.cache["data"] = ret - return ret def get_download_link(self, post: Dict): diff --git a/src/data/sources/uztracker.py b/src/data/sources/uztracker.py index 027394e..1e871af 100644 --- a/src/data/sources/uztracker.py +++ b/src/data/sources/uztracker.py @@ -43,7 +43,7 @@ class UztrackerScraper: except requests.RequestException as e: consoleLog(f"Failed to fetch {search_url}: {e}") - return None + return [] def get_download_link(self, post: Dict): return get_magnet_link(post["url"]) diff --git a/src/interface/dialogs/contextmenu.py b/src/interface/dialogs/contextmenu.py index fb8c98f..1125b43 100644 --- a/src/interface/dialogs/contextmenu.py +++ b/src/interface/dialogs/contextmenu.py @@ -34,10 +34,12 @@ class ContextMenu: if not hasattr(self, '_context_menu_row'): return row = self._context_menu_row - if row < 0 or row >= len(state.active_downloads): - return - magnet_link = list(state.active_downloads.keys())[row] - magnetdl = state.active_downloads[magnet_link] + with state.downloads_lock: + if row < 0 or row >= len(state.active_downloads): + return + keys = list(state.active_downloads.keys()) + magnet_link = keys[row] + magnetdl = state.active_downloads[magnet_link] download_path = magnetdl.save_path() if download_path and os.path.exists(download_path): if platform.system() == "Windows": @@ -51,9 +53,11 @@ class ContextMenu: if not hasattr(self, '_context_menu_row'): return row = self._context_menu_row - if row < 0 or row >= len(state.active_downloads): - return - magnet_link = list(state.active_downloads.keys())[row] + with state.downloads_lock: + if row < 0 or row >= len(state.active_downloads): + return + keys = list(state.active_downloads.keys()) + magnet_link = keys[row] clipboard = QtWidgets.QApplication.clipboard() clipboard.setText(magnet_link) @@ -64,7 +68,8 @@ class ContextMenu: with state.downloads_lock: if row < 0 or row >= len(state.active_downloads): return - magnet_link = list(state.active_downloads.keys())[row] + keys = list(state.active_downloads.keys()) + magnet_link = keys[row] magnetdl = state.active_downloads[magnet_link] # Cache the name BEFORE removing from session diff --git a/src/interface/dialogs/downloadmodel.py b/src/interface/dialogs/downloadmodel.py index 6cb0237..d6e872e 100644 --- a/src/interface/dialogs/downloadmodel.py +++ b/src/interface/dialogs/downloadmodel.py @@ -34,7 +34,8 @@ class DownloadModel(QAbstractTableModel): return None try: - magnet_link = list(state.active_downloads.keys())[index.row()] + keys = list(state.active_downloads.keys()) + magnet_link = keys[index.row()] magnetdl = state.active_downloads[magnet_link] status = magnetdl.status() except (IndexError, KeyError, RuntimeError): @@ -101,7 +102,8 @@ class DownloadModel(QAbstractTableModel): if row >= len(state.active_downloads) or row < 0: return try: - magnet_link = list(state.active_downloads.keys())[row] + keys = list(state.active_downloads.keys()) + magnet_link = keys[row] magnetdl = state.active_downloads[magnet_link] status = magnetdl.status() except (IndexError, KeyError, RuntimeError): diff --git a/src/interface/gui.py b/src/interface/gui.py index cd1765a..348ef41 100644 --- a/src/interface/gui.py +++ b/src/interface/gui.py @@ -315,6 +315,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): def _on_log_signal(self, text): if hasattr(self, 'consoleLog'): self.consoleLog.append(text) + # Delete lines if exceeding 500 + doc = self.consoleLog.document() + if doc.blockCount() > 500: + cursor = self.consoleLog.textCursor() + cursor.movePosition(cursor.MoveOperation.Start) + cursor.movePosition(cursor.MoveOperation.Down, cursor.MoveMode.KeepAnchor, doc.blockCount() - 500) + cursor.removeSelectedText() + cursor.deleteChar() + self.consoleLog.verticalScrollBar().setValue( self.consoleLog.verticalScrollBar().maximum() ) diff --git a/src/network/direct_download/__init__.py b/src/network/direct_download/__init__.py index a082b51..f423484 100644 --- a/src/network/direct_download/__init__.py +++ b/src/network/direct_download/__init__.py @@ -15,9 +15,10 @@ def add_direct_download(url: str, title: str, dl_path: Optional[str] = None, hea if dl_path is None: dl_path = state.download_path - if url in state.active_downloads: - consoleLog(f"Download already active: {title}") - return + with state.downloads_lock: + if url in state.active_downloads: + consoleLog(f"Download already active: {title}") + return filename = ( detect_filename_from_headers(url, DirectDownloadHandle.USER_AGENT) @@ -26,7 +27,8 @@ def add_direct_download(url: str, title: str, dl_path: Optional[str] = None, hea ) handle = DirectDownloadHandle(url, filename, dl_path, headers, single_threaded) - state.active_downloads[url] = handle + with state.downloads_lock: + state.active_downloads[url] = handle add_download_log(title, url, "", False) handle.start() consoleLog(f"Started direct download: {filename}") diff --git a/src/network/direct_download/status.py b/src/network/direct_download/status.py index aabb87b..7f997a0 100644 --- a/src/network/direct_download/status.py +++ b/src/network/direct_download/status.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from collections import deque from typing import Optional import libtorrent as lt import threading @@ -31,7 +32,7 @@ class DirectDownloadStatus: self._upload_rate = 0 self._chunk_bytes: dict[int, int] = {} - self._speed_window: list[tuple[float, int]] = [] + self._speed_window: deque[tuple[float, int]] = deque() self._speed_window_size = 3.0 @property @@ -65,9 +66,8 @@ class DirectDownloadStatus: self._speed_window.append((now, self._total_wanted_done)) cutoff = now - self._speed_window_size - self._speed_window = [ - (t, b) for t, b in self._speed_window if t >= cutoff - ] + while self._speed_window and self._speed_window[0][0] < cutoff: + self._speed_window.popleft() if len(self._speed_window) >= 2: oldest_time, oldest_bytes = self._speed_window[0] dt = now - oldest_time diff --git a/src/network/libtorrent_int.py b/src/network/libtorrent_int.py index db31889..a1e59da 100644 --- a/src/network/libtorrent_int.py +++ b/src/network/libtorrent_int.py @@ -10,7 +10,7 @@ import time import os -loop_running = False +_loop_lock = threading.Lock() def get_free_space_mb(dirname): if platform.system() == 'Windows': @@ -23,7 +23,9 @@ def get_free_space_mb(dirname): def check_space(): while not state.shutdown_event.is_set(): - for _, magnetdl in list(state.active_downloads.items()): + with state.downloads_lock: + items = list(state.active_downloads.items()) + for _, magnetdl in items: try: status = magnetdl.status() except RuntimeError: @@ -81,9 +83,11 @@ def add_download(magnet_uri): init_session() free_space = get_free_space_mb(state.download_path) - if magnet_uri in state.active_downloads: + with state.downloads_lock: + already_active = magnet_uri in state.active_downloads + handle = state.active_downloads.get(magnet_uri) if already_active else None + if already_active: try: - handle = state.active_downloads[magnet_uri] status = handle.status() if status.has_metadata: @@ -93,7 +97,8 @@ def add_download(magnet_uri): consoleLog(f"File Deleted, redownloading: {status.name}") state.dl_session.remove_torrent(handle) - del state.active_downloads[magnet_uri] + with state.downloads_lock: + del state.active_downloads[magnet_uri] else: consoleLog("Skipping Downloading, download already running...") return False @@ -102,23 +107,26 @@ def add_download(magnet_uri): return False except RuntimeError as e: consoleLog(f"Error in LibTorrent Handle: {e}") - del state.active_downloads[magnet_uri] + with state.downloads_lock: + del state.active_downloads[magnet_uri] try: params = lt.parse_magnet_uri(magnet_uri) params.save_path = state.download_path handle = state.dl_session.add_torrent(params) + metadata_timeout = 60 + metadata_start = time.time() while not handle.has_metadata(): + if time.time() - metadata_start > metadata_timeout: + state.dl_session.remove_torrent(handle) + consoleLog("Timed out waiting for torrent metadata") + return False time.sleep(1) total_size = handle.get_torrent_info().total_size() - if free_space > total_size: - magnetdl = lt.parse_magnet_uri(magnet_uri) - magnetdl.save_path = state.download_path - download = state.dl_session.add_torrent(magnetdl) - else: + if free_space <= total_size: state.dl_session.remove_torrent(handle) consoleLog("Not enough free space to download this item.") return False @@ -126,8 +134,9 @@ def add_download(magnet_uri): except Exception as e: consoleLog(f"Failed to add torrent or fetch info: {e}") return False - if download: - state.active_downloads[magnet_uri] = download + if handle: + with state.downloads_lock: + state.active_downloads[magnet_uri] = handle consoleLog(f"Added {magnet_uri} to downloads") run_thread(threading.Thread(target=dl_status_loop)) @@ -140,9 +149,10 @@ def add_seed(magnet_uri, file_path): init_session() - if magnet_uri in state.active_downloads: - consoleLog("Already seeding this torrent") - return False + with state.downloads_lock: + if magnet_uri in state.active_downloads: + consoleLog("Already seeding this torrent") + return False try: magnetdl = lt.parse_magnet_uri(magnet_uri) @@ -151,43 +161,43 @@ def add_seed(magnet_uri, file_path): except Exception as e: consoleLog(f"Failed to add seed: {e}") return False - state.active_downloads[magnet_uri] = handle + with state.downloads_lock: + state.active_downloads[magnet_uri] = handle state.seeded_magnets.add(magnet_uri) return True def dl_status_loop(): - global loop_running - if loop_running == True: + if not _loop_lock.acquire(blocking=False): return - loop_running = True - completed_set = set() - - if not state.active_downloads: - consoleLog("No active downloads") - loop_running = False - return - - while state.active_downloads and not state.shutdown_event.is_set(): - for magnet_uri, magnetdl in list(state.active_downloads.items()): - try: - status = magnetdl.status() - except RuntimeError: - continue - - if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set: - consoleLog(f"Download completed: {status.name}") - - completed_set.add(magnet_uri) + try: + completed_set = set() if not state.active_downloads: - loop_running = False - break + consoleLog("No active downloads") + return - time.sleep(1) - - loop_running = False + while state.active_downloads and not state.shutdown_event.is_set(): + with state.downloads_lock: + items = list(state.active_downloads.items()) + for magnet_uri, magnetdl in items: + try: + status = magnetdl.status() + except RuntimeError: + continue + + if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set: + consoleLog(f"Download completed: {status.name}") + + completed_set.add(magnet_uri) + + if not state.active_downloads: + break + + time.sleep(1) + finally: + _loop_lock.release() def update_settings(): diff --git a/src/network/libtorrent_misc.py b/src/network/libtorrent_misc.py index eeb471e..b1c6664 100644 --- a/src/network/libtorrent_misc.py +++ b/src/network/libtorrent_misc.py @@ -9,19 +9,23 @@ import os def cleanup_session(): if state.dl_session is not None: - for magnetdl in state.active_downloads.values(): - if hasattr(magnetdl, 'pause'): # Check it's a handle - magnetdl.pause() + with state.downloads_lock: + for magnetdl in state.active_downloads.values(): + if hasattr(magnetdl, 'pause'): # check it's a handle + magnetdl.pause() del state.dl_session state.dl_session = None - state.active_downloads.clear() + with state.downloads_lock: + state.active_downloads.clear() def send_notification(shutdown_event): notified = set() while not shutdown_event.is_set(): try: - for magnet_uri, magnetdl in list(state.active_downloads.items()): + with state.downloads_lock: + items = list(state.active_downloads.items()) + for magnet_uri, magnetdl in items: if isinstance(magnetdl, dict): continue @@ -42,7 +46,9 @@ def update_log(shutdown_event): updated = set() while not shutdown_event.is_set(): try: - for magnet_uri, magnetdl in list(state.active_downloads.items()): + with state.downloads_lock: + items = list(state.active_downloads.items()) + for magnet_uri, magnetdl in items: if isinstance(magnetdl, dict): continue @@ -63,7 +69,9 @@ def update_log(shutdown_event): def check_deleted_files(shutdown_event): while not shutdown_event.is_set(): try: - for magnet_uri, magnetdl in list(state.active_downloads.items()): + with state.downloads_lock: + items = list(state.active_downloads.items()) + for magnet_uri, magnetdl in items: if isinstance(magnetdl, dict): continue @@ -75,7 +83,8 @@ def check_deleted_files(shutdown_event): if not os.path.exists(file_path): consoleLog(f"Registered File Deletion: {status.name}") state.dl_session.remove_torrent(magnetdl) - del state.active_downloads[magnet_uri] + with state.downloads_lock: + del state.active_downloads[magnet_uri] except Exception as e: consoleLog(f"Exception while checking for file deletions: {e}") time.sleep(5) \ No newline at end of file diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 1004d0a..d1dc6e0 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -38,6 +38,9 @@ class AppState(QObject): self._image_opacity: int = 100 self._image_as_wallpaper: bool = False self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center + self._image_custom_position: bool = False + self._image_x: int = 0 + self._image_y: int = 0 # Trackers / Scraping self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers diff --git a/src/utils/logging/logs.py b/src/utils/logging/logs.py index da8ade4..03b9de4 100644 --- a/src/utils/logging/logs.py +++ b/src/utils/logging/logs.py @@ -8,6 +8,27 @@ import os import re +def _downloads_file_path() -> str: + return os.path.join(state.settings_path, "downloads.json") + +def _load_downloads() -> list[Download]: + downloads_file = _downloads_file_path() + if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: + try: + with open(downloads_file, "r") as file: + existing_data = json.load(file) + return [Download(**d) for d in existing_data.get("data", [])] + except (json.JSONDecodeError, TypeError) as e: + consoleLog(f"Error loading downloads.json: {e}") + return [] + +def _save_downloads(downloads: list[Download]) -> DownloadList: + download_list = DownloadList(data=downloads, count=len(downloads)) + with open(_downloads_file_path(), "w") as file: + json.dump(asdict(download_list), file, indent=4) + return download_list + + def add_download_log(title, url, magnet_uri, completed) -> DownloadList: # wait for metadata outside the lock to avoid blocking other threads magnetdl = state.active_downloads.get(magnet_uri) @@ -28,17 +49,7 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList: return _add_download_log_inner(title, url, magnet_uri, completed, path) def _add_download_log_inner(title, url, magnet_uri, completed, path) -> DownloadList: - downloads_file = os.path.join(state.settings_path, "downloads.json") - - if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: - try: - with open(downloads_file, "r") as file: - existing_data = json.load(file) - downloads = [Download(**d) for d in existing_data.get("data", [])] - except json.JSONDecodeError: - downloads = [] - else: - downloads = [] + downloads = _load_downloads() if any((magnet_uri and d.magnet_uri == magnet_uri) or (url and d.url == url) for d in downloads): if magnet_uri and magnet_uri in state.active_downloads: @@ -60,30 +71,14 @@ def _add_download_log_inner(title, url, magnet_uri, completed, path) -> Download )) consoleLog(f"Added {title} to Log File") - - download_list = DownloadList(data=downloads, count=len(downloads)) - with open(downloads_file, "w") as file: - json.dump(asdict(download_list), file, indent=4) - - return download_list + return _save_downloads(downloads) def remove_download_log(magnet_uri) -> DownloadList: with state.downloads_lock: return _remove_download_log_inner(magnet_uri) def _remove_download_log_inner(magnet_uri) -> DownloadList: - downloads_file = os.path.join(state.settings_path, "downloads.json") - - if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: - try: - with open(downloads_file, "r") as file: - existing_data = json.load(file) - downloads = [Download(**d) for d in existing_data.get("data", [])] - except json.JSONDecodeError: - downloads = [] - else: - downloads = [] - + downloads = _load_downloads() magnet_link = (magnet_uri or "").strip() if not magnet_link: @@ -93,30 +88,14 @@ def _remove_download_log_inner(magnet_uri) -> DownloadList: downloads = [d for d in downloads if (getattr(d, 'magnet_uri', None) or "").strip() != magnet_link and (getattr(d, 'url', None) or "").strip() != magnet_link] consoleLog(f"Removed {title} from Log File") - - download_list = DownloadList(data=downloads, count=len(downloads)) - with open(downloads_file, "w") as file: - json.dump(asdict(download_list), file, indent=4) - - return download_list + return _save_downloads(downloads) def update_download_completed(magnet_uri, completed) -> DownloadList: with state.downloads_lock: return _update_download_completed_inner(magnet_uri, completed) def _update_download_completed_inner(magnet_uri, completed) -> DownloadList: - downloads_file = os.path.join(state.settings_path, "downloads.json") - - if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: - try: - with open(downloads_file, "r") as file: - existing_data = json.load(file) - downloads = [Download(**d) for d in existing_data.get("data", [])] - except (json.JSONDecodeError, TypeError) as e: - consoleLog(f"Error loading downloads.json: {e}") - downloads = [] - else: - downloads = [] + downloads = _load_downloads() identifier = (magnet_uri or "").strip() if state.debug: @@ -145,13 +124,8 @@ def _update_download_completed_inner(magnet_uri, completed) -> DownloadList: consoleLog("No matching download found to update") return DownloadList(data=downloads, count=len(downloads)) - download_list = DownloadList(data=downloads, count=len(downloads)) - with open(downloads_file, "w") as file: - json.dump(asdict(download_list), file, indent=4) - consoleLog("Updated download log") - - return download_list + return _save_downloads(downloads) def get_download_logs() -> DownloadList: @@ -159,18 +133,7 @@ def get_download_logs() -> DownloadList: return _get_download_logs_inner() def _get_download_logs_inner() -> DownloadList: - downloads_file = os.path.join(state.settings_path, "downloads.json") - - if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: - try: - with open(downloads_file, "r") as file: - existing_data = json.load(file) - downloads = [Download(**d) for d in existing_data.get("data", [])] - except json.JSONDecodeError: - downloads = [] - else: - downloads = [] - + downloads = _load_downloads() return DownloadList(data=downloads, count=len(downloads)) @@ -195,18 +158,7 @@ def update_download_completed_by_hash(info_hash, completed) -> DownloadList: return _update_download_completed_by_hash_inner(info_hash, completed) def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadList: - downloads_file = os.path.join(state.settings_path, "downloads.json") - - if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0: - try: - with open(downloads_file, "r") as file: - existing_data = json.load(file) - downloads = [Download(**d) for d in existing_data.get("data", [])] - except (json.JSONDecodeError, TypeError) as e: - consoleLog(f"Error loading downloads.json: {e}") - downloads = [] - else: - downloads = [] + downloads = _load_downloads() info_hash_upper = (info_hash or "").upper().strip() consoleLog(f"Updating download by hash: {info_hash_upper}") @@ -228,13 +180,8 @@ def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadLi consoleLog("No matching download found to update") return DownloadList(data=downloads, count=len(downloads)) - download_list = DownloadList(data=downloads, count=len(downloads)) - with open(downloads_file, "w") as file: - json.dump(asdict(download_list), file, indent=4) - consoleLog("Updated download log") - - return download_list + return _save_downloads(downloads) def set_main_window(window): diff --git a/src/utils/network/download.py b/src/utils/network/download.py index 86a8152..74362a5 100644 --- a/src/utils/network/download.py +++ b/src/utils/network/download.py @@ -37,10 +37,16 @@ def run_download(post, headers: Optional[dict] = None): if ismagnet: link = result + if not link: + consoleLog("Failed to retrieve magnet link") + return add_magnet(link) add_download_log(post.get("title", "Unknown"), "", link, False) else: link, link_headers = result if isinstance(result, tuple) else (result, None) + if not link: + consoleLog("Failed to retrieve download link") + return final_headers = headers or link_headers add_direct_download(link, post.get("title", "Unknown"), headers=final_headers, single_threaded=final_headers is not None) From 49ba699155ecc9d00d8a4075753b4e60d58c3ce1 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:44:21 +0200 Subject: [PATCH 03/24] fix: Debounce resize and cache loaded images Add a 100ms single-shot QTimer to debounce Resize events and avoid reloading/layout on every resize. Cache the loaded QImage in _cached_qimage/_cached_path and only reload from disk when the image path changes. Extract layout/apply logic into _apply_layout and clear cache in update_image_overlay when a new path is provided. These changes reduce disk I/O and repeated layout work during rapid resizes. --- src/interface/dialogs/image.py | 38 +++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index f921958..922ce46 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -1,5 +1,5 @@ from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget -from PySide6.QtCore import Qt, QSize, QEvent, QObject +from PySide6.QtCore import Qt, QSize, QEvent, QObject, QTimer from PySide6.QtGui import QImage, QPixmap from utils.data.state import state import darkdetect @@ -17,8 +17,15 @@ class Image(QObject): self.overlay_label.setGraphicsEffect(self.opacity_effect) self._current_image_path = None + self._cached_qimage = None self._wallpaper_active = False self._original_stylesheets = {} + + self._resize_timer = QTimer(self) + self._resize_timer.setSingleShot(True) + self._resize_timer.setInterval(100) + self._resize_timer.timeout.connect(self._on_resize_finished) + parent.installEventFilter(self) state.image_changed.connect(self.update_image_overlay) @@ -28,9 +35,13 @@ class Image(QObject): def eventFilter(self, obj, event): if obj == self.application and event.type() == QEvent.Type.Resize: if self._current_image_path: - self._load_and_display(self._current_image_path) + self._resize_timer.start() return False + def _on_resize_finished(self): + if self._current_image_path: + self._apply_layout() + def _set_wallpaper_transparency(self, enabled): if self._wallpaper_active == enabled: return @@ -117,12 +128,27 @@ class Image(QObject): self._set_wallpaper_transparency(False) return self._current_image_path = image_path - parent = self.application - image = QImage(image_path) - if image.isNull(): + + # Only reload from disk when the path actually changed + if self._cached_qimage is None or image_path != getattr(self, '_cached_path', None): + image = QImage(image_path) + if image.isNull(): + self.overlay_label.hide() + self._cached_qimage = None + return + self._cached_qimage = image + self._cached_path = image_path + + self._apply_layout() + + def _apply_layout(self): + if self._cached_qimage is None or not state.image_enabled: self.overlay_label.hide() return + parent = self.application + image = self._cached_qimage + if getattr(state, "image_as_wallpaper", False): scaled_image = image.scaled( parent.size(), @@ -184,4 +210,6 @@ class Image(QObject): self.overlay_label.show() def update_image_overlay(self, new_image_path): + if new_image_path != getattr(self, '_cached_path', None): + self._cached_qimage = None self._load_and_display(new_image_path) \ No newline at end of file From a29f58c023b8497929058ab4272639c8e016a7c1 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:52:08 +0200 Subject: [PATCH 04/24] Add fast layout update for image overlay Introduce _apply_layout_fast to update the image overlay quickly on window resize using the cached QImage and FastTransformation. Called from the Resize event handler before the resize timer, it handles wallpaper mode (fast scale, center-crop to parent) and non-wallpaper mode (positioning by state.image_position and state.image_offset) to provide immediate visual feedback without running the heavier _apply_layout. --- src/interface/dialogs/image.py | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 922ce46..408f880 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -35,6 +35,7 @@ class Image(QObject): def eventFilter(self, obj, event): if obj == self.application and event.type() == QEvent.Type.Resize: if self._current_image_path: + self._apply_layout_fast() self._resize_timer.start() return False @@ -141,6 +142,41 @@ class Image(QObject): self._apply_layout() + def _apply_layout_fast(self): + if self._cached_qimage is None or not state.image_enabled: + return + + parent = self.application + image = self._cached_qimage + + if getattr(state, "image_as_wallpaper", False): + scaled = image.scaled( + parent.size(), + Qt.AspectRatioMode.KeepAspectRatioByExpanding, + Qt.TransformationMode.FastTransformation + ) + cx = (scaled.width() - parent.width()) // 2 + cy = (scaled.height() - parent.height()) // 2 + cropped = scaled.copy(cx, cy, parent.width(), parent.height()) + self.overlay_label.setPixmap(QPixmap.fromImage(cropped)) + self.overlay_label.setGeometry(0, 0, parent.width(), parent.height()) + else: + w = self.overlay_label.width() + h = self.overlay_label.height() + pos = state.image_position + off = int(state.image_offset) + if pos == "top-left": + x, y = off, off + elif pos == "top-right": + x, y = parent.width() - w - off, off + elif pos == "bottom-left": + x, y = off, parent.height() - h - off + elif pos == "center": + x, y = (parent.width() - w) // 2, (parent.height() - h) // 2 + else: + x, y = parent.width() - w - off, parent.height() - h - off + self.overlay_label.move(x, y) + def _apply_layout(self): if self._cached_qimage is None or not state.image_enabled: self.overlay_label.hide() From ea2470587d137fdb43e64c15954a8312ebceeb06 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:30:27 +0200 Subject: [PATCH 05/24] feat: Add darkreader lock and 404 tab handling Add a tag to prevent Dark Reader from altering the docs styling. Adjust client-side routing so only '/' is replaced with the 'start' tab; unknown paths now open the 'notfound' tab instead of being redirected to start. --- docs/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 35f9f4a..53b4b93 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,6 +3,7 @@ + SoftwareManager - Main @@ -225,9 +226,11 @@ openTab(null, 'api', false); } else if (currentPath === '/docs') { openTab(null, 'docs', false); - } else { + } else if (currentPath === '/') { history.replaceState({ tab: 'start' }, "", "/"); - openTab(null, 'start', false); + openTab(null, 'start', false); + } else { + openTab(null, 'notfound', false); } }; From 6c148647d8ba7cc403a95b3593b5c0138507c044 Mon Sep 17 00:00:00 2001 From: shayaa <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:00:46 +0200 Subject: [PATCH 06/24] Configure Dependabot for daily updates Updated Dependabot configuration to include daily updates for GitHub Actions and Python dependencies. --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c80b447 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + # Keeps your GitHub Actions (CI/CD) up to date daily + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + # Keeps your Python dependencies (requirements.txt / pyproject.toml) up to date daily + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" From 348c5244532ba7f2bcfcaecf8c7b12dc4a20f215 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:01:40 +0000 Subject: [PATCH 07/24] build(deps): bump actions/download-artifact from 4 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/create-release.yml | 2 +- .github/workflows/dev.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7cddcd5..3145eec 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -214,7 +214,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: path: artifacts diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f18f4e2..0e8fa49 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -219,7 +219,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: path: artifacts From 5486e75ced8ab94748211570f4bf67a43a64de90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:01:45 +0000 Subject: [PATCH 08/24] build(deps): bump actions/upload-artifact from 4 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/create-release.yml | 4 ++-- .github/workflows/dev.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7cddcd5..49de32e 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -190,7 +190,7 @@ jobs: - name: Upload build artifact (Windows) if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: | @@ -200,7 +200,7 @@ jobs: - name: Upload build artifact (Linux/macOS) if: runner.os != 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: dist-final/${{ matrix.artifact_name }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f18f4e2..d504830 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -195,7 +195,7 @@ jobs: - name: Upload build artifact (Windows) if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: | @@ -205,7 +205,7 @@ jobs: - name: Upload build artifact (Linux/macOS) if: runner.os != 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: dist-final/${{ matrix.artifact_name }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9997bbe..7ae0de0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -170,7 +170,7 @@ jobs: - name: Upload artifact (Windows) if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: dist-final/${{ matrix.artifact_name }}.zip @@ -178,7 +178,7 @@ jobs: - name: Upload artifact (Linux/macOS) if: runner.os != 'Windows' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-build path: dist-final/${{ matrix.artifact_name }} From a31d9c10cc318dbfbac958a11657afb8cbf38cf6 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:05:27 +0200 Subject: [PATCH 09/24] fix: remove aiohttp from requirements and README --- README.md | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index 751f26e..3884415 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa darkdetect (0.7.1) pyinstaller (6.15.0) PyQtDarkTheme-fork (2.3.4) - aiohttp (3.13.0) plyer (2.1.0) psutil (7.1.0) libtorrent (2.0.11) diff --git a/requirements.txt b/requirements.txt index a5fea62..f672c33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ beautifulsoup4==4.13.5 darkdetect==0.7.1 pyinstaller==6.15.0 PyQtDarkTheme-fork==2.3.4 -aiohttp==3.13.0 plyer==2.1.0 psutil==7.1.0 libtorrent==2.0.11 From fd3a0390df7ed22b008bd051b9fcec51cf1cb314 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:13:14 +0200 Subject: [PATCH 10/24] feat: Add sync README dependencies workflow and script Introduce an automated workflow to keep the README's Python Dependencies section in sync with requirements.txt. Adds .github/workflows/sync-readme-dependencies.yml which runs scripts/sync_readme_dependencies.py on changes to requirements.txt (or manually), then opens a PR via peter-evans/create-pull-request. Adds the sync_readme_dependencies.py script that parses requirements.txt and replaces a managed block in README.md delimited by / . Also updates README.md to include those markers and correct dependency names/formatting. Additionally, change create-release.yml to use RELEASE_TOKEN (secrets.RELEASE_TOKEN) for release uploads instead of the repository GITHUB_TOKEN. --- .github/workflows/create-release.yml | 2 +- .../workflows/sync-readme-dependencies.yml | 38 +++++++++ README.md | 8 +- scripts/sync_readme_dependencies.py | 79 +++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/sync-readme-dependencies.yml create mode 100644 scripts/sync_readme_dependencies.py diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 7cddcd5..f1d6e83 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -248,4 +248,4 @@ jobs: release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-macos release/SHA256SUMS.txt env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/sync-readme-dependencies.yml b/.github/workflows/sync-readme-dependencies.yml new file mode 100644 index 0000000..5141452 --- /dev/null +++ b/.github/workflows/sync-readme-dependencies.yml @@ -0,0 +1,38 @@ +name: Sync README Dependencies + +on: + push: + branches: + - main + paths: + - requirements.txt + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Update README dependencies + run: python scripts/sync_readme_dependencies.py + + - name: Create pull request + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "Sync README dependencies" + branch: auto/sync-readme-dependencies + title: "Sync README dependencies" + body: "Automated update of the Python Dependencies section in README.md to match `requirements.txt`." + labels: documentation \ No newline at end of file diff --git a/README.md b/README.md index 3884415..da00f99 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,11 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa ## Python Dependencies (also included in requirements.txt) - ```bash + + ```text requests (2.32.2) - PySide6 (6.10.1) - beautifulsoup (44.13.5) + PySide6-Essentials (6.10.1) + beautifulsoup4 (4.13.5) darkdetect (0.7.1) pyinstaller (6.15.0) PyQtDarkTheme-fork (2.3.4) @@ -47,6 +48,7 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa libtorrent (2.0.11) libtorrent-windows-dll (0.0.3) ``` + ## Activity diff --git a/scripts/sync_readme_dependencies.py b/scripts/sync_readme_dependencies.py new file mode 100644 index 0000000..5820139 --- /dev/null +++ b/scripts/sync_readme_dependencies.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from pathlib import Path +import re + + +ROOT_DIR = Path(__file__).resolve().parent.parent +README_PATH = ROOT_DIR / "README.md" +REQUIREMENTS_PATH = ROOT_DIR / "requirements.txt" + +START_MARKER = "" +END_MARKER = "" +REQUIREMENT_PATTERN = re.compile( + r"^(?P[A-Za-z0-9_.\-\[\]]+)\s*(?P(==|~=|>=|<=|!=|>|<).+)?$" +) + + +def iter_requirements(requirements_text: str) -> list[str]: + formatted_lines: list[str] = [] + + for raw_line in requirements_text.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + match = REQUIREMENT_PATTERN.match(line) + if not match: + formatted_lines.append(f" {line}") + continue + + name = match.group("name") + specifier = (match.group("specifier") or "").strip() + + if specifier.startswith("=="): + formatted_lines.append(f" {name} ({specifier[2:]})") + elif specifier: + formatted_lines.append(f" {name} {specifier}") + else: + formatted_lines.append(f" {name}") + + return formatted_lines + + +def render_dependency_block(requirements_text: str) -> str: + dependency_lines = iter_requirements(requirements_text) + return "\n".join( + [ + START_MARKER, + " ```text", + *dependency_lines, + " ```", + END_MARKER, + ] + ) + + +def replace_managed_block(readme_text: str, rendered_block: str) -> str: + start_index = readme_text.find(START_MARKER) + end_index = readme_text.find(END_MARKER) + + if start_index == -1 or end_index == -1 or end_index < start_index: + raise RuntimeError("README dependency markers are missing or invalid.") + + end_index += len(END_MARKER) + return readme_text[:start_index] + rendered_block + readme_text[end_index:] + + +def main() -> None: + requirements_text = REQUIREMENTS_PATH.read_text(encoding="utf-8") + readme_text = README_PATH.read_text(encoding="utf-8") + rendered_block = render_dependency_block(requirements_text) + updated_readme = replace_managed_block(readme_text, rendered_block) + + if updated_readme != readme_text: + README_PATH.write_text(updated_readme, encoding="utf-8") + + +if __name__ == "__main__": + main() \ No newline at end of file From 5a7f2e150189be34829d00b1457b60733ee467d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:17:25 +0000 Subject: [PATCH 11/24] build(deps): bump darkdetect from 0.7.1 to 0.8.0 Bumps [darkdetect](https://github.com/albertosottile/darkdetect) from 0.7.1 to 0.8.0. - [Release notes](https://github.com/albertosottile/darkdetect/releases) - [Commits](https://github.com/albertosottile/darkdetect/compare/v0.7.1...v0.8.0) --- updated-dependencies: - dependency-name: darkdetect dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f672c33..a3a58e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ requests==2.32.2 PySide6-Essentials==6.10.1 beautifulsoup4==4.13.5 -darkdetect==0.7.1 +darkdetect==0.8.0 pyinstaller==6.15.0 PyQtDarkTheme-fork==2.3.4 plyer==2.1.0 From 87b5d63e1bdebac0b08a2296c183dc7d54da5170 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:23:50 +0000 Subject: [PATCH 12/24] build(deps): bump beautifulsoup4 from 4.13.5 to 4.14.3 Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.13.5 to 4.14.3. --- updated-dependencies: - dependency-name: beautifulsoup4 dependency-version: 4.14.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a3a58e1..119dd76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ requests==2.32.2 PySide6-Essentials==6.10.1 -beautifulsoup4==4.13.5 +beautifulsoup4==4.14.3 darkdetect==0.8.0 pyinstaller==6.15.0 PyQtDarkTheme-fork==2.3.4 From 32bacd1211463a5efb8fd9b4ccd4cd0b1f23d03e Mon Sep 17 00:00:00 2001 From: shayaa <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:24:50 +0200 Subject: [PATCH 13/24] Add token to sync README dependencies workflow Added a token to the sync README dependencies workflow. --- .github/workflows/sync-readme-dependencies.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync-readme-dependencies.yml b/.github/workflows/sync-readme-dependencies.yml index 5141452..50be6cf 100644 --- a/.github/workflows/sync-readme-dependencies.yml +++ b/.github/workflows/sync-readme-dependencies.yml @@ -35,4 +35,5 @@ jobs: branch: auto/sync-readme-dependencies title: "Sync README dependencies" body: "Automated update of the Python Dependencies section in README.md to match `requirements.txt`." - labels: documentation \ No newline at end of file + labels: documentation + token: ${{ secrets.RELEASE_TOKEN }} From 49c89249c6910ffede34cee524fc2c536e1f9a0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:26:22 +0000 Subject: [PATCH 14/24] build(deps): bump pyinstaller from 6.15.0 to 6.19.0 Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.15.0 to 6.19.0. - [Release notes](https://github.com/pyinstaller/pyinstaller/releases) - [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst) - [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.15.0...v6.19.0) --- updated-dependencies: - dependency-name: pyinstaller dependency-version: 6.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 119dd76..9bf6f23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ requests==2.32.2 PySide6-Essentials==6.10.1 beautifulsoup4==4.14.3 darkdetect==0.8.0 -pyinstaller==6.15.0 +pyinstaller==6.19.0 PyQtDarkTheme-fork==2.3.4 plyer==2.1.0 psutil==7.1.0 From ee8c770db2f073d9377a421e6342a3a4cac66143 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:28:04 +0000 Subject: [PATCH 15/24] build(deps): bump requests in the pip group across 1 directory Bumps the pip group with 1 update in the / directory: [requests](https://github.com/psf/requests). Updates `requests` from 2.32.2 to 2.33.0 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 119dd76..1d8e2d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -requests==2.32.2 +requests==2.33.0 PySide6-Essentials==6.10.1 beautifulsoup4==4.14.3 darkdetect==0.8.0 From 5a63131777ce8b7986bbbc497f93f06467cd5061 Mon Sep 17 00:00:00 2001 From: shayaa <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:29:51 +0200 Subject: [PATCH 16/24] Change token from RELEASE_TOKEN to PR_TOKEN --- .github/workflows/sync-readme-dependencies.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-readme-dependencies.yml b/.github/workflows/sync-readme-dependencies.yml index 50be6cf..6ce45ef 100644 --- a/.github/workflows/sync-readme-dependencies.yml +++ b/.github/workflows/sync-readme-dependencies.yml @@ -36,4 +36,4 @@ jobs: title: "Sync README dependencies" body: "Automated update of the Python Dependencies section in README.md to match `requirements.txt`." labels: documentation - token: ${{ secrets.RELEASE_TOKEN }} + token: ${{ secrets.PR_TOKEN }} From 2d292d47967f78b716b6090cc7fb74b77ff2eb5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:32:07 +0000 Subject: [PATCH 17/24] build(deps): bump pyside6-essentials from 6.10.1 to 6.11.0 Bumps [pyside6-essentials](https://pyside.org) from 6.10.1 to 6.11.0. --- updated-dependencies: - dependency-name: pyside6-essentials dependency-version: 6.11.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6a8e3f1..ac6023a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ requests==2.33.0 -PySide6-Essentials==6.10.1 +PySide6-Essentials==6.11.0 beautifulsoup4==4.14.3 darkdetect==0.8.0 pyinstaller==6.19.0 From 3d11447e2235554e56ebfdfc5100f8a7d13a8aa1 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:38:37 +0000 Subject: [PATCH 18/24] Sync README dependencies --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index da00f99..8ae5aa5 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa ```text - requests (2.32.2) - PySide6-Essentials (6.10.1) - beautifulsoup4 (4.13.5) - darkdetect (0.7.1) - pyinstaller (6.15.0) + requests (2.33.0) + PySide6-Essentials (6.11.0) + beautifulsoup4 (4.14.3) + darkdetect (0.8.0) + pyinstaller (6.19.0) PyQtDarkTheme-fork (2.3.4) plyer (2.1.0) psutil (7.1.0) From cf19fa1c3d89bc20002568ad22e01854260c4fd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:39:34 +0000 Subject: [PATCH 19/24] build(deps): bump requests from 2.32.2 to 2.33.1 Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.33.1. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.33.1) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac6023a..b12c74b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -requests==2.33.0 +requests==2.33.1 PySide6-Essentials==6.11.0 beautifulsoup4==4.14.3 darkdetect==0.8.0 From 477d595db263817d6724b582473e103b95286771 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:40:56 +0000 Subject: [PATCH 20/24] Sync README dependencies --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ae5aa5..f0f6281 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa ```text - requests (2.33.0) + requests (2.33.1) PySide6-Essentials (6.11.0) beautifulsoup4 (4.14.3) darkdetect (0.8.0) From 21828e3a4846d29b0707190f96115fe96d5a0bd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:42:20 +0000 Subject: [PATCH 21/24] build(deps): bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/create-release.yml | 4 ++-- .github/workflows/dev.yml | 4 ++-- .github/workflows/sync-readme-dependencies.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 8f53a5d..c68123c 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 @@ -213,7 +213,7 @@ jobs: if: success() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: path: artifacts diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0e8fa49..7b1e4c2 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 @@ -218,7 +218,7 @@ jobs: if: success() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: path: artifacts diff --git a/.github/workflows/sync-readme-dependencies.yml b/.github/workflows/sync-readme-dependencies.yml index 6ce45ef..7651ac0 100644 --- a/.github/workflows/sync-readme-dependencies.yml +++ b/.github/workflows/sync-readme-dependencies.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9997bbe..132679f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 From 54bf6598ab85c1be8eb89869aa0ecf5b2479b4ba Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:07:48 +0200 Subject: [PATCH 22/24] fix(deps): downgrade darkdetect to version 0.7.1 and update dependabot config --- .github/dependabot.yml | 3 +++ requirements.txt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c80b447..4073b8e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,6 @@ updates: directory: "/" schedule: interval: "daily" + ignore: + - dependency-name: "darkdetect" + versions: [">=0.8.0"] diff --git a/requirements.txt b/requirements.txt index b12c74b..a519554 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ requests==2.33.1 PySide6-Essentials==6.11.0 beautifulsoup4==4.14.3 -darkdetect==0.8.0 +darkdetect==0.7.1 pyinstaller==6.19.0 PyQtDarkTheme-fork==2.3.4 plyer==2.1.0 From 41e418b9a865cdca9ef5d43f11e5ed40b8d6f392 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:09:49 +0000 Subject: [PATCH 23/24] Sync README dependencies --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0f6281..af4795a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa requests (2.33.1) PySide6-Essentials (6.11.0) beautifulsoup4 (4.14.3) - darkdetect (0.8.0) + darkdetect (0.7.1) pyinstaller (6.19.0) PyQtDarkTheme-fork (2.3.4) plyer (2.1.0) From e52f9301d8139c0d040d8a2c47ea4e7564e22478 Mon Sep 17 00:00:00 2001 From: shayaa <101264710+Vxrtrauter@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:03:22 +0200 Subject: [PATCH 24/24] Enhance README with Steamrip details and warning Updated the description to include Steamrip and added a note about its experimental status. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af4795a..0c05922 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # SoftwareManager -SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker and the official M0nkrus Telegram channel. +SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker, Steamrip and the official M0nkrus Telegram channel. + +> [!NOTE] +> Our Steamrip implementation is currently very experimental and potentially unstable.
+> Downloads will fail if there's no BuzzHeavier download available. We're working on this. ## Features - Searching & Downloading Software from various pages