refactor: remove core/ folder

This commit is contained in:
Vxrtrauter
2026-03-19 20:42:28 +01:00
parent 53d4a240a0
commit 325eda54fa
52 changed files with 142 additions and 142 deletions
View File
+80
View File
@@ -0,0 +1,80 @@
from utils.data.state import state
import configparser
import platform
import os
def create_config():
config = configparser.ConfigParser()
config["General"] = {
"debug": True,
"ignore_updates": f"{state.ignore_updates}",
"autoresume": f"{state.autoresume}",
"window_transparency": f"{state.window_transparency}"
}
config["Network"] = {
"api_url": f"{state.api_url}",
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
"download_speed_limit": f"{state.down_speed_limit}",
"upload_speed_limit": f"{state.up_speed_limit}",
"max_connections": f"{state.max_connections}",
"max_downloads": f"{state.max_downloads}"
}
config["Paths"] = {
"download_path": f"{state.download_path}",
"image_path": f"{state.image_path}"
}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(state.settings_path, "config.yml")
if not os.path.exists(config_file):
create_config()
return
config.read(config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
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.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_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
create_config()
+31
View File
@@ -0,0 +1,31 @@
from network.libtorrent_int import update_settings
from utils.config.config import create_config
from utils.logging.logs import consoleLog
from utils.data.state import state
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, bound_interface=None):
if apiurl is not None:
state.api_url = apiurl
if download_path is not None:
state.download_path = download_path
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:
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
if bound_interface is not None:
state.bound_interface = None if bound_interface == "None" else bound_interface
update_settings()
consoleLog("Saved Settings")
create_config()
close()
+16
View File
@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import List
@dataclass
class Download:
title: str
url: str
magnet_uri: str
path: str
completed: bool
@dataclass
class DownloadList:
count: int
data: List[Download]
+67
View File
@@ -0,0 +1,67 @@
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget
from typing import Any, List, Dict
from pathlib import Path
import threading
class AppState(QObject):
image_changed = Signal(str)
def __init__(self):
super().__init__()
# General
self.version: str = "Unknown"
self.debug: bool = False
self.main_window: Any = None
self.loop_running: bool = False
self.shutdown_event = threading.Event()
self.log_buffer: List[str] = []
# Paths
self.settings_path: str = ""
self._image_path: str = ""
self.download_path: str = str(Path.home() / "Downloads")
# GUI
self.window_transparency: bool = False
self.trackertable: QTableWidget
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
# Trackers / Scraping
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
self.currenttracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz"
self.trackers: Dict[str,Dict[str,Any]] = {} # each tracker should add itself here
# an example:
# "rutracker" : {
# "name" : "rutracker",
# "headers" : ["author", "title"],
# "scrapeFunc" : function,
# }
# LibTorrent / Download related stuff
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.seeded_magnets: set = set()
self.ignore_updates: bool = False
self.autoresume: bool = True
self.up_speed_limit: int = 0
self.down_speed_limit: int = 0
self.max_connections: int = 200
self.max_downloads: int = 10
self.downloads_lock = threading.RLock()
@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()
+22
View File
@@ -0,0 +1,22 @@
from utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
import requests
def get_magnet_link(post_url):
try:
response = requests.get(post_url, timeout=15) # eventually impl. cloudscraper
consoleLog("Sent Request to retrieve Magnet Link...")
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
if magnet_link:
consoleLog(f"Magnet Link Retrieved: {magnet_link['href']}")
return magnet_link['href']
else:
consoleLog("Magnet Link not Found!")
return None
except requests.RequestException as e:
consoleLog(f"Failed to fetch {post_url}: {e}")
return None
+14
View File
@@ -0,0 +1,14 @@
from network.libtorrent_misc import cleanup_session
from utils.logging.logs import consoleLog
from utils.data.state import state
import os
def closehelper():
state.shutdown_event.set()
try:
cleanup_session()
except Exception as e:
consoleLog(f"Exception while cleaning up LibTorrent Session: {e}")
def force_exit():
os._exit(0)
+9
View File
@@ -0,0 +1,9 @@
from utils.logging.logs import consoleLog
def run_thread(thread):
target_name = thread._target.__name__
try:
thread.start()
consoleLog(f"Started Thread: {target_name}")
except Exception as e:
consoleLog(f"Exception while starting thread {target_name}: {e}")
+36
View File
@@ -0,0 +1,36 @@
from utils.network.download import run_download_direct, seed_magnet
from utils.logging.logs import consoleLog, remove_download_log
import os
def split_data(data):
count = data.count
downloads = data.data
return count, downloads
def check_completed(downloads, resume):
for download in downloads:
if download.completed == False:
consoleLog(f"Found unfinished download: {download.title}")
if resume == True:
if download.magnet_uri:
run_download_direct(download.magnet_uri, download.title)
consoleLog(f"Resuming Magnet: {download.title}")
elif download.url:
from network.direct_download import add_direct_download
add_direct_download(download.url, download.title)
consoleLog(f"Resuming Direct Download: {download.title}")
def check_downloads(downloads):
for download in downloads:
if download.completed == True and os.path.exists(download.path):
consoleLog(f"Existing Download: {download.title}")
try:
seed_magnet(download.magnet_uri, download.path)
except Exception as e:
consoleLog(f"Failed to seed {download.title}: {e}")
elif download.completed == True and not os.path.exists(download.path):
consoleLog(f"Inexistent Download: {download.title}")
remove_download_log(download.magnet_uri)
+266
View File
@@ -0,0 +1,266 @@
from utils.data.models import Download, DownloadList
from utils.data.state import state
from dataclasses import asdict
from datetime import datetime
import json
import time
import os
import re
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
# wait for metadata outside the lock to avoid blocking other threads
magnetdl = state.active_downloads.get(magnet_uri)
if magnetdl:
status = magnetdl.status()
timeout = 60
start = time.time()
while not status.has_metadata and (time.time() - start) < timeout:
time.sleep(0.5)
status = magnetdl.status()
torrent_name = status.name
save_path = status.save_path
consoleLog(f"Torrent name (has_metadata={status.has_metadata}): {torrent_name}")
path = os.path.join(save_path, torrent_name)
else:
path = os.path.join(state.download_path, title)
with state.downloads_lock:
return _add_download_log_inner(title, url, magnet_uri, completed, path)
def _add_download_log_inner(title, url, magnet_uri, completed, path) -> 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((magnet_uri and d.magnet_uri == magnet_uri) or (url and d.url == url) for d in downloads):
if magnet_uri and magnet_uri in state.active_downloads:
consoleLog("Skipping Logging, download already running...")
return DownloadList(data=downloads, count=len(downloads))
consoleLog("File already in Log, updating Download State...")
hash = extract_hash_from_magnet(magnet_uri)
if hash:
update_download_completed_by_hash(hash, False)
return DownloadList(data=downloads, count=len(downloads))
downloads.append(Download(
title=title,
url=url,
magnet_uri=magnet_uri,
path=path,
completed=completed
))
consoleLog(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 remove_download_log(magnet_uri) -> DownloadList:
with state.downloads_lock:
return _remove_download_log_inner(magnet_uri)
def _remove_download_log_inner(magnet_uri) -> 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 = []
magnet_link = (magnet_uri or "").strip()
if not magnet_link:
return DownloadList(data=downloads, count=len(downloads))
title = next((getattr(d, 'title', 'Unknown') for d in downloads if (getattr(d, 'magnet_uri', None) or '').strip() == magnet_link or (getattr(d, 'url', None) or '').strip() == magnet_link), 'Unknown')
downloads = [d for d in downloads if (getattr(d, 'magnet_uri', None) or "").strip() != magnet_link and (getattr(d, 'url', None) or "").strip() != magnet_link]
consoleLog(f"Removed {title} from 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:
with state.downloads_lock:
return _update_download_completed_inner(magnet_uri, completed)
def _update_download_completed_inner(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:
consoleLog(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
identifier = (magnet_uri or "").strip()
if state.debug:
consoleLog(f"Updating download completed for identifier: {identifier!r}")
consoleLog(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]
consoleLog(f" [{i}] magnet: {mag}... url: {url}...")
except Exception as e:
consoleLog(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 identifier and (stored_magnet == identifier or stored_url == identifier):
download.completed = completed
found = True
except Exception as e:
consoleLog(f"Error comparing download: {e}")
if not found:
consoleLog("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)
consoleLog("Updated download log")
return download_list
def get_download_logs() -> DownloadList:
with state.downloads_lock:
return _get_download_logs_inner()
def _get_download_logs_inner() -> 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):
if not magnet_uri:
return None
try:
import libtorrent as lt
params = lt.parse_magnet_uri(magnet_uri)
if hasattr(params, 'info_hashes'): # lt 2.0+
return str(params.info_hashes.v1).upper()
return str(params.info_hash).upper()
except Exception:
match = re.search(r'urn:btih:([a-zA-Z0-9]+)', magnet_uri)
if match:
return match.group(1).upper()
return None
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
with state.downloads_lock:
return _update_download_completed_by_hash_inner(info_hash, completed)
def _update_download_completed_by_hash_inner(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:
consoleLog(f"Error loading downloads.json: {e}")
downloads = []
else:
downloads = []
info_hash_upper = (info_hash or "").upper().strip()
consoleLog(f"Updating download by hash: {info_hash_upper}")
consoleLog(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:
consoleLog(f"Error comparing download: {e}")
if not found:
consoleLog("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)
consoleLog("Updated download log")
return download_list
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 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:
state.log_buffer.append(formatted_text)
if state.debug or printAnyways:
print(formatted_text)
+54
View File
@@ -0,0 +1,54 @@
from network.direct_download import add_direct_download
from network.libtorrent_wrapper import add_magnet
from PySide6.QtWidgets import QTableWidgetItem
from utils.logging.logs import add_download_log
from utils.general.wrappers import run_thread
from network.libtorrent_int import add_seed
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
from typing import Optional
import threading
def download_selected(items: list[QTableWidgetItem]):
if not items:
consoleLog("No item selected for download.")
return
seen = set()
for item in items:
if item.column() != 0:
continue
post_idx = item.data(Qt.ItemDataRole.UserRole)
if post_idx is None:
post_idx = item.row()
if post_idx not in seen:
seen.add(post_idx)
post = state.posts[post_idx]
consoleLog(f"Downloading {post.get('title', 'Unknown')}")
run_thread(threading.Thread(target=run_download, args=(post,)))
def run_download(post, headers: Optional[dict] = None):
linkfunc = state.trackers[state.currenttracker]["linkFunc"]
ismagnet = state.trackers[state.currenttracker]["isMagnet"]
result = linkfunc(post)
if ismagnet:
link = result
add_magnet(link)
add_download_log(post.get("title", "Unknown"), "", link, False)
else:
link, link_headers = result if isinstance(result, tuple) else (result, None)
final_headers = headers or link_headers
add_direct_download(link, post.get("title", "Unknown"), headers=final_headers, single_threaded=final_headers is not None)
def run_download_direct(magnet_uri, title="Direct Download"):
consoleLog(f"Magnet: {title}")
add_magnet(magnet_uri)
add_download_log(title, "", magnet_uri, False)
def seed_magnet(magnet_uri, file_path):
consoleLog(f"Seeding: {magnet_uri[:60]}")
add_seed(magnet_uri, file_path)
+35
View File
@@ -0,0 +1,35 @@
import json
def split_data(data):
p_json = json.loads(data)
count = p_json["count"]
posts = p_json["data"]
query = p_json["query"]
success = p_json["success"]
cached = p_json["cached"]
return count, posts, query, success, cached
def format_data(data):
post_author = [post["author"] for post in data]
post_titles = [post["title"] for post in data]
post_links = [post["url"] for post in data]
post_seeders = [post["seeders"] for post in data]
post_leechers = [post["leechers"] for post in data]
return post_titles, post_links, post_author, post_seeders, post_leechers
def format_data_minimal(data):
post_author = [post["author"] for post in data]
post_titles = [post["title"] for post in data]
post_links = [post["url"] for post in data]
return post_author, post_titles, post_links
# https://open.spotify.com/track/0F0fMcIdL2FmcC9eYqwazX
+42
View File
@@ -0,0 +1,42 @@
from utils.logging.logs import consoleLog
from utils.data.state import state
import requests
def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
try:
response = requests.get(url, timeout=15)
except requests.RequestException as e:
consoleLog(f"Failed to fetch releases: {e}")
return None, None
if response.status_code != 200:
consoleLog(f"Failed to fetch releases: {response.status_code}")
return None, None
release = response.json()
latest_version = release.get("name") or release.get("tag_name")
assets = release.get("assets", [])
release_assets = []
if latest_version != state.version:
consoleLog(f"New release available: {latest_version}")
if assets:
consoleLog("Assets:")
for asset in assets:
consoleLog(f"{asset['name']}")
release_assets.append(dict(
name=asset['name'],
url=asset['browser_download_url'],
hash=asset.get('digest')
))
return release_assets, latest_version
else:
return None, None
else:
consoleLog("Already up-to-date.")
return None, None
+96
View File
@@ -0,0 +1,96 @@
from network.direct_download.handle import DirectDownloadHandle
from utils.logging.logs import consoleLog
from 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)