Compare commits

..

4 Commits

Author SHA1 Message Date
Vxrtrauter e97f9ffe8a refactor imports 2026-03-11 15:40:25 +01:00
Vxrtrauter c4685dc2de fix: retry file deletion to allow libtorrent to release handles 2026-03-11 00:57:48 +01:00
Vxrtrauter f34d02d3d0 fix: minor inexistent variable in loghandler 2026-03-11 00:44:53 +01:00
Vxrtrauter 91b5c12afa feat: multithreaded update downloads + refactor updater into utils/network 2026-03-11 00:43:00 +01:00
22 changed files with 202 additions and 162 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
from core.utils.data.state import state
from core.utils.data.tracker import get_magnet_link
from core.utils.data.state import state
from bs4 import BeautifulSoup
from typing import Dict
import requests
+1 -1
View File
@@ -27,5 +27,5 @@ def scrape_gofile(url):
return temp[child[0]]["link"]
except:
consoleLog("gofile didnt auth you, either the api has changed\n or you sent to many requests, please try again later.\n If it still doesnt work please open an issue on Github.")
consoleLog("GoFile Authorisation failed, if the Issue persists, please open an Issue on GitHub")
+4 -4
View File
@@ -1,9 +1,9 @@
import requests
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.utils.data.tracker import get_magnet_link
from core.utils.network.jsonhandler import split_data, format_data
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from typing import Dict
import requests
def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
+2 -1
View File
@@ -6,8 +6,9 @@ from bs4 import BeautifulSoup
from typing import Dict
import webbrowser
import requests
import re
import time
import re
cache = {
"data": [],
+4 -4
View File
@@ -1,10 +1,10 @@
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from core.utils.data.tracker import get_magnet_link
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from typing import Dict
import requests
def scrape_uztracker(query):
+6 -5
View File
@@ -1,12 +1,12 @@
from core.utils.config.settings import save_settings
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.interface.utils.tabhelper import general_tab
from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.tabhelper import network_tab
from core.utils.config.settings import save_settings
from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.svghelper import svg_icon
from PySide6 import QtWidgets
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from PySide6.QtCore import Qt, QSize
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
QPushButton,
@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
QComboBox,
QTabWidget,
)
import platform
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
+33 -93
View File
@@ -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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon
SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="5" y="3" width="4" height="18" rx="1" fill="{color}"/><rect x="15" y="3" width="4" height="18" rx="1" fill="{color}"/></svg>'
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
def _verify_hash(file_path, expected_hash):
import hashlib
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):
import tempfile
import time
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 installer...", None, 0, 0)
progress.setWindowTitle("Updating")
progress.setWindowModality(Qt.WindowModality.ApplicationModal)
progress.setCancelButton(None)
progress.setMinimumDuration(0)
progress.setAutoClose(False)
progress.setAutoReset(False)
progress.setRange(0, 100)
progress.setValue(0)
progress.show()
QtWidgets.QApplication.processEvents()
response = r.get(url, allow_redirects=True, stream=True)
total = int(response.headers.get("content-length", 0))
downloaded = 0
with open(installer_path, "wb") as f:
for chunk in response.iter_content(chunk_size=65536):
f.write(chunk)
downloaded += len(chunk)
if total > 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)
@@ -1052,7 +989,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
try:
status = magnetdl.status()
save_path = status.save_path
@@ -1063,9 +999,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
state.active_downloads.pop(magnet_link, None)
remove_download_log(magnet_link)
return
download_path = os.path.join(save_path, torrent_name)
confirm = QMessageBox.question(
self, "Delete Files",
f"Are you sure you want to delete the downloaded files of '{torrent_name}'? This action cannot be undone.",
@@ -1075,23 +1009,29 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
with state.downloads_lock:
state.active_downloads.pop(magnet_link, None)
remove_download_log(magnet_link)
if state.dl_session:
try:
state.dl_session.remove_torrent(magnetdl)
time.sleep(0.5)
except Exception:
pass
if download_path and os.path.exists(download_path):
try:
if os.path.isfile(download_path):
os.remove(download_path)
import shutil
last_error = None
for attempt in range(3):
try:
if os.path.isfile(download_path):
os.remove(download_path)
else:
shutil.rmtree(download_path)
consoleLog(f"Deleted files for: {torrent_name}", True)
else:
import shutil
shutil.rmtree(download_path)
consoleLog(f"Deleted files for: {torrent_name}", True)
except Exception as e:
consoleLog(f"Error deleting files: {e}", True)
last_error = None
break
except Exception as e:
last_error = e
if attempt < 2:
time.sleep(0.5)
if last_error:
consoleLog(f"Error deleting files: {last_error}", True)
else:
consoleLog(f"Removed entry (files not found): {torrent_name}", True)
consoleLog(f"Removed entry (files not found): {torrent_name}", True)
+10 -10
View File
@@ -1,17 +1,17 @@
import os
import threading
import requests
import json
import hashlib
import time
from core.utils.logging.logs import consoleLog, update_download_completed
from concurrent.futures import ThreadPoolExecutor, as_completed
from .status import DirectDownloadStatus, ChunkSpec
from core.utils.data.state import state
from .utils import format_size
from typing import Optional
import libtorrent as lt
import threading
import requests
import hashlib
import json
import time
import os
from core.utils.logging.logs import consoleLog, update_download_completed
from core.utils.data.state import state
from .status import DirectDownloadStatus, ChunkSpec
from .utils import format_size
class SimpleThrottler:
def __init__(self):
+2 -2
View File
@@ -1,8 +1,8 @@
import threading
import time
from dataclasses import dataclass
from typing import Optional
import libtorrent as lt
import threading
import time
@dataclass
+3 -3
View File
@@ -1,7 +1,7 @@
import os
import requests
from typing import Optional
from urllib.parse import urlparse, unquote
from typing import Optional
import requests
import os
def sanitize_filename(name: str) -> str:
# Remove or replace dangerous characters
+8 -8
View File
@@ -1,13 +1,13 @@
import time
import os
from core.network.interface import get_interface_ip
from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
import libtorrent as lt
import threading
import platform
import ctypes
from core.utils.general.wrappers import run_thread
from core.network.interface import get_interface_ip
import threading
import libtorrent as lt
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
import time
import os
global loop_running
+2 -2
View File
@@ -1,10 +1,10 @@
from core.utils.data.state import state
from core.utils.logging.logs import update_download_completed_by_hash
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from plyer import notification
import libtorrent as lt
import os
import time
import os
def cleanup_session():
+2 -1
View File
@@ -1,5 +1,6 @@
from core.utils.logging.logs import consoleLog
from core.network.libtorrent_int import add_download
from core.utils.logging.logs import consoleLog
def add_magnet(uri):
if uri is not None and uri.startswith("magnet:?"):
+1 -1
View File
@@ -1,6 +1,6 @@
from core.network.libtorrent_int import update_settings
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from .config import create_config
+1 -1
View File
@@ -1,8 +1,8 @@
import threading
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget
from typing import Any, List, Dict
from pathlib import Path
import threading
class AppState(QObject):
image_changed = Signal(str)
+2 -2
View File
@@ -1,6 +1,6 @@
import requests
from bs4 import BeautifulSoup
from core.utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
import requests
def get_magnet_link(post_url):
+3 -4
View File
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog, remove_download_log
from core.utils.network.download import run_download_direct, seed_magnet
from core.utils.logging.logs import consoleLog, remove_download_log
import os
def split_data(data):
@@ -14,13 +14,12 @@ 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
add_direct_download(download.url, download.title, dl_dir)
add_direct_download(download.url, download.title)
consoleLog(f"Resuming Direct Download: {download.title}")
def check_downloads(downloads):
+2 -2
View File
@@ -3,10 +3,10 @@ from core.utils.data.state import state
from dataclasses import asdict
from datetime import datetime
import json
import time
import os
import re
import time
import threading
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
# wait for metadata outside the lock to avoid blocking other threads
+9 -9
View File
@@ -1,12 +1,12 @@
from PySide6.QtWidgets import QTableWidgetItem
from PySide6.QtCore import Qt
from core.utils.logging.logs import consoleLog
from core.network.libtorrent_wrapper import add_magnet
from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import add_download_log
from core.network.libtorrent_int import add_seed
from core.utils.data.state import state
from core.network.direct_download import add_direct_download
from core.network.libtorrent_wrapper import add_magnet
from core.utils.logging.logs import add_download_log
from core.utils.general.wrappers import run_thread
from core.network.libtorrent_int import add_seed
from PySide6.QtWidgets import QTableWidgetItem
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from PySide6.QtCore import Qt
import threading
@@ -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)
+4 -2
View File
@@ -1,6 +1,8 @@
import requests
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
import requests
def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
+96
View File
@@ -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)
+6 -6
View File
@@ -1,21 +1,21 @@
from core.interface.gui import MainWindow
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
from core.network.libtorrent_misc import send_notification, update_log, check_deleted_files
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces
from core.utils.logging.logs import get_download_logs
from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces
from core.utils.config.config import read_config
from core.utils.logging.logs import consoleLog
from core.interface.gui import MainWindow
from core.utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
import qdarktheme
import threading
import platform
import signal
import sys
import time
import sys
def run_gui():
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)