Compare commits

...

17 Commits

Author SHA1 Message Date
Vxrtrauter 2fb9ad9e28 daily commit 2026-02-18 17:20:42 +01:00
Vxrtrauter 193188c383 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-02-19 17:19:26 +01:00
Vxrtrauter d9de5c6ff5 daily commit 2026-02-19 17:19:15 +01:00
KeksNino 12223d18de add transparency to settings dialog on linux/macos 2026-02-17 00:29:55 +01:00
KeksNino 544733ce6b fix app icon in nuitka build process and remove settings pngs 2026-02-17 00:10:30 +01:00
KeksNino 7aee2bcfe9 remove include flags for assets in github workflows 2026-02-16 23:58:34 +01:00
KeksNino d274e64841 fix assets not showing up by loading them from base64 2026-02-16 23:49:21 +01:00
KeksNino 23c74f6487 fix: wrong assets path on windows 2026-02-16 02:34:44 +01:00
KeksNino 19ea09d035 fix: build_info.json path in github CI 2026-02-11 21:28:06 +01:00
shayaa 92359d75e5 Fix asset paths in dev.yml for logo and data files 2026-02-11 21:27:01 +01:00
shayaa 315a68c000 Update asset paths in dev.yml for PyInstaller 2026-02-11 21:26:13 +01:00
shayaa 4b0ef44efd Update file paths to src/main.py in dev.yml 2026-02-11 21:23:58 +01:00
KeksNino 6dc28615a0 refactor: move main.py and core into new src folder 2026-02-11 21:19:35 +01:00
shayaa 2ea9a329d1 Update server hosting instructions in README 2026-02-11 03:49:12 +01:00
Vxrtrauter 9f8111ac73 refactor: streamline update check logic and enhance log handling 2026-02-11 03:27:49 +01:00
Vxrtrauter e5b809d6d4 # This is a combination of 9 commits.
# This is the 1st commit message:

fix: update Python version to 3.12 and switch from PyInstaller to Nuitka for building executables

# This is the commit message #2:

fix: correct UPX path for Windows installation in CI workflow

# This is the commit message #3:

fix: update UPX installation path handling for Windows and Linux in CI workflow

# This is the commit message #4:

fix: enhance UPX installation by setting UPX_BINARY environment variable for better compatibility

# This is the commit message #5:

fix: add --assume-yes-for-downloads option to Nuitka build commands for improved automation

# This is the commit message #6:

fix: update asset path handling in MainWindow for improved compatibility with frozen applications

# This is the commit message #7:

fix: update Nuitka build options and refine PySide6 dependency for improved compatibility

# This is the commit message #8:

fix: add Windows icon conversion step and update Nuitka build options for improved compatibility

# This is the commit message #9:

