From 91b5c12afa6c5e4eadb8420e2a6ab326360f8e2e Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:43:00 +0100 Subject: [PATCH] feat: multithreaded update downloads + refactor updater into utils/network --- src/core/interface/gui.py | 93 +++++---------------------- src/core/utils/logging/loghandler.py | 3 +- src/core/utils/network/download.py | 2 +- src/core/utils/network/updater.py | 96 ++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 81 deletions(-) create mode 100644 src/core/utils/network/updater.py diff --git a/src/core/interface/gui.py b/src/core/interface/gui.py index 0b8f224..83b67d9 100644 --- a/src/core/interface/gui.py +++ b/src/core/interface/gui.py @@ -1,16 +1,17 @@ from core.utils.logging.logs import consoleLog, remove_download_log, flush_log_buffer -from core.utils.general.wrappers import run_thread -from core.utils.data.state import state +from core.interface.assets.base64_icons import settings_white_base64 +from core.interface.assets.base64_icons import settings_black_base64 +from core.interface.utils.searchhelper import return_pressed +from core.interface.dialogs.settings import settings_dialog +from core.interface.assets.base64_icons import logo_base64 from core.utils.network.download import download_selected from core.utils.network.update_checker import get_updates +from core.utils.network.updater import download_update from core.interface.utils.tabhelper import create_tab -from core.interface.utils.searchhelper import return_pressed from core.interface.utils.svghelper import svg_icon -from core.interface.dialogs.settings import settings_dialog -from core.interface.assets.base64_icons import settings_black_base64 -from core.interface.assets.base64_icons import settings_white_base64 -from core.interface.assets.base64_icons import logo_base64 from core.utils.general.shutdown import closehelper +from core.utils.general.wrappers import run_thread +from core.utils.data.state import state from PySide6 import QtWidgets from PySide6.QtCore import ( @@ -52,16 +53,16 @@ from PySide6.QtGui import ( QColor, ) +import libtorrent as lt +import subprocess import darkdetect import threading import platform -import requests as r -import os -import subprocess -import libtorrent as lt -import sys -import json import base64 +import json +import time +import sys +import os def _is_dark_mode(): @@ -141,70 +142,6 @@ SVG_PLAY = ' 0: - progress.setValue(int(downloaded * 100 / total)) - QtWidgets.QApplication.processEvents() - if not os.path.exists(installer_path): - progress.close() - raise FileNotFoundError("Executable not found") - if setup_hash: - if _verify_hash(installer_path, setup_hash): - consoleLog(f"Sucessfully validated installer hash ({setup_hash})") - else: - consoleLog("Error: Invalid Filehash, file may be corrupted") - sys.exit(0) - else: - consoleLog("Skipping Hash Verification (no hash found for release)") - progress.setLabelText("Installing update...") - progress.setValue(100) - QtWidgets.QApplication.processEvents() - subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"]) - time.sleep(1) - sys.exit(0) - def windowCloseHelper(): QGuiApplication.quit() @@ -282,7 +219,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore) response = msg.exec_() if response == QMessageBox.StandardButton.Ok: - _download_update(assets) + download_update(assets) self.setWindowTitle("Software Manager") self.setGeometry(100, 100, 800, 600) diff --git a/src/core/utils/logging/loghandler.py b/src/core/utils/logging/loghandler.py index d1a1055..6d6c47f 100644 --- a/src/core/utils/logging/loghandler.py +++ b/src/core/utils/logging/loghandler.py @@ -14,9 +14,8 @@ def check_completed(downloads, resume): if download.completed == False: consoleLog(f"Found unfinished download: {download.title}") if resume == True: - dl_dir = os.path.dirname(download.path) if download.magnet_uri: - run_download_direct(download.magnet_uri, dl_dir, download.title) + run_download_direct(download.magnet_uri, download.title) consoleLog(f"Resuming Magnet: {download.title}") elif download.url: from core.network.direct_download import add_direct_download diff --git a/src/core/utils/network/download.py b/src/core/utils/network/download.py index a4dba30..59b3cf8 100644 --- a/src/core/utils/network/download.py +++ b/src/core/utils/network/download.py @@ -41,7 +41,7 @@ def run_download(post): else: add_direct_download(link, post.get("title", "Unknown")) -def run_download_direct(magnet_uri, dl_path=None, title="Direct Download"): +def run_download_direct(magnet_uri, title="Direct Download"): consoleLog(f"Magnet: {title}") add_magnet(magnet_uri) add_download_log(title, "", magnet_uri, False) diff --git a/src/core/utils/network/updater.py b/src/core/utils/network/updater.py new file mode 100644 index 0000000..7acf76d --- /dev/null +++ b/src/core/utils/network/updater.py @@ -0,0 +1,96 @@ +from core.network.direct_download.handle import DirectDownloadHandle +from core.utils.logging.logs import consoleLog +from core.utils.data.state import state +from PySide6.QtCore import Qt +from PySide6 import QtWidgets +import libtorrent as lt +import subprocess +import tempfile +import hashlib +import time +import sys +import os + + +def _verify_hash(file_path: str, expected_hash: str) -> bool: + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + return f"sha256:{sha256.hexdigest()}" == expected_hash + + +def download_update(assets: list): + filename = None + setup_hash = None + url = None + + for asset in assets: + if "-windows-setup.exe" in asset["name"]: + filename = asset["name"] + setup_hash = asset["hash"] + url = asset["url"] + break + + if not filename: + consoleLog("Error: No Windows installer found in release assets") + return + + installer_path = os.path.join(tempfile.gettempdir(), filename) + + progress = QtWidgets.QProgressDialog("Downloading update... (0.0 MB/s)", None, 0, 100) + progress.setWindowTitle("Updating") + progress.setWindowModality(Qt.WindowModality.ApplicationModal) + progress.setCancelButton(None) + progress.setMinimumDuration(0) + progress.setAutoClose(False) + progress.setAutoReset(False) + progress.setValue(0) + progress.show() + QtWidgets.QApplication.processEvents() + + original_limit = state.down_speed_limit + state.down_speed_limit = 0 + handle = DirectDownloadHandle(url, filename, tempfile.gettempdir()) + handle.start() + + while True: + QtWidgets.QApplication.processEvents() + status = handle.status() + + if status.error: + state.down_speed_limit = original_limit + progress.close() + consoleLog(f"Update download failed: {status.error}") + return + + if status.total_wanted > 0: + pct = int(status.total_wanted_done * 100 / status.total_wanted) + speed_mb = status.download_rate / (1024 * 1024) + progress.setValue(pct) + progress.setLabelText(f"Downloading update... ({speed_mb:.1f} MB/s)") + + if status.state == lt.torrent_status.seeding: + break + + time.sleep(0.1) + + state.down_speed_limit = original_limit + + if setup_hash: + if _verify_hash(installer_path, setup_hash): + consoleLog(f"Successfully validated installer hash ({setup_hash})") + else: + progress.close() + consoleLog("Error: Invalid file hash, file may be corrupted") + sys.exit(0) + else: + consoleLog("Skipping hash verification (no hash found for release)") + + progress.setLabelText("Installing update...") + progress.setValue(100) + QtWidgets.QApplication.processEvents() + + subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"]) + time.sleep(1) + sys.exit(0) \ No newline at end of file