Compare commits

...

23 Commits

Author SHA1 Message Date
Vxrtrauter e0eaf250e5 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-13 21:32:07 +01:00
Vxrtrauter 1c4c010e4e Add download completion tracking and update logging functionality 2026-01-13 21:31:33 +01:00
shayaa c1509f9172 fix crash because of me being retard 2026-01-13 20:36:15 +01:00
Vxrtrauter 97eabf6ec6 Add basic download logging + fix debug 2026-01-09 23:23:16 +01:00
Vxrtrauter 5eca1a678e Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-07 23:07:14 +01:00
Vxrtrauter 7e59fdc2d3 add (almost) final download log logic 2026-01-07 23:07:08 +01:00
Vxrtrauter ad6fa428a0 add basic download log json structure 2026-01-07 22:41:11 +01:00
Vxrtrauter 56e8fada4d add settings_path to AppState and add models for download logging 2026-01-07 22:23:29 +01:00
shayaa e950d2f90d Refactor dev.yml to use bash for build steps
Updated the workflow to use bash for injecting commit hash and building executables on Windows and Unix. (done with claude)
2025-12-20 04:29:22 +01:00
Vxrtrauter 1a59c45ba4 fix qtablewidget section width 2025-12-16 11:25:40 +01:00
Vxrtrauter 293c1c3ad1 fix settings icon again? probably os issue 2025-12-16 11:00:11 +01:00
KeksNino f30cc5da5e fix typo 2025-11-15 00:20:41 +01:00
KeksNino 004c7ad416 add build_info.json
to fix updater when running from source
2025-11-15 00:19:50 +01:00
Vxrtrauter 65f7c04936 refactor: clean up unused imports and improve code organization 2025-11-15 00:16:47 +01:00
KeksNino 9d0d5332bf fixed updater 2025-10-31 13:45:30 +01:00
KeksNino 6d9991188a fixed config parser error handling 2025-10-31 03:22:47 +01:00
KeksNino f397bc18ce added option to display image in bottom right 2025-10-31 03:18:59 +01:00
KeksNino 59e45efbb6 fix aria2c dependency in pyinstaller 2025-10-25 21:59:29 +02:00
KeksNino 27f0a3f71d fix wget path on windows in workflows 2025-10-25 21:57:31 +02:00
KeksNino 1d500305f7 add aria2c dependency to exe binary on windows 2025-10-25 21:52:39 +02:00
Vxrtrauter 20a310264f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-24 10:51:51 +02:00
Vxrtrauter aee8f16353 rm unused import 2025-10-24 10:50:52 +02:00
KeksNino 2354adb349 update updater download url 2025-10-20 19:10:49 +02:00
14 changed files with 402 additions and 88 deletions
+29 -9
View File
@@ -81,22 +81,42 @@ jobs:
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION" echo "Generated version: $VERSION"
- name: Inject commit hash into code (Windows) - name: Inject commit hash into code
if: runner.os == 'Windows' shell: bash
shell: pwsh
run: | run: |
$json = '{ cat > build_info.json << EOF
"version": "' + $env:VERSION + '" {
}' "version": "${{ env.VERSION }}"
$json | Out-File -Encoding utf8 build_info.json }
EOF
- name: Download aria2c (Windows)
if: runner.os == 'Windows'
run: |
mkdir -p deps
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/
- name: Build executable with PyInstaller (Windows) - name: Build executable with PyInstaller (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: pyinstaller --onefile --windowed --add-data "build_info.json:." --name ${{ env.APP_NAME }} main.py shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json;." \
--add-data "deps/aria2c.exe;." \
--add-data "core/interface/assets;core/interface/assets" \
--name ${{ env.APP_NAME }} \
main.py
- name: Build executable with PyInstaller (Unix) - name: Build executable with PyInstaller (Unix)
if: runner.os != 'Windows' if: runner.os != 'Windows'
run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--add-data "core/interface/assets:core/interface/assets" \
--name ${{ env.APP_NAME }} \
main.py
- name: Make executable (Unix) - name: Make executable (Unix)
if: runner.os != 'Windows' if: runner.os != 'Windows'
+3
View File
@@ -0,0 +1,3 @@
{
"Version": "dev"
}
-1
View File
@@ -1,6 +1,5 @@
import requests import requests
import asyncio import asyncio
import aiohttp
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.utils.data.state import state from core.utils.data.state import state
+16 -1
View File
@@ -114,6 +114,21 @@ def settings_dialog(self):
speed_limit.setFixedHeight(30) speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container) dialog.layout().addWidget(speed_limit_container)
###############
# IMAGE PATH #
###############
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart):"))
image_path_layout.addWidget(image_path)
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path)
dialog.layout().addWidget(image_path_container)
############### ###############
# SAVE/CANCEL # # SAVE/CANCEL #
############### ###############
@@ -122,7 +137,7 @@ def settings_dialog(self):
save_btn = QPushButton("Save") save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel") 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())) save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value(), image_path.text()))
cancel_btn.clicked.connect(dialog.reject) cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn) layout.addWidget(cancel_btn)
+52 -13
View File
@@ -1,6 +1,6 @@
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6 import QtCore from PySide6 import QtCore
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QTableView, QTableView,
@@ -18,7 +18,7 @@ from PySide6.QtWidgets import (
QTableWidget, QTableWidget,
) )
from PySide6.QtGui import QIcon, QAction, QCloseEvent from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
import darkdetect import darkdetect
import threading import threading
import platform import platform
@@ -38,25 +38,30 @@ from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog from core.interface.dialogs.settings import settings_dialog
from core.network.aria2_integration import dlprogress from core.network.aria2_integration import dlprogress
print(os.path.abspath(__file__))
def download_update(latest_version): def download_update(latest_version):
old_filename = f"SoftwareManager-dev-{state.version.replace('-dev', '')}-windows.exe"
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" 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}-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
print("Downloading update...") print("Downloading update...")
if os.path.exists(old_filename):
os.remove(old_filename)
response = r.get(url, allow_redirects=True) response = r.get(url, allow_redirects=True)
with open(new_filename, "wb") as f: with open(new_filename, "wb") as f:
f.write(response.content) f.write(response.content)
if not os.path.exists(new_filename): if not os.path.exists(new_filename):
raise FileNotFoundError(f"Executable not found") raise FileNotFoundError("Executable not found")
subprocess.Popen([new_filename], shell=True) subprocess.Popen([new_filename], shell=True)
time.sleep(0.5) time.sleep(0.5)
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("New Version")
msg.setText("New version installed.")
msg.setInformativeText("Please remove the old exe.")
msg.setStandardButtons(QMessageBox.Ok)
sys.exit(0) sys.exit(0)
class MainWindow(QtWidgets.QMainWindow, QWidget): class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -71,14 +76,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if state.ignore_updates is False: if state.ignore_updates is False:
if platform.system() == "Windows": if platform.system() == "Windows":
result = check_for_updates() result = check_for_updates()
if result != (None, None):
assets, latest_version = result assets, latest_version = result
if assets: if assets:
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Information) msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("Update Available") msg.setWindowTitle("Update Available")
msg.setText(f"A new version is available.") msg.setText("A new version is available.")
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.") msg.setInformativeText("Press Ok to download the update.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore) msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
response = msg.exec_() response = msg.exec_()
@@ -105,19 +111,25 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.emptyResults.setAlignment(Qt.AlignCenter) self.emptyResults.setAlignment(Qt.AlignCenter)
self.emptyResults.hide() self.emptyResults.hide()
self.download_model = None self.download_model = None
self.downloadList = QTableView() self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.") self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload = QLabel("No items in downloads.")
self.progressbar = QProgressBar() self.progressbar = QProgressBar()
# Table Widget for Item List
self.qtablewidget = QTableWidget() self.qtablewidget = QTableWidget()
self.qtablewidget.setColumnCount(2) self.qtablewidget.setColumnCount(2)
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.qtablewidget.verticalHeader().setVisible(False) self.qtablewidget.verticalHeader().setVisible(False)
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"]) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.setStretchLastSection(False)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground) self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground) self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
@@ -190,12 +202,25 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None) self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
self.image = QImage(state.image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label = QLabel(self)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
offset_x = -1350
offset_y = -550
x = self.width() - self.overlay_label.width() - offset_x
y = self.height() - self.overlay_label.height() - offset_y
self.overlay_label.move(x, y)
# temporarily disabled
# state.image_changed.connect(self.update_image_overlay)
containerLayout.addWidget(self.tabs) containerLayout.addWidget(self.tabs)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom) containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar") toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar) self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft) toolbar.setLayoutDirection(Qt.RightToLeft)
@@ -226,6 +251,13 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update))) self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
self.download_timer.start(500) self.download_timer.start(500)
def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
def download_list_update(self): def download_list_update(self):
if self.download_model: if self.download_model:
self.download_model.layoutAboutToBeChanged.emit() self.download_model.layoutAboutToBeChanged.emit()
@@ -249,3 +281,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
else: else:
self.emptyResults.hide() self.emptyResults.hide()
self.qtablewidget.show() self.qtablewidget.show()
# thank you claude
def resizeEvent(self, event):
super().resizeEvent(event)
table_width = self.qtablewidget.viewport().width()
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
+21
View File
@@ -1,6 +1,7 @@
import aria2p import aria2p
import subprocess import subprocess
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.logs import update_download_completed_by_hash
from plyer import notification from plyer import notification
import time import time
import sys import sys
@@ -73,3 +74,23 @@ def send_notification(shutdown_event):
except Exception: except Exception:
pass pass
time.sleep(5) 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:
if state.debug:
print(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 as e:
if state.debug:
print(f"Error in update_log: {e}")
import traceback
traceback.print_exc()
time.sleep(5)
+14 -6
View File
@@ -7,17 +7,17 @@ def create_config():
config = configparser.ConfigParser() 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"speed_limit": f"{state.speed_limit}",
"ignore_updates": f"{state.ignore_updates}"} "ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}"}
if platform.system() == "Windows": if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming")) config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else: else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
settings_path = os.path.join(config_dir, "SoftwareManager") state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(settings_path, exist_ok=True) os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(settings_path, "config.yml"), 'w') as cf: with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
config.write(cf) config.write(cf)
@@ -29,17 +29,25 @@ def read_config():
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming")) config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else: else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
settings_path = os.path.join(config_dir, "SoftwareManager") state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(settings_path, "config.yml") config_file = os.path.join(state.settings_path, "config.yml")
if not os.path.exists(config_file): if not os.path.exists(config_file):
create_config() create_config()
config.read(config_file) config.read(config_file)
if state.debug is not None:
state.debug = config.getboolean("General", "debug") state.debug = config.getboolean("General", "debug")
if state.api_url is not None:
state.api_url = config.get("General", "api_url") state.api_url = config.get("General", "api_url")
if state.aria2_threads is not None:
state.aria2_threads = config.getint("General", "aria2_threads") state.aria2_threads = config.getint("General", "aria2_threads")
if state.download_path is not None:
state.download_path = config.get("General", "download_path") state.download_path = config.get("General", "download_path")
if state.speed_limit is not None:
state.speed_limit = config.getint("General", "speed_limit") state.speed_limit = config.getint("General", "speed_limit")
if state.ignore_updates is not None:
state.ignore_updates = config.getboolean("General", "ignore_updates") state.ignore_updates = config.getboolean("General", "ignore_updates")
if state.image_path is not None:
state.image_path = config.get("General", "image_path")
+3 -1
View File
@@ -13,7 +13,7 @@ def restart_aria2c():
atexit.unregister(kill_aria2server) atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server) atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None): def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None):
if thread_count is not None: if thread_count is not None:
state.aria2_threads = thread_count state.aria2_threads = thread_count
if apiurl is not None: if apiurl is not None:
@@ -22,6 +22,8 @@ def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_p
state.download_path = download_path state.download_path = download_path
if speed_limit is not None: if speed_limit is not None:
state.speed_limit = speed_limit state.speed_limit = speed_limit
if image_path is not None:
state.image_path = image_path
create_config() create_config()
restart_aria2c() restart_aria2c()
+15
View File
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from typing import List
@dataclass
class Download:
title: str
url: str
magnet_uri: str
completed: bool
@dataclass
class DownloadList:
count: int
data: List[Download]
+35 -20
View File
@@ -1,24 +1,39 @@
from dataclasses import dataclass from PySide6.QtCore import QObject, Signal
from typing import Optional, Any, List, Dict from typing import Optional, Any, List, Dict
from pathlib import Path from pathlib import Path
@dataclass class AppState(QObject):
class AppState: image_changed = Signal(str)
posts: List[Dict[str, Any]]
post_titles: List[str]
post_urls: List[str]
post_author: List[str]
downloads: List[str]
version: str
ignore_updates: bool = False
debug: bool = False
tracker: str = "rutracker"
api_url: str = "https://api.michijackson.xyz"
download_path: str = str(Path.home() / "Downloads")
speed_limit: int = 0
aria2process: Optional[Any] = None
aria2: Any = None
aria2p: Any = None
aria2_threads: int = 4
state = AppState(posts=[], post_titles=[], post_urls=[], post_author=[], downloads=[], download_path="", version="") def __init__(self):
super().__init__()
self.posts: List[Dict[str, Any]] = []
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
self.debug: bool = False
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.aria2process: Optional[Any] = None
self.aria2: Any = None
self.aria2p: Any = None
self.aria2_threads: int = 4
self.settings_path: str = None
@property
def image_path(self) -> str:
return self._image_path
@image_path.setter
def image_path(self, new_path: str):
if new_path != self._image_path:
self._image_path = new_path
self.image_changed.emit(new_path)
state = AppState()
+171
View File
@@ -0,0 +1,171 @@
from core.utils.data.models import Download, DownloadList
from core.utils.data.state import state
from dataclasses import asdict
import json
import os
import re
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
if state.debug:
print("Skipped Logging, download already in file")
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
downloads.append(Download(
title=title,
url=url,
magnet_uri=magnet_uri,
completed=completed,
))
if state.debug:
print(f"Added {title} to Log File")
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
return download_list
def update_download_completed(magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except (json.JSONDecodeError, TypeError) as e:
if state.debug:
print(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
identifier = (magnet_uri or "").strip()
if state.debug:
print(f"Updating download completed for identifier: {identifier!r}")
print(f"Total downloads in file: {len(downloads)}")
for i, d in enumerate(downloads):
try:
mag = (getattr(d, 'magnet_uri', None) or '')[:50]
url = (getattr(d, 'url', None) or '')[:50]
print(f" [{i}] magnet: {mag}... url: {url}...")
except Exception as e:
print(f" [{i}] Error reading download: {e}")
found = False
for download in downloads:
try:
stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip()
stored_url = (getattr(download, 'url', None) or "").strip()
if state.debug:
print(f"Comparing with magnet: {stored_magnet[:50] if stored_magnet else 'None'}...")
if identifier and (stored_magnet == identifier or stored_url == identifier):
if state.debug:
print(f"Match found! Setting completed={completed}")
download.completed = completed
found = True
except Exception as e:
if state.debug:
print(f"Error comparing download: {e}")
if not found:
if state.debug:
print("No matching download found to update")
return DownloadList(data=downloads, count=len(downloads))
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
if state.debug:
print("Updated download log")
return download_list
def get_download_logs() -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except json.JSONDecodeError:
downloads = []
else:
downloads = []
return DownloadList(data=downloads, count=len(downloads))
def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
match = re.search(r'urn:btih:([A-F0-9]+)', magnet_uri, re.IGNORECASE)
if match:
return match.group(1).upper()
return None
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json")
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
try:
with open(downloads_file, "r") as file:
existing_data = json.load(file)
downloads = [Download(**d) for d in existing_data.get("data", [])]
except (json.JSONDecodeError, TypeError) as e:
if state.debug:
print(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
info_hash_upper = (info_hash or "").upper().strip()
if state.debug:
print(f"Updating download by hash: {info_hash_upper}")
print(f"Total downloads in file: {len(downloads)}")
found = False
for download in downloads:
try:
magnet_uri = getattr(download, 'magnet_uri', None)
if magnet_uri:
stored_hash = extract_hash_from_magnet(magnet_uri)
if stored_hash and stored_hash == info_hash_upper:
download.completed = completed
found = True
except Exception as e:
if state.debug:
print(f"Error comparing download: {e}")
if not found:
if state.debug:
print("No matching download found to update")
return DownloadList(data=downloads, count=len(downloads))
download_list = DownloadList(data=downloads, count=len(downloads))
with open(downloads_file, "w") as file:
json.dump(asdict(download_list), file, indent=4)
if state.debug:
print("Updated download log")
return download_list
+3 -1
View File
@@ -4,6 +4,8 @@ from core.utils.data.tracker import get_magnet_link
from core.network.aria2_wrapper import start_client from core.network.aria2_wrapper import start_client
from core.network.aria2_wrapper import add_magnet from core.network.aria2_wrapper import add_magnet
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.general.logs import add_download_log
import threading import threading
@@ -12,7 +14,6 @@ def download_selected(item, posts, post_titles):
if state.debug: if state.debug:
print(f"Downloading {item.text()}") print(f"Downloading {item.text()}")
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles))) run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
else: else:
if state.debug: if state.debug:
print("No item selected for download.") print("No item selected for download.")
@@ -23,6 +24,7 @@ def run_download(item, posts, post_titles):
print("Selected URL: ", post_url) print("Selected URL: ", post_url)
magnet_uri = get_magnet_link(post_url) magnet_uri = get_magnet_link(post_url)
start_client() start_client()
add_download_log(item, post_url, magnet_uri, False)
add_magnet(magnet_uri) add_magnet(magnet_uri)
+2
View File
@@ -30,6 +30,8 @@ def check_for_updates():
for asset in assets: for asset in assets:
print(f" - {asset['name']}: {asset['browser_download_url']}") print(f" - {asset['name']}: {asset['browser_download_url']}")
return assets, latest_version return assets, latest_version
else:
return None, None
else: else:
if state.debug: if state.debug:
print("Already up-to-date.") print("Already up-to-date.")
+3 -1
View File
@@ -1,7 +1,7 @@
from core.interface.gui import MainWindow from core.interface.gui import MainWindow
from core.utils.data.state import state from core.utils.data.state import state
from core.network.aria2_integration import aria2server from core.network.aria2_integration import aria2server
from core.network.aria2_integration import send_notification from core.network.aria2_integration import send_notification, update_log
from core.utils.general.shutdown import closehelper, shutdown_event from core.utils.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.config.config import read_config from core.utils.config.config import read_config
@@ -37,12 +37,14 @@ def keyboardinterrupthandler(signum, frame):
if __name__ == "__main__": if __name__ == "__main__":
read_config() read_config()
if args.debug:
state.debug = args.debug # override of read_config state.debug = args.debug # override of read_config
if state.debug: if state.debug:
print("Starting Aria2 Server") print("Starting Aria2 Server")
state.aria2process = run_aria2server() state.aria2process = run_aria2server()
signal.signal(signal.SIGINT, keyboardinterrupthandler) 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))
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
if state.debug: if state.debug:
print("Launching GUI") print("Launching GUI")
run_gui() run_gui()