fix: refactor Nuitka and UPX installation steps for improved clarity and compatibility
2026-02-11 03:01:31 +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
36 changed files with 157 additions and 75 deletions
+92 -32
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,36 +88,58 @@ 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 \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
main.py
- name: Build executable with PyInstaller (Unix) - name: Convert icon for Windows
if: runner.os != 'Windows' if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/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-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 \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash shell: bash
run: | run: |
pyinstaller --onefile --windowed \ pyinstaller --onefile --windowed \
--add-data "build_info.json:." \ --add-data "build_info.json:." \
--add-data "build_info.json:core" \ --add-data "build_info.json:src/core" \
--add-data "build_info.json:core/interface" \ --add-data "build_info.json:src/core/interface" \
--add-data "core/interface/assets:core/interface/assets" \
--icon "core/interface/assets/logo.png" \
--exclude-module PyQt6 \ --exclude-module PyQt6 \
--exclude-module PyQt5 \ --exclude-module PyQt5 \
--hidden-import=backports.tarfile \ --hidden-import=backports.tarfile \
@@ -120,24 +148,43 @@ jobs:
--hidden-import=jaraco.text \ --hidden-import=jaraco.text \
--hidden-import=jaraco.functools \ --hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \ --name ${{ env.APP_NAME }} \
main.py src/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:src/core" \
--add-data "build_info.json:src/core/interface" \
--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: 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 +209,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 +231,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 +249,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 }}
+1 -1
View File
@@ -30,7 +30,7 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
## Additional Information ## Additional Information
- 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. - 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.
- MacOS Builds haven't been properly tested - We're thankful to receive feedback. - 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. - If you want to host your own server, clone our [server repository](https://github.com/KeksPirates/SoftwareManager-Server), 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 repo.
## Python Dependencies ## Python Dependencies
(also included in requirements.txt) (also included in requirements.txt)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+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
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -4,7 +4,8 @@ from core.utils.general.logs import consoleLog
from core.interface.utils.tabhelper import general_tab from core.interface.utils.tabhelper import general_tab
from core.interface.utils.tabhelper import paths_tab from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.tabhelper import network_tab from core.interface.utils.tabhelper import network_tab
from PySide6 import QtWidgets from PySide6 import QtWidgets, Qt
from PySide6.QtCore import Qt
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QPushButton, QPushButton,
@@ -31,6 +32,9 @@ def settings_dialog(self):
dialog.setLayout(QVBoxLayout()) dialog.setLayout(QVBoxLayout())
if state.window_transparency and platform.system() != "Windows":
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def close_settings(): def close_settings():
dialog.reject() dialog.reject()
@@ -1,4 +1,4 @@
from PySide6 import QtWidgets from PySide6 import QtWidgets, QtCore, QtGui
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
@@ -29,6 +29,7 @@ import libtorrent as lt
import time import time
import sys import sys
import json import json
import base64
from core.utils.general.logs import consoleLog, remove_download_log, flush_log_buffer 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
@@ -37,6 +38,9 @@ from core.utils.network.update_checker import check_for_updates
from core.interface.utils.tabhelper import create_tab from core.interface.utils.tabhelper import create_tab
from core.interface.utils.searchhelper import return_pressed from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog from core.interface.dialogs.settings import settings_dialog
from core.interface.assets.base64_icons import settings_black_base64
from core.interface.assets.base64_icons import settings_white_base64
from core.interface.assets.base64_icons import logo_base64
from core.network.libtorrent_misc import cleanup_session from core.network.libtorrent_misc import cleanup_session
from core.utils.general.shutdown import closehelper from core.utils.general.shutdown import closehelper
@@ -72,20 +76,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
MainWindow._instance = self MainWindow._instance = self
self.log_signal.connect(self._on_log_signal) self.log_signal.connect(self._on_log_signal)
def get_asset_path(filename): pixmap = QPixmap()
if getattr(sys, 'frozen', False): image_data = base64.b64decode(logo_base64)
base_path = sys._MEIPASS pixmap.loadFromData(image_data)
else: self.setWindowIcon(QIcon(pixmap))
base_path = os.path.dirname(__file__)
asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename)
if os.path.exists(asset_path):
return asset_path
return os.path.join('core', 'interface', 'assets', filename)
self.setWindowIcon(QIcon(get_asset_path("logo.png")))
if platform.system() == "Windows": if platform.system() == "Windows":
try: try:
@@ -102,23 +96,22 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
state.version = build_info.get("version") state.version = build_info.get("version")
# Check for updates on Windows # Check for updates on Windows
if state.ignore_updates is False: if state.ignore_updates is False and platform.system() == "Windows":
if platform.system() == "Windows": result = check_for_updates()
result = check_for_updates() if result != (None, None):
if result != (None, None): assets, latest_version = result
assets, latest_version = result
if assets: if assets:
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information) msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("Update Available") msg.setWindowTitle("Update Available")
msg.setText("A new version is available.") msg.setText("A new version is available.")
msg.setInformativeText("Press Ok to download the update.") msg.setInformativeText("Press Ok to download the update.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore) msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
response = msg.exec_() response = msg.exec_()
if response == QMessageBox.StandardButton.Ok: if response == QMessageBox.StandardButton.Ok:
download_update(latest_version) download_update(latest_version)
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
@@ -417,10 +410,18 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
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;")
if darkdetect.isDark(): if darkdetect.isDark():
self.settings_btn.setIcon(QIcon(get_asset_path("settings_white.png"))) settings_white_pixmap = QPixmap()
settings_white = base64.b64decode(settings_white_base64)
settings_white_pixmap.loadFromData(settings_white)
self.settings_btn.setIcon(QIcon(settings_white_pixmap))
else: else:
self.settings_btn.setIcon(QIcon(get_asset_path("settings_black.png"))) settings_black_pixmap = QPixmap()
settings_black = base64.b64decode(settings_black_base64)
settings_black_pixmap.loadFromData(settings_black)
self.settings_btn.setIcon(QIcon(settings_black_pixmap))
self.settings_btn.setToolTip("Settings") self.settings_btn.setToolTip("Settings")
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.settings_btn.clicked.connect(lambda: settings_dialog(self)) self.settings_btn.clicked.connect(lambda: settings_dialog(self))
@@ -475,10 +476,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
MainWindow._instance.log_signal.emit(text) MainWindow._instance.log_signal.emit(text)
def _on_log_signal(self, text): def _on_log_signal(self, text):
self.consoleLog.append(text) if hasattr(self, 'consoleLog'):
self.consoleLog.verticalScrollBar().setValue( self.consoleLog.append(text)
self.consoleLog.verticalScrollBar().maximum() self.consoleLog.verticalScrollBar().setValue(
) self.consoleLog.verticalScrollBar().maximum()
)
def update_image_overlay(self, new_image_path): def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path) self.image = QImage(new_image_path)
@@ -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)
@@ -24,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)
View File