Compare commits

...

9 Commits

23 changed files with 316 additions and 237 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.tracker import get_magnet_link
from core.utils.data.state import state
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from typing import Dict from typing import Dict
import requests import requests
+1 -1
View File
@@ -27,5 +27,5 @@ def scrape_gofile(url):
return temp[child[0]]["link"] return temp[child[0]]["link"]
except: 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.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 from typing import Dict
import requests
def scrape_rutracker(query): def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15) 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 from typing import Dict
import webbrowser import webbrowser
import requests import requests
import re
import time import time
import re
cache = { cache = {
"data": [], "data": [],
+4 -4
View File
@@ -1,10 +1,10 @@
import requests from core.utils.data.tracker import get_magnet_link
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.state import state 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 from typing import Dict
import requests
def scrape_uztracker(query): 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 general_tab
from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.tabhelper import network_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 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.QtCore import Qt, QSize
from PySide6 import QtWidgets
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QPushButton, QPushButton,
@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
QComboBox, QComboBox,
QTabWidget, QTabWidget,
) )
import platform 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>' 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>'
+123 -136
View File
@@ -1,16 +1,17 @@
from core.utils.logging.logs import consoleLog, remove_download_log, flush_log_buffer from core.utils.logging.logs import consoleLog, remove_download_log, flush_log_buffer
from core.utils.general.wrappers import run_thread from core.interface.assets.base64_icons import settings_white_base64
from core.utils.data.state import state 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.download import download_selected
from core.utils.network.update_checker import get_updates 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.tabhelper import create_tab
from core.interface.utils.searchhelper import return_pressed
from core.interface.utils.svghelper import svg_icon 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.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.utils.data.state import state
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import ( from PySide6.QtCore import (
@@ -52,16 +53,16 @@ from PySide6.QtGui import (
QColor, QColor,
) )
import libtorrent as lt
import subprocess
import darkdetect import darkdetect
import threading import threading
import platform import platform
import requests as r
import os
import subprocess
import libtorrent as lt
import sys
import json
import base64 import base64
import json
import time
import sys
import os
def _is_dark_mode(): def _is_dark_mode():
@@ -141,75 +142,9 @@ 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_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>' 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(): def windowCloseHelper():
QGuiApplication.quit() QGuiApplication.quit()
class ElidedItemDelegate(QStyledItemDelegate): class ElidedItemDelegate(QStyledItemDelegate):
def __init__(self, get_hovered_row, parent=None): def __init__(self, get_hovered_row, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -274,17 +209,17 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
state.version = build_info.get("version") state.version = build_info.get("version")
if state.ignore_updates is False and platform.system() == "Windows": if state.ignore_updates is False and platform.system() == "Windows":
assets = get_updates() assets, latest = get_updates()
if assets != None: if assets != None:
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information) msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("Update Available") msg.setWindowTitle("Update Available")
msg.setText("A new version is available.") msg.setText(f"A new version is available\n({latest})")
msg.setInformativeText("Press Ok to download the update.") msg.setInformativeText("Press Ok to download.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore) msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
response = msg.exec_() response = msg.exec_()
if response == QMessageBox.StandardButton.Ok: if response == QMessageBox.StandardButton.Ok:
_download_update(assets) download_update(assets)
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
@@ -488,22 +423,40 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
button = editor.findChild(QtWidgets.QPushButton) button = editor.findChild(QtWidgets.QPushButton)
magnet_link = list(state.active_downloads.keys())[index.row()] if not button:
magnetdl = state.active_downloads[magnet_link] return
status = magnetdl.status() try:
if button: with state.downloads_lock:
if status.state == lt.torrent_status.seeding: keys = list(state.active_downloads.keys())
button.setIcon(svg_icon(SVG_FOLDER, 18)) if index.row() >= len(keys):
button.setText("") return
else: magnet_link = keys[index.row()]
is_user_paused = status.paused and not status.auto_managed magnetdl = state.active_downloads[magnet_link]
button.setIcon(svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18)) status = magnetdl.status()
button.setText("") except (RuntimeError, IndexError, KeyError):
return
if status.state == lt.torrent_status.seeding:
button.setIcon(svg_icon(SVG_FOLDER, 18))
button.setText("")
else:
is_user_paused = status.paused and not status.auto_managed
button.setIcon(svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
button.setText("")
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
magnet_link = list(state.active_downloads.keys())[index.row()] try:
magnetdl = state.active_downloads[magnet_link] with state.downloads_lock:
status = magnetdl.status() keys = list(state.active_downloads.keys())
if index.row() >= len(keys):
return QWidget(parent)
magnet_link = keys[index.row()]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
except (RuntimeError, IndexError, KeyError):
return QWidget(parent)
widget = QWidget(parent) widget = QWidget(parent)
widget.setStyleSheet("border: none; background: transparent;") widget.setStyleSheet("border: none; background: transparent;")
layout = QHBoxLayout(widget) layout = QHBoxLayout(widget)
@@ -526,6 +479,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
widget.setLayout(layout) widget.setLayout(layout)
return widget return widget
def editorEvent(self, event, model, option, index): def editorEvent(self, event, model, option, index):
return False return False
@@ -846,10 +800,14 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if idx.isValid(): if idx.isValid():
editor = self.downloadList.indexWidget(idx) editor = self.downloadList.indexWidget(idx)
if editor and delegate: if editor and delegate:
delegate.setEditorData(editor, idx) try:
delegate.setEditorData(editor, idx)
except RuntimeError:
pass
self._update_speed_label() self._update_speed_label()
def _update_speed_label(self): def _update_speed_label(self):
total_down = 0 total_down = 0
total_up = 0 total_up = 0
@@ -991,60 +949,89 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if not hasattr(self, '_context_menu_row'): if not hasattr(self, '_context_menu_row'):
return return
row = self._context_menu_row row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads): with state.downloads_lock:
return if row < 0 or row >= len(state.active_downloads):
magnet_link = list(state.active_downloads.keys())[row] return
magnetdl = state.active_downloads[magnet_link] magnet_link = list(state.active_downloads.keys())[row]
confirm = QMessageBox.question(self, "Cancel Download", f"Are you sure you want to cancel the download of '{magnetdl.status().name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) magnetdl = state.active_downloads[magnet_link]
# Cache the name BEFORE removing from session
try:
torrent_name = magnetdl.status().name
except RuntimeError:
torrent_name = "Unknown"
confirm = QMessageBox.question(
self, "Cancel Download",
f"Are you sure you want to cancel the download of '{torrent_name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if confirm == QMessageBox.StandardButton.Yes: if confirm == QMessageBox.StandardButton.Yes:
if hasattr(magnetdl, 'stop'): with state.downloads_lock:
magnetdl.stop() state.active_downloads.pop(magnet_link, None)
elif state.dl_session: remove_download_log(magnet_link)
if state.dl_session:
try: try:
state.dl_session.remove_torrent(magnetdl) state.dl_session.remove_torrent(magnetdl)
except Exception: except Exception:
pass pass
del state.active_downloads[magnet_link]
remove_download_log(magnet_link) consoleLog(f"Cancelled download: {torrent_name}", True)
consoleLog(f"Cancelled download: {magnetdl.status().name}", True)
def deleteFileAction(self): def deleteFileAction(self):
if not hasattr(self, '_context_menu_row'): if not hasattr(self, '_context_menu_row'):
return return
row = self._context_menu_row row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads): with state.downloads_lock:
if row < 0 or row >= len(state.active_downloads):
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
try:
status = magnetdl.status()
save_path = status.save_path
torrent_name = status.name
except RuntimeError:
consoleLog("Error: torrent handle already invalid", True)
with state.downloads_lock:
state.active_downloads.pop(magnet_link, None)
remove_download_log(magnet_link)
return return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
save_path = status.save_path
torrent_name = status.name
download_path = os.path.join(save_path, torrent_name) 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 '{magnetdl.status().name}'? This action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) 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.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if confirm == QMessageBox.StandardButton.Yes: if confirm == QMessageBox.StandardButton.Yes:
if hasattr(magnetdl, 'stop'): with state.downloads_lock:
magnetdl.stop() state.active_downloads.pop(magnet_link, None)
elif state.dl_session: remove_download_log(magnet_link)
if state.dl_session:
try: try:
state.dl_session.remove_torrent(magnetdl) state.dl_session.remove_torrent(magnetdl)
time.sleep(0.5)
except Exception: except Exception:
pass pass
if download_path and os.path.exists(download_path): if download_path and os.path.exists(download_path):
try: import shutil
if os.path.isfile(download_path): last_error = None
remove_download_log(magnet_link) for attempt in range(3):
os.remove(download_path) try:
del state.active_downloads[magnet_link] if os.path.isfile(download_path):
consoleLog(f"Deleted files for: {magnetdl.status().name}", True) os.remove(download_path)
else: else:
import shutil shutil.rmtree(download_path)
remove_download_log(magnet_link) consoleLog(f"Deleted files for: {torrent_name}", True)
shutil.rmtree(download_path) last_error = None
del state.active_downloads[magnet_link] break
consoleLog(f"Deleted files for: {magnetdl.status().name}", True) except Exception as e:
except Exception as e: last_error = e
consoleLog(f"Error deleting files: {e}", True) if attempt < 2:
time.sleep(0.5)
if last_error:
consoleLog(f"Error deleting files: {last_error}", True)
else: else:
del state.active_downloads[magnet_link] consoleLog(f"Removed entry (files not found): {torrent_name}", True)
remove_download_log(magnet_link)
consoleLog(f"Removed entry (files not found): {magnetdl.status().name}", True)
+12 -12
View File
@@ -1,17 +1,17 @@
import os from core.utils.logging.logs import consoleLog, update_download_completed
import threading
import requests
import json
import hashlib
import time
from concurrent.futures import ThreadPoolExecutor, as_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 from typing import Optional
import libtorrent as lt 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: class SimpleThrottler:
def __init__(self): def __init__(self):
@@ -213,11 +213,11 @@ class DirectDownloadHandle:
self._status.mark_completed() self._status.mark_completed()
update_download_completed(self.url, True) update_download_completed(self.url, True)
self._clear_state() self._clear_state()
consoleLog(f"Finished downloading {self._name}") consoleLog(f"Finished downloading {self._name}")
except Exception as e: except Exception as e:
self._status.mark_error(str(e)) self._status.mark_error(str(e))
consoleLog(f"Download failed for {self._name}: {e}") consoleLog(f"Download failed for {self._name}: {e}")
finally: finally:
if self._session: if self._session:
self._session.close() self._session.close()
+2 -2
View File
@@ -1,8 +1,8 @@
import threading
import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import libtorrent as lt import libtorrent as lt
import threading
import time
@dataclass @dataclass
+3 -3
View File
@@ -1,7 +1,7 @@
import os
import requests
from typing import Optional
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
from typing import Optional
import requests
import os
def sanitize_filename(name: str) -> str: def sanitize_filename(name: str) -> str:
# Remove or replace dangerous characters # Remove or replace dangerous characters
+11 -11
View File
@@ -1,13 +1,13 @@
import time from core.network.interface import get_interface_ip
import os 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 platform
import ctypes import ctypes
from core.utils.general.wrappers import run_thread import time
from core.network.interface import get_interface_ip import os
import threading
import libtorrent as lt
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog
global loop_running global loop_running
@@ -55,7 +55,7 @@ def init_session():
consoleLog("Initialized Session") consoleLog("Initialized Session")
def add_download(magnet_uri, dl_path=state.download_path): def add_download(magnet_uri):
if state.active_downloads is None: if state.active_downloads is None:
state.active_downloads = {} state.active_downloads = {}
@@ -89,7 +89,7 @@ def add_download(magnet_uri, dl_path=state.download_path):
try: try:
params = lt.parse_magnet_uri(magnet_uri) params = lt.parse_magnet_uri(magnet_uri)
params.save_path = "." params.save_path = state.download_path
handle = state.dl_session.add_torrent(params) handle = state.dl_session.add_torrent(params)
@@ -100,7 +100,7 @@ def add_download(magnet_uri, dl_path=state.download_path):
if free_space > total_size: if free_space > total_size:
magnetdl = lt.parse_magnet_uri(magnet_uri) magnetdl = lt.parse_magnet_uri(magnet_uri)
magnetdl.save_path = dl_path magnetdl.save_path = state.download_path
download = state.dl_session.add_torrent(magnetdl) download = state.dl_session.add_torrent(magnetdl)
else: else:
download = None download = None
+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 update_download_completed_by_hash
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from plyer import notification from plyer import notification
import libtorrent as lt import libtorrent as lt
import os
import time import time
import os
def cleanup_session(): def cleanup_session():
+4 -6
View File
@@ -1,12 +1,10 @@
from core.utils.logging.logs import consoleLog
from core.network.libtorrent_int import add_download from core.network.libtorrent_int import add_download
from core.utils.logging.logs import consoleLog
def add_magnet(uri, dl_path=None):
def add_magnet(uri):
if uri is not None and uri.startswith("magnet:?"): if uri is not None and uri.startswith("magnet:?"):
if dl_path: add_download(uri)
add_download(uri, dl_path)
else:
add_download(uri)
consoleLog("Magnet URI added to LibTorrent") consoleLog("Magnet URI added to LibTorrent")
else: else:
consoleLog(f"Invalid Magnet Link: {uri}") consoleLog(f"Invalid Magnet Link: {uri}")
+11 -10
View File
@@ -1,7 +1,8 @@
import os
import platform
import configparser
from core.utils.data.state import state from core.utils.data.state import state
import configparser
import platform
import os
def create_config(): def create_config():
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -15,7 +16,7 @@ def create_config():
config["Network"] = { config["Network"] = {
"api_url": f"{state.api_url}", "api_url": f"{state.api_url}",
"download_path": f"{state.download_path}", "bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
"download_speed_limit": f"{state.down_speed_limit}", "download_speed_limit": f"{state.down_speed_limit}",
"upload_speed_limit": f"{state.up_speed_limit}", "upload_speed_limit": f"{state.up_speed_limit}",
"max_connections": f"{state.max_connections}", "max_connections": f"{state.max_connections}",
@@ -23,7 +24,7 @@ def create_config():
} }
config["Paths"] = { config["Paths"] = {
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None", "download_path": f"{state.download_path}",
"image_path": f"{state.image_path}" "image_path": f"{state.image_path}"
} }
@@ -64,16 +65,16 @@ def read_config():
# Network # Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url) state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.download_path = config.get("Network", "download_path", fallback=state.download_path) state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit) state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit) state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections) state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads) state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths # Paths
state.bound_interface = config.get("Paths", "bound_interface", fallback=state.bound_interface) state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
if state.bound_interface == "None":
state.bound_interface = None
state.image_path = config.get("Paths", "image_path", fallback=state.image_path) state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
create_config() create_config()
+1 -2
View File
@@ -1,6 +1,6 @@
from core.network.libtorrent_int import update_settings 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.logging.logs import consoleLog
from core.utils.data.state import state
from .config import create_config from .config import create_config
@@ -23,7 +23,6 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
state.max_downloads = max_downloads state.max_downloads = max_downloads
if bound_interface is not None: if bound_interface is not None:
state.bound_interface = None if bound_interface == "None" else bound_interface state.bound_interface = None if bound_interface == "None" else bound_interface
update_settings() update_settings()
consoleLog("Saved Settings") consoleLog("Saved Settings")
+2 -3
View File
@@ -1,8 +1,8 @@
import threading
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget from PySide6.QtWidgets import QTableWidget
from typing import Any, List, Dict from typing import Any, List, Dict
from pathlib import Path from pathlib import Path
import threading
class AppState(QObject): class AppState(QObject):
image_changed = Signal(str) image_changed = Signal(str)
@@ -18,7 +18,7 @@ class AppState(QObject):
self.currenttracker: str = "rutracker" self.currenttracker: str = "rutracker"
self.trackertable: QTableWidget self.trackertable: QTableWidget
self.trackers: Dict[str,Dict[str,Any]] = {}# each tracker should add itself here self.trackers: Dict[str,Dict[str,Any]] = {} # each tracker should add itself here
''' '''
an example: an example:
"rutracker" : { "rutracker" : {
@@ -41,7 +41,6 @@ class AppState(QObject):
self.interfaces: List = [] self.interfaces: List = []
self.active_interfaces: List = [] self.active_interfaces: List = []
self.bound_interface: Any = None self.bound_interface: Any = None
self.log_buffer: List[str] = [] self.log_buffer: List[str] = []
self.downloads_lock = threading.RLock() self.downloads_lock = threading.RLock()
self.main_window: Any = None self.main_window: Any = None
+2 -2
View File
@@ -1,6 +1,6 @@
import requests
from bs4 import BeautifulSoup
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
import requests
def get_magnet_link(post_url): def get_magnet_link(post_url):
+3 -5
View File
@@ -1,6 +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.network.download import run_download_direct, seed_magnet
from core.utils.logging.logs import consoleLog, remove_download_log
import os import os
def split_data(data): def split_data(data):
@@ -15,13 +14,12 @@ def check_completed(downloads, resume):
if download.completed == False: if download.completed == False:
consoleLog(f"Found unfinished download: {download.title}") consoleLog(f"Found unfinished download: {download.title}")
if resume == True: if resume == True:
dl_dir = os.path.dirname(download.path)
if download.magnet_uri: 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}") consoleLog(f"Resuming Magnet: {download.title}")
elif download.url: elif download.url:
from core.network.direct_download import add_direct_download 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}") consoleLog(f"Resuming Direct Download: {download.title}")
def check_downloads(downloads): def check_downloads(downloads):
+2 -4
View File
@@ -3,10 +3,10 @@ from core.utils.data.state import state
from dataclasses import asdict from dataclasses import asdict
from datetime import datetime from datetime import datetime
import json import json
import time
import os import os
import re import re
import time
import threading
def add_download_log(title, url, magnet_uri, completed) -> DownloadList: def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
# wait for metadata outside the lock to avoid blocking other threads # wait for metadata outside the lock to avoid blocking other threads
@@ -135,9 +135,7 @@ def _update_download_completed_inner(magnet_uri, completed) -> DownloadList:
try: try:
stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip() stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip()
stored_url = (getattr(download, 'url', None) or "").strip() stored_url = (getattr(download, 'url', None) or "").strip()
consoleLog(f"Comparing with magnet: {stored_magnet[:50] if stored_magnet else 'None'}...")
if identifier and (stored_magnet == identifier or stored_url == identifier): if identifier and (stored_magnet == identifier or stored_url == identifier):
consoleLog(f"Match found! Setting completed={completed}")
download.completed = completed download.completed = completed
found = True found = True
except Exception as e: except Exception as e:
+10 -10
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.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 import threading
@@ -41,9 +41,9 @@ def run_download(post):
else: else:
add_direct_download(link, post.get("title", "Unknown")) 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}") consoleLog(f"Magnet: {title}")
add_magnet(magnet_uri, dl_path) add_magnet(magnet_uri)
add_download_log(title, "", magnet_uri, False) add_download_log(title, "", magnet_uri, False)
def seed_magnet(magnet_uri, file_path): def seed_magnet(magnet_uri, file_path):
+7 -6
View File
@@ -1,10 +1,11 @@
import requests
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
import requests
def get_updates(): def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest" url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
try: try:
response = requests.get(url, timeout=15) response = requests.get(url, timeout=15)
except requests.RequestException as e: except requests.RequestException as e:
@@ -33,9 +34,9 @@ def get_updates():
url=asset['browser_download_url'], url=asset['browser_download_url'],
hash=asset.get('digest') hash=asset.get('digest')
)) ))
return release_assets return release_assets, latest_version
else: else:
return None return None, None
else: else:
consoleLog("Already up-to-date.") consoleLog("Already up-to-date.")
return None return None, None
+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)
+7 -7
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.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.logging.logs import get_download_logs
from core.utils.general.shutdown import closehelper from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread 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.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 import QtWidgets
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
import qdarktheme import qdarktheme
import threading import threading
import platform import platform
import signal import signal
import sys
import time import time
import sys
def run_gui(): def run_gui():
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
@@ -63,4 +63,4 @@ def main():
run_gui() run_gui()
if __name__ == "__main__": if __name__ == "__main__":
main() main()