diff --git a/.gitignore b/.gitignore index 9bd9aa6..4895b70 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.python-version # Spyder project settings .spyderproject diff --git a/src/data/hosts/buzzheavier.py b/src/data/hosts/buzzheavier.py index c0d8251..f11cefb 100644 --- a/src/data/hosts/buzzheavier.py +++ b/src/data/hosts/buzzheavier.py @@ -1,9 +1,13 @@ +from utils.logging.logs import consoleLog import requests def scrape_buzzheavier(url): - - response = requests.get(url) - response.raise_for_status() + try: + response = requests.get(url, timeout=15) + response.raise_for_status() + except requests.RequestException as e: + consoleLog(f"Buzzheavier: Failed to fetch page - {e}") + return None download_url = url + '/download' @@ -12,7 +16,11 @@ def scrape_buzzheavier(url): 'hx-request': 'true', 'referer': url } - - head_response = requests.head(download_url, headers=headers, allow_redirects=False) - hx_redirect = head_response.headers.get('hx-redirect') - return hx_redirect \ No newline at end of file + + try: + head_response = requests.head(download_url, headers=headers, allow_redirects=False, timeout=15) + hx_redirect = head_response.headers.get('hx-redirect') + return hx_redirect + except requests.RequestException as e: + consoleLog(f"Buzzheavier: Failed to fetch download URL - {e}") + return None \ No newline at end of file diff --git a/src/data/hosts/gofile.py b/src/data/hosts/gofile.py index d99a6de..0ca0335 100644 --- a/src/data/hosts/gofile.py +++ b/src/data/hosts/gofile.py @@ -23,42 +23,44 @@ def scrape_gofile(url): content_id = match.group(1) session = requests.Session() + try: + # Create a guest account + r = session.post(f"{_API_BASE}/accounts", headers={"User-Agent": _USER_AGENT}, timeout=15) + data = r.json() + if data.get("status") != "ok": + consoleLog("GoFile: Failed to create guest account") + return None - # Create a guest account - r = session.post(f"{_API_BASE}/accounts", headers={"User-Agent": _USER_AGENT}) - data = r.json() - if data.get("status") != "ok": - consoleLog("GoFile: Failed to create guest account") - return None + account_token = data["data"]["token"] + website_token = _generate_website_token(account_token) - account_token = data["data"]["token"] - website_token = _generate_website_token(account_token) + headers = { + "User-Agent": _USER_AGENT, + "Authorization": f"Bearer {account_token}", + "X-Website-Token": website_token, + "X-BL": "en-US", + "Referer": "https://gofile.io/", + "Origin": "https://gofile.io", + } - headers = { - "User-Agent": _USER_AGENT, - "Authorization": f"Bearer {account_token}", - "X-Website-Token": website_token, - "X-BL": "en-US", - "Referer": "https://gofile.io/", - "Origin": "https://gofile.io", - } + # Fetch folder contents + r = session.get(f"{_API_BASE}/contents/{content_id}", headers=headers, timeout=15) + data = r.json() + if data.get("status") != "ok": + consoleLog(f"GoFile: API error - {data.get('status')}") + return None - # Fetch folder contents - r = session.get(f"{_API_BASE}/contents/{content_id}", headers=headers) - data = r.json() - if data.get("status") != "ok": - consoleLog(f"GoFile: API error - {data.get('status')}") - return None + children = data["data"].get("children", {}) + if not children: + consoleLog("GoFile: No files found") + return None - children = data["data"].get("children", {}) - if not children: - consoleLog("GoFile: No files found") - return None + first_child = next(iter(children.values())) + link = first_child.get("link") + if not link: + consoleLog("GoFile: No download link in response") + return None - first_child = next(iter(children.values())) - link = first_child.get("link") - if not link: - consoleLog("GoFile: No download link in response") - return None - - return link, headers \ No newline at end of file + return link, headers + finally: + session.close() \ No newline at end of file diff --git a/src/data/sources/rutracker.py b/src/data/sources/rutracker.py index 73b6a4b..59a259e 100644 --- a/src/data/sources/rutracker.py +++ b/src/data/sources/rutracker.py @@ -2,6 +2,7 @@ from utils.network.jsonhandler import split_data, format_data from utils.data.tracker import get_magnet_link from utils.logging.logs import consoleLog from utils.data.state import state +from urllib.parse import quote from typing import Dict import requests @@ -11,10 +12,18 @@ class RutrackerScraper: is_magnet = True def search(self, query): - search = requests.get(f"{state.api_url}/search?q={query}", timeout=15) + try: + search = requests.get(f"{state.api_url}/search?q={quote(query)}", timeout=15) + except requests.RequestException as e: + consoleLog(f"Failed to reach server: {e}") + return [] consoleLog("Sent request to server") if search: - _, data, _, success, cached = split_data(search.text) + try: + _, data, _, success, cached = split_data(search.text) + except (KeyError, ValueError) as e: + consoleLog(f"Failed to parse server response: {e}") + return [] if cached: consoleLog("Server response cached") if success: diff --git a/src/data/sources/steamrip.py b/src/data/sources/steamrip.py index af0c376..7162f17 100644 --- a/src/data/sources/steamrip.py +++ b/src/data/sources/steamrip.py @@ -27,7 +27,7 @@ class SteamripScraper: try: url = f"https://steamrip.com/games-list-page/" - response = requests.get(url) + response = requests.get(url, timeout=15) response.raise_for_status() text = response.text except requests.RequestException as e: @@ -61,7 +61,7 @@ class SteamripScraper: def scrape_steamrip_game_downloads(self, url): try: - response = requests.get(url) + response = requests.get(url, timeout=15) response.raise_for_status() except requests.RequestException as e: consoleLog(f"SteamRip: Failed to fetch game page - {e}") diff --git a/src/data/sources/uztracker.py b/src/data/sources/uztracker.py index 1e871af..c3c2137 100644 --- a/src/data/sources/uztracker.py +++ b/src/data/sources/uztracker.py @@ -19,7 +19,7 @@ class UztrackerScraper: posts = [] try: - response = requests.get(search_url) + response = requests.get(search_url, timeout=15) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_')) diff --git a/src/interface/dialogs/contextmenu.py b/src/interface/dialogs/contextmenu.py index 7e8d409..dffeb66 100644 --- a/src/interface/dialogs/contextmenu.py +++ b/src/interface/dialogs/contextmenu.py @@ -1,4 +1,5 @@ from PySide6.QtCore import Qt, QPoint, Signal, QThread +from network.direct_download.handle import DirectDownloadHandle from utils.logging.logs import consoleLog, remove_download_log from utils.network.download import download_selected from utils.data.tracker import get_magnet_link @@ -121,7 +122,12 @@ class ContextMenu_Downloads: remove_download_log(magnet_link) - if state.dl_session: + if isinstance(magnetdl, DirectDownloadHandle): + try: + magnetdl.stop() + except Exception as e: + consoleLog(f"Exception while stopping direct download: {e}") + elif state.dl_session: try: state.dl_session.remove_torrent(magnetdl) except Exception as e: @@ -179,29 +185,24 @@ class ContextMenu_Downloads: remove_download_log(magnet_link) - if state.dl_session: + if isinstance(magnetdl, DirectDownloadHandle): + try: + magnetdl.stop() + except Exception as e: + consoleLog(f"Exception while stopping direct download: {e}") + elif state.dl_session: try: state.dl_session.remove_torrent(magnetdl) - time.sleep(0.5) except Exception as e: consoleLog(f"Exception while removing download from LibTorrent: {e}") if download_path and os.path.exists(download_path): - last_error = None - for attempt in range(3): - try: - if os.path.isfile(download_path) or os.path.isdir(download_path): - send2trash(download_path) - consoleLog(f"Deleted files for: {torrent_name}", 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) + try: + if os.path.isfile(download_path) or os.path.isdir(download_path): + send2trash(download_path) + consoleLog(f"Deleted files for: {torrent_name}", True) + except Exception as e: + consoleLog(f"Error deleting files: {e}", True) else: consoleLog(f"Removed entry (files not found): {torrent_name}", True) diff --git a/src/interface/gui.py b/src/interface/gui.py index 4cf0eba..384bea3 100644 --- a/src/interface/gui.py +++ b/src/interface/gui.py @@ -17,6 +17,7 @@ from interface.utils.searchhelper import run_search from interface.utils.tabhelper import create_tab from utils.general.shutdown import closehelper from utils.general.wrappers import run_thread +from utils.logging.logs import log_emitter from interface.dialogs.image import Image from utils.data.state import state @@ -68,14 +69,12 @@ class CenteredDelegate(QStyledItemDelegate): class MainWindow(QtWidgets.QMainWindow, QWidget): eventFilter = eventFilter - log_signal = Signal(str) search_results_signal = Signal(list) _instance = None def __init__(self): super().__init__() MainWindow._instance = self - self.log_signal.connect(self._on_log_signal) self.search_results_signal.connect(self._on_search_results) pixmap = QPixmap() @@ -132,6 +131,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.consoleLog = QTextEdit() self.statusbar = QStatusBar() + log_emitter.new_log.connect(self._on_log_signal) flush_log_buffer() self._tracker_elided_delegate = ElidedItemDelegate(lambda: self._tracker_hovered_row, self) @@ -307,6 +307,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): table.setItemDelegateForColumn(i, self._tracker_hover_delegate) def _start_search(self): + self.searchbar.setEnabled(False) + state.trackertable.setRowCount(0) + self.show_empty_results(False) + def _search_thread(): try: run_search(self) @@ -315,13 +319,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.search_results_signal.emit([]) run_thread(threading.Thread(target=_search_thread)) - @staticmethod - def add_log(text): - if hasattr(MainWindow, "_instance") and MainWindow._instance: - MainWindow._instance.log_signal.emit(text) - return True - return False - def _on_log_signal(self, text): if hasattr(self, 'consoleLog'): self.consoleLog.append(text) diff --git a/src/interface/utils/searchhelper.py b/src/interface/utils/searchhelper.py index eeefdf3..b2bf889 100644 --- a/src/interface/utils/searchhelper.py +++ b/src/interface/utils/searchhelper.py @@ -6,9 +6,6 @@ for scraper in SCRAPERS: state.trackers[scraper.name] = scraper def run_search(self) -> None: - state.trackertable.setRowCount(0) - self.show_empty_results(False) - search_text = self.searchbar.text() # Check for empty search if search_text == "": @@ -16,12 +13,11 @@ def run_search(self) -> None: return state.posts = [] - self.searchbar.setEnabled(False) consoleLog(f"User searched for: {search_text}") - + # Get current tracker and call its search function scraper = state.trackers[state.currenttracker] state.posts = scraper.search(search_text) - + # Update GUI headers to match the current scraper self.search_results_signal.emit(scraper.headers) \ No newline at end of file diff --git a/src/main.py b/src/main.py index af094f5..6a3f5f5 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,3 @@ -from network.libtorrent_misc import send_notification, update_log, check_deleted_files from utils.logging.loghandler import split_data, check_completed, check_downloads from network.interface import list_interfaces, init_interfaces from PySide6.QtNetwork import QLocalServer, QLocalSocket @@ -9,12 +8,13 @@ from utils.general.shutdown import closehelper from utils.general.wrappers import run_thread from interface.assets.base64_icons import logo_base64 from PySide6.QtWidgets import QSystemTrayIcon, QMenu +from network.libtorrent_misc import AppDaemons from utils.general.shutdown import force_exit from utils.config.config import read_config from PySide6.QtGui import QAction, QPixmap from utils.logging.logs import consoleLog from interface.gui import MainWindow -from PySide6.QtCore import Qt, QObject +from PySide6.QtCore import Qt, QObject, QThread from utils.data.state import state from PySide6 import QtWidgets import qdarktheme @@ -45,7 +45,6 @@ class SingleInstance(QObject): self.server.listen(SERVER_NAME) self.server.newConnection.connect(self.handle_connection) self.is_running = False - self.is_running = False def handle_connection(self): socket = self.server.nextPendingConnection() @@ -143,9 +142,12 @@ def main(): init_interfaces() consoleLog(f"Current Bound: {state.bound_interface}") # Start background daemon threads - run_thread(threading.Thread(target=send_notification, args=(state.shutdown_event,), daemon=True)) - run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True)) - run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True)) + app._daemon_thread = QThread() + app._daemons = AppDaemons() + app._daemons.moveToThread(app._daemon_thread) + app._daemon_thread.started.connect(app._daemons.start_all) + app.aboutToQuit.connect(app._daemon_thread.quit) + app._daemon_thread.start() # Start background non-daemon threads run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume))) run_thread(threading.Thread(target=check_downloads, args=(downloads,))) diff --git a/src/network/libtorrent_int.py b/src/network/libtorrent_int.py index a1e59da..0c71d3d 100644 --- a/src/network/libtorrent_int.py +++ b/src/network/libtorrent_int.py @@ -163,7 +163,7 @@ def add_seed(magnet_uri, file_path): return False with state.downloads_lock: state.active_downloads[magnet_uri] = handle - state.seeded_magnets.add(magnet_uri) + state.seeded_magnets.add(magnet_uri) return True diff --git a/src/network/libtorrent_misc.py b/src/network/libtorrent_misc.py index b1c6664..72532e6 100644 --- a/src/network/libtorrent_misc.py +++ b/src/network/libtorrent_misc.py @@ -1,5 +1,6 @@ from utils.logging.logs import update_download_completed_by_hash from utils.logging.logs import consoleLog +from PySide6.QtCore import QObject, QTimer from utils.data.state import state from plyer import notification import libtorrent as lt @@ -19,55 +20,28 @@ def cleanup_session(): with state.downloads_lock: state.active_downloads.clear() -def send_notification(shutdown_event): - notified = set() - while not shutdown_event.is_set(): - try: - with state.downloads_lock: - items = list(state.active_downloads.items()) - for magnet_uri, magnetdl in items: - if isinstance(magnetdl, dict): - continue - - status = magnetdl.status() - - if status.state == lt.torrent_status.seeding and magnet_uri not in notified: - notification.notify( - title="Download finished", - message=f"{status.name} has finished downloading.", - timeout=4 - ) - notified.add(magnet_uri) - except Exception as e: - consoleLog(f"Exception while sending notification: {e}") - time.sleep(5) +class AppDaemons(QObject): + def __init__(self): + super().__init__() -def update_log(shutdown_event): - updated = set() - while not shutdown_event.is_set(): - try: - with state.downloads_lock: - items = list(state.active_downloads.items()) - for magnet_uri, magnetdl in items: - if isinstance(magnetdl, dict): - continue - - status = magnetdl.status() - - if status.state == lt.torrent_status.seeding and magnet_uri not in updated and magnet_uri not in state.seeded_magnets: - consoleLog(f"Marking {status.name} as completed") - if hasattr(status, 'info_hashes'): - info_hash = str(status.info_hashes.v1) - else: - info_hash = str(status.info_hash) - update_download_completed_by_hash(info_hash, True) - updated.add(magnet_uri) - except Exception as e: - consoleLog(f"Exception while updating log file: {e}") - time.sleep(5) + self.notified_magnets = set() + self.updated_magnets = set() -def check_deleted_files(shutdown_event): - while not shutdown_event.is_set(): + # Create Timers + self.deleted_files_timer = QTimer(self) + self.completed_downloads_timer = QTimer(self) + + # Connect Timer Signals + self.deleted_files_timer.timeout.connect(self.check_deleted_files) + self.completed_downloads_timer.timeout.connect(self.check_completed_downloads) + + def start_all(self): + consoleLog("Starting Timer: deleted_files") + self.deleted_files_timer.start(2000) + consoleLog("Starting Timer: completed_downloads") + self.completed_downloads_timer.start(5000) + + def check_deleted_files(self): try: with state.downloads_lock: items = list(state.active_downloads.items()) @@ -87,4 +61,36 @@ def check_deleted_files(shutdown_event): 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 + + def check_completed_downloads(self): + try: + with state.downloads_lock: + items = list(state.active_downloads.items()) + seeded = set(state.seeded_magnets) + for magnet_uri, magnetdl in items: + if isinstance(magnetdl, dict): + continue + + status = magnetdl.status() + + if status.state == lt.torrent_status.seeding: + + if magnet_uri not in self.updated_magnets and magnet_uri not in seeded: + consoleLog(f"Marking {status.name} as completed") + if hasattr(status, 'info_hashes'): + info_hash = str(status.info_hashes.v1) + else: + info_hash = str(status.info_hash) + update_download_completed_by_hash(info_hash, True) + self.updated_magnets.add(magnet_uri) + + if magnet_uri not in self.notified_magnets: + notification.notify( + title="Download finished", + message=f"{status.name} has finished downloading.", + timeout=4 + ) + self.notified_magnets.add(magnet_uri) + + except Exception as e: + consoleLog(f"Exception while checking downloads: {e}") \ No newline at end of file diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 6b08e9a..5277614 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -61,6 +61,7 @@ class AppState(QObject): self.max_connections: int = 200 self.max_downloads: int = 10 self.downloads_lock = threading.RLock() + self._log_lock = threading.Lock() @property def image_path(self) -> str: diff --git a/src/utils/logging/logs.py b/src/utils/logging/logs.py index 03b9de4..16819df 100644 --- a/src/utils/logging/logs.py +++ b/src/utils/logging/logs.py @@ -1,4 +1,5 @@ from utils.data.models import Download, DownloadList +from PySide6.QtCore import QObject, Signal from utils.data.state import state from dataclasses import asdict from datetime import datetime @@ -7,6 +8,10 @@ import time import os import re +class LogSignal(QObject): + new_log = Signal(str) + +log_emitter = LogSignal() def _downloads_file_path() -> str: return os.path.join(state.settings_path, "downloads.json") @@ -187,27 +192,22 @@ def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadLi def set_main_window(window): state.main_window = window -def flush_log_buffer(): # credits to claude - if state.log_buffer: - try: - from interface.gui import MainWindow - for log_entry in state.log_buffer: - MainWindow.add_log(log_entry) - state.log_buffer = [] - except Exception as e: - consoleLog(f"Exception while flushing log buffer: {e}") +def flush_log_buffer(): + with state._log_lock: + for text in state.log_buffer: + log_emitter.new_log.emit(text) + + state.log_buffer.clear() def consoleLog(text, printAnyways = False): now = datetime.now() current_time = now.strftime("%H:%M:%S") formatted_text = f"[{current_time}] {text}" - try: - from interface.gui import MainWindow - if not MainWindow.add_log(formatted_text): - state.log_buffer.append(formatted_text) - except Exception: + with state._log_lock: state.log_buffer.append(formatted_text) + log_emitter.new_log.emit(formatted_text) + if state.debug or printAnyways: print(formatted_text) diff --git a/src/utils/network/updater.py b/src/utils/network/updater.py index c07b585..5003897 100644 --- a/src/utils/network/updater.py +++ b/src/utils/network/updater.py @@ -2,13 +2,12 @@ from network.direct_download.handle import DirectDownloadHandle from utils.general.shutdown import closehelper from utils.logging.logs import consoleLog from utils.data.state import state -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QTimer, QEventLoop from PySide6 import QtWidgets import libtorrent as lt import subprocess import tempfile import hashlib -import time import sys import os @@ -42,7 +41,6 @@ def download_update(assets: list): 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) @@ -55,14 +53,19 @@ def download_update(assets: list): handle = DirectDownloadHandle(url, filename, tempfile.gettempdir()) handle.start() - while True: - QtWidgets.QApplication.processEvents() + loop = QEventLoop() + timer = QTimer() + timer.setInterval(100) + + def poll_progress(): status = handle.status() if status.error: state.down_speed_limit = original_limit progress.close() consoleLog(f"Update download failed: {status.error}") + timer.stop() + loop.quit() return if status.total_wanted > 0: @@ -72,9 +75,25 @@ def download_update(assets: list): progress.setLabelText(f"Downloading update... ({speed_mb:.1f} MB/s)") if status.state == lt.torrent_status.seeding: - break + timer.stop() + loop.quit() - time.sleep(0.1) + if progress.wasCanceled(): + state.down_speed_limit = original_limit + consoleLog("Update download cancelled by user.") + timer.stop() + loop.quit() + return + + timer.timeout.connect(poll_progress) + timer.start() + loop.exec() + + if progress.wasCanceled(): + return + + if handle.status().error: + return state.down_speed_limit = original_limit @@ -84,6 +103,11 @@ def download_update(assets: list): else: progress.close() consoleLog("Error: Invalid file hash, file may be corrupted") + QtWidgets.QMessageBox.critical( + None, + "Update Failed", + "Error: Invalid file hash, file may be corrupted" + ) sys.exit(0) else: consoleLog("Skipping hash verification (no hash found for release)") @@ -94,5 +118,4 @@ def download_update(assets: list): closehelper() subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"]) - time.sleep(1) os._exit(0) \ No newline at end of file