Compare commits

...

10 Commits

Author SHA1 Message Date
KeksNino 3b51552313 check for free disk space at startup
resolve merge
2026-03-21 02:08:23 +01:00
shayaa cac2ad3ab1 Revise disclaimer in README.md
Updated disclaimer for legal and ethical use of SoftwareManager.
2026-03-21 00:58:17 +01:00
shayaa cdbf9165d2 Update image path for logo in test workflow 2026-03-21 00:55:34 +01:00
shayaa a56969f19d Add GitHub Actions workflow for building testing executables 2026-03-17 23:43:47 +01:00
shayaa 5bb732072e Merge pull request #48 from KeksPirates/fix/link-updater-to-gui
link updater to gui again
2026-03-16 03:04:35 +01:00
Vxrtrauter f50bfd0aa0 link updater to gui again 2026-03-16 03:04:14 +01:00
shayaa 71adbfcd96 Merge pull request #47 from KeksPirates/fix/invalid-version-detection
Fix invalid version detection
2026-03-16 02:45:46 +01:00
Vxrtrauter 270ee027f7 feat: add semi-dynamic discovering of build info file 2026-03-16 02:31:03 +01:00
Vxrtrauter a443f64501 refactor: move build_info.json to root folder, update workflow to support changes, rename function to more accurate name 2026-03-16 02:02:03 +01:00
Vxrtrauter 0e05f42741 Fix invalid version detection, refactor main.py and add comments for improved code understanding 2026-03-16 01:35:52 +01:00
11 changed files with 271 additions and 32 deletions
+1 -7
View File
@@ -125,8 +125,6 @@ jobs:
--upx-binary="${{ env.UPX_BINARY }}" \ --upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \ --noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \ --include-data-files=build_info.json=build_info.json \
--include-data-files=build_info.json=src/build_info.json \
--include-data-files=build_info.json=src/core/interface/build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \ --output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \ --output-dir=nuitka-build \
src/main.py src/main.py
@@ -137,8 +135,6 @@ jobs:
run: | run: |
pyinstaller --onefile --windowed \ pyinstaller --onefile --windowed \
--add-data "build_info.json:." \ --add-data "build_info.json:." \
--add-data "build_info.json:src/core" \
--add-data "build_info.json:src/core/interface" \
--exclude-module PyQt6 \ --exclude-module PyQt6 \
--exclude-module PyQt5 \ --exclude-module PyQt5 \
--hidden-import=backports.tarfile \ --hidden-import=backports.tarfile \
@@ -155,8 +151,6 @@ jobs:
run: | run: |
pyinstaller --onefile \ pyinstaller --onefile \
--add-data "build_info.json:." \ --add-data "build_info.json:." \
--add-data "build_info.json:src/core" \
--add-data "build_info.json:src/core/interface" \
--exclude-module PyQt6 \ --exclude-module PyQt6 \
--exclude-module PyQt5 \ --exclude-module PyQt5 \
--hidden-import=backports.tarfile \ --hidden-import=backports.tarfile \
@@ -287,4 +281,4 @@ jobs:
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt release/SHA256SUMS.txt
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+185
View File
@@ -0,0 +1,185 @@
name: Build Executables (Test)
on:
workflow_dispatch:
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
fail-fast: false # keep other platforms building even if one fails
matrix:
include:
- os: ubuntu-22.04
artifact_name: SoftwareManager-test-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-windows
- os: macos-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.7"
enable-cache: true
- name: Install Python dependencies
run: |
uv pip install -r requirements.txt
uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env:
UV_SYSTEM_PYTHON: 1
- name: Generate version
id: version
shell: bash
run: |
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
BRANCH="${{ github.head_ref || github.ref_name }}"
BRANCH_SLUG="${BRANCH//\//-}" # replace slashes with dashes
VERSION="${BRANCH_SLUG}-${SHORT_SHA}-test"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject build info
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Install UPX (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
7z x upx.zip -oupx-dir
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
upx --version | head -1
- name: Convert icon for Windows
if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
- name: Build with Nuitka (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
python -m nuitka \
--standalone \
--assume-yes-for-downloads \
--windows-console-mode=disable \
--windows-icon-from-ico=app_icon.ico \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Stage artifact
shell: bash
run: |
mkdir -p dist-final
if [ "$RUNNER_OS" == "Windows" ]; then
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
ls -la dist-final/
- name: Create ZIP (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip"
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}.zip
retention-days: 7
- name: Upload artifact (Linux/macOS)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}
retention-days: 7
-1
View File
@@ -62,7 +62,6 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
</picture> </picture>
</a> </a>
---
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool. **Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
+20 -3
View File
@@ -1,18 +1,35 @@
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.utils.network.updater import download_update
from PySide6.QtWidgets import QDialog, QMessageBox from PySide6.QtWidgets import QDialog, QMessageBox
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state from core.utils.data.state import state
from pathlib import Path
import platform import platform
import json import json
import os import os
def check_version(): def _get_build_info_path() -> (Path | None):
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json") current_dir = Path(__file__).resolve().parent
for _ in range(6): # Climb max 6 directories
file = current_dir / "build_info.json"
if file.exists():
return file
# Go up one directory if file isn't found
current_dir = current_dir.parent
return None
def get_version() -> None:
# Get build info filepath
build_info_path = _get_build_info_path()
if os.path.exists(build_info_path): if os.path.exists(build_info_path):
with open(build_info_path, "r") as f: with open(build_info_path, "r") as f:
build_info = json.load(f) build_info = json.load(f)
state.version = build_info.get("version") state.version = build_info.get("version")
else:
consoleLog(f"Could not find build info file (Path: {build_info_path})")
class UpdateDialog(QDialog): class UpdateDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
+19 -8
View File
@@ -5,6 +5,7 @@ from core.interface.dialogs.downloadlist import _create_download_list
from core.interface.assets.base64_icons import settings_white_base64 from core.interface.assets.base64_icons import settings_white_base64
from core.interface.assets.base64_icons import settings_black_base64 from core.interface.assets.base64_icons import settings_black_base64
from core.interface.dialogs.downloadlist import download_list_update from core.interface.dialogs.downloadlist import download_list_update
from core.interface.dialogs.update import get_version, UpdateDialog
from core.utils.logging.logs import consoleLog, flush_log_buffer from core.utils.logging.logs import consoleLog, flush_log_buffer
from core.interface.dialogs.downloadmodel import DownloadModel from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.utils.searchhelper import return_pressed from core.interface.utils.searchhelper import return_pressed
@@ -18,9 +19,7 @@ from core.utils.general.wrappers import run_thread
from core.interface.dialogs.image import Image from core.interface.dialogs.image import Image
from core.utils.data.state import state from core.utils.data.state import state
import core.interface.dialogs.hoverrowdelegate
import core.interface.dialogs.contextmenu import core.interface.dialogs.contextmenu
import core.interface.dialogs.update
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import ( from PySide6.QtCore import (
@@ -77,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
pixmap.loadFromData(image_data) pixmap.loadFromData(image_data)
self.setWindowIcon(QIcon(pixmap)) self.setWindowIcon(QIcon(pixmap))
# set appid on windows # Set AppID on Windows
if platform.system() == "Windows": if platform.system() == "Windows":
try: try:
import ctypes import ctypes
@@ -86,23 +85,30 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
except Exception as e: except Exception as e:
consoleLog(f"Could not set app ID: {e}") consoleLog(f"Could not set app ID: {e}")
# Get current version
# core.interface.dialogs.update # core.interface.dialogs.update
get_version()
UpdateDialog()
# Initialize Window
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
self.controls = QWidget() self.controls = QWidget()
self.controlsLayout = QVBoxLayout() self.controlsLayout = QVBoxLayout()
# Searchbar
self.searchbar = QLineEdit() self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...") self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True) self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30) self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(self._start_search) self.searchbar.returnPressed.connect(self._start_search)
# Download Button
self.dlbutton = QtWidgets.QPushButton("Download") self.dlbutton = QtWidgets.QPushButton("Download")
self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor) self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
# Dialog for empty results
self.emptyResults = QLabel("No Results") self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter) self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.emptyResults.hide() self.emptyResults.hide()
@@ -157,23 +163,32 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(state.trackertable) self.horizontal_layout.addWidget(state.trackertable)
# Tabs
self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout) self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
# Corner Widget (Settings Button, Tracker list, Tab button container)
self.corner_widget = QWidget() self.corner_widget = QWidget()
self.corner_layout = QHBoxLayout(self.corner_widget) self.corner_layout = QHBoxLayout(self.corner_widget)
self.corner_layout.setContentsMargins(0, 0, 0, 0) self.corner_layout.setContentsMargins(0, 0, 0, 0)
# Tracker list
self.tracker_list = QComboBox() self.tracker_list = QComboBox()
self.tracker_list.addItems(list(state.trackers.keys())) self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker) self.tracker_list.activated.connect(self.set_tracker)
self.corner_layout.addWidget(self.tracker_list) self.corner_layout.addWidget(self.tracker_list)
# Settings button
self.settings_btn = QtWidgets.QToolButton() self.settings_btn = QtWidgets.QToolButton()
self.settings_btn.setIconSize(QSize(21, 21)) self.settings_btn.setIconSize(QSize(21, 21))
self.settings_btn.setStyleSheet("background: transparent;") self.settings_btn.setStyleSheet("background: transparent;")
self.settings_btn.setToolTip("Settings")
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
self.corner_layout.addWidget(self.settings_btn)
# Adjust theme based on system preferences
if darkdetect.isDark(): if darkdetect.isDark():
settings_white_pixmap = QPixmap() settings_white_pixmap = QPixmap()
settings_white = base64.b64decode(settings_white_base64) settings_white = base64.b64decode(settings_white_base64)
@@ -185,11 +200,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
settings_black_pixmap.loadFromData(settings_black) settings_black_pixmap.loadFromData(settings_black)
self.settings_btn.setIcon(QIcon(settings_black_pixmap)) self.settings_btn.setIcon(QIcon(settings_black_pixmap))
self.settings_btn.setToolTip("Settings")
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
self.corner_layout.addWidget(self.settings_btn)
self.tab_wrapper = QWidget() self.tab_wrapper = QWidget()
self.tab_layout = QVBoxLayout() self.tab_layout = QVBoxLayout()
self.tab_layout.setContentsMargins(0, 0, 0, 0) self.tab_layout.setContentsMargins(0, 0, 0, 0)
@@ -221,6 +231,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.version = QLabel(f"Version: {state.version}") self.version = QLabel(f"Version: {state.version}")
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft) self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
# (\u2193) Arrow down, (\u2191) Arrow up
self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s") self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s")
self.speed_label.setStyleSheet("padding-right: 6px;") self.speed_label.setStyleSheet("padding-right: 6px;")
self.statusbar.addPermanentWidget(self.speed_label) self.statusbar.addPermanentWidget(self.speed_label)
+2
View File
@@ -29,6 +29,7 @@ def get_active_interfaces():
def list_interfaces() -> None: def list_interfaces() -> None:
consoleLog("Fetching Network Interfaces...")
addrs = psutil.net_if_addrs() addrs = psutil.net_if_addrs()
stats = psutil.net_if_stats() stats = psutil.net_if_stats()
@@ -45,6 +46,7 @@ def list_interfaces() -> None:
consoleLog(f"Found Interface: {interface} [{status}]") consoleLog(f"Found Interface: {interface} [{status}]")
def init_interfaces(): def init_interfaces():
consoleLog("Initializing Interface variables...")
addrs = psutil.net_if_addrs() addrs = psutil.net_if_addrs()
state.interfaces = list(addrs.keys()) state.interfaces = list(addrs.keys())
state.active_interfaces = get_active_interfaces() state.active_interfaces = get_active_interfaces()
+19
View File
@@ -21,6 +21,25 @@ def get_free_space_mb(dirname):
st = os.statvfs(dirname) st = os.statvfs(dirname)
return st.f_bavail * st.f_frsize return st.f_bavail * st.f_frsize
def check_space():
while not state.shutdown_event.is_set():
for magnet_uri, magnetdl in list(state.active_downloads.items()):
try:
status = magnetdl.status()
except RuntimeError:
continue
if status.state == lt.torrent_status.downloading:
free_space = get_free_space_mb(state.download_path)
total_size = status.total_wanted
if free_space < total_size:
consoleLog(f"Not enough free space to continue downloading: {status.name}")
magnetdl.pause()
break
time.sleep(5)
def init_session(): def init_session():
if state.dl_session is not None: if state.dl_session is not None:
return return
+1 -1
View File
@@ -1,3 +1,4 @@
from core.network.libtorrent_misc import cleanup_session
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
import os import os
@@ -5,7 +6,6 @@ import os
def closehelper(): def closehelper():
state.shutdown_event.set() state.shutdown_event.set()
try: try:
from core.network.libtorrent_misc import cleanup_session
cleanup_session() cleanup_session()
except Exception as e: except Exception as e:
consoleLog(f"Exception while cleaning up LibTorrent Session: {e}") consoleLog(f"Exception while cleaning up LibTorrent Session: {e}")
+8 -1
View File
@@ -1,2 +1,9 @@
from core.utils.logging.logs import consoleLog
def run_thread(thread): def run_thread(thread):
thread.start() 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}")
+16 -11
View File
@@ -2,8 +2,11 @@ from core.network.libtorrent_misc import send_notification, update_log, check_de
from core.utils.logging.loghandler import split_data, check_completed, check_downloads from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces 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.logging.logs import set_main_window
from core.network.libtorrent_int import check_space
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.general.shutdown import force_exit
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.utils.logging.logs import consoleLog
from core.interface.gui import MainWindow from core.interface.gui import MainWindow
@@ -22,45 +25,47 @@ def run_gui():
qdarktheme.setup_theme("auto") qdarktheme.setup_theme("auto")
widget = MainWindow() widget = MainWindow()
# Check OS for window transparency compatibility and apply
if state.window_transparency and platform.system() != "Windows": if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"}) qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
from core.utils.logging.logs import set_main_window
set_main_window(widget) set_main_window(widget)
widget.show() widget.show()
sys.exit(app.exec()) sys.exit(app.exec())
def keyboardinterrupthandler(signum, frame): def keyboardinterrupthandler(signum, frame):
closehelper() closehelper()
from core.utils.general.shutdown import force_exit
force_exit() force_exit()
def main(): def main():
# Begin counting startup time
start_time = time.perf_counter() start_time = time.perf_counter()
# Parse saved files
read_config() read_config()
logs = get_download_logs() logs = get_download_logs()
_, downloads = split_data(logs) _, downloads = split_data(logs)
# Initialize Keyboardinterrupt signal
signal.signal(signal.SIGINT, keyboardinterrupthandler) signal.signal(signal.SIGINT, keyboardinterrupthandler)
consoleLog("Starting SoftwareManager...") consoleLog("Starting SoftwareManager...")
consoleLog("Fetching Network Interfaces...") # Get network interface info
list_interfaces() list_interfaces()
consoleLog("Initializing Interface variables...")
init_interfaces() init_interfaces()
consoleLog(f"Current Bound: {state.bound_interface}") 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=send_notification, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: send_notification")
run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True)) run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: update_log")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
consoleLog("Started Thread: check_completed")
run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True)) run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: check_deleted_files") # 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,))) run_thread(threading.Thread(target=check_downloads, args=(downloads,)))
consoleLog("Started Thread: check_downloads") run_thread(threading.Thread(target=check_space, args=()))
# Finish counting startup time
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI") consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
# Launch GUI
run_gui() run_gui()
if __name__ == "__main__": if __name__ == "__main__":
main() main()