Compare commits

...

96 Commits

Author SHA1 Message Date
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
Vxrtrauter 7b2522438c feat: add network interface binding to settings dialog and update save_settings function 2026-02-06 23:50:18 +01:00
Vxrtrauter 0686c7aec2 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-02-06 17:52:21 +01:00
Vxrtrauter 6be1784903 feat: add functionality to retrieve and bind IP address of network interfaces 2026-02-06 17:50:42 +01:00
Vxrtrauter 4589322f0a feat: initialize network interface state and log interface details 2026-02-05 19:15:39 +01:00
shayaa 8132d789ef Add activity section to README
Added activity section with analytics image to README.
2026-02-05 17:35:38 +01:00
Vxrtrauter 3f39e67933 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-02-05 17:28:50 +01:00
Vxrtrauter d43d6e57cc add more interface stuff 2026-02-05 00:36:12 +01:00
KeksNino 28cffe59f5 feat: add context menu option to delete file 2026-02-04 21:34:03 +01:00
Vxrtrauter f1726f4e9a Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-02-04 20:44:51 +01:00
KeksNino b7ca895aeb fix: open folder because state.download_path is None in gui.py 2026-02-04 19:35:35 +01:00
KeksNino e95ad11672 add context menu to cancel downloads, open folder and copy magnet link 2026-02-04 18:56:03 +01:00
Vxrtrauter f2a79e5eed test more stuff 2026-02-04 04:04:31 +01:00
Vxrtrauter 990135450f test network stuff 2026-02-04 03:04:24 +01:00
KeksNino 62c6afe350 disable library tab (may be added again in the future) 2026-02-03 17:41:59 +01:00
KeksNino 1aaf56a4dc fix: window transparency on linux/macos 2026-02-03 17:04:09 +01:00
Vxrtrauter 47b643d68b revert all workflow changes 2026-02-02 02:31:00 +01:00
Vxrtrauter de1c8577d6 test new workflow 2026-02-02 02:26:41 +01:00
Vxrtrauter c835b8375c test new workflow 2026-02-02 02:23:52 +01:00
Vxrtrauter e7291e8e99 revert workflow changes 2026-02-02 02:23:22 +01:00
Vxrtrauter a4efe9185a update workflows 2026-02-02 02:18:33 +01:00
Vxrtrauter 86b4f023ac update QApplication instance handling (switch to pyqtdarktheme-fork) 2026-02-02 00:50:32 +01:00
KeksNino 006da55aa0 fix: pause/resume button showing up twice 2026-02-01 20:28:24 +01:00
KeksNino 214a389ad3 fix: if statement to disable windows transparency 2026-02-01 20:14:49 +01:00
KeksNino d4005d7e86 fix: disable transparency on windows as it is broken 2026-01-31 19:17:30 +01:00
KeksNino 08a45cb422 feat: add qt window transparency 2026-01-31 18:46:40 +01:00
Vxrtrauter c49726fe42 feat: improve cursor styles for buttons and combo box in GUI 2026-01-31 07:51:00 +01:00
Vxrtrauter ecc117dedc feat: enhance GUI layout and styling for better usability 2026-01-31 07:36:40 +01:00
KeksNino c51fc9ea4a feat: remove toolbar, will be improved later 2026-01-31 05:49:30 +01:00
KeksNino 48ed11c248 remove leftover aria2 code 2026-01-31 05:18:45 +01:00
Vxrtrauter d672e3d85f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-31 05:09:36 +01:00
Vxrtrauter 564c0f2ac5 refactor: enhance settings update logging and ensure console output on save 2026-01-31 05:09:34 +01:00
KeksNino 8a48c6c730 remove aria2 from github CI 2026-01-31 05:08:49 +01:00
Vxrtrauter 6432511639 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-31 05:04:28 +01:00
Vxrtrauter 054e1d0aa5 add error handling for update_settings 2026-01-31 05:04:26 +01:00
KeksNino 6f7a9e13f4 switch to uv for github workflow to improve workflow time 2026-01-31 05:00:47 +01:00
Vxrtrauter 89de75c3d7 link update_settings to settings dialog 2026-01-31 04:52:38 +01:00
Vxrtrauter f41520e595 fixed minor issues 2026-01-31 04:42:15 +01:00
shayaa 79c8de1dbf Merge pull request #29 from KeksPirates/switch-lt
Fully replace Aria2 with LibTorrent
2026-01-31 04:37:24 +01:00
shayaa dc933d54f3 fix broken merge 2026-01-31 04:36:49 +01:00
shayaa f78d8780cf Merge branch 'main' into switch-lt 2026-01-31 04:25:40 +01:00
Vxrtrauter 7f155547ec add libtorrent and libtorrent-windows-dll to requirements 2026-01-31 04:19:28 +01:00
Vxrtrauter 8e431f9826 integrate libtorrent for download status notifications and update logging 2026-01-31 04:17:20 +01:00
Vxrtrauter 748f0865b7 refactor shutdown process and integrate libtorrent session cleanup 2026-01-31 04:12:37 +01:00
Vxrtrauter a19cf469f6 update configs and settings 2026-01-31 03:54:38 +01:00
Vxrtrauter 8f9f77d63f update settings dialog & config, remove aria2 2026-01-31 03:05:31 +01:00
Vxrtrauter fa3a43887a integrate downloads in downloads tab 2026-01-31 02:20:26 +01:00
Vxrtrauter 685461e453 integrate download func in gui (partially) 2026-01-31 00:44:55 +01:00
KeksNino f3379174b0 feat: improve pause/resume/open folder button in downloads tab 2026-01-30 21:59:31 +01:00
Vxrtrauter b217a74fce add basic libtorrent integration 2026-01-30 21:31:30 +01:00
KeksNino 1da62ef414 feat: browse directories to select download_path and image_path 2026-01-30 10:43:43 +01:00
KeksNino 4b5c36030d fix macos open folder button 2026-01-30 10:32:10 +01:00
KeksNino cc5f6a804c show open folder button when download is complete 2026-01-29 22:55:30 +01:00
KeksNino b783d0a0cb only show ignore auto updates on windows 2026-01-24 10:28:58 +01:00
KeksNino d057624548 fix pause/resume and add eta + completed length in download 2026-01-23 23:18:04 +01:00
KeksNino 81f7970c47 fix workflow with ubuntu 22.04 2026-01-23 15:27:22 +01:00
KeksNino 6a41ac44c9 switch workflow to ubuntu 22.04 for better glibc compat 2026-01-22 19:21:52 +01:00
Vxrtrauter 26c5410f3f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-01-20 17:41:43 +01:00
Vxrtrauter 7067f243ae update appid 2026-01-20 17:39:06 +01:00
Vxrtrauter d82008c9fe fix no item in downloads text 2026-01-20 11:26:49 +01:00
Vxrtrauter 83ff2720c8 add error handling for magnet uri being none 2026-01-20 10:43:02 +01:00
Vxrtrauter 83adbb027f fix aria2 connection refused errors caused by autoresume 2026-01-19 13:49:20 +01:00
KeksNino 606c70efcc add unicode icons for pause/resume and fix action column size 2026-01-18 21:39:40 +01:00
KeksNino 9970086901 WIP: add pause/resume button in downloads tab 2026-01-18 02:03:31 +01:00
Vxrtrauter 0d631e0ab8 fix assets not showing up in build 2026-01-17 15:26:08 +01:00
Vxrtrauter 1c1ce3e69d update workflow again 2026-01-17 14:56:30 +01:00
Vxrtrauter b5931f5590 fix actions 2026-01-17 14:51:53 +01:00
Vxrtrauter 84ea030e2a Enhance asset handling: update icon loading to use dynamic path resolution 2026-01-17 14:46:07 +01:00
Vxrtrauter 356f3bf070 fix dependencies 2026-01-17 14:34:47 +01:00
Vxrtrauter 6ff29e2ea6 add logo to gui and assets 2026-01-17 14:30:48 +01:00
Vxrtrauter 847b50ab10 image overlay error message: add check for valid image path 2026-01-17 14:08:47 +01:00
Vxrtrauter 5bc81b5215 Enhance console logging: add option to print logs regardless of debug state 2026-01-17 02:00:16 +01:00
Vxrtrauter efe83613d0 Fixed Download Log Updating and add more Logging 2026-01-17 01:44:28 +01:00
KeksNino 3597cba207 update pyside6 version 2026-01-14 18:50:11 +01:00
Vxrtrauter 749051aa4f Enhance build process: add backports dependencies and generate release notes 2026-01-14 14:21:00 +01:00
27 changed files with 1226 additions and 410 deletions
+163 -47
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:
@@ -24,7 +24,7 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- os: ubuntu-latest - os: ubuntu-22.04
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
asset_name: SoftwareManager-dev-${{ github.sha }}-linux asset_name: SoftwareManager-dev-${{ github.sha }}-linux
- os: windows-latest - os: windows-latest
@@ -44,30 +44,22 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Install system dependencies (Linux) - name: Install uv
if: runner.os == 'Linux' uses: astral-sh/setup-uv@v7
run: | with:
sudo apt-get update version: "0.9.7"
sudo apt-get install -y \ enable-cache: true
libxcb-xinerama0 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-keysyms1 \
libxcb-render-util0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-shape0 \
libxcb-util1
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
python -m pip install --upgrade pip uv pip install -r requirements.txt
pip install -r requirements.txt uv pip install nuitka
pip install pyinstaller env:
UV_SYSTEM_PYTHON: 1
- name: Generate version - name: Generate version
id: version id: version
@@ -90,44 +82,136 @@ jobs:
} }
EOF EOF
- name: Download aria2c (Windows) - name: Install UPX
if: runner.os == 'Windows' shell: bash
run: | run: |
mkdir -p deps if [ "$RUNNER_OS" == "Windows" ]; then
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/ 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
elif [ "$RUNNER_OS" == "Linux" ]; then
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-amd64_linux.tar.xz -o upx.tar.xz
tar xf upx.tar.xz
UPX_DIR="$(pwd)/upx-5.1.0-amd64_linux"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx" >> $GITHUB_ENV
else
brew install upx
echo "UPX_BINARY=$(which upx)" >> $GITHUB_ENV
fi
upx --version | head -1
- name: Build executable with PyInstaller (Windows) - name: Build with Nuitka (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: bash shell: bash
run: | run: |
pyinstaller --onefile --windowed \ python -m nuitka \
--add-data "build_info.json;." \ --onefile \
--add-data "deps/aria2c.exe;." \ --onefile-no-compression \
--add-data "core/interface/assets;core/interface/assets" \ --onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
--name ${{ env.APP_NAME }} \ --assume-yes-for-downloads \
--windows-console-mode=disable \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--noinclude-dlls='Qt6WebEngine*' \
--noinclude-dlls='Qt6Multimedia*' \
--noinclude-dlls='Qt6Quick*' \
--noinclude-dlls='Qt6Qml*' \
--noinclude-dlls='Qt6Designer*' \
--noinclude-dlls='Qt63D*' \
--noinclude-dlls='Qt6Bluetooth*' \
--noinclude-dlls='Qt6Nfc*' \
--noinclude-dlls='Qt6Sensors*' \
--noinclude-dlls='Qt6Serial*' \
--noinclude-dlls='Qt6Test*' \
--noinclude-dlls='Qt6Location*' \
--noinclude-dlls='Qt6Pdf*' \
--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 Nuitka (Linux)
if: runner.os != 'Windows' if: runner.os == 'Linux'
shell: bash shell: bash
run: | run: |
pyinstaller --onefile --windowed \ python -m nuitka \
--add-data "build_info.json:." \ --onefile \
--add-data "core/interface/assets:core/interface/assets" \ --onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
--name ${{ env.APP_NAME }} \ --assume-yes-for-downloads \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--noinclude-dlls='libQt6WebEngine*' \
--noinclude-dlls='libQt6Multimedia*' \
--noinclude-dlls='libQt6Quick*' \
--noinclude-dlls='libQt6Qml*' \
--noinclude-dlls='libQt6Designer*' \
--noinclude-dlls='libQt63D*' \
--noinclude-dlls='libQt6Bluetooth*' \
--noinclude-dlls='libQt6Nfc*' \
--noinclude-dlls='libQt6Sensors*' \
--noinclude-dlls='libQt6Serial*' \
--noinclude-dlls='libQt6Test*' \
--noinclude-dlls='libQt6Location*' \
--noinclude-dlls='libQt6Pdf*' \
--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 }} \
main.py main.py
- name: Make executable (Unix) - name: Build with Nuitka (macOS)
if: runner.os != 'Windows' if: runner.os == 'macOS'
run: chmod +x dist/${{ env.APP_NAME }}
- name: Rename artifact
shell: bash shell: bash
run: | run: |
cd dist python -m nuitka \
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }} --onefile \
ls -la --onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--noinclude-qt-translations \
--noinclude-dlls='libQt6WebEngine*' \
--noinclude-dlls='libQt6Multimedia*' \
--noinclude-dlls='libQt6Quick*' \
--noinclude-dlls='libQt6Qml*' \
--noinclude-dlls='libQt6Designer*' \
--noinclude-dlls='libQt63D*' \
--noinclude-dlls='libQt6Bluetooth*' \
--noinclude-dlls='libQt6Nfc*' \
--noinclude-dlls='libQt6Sensors*' \
--noinclude-dlls='libQt6Serial*' \
--noinclude-dlls='libQt6Test*' \
--noinclude-dlls='libQt6Location*' \
--noinclude-dlls='libQt6Pdf*' \
--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 }} \
main.py
- name: Prepare artifact
shell: bash
run: |
mkdir -p dist
if [ "$RUNNER_OS" == "Windows" ]; then
mv ${{ env.APP_NAME }}.exe dist/${{ matrix.artifact_name }}
else
mv ${{ env.APP_NAME }} dist/${{ matrix.artifact_name }}
chmod +x dist/${{ matrix.artifact_name }}
fi
ls -la dist/
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -139,7 +223,7 @@ jobs:
release: release:
name: Create Release name: Create Release
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-22.04
if: success() if: success()
steps: steps:
@@ -152,20 +236,52 @@ jobs:
run: | run: |
mkdir -p release mkdir -p release
SHORT_SHA="${{ needs.build.outputs.short_sha }}" SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/ubuntu-latest-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux cp artifacts/ubuntu-22.04-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
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
run: |
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
CHECKSUMS=$(cat release/SHA256SUMS.txt)
cat > release_notes.md << EOF
## Development Build
**Commit:** [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
### Changes
$(git log -1 --pretty=format:"- %s" ${{ github.sha }})
### Downloads
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
### SHA-256 Checksums
\`\`\`
${CHECKSUMS}
\`\`\`
EOF
cat release_notes.md
- name: Create Release (Public) - name: Create Release (Public)
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ needs.build.outputs.version }} tag_name: ${{ needs.build.outputs.version }}
name: ${{ needs.build.outputs.version }} name: ${{ needs.build.outputs.version }}
body_path: release_notes.md
files: | files: |
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 }}
+4
View File
@@ -214,3 +214,7 @@ __marimo__/
*.sublime-project *.sublime-project
*.code-workspace *.code-workspace
*.swp *.swp
# Local build files
build-local.ps1
build-local.bat
+40 -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,19 +14,44 @@ 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
![Alt](https://repobeats.axiom.co/api/embed/3a61ea02ffdeb9ed5dd948d974a21e0d6cfae36c.svg "Repobeats analytics image")
## Star History ## Star History
@@ -38,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:
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+156 -56
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,
@@ -12,7 +15,11 @@ from PySide6.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QSpinBox, QSpinBox,
QCheckBox, QCheckBox,
) QFileDialog,
QComboBox,
QTabWidget,
)
import platform
def settings_dialog(self): def settings_dialog(self):
@@ -20,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, 400) 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()
@@ -32,14 +38,16 @@ def settings_dialog(self):
update_checkbox_layout = QHBoxLayout() update_checkbox_layout = QHBoxLayout()
# ignore updates checkbox # ignore updates checkbox
update_checkbox = QCheckBox()
update_checkbox_container.setLayout(update_checkbox_layout) if platform.system() == "Windows":
update_checkbox_layout.addWidget(QLabel("Ignore Updates: ")) update_checkbox = QCheckBox()
update_checkbox_layout.addStretch() update_checkbox_container.setLayout(update_checkbox_layout)
update_checkbox.setChecked(state.ignore_updates) update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked)) update_checkbox_layout.addStretch()
update_checkbox_layout.addWidget(update_checkbox) update_checkbox.setChecked(state.ignore_updates)
dialog.layout().addWidget(update_checkbox_container) update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
dialog.layout().addWidget(update_checkbox_container)
# auto-resume downloads checkbox # auto-resume downloads checkbox
@@ -52,39 +60,26 @@ def settings_dialog(self):
autoresume_layout.addStretch() autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume) autoresume_checkbox.setChecked(state.autoresume)
autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked))
autoresume_layout.addWidget(autoresume_checkbox) autoresume_layout.addWidget(autoresume_checkbox)
dialog.layout().addWidget(autoresume_container)
##################
# THREAD SETTING #
##################
thread_box = QSpinBox()
thread_box.setMinimum(1)
thread_box.setMaximum(16)
thread_box.setValue(state.aria2_threads)
# container for tight space # transparent window checkbox
thread_container = QWidget() transparent_window_container = QWidget()
thread_layout = QHBoxLayout() transparent_window_layout = QHBoxLayout()
thread_layout.addWidget(QLabel("Threads:")) transparent_window_checkbox = QCheckBox()
thread_layout.addWidget(thread_box) transparent_window_container.setLayout(transparent_window_layout)
thread_container.setLayout(thread_layout) transparent_window_layout.addWidget(QLabel("Window Transparency (requires restart) (Linux/MacOS only): "))
thread_container.setMaximumHeight(80)
# Dimensions transparent_window_layout.addStretch()
thread_box.setFixedWidth(90) transparent_window_checkbox.setChecked(state.window_transparency)
thread_box.setFixedHeight(30) transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox)
dialog.layout().addWidget(thread_container)
################## ##################
# SERVER SETTING # # SERVER SETTING #
################## ##################
api_url_container = QWidget() api_url_container = QWidget()
api_url_layout = QHBoxLayout() api_url_layout = QHBoxLayout()
@@ -94,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 #
@@ -109,25 +103,15 @@ 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():
# SPEED LIMITING # dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
################## if dir_path:
download_path.setText(dir_path)
speed_limit_container = QWidget() browse_button = QPushButton("📁")
speed_limit_layout = QHBoxLayout() download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
speed_limit = QSpinBox()
speed_limit.setMinimum(0)
speed_limit.setMaximum(10000000)
speed_limit.setValue(state.speed_limit)
speed_limit_container.setLayout(speed_limit_layout)
speed_limit_layout.addWidget(speed_limit)
speed_limit.setFixedWidth(180)
speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container)
############### ###############
# IMAGE PATH # # IMAGE PATH #
@@ -142,7 +126,107 @@ 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():
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
if file_path:
image_path.setText(file_path)
browse_button = QPushButton("📁")
image_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_image_path)
##################
# SPEED LIMITING #
##################
down_speed_limit_container = QWidget()
down_speed_limit_layout = QHBoxLayout()
down_speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
down_speed_limit = QSpinBox()
down_speed_limit.setMinimum(0)
down_speed_limit.setMaximum(10000000)
down_speed_limit.setValue(state.down_speed_limit)
down_speed_limit_container.setLayout(down_speed_limit_layout)
down_speed_limit_layout.addWidget(down_speed_limit)
down_speed_limit.setFixedWidth(180)
down_speed_limit.setFixedHeight(30)
up_speed_limit_container = QWidget()
up_speed_limit_layout = QHBoxLayout()
up_speed_limit_layout.addWidget(QLabel("Max Upload Speed (KiB, 0 for unlimited): "))
up_speed_limit = QSpinBox()
up_speed_limit.setMinimum(0)
up_speed_limit.setMaximum(10000000)
up_speed_limit.setValue(state.up_speed_limit)
up_speed_limit_container.setLayout(up_speed_limit_layout)
up_speed_limit_layout.addWidget(up_speed_limit)
up_speed_limit.setFixedWidth(180)
up_speed_limit.setFixedHeight(30)
######################
# CONNECTION CONFIGS #
######################
max_connections_container = QWidget()
max_connections_layout = QHBoxLayout()
max_connections_layout.addWidget(QLabel("Max Connections: "))
max_connections = QSpinBox()
max_connections.setMinimum(0)
max_connections.setMaximum(10000000)
max_connections.setValue(state.max_connections)
max_connections_container.setLayout(max_connections_layout)
max_connections_layout.addWidget(max_connections)
max_connections.setFixedWidth(180)
max_connections.setFixedHeight(30)
####################
# DOWNLOAD CONFIGS #
####################
max_downloads_container = QWidget()
max_downloads_layout = QHBoxLayout()
max_downloads_layout.addWidget(QLabel("Max Downloads: "))
max_downloads = QSpinBox()
max_downloads.setMinimum(0)
max_downloads.setMaximum(10000000)
max_downloads.setValue(state.max_downloads)
max_downloads_container.setLayout(max_downloads_layout)
max_downloads_layout.addWidget(max_downloads)
max_downloads.setFixedWidth(180)
max_downloads.setFixedHeight(30)
#####################
# INTERFACE BINDING #
#####################
interface_container = QWidget()
interface_layout = QHBoxLayout()
interface_label = QLabel("Network Interface:")
interface_layout.addWidget(interface_label)
interface_select = QComboBox()
interface_select.addItems(["None"] + state.interfaces)
target = state.bound_interface if state.bound_interface else "None"
index = interface_select.findText(target)
if index >= 0:
interface_select.setCurrentIndex(index)
else:
interface_select.setCurrentIndex(0)
interface_select.setFixedWidth(180)
interface_select.setFixedHeight(30)
interface_select.setFixedWidth(180)
interface_select.setFixedHeight(30)
interface_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout)
############### ###############
# SAVE/CANCEL # # SAVE/CANCEL #
@@ -152,12 +236,28 @@ def settings_dialog(self):
save_btn = QPushButton("Save") save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel") cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value(), image_path.text())) save_btn.clicked.connect(lambda: save_settings(
close_settings,
api_url.text(),
download_path.text(),
down_speed_limit.value(),
up_speed_limit.value(),
image_path.text(),
autoresume_checkbox.isChecked(),
max_connections.value(),
max_downloads.value(),
interface_select.currentText()))
cancel_btn.clicked.connect(dialog.reject) cancel_btn.clicked.connect(dialog.reject)
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()
+391 -82
View File
@@ -1,13 +1,11 @@
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6 import QtCore from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QTableView, QTableView,
QWidget, QWidget,
QVBoxLayout, QVBoxLayout,
QListWidget, QListWidget,
QToolBar,
QLabel, QLabel,
QHBoxLayout, QHBoxLayout,
QComboBox, QComboBox,
@@ -16,36 +14,38 @@ from PySide6.QtWidgets import (
QHeaderView, QHeaderView,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTextEdit QTextEdit,
) QStyledItemDelegate,
)
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent
import darkdetect import darkdetect
import threading import threading
import platform import platform
import requests as r import requests as r
import os import os
import subprocess import subprocess
import libtorrent as lt
import time import time
import sys import sys
import json import json
from core.utils.general.logs import consoleLog 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
from core.utils.network.update_checker import check_for_updates from core.utils.network.update_checker import check_for_updates
from core.utils.general.shutdown import closehelper
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.network.aria2_integration import dlprogress from core.network.libtorrent_misc import cleanup_session
from core.utils.general.shutdown import closehelper
def download_update(latest_version): def download_update(latest_version):
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
consoleLog("Downloading update...") print("Downloading update...", True)
response = r.get(url, allow_redirects=True) response = r.get(url, allow_redirects=True)
with open(new_filename, "wb") as f: with open(new_filename, "wb") as f:
f.write(response.content) f.write(response.content)
@@ -55,11 +55,11 @@ def download_update(latest_version):
time.sleep(0.5) time.sleep(0.5)
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Information) msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("New Version") msg.setWindowTitle("New Version")
msg.setText("New version installed.") msg.setText("New version installed.")
msg.setInformativeText("Please remove the old exe.") msg.setInformativeText("Please remove the old exe.")
msg.setStandardButtons(QMessageBox.Ok) msg.setStandardButtons(QMessageBox.StandardButton.Ok)
sys.exit(0) sys.exit(0)
@@ -72,6 +72,30 @@ 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):
base_path = os.path.dirname(os.path.abspath(__file__))
asset_path = os.path.join(base_path, 'assets', filename)
if os.path.exists(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)
self.setWindowIcon(QIcon(get_asset_path("logo.png")))
if platform.system() == "Windows":
try:
import ctypes
appid = 'SoftwareManager'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
except Exception as e:
consoleLog(f"Could not set app ID: {e}")
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json") build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
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:
@@ -87,14 +111,14 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if assets: if assets:
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.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.Ok | QMessageBox.Ignore) msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
response = msg.exec_() response = msg.exec_()
if response == QMessageBox.Ok: if response == QMessageBox.StandardButton.Ok:
download_update(latest_version) download_update(latest_version)
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
@@ -111,38 +135,44 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
self.dlbutton = QtWidgets.QPushButton("Download") self.dlbutton = QtWidgets.QPushButton("Download")
self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
self.libraryList = QListWidget() self.libraryList = QListWidget()
self.emptyResults = QLabel("No Results") self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignCenter) self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.emptyResults.hide() self.emptyResults.hide()
self.download_model = None self.download_model = None
self.downloadList = QTableView() self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.") self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload = QLabel("No items in downloads.")
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
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.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"])
header = self.qtablewidget.horizontalHeader() header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(1, QHeaderView.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
header.setStretchLastSection(False) header.setStretchLastSection(False)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground) self.qtablewidget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground) self.qtablewidget.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
header = self.qtablewidget.horizontalHeader() header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
header.resizeSection(1, 500) header.resizeSection(1, 500)
container = QWidget() container = QWidget()
@@ -153,45 +183,180 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
class DownloadModel(QAbstractTableModel): class DownloadModel(QAbstractTableModel):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.headers = ["Name", "Status", "Progress", "Speed", "Size", "Total Size"] self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"]
def rowCount(self, parent=QModelIndex()): def rowCount(self, parent=QModelIndex()):
return len(state.downloads) return len(state.active_downloads)
def columnCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()):
return len(self.headers) return len(self.headers)
def headerData(self, section, orientation, role=Qt.DisplayRole): def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal: if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section] return self.headers[section]
return None return None
def data(self, index, role=Qt.DisplayRole): def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
download = state.downloads[index.row()]
col = index.column() col = index.column()
if index.row() >= len(state.active_downloads) or index.row() < 0:
return None
magnet_link = list(state.active_downloads.keys())[index.row()]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
if col == 0: if col == 0:
return download.name
elif col == 1:
return getattr(download, 'status', 'Downloading')
elif col == 2:
return f"{int(download.progress)}%"
elif col == 3:
return f"{download.download_speed_string()}"
elif col == 4:
pass pass
elif col == 1:
return status.name if status.has_metadata else "Fetching metadata..."
elif col == 2:
if status.state == lt.torrent_status.downloading:
return "Downloading"
elif status.state == lt.torrent_status.seeding:
return "Seeding"
elif status.paused:
return "Paused"
else:
return "Queued"
elif col == 3:
return f"{status.progress * 100:.1f}%"
elif col == 4:
speed_kbs = status.download_rate / 1024
if speed_kbs > 1024:
return f"{speed_kbs / 1024:.1f} MB/s"
else:
return f"{speed_kbs:.1f} kB/s"
elif col == 5: elif col == 5:
return f"{download.total_length_string()}" downloaded_mb = status.total_wanted_done / (1024 * 1024)
if downloaded_mb > 1024:
return f"{downloaded_mb / 1024:.2f} GB"
else:
return f"{downloaded_mb:.1f} MB"
elif col == 6:
total_mb = status.total_wanted / (1024 * 1024)
if total_mb > 1024:
return f"{total_mb / 1024:.2f} GB"
else:
return f"{total_mb:.1f} MB"
elif col == 7:
if status.download_rate > 0:
bytes_left = status.total_wanted - status.total_wanted_done
eta_seconds = bytes_left / status.download_rate
if eta_seconds < 60:
return f"{int(eta_seconds)}s"
elif eta_seconds < 3600:
minutes = int(eta_seconds / 60)
seconds = int(eta_seconds % 60)
return f"{minutes}m {seconds}s"
else:
hours = int(eta_seconds / 3600)
minutes = int((eta_seconds % 3600) / 60)
return f"{hours}h {minutes}m"
else:
return "" if status.paused else "Stalled"
if role == Qt.UserRole and index.column() == 0:
magnet_link = list(state.active_downloads.keys())[index.row()]
magnetdl = state.active_downloads[magnet_link]
return magnetdl.status().paused
return None return None
def toggle_pause_resume(self, row):
if row >= len(state.active_downloads) or row < 0:
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
if status.state == lt.torrent_status.seeding:
save_path = magnetdl.save_path()
if save_path and os.path.exists(save_path):
if platform.system() == "Windows":
os.startfile(os.path.normpath(save_path))
elif platform.system() == "Linux":
subprocess.Popen(["xdg-open", save_path])
elif platform.system() == "Darwin":
subprocess.Popen(["open", save_path])
return
is_paused = bool(magnetdl.flags() & lt.torrent_flags.paused)
if is_paused:
magnetdl.set_flags(lt.torrent_flags.auto_managed)
magnetdl.resume()
consoleLog(f"Resumed download: {status.name}", True)
else:
magnetdl.unset_flags(lt.torrent_flags.auto_managed)
magnetdl.pause()
consoleLog(f"Paused download: {status.name}", True)
idx = self.index(row, 0)
self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int)
def setEditorData(self, editor, index):
button = editor.findChild(QtWidgets.QPushButton)
magnet_link = list(state.active_downloads.keys())[index.row()]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
if button:
if status.state == lt.torrent_status.seeding:
button.setText("📁")
else:
button.setText("▶︎" if status.paused else "⏸︎")
def createEditor(self, parent, option, index):
magnet_link = list(state.active_downloads.keys())[index.row()]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
widget = QWidget(parent)
widget.setStyleSheet("border: none;")
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
if status.state == lt.torrent_status.seeding:
btnPause = QtWidgets.QPushButton("📁")
else:
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎")
btnPause.setFixedSize(40, 30)
btnPause.clicked.connect(lambda: self.clicked.emit(index.row()))
layout.addStretch()
layout.addWidget(btnPause)
layout.addStretch()
widget.setLayout(layout)
return widget
def editorEvent(self, event, model, option, index):
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
self.clicked.emit(index.row())
return True
return False
self.download_model = DownloadModel() self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model) self.downloadList.setModel(self.download_model)
self.downloadList.horizontalHeader().setStretchLastSection(False) self.downloadList.horizontalHeader().setStretchLastSection(False)
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.downloadList.setSelectionBehavior(QTableView.SelectRows) self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.downloadList.setAttribute(Qt.WA_TranslucentBackground) self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground) self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
download_model = self.download_model
def on_pause_resume_clicked(row):
download_model.toggle_pause_resume(row)
delegate = PauseResumeDelegate(self.downloadList)
self.downloadList.setItemDelegateForColumn(0, delegate)
delegate.clicked.connect(on_pause_resume_clicked)
# download button triggers # download button triggers
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles)))) self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
@@ -201,66 +366,110 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
# Tabs # Tabs
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.tabs.setStyleSheet("""
QTabBar::tab {
font-size: 11px;
padding: 2px 8px;
height: 25px;
}
QTabWidget::pane {
margin-top: -20px;
}
""")
self.horizontal_layout = QHBoxLayout() self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(self.qtablewidget) self.horizontal_layout.addWidget(self.qtablewidget)
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout) self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None) # self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
self.image = QImage(state.image_path) if state.image_path is not None and os.path.exists(state.image_path):
self.pixmap = QPixmap.fromImage(self.image) self.image = QImage(state.image_path)
self.overlay_label = QLabel(self) self.image = self.image.scaledToWidth(300, Qt.SmoothTransformation)
self.overlay_label.setPixmap(self.pixmap) self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.adjustSize() self.overlay_label = QLabel(self)
self.overlay_label.raise_() self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
offset_x = -1350 self.overlay_label.raise_()
offset_y = -550
x = self.width() - self.overlay_label.width() - offset_x
y = self.height() - self.overlay_label.height() - offset_y
self.overlay_label.move(x, y)
# offset_x = -1350
# offset_y = -550
x = self.width() - self.overlay_label.width() # - offset_x
y = self.height() - self.overlay_label.height() # - offset_y
self.overlay_label.move(x, y)
# temporarily disabled # temporarily disabled
# state.image_changed.connect(self.update_image_overlay) # state.image_changed.connect(self.update_image_overlay)
containerLayout.addWidget(self.tabs)
self.corner_widget = QWidget()
self.corner_layout = QHBoxLayout(self.corner_widget)
self.corner_layout.setContentsMargins(0, 0, 0, 0)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft)
self.tracker_list = QComboBox() self.tracker_list = QComboBox()
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"]) self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
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.settings_btn = QtWidgets.QToolButton()
self.settings_btn.setIconSize(QSize(21, 21))
self.settings_btn.setStyleSheet("background: transparent;")
if darkdetect.isDark(): if darkdetect.isDark():
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self) self.settings_btn.setIcon(QIcon(get_asset_path("settings_white.png")))
else: else:
settings_action = QAction(QIcon("core/interface/assets/settings_black.png"), "Settings", self) self.settings_btn.setIcon(QIcon(get_asset_path("settings_black.png")))
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)
settings_action.triggered.connect(lambda: settings_dialog(self)) self.tab_wrapper = QWidget()
toolbar.addAction(settings_action) self.tab_layout = QVBoxLayout()
toolbar.addWidget(self.tracker_list) self.tab_layout.setContentsMargins(0, 0, 0, 0)
self.tab_layout.setSpacing(0)
toolbar.setMovable(False) self.tab_bar_layout = QHBoxLayout()
self.tab_bar_layout.setContentsMargins(0, 0, 0, 0)
self.tab_bar_layout.setSpacing(0)
self.tab_bar_layout.addWidget(self.tabs.tabBar(), stretch=0)
self.tab_bar_layout.addStretch()
self.tab_bar_layout.addWidget(self.corner_widget, stretch=0)
self.tab_layout.addLayout(self.tab_bar_layout)
self.tab_layout.addWidget(self.tabs)
self.tab_wrapper.setLayout(self.tab_layout)
containerLayout.addWidget(self.tab_wrapper)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
# self.progressbar.setValue(0)
self.consoleLog.setReadOnly(True) self.consoleLog.setReadOnly(True)
self.consoleLog.setFixedHeight(150) self.consoleLog.setFixedHeight(150)
containerLayout.addWidget(self.consoleLog) containerLayout.addWidget(self.consoleLog)
self.progress_timer = QTimer()
self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress)))
self.progress_timer.start(1000)
self.download_timer = QTimer() self.download_timer = QTimer()
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update))) self.download_timer.timeout.connect(self.download_list_update)
self.download_timer.start(500) self.download_timer.start(500)
self.active_timer = QTimer()
self.active_timer.timeout.connect(self.show_empty_downloads)
self.active_timer.start(500)
self.context_menu = QtWidgets.QMenu(self)
self.context_menu.addAction("Open Containing Folder", self.openFolderAction)
self.context_menu.addAction("Copy Magnet URI", self.copyMagnetURIAction)
self.context_menu.addAction("Cancel Download", self.cancelDownloadAction)
self.context_menu.addAction("Delete File", self.deleteFileAction)
@staticmethod @staticmethod
def add_log(text): def add_log(text):
if MainWindow._instance: if MainWindow._instance:
@@ -283,6 +492,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if self.download_model: if self.download_model:
self.download_model.layoutAboutToBeChanged.emit() self.download_model.layoutAboutToBeChanged.emit()
self.download_model.layoutChanged.emit() self.download_model.layoutChanged.emit()
for row in range(self.download_model.rowCount()):
idx = self.download_model.index(row, 0)
self.downloadList.closePersistentEditor(idx)
self.downloadList.openPersistentEditor(idx)
def closeEvent(self, event: QCloseEvent): def closeEvent(self, event: QCloseEvent):
closehelper() closehelper()
@@ -291,18 +505,21 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def set_tracker(self, _): def set_tracker(self, _):
state.tracker = self.tracker_list.currentText() state.tracker = self.tracker_list.currentText()
def update_progress(self):
progress = dlprogress()
self.progressbar.setValue(progress)
def show_empty_results(self, show: bool): def show_empty_results(self, show: bool):
if show: if show:
self.qtablewidget.hide() self.qtablewidget.hide()
self.emptyResults.show() self.emptyResults.show()
else: else:
self.emptyResults.hide()
self.qtablewidget.show() self.qtablewidget.show()
self.emptyResults.hide()
def show_empty_downloads(self):
if len(state.active_downloads) > 0:
self.emptyDownload.hide()
self.downloadList.show()
else:
self.emptyDownload.show()
self.downloadList.hide()
# thank you claude # thank you claude
def resizeEvent(self, event): def resizeEvent(self, event):
@@ -310,6 +527,98 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
table_width = self.qtablewidget.viewport().width() table_width = self.qtablewidget.viewport().width()
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3)) self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
def contextMenuEvent(self, event: QContextMenuEvent):
if self.downloadList.underMouse():
pos = self.downloadList.viewport().mapFromGlobal(event.globalPos())
index = self.downloadList.indexAt(pos)
if index.isValid():
row = index.row()
self._context_menu_row = row
self.context_menu.exec(event.globalPos())
else:
super().contextMenuEvent(event)
def openFolderAction(self):
if not hasattr(self, '_context_menu_row'):
return
row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads):
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
download_path = magnetdl.save_path()
if download_path and os.path.exists(download_path):
if platform.system() == "Windows":
os.startfile(os.path.normpath(download_path))
elif platform.system() == "Linux":
subprocess.Popen(["xdg-open", download_path])
elif platform.system() == "Darwin":
subprocess.Popen(["open", download_path])
def copyMagnetURIAction(self):
if not hasattr(self, '_context_menu_row'):
return
row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads):
return
magnet_link = list(state.active_downloads.keys())[row]
clipboard = QtWidgets.QApplication.clipboard()
clipboard.setText(magnet_link)
def cancelDownloadAction(self):
if not hasattr(self, '_context_menu_row'):
return
row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads):
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
confirm = QMessageBox.question(self, "Cancel Download", f"Are you sure you want to cancel the download of '{magnetdl.status().name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
consoleLog(f"Cancelled download: {magnetdl.status().name}", True)
def deleteFileAction(self):
if not hasattr(self, '_context_menu_row'):
return
row = self._context_menu_row
if row < 0 or row >= len(state.active_downloads):
return
magnet_link = list(state.active_downloads.keys())[row]
magnetdl = state.active_downloads[magnet_link]
status = magnetdl.status()
save_path = status.save_path
torrent_name = status.name
download_path = os.path.join(save_path, torrent_name)
confirm = QMessageBox.question(self, "Delete Files", f"Are you sure you want to delete the downloaded files of '{magnetdl.status().name}'? This action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if confirm == QMessageBox.StandardButton.Yes:
if download_path and os.path.exists(download_path):
try:
if os.path.isfile(download_path):
os.remove(download_path)
del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
else:
import shutil
shutil.rmtree(download_path)
del state.active_downloads[magnet_link]
remove_download_log(magnet_link)
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
except Exception as e:
consoleLog(f"Error deleting files: {e}", True)
+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"])
+40 -1
View File
@@ -12,6 +12,45 @@ def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
layout.addWidget(software_list) layout.addWidget(software_list)
if dlbutton: if dlbutton:
layout.addWidget(dlbutton) layout.addWidget(dlbutton)
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
-92
View File
@@ -1,92 +0,0 @@
import aria2p
import subprocess
from core.utils.data.state import state
from core.utils.general.logs import update_download_completed_by_hash
from plyer import notification
import time
import sys
def run_aria2p():
state.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
return state.aria2
def aria2server():
download_path = state.download_path
speed_limit = state.speed_limit
cmd = [
"aria2c",
"--enable-rpc",
"--disable-ipv6", # added this since it caused problems with vpns
"--rpc-listen-all",
"--rpc-listen-port=6800",
f"--dir={download_path}",
f"--max-download-limit={speed_limit * 1024}",
"-x", str(state.aria2_threads),
"-s", str(state.aria2_threads),
]
aria2server = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
)
return aria2server
def dlprogress():
try:
state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata]
downloads = state.downloads
if downloads:
for download in downloads:
progress = download.progress_string(0)
progress_int = int(progress.strip('%'))
return progress_int
return 0
except:
return 0
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
try:
for d in state.aria2.get_downloads():
if d.is_metadata:
continue
if d.progress == 100 and d.gid not in notified:
notification.notify(
title="Download finished",
message=f"{d.name} has finished downloading.",
timeout=4
)
notified.add(d.gid)
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
except Exception:
pass
time.sleep(5)
def update_log(shutdown_event):
updated = set()
while not shutdown_event.is_set():
try:
for d in state.aria2.get_downloads():
if d.is_metadata:
continue
if d.progress == 100 and d.gid not in updated:
consoleLog(f"Marking {d.name} as completed")
update_download_completed_by_hash(d.info_hash, True)
updated.add(d.gid)
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
except Exception:
pass
time.sleep(5)
-15
View File
@@ -1,15 +0,0 @@
from .aria2_integration import run_aria2p
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
def start_client():
global aria2
aria2 = run_aria2p()
consoleLog("Started Aria2p")
def add_magnet(uri):
aria2.add_magnet(uri)
consoleLog("Magnet URI added to Aria2")
+52
View File
@@ -0,0 +1,52 @@
from core.utils.general.logs import consoleLog
from core.utils.data.state import state
import psutil
addrs = psutil.net_if_addrs()
stats = psutil.net_if_stats()
def get_net_interfaces():
for interface in addrs.keys():
consoleLog(f"Found Interface: {interface}")
return addrs.keys()
def get_active_interfaces():
active = []
for interface, addr_list in addrs.items():
up = stats[interface].isup
for addr in addr_list:
if addr.family == 2: # ipv4
ipv4 = addr.address
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
active.append(interface)
consoleLog(f"Found Active: {interface}")
return active
def list_interfaces() -> None:
for interface, addr_list in addrs.items():
up = stats[interface].isup
for addr in addr_list:
if addr.family == 2: # ipv4
ipv4 = addr.address
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
status = "ACTIVE"
else:
status = "INACTIVE"
consoleLog(f"Found Interface: {interface} [{status}]")
def init_interfaces():
state.interfaces = list(addrs.keys())
state.active_interfaces = get_active_interfaces()
def get_interface_ip(interface_name):
if interface_name in addrs:
for addr in addrs[interface_name]:
if addr.family == 2: # ipv4
if not addr.address.startswith("127.") and not addr.address.startswith("169.254"):
return addr.address
return None
+126
View File
@@ -0,0 +1,126 @@
import time
from core.utils.general.wrappers import run_thread
from core.network.interface import get_interface_ip
import threading
import libtorrent as lt
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
global loop_running
loop_running = False
def init_session():
if state.dl_session is not None:
return
state.dl_session = lt.session()
settings = {
"upload_rate_limit": state.up_speed_limit,
"download_rate_limit": state.down_speed_limit,
"enable_dht": True,
"enable_lsd": True,
"enable_upnp": True,
"enable_natpmp": True,
"dht_bootstrap_nodes": "router.bittorrent.com:6881,dht.transmissionbt.com:6881",
"connections_limit": state.max_connections,
"active_downloads": state.max_downloads
}
if state.bound_interface:
interface_ip = get_interface_ip(state.bound_interface)
if interface_ip:
settings["outgoing_interfaces"] = interface_ip
settings["listen_interfaces"] = f"{interface_ip}:6881"
consoleLog(f"Binding to Interface IP: {interface_ip}")
else:
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
state.dl_session.apply_settings(settings)
consoleLog("Initialized Session")
def add_download(magnet_uri, dl_path=state.download_path):
if state.active_downloads is None:
state.active_downloads = {}
init_session()
if magnet_uri in state.active_downloads:
consoleLog("Skipping Downloading, download already running...")
return
magnetdl = lt.parse_magnet_uri(magnet_uri)
magnetdl.save_path = dl_path
download = state.dl_session.add_torrent(magnetdl)
state.active_downloads[magnet_uri] = download
consoleLog(f"Added {magnet_uri} to downloads")
run_thread(threading.Thread(target=dl_status_loop))
def dl_status_loop():
global loop_running
if loop_running == True:
return
loop_running = True
completed_set = set()
if not state.active_downloads:
consoleLog("No active downloads")
loop_running = False
return
while state.active_downloads:
for magnet_uri, magnetdl in list(state.active_downloads.items()):
status = magnetdl.status()
if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set:
consoleLog(f"Download completed: {status.name}")
completed_set.add(magnet_uri)
if not state.active_downloads:
loop_running = False
break
time.sleep(1)
loop_running = False
def update_settings():
if state.dl_session is None:
return
settings = {
"upload_rate_limit": state.up_speed_limit,
"download_rate_limit": state.down_speed_limit,
"connections_limit": state.max_connections,
"active_downloads": state.max_downloads
}
if state.bound_interface:
interface_ip = get_interface_ip(state.bound_interface)
if interface_ip:
settings["outgoing_interfaces"] = interface_ip
settings["listen_interfaces"] = f"{interface_ip}:6881"
consoleLog(f"Binding to Interface IP: {interface_ip}")
else:
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
state.dl_session.apply_settings(settings)
def update_bound_interface():
settings = { "outgoing_interfaces": state.bound_interface }
state.dl_session.apply_settings(settings)
+60
View File
@@ -0,0 +1,60 @@
from core.utils.data.state import state
from core.utils.general.logs import update_download_completed_by_hash
from core.utils.general.logs import consoleLog
from plyer import notification
import libtorrent as lt
import time
def cleanup_session():
if state.dl_session is not None:
for magnetdl in state.active_downloads.values():
if hasattr(magnetdl, 'pause'): # Check it's a handle
magnetdl.pause()
del state.dl_session
state.dl_session = None
state.active_downloads.clear()
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
try:
for magnet_uri, magnetdl in list(state.active_downloads.items()):
if isinstance(magnetdl, dict):
continue
status = magnetdl.status()
if status.state == lt.torrent_status.seeding and magnet_uri not in notified:
notification.notify(
title="Download finished",
message=f"{status.name} has finished downloading.",
timeout=4
)
notified.add(magnet_uri)
except Exception:
pass
time.sleep(5)
def update_log(shutdown_event):
updated = set()
while not shutdown_event.is_set():
try:
for magnet_uri, magnetdl in list(state.active_downloads.items()):
if isinstance(magnetdl, dict):
continue
status = magnetdl.status()
if status.state == lt.torrent_status.seeding and magnet_uri not in updated:
consoleLog(f"Marking {status.name} as completed")
info_hash = str(status.info_hash)
update_download_completed_by_hash(info_hash, True)
updated.add(magnet_uri)
except Exception:
pass
time.sleep(5)
+9
View File
@@ -0,0 +1,9 @@
from core.utils.general.logs import consoleLog
from core.network.libtorrent_int import add_download
def add_magnet(uri):
if uri is not None and uri.startswith("magnet:?"):
add_download(uri)
consoleLog("Magnet URI added to LibTorrent")
else:
consoleLog(f"Invalid Magnet Link: {uri}")
+36 -9
View File
@@ -6,8 +6,26 @@ from core.utils.data.state import state
def create_config(): def create_config():
config = configparser.ConfigParser() config = configparser.ConfigParser()
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}", config["General"] = {
"ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}"} "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}",
"download_path": f"{state.download_path}",
"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"] = {
"bound_interface": f"{state.bound_interface}",
"image_path": f"{state.image_path}"
}
if platform.system() == "Windows": if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming")) config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
@@ -37,14 +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.aria2_threads = config.getint("General", "aria2_threads", fallback=state.aria2_threads)
state.download_path = config.get("General", "download_path", fallback=state.download_path)
state.speed_limit = config.getint("General", "speed_limit", fallback=state.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.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
create_config() # 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()
+16 -18
View File
@@ -1,33 +1,31 @@
from core.network.libtorrent_int import update_settings
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.shutdown import kill_aria2server from core.utils.general.logs import consoleLog
from .config import create_config from .config import create_config
def restart_aria2c():
import main # had to do this because of circle import :(
import signal
import atexit
kill_aria2server()
state.aria2process.wait()
state.aria2process = main.run_aria2server()
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None, autoresume=None): 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 thread_count is not None:
state.aria2_threads = thread_count
if apiurl is not None: if apiurl is not None:
state.api_url = apiurl state.api_url = apiurl
if download_path is not None: if download_path is not None:
state.download_path = download_path state.download_path = download_path
if speed_limit is not None: if down_speed_limit is not None:
state.speed_limit = speed_limit 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: if image_path is not None:
state.image_path = image_path state.image_path = image_path
if autoresume is not None: if autoresume is not None:
state.autoresume = autoresume 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() create_config()
restart_aria2c()
close() close()
+13 -7
View File
@@ -11,7 +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.downloads: 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
@@ -20,12 +21,17 @@ class AppState(QObject):
self.tracker: str = "rutracker" self.tracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz" self.api_url: str = "https://api.michijackson.xyz"
self.download_path: str = str(Path.home() / "Downloads") self.download_path: str = str(Path.home() / "Downloads")
self.speed_limit: int = 0 self.up_speed_limit: int = 0
self.aria2process: Optional[Any] = None self.down_speed_limit: int = 0
self.aria2: Any = None self.max_connections: int = 200
self.aria2p: Any = None self.max_downloads: int = 10
self.aria2_threads: int = 4 self.settings_path: str = ""
self.settings_path: str = None self.dl_session: Any = None
self.active_downloads: List[str] = {}
self.window_transparency: bool = False
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: str = None
@property @property
def image_path(self) -> str: def image_path(self) -> str:
+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]
-1
View File
@@ -1,4 +1,3 @@
from core.utils.data.state import state
from core.utils.general.logs import consoleLog from core.utils.general.logs import consoleLog
from core.utils.network.download import run_download_direct from core.utils.network.download import run_download_direct
+50 -6
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)
@@ -41,6 +48,30 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
return download_list return download_list
def remove_download_log(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()
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 {magnet_link} 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: def update_download_completed(magnet_uri, completed) -> DownloadList:
downloads_file = os.path.join(state.settings_path, "downloads.json") downloads_file = os.path.join(state.settings_path, "downloads.json")
@@ -165,15 +196,28 @@ def set_main_window(window):
global _main_window global _main_window
_main_window = window _main_window = window
def consoleLog(text): 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):
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: if state.debug or printAnyways:
print(f"[{current_time}] {text}") print(formatted_text)
+4 -25
View File
@@ -1,30 +1,9 @@
from core.utils.data.state import state
from core.utils.general.logs import consoleLog
import threading import threading
import psutil from core.network.libtorrent_misc import cleanup_session
import sys from core.utils.general.wrappers import run_thread
shutdown_event = threading.Event() shutdown_event = threading.Event()
def kill_aria2server():
if sys.platform.startswith("win"):
process = "aria2c.exe"
else:
process = "aria2c"
if state.aria2process:
state.aria2process.kill()
consoleLog("Killed Aria2")
try:
for proc in psutil.process_iter():
if proc.name() == process:
proc.kill()
consoleLog(f"Killed Aria2c (PID {proc.pid})")
except psutil.NoSuchProcess:
pass
def closehelper(): def closehelper():
shutdown_event.set() run_thread(threading.Thread(target=cleanup_session))
kill_aria2server() shutdown_event.set()
+4 -6
View File
@@ -2,8 +2,7 @@ from core.utils.data.state import state
from core.utils.general.logs import consoleLog from core.utils.general.logs import consoleLog
from core.utils.data.tracker import get_item_url from core.utils.data.tracker import get_item_url
from core.utils.data.tracker import get_magnet_link from core.utils.data.tracker import get_magnet_link
from core.network.aria2_wrapper import start_client from core.network.libtorrent_wrapper import add_download
from core.network.aria2_wrapper import add_magnet
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.general.logs import add_download_log from core.utils.general.logs import add_download_log
@@ -21,13 +20,12 @@ def run_download(item, posts, post_titles):
post_url = get_item_url(item, posts, post_titles) post_url = get_item_url(item, posts, post_titles)
consoleLog(f"Selected URL: {post_url}") consoleLog(f"Selected URL: {post_url}")
magnet_uri = get_magnet_link(post_url) magnet_uri = get_magnet_link(post_url)
start_client()
add_download_log(item, post_url, magnet_uri, False) add_download_log(item, post_url, magnet_uri, False)
add_magnet(magnet_uri) add_download(magnet_uri)
def run_download_direct(magnet_uri): def run_download_direct(magnet_uri):
start_client() add_download(magnet_uri)
add_magnet(magnet_uri)
+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
+26 -18
View File
@@ -1,59 +1,67 @@
from core.interface.gui import MainWindow from core.interface.gui import MainWindow
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.logs import consoleLog from core.utils.general.logs import consoleLog
from core.network.aria2_integration import aria2server from core.network.libtorrent_misc import send_notification, update_log
from core.network.aria2_integration import send_notification, update_log
from core.utils.general.logs import get_download_logs from core.utils.general.logs import get_download_logs
from core.utils.general.shutdown import closehelper, shutdown_event from core.utils.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.general.loghandler import split_data, check_completed from core.utils.general.loghandler import split_data, check_completed
from core.network.interface import list_interfaces, init_interfaces
from core.network.libtorrent_int import update_bound_interface
from core.utils.config.config import read_config from core.utils.config.config import read_config
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import Qt
import qdarktheme import qdarktheme
import darkdetect
import threading import threading
import platform
import signal import signal
import argparse import argparse
import sys import sys
import time
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true") parser.add_argument("--debug", action="store_true")
args = parser.parse_args() args = parser.parse_args()
def run_gui(): def run_gui():
app = QtWidgets.QApplication([]) app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
if darkdetect.isDark: qdarktheme.setup_theme("auto")
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
else:
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
widget = MainWindow() widget = MainWindow()
if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
from core.utils.general.logs import set_main_window from core.utils.general.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 run_aria2server():
aria2process = aria2server()
return aria2process
def keyboardinterrupthandler(signum, frame): 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
consoleLog("Starting Aria2 Server")
state.aria2process = run_aria2server()
signal.signal(signal.SIGINT, keyboardinterrupthandler) signal.signal(signal.SIGINT, keyboardinterrupthandler)
consoleLog("Starting SoftwareManager...")
consoleLog("Fetching Network Interfaces...")
list_interfaces()
consoleLog("Initializing Interface variables...")
init_interfaces()
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))
consoleLog("Started Thread: send_notification")
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")
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()
+6 -5
View File
@@ -1,10 +1,11 @@
requests==2.32.2 requests==2.32.2
PySide6==6.9.2 PySide6-Essentials==6.10.1
beautifulsoup4==4.13.5 beautifulsoup4==4.13.5
darkdetect==0.8.0 darkdetect==0.7.1
aria2p==0.12.1
pyinstaller==6.15.0 pyinstaller==6.15.0
pyqtdarktheme==0.1.7 PyQtDarkTheme-fork==2.3.4
aiohttp==3.13.0 aiohttp==3.13.0
plyer==2.1.0 plyer==2.1.0
psutil==7.1.0 psutil==7.1.0
libtorrent==2.0.11
libtorrent-windows-dll==0.0.3