From b217a74fce1bca66e82a858babadc883855b4f81 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:31:30 +0100 Subject: [PATCH 1/9] add basic libtorrent integration --- core/network/libtorrent_int.py | 43 ++++++++++++++++++++++++++++++++++ core/utils/data/state.py | 2 ++ 2 files changed, 45 insertions(+) create mode 100644 core/network/libtorrent_int.py diff --git a/core/network/libtorrent_int.py b/core/network/libtorrent_int.py new file mode 100644 index 0000000..47e37c6 --- /dev/null +++ b/core/network/libtorrent_int.py @@ -0,0 +1,43 @@ +import subprocess +import libtorrent as lt +from core.utils.data.state import state +from core.utils.general.logs import consoleLog + + + +def init_session(): + if state.dl_session is not None: + return + + state.dl_session = lt.session() + + settings = { + "upload_rate_limit": 0, + "download_rate_limit": 0, + "enable_dht": True, + "enable_lsd": True, + "enable_upnp": True, + "enable_natpmp": True, + "dht_bootstrap_nodes": "router.bittorrent.com:6881,dht.transmissionbt.com:6881", + "connections_limit": 200, + "active_downloads": 10 + } + + state.dl_session.apply_settings(settings) + + +def add_download(magnet_uri, dl_path=state.download_path): + + init_session() + + if magnet_uri in state.active_downloads: + consoleLog("Skipping, download already running...") + return + + consoleLog(f"Adding {magnet_uri} to downloads...") + + magnetdl = lt.parse_magnet_uri(magnet_uri) + magnetdl.save_path = dl_path + + download = state.dl_session.add_torrent(magnetdl) + state.active_downloads[magnet_uri] = magnetdl diff --git a/core/utils/data/state.py b/core/utils/data/state.py index b5584bf..b8e8f3d 100644 --- a/core/utils/data/state.py +++ b/core/utils/data/state.py @@ -26,6 +26,8 @@ class AppState(QObject): self.aria2p: Any = None self.aria2_threads: int = 4 self.settings_path: str = None + self.dl_session: Any = None + self.active_downloads: Any = None @property def image_path(self) -> str: From 685461e453ade8299cdc10ffa653561ab476db49 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:44:55 +0100 Subject: [PATCH 2/9] integrate download func in gui (partially) --- core/network/libtorrent_int.py | 61 +++++++++++++++++++++++++++++- core/network/libtorrent_wrapper.py | 12 ++++++ core/utils/network/download.py | 10 ++--- 3 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 core/network/libtorrent_wrapper.py diff --git a/core/network/libtorrent_int.py b/core/network/libtorrent_int.py index 47e37c6..ee6d84d 100644 --- a/core/network/libtorrent_int.py +++ b/core/network/libtorrent_int.py @@ -1,9 +1,13 @@ -import subprocess +import time +from core.utils.general.wrappers import run_thread +import threading import libtorrent as lt from core.utils.data.state import state from core.utils.general.logs import consoleLog +global loop_running +loop_running = False def init_session(): if state.dl_session is not None: @@ -23,11 +27,18 @@ def init_session(): "active_downloads": 10 } + + state.dl_session.apply_settings(settings) + consoleLog("Initialized Session") + def add_download(magnet_uri, dl_path=state.download_path): + if state.active_downloads is None: + state.active_downloads = {} + init_session() if magnet_uri in state.active_downloads: @@ -39,5 +50,51 @@ def add_download(magnet_uri, dl_path=state.download_path): magnetdl = lt.parse_magnet_uri(magnet_uri) magnetdl.save_path = dl_path + consoleLog(f"Download Path: {dl_path}") + download = state.dl_session.add_torrent(magnetdl) - state.active_downloads[magnet_uri] = magnetdl + state.active_downloads[magnet_uri] = download + consoleLog("added download") + + run_thread(threading.Thread(target=dl_status_loop)) + + + +def dl_status_loop(): + global loop_running + + if loop_running == True: + return + + loop_running = True + completed = [] + + if not state.active_downloads: + consoleLog("No active downloads") + return + + while state.active_downloads: + completed.clear() + + for magnet_uri, magnetdl in list(state.active_downloads.items()): + status = magnetdl.status() + + if status.state == lt.torrent_status.seeding: + completed.append(magnet_uri) + consoleLog(f"Download completed: {status.name}") + continue + + for magnet_uri in completed: + magnetdl = state.active_downloads[magnet_uri] + state.dl_session.remove_torrent(magnetdl) + del state.active_downloads[magnet_uri] + + if not state.active_downloads: + loop_running = False + break + + time.sleep(1) + + + + diff --git a/core/network/libtorrent_wrapper.py b/core/network/libtorrent_wrapper.py new file mode 100644 index 0000000..4a24292 --- /dev/null +++ b/core/network/libtorrent_wrapper.py @@ -0,0 +1,12 @@ +from core.utils.general.logs import consoleLog +from core.network.libtorrent_int import add_download, dl_status_loop + +def add_magnet(uri): + if uri is not None and uri.startswith("magnet:?"): + add_download(uri) + consoleLog("Magnet URI added to LibTorrent") + else: + consoleLog(f"Invalid Magnet Link: {uri}") + + + diff --git a/core/utils/network/download.py b/core/utils/network/download.py index 6351f8f..4863f34 100644 --- a/core/utils/network/download.py +++ b/core/utils/network/download.py @@ -2,8 +2,7 @@ from core.utils.data.state import state from core.utils.general.logs import consoleLog from core.utils.data.tracker import get_item_url from core.utils.data.tracker import get_magnet_link -from core.network.aria2_wrapper import start_client -from core.network.aria2_wrapper import add_magnet +from core.network.libtorrent_wrapper import add_download from core.utils.general.wrappers import run_thread from core.utils.general.logs import add_download_log @@ -21,13 +20,12 @@ def run_download(item, posts, post_titles): post_url = get_item_url(item, posts, post_titles) consoleLog(f"Selected URL: {post_url}") magnet_uri = get_magnet_link(post_url) - start_client() + add_download_log(item, post_url, magnet_uri, False) - add_magnet(magnet_uri) + add_download(magnet_uri) def run_download_direct(magnet_uri): - start_client() - add_magnet(magnet_uri) + add_download(magnet_uri) From fa3a43887a4f379af1468fc754449651e6d679d8 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:20:26 +0100 Subject: [PATCH 3/9] integrate downloads in downloads tab --- core/interface/gui.py | 107 +++++++++++++++++++++++---------- core/network/libtorrent_int.py | 30 ++++----- core/utils/data/state.py | 3 +- 3 files changed, 86 insertions(+), 54 deletions(-) diff --git a/core/interface/gui.py b/core/interface/gui.py index c596253..18f5093 100644 --- a/core/interface/gui.py +++ b/core/interface/gui.py @@ -29,6 +29,7 @@ import platform import requests as r import os import subprocess +import libtorrent as lt import time import sys import json @@ -183,7 +184,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"] def rowCount(self, parent=QModelIndex()): - return len(state.downloads) + return len(state.active_downloads) def columnCount(self, parent=QModelIndex()): return len(self.headers) @@ -194,36 +195,79 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): return None def data(self, index, role=Qt.DisplayRole): - col = index.column() - download = state.downloads[index.row()] if role == Qt.DisplayRole: col = index.column() - + + magnet_link = list(state.active_downloads.keys())[index.row()] + magnetdl = state.active_downloads[magnet_link] + + status = magnetdl.status() + if col == 0: - return "▶︎" if download.is_paused else "⏸︎" + is_paused = magnetdl.status().paused + return "▶︎" if is_paused else "⏸︎" elif col == 1: - return download.name + return status.name if status.has_metadata else "Fetching metadata..." elif col == 2: - return getattr(download, 'status', 'Downloading') + if status.state == lt.torrent_status.downloading: + return "Downloading" + elif status.state == lt.torrent_status.seeding: + return "Seeding" + elif status.paused: + return "Paused" + else: + return "Queued" elif col == 3: - return f"{int(download.progress)}%" + return f"{status.progress * 100:.1f}%" elif col == 4: - return f"{download.download_speed_string()}" + speed_kbs = status.download_rate / 1024 + if speed_kbs > 1024: + return f"{speed_kbs / 1024:.1f} MB/s" + else: + return f"{speed_kbs:.1f} kB/s" elif col == 5: - return f"{download.completed_length_string()}" + downloaded_mb = status.total_wanted_done / (1024 * 1024) + if downloaded_mb > 1024: + return f"{downloaded_mb / 1024:.2f} GB" + else: + return f"{downloaded_mb:.1f} MB" elif col == 6: - return f"{download.total_length_string()}" + total_mb = status.total_wanted / (1024 * 1024) + if total_mb > 1024: + return f"{total_mb / 1024:.2f} GB" + else: + return f"{total_mb:.1f} MB" elif col == 7: - return f"{download.eta_string()}" + if status.download_rate > 0: + bytes_left = status.total_wanted - status.total_wanted_done + eta_seconds = bytes_left / status.download_rate + + if eta_seconds < 60: + return f"{int(eta_seconds)}s" + elif eta_seconds < 3600: + minutes = int(eta_seconds / 60) + seconds = int(eta_seconds % 60) + return f"{minutes}m {seconds}s" + else: + hours = int(eta_seconds / 3600) + minutes = int((eta_seconds % 3600) / 60) + return f"{hours}h {minutes}m" + else: + return "∞" if status.paused else "Stalled" + + if role == Qt.UserRole and index.column() == 0: + magnet_link = list(state.active_downloads.keys())[index.row()] + magnetdl = state.active_downloads[magnet_link] + return magnetdl.status().paused - if role == Qt.UserRole and col == 0: - return download.is_paused - return None def toggle_pause_resume(self, row): - download = state.downloads[row] - if download.progress == 100: + magnet_link = list(state.active_downloads.keys())[row] + magnetdl = state.active_downloads[magnet_link] + status = magnetdl.status() + + if status.state == lt.torrent_status.seeding: if state.download_path is not None and os.path.exists(state.download_path): if platform.system() == "Windows": os.startfile(os.path.normpath(state.download_path)) @@ -231,21 +275,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): subprocess.Popen(["xdg-open", state.download_path]) elif platform.system() == "Darwin": subprocess.Popen(["open", state.download_path]) - else: - if platform.system() == "Windows": - os.startfile(os.path.normpath(os.getcwd())) - elif platform.system() == "Linux": - subprocess.Popen(["xdg-open", os.getcwd()]) - elif platform.system() == "Darwin": - subprocess.Popen(["open", os.getcwd()]) return - - if download.is_paused: - download.resume() - consoleLog(f"Resumed download: {download.name}", True) + + if status.paused: + magnetdl.resume() + consoleLog(f"Resumed download: {status.name}", True) else: - download.pause() - consoleLog(f"Paused download: {download.name}", True) + magnetdl.pause() + consoleLog(f"Paused download: {status.name}", True) + idx = self.index(row, 0) self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole]) @@ -258,12 +296,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): return paused = index.data(Qt.UserRole) - download = state.downloads[index.row()] + + magnet_link = list(state.active_downloads.keys())[index.row()] + magnetdl = state.active_downloads[magnet_link] + status = magnetdl.status() button = QStyleOptionButton() button.rect = option.rect - if download.progress == 100: + if status.state == lt.torrent_status.seeding: button.text = "📁" else: button.text = "▶︎" if paused else "⏸︎" @@ -415,7 +456,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.emptyResults.hide() def show_empty_downloads(self): - if len(state.downloads) > 0: + if len(state.active_downloads) > 0: self.emptyDownload.hide() self.downloadList.show() else: diff --git a/core/network/libtorrent_int.py b/core/network/libtorrent_int.py index ee6d84d..4f7d847 100644 --- a/core/network/libtorrent_int.py +++ b/core/network/libtorrent_int.py @@ -45,16 +45,13 @@ def add_download(magnet_uri, dl_path=state.download_path): consoleLog("Skipping, download already running...") return - consoleLog(f"Adding {magnet_uri} to downloads...") magnetdl = lt.parse_magnet_uri(magnet_uri) magnetdl.save_path = dl_path - consoleLog(f"Download Path: {dl_path}") - download = state.dl_session.add_torrent(magnetdl) state.active_downloads[magnet_uri] = download - consoleLog("added download") + consoleLog(f"Added {magnet_uri} to downloads") run_thread(threading.Thread(target=dl_status_loop)) @@ -62,38 +59,33 @@ def add_download(magnet_uri, dl_path=state.download_path): def dl_status_loop(): global loop_running - if loop_running == True: return loop_running = True - completed = [] - + completed_set = set() + if not state.active_downloads: consoleLog("No active downloads") + loop_running = False return - + while state.active_downloads: - completed.clear() - for magnet_uri, magnetdl in list(state.active_downloads.items()): status = magnetdl.status() - if status.state == lt.torrent_status.seeding: - completed.append(magnet_uri) + if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set: consoleLog(f"Download completed: {status.name}") - continue + + completed_set.add(magnet_uri) - for magnet_uri in completed: - magnetdl = state.active_downloads[magnet_uri] - state.dl_session.remove_torrent(magnetdl) - del state.active_downloads[magnet_uri] - if not state.active_downloads: loop_running = False break - + time.sleep(1) + + loop_running = False diff --git a/core/utils/data/state.py b/core/utils/data/state.py index b8e8f3d..cf6d33f 100644 --- a/core/utils/data/state.py +++ b/core/utils/data/state.py @@ -11,7 +11,6 @@ class AppState(QObject): self.post_titles: List[str] = [] self.post_urls: List[str] = [] self.post_author: List[str] = [] - self.downloads: List[str] = [] self.version: str = "dev" self._image_path: str = "" self.ignore_updates: bool = False @@ -27,7 +26,7 @@ class AppState(QObject): self.aria2_threads: int = 4 self.settings_path: str = None self.dl_session: Any = None - self.active_downloads: Any = None + self.active_downloads: List[str] = {} @property def image_path(self) -> str: From 8f9f77d63fb4a56024b5f8852bee0adfdb8102da Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:05:31 +0100 Subject: [PATCH 4/9] update settings dialog & config, remove aria2 --- core/interface/dialogs/settings.py | 46 +++++++----- core/interface/gui.py | 9 --- core/network/aria2_integration.py | 109 ----------------------------- core/network/aria2_wrapper.py | 18 ----- core/network/libtorrent_int.py | 18 +++-- core/network/libtorrent_misc.py | 45 ++++++++++++ core/utils/config/config.py | 5 +- core/utils/config/settings.py | 20 ++---- core/utils/data/state.py | 5 +- main.py | 12 +--- 10 files changed, 102 insertions(+), 185 deletions(-) delete mode 100644 core/network/aria2_integration.py delete mode 100644 core/network/aria2_wrapper.py create mode 100644 core/network/libtorrent_misc.py diff --git a/core/interface/dialogs/settings.py b/core/interface/dialogs/settings.py index 24bb5a9..81cf02b 100644 --- a/core/interface/dialogs/settings.py +++ b/core/interface/dialogs/settings.py @@ -60,9 +60,9 @@ def settings_dialog(self): autoresume_layout.addWidget(autoresume_checkbox) dialog.layout().addWidget(autoresume_container) - ################## - # THREAD SETTING # - ################## + ##################### + # DOWNLOAD SETTINGS # + ##################### thread_box = QSpinBox() thread_box.setMinimum(1) thread_box.setMaximum(16) @@ -128,19 +128,33 @@ def settings_dialog(self): # SPEED LIMITING # ################## - speed_limit_container = QWidget() - speed_limit_layout = QHBoxLayout() + down_speed_limit_container = QWidget() + down_speed_limit_layout = QHBoxLayout() - speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): ")) - speed_limit = QSpinBox() - speed_limit.setMinimum(0) - speed_limit.setMaximum(10000000) - speed_limit.setValue(state.speed_limit) - speed_limit_container.setLayout(speed_limit_layout) - speed_limit_layout.addWidget(speed_limit) - speed_limit.setFixedWidth(180) - speed_limit.setFixedHeight(30) - dialog.layout().addWidget(speed_limit_container) + down_speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): ")) + down_speed_limit = QSpinBox() + down_speed_limit.setMinimum(0) + down_speed_limit.setMaximum(10000000) + down_speed_limit.setValue(state.down_speed_limit) + down_speed_limit_container.setLayout(down_speed_limit_layout) + down_speed_limit_layout.addWidget(down_speed_limit) + down_speed_limit.setFixedWidth(180) + down_speed_limit.setFixedHeight(30) + dialog.layout().addWidget(down_speed_limit_container) + + up_speed_limit_container = QWidget() + up_speed_limit_layout = QHBoxLayout() + + up_speed_limit_layout.addWidget(QLabel("Max Upload Speed (KiB, 0 for unlimited): ")) + up_speed_limit = QSpinBox() + up_speed_limit.setMinimum(0) + up_speed_limit.setMaximum(10000000) + up_speed_limit.setValue(state.up_speed_limit) + up_speed_limit_container.setLayout(up_speed_limit_layout) + up_speed_limit_layout.addWidget(up_speed_limit) + up_speed_limit.setFixedWidth(180) + up_speed_limit.setFixedHeight(30) + dialog.layout().addWidget(up_speed_limit_container) ############### # IMAGE PATH # @@ -174,7 +188,7 @@ def settings_dialog(self): save_btn = QPushButton("Save") cancel_btn = QPushButton("Cancel") - save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value(), image_path.text())) + save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), down_speed_limit.value(), up_speed_limit.value(), image_path.text())) cancel_btn.clicked.connect(dialog.reject) layout.addWidget(cancel_btn) diff --git a/core/interface/gui.py b/core/interface/gui.py index 18f5093..2e838d1 100644 --- a/core/interface/gui.py +++ b/core/interface/gui.py @@ -42,7 +42,6 @@ from core.utils.general.shutdown import closehelper from core.interface.utils.tabhelper import create_tab from core.interface.utils.searchhelper import return_pressed from core.interface.dialogs.settings import settings_dialog -from core.network.aria2_integration import dlprogress def download_update(latest_version): @@ -400,10 +399,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.consoleLog.setFixedHeight(150) containerLayout.addWidget(self.consoleLog) - self.progress_timer = QTimer() - self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress))) - self.progress_timer.start(1000) - self.download_timer = QTimer() self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update))) self.download_timer.start(500) @@ -443,10 +438,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): def set_tracker(self, _): state.tracker = self.tracker_list.currentText() - def update_progress(self): - progress = dlprogress() - self.progressbar.setValue(progress) - def show_empty_results(self, show: bool): if show: self.qtablewidget.hide() diff --git a/core/network/aria2_integration.py b/core/network/aria2_integration.py deleted file mode 100644 index 77fa6eb..0000000 --- a/core/network/aria2_integration.py +++ /dev/null @@ -1,109 +0,0 @@ -import aria2p -import subprocess -from core.utils.data.state import state -from core.utils.general.logs import update_download_completed_by_hash -from core.utils.general.logs import consoleLog -from plyer import notification -import socket -import time -import sys - -def run_aria2p(): - - state.aria2 = aria2p.API( - aria2p.Client( - host="http://localhost", - port=6800, - secret="" - ) - ) - - return state.aria2 - - -def aria2server(): - download_path = state.download_path - speed_limit = state.speed_limit - - cmd = [ - "aria2c", - "--enable-rpc", - "--disable-ipv6", # added this since it caused problems with vpns - "--rpc-listen-all", - "--rpc-listen-port=6800", - f"--dir={download_path}", - f"--max-download-limit={speed_limit * 1024}", - "-x", str(state.aria2_threads), - "-s", str(state.aria2_threads), - ] - - aria2server = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 - ) - - max_checks = 50 - for check in range(max_checks): - try: - sock = socket.socket() - sock.settimeout(0.1) - sock.connect(("localhost", 6800)) - sock.close() - break - except(ConnectionRefusedError, socket.timeout): - time.sleep(0.1) - else: - raise RuntimeError("Aria2 Server failed to start") - - consoleLog("Aria2 Server started") - return aria2server - -def dlprogress(): - try: - state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata] - downloads = state.downloads - if downloads: - for download in downloads: - progress = download.progress_string(0) - progress_int = int(progress.strip('%')) - return progress_int - return 0 - except: - return 0 - -def send_notification(shutdown_event): - notified = set() - while not shutdown_event.is_set(): - try: - for d in state.aria2.get_downloads(): - if d.is_metadata: - continue - if d.progress == 100 and d.gid not in notified: - notification.notify( - title="Download finished", - message=f"{d.name} has finished downloading.", - timeout=4 - ) - notified.add(d.gid) - state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] - except Exception: - pass - time.sleep(5) - -def update_log(shutdown_event): - updated = set() - while not shutdown_event.is_set(): - try: - for d in state.aria2.get_downloads(): - if d.is_metadata: - continue - if d.progress == 100 and d.gid not in updated: - consoleLog(f"Marking {d.name} as completed") - update_download_completed_by_hash(d.info_hash, True) - updated.add(d.gid) - state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] - except Exception: - pass - time.sleep(5) \ No newline at end of file diff --git a/core/network/aria2_wrapper.py b/core/network/aria2_wrapper.py deleted file mode 100644 index 46361da..0000000 --- a/core/network/aria2_wrapper.py +++ /dev/null @@ -1,18 +0,0 @@ -from .aria2_integration import run_aria2p -from core.utils.data.state import state -from core.utils.general.logs import consoleLog - -def start_client(): - global aria2 - aria2 = run_aria2p() - consoleLog("Started Aria2p") - -def add_magnet(uri): - if uri is not None and uri.startswith("magnet:?"): - aria2.add_magnet(uri) - consoleLog("Magnet URI added to Aria2") - else: - consoleLog(f"Invalid Magnet Link: {uri}") - - - diff --git a/core/network/libtorrent_int.py b/core/network/libtorrent_int.py index 4f7d847..4b65779 100644 --- a/core/network/libtorrent_int.py +++ b/core/network/libtorrent_int.py @@ -16,15 +16,15 @@ def init_session(): state.dl_session = lt.session() settings = { - "upload_rate_limit": 0, - "download_rate_limit": 0, + "upload_rate_limit": state.up_speed_limit, + "download_rate_limit": state.down_speed_limit, "enable_dht": True, "enable_lsd": True, "enable_upnp": True, "enable_natpmp": True, "dht_bootstrap_nodes": "router.bittorrent.com:6881,dht.transmissionbt.com:6881", - "connections_limit": 200, - "active_downloads": 10 + "connections_limit": state.max_connections, + "active_downloads": state.max_downloads } @@ -87,6 +87,14 @@ def dl_status_loop(): loop_running = False +def update_settings(): + settings = { + "upload_rate_limit": state.up_speed_limit, + "download_rate_limit": state.down_speed_limit, + "connections_limit": state.max_connections, + "active_downloads": state.max_downloads + } - + state.dl_session.apply_settings(settings) + consoleLog("Updated Settings") diff --git a/core/network/libtorrent_misc.py b/core/network/libtorrent_misc.py new file mode 100644 index 0000000..ca9e978 --- /dev/null +++ b/core/network/libtorrent_misc.py @@ -0,0 +1,45 @@ +import aria2p +import subprocess +from core.utils.data.state import state +from core.utils.general.logs import update_download_completed_by_hash +from core.utils.general.logs import consoleLog +from plyer import notification +import socket +import time +import sys + + +def send_notification(shutdown_event): + notified = set() + while not shutdown_event.is_set(): + try: + for d in state.aria2.get_downloads(): + if d.is_metadata: + continue + if d.progress == 100 and d.gid not in notified: + notification.notify( + title="Download finished", + message=f"{d.name} has finished downloading.", + timeout=4 + ) + notified.add(d.gid) + state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] + except Exception: + pass + time.sleep(5) + +def update_log(shutdown_event): + updated = set() + while not shutdown_event.is_set(): + try: + for d in state.aria2.get_downloads(): + if d.is_metadata: + continue + if d.progress == 100 and d.gid not in updated: + consoleLog(f"Marking {d.name} as completed") + update_download_completed_by_hash(d.info_hash, True) + updated.add(d.gid) + state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] + except Exception: + pass + time.sleep(5) \ No newline at end of file diff --git a/core/utils/config/config.py b/core/utils/config/config.py index 0532d17..fcc19fe 100644 --- a/core/utils/config/config.py +++ b/core/utils/config/config.py @@ -6,7 +6,7 @@ from core.utils.data.state import state def create_config(): config = configparser.ConfigParser() - config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}", + config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"download_speed_limit": f"{state.down_speed_limit}", f"upload_speed_limit": f"{state.up_speed_limit}", "ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}"} if platform.system() == "Windows": @@ -42,7 +42,8 @@ def read_config(): state.api_url = config.get("General", "api_url", fallback=state.api_url) state.aria2_threads = config.getint("General", "aria2_threads", fallback=state.aria2_threads) state.download_path = config.get("General", "download_path", fallback=state.download_path) - state.speed_limit = config.getint("General", "speed_limit", fallback=state.speed_limit) + state.down_speed_limit = config.getint("General", "download_speed_limit", fallback=state.down_speed_limit) + state.up_speed_limit = config.getint("General", "upload_speed_limit", fallback=state.up_speed_limit) state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates) state.image_path = config.get("General", "image_path", fallback=state.image_path) state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume) diff --git a/core/utils/config/settings.py b/core/utils/config/settings.py index 3f67f51..35eabfc 100644 --- a/core/utils/config/settings.py +++ b/core/utils/config/settings.py @@ -1,27 +1,18 @@ from core.utils.data.state import state -from core.utils.general.shutdown import kill_aria2server from .config import create_config -def restart_aria2c(): - import main # had to do this because of circle import :( - import signal - import atexit - kill_aria2server() - state.aria2process.wait() - state.aria2process = main.run_aria2server() - signal.signal(signal.SIGINT, main.keyboardinterrupthandler) - atexit.unregister(kill_aria2server) - atexit.register(kill_aria2server) -def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None, autoresume=None): +def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None): if thread_count is not None: state.aria2_threads = thread_count if apiurl is not None: state.api_url = apiurl if download_path is not None: state.download_path = download_path - if speed_limit is not None: - state.speed_limit = speed_limit + if down_speed_limit is not None: + state.down_speed_limit = down_speed_limit + if up_speed_limit is not None: + state.up_speed_limit = up_speed_limit if image_path is not None: state.image_path = image_path if autoresume is not None: @@ -29,5 +20,4 @@ def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_p create_config() - restart_aria2c() close() diff --git a/core/utils/data/state.py b/core/utils/data/state.py index cf6d33f..c2b952b 100644 --- a/core/utils/data/state.py +++ b/core/utils/data/state.py @@ -19,7 +19,10 @@ class AppState(QObject): self.tracker: str = "rutracker" self.api_url: str = "https://api.michijackson.xyz" self.download_path: str = str(Path.home() / "Downloads") - self.speed_limit: int = 0 + self.up_speed_limit: int = 0 + self.down_speed_limit: int = 0 + self.max_connections: int = 200 + self.max_downloads: int = 10 self.aria2process: Optional[Any] = None self.aria2: Any = None self.aria2p: Any = None diff --git a/main.py b/main.py index f02807d..47edf20 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,6 @@ from core.interface.gui import MainWindow from core.utils.data.state import state from core.utils.general.logs import consoleLog -from core.network.aria2_integration import aria2server -from core.network.aria2_integration import send_notification, update_log from core.utils.general.logs import get_download_logs from core.utils.general.shutdown import closehelper, shutdown_event from core.utils.general.wrappers import run_thread @@ -35,10 +33,6 @@ def run_gui(): widget.show() sys.exit(app.exec()) -def run_aria2server(): - aria2process = aria2server() - return aria2process - def keyboardinterrupthandler(signum, frame): closehelper() @@ -48,12 +42,10 @@ if __name__ == "__main__": count, downloads = split_data(logs) if args.debug: state.debug = args.debug # override of read_config - consoleLog("Starting Aria2 Server") - state.aria2process = run_aria2server() signal.signal(signal.SIGINT, keyboardinterrupthandler) - run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True)) + # run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True)) consoleLog("Started send_notification thread") - run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True)) + # run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True)) consoleLog("Started update_log thread") run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume))) consoleLog("Started check_completed thread") From a19cf469f68c69439e0599a9c20cf9525edf6b20 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:54:38 +0100 Subject: [PATCH 5/9] update configs and settings --- core/interface/dialogs/settings.py | 41 ++++++++++++++++++++++++++++-- core/network/libtorrent_wrapper.py | 2 +- core/utils/config/config.py | 7 ++--- core/utils/config/settings.py | 9 ++++--- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/core/interface/dialogs/settings.py b/core/interface/dialogs/settings.py index 81cf02b..7dd7ea5 100644 --- a/core/interface/dialogs/settings.py +++ b/core/interface/dialogs/settings.py @@ -22,7 +22,7 @@ def settings_dialog(self): consoleLog("Settings dialog opened") dialog = QDialog(self) dialog.setWindowTitle("Settings") - dialog.setFixedSize(800, 400) + dialog.setFixedSize(800, 550) dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(QLabel("Settings")) @@ -156,6 +156,43 @@ def settings_dialog(self): up_speed_limit.setFixedHeight(30) dialog.layout().addWidget(up_speed_limit_container) + ###################### + # CONNECTION CONFIGS # + ###################### + + max_connections_container = QWidget() + max_connections_layout = QHBoxLayout() + + max_connections_layout.addWidget(QLabel("Max Connections: ")) + max_connections = QSpinBox() + max_connections.setMinimum(0) + max_connections.setMaximum(10000000) + max_connections.setValue(state.max_connections) + max_connections_container.setLayout(max_connections_layout) + max_connections_layout.addWidget(max_connections) + max_connections.setFixedWidth(180) + max_connections.setFixedHeight(30) + dialog.layout().addWidget(max_connections_container) + + #################### + # DOWNLOAD CONFIGS # + #################### + + max_downloads_container = QWidget() + max_downloads_layout = QHBoxLayout() + + max_downloads_layout.addWidget(QLabel("Max Downloads: ")) + max_downloads = QSpinBox() + max_downloads.setMinimum(0) + max_downloads.setMaximum(10000000) + max_downloads.setValue(state.max_downloads) + max_downloads_container.setLayout(max_downloads_layout) + max_downloads_layout.addWidget(max_downloads) + max_downloads.setFixedWidth(180) + max_downloads.setFixedHeight(30) + dialog.layout().addWidget(max_downloads_container) + + ############### # IMAGE PATH # ############### @@ -188,7 +225,7 @@ def settings_dialog(self): save_btn = QPushButton("Save") cancel_btn = QPushButton("Cancel") - save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), down_speed_limit.value(), up_speed_limit.value(), image_path.text())) + save_btn.clicked.connect(lambda: save_settings(close_settings, api_url.text(), download_path.text(), down_speed_limit.value(), up_speed_limit.value(), image_path.text(), None, max_connections.value(), max_downloads.value())) cancel_btn.clicked.connect(dialog.reject) layout.addWidget(cancel_btn) diff --git a/core/network/libtorrent_wrapper.py b/core/network/libtorrent_wrapper.py index 4a24292..237a808 100644 --- a/core/network/libtorrent_wrapper.py +++ b/core/network/libtorrent_wrapper.py @@ -1,5 +1,5 @@ from core.utils.general.logs import consoleLog -from core.network.libtorrent_int import add_download, dl_status_loop +from core.network.libtorrent_int import add_download def add_magnet(uri): if uri is not None and uri.startswith("magnet:?"): diff --git a/core/utils/config/config.py b/core/utils/config/config.py index fcc19fe..a144743 100644 --- a/core/utils/config/config.py +++ b/core/utils/config/config.py @@ -6,8 +6,8 @@ from core.utils.data.state import state def create_config(): config = configparser.ConfigParser() - config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"download_speed_limit": f"{state.down_speed_limit}", f"upload_speed_limit": f"{state.up_speed_limit}", - "ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}"} + config["General"] = {"debug": True, "api_url": f"{state.api_url}", "download_path": f"{state.download_path}", f"download_speed_limit": f"{state.down_speed_limit}", f"upload_speed_limit": f"{state.up_speed_limit}", + "ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}", "max_connections": f"{state.max_connections}", "max_downloads": f"{state.max_downloads}"} if platform.system() == "Windows": config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming")) @@ -40,12 +40,13 @@ def read_config(): state.debug = config.getboolean("General", "debug", fallback=state.debug) state.api_url = config.get("General", "api_url", fallback=state.api_url) - state.aria2_threads = config.getint("General", "aria2_threads", fallback=state.aria2_threads) state.download_path = config.get("General", "download_path", fallback=state.download_path) state.down_speed_limit = config.getint("General", "download_speed_limit", fallback=state.down_speed_limit) state.up_speed_limit = config.getint("General", "upload_speed_limit", fallback=state.up_speed_limit) state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates) state.image_path = config.get("General", "image_path", fallback=state.image_path) state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume) + state.max_connections = config.getint("General", "max_connections", fallback=state.max_connections) + state.max_downloads = config.getint("General", "max_downloads", fallback=state.max_downloads) create_config() \ No newline at end of file diff --git a/core/utils/config/settings.py b/core/utils/config/settings.py index 35eabfc..0d7d143 100644 --- a/core/utils/config/settings.py +++ b/core/utils/config/settings.py @@ -2,9 +2,7 @@ from core.utils.data.state import state from .config import create_config -def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None): - if thread_count is not None: - state.aria2_threads = thread_count +def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None): if apiurl is not None: state.api_url = apiurl if download_path is not None: @@ -17,7 +15,10 @@ def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_p state.image_path = image_path if autoresume is not None: state.autoresume = autoresume - + if max_connections is not None: + state.max_connections = max_connections + if max_downloads is not None: + state.max_downloads = max_downloads create_config() close() From 748f0865b79bc0c3471bad5454b0b7b28b98a4f4 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:12:37 +0100 Subject: [PATCH 6/9] refactor shutdown process and integrate libtorrent session cleanup --- core/interface/gui.py | 9 +++------ core/network/libtorrent_misc.py | 17 +++++++++++++---- core/utils/general/shutdown.py | 29 ++++------------------------- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/core/interface/gui.py b/core/interface/gui.py index 2e838d1..9209022 100644 --- a/core/interface/gui.py +++ b/core/interface/gui.py @@ -38,17 +38,18 @@ from core.utils.general.wrappers import run_thread from core.utils.data.state import state from core.utils.network.download import download_selected from core.utils.network.update_checker import check_for_updates -from core.utils.general.shutdown import closehelper from core.interface.utils.tabhelper import create_tab from core.interface.utils.searchhelper import return_pressed from core.interface.dialogs.settings import settings_dialog +from core.network.libtorrent_misc import cleanup_session +from core.utils.general.shutdown import closehelper def download_update(latest_version): new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" - consoleLog("Downloading update...", True) + print("Downloading update...", True) response = r.get(url, allow_redirects=True) with open(new_filename, "wb") as f: f.write(response.content) @@ -459,7 +460,3 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): super().resizeEvent(event) table_width = self.qtablewidget.viewport().width() self.qtablewidget.setColumnWidth(1, int(table_width * 0.3)) - - - - diff --git a/core/network/libtorrent_misc.py b/core/network/libtorrent_misc.py index ca9e978..87f3e92 100644 --- a/core/network/libtorrent_misc.py +++ b/core/network/libtorrent_misc.py @@ -1,12 +1,21 @@ -import aria2p -import subprocess from core.utils.data.state import state from core.utils.general.logs import update_download_completed_by_hash from core.utils.general.logs import consoleLog from plyer import notification -import socket import time -import sys + + +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() + + del state.dl_session + state.dl_session = None + state.active_downloads.clear() + + def send_notification(shutdown_event): diff --git a/core/utils/general/shutdown.py b/core/utils/general/shutdown.py index f157aa1..28ab513 100644 --- a/core/utils/general/shutdown.py +++ b/core/utils/general/shutdown.py @@ -1,30 +1,9 @@ -from core.utils.data.state import state -from core.utils.general.logs import consoleLog import threading -import psutil -import sys - +from core.network.libtorrent_misc import cleanup_session +from core.utils.general.wrappers import run_thread shutdown_event = threading.Event() -def kill_aria2server(): - if sys.platform.startswith("win"): - process = "aria2c.exe" - else: - process = "aria2c" - - if state.aria2process: - state.aria2process.kill() - consoleLog("Killed Aria2") - - try: - for proc in psutil.process_iter(): - if proc.name() == process: - proc.kill() - consoleLog(f"Killed Aria2c (PID {proc.pid})") - except psutil.NoSuchProcess: - pass - def closehelper(): - shutdown_event.set() - kill_aria2server() \ No newline at end of file + run_thread(threading.Thread(target=cleanup_session)) + shutdown_event.set() \ No newline at end of file From 8e431f9826ca5a47332ba9dfcf1addaa3bf6cf09 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:17:20 +0100 Subject: [PATCH 7/9] integrate libtorrent for download status notifications and update logging --- core/network/libtorrent_misc.py | 32 +++++++++++++++++++------------- main.py | 5 +++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/network/libtorrent_misc.py b/core/network/libtorrent_misc.py index 87f3e92..3760796 100644 --- a/core/network/libtorrent_misc.py +++ b/core/network/libtorrent_misc.py @@ -2,6 +2,7 @@ from core.utils.data.state import state from core.utils.general.logs import update_download_completed_by_hash from core.utils.general.logs import consoleLog from plyer import notification +import libtorrent as lt import time @@ -22,17 +23,19 @@ def send_notification(shutdown_event): notified = set() while not shutdown_event.is_set(): try: - for d in state.aria2.get_downloads(): - if d.is_metadata: + for magnet_uri, magnetdl in list(state.active_downloads.items()): + if isinstance(magnetdl, dict): continue - if d.progress == 100 and d.gid not in notified: + + status = magnetdl.status() + + if status.state == lt.torrent_status.seeding and magnet_uri not in notified: notification.notify( title="Download finished", - message=f"{d.name} has finished downloading.", + message=f"{status.name} has finished downloading.", timeout=4 ) - notified.add(d.gid) - state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] + notified.add(magnet_uri) except Exception: pass time.sleep(5) @@ -41,14 +44,17 @@ def update_log(shutdown_event): updated = set() while not shutdown_event.is_set(): try: - for d in state.aria2.get_downloads(): - if d.is_metadata: + for magnet_uri, magnetdl in list(state.active_downloads.items()): + if isinstance(magnetdl, dict): continue - if d.progress == 100 and d.gid not in updated: - consoleLog(f"Marking {d.name} as completed") - update_download_completed_by_hash(d.info_hash, True) - updated.add(d.gid) - state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata] + + status = magnetdl.status() + + if status.state == lt.torrent_status.seeding and magnet_uri not in updated: + consoleLog(f"Marking {status.name} as completed") + info_hash = str(status.info_hash) + update_download_completed_by_hash(info_hash, True) + updated.add(magnet_uri) except Exception: pass time.sleep(5) \ No newline at end of file diff --git a/main.py b/main.py index 47edf20..bc1d890 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from core.interface.gui import MainWindow from core.utils.data.state import state from core.utils.general.logs import consoleLog +from core.network.libtorrent_misc import send_notification, update_log from core.utils.general.logs import get_download_logs from core.utils.general.shutdown import closehelper, shutdown_event from core.utils.general.wrappers import run_thread @@ -43,9 +44,9 @@ if __name__ == "__main__": if args.debug: state.debug = args.debug # override of read_config signal.signal(signal.SIGINT, keyboardinterrupthandler) - # run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True)) + run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True)) consoleLog("Started send_notification thread") - # run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True)) + run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True)) consoleLog("Started update_log thread") run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume))) consoleLog("Started check_completed thread") From 7f155547eccaad8d003fca9c5c6ee9955482e7bf Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:19:28 +0100 Subject: [PATCH 8/9] add libtorrent and libtorrent-windows-dll to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fcbb043..e0d7f51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,10 @@ requests==2.32.2 PySide6==6.10.1 beautifulsoup4==4.13.5 darkdetect==0.8.0 -aria2p==0.12.1 pyinstaller==6.15.0 pyqtdarktheme==0.1.7 aiohttp==3.13.0 plyer==2.1.0 psutil==7.1.0 +libtorrent==2.0.11 +libtorrent-windows-dll==0.0.3 \ No newline at end of file From dc933d54f3f6af3b65f54c1c2cfc51590ebeb1ea Mon Sep 17 00:00:00 2001 From: shayaa <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:36:49 +0100 Subject: [PATCH 9/9] fix broken merge --- core/interface/gui.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/interface/gui.py b/core/interface/gui.py index cff36a3..1219a40 100644 --- a/core/interface/gui.py +++ b/core/interface/gui.py @@ -294,11 +294,18 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): download = state.downloads[index.row()] button = editor.findChild(QtWidgets.QPushButton) + paused = index.data(Qt.UserRole) + + magnet_link = list(state.active_downloads.keys())[index.row()] + magnetdl = state.active_downloads[magnet_link] + status = magnetdl.status() + if button: - if download.progress == 100: + if status.state == lt.torrent_status.seeding: button.setText("📁") else: - button.setText("▶︎" if download.is_paused else "⏸︎") + button.setText("▶︎" if status.paused else "⏸︎") + def createEditor(self, parent, option, index): @@ -308,10 +315,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): widget.setStyleSheet("border: none;") layout = QHBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) - if download.progress == 100: + if status.state == lt.torrent_status.seeding: btnPause = QtWidgets.QPushButton("📁") else: - btnPause = QtWidgets.QPushButton("▶︎" if download.is_paused else "⏸︎") + btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎") btnPause.setFixedSize(40, 30) btnPause.clicked.connect(lambda: self.clicked.emit(index.row())) layout.addStretch()