Compare commits

...

25 Commits

Author SHA1 Message Date
Vxrtrauter 3b2d125849 fix: update Python dependency installation and enhance icon conversion process for improved compatibility 2026-02-11 02:43:35 +01:00
Vxrtrauter bfb3a4eeb3 fix: refactor Nuitka and UPX installation steps for improved clarity and compatibility 2026-02-11 02:37:20 +01:00
Vxrtrauter 431f5fd684 fix: add Windows icon conversion step and update Nuitka build options for improved compatibility 2026-02-11 02:30:56 +01:00
Vxrtrauter ddc45f41c1 fix: update Nuitka build options and refine PySide6 dependency for improved compatibility 2026-02-11 02:13:04 +01:00
Vxrtrauter a58cda7e8a fix: update asset path handling in MainWindow for improved compatibility with frozen applications 2026-02-11 01:55:03 +01:00
Vxrtrauter 375925e56c fix: add --assume-yes-for-downloads option to Nuitka build commands for improved automation 2026-02-11 01:34:59 +01:00
Vxrtrauter 0b3c8819de fix: enhance UPX installation by setting UPX_BINARY environment variable for better compatibility 2026-02-11 01:30:10 +01:00
Vxrtrauter 626bb7ff62 fix: update UPX installation path handling for Windows and Linux in CI workflow 2026-02-11 01:28:19 +01:00
Vxrtrauter f10c51f894 fix: correct UPX path for Windows installation in CI workflow 2026-02-11 01:26:05 +01:00
Vxrtrauter 6a1f7fa677 fix: update Python version to 3.12 and switch from PyInstaller to Nuitka for building executables 2026-02-11 01:17:47 +01:00
Vxrtrauter b573ca8fed fix: download logging for already completed downloads & improve logging for certain parts of the program 2026-02-10 02:20:14 +01:00
shayaa 8a45d71655 Revise README for clarity and format changes
Updated README to clarify usage instructions and dependencies format.
2026-02-10 01:48:51 +01:00
shayaa 0c7bc426db Change code block syntax from sh to yml 2026-02-10 01:48:25 +01:00
shayaa 1f06a19c17 Change code block language from py to sh 2026-02-10 01:48:03 +01:00
Vxrtrauter da92c4ab0a Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-02-10 01:47:29 +01:00
Vxrtrauter f6e3ff4f27 docs: update README to clarify features and installation instructions 2026-02-10 01:47:27 +01:00
KeksNino 88042b2d3d fix: downloads randomly resuming after a few seconds when paused
credits to claude opus 4.5, i could not figure this out
2026-02-09 23:17:18 +01:00
Vxrtrauter 9265a9cd1c refactor: change init_uztracker to synchronous and update data formatting for m0nkrus 2026-02-09 04:36:28 +01:00
Vxrtrauter adc9f6f6d4 QOL: tested stuff, fixed main.py typos, removed unused variables and added startup timer because it looks cool 2026-02-08 23:54:10 +01:00
Vxrtrauter a3a6ef5eec fix: update config handling and improve tracker data retrieval 2026-02-07 04:02:14 +01:00
KeksNino 8e5e0425ef feat: add seeders and leechers column in search results 2026-02-07 03:29:52 +01:00
Vxrtrauter c2b329aed4 update: change api request format to new server 2026-02-07 03:03:14 +01:00
KeksNino c5f2be90af fix: move update checkbox to general tab 2026-02-07 01:52:11 +01:00
KeksNino 695b80c6de feat: add tabs to settings menu 2026-02-07 01:47:25 +01:00
Vxrtrauter b7188a515a feat: add flush_log_buffer function and update consoleLog to handle log entries 2026-02-07 01:42:05 +01:00
16 changed files with 299 additions and 106 deletions
+91 -26
View File
@@ -15,7 +15,7 @@ permissions:
contents: write contents: write
env: env:
PYTHON_VERSION: "3.11" PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager" APP_NAME: "SoftwareManager"
jobs: jobs:
@@ -57,7 +57,13 @@ jobs:
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
uv pip install -r requirements.txt uv pip install -r requirements.txt
uv pip install pyinstaller backports.tarfile pillow uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env: env:
UV_SYSTEM_PYTHON: 1 UV_SYSTEM_PYTHON: 1
@@ -82,28 +88,53 @@ jobs:
} }
EOF EOF
- name: Build executable with PyInstaller (Windows) - name: Install UPX (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash shell: bash
run: | run: |
pyinstaller --onefile --windowed \ curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
--add-data "build_info.json;." \ 7z x upx.zip -oupx-dir
--add-data "build_info.json;core" \ UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
--add-data "build_info.json;core/interface" \ echo "$UPX_DIR" >> $GITHUB_PATH
--add-data "core/interface/assets;core/interface/assets" \ export PATH="$UPX_DIR:$PATH"
--icon "core/interface/assets/logo.png" \ echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
--exclude-module PyQt6 \ upx --version | head -1
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \ - name: Convert icon for Windows
--hidden-import=backports \ if: runner.os == 'Windows'
--hidden-import=jaraco.context \ shell: bash
--hidden-import=jaraco.text \ run: |
--hidden-import=jaraco.functools \ python -c "
--name ${{ env.APP_NAME }} \ from PIL import Image
img = Image.open('core/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 \
--onefile \
--onefile-no-compression \
--onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
--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-dir=core/interface/assets=core/interface/assets \
--include-data-files=build_info.json=build_info.json \
--include-data-files=build_info.json=core/build_info.json \
--include-data-files=build_info.json=core/interface/build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
main.py main.py
- name: Build executable with PyInstaller (Unix) - name: Build with PyInstaller (Linux)
if: runner.os != 'Windows' if: runner.os == 'Linux'
shell: bash shell: bash
run: | run: |
pyinstaller --onefile --windowed \ pyinstaller --onefile --windowed \
@@ -122,22 +153,43 @@ jobs:
--name ${{ env.APP_NAME }} \ --name ${{ env.APP_NAME }} \
main.py main.py
- name: Make executable (Unix) - name: Build with PyInstaller (macOS)
if: runner.os != 'Windows' if: runner.os == 'macOS'
run: chmod +x dist/${{ env.APP_NAME }} shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--add-data "build_info.json:core" \
--add-data "build_info.json:core/interface" \
--add-data "core/interface/assets:core/interface/assets" \
--icon "core/interface/assets/logo.png" \
--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 }} \
main.py
- name: Rename artifact - name: Rename artifact
shell: bash shell: bash
run: | run: |
cd dist mkdir -p dist-final
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }} if [ "$RUNNER_OS" == "Windows" ]; then
ls -la mv ${{ env.APP_NAME }}.exe dist-final/${{ matrix.artifact_name }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
ls -la dist-final/
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.os }}-build name: ${{ matrix.os }}-build
path: dist/${{ matrix.artifact_name }} path: dist-final/${{ matrix.artifact_name }}
retention-days: 1 retention-days: 1
release: release:
@@ -162,9 +214,16 @@ jobs:
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
ls -la release/ ls -la release/
- name: Generate checksums
run: |
cd release
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Generate release notes - name: Generate release notes
run: | run: |
SHORT_SHA="${{ needs.build.outputs.short_sha }}" SHORT_SHA="${{ needs.build.outputs.short_sha }}"
CHECKSUMS=$(cat release/SHA256SUMS.txt)
cat > release_notes.md << EOF cat > release_notes.md << EOF
## Development Build ## Development Build
@@ -177,6 +236,11 @@ jobs:
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\` - Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\` - Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\` - macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
### SHA-256 Checksums
\`\`\`
${CHECKSUMS}
\`\`\`
EOF EOF
cat release_notes.md cat release_notes.md
@@ -190,5 +254,6 @@ jobs:
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+37 -10
View File
@@ -1,11 +1,11 @@
# SoftwareManager - A Download Manager/Launcher # SoftwareManager
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources. SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker and the official M0nkrus Telegram channel.
## Features ## Features
- VPN Binding (impl. soon) - Searching & Downloading Software from various pages
- Bandwidth/Download speed limiting - Network Interface Binding
- Start downloads via a predefined magnet URI. - Simple UI, Speed Limiting etc.
- View detailed download information (name, speed, and status). - No Logins required
## Installation ## Installation
1. Download the binary file for your operating system from releases 1. Download the binary file for your operating system from releases
@@ -14,18 +14,40 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
## Manual Installation ## Manual Installation
1. Ensure Python 3.x is installed on your system. 1. Ensure Python 3.x is installed on your system.
2. Clone the Repository:
```bash
git clone https://github.com/KeksPirates/SoftwareManager.git && cd SoftwareManager
```
2. Install Dependencies: 2. Install Dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. Run main.py: 3. Run main.py:
```bash ```bash
python3 main.py python main.py
``` ```
## Notes ## Additional Information
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of its branch. - SoftwareManager is still in Development and most likely contains issues. Please report any bugs you find via the Issues tab. Contributions are also greatly appreciated.
- This tool is for educational purposes only. Use it responsibly. - MacOS Builds haven't been properly tested - We're thankful to receive feedback.
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of the server branch.
## Python Dependencies
(also included in requirements.txt)
```bash
requests (2.32.2)
PySide6 (6.10.1)
beautifulsoup (44.13.5)
darkdetect (0.7.1)
pyinstaller (6.15.0)
PyQtDarkTheme-fork (2.3.4)
aiohttp (3.13.0)
plyer (2.1.0)
psutil (7.1.0)
libtorrent (2.0.11)
libtorrent-windows-dll (0.0.3)
```
## Activity ## Activity
@@ -41,6 +63,11 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
</picture> </picture>
</a> </a>
## Contributors
<a href="https://github.com/KeksPirates/SoftwareManager/graphs/contributors">
<img src="https://contrib.rocks/image?repo=KeksPirates/SoftwareManager" />
</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.
+1 -1
View File
@@ -4,7 +4,7 @@ from core.utils.general.logs import consoleLog
def scrape_rutracker(search_text): def scrape_rutracker(search_text):
search = requests.get(f"{state.api_url}/search/{search_text}") search = requests.get(f"{state.api_url}/search?q={search_text}")
consoleLog("Sent request to server") consoleLog("Sent request to server")
if search: if search:
try: try:
+6 -4
View File
@@ -1,14 +1,16 @@
import requests import requests
import asyncio import asyncio
import threading
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.logs import consoleLog from core.utils.general.logs import consoleLog
from core.utils.general.wrappers import run_thread
global url_uztracker global url_uztracker
url_uztracker = "https://uztracker.net/tracker.php?nm=" url_uztracker = "https://uztracker.net/tracker.php?nm="
async def init_uztracker(): def init_uztracker():
global up global up
global soup global soup
try: try:
@@ -19,12 +21,12 @@ async def init_uztracker():
up = False up = False
consoleLog(f"Uztracker seems down, status code: {response.status_code}") consoleLog(f"Uztracker seems down, status code: {response.status_code}")
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
consoleLog(f"\nRequest Exception on {url_uztracker}:") consoleLog(f"Request Exception on {url_uztracker}:")
consoleLog(str(e)) consoleLog(str(e))
consoleLog("\nIs the Site down?") consoleLog("Is the Site down?")
up = False up = False
asyncio.run(init_uztracker()) run_thread(threading.Thread(target=init_uztracker))
def scrape_uztracker(search): def scrape_uztracker(search):
if up: if up:
+16 -17
View File
@@ -1,6 +1,9 @@
from core.utils.config.settings import save_settings from core.utils.config.settings import save_settings
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.logs import consoleLog from core.utils.general.logs import consoleLog
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 PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
@@ -13,7 +16,8 @@ from PySide6.QtWidgets import (
QSpinBox, QSpinBox,
QCheckBox, QCheckBox,
QFileDialog, QFileDialog,
QComboBox QComboBox,
QTabWidget,
) )
import platform import platform
@@ -23,10 +27,9 @@ def settings_dialog(self):
consoleLog("Settings dialog opened") consoleLog("Settings dialog opened")
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle("Settings") dialog.setWindowTitle("Settings")
dialog.setFixedSize(800, 550) dialog.setFixedSize(700, 450)
dialog.setLayout(QVBoxLayout()) dialog.setLayout(QVBoxLayout())
dialog.layout().addWidget(QLabel("Settings"))
def close_settings(): def close_settings():
dialog.reject() dialog.reject()
@@ -58,7 +61,6 @@ def settings_dialog(self):
autoresume_layout.addStretch() autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume) autoresume_checkbox.setChecked(state.autoresume)
autoresume_layout.addWidget(autoresume_checkbox) autoresume_layout.addWidget(autoresume_checkbox)
dialog.layout().addWidget(autoresume_container)
# transparent window checkbox # transparent window checkbox
@@ -73,12 +75,11 @@ def settings_dialog(self):
transparent_window_checkbox.setChecked(state.window_transparency) transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked)) transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox) transparent_window_layout.addWidget(transparent_window_checkbox)
dialog.layout().addWidget(transparent_window_container)
################## ##################
# SERVER SETTING # # SERVER SETTING #
################## ##################
api_url_container = QWidget() api_url_container = QWidget()
api_url_layout = QHBoxLayout() api_url_layout = QHBoxLayout()
@@ -88,7 +89,6 @@ def settings_dialog(self):
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
api_url_container.setLayout(api_url_layout) api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url) api_url.setText(state.api_url)
dialog.layout().addWidget(api_url_container)
################# #################
# DOWNLOAD PATH # # DOWNLOAD PATH #
@@ -103,7 +103,6 @@ def settings_dialog(self):
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
download_path_container.setLayout(download_path_layout) download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path) download_path.setText(state.download_path)
dialog.layout().addWidget(download_path_container)
def browse_download_path(): def browse_download_path():
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path) dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
@@ -127,7 +126,6 @@ def settings_dialog(self):
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
image_path_container.setLayout(image_path_layout) image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path) image_path.setText(state.image_path)
dialog.layout().addWidget(image_path_container)
def browse_image_path(): def browse_image_path():
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0] file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
@@ -154,8 +152,7 @@ def settings_dialog(self):
down_speed_limit_layout.addWidget(down_speed_limit) down_speed_limit_layout.addWidget(down_speed_limit)
down_speed_limit.setFixedWidth(180) down_speed_limit.setFixedWidth(180)
down_speed_limit.setFixedHeight(30) down_speed_limit.setFixedHeight(30)
dialog.layout().addWidget(down_speed_limit_container)
up_speed_limit_container = QWidget() up_speed_limit_container = QWidget()
up_speed_limit_layout = QHBoxLayout() up_speed_limit_layout = QHBoxLayout()
@@ -168,7 +165,6 @@ def settings_dialog(self):
up_speed_limit_layout.addWidget(up_speed_limit) up_speed_limit_layout.addWidget(up_speed_limit)
up_speed_limit.setFixedWidth(180) up_speed_limit.setFixedWidth(180)
up_speed_limit.setFixedHeight(30) up_speed_limit.setFixedHeight(30)
dialog.layout().addWidget(up_speed_limit_container)
###################### ######################
# CONNECTION CONFIGS # # CONNECTION CONFIGS #
@@ -186,7 +182,6 @@ def settings_dialog(self):
max_connections_layout.addWidget(max_connections) max_connections_layout.addWidget(max_connections)
max_connections.setFixedWidth(180) max_connections.setFixedWidth(180)
max_connections.setFixedHeight(30) max_connections.setFixedHeight(30)
dialog.layout().addWidget(max_connections_container)
#################### ####################
# DOWNLOAD CONFIGS # # DOWNLOAD CONFIGS #
@@ -204,7 +199,6 @@ def settings_dialog(self):
max_downloads_layout.addWidget(max_downloads) max_downloads_layout.addWidget(max_downloads)
max_downloads.setFixedWidth(180) max_downloads.setFixedWidth(180)
max_downloads.setFixedHeight(30) max_downloads.setFixedHeight(30)
dialog.layout().addWidget(max_downloads_container)
##################### #####################
# INTERFACE BINDING # # INTERFACE BINDING #
@@ -232,8 +226,7 @@ def settings_dialog(self):
interface_select.setFixedHeight(30) interface_select.setFixedHeight(30)
interface_layout.addWidget(interface_select) interface_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout) interface_container.setLayout(interface_layout)
dialog.layout().addWidget(interface_container)
############### ###############
# SAVE/CANCEL # # SAVE/CANCEL #
@@ -259,6 +252,12 @@ def settings_dialog(self):
layout.addWidget(cancel_btn) layout.addWidget(cancel_btn)
layout.addWidget(save_btn) layout.addWidget(save_btn)
self.tabs = QTabWidget()
self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs)
self.tab2 = paths_tab("Paths", download_path_container, image_path_container, self.tabs)
self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs)
dialog.layout().addWidget(self.tabs)
dialog.layout().addLayout(layout) dialog.layout().addLayout(layout)
dialog.exec() dialog.exec()
+21 -15
View File
@@ -29,7 +29,7 @@ import libtorrent as lt
import time import time
import sys import sys
import json import json
from core.utils.general.logs import consoleLog, remove_download_log from core.utils.general.logs import consoleLog, remove_download_log, flush_log_buffer
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.network.download import download_selected from core.utils.network.download import download_selected
@@ -73,15 +73,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.log_signal.connect(self._on_log_signal) self.log_signal.connect(self._on_log_signal)
def get_asset_path(filename): def get_asset_path(filename):
if getattr(sys, 'frozen', False): base_path = os.path.dirname(os.path.abspath(__file__))
base_path = sys._MEIPASS asset_path = os.path.join(base_path, 'assets', filename)
else:
base_path = os.path.dirname(__file__)
asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename)
if os.path.exists(asset_path): if os.path.exists(asset_path):
return asset_path return asset_path
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
asset_path = os.path.join(sys._MEIPASS, 'core', 'interface', 'assets', filename)
if os.path.exists(asset_path):
return asset_path
return os.path.join('core', 'interface', 'assets', filename) return os.path.join('core', 'interface', 'assets', filename)
self.setWindowIcon(QIcon(get_asset_path("logo.png"))) self.setWindowIcon(QIcon(get_asset_path("logo.png")))
@@ -150,13 +151,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.consoleLog = QTextEdit() self.consoleLog = QTextEdit()
self.progressbar = QProgressBar() self.progressbar = QProgressBar()
flush_log_buffer()
# Table Widget for Item List # Table Widget for Item List
self.qtablewidget = QTableWidget() self.qtablewidget = QTableWidget()
self.qtablewidget.setColumnCount(2) self.qtablewidget.setColumnCount(4)
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
self.qtablewidget.verticalHeader().setVisible(False) self.qtablewidget.verticalHeader().setVisible(False)
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"]) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
@@ -283,10 +286,13 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
subprocess.Popen(["open", save_path]) subprocess.Popen(["open", save_path])
return return
if status.paused: is_paused = bool(magnetdl.flags() & lt.torrent_flags.paused)
if is_paused:
magnetdl.set_flags(lt.torrent_flags.auto_managed)
magnetdl.resume() magnetdl.resume()
consoleLog(f"Resumed download: {status.name}", True) consoleLog(f"Resumed download: {status.name}", True)
else: else:
magnetdl.unset_flags(lt.torrent_flags.auto_managed)
magnetdl.pause() magnetdl.pause()
consoleLog(f"Paused download: {status.name}", True) consoleLog(f"Paused download: {status.name}", True)
@@ -360,7 +366,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
# Tabs # Tabs
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.tabs = QTabWidget()
self.tabs.setStyleSheet(""" self.tabs.setStyleSheet("""
QTabBar::tab { QTabBar::tab {
font-size: 11px; font-size: 11px;
@@ -382,16 +387,17 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if state.image_path is not None and os.path.exists(state.image_path): if state.image_path is not None and os.path.exists(state.image_path):
self.image = QImage(state.image_path) self.image = QImage(state.image_path)
self.image = self.image.scaledToWidth(300, Qt.SmoothTransformation)
self.pixmap = QPixmap.fromImage(self.image) self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label = QLabel(self) self.overlay_label = QLabel(self)
self.overlay_label.setPixmap(self.pixmap) self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize() self.overlay_label.adjustSize()
self.overlay_label.raise_() self.overlay_label.raise_()
offset_x = -1350 # offset_x = -1350
offset_y = -550 # offset_y = -550
x = self.width() - self.overlay_label.width() - offset_x x = self.width() - self.overlay_label.width() # - offset_x
y = self.height() - self.overlay_label.height() - offset_y y = self.height() - self.overlay_label.height() # - offset_y
self.overlay_label.move(x, y) self.overlay_label.move(x, y)
# temporarily disabled # temporarily disabled
+8 -4
View File
@@ -3,7 +3,7 @@ from core.utils.general.logs import consoleLog
from core.data.scrapers.uztracker import scrape_uztracker from core.data.scrapers.uztracker import scrape_uztracker
from core.data.scrapers.rutracker import scrape_rutracker from core.data.scrapers.rutracker import scrape_rutracker
from core.data.scrapers.monkrus import scrape_monkrus_telegram from core.data.scrapers.monkrus import scrape_monkrus_telegram
from core.utils.network.jsonhandler import split_data, format_data from core.utils.network.jsonhandler import split_data, format_data, format_data_m0nkrus
from core.utils.data.state import state from core.utils.data.state import state
def return_pressed(self): def return_pressed(self):
@@ -44,15 +44,19 @@ def return_pressed(self):
self.qtablewidget.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
state.post_titles, _, state.post_author = format_data(state.posts) state.post_titles, _, state.post_author, state.post_seeders, state.post_leechers = format_data(state.posts)
self.show_empty_results(False) self.show_empty_results(False)
self.qtablewidget.clear() self.qtablewidget.clear()
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"]) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
self.qtablewidget.setRowCount(len(state.post_titles)) self.qtablewidget.setRowCount(len(state.post_titles))
for i, author in enumerate(state.post_author): for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author)) self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles): for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title)) self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
for i, seeders in enumerate(state.post_seeders):
self.qtablewidget.setItem(i, 2, QTableWidgetItem(seeders))
for i, leechers in enumerate(state.post_leechers):
self.qtablewidget.setItem(i, 3, QTableWidgetItem(leechers))
if state.debug == True: if state.debug == True:
consoleLog(f"Response Cached: {cached}") consoleLog(f"Response Cached: {cached}")
else: else:
@@ -68,7 +72,7 @@ def return_pressed(self):
self.qtablewidget.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
state.post_titles, _, state.post_author = format_data(state.posts) state.post_titles, _, state.post_author = format_data_m0nkrus(state.posts)
self.show_empty_results(False) self.show_empty_results(False)
self.qtablewidget.clear() self.qtablewidget.clear()
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"]) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
+39
View File
@@ -15,3 +15,42 @@ def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
tab.setLayout(layout) tab.setLayout(layout)
tabs.addTab(tab, title) tabs.addTab(tab, title)
return tab return tab
#################
# SETTINGS TABS #
#################
def general_tab(title, autoresume, update_checkbox, transparent_window, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(autoresume)
layout.addWidget(update_checkbox)
layout.addWidget(transparent_window)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
def paths_tab(title, download_path, image_path, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(download_path)
layout.addWidget(image_path)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
def network_tab(title, network_interface, max_connections, max_downloads, up_speed_limit, down_speed_limit, api_url, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(network_interface)
layout.addWidget(max_connections)
layout.addWidget(max_downloads)
layout.addWidget(up_speed_limit)
layout.addWidget(down_speed_limit)
layout.addWidget(api_url)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
+3 -3
View File
@@ -36,7 +36,7 @@ def init_session():
settings["listen_interfaces"] = f"{interface_ip}:6881" settings["listen_interfaces"] = f"{interface_ip}:6881"
consoleLog(f"Binding to Interface IP: {interface_ip}") consoleLog(f"Binding to Interface IP: {interface_ip}")
else: else:
consoleLog(f"Error finding IP for Interface {state.bound_interface}") consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
state.dl_session.apply_settings(settings) state.dl_session.apply_settings(settings)
@@ -51,7 +51,7 @@ def add_download(magnet_uri, dl_path=state.download_path):
init_session() init_session()
if magnet_uri in state.active_downloads: if magnet_uri in state.active_downloads:
consoleLog("Skipping, download already running...") consoleLog("Skipping Downloading, download already running...")
return return
magnetdl = lt.parse_magnet_uri(magnet_uri) magnetdl = lt.parse_magnet_uri(magnet_uri)
@@ -114,7 +114,7 @@ def update_settings():
settings["listen_interfaces"] = f"{interface_ip}:6881" settings["listen_interfaces"] = f"{interface_ip}:6881"
consoleLog(f"Binding to Interface IP: {interface_ip}") consoleLog(f"Binding to Interface IP: {interface_ip}")
else: else:
consoleLog(f"Error finding IP for Interface {state.bound_interface}") consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
state.dl_session.apply_settings(settings) state.dl_session.apply_settings(settings)
+27 -14
View File
@@ -8,16 +8,23 @@ def create_config():
config["General"] = { config["General"] = {
"debug": True, "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}", "api_url": f"{state.api_url}",
"download_path": f"{state.download_path}", "download_path": f"{state.download_path}",
"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}",
"ignore_updates": f"{state.ignore_updates}",
"image_path": f"{state.image_path}",
"autoresume": f"{state.autoresume}",
"max_connections": f"{state.max_connections}", "max_connections": f"{state.max_connections}",
"max_downloads": f"{state.max_downloads}", "max_downloads": f"{state.max_downloads}"
"window_transparency": f"{state.window_transparency}", }
config["Paths"] = {
"bound_interface": f"{state.bound_interface}",
"image_path": f"{state.image_path}"
} }
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -48,17 +55,23 @@ def read_config():
return return
config.read(config_file) config.read(config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug) state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.api_url = config.get("General", "api_url", fallback=state.api_url)
state.download_path = config.get("General", "download_path", fallback=state.download_path)
state.down_speed_limit = config.getint("General", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("General", "upload_speed_limit", fallback=state.up_speed_limit)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates) state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.image_path = config.get("General", "image_path", fallback=state.image_path)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume) state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.max_connections = config.getint("General", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("General", "max_downloads", fallback=state.max_downloads)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency) 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.download_path = config.get("Network", "download_path", fallback=state.download_path)
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.bound_interface = config.get("Paths", "bound_interface", fallback=state.bound_interface)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
create_config() create_config()
+2
View File
@@ -11,6 +11,8 @@ class AppState(QObject):
self.post_titles: List[str] = [] self.post_titles: List[str] = []
self.post_urls: List[str] = [] self.post_urls: List[str] = []
self.post_author: List[str] = [] self.post_author: List[str] = []
self.post_seeders: List[str] = []
self.post_leechers: List[str] = []
self.version: str = "dev" self.version: str = "dev"
self._image_path: str = "" self._image_path: str = ""
self.ignore_updates: bool = False self.ignore_updates: bool = False
+2 -2
View File
@@ -15,12 +15,12 @@ def get_item_url(item, posts, post_titles): # softwarelist currentitem, post lis
return item return item
if state.tracker == "rutracker": if state.tracker == "rutracker":
item_dict = posts[post_index] item_dict = posts[post_index]
_, post_links, _ = format_data([item_dict]) _, post_links, _, _, _ = format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}") consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0] return post_links[0]
if state.tracker == "m0nkrus": if state.tracker == "m0nkrus":
item_dict = posts[post_index] item_dict = posts[post_index]
_, post_links, _ = format_data([item_dict]) _, post_links, _, _, _,= format_data([item_dict])
consoleLog(f"Found post URL: {post_links[0]}") consoleLog(f"Found post URL: {post_links[0]}")
return post_links[0] return post_links[0]
+24 -4
View File
@@ -6,6 +6,8 @@ import json
import os import os
import re import re
_log_buffer = []
def add_download_log(title, url, magnet_uri, completed) -> DownloadList: def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
@@ -22,7 +24,12 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads): if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
consoleLog("Skipped Logging, download already in file") if magnet_uri in state.active_downloads:
consoleLog("Skipping Logging, download already running...")
return
consoleLog("File already in Log, updating Download State...")
hash = extract_hash_from_magnet(magnet_uri)
update_download_completed_by_hash(hash, False)
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid) return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
@@ -189,15 +196,28 @@ def set_main_window(window):
global _main_window global _main_window
_main_window = window _main_window = window
def flush_log_buffer(): # credits to claude
global _log_buffer
if _log_buffer:
try:
from core.interface.gui import MainWindow
for log_entry in _log_buffer:
MainWindow.add_log(log_entry)
_log_buffer = []
except Exception:
pass
def consoleLog(text, printAnyways = False): def consoleLog(text, printAnyways = False):
now = datetime.now() now = datetime.now()
current_time = now.strftime("%H:%M:%S") current_time = now.strftime("%H:%M:%S")
formatted_text = f"[{current_time}] {text}"
try: try:
from core.interface.gui import MainWindow from core.interface.gui import MainWindow
MainWindow.add_log(f"[{current_time}] {text}") MainWindow.add_log(formatted_text)
except Exception: except Exception:
pass global _log_buffer
_log_buffer.append(formatted_text)
if state.debug or printAnyways: if state.debug or printAnyways:
print(f"[{current_time}] {text}") print(formatted_text)
+13 -1
View File
@@ -11,7 +11,16 @@ def split_data(data):
return count, posts, query, success, cached return count, posts, query, success, cached
def format_data(data): 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_m0nkrus(data):
post_author = [post["author"] for post in data] post_author = [post["author"] for post in data]
post_titles = [post["title"] for post in data] post_titles = [post["title"] for post in data]
post_links = [post["url"] for post in data] post_links = [post["url"] for post in data]
@@ -20,4 +29,7 @@ def format_data(data):
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3 # https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
+8 -4
View File
@@ -17,6 +17,7 @@ import platform
import signal import signal
import argparse import argparse
import sys import sys
import time
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -41,16 +42,17 @@ def keyboardinterrupthandler(signum, frame):
closehelper() closehelper()
if __name__ == "__main__": if __name__ == "__main__":
start_time = time.perf_counter()
read_config() read_config()
logs = get_download_logs() logs = get_download_logs()
count, downloads = split_data(logs) _, downloads = split_data(logs)
if args.debug: if args.debug:
state.debug = args.debug # override of read_config state.debug = args.debug # override of read_config
signal.signal(signal.SIGINT, keyboardinterrupthandler) signal.signal(signal.SIGINT, keyboardinterrupthandler)
consoleLog("Started check_completed thread") consoleLog("Starting SoftwareManager...")
consoleLog("Fetching Network Interfaces...") consoleLog("Fetching Network Interfaces...")
list_interfaces() list_interfaces()
consoleLog("Initialiting Interface variables...") consoleLog("Initializing Interface variables...")
init_interfaces() init_interfaces()
consoleLog(f"Current Bound: {state.bound_interface}") consoleLog(f"Current Bound: {state.bound_interface}")
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True)) run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
@@ -58,6 +60,8 @@ if __name__ == "__main__":
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True)) run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
consoleLog("Started Thread: update_log") consoleLog("Started Thread: update_log")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume))) run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
consoleLog("Launching GUI") consoleLog("Started Thread: check_completed")
elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
run_gui() run_gui()
+1 -1
View File
@@ -1,5 +1,5 @@
requests==2.32.2 requests==2.32.2
PySide6==6.10.1 PySide6-Essentials==6.10.1
beautifulsoup4==4.13.5 beautifulsoup4==4.13.5
darkdetect==0.7.1 darkdetect==0.7.1
pyinstaller==6.15.0 pyinstaller==6.15.0