Compare commits

...

37 Commits

Author SHA1 Message Date
Vxrtrauter 912d9d8c00 fix: Add shutdown helper call and modify exit behavior in updater 2026-04-07 02:26:15 +02:00
Vxrtrauter ce2cd9cdd8 Update release workflow to use RELEASE_TOKEN instead of GITHUB_TOKEN 2026-04-07 01:48:59 +02:00
Vxrtrauter 5e6e96a8cd Add dynamic release name using short SHA in release workflow 2026-04-07 01:45:26 +02:00
Vxrtrauter 8161e28d67 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-04-07 01:13:45 +02:00
Vxrtrauter 6253aa0eb0 Add release workflow and simplify dev workflow
Remove automatic main push trigger from dev workflow (now only workflow_dispatch) and add a new release workflow (.github/workflows/release.yml). The release workflow triggers on tag pushes, builds artifacts for Linux, Windows and macOS using a matrix, sets up Python/uv, installs build tools (Nuitka/PyInstaller/UPX where needed), injects a short commit SHA into build_info.json, packages artifacts, uploads them, generates checksums, and creates a GitHub release with the built assets.
2026-04-07 01:13:38 +02:00
shayaa c063c226db Merge pull request #53 from KeksPirates/fix/image-resizing
Fix/image resizing, add accent color support
2026-04-07 01:09:02 +02:00
Vxrtrauter 929fdf69e5 Update and revert image path in settings dialog
Capture the original state.image_path at dialog start, connect the Image Path QLineEdit's textChanged signal to update state.image_path immediately, and restore the original image_path if the settings dialog is cancelled. This matches existing undo behavior for width/offset/opacity and prevents partial image path edits from persisting when the dialog is rejected.
2026-04-07 01:07:22 +02:00
shayaa 48b9919b7a Merge pull request #52 from KeksPirates/feat/docs
Feat/docs
2026-04-06 23:15:04 +02:00
Vxrtrauter c43fd006fd Add About page and expand docs content/styles
Add a new docs/about page (docs/docs.html) with site header/footer and basic About layout. Expand the Docs tab in docs/index.html with a Features section, General Information, and Sources table (including a link to the server repo and usage notes). Update docs/main.css to add styles for .sources-text, .docs-grid, .docs-tips and headings to support the new documentation layout and improve visual presentation.
2026-04-05 08:27:41 +02:00
Vxrtrauter 12506dd1df feat: Add customizable accent color setting
Introduce an accent color preference across the app: add a settings UI input (with restart note), persist it to AppState and config (read/write), and accept it in save_settings. Integrate the value into theming: compute a semi-transparent selection color, apply it to table selection styles, and pass it to qdarktheme as the primary color (including when window transparency is enabled). Also apply the accent to the tracker combo selection styling as a UI polish.
2026-04-05 06:57:00 +02:00
Vxrtrauter 9e3d1cf90a feat: Add image position presets and sliders
Introduce image positioning presets and UI improvements for image overlay control. image.py: compute overlay coordinates for top-left/top-right/bottom-left/bottom-right/center positions and respect corner offset and opacity. settings.py: replace spinboxes with horizontal sliders (with value labels and custom style), add Wallpaper Mode checkbox and Position combo, wire controls to state and include them in the settings tab; ensure dialog rollback restores new temp settings. config/config.py & utils/settings.py: persist and load new image_as_wallpaper and image_position settings and extend save_settings to accept them. data/state.py: add new state fields and properties for image_position, custom position flags and x/y coordinates; change default image_as_wallpaper to false. These changes enable preset positioning, improved UX for sizing/offset/opacity, and persistence of new image options.
2026-04-05 06:12:54 +02:00
Vxrtrauter 2ad02435a1 feat/fix: "wallpaper" transparency handling
Introduce dynamic wallpaper transparency support for the image overlay. Adds QStackedWidget import and a new _set_wallpaper_transparency method that toggles WA_TranslucentBackground on the main window and key child widgets, updates stylesheets (with originals preserved/restored), and uses darkdetect to adapt combo popup background. Adds state fields (_wallpaper_active, _original_stylesheets), sets overlay opacity from state, and calls the transparency toggler when loading/hiding the overlay to avoid redundant updates.
2026-04-05 05:22:24 +02:00
Vxrtrauter f11837217c feat: add basic "wallpaper" functionality 2026-04-05 04:02:10 +02:00
Vxrtrauter 1a7de70c0b feat: add image opacity, config migration, settings
Add image overlay opacity support and related UI/settings, plus robust config migration and error handling.

Key changes:
- UI: image overlay now uses QGraphicsOpacityEffect, hides when disabled, and updates opacity on state changes (src/interface/dialogs/image.py).
- New NotificationPopup dialog for user alerts (src/interface/dialogs/notificationpopup.py).
- Settings dialog: added Image Opacity control, adjusted labels, persist/rollback behavior on cancel, and include opacity when saving (src/interface/dialogs/settings.py).
- State: added properties for image_opacity and image_enabled with change notifications; renamed image_width setter param for clarity (src/utils/data/state.py).
- Config: switch from config.yml to config.ini, implement one-time migration, typed read/write of values, validation/error handling, automatic backups of corrupted files and user notification (src/utils/config/config.py).
- Settings save: accept and persist image_opacity (src/utils/config/settings.py).
- Startup: initialize QApplication earlier and pass app to run_gui (src/main.py).
- Minor: removed an extra blank line in log handler and small import cleanups.

These changes enable adjustable overlay transparency, improve config reliability, and surface errors to users while preserving/backuping old configs.
2026-04-04 07:36:03 +02:00
Vxrtrauter a3ff23147f Restore image settings on cancel; live sync
Save original image_width/image_offset when opening Settings and restore them if the dialog is cancelled. Add valueChanged handlers to immediately update state.image_width and state.image_offset, enforce an image width maximum of 2500, and refactor Save wiring to a dedicated handler that calls save_settings(dialog.accept, ...). Connect dialog.finished to restore temp values on rejection and clean up dialog layout/logic.
2026-04-04 05:36:16 +02:00
Vxrtrauter fa4e753861 fix: braces in api docs 2026-04-04 04:54:58 +02:00
Vxrtrauter b0bbb98927 Convert docs to single-page tabs and update styles
Merge API and About pages into a single SPA-style index: removed docs/about.html and docs/api.html and moved their content into docs/index.html as tabbed sections (start, api, docs, notfound). Add client-side routing and history handling via a new script to support deep links and pushState, plus a Netlify-style docs/_redirects rule to serve index for all routes. Update styles in docs/main.css to target the new tab UI (.tab, .tablinks, .logo-text) and add styles for the 404 view. Minor markup and layout tweaks (images, buttons, response examples) to match the consolidated page.
2026-04-04 04:52:40 +02:00
KeksNino fda45efcb5 refactor: create a helper function to create widgets in settings dialog 2026-04-04 03:52:20 +02:00
KeksNino 3de2110a3e fix: redraws pixmap after changing image settings so restart is no longer required 2026-04-04 02:51:45 +02:00
Vxrtrauter cb7e74a224 Add configurable image overlay settings
Introduce configurable image overlay options and persist them. Added new state fields (image_width, image_offset, image_enabled) with defaults and wired them into the image overlay: the image display now respects the enabled flag, uses image_width for scaling and image_offset for placement. Extended the Settings dialog (resized, API field sizing) with a new Image tab containing enable checkbox, width and offset spinboxes, and included these values when saving. Updated config read/write to include an Image section so settings persist across runs and extended save_settings to accept image_width and image_offset. Minor import/order cleanup in image dialog.
2026-04-04 02:06:06 +02:00
Vxrtrauter 5fe3502bd6 add initial website structure 2026-03-23 21:36:21 +01:00
KeksNino b59768bcff fix image resizing and general code of image class 2026-03-22 01:41:16 +01:00
shayaa d4b7067484 Merge pull request #51 from KeksPirates/refactor/improve-code-quality
Refactor/improve code quality
2026-03-21 02:50:07 +01:00
Vxrtrauter 77c2e9cd9e fix: tab layout, clear search state, and cleanup
UI: remove trackertable from the Search tab and reorder widgets to correct layout. Tab helper: add QLayout directly instead of wrapping it in a temporary QWidget to avoid extra nesting. Search helper: clear the trackertable rows and reset state.posts when starting a new search to ensure fresh results. Minor: import check_space in main and simplify an unused loop variable in libtorrent_int for clarity/linting.
2026-03-21 02:48:48 +01:00
Vxrtrauter 290388a7dc Merge branch 'main' into refactor/improve-code-quality 2026-03-21 02:48:04 +01:00
Vxrtrauter 9e7607dd36 refactor: get_magnet to get_download_link
Rename scraper method get_magnet -> get_download_link in monkrus, rutracker and uztracker. Update downloader code to call .get_download_link and use the .is_magnet flag on tracker objects (replace dict access). Switch direct_download package imports to absolute paths and perform a small import reorder in direct_download/utils.py. These changes unify the download API and adapt call sites to the new tracker interface.
2026-03-21 02:03:49 +01:00
KeksNino 616552063d fix gui not starting, caused by tabhelper 2026-03-21 01:47:46 +01:00
KeksNino 9eb70af1a1 refactored tabhelper function 2026-03-21 01:14:25 +01:00
Vxrtrauter 4d4187ce63 fix: revert update.py changes 2026-03-21 01:09:31 +01:00
KeksNino 23e44cb128 remove qt widget from updater.py and use class instead 2026-03-21 00:39:28 +01:00
Vxrtrauter 0a591e1cda refactor: update progress dialog and tidy UI 2026-03-21 00:27:56 +01:00
Vxrtrauter c0f5060451 fix: searchbar disabling itself when searching for nothing 2026-03-20 22:51:28 +01:00
Vxrtrauter c6940f0178 fix: add self argument to function 2026-03-20 02:26:42 +01:00
Vxrtrauter 2afb3e7716 refactor: improve scraper handling by utilizing classes for registration 2026-03-20 02:22:40 +01:00
Vxrtrauter 325eda54fa refactor: remove core/ folder 2026-03-19 20:42:28 +01:00
Vxrtrauter 53d4a240a0 refactor state.py and add comments to it, add another check to version getter. 2026-03-18 23:33:33 +01:00
Vxrtrauter 2beabbb1ba refactor: improve search functionality and code organization in GUI components 2026-03-16 14:24:23 +01:00
75 changed files with 2327 additions and 1007 deletions
+1 -4
View File
@@ -1,9 +1,6 @@
name: Build Executables (Dev)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
@@ -106,7 +103,7 @@ jobs:
run: |
python -c "
from PIL import Image
img = Image.open('src/core/interface/assets/logo.png')
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
+237
View File
@@ -0,0 +1,237 @@
name: Release
on:
push:
tags:
- "*"
permissions:
contents: write
env:
PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-22.04
artifact_name: SoftwareManager-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-${{ github.sha }}-windows
- os: macos-latest
artifact_name: SoftwareManager-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
outputs:
short_sha: ${{ steps.version.outputs.short_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.7"
enable-cache: true
- name: Install Python dependencies
run: |
uv pip install -r requirements.txt
uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env:
UV_SYSTEM_PYTHON: 1
- name: Generate version
id: version
shell: bash
run: |
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
echo "VERSION=$SHORT_SHA" >> $GITHUB_ENV
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
- name: Inject commit hash into code
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Install UPX (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
7z x upx.zip -oupx-dir
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
upx --version | head -1
- name: Convert icon for Windows
if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
- name: Build with Nuitka (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
python -m nuitka \
--standalone \
--assume-yes-for-downloads \
--windows-console-mode=disable \
--windows-icon-from-ico=app_icon.ico \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Rename artifact
shell: bash
run: |
mkdir -p dist-final
if [ "$RUNNER_OS" == "Windows" ]; then
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
- name: Create ZIP (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip"
- name: Build Installer (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
SETUP_NAME="SoftwareManager-${{ env.VERSION }}-windows-setup"
iscc //DMyAppVersion="${{ env.VERSION }}" \
//DMySourceDir="dist-final\\${{ env.APP_NAME }}" \
//DMyOutputDir="dist-final" \
//DMyOutputFilename="${SETUP_NAME}" \
installer.iss
echo "SETUP_NAME=${SETUP_NAME}" >> $GITHUB_ENV
- name: Upload build artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: |
dist-final/${{ matrix.artifact_name }}.zip
dist-final/${{ env.SETUP_NAME }}.exe
retention-days: 1
- name: Upload build artifact (Linux/macOS)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}
retention-days: 1
release:
name: Create Release
needs: build
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/ubuntu-22.04-build/SoftwareManager-*-linux release/SoftwareManager-${SHORT_SHA}-linux
cp artifacts/windows-latest-build/SoftwareManager-*-windows.zip release/SoftwareManager-${SHORT_SHA}-windows.zip
cp artifacts/windows-latest-build/SoftwareManager-*-windows-setup.exe release/SoftwareManager-${SHORT_SHA}-windows-setup.exe
cp artifacts/macos-latest-build/SoftwareManager-*-macos release/SoftwareManager-${SHORT_SHA}-macos
chmod +x release/SoftwareManager-*-linux release/SoftwareManager-*-macos
- name: Generate checksums
run: |
cd release
sha256sum * > SHA256SUMS.txt
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ${{ needs.build.outputs.short_sha }}
generate_release_notes: true
files: |
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-windows.zip
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-windows-setup.exe
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+238
View File
@@ -0,0 +1,238 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="main.css"/>
<title>SoftwareManager - Main</title>
<!-- <link rel="icon" type="image/x-icon" href="/assets/icons/favicon.ico"> -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<header>
<div class="logo-text">
<h1>KeksPirates</h1>
</div>
<nav class="tab">
<a class="tablinks" onclick="openTab(event, 'start')" id="defaultOpen">Home</a>
<a class="tablinks" onclick="openTab(event, 'api')">API</a>
<a class="tablinks" onclick="openTab(event, 'docs')">Docs</a>
<!-- <a class="tablinks" onclick="openTab(event, 'notfound')">404</a> -->
</nav>
</header>
<main>
<div id="start" class="tabcontent">
<div class="introduction-frame">
<div class="introduction-text inline-block">
<h1 class="hometitle"><br><span style="color: #db65d5">SoftwareManager</span></h1>
<p class="title_subtext">Simply search and download software from various sources</p>
<a href="https://github.com/KeksPirates/SoftwareManager" class="btn_big github"><img class="github-button" src="assets/icons/GitHub_Invertocat_White.png">GitHub</a>
<a href="https://github.com/KeksPirates/SoftwareManager/releases/latest" class="btn_big download"><img class="download-button" src="assets/icons/download_icon_black.png">Download</a>
</div>
<div class="introduction-image inline-block">
<div class="softwaremanager-scene">
<img class="softwaremanager-image" src="assets/softwaremanager/search_tab_rounded.png">
</div>
<img class="softwaremanager-image-2" src="assets/softwaremanager/search_tab_rounded.png">
</div>
</div>
</div>
<div id="api" class="tabcontent">
<h1 class="title"><span class="json-string">API Documentation</span> </h1>
<p>This API allows you to search for posts on rutracker.</p>
<div class="base-information-frame">
<h3>API URL</h3>
<p class="api-url">https://api.michijackson.xyz/</p>
<br>
<h3>Endpoint</h3>
<p class="endpoint-url"><span class="request-type">GET</span><span class="endpoint-type-container">/search?q=<span class="endpoint-parameter">{query}</span></span></p>
<div class="endpoint-parameters-container">
<div class="endpoint-parameters-header">
<p>Parameter</p>
<p>Type</p>
<p>Required</p>
<p>Description</p>
</div>
<div class="endpoint-parameters-content">
<p class="endpoint-parameter">q</p>
<p class="endpoint-type">String</p>
<p class="endpoint-required">True</p>
<p class="endpoint-description">Search query</p>
</div>
</div>
</div>
<div class="request-frame">
<h2>Example Request</h2>
<div class="request-type-container">
<p><span class="request-type">GET</span> <span class="request-type-subcontainer">api.michijackson.xyz/search?q=keks</span></p>
</div>
</div>
<div class="api-response-frame">
<h3>Response</h3>
<pre class="api-response"><span class="json-punctuation">{</span>
"cached"<span class="json-punctuation">:</span> <span class="json-boolean">false</span><span class="json-punctuation">,</span>
"count"<span class="json-punctuation">:</span> <span class="json-integer">1</span><span class="json-punctuation">,</span>
"data"<span class="json-punctuation">:</span> <span class="json-punctuation">[</span>
<span class="json-punctuation">{</span>
"author"<span class="json-punctuation">:</span> <span class="json-string">"Viterrson"</span><span class="json-punctuation">,</span>
"id"<span class="json-punctuation">:</span> <span class="json-integer">1</span><span class="json-punctuation">,</span>
"leechers"<span class="json-punctuation">:</span> <span class="json-integer">0</span><span class="json-punctuation">,</span>
"seeders"<span class="json-punctuation">:</span> <span class="json-integer">3</span><span class="json-punctuation">,</span>
"title"<span class="json-punctuation">:</span> <span class="json-string">"(Hard - Rock) Keks - Keks - 1983, MP3, 320 kbps"</span><span class="json-punctuation">,</span>
"url"<span class="json-punctuation">:</span> <span class="json-string">"https://rutracker.org/forum/viewtopic.php?t=3835559"</span>
<span class="json-punctuation">}</span>
<span class="json-punctuation">],</span>
"query"<span class="json-punctuation">:</span> <span class="json-string">"keks"</span><span class="json-punctuation">,</span>
"success"<span class="json-punctuation">:</span> <span class="json-boolean">true</span>
<span class="json-punctuation">}</span></pre>
</div>
</div>
<div id="docs" class="tabcontent">
<h1 class="title"><span style="color: #db65d5">Docs</span></h1>
<p>SoftwareManager is a desktop app to search, browse, and download software & games from multiple sources in one place.</p>
<div class="base-information-frame">
<h2>Features</h2>
<div class="endpoint-parameters-container docs-tips">
<p class="sources-text">• Searching & Downloading Software from various pages</p>
<p class="sources-text">• Network Interface Binding</p>
<p class="sources-text">• Simple UI, Speed Limiting etc.</p>
<p class="sources-text">• No Logins required</p>
</div>
</div>
<div class="base-information-frame">
<h2>General Information</h2>
<div class="endpoint-parameters-container docs-tips">
<p class="sources-text">• Rutracker is the default source since it is the largest index overall.</p>
<p class="sources-text">• Use m0nkrus if you want Adobe software, it directly gets the posts from his Telegram channel.</p>
<p class="sources-text">• Torrents start seeding after completion and start seeding on application start.</p>
<p class="sources-text">• MacOS Builds haven't been properly tested - We're thankful to receive feedback.</p>
<p class="sources-text">• If you want to host your own server, clone our <a href="https://github.com/KeksPirates/SoftwareManager-Server" style="color: #db65d5;">server repository</a>, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of the server repo.</p>
<p class="sources-text">• SoftwareManager is still in development and most likely contains issues. Please report any bugs you find via GitHub. Contributions are also greatly appreciated.</p>
</div>
</div>
<div class="base-information-frame">
<h2>Sources</h2>
<br>
<p>Use the dropdown menu in the top right to select your source.</p>
<div class="endpoint-parameters-container docs-grid">
<div class="endpoint-parameters-header">
<p>Source</p>
<p>Category</p>
<p>Notes</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">RuTracker</p>
<p class="sources-text">Software & games</p>
<p class="sources-text">Has an extremely large amount of Software. Relies on an API for searching.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">UzTracker</p>
<p class="sources-text">Software & games</p>
<p class="sources-text">Similar to RuTracker, smaller. Use this as fallback if RuTracker is down or there's API issues.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">m0nkrus</p>
<p class="sources-text">Adobe Software</p>
<p class="sources-text">Lots of curated releases. Focused on mainly professional Software.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">SteamRip</p>
<p class="sources-text">Games</p>
<p class="sources-text">Currently very experimental due to download link retrieveal. May not always work as expected.</p>
</div>
</div>
</div>
</div>
<div id="notfound" class="tabcontent">
<div class="notfound-frame">
<h1 class="notfound-title">404</h1>
<br>
<h1 class="notfound-subtext">(╯°□°)╯︵ ┻━┻</h1>
<a class="btn_big download" onclick="openTab(event, 'start')" style="cursor: pointer;">Go Home</a>
</div>
</div>
</main>
<footer>
<small>KeksPirates - SoftwareManager</small>
</footer>
<script>
function openTab(evt, tabName, pushToHistory = true) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
let activeButton = document.querySelector(`.tablinks[onclick*="${tabName}"]`);
if (activeButton) {
activeButton.className += " active";
}
if (pushToHistory) {
let path = "/";
if (tabName === 'api') path = "/api";
if (tabName === 'docs') path = "/docs";
history.pushState({ tab: tabName }, "", path);
}
}
window.addEventListener('popstate', function(event) {
if (event.state && event.state.tab) {
openTab(null, event.state.tab, false);
} else {
openTab(null, 'start', false);
}
});
window.onload = function() {
let currentPath = window.location.pathname;
if (currentPath === '/api') {
openTab(null, 'api', false);
} else if (currentPath === '/docs') {
openTab(null, 'docs', false);
} else {
history.replaceState({ tab: 'start' }, "", "/");
openTab(null, 'start', false);
}
};
</script>
</body>
</html>
+476
View File
@@ -0,0 +1,476 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--main-font: "Manrope", sans-serif;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
display: flex;
flex-direction: column;
min-height: 100vh;
background: #0f1011;
}
header {
height: 65px;
background: #1a1c1d;
color: #fff;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.tab a {
display: inline-block;
padding: 0.4rem 0.8rem;
margin-left: 0.5rem;
text-decoration: none;
color: #fafafa;
border-radius: 8px;
box-shadow: 0 0 6px 1px #dd06cb00;
font-family: var(--main-font);
transition: background 0.5s, color 0.5s;
cursor: pointer;
}
nav a:hover {
background: #db65d517;
color: #ffd1fd;
}
nav a.active {
background: #db65d55d;
color: #fdd2fc;
}
.logo-text h1 {
font-family: var(--main-font);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.02em;
background-image: linear-gradient(147deg, #da84d5, #db65d5);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
main {
font-family: var(--main-font);
color: #fafafa;
padding: 3rem;
}
main h1.hometitle {
font-size: 4.0rem;
line-height: 4rem;
margin-top: 1.8rem;
}
main p.title_subtext {
margin-top: 0.5rem;
font-size: 1.2rem;
}
.softwaremanager-scene {
perspective: 700px;
width: fit-content;
}
main img.softwaremanager-image {
margin-top: 0rem;
user-select: none;
transform: rotateX(8deg) rotateY(-12deg) rotateZ(2deg) scale(0.85) translateX(-1rem);
box-shadow: 15px 20px 15px #0f0f0f;
/* transition: transform 0.4s ease-out; */
}
/* main img.softwaremanager-image:hover {
transform: rotateX(8deg) rotateY(-13deg) rotateZ(2deg) scale(0.86) translateX(-1rem);
} */
/* transform: rotateX(8443deg) rotateY(-14234233deg) rotateZ(2deg) scale(4.863) translateX(-133rem); lmao */
main .introduction-text {
position: relative;
text-align: right;
margin-right: -3rem;
margin-top: 3rem;
z-index: 2;
user-select: none;
}
.introduction-image {
position: relative;
z-index: 1;
}
.inline-block {
display: inline-block;
vertical-align: top;
}
main .introduction-frame {
text-align: center;
position: relative;
font-size: 0
}
main .introduction-frame > * {
font-size: 1rem;
}
main .btn_big {
font-family: var(--main-font);
background: #1a1c1d;
padding: 0.7rem 1.4rem;
margin-top: 1.5rem;
border: none;
border-radius: 10px;
align-items: center;
display: inline-flex;
color: #fafafa;
text-decoration: none;
font-weight: bold;
user-select: none;
}
main .btn_big img {
height: 20px;
width: auto;
margin-right: 0.5rem;
}
main .btn_big.github {
margin-right: 1rem;
transition: background 0.5s;
}
main .btn_big.github:hover {
background: #18191a;
}
main .btn_big.download {
background: #ca62c5;
color: #0f1011;
transition: background 0.5s;
}
main .btn_big.download:hover {
background: #b551b0;
}
main .introduction-text h1,
main .introduction-text p {
user-select: text;
}
.softwaremanager-image-2 {
display: none;
}
footer {
color: #585858;
font-family: var(--main-font);
align-self: center;
padding: 1rem;
position: fixed;
bottom: 0;
user-select: none;
}
::selection {
background: #db65d528;
}
.base-information-frame {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.program-preview {
box-sizing: inherit;
background: #1a1c1d;
width: auto;
height: 20rem;
border-radius: 10px;
}
pre.api-response {
font-family: monospace;
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.api-response-frame {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.request-type {
color: #fdd2fc;
padding: 1px 6px;
background: #db65d55d;
border-radius: 6px;
}
.request-type-container {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.endpoint-type-container {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.request-type-subcontainer {
background: #1a1c1d;
padding: 0.5rem;
border-radius: 10px;
margin-top: 1rem;
}
.api-url {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.endpoint-url {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
color: #8f8d8d
}
.endpoint-parameters-container {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
color: #8f8d8d;
}
.endpoint-parameters-header {
margin-top: -0.3rem;
margin-bottom: 0.9rem;
font-size: 0.9rem;
grid-template-columns: 9rem 9rem 9rem 9rem;
display: grid;
}
.endpoint-parameters-content {
font-size: 1rem;
grid-template-columns: 9rem 9rem 9rem 9rem;
display: grid;
}
.request-frame {
margin-top: 1rem;
}
.endpoint-parameter { color: #ffab4a; }
.endpoint-type { color: #db65d5 }
.endpoint-required { color: #66ff80}
.endpoint-description { color: #b1b0b0 }
.json-punctuation { color: #5c5c5c; }
.json-string { color: #db65d5; }
.json-boolean { color: #ffab4a; }
.json-integer { color: #66ff80; }
.sources-text {
color: #ffffff;
font-size: 1rem;
}
.docs-grid .endpoint-parameters-header,
.docs-grid .endpoint-parameters-content {
grid-template-columns: 10rem 14rem 1fr;
}
.docs-grid .endpoint-parameters-header p {
color: #8f8d8d;
}
.docs-grid .endpoint-parameters-content {
padding: 0.5rem 0;
border-top: 1px solid #ffffff0a;
}
.docs-grid .endpoint-parameters-content:first-of-type {
border-top: none;
}
.docs-tips .sources-text {
padding: 0.25rem 0;
}
.base-information-frame h2 {
color: #db65d5;
}
.notfound-frame {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 75vh;
text-align: center;
gap: 10px;
}
.notfound-title {
font-size: 4.0rem;
line-height: 4rem;
margin-top: 1.8rem;
background-image: linear-gradient(135deg, #c574c1, #b851b2);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.notfound-subtext {
font-size: 2rem;
}
@media(max-width: 560px) {
header {
padding: 1rem;
}
main {
margin: 0rem;
padding: 1rem;
}
main .introduction {
display: block;
margin-top: -1rem;
padding: 1rem;
}
main .introduction-text {
position: relative;
text-align: center;
margin-top: 0rem;
z-index: 2;
user-select: none;
margin-right: 0rem;
padding-bottom: 2rem;
}
main h1.hometitle {
font-size: 2rem;
line-height: 2.2rem;
text-align: left;
}
main p.title_subtext {
text-align: left;
}
.introduction-image {
display: none;
}
main .btn_big {
display: block;
margin: 1rem auto;
}
main .btn_big.github{
margin-right: 0rem;
}
}
@media (min-width: 561px) and (max-width: 1393px) {
header {
padding: 1rem;
}
main {
margin: 0rem;
padding: 1rem;
}
main .introduction {
display: flex;
margin-top: -1rem;
padding: 1rem;
flex-direction: column;
align-items: center;
}
main .introduction-text {
position: relative;
text-align: center;
margin-top: 0rem;
z-index: 2;
user-select: none;
margin-left: 2rem;
margin-right: 2rem;
padding-bottom: 2rem;
}
main h1.hometitle {
font-size: 2rem;
line-height: 2.2rem;
text-align: center;
}
main p.title_subtext {
text-align: center;
}
.softwaremanager-image {
display: none;
}
.softwaremanager-image-2 {
display: block;
width: 100%;
height: auto;
margin-left: auto;
margin-right: auto;
}
.introduction-image {
width: 100%;
max-width: 50rem;
user-select: none;
}
main .introduction .inline-block {
display: block;
}
}
+9
View File
@@ -66,6 +66,15 @@ Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
Exec('taskkill', '/F /IM {#MyAppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Result := '';
end;
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
Filename: "{app}\{#MyAppExeName}"; Flags: nowait skipifnotsilent
View File
View File
-87
View File
@@ -1,87 +0,0 @@
from core.utils.data.tracker import get_magnet_link
from core.utils.data.state import state
from bs4 import BeautifulSoup
from typing import Dict
import requests
import time
cache = {
"data": [],
"last_fetched": 0
}
cache_expiry = 300
def _get_telegram_posts():
post = 100
max_posts = 250
posts = []
added = set()
current_time = time.time()
if cache["data"] and (current_time - cache["last_fetched"] < cache_expiry):
return cache["data"]
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
if not bubbles:
break
for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if post_txt is None or post_txt.b is None:
continue
title = post_txt.b.text
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
if link:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
title=title,
author="m0nkrus",
id=len(posts) + 1,
url=post_url
))
post += 22
posts.reverse()
cache["data"] = posts
cache["last_fetched"] = current_time
return(posts)
def scrape_m0nkrus(query):
posts = _get_telegram_posts()
filtered_posts = []
for post in posts:
if query.lower() in post["title"].lower():
filtered_post = post.copy()
filtered_post["id"] = len(filtered_posts) + 1
filtered_posts.append(filtered_post)
return filtered_posts
def get_magnet(post: Dict):
return get_magnet_link(post["url"])
Metadata = {
"name" : "m0nkrus",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_m0nkrus,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_m0nkrus():
state.trackers.update({Metadata["name"] : Metadata})
-52
View File
@@ -1,52 +0,0 @@
from core.utils.network.jsonhandler import split_data, format_data
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from typing import Dict
import requests
def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
consoleLog("Sent request to server")
if search:
_, data, _, success, cached = split_data(search.text)
if cached:
consoleLog("Server response cached")
if success:
sorted_data = []
for entry in data:
sorted_data.append(
{
"title" : entry["title"],
"author" : entry["author"],
"seeders" : entry["seeders"],
"leechers" : entry["leechers"],
"url" : entry["url"],
"id" : entry["id"],
}
)
return sorted_data
else:
consoleLog("Scraping failed server-side, unable to fetch posts from rutracker")
return []
else:
consoleLog("No response from server, returning nothing")
return []
def get_magnet(post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
Metadata = {
"name" : "rutracker",
"headers" : ["Post Title", "Author", "Seeders", "Leechers"],
"scrapeFunc" : scrape_rutracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_rutracker():
state.trackers.update({Metadata["name"] : Metadata})
-122
View File
@@ -1,122 +0,0 @@
from core.data.scrapers.provider.buzzheavier import scrape_buzzheavier
from core.data.scrapers.provider.gofile import scrape_gofile
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from bs4 import BeautifulSoup
from typing import Dict
import webbrowser
import requests
import time
import re
cache = {
"data": [],
"last_fetched": 0
}
cache_expiry = 300
def get_Metadata():
return Metadata
def scrape_steamrip_links():
current_time = time.time()
if cache["data"] != [] and (current_time - cache["last_fetched"] < cache_expiry):
return cache["data"]
url = f"https://steamrip.com/games-list-page/"
response = requests.get(url)
text = response.text
soup = BeautifulSoup(text, "html.parser")
games = soup.find_all("li", class_="az-list-item")
if len(games) == 0:
return []
links = []
names = []
for gamehtml in games:
link = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
links += re.findall(r'(?<=href=")[^"]*', link.__str__())
name = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
names += re.findall(r'(?<=\/">)[^<]*', name.__str__())
# construct the list[dict[str,str]]
ret = []
for i in range(len(names)):
ret.append({"title" : names[i], "url" : links[i]})
return ret
def scrape_steamrip_game_downloads(gamelink):
url = "https://steamrip.com" + gamelink
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
download_link_elements = soup.find_all("a",class_="shortc-button")
download_links = ["buzzheavier", "gofile"]
for download_link in download_link_elements:
pure = download_link.attrs.get("href")
if pure[2] == "b":
download_links[0] = "https:" + pure
if pure[2] == "g":
download_links[1] = "https:" + pure
ret = []
for link in download_links:
if len(link) != 1:
ret.append(link)
cache["data"] = ret
return ret
def get_download_link(post: Dict):
url = post["url"]
links = scrape_steamrip_game_downloads(url)
best = ""
for link in links:
if link[0] == "h":
best = link
break
if links.index(best) == 0:
link = scrape_buzzheavier(best)
return link
elif links.index(best) == 1:
link, headers = scrape_gofile(best)
return link, headers
else:
consoleLog("Unable to retrieve download link due to captcha, launching browser...")
webbrowser.open(best)
return None, None
def filter_steamrip(query: str):
games = scrape_steamrip_links()
filtered_games = [
game for game in games
if query.lower() in game["title"].lower()
]
return filtered_games
Metadata = {
"name" : "steamrip",
"headers" : ["Game"],
"scrapeFunc" : filter_steamrip,
"linkFunc" : get_download_link,
"isMagnet" : False,
}
def init_steamrip():
state.trackers.update({Metadata["name"] : Metadata})
-55
View File
@@ -1,55 +0,0 @@
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from typing import Dict
import requests
def scrape_uztracker(query):
base_url="https://uztracker.net/"
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
consoleLog(search_url)
posts = []
try:
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
for link in links:
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
if not theme_link or not theme_link.b:
continue
url = urljoin(base_url, theme_link['href'])
title = theme_link.b.text
author_link = link.find('a', class_="med")
author = author_link.text.strip() if author_link else "Unknown"
posts.append(dict(
title=title,
author=author,
url=url,
))
return posts
except requests.RequestException as e:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
def get_magnet(post: Dict):
return get_magnet_link(post["url"])
Metadata = {
"name" : "uztracker",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_uztracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_uztracker():
state.trackers.update({Metadata["name"] : Metadata})
-29
View File
@@ -1,29 +0,0 @@
from PySide6.QtGui import QImage, QPixmap
from core.utils.data.state import state
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
import os
class Image():
def __init__(self, parent):
self.overlay_label = QLabel(parent)
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
if state.image_path is not None and os.path.exists(state.image_path):
self.image = QImage(state.image_path)
self.image = self.image.scaledToWidth(300, Qt.TransformationMode.SmoothTransformation)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
x = parent.width() - self.overlay_label.width()
y = parent.height() - self.overlay_label.height()
self.overlay_label.move(x, y)
def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
-292
View File
@@ -1,292 +0,0 @@
from core.interface.utils.tabhelper import general_tab
from core.interface.utils.tabhelper import network_tab
from core.utils.config.settings import save_settings
from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.svghelper import svg_icon
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from PySide6.QtCore import Qt, QSize
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
QPushButton,
QWidget,
QVBoxLayout,
QDialog,
QLabel,
QHBoxLayout,
QSpinBox,
QCheckBox,
QFileDialog,
QComboBox,
QTabWidget,
)
import platform
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
def settings_dialog(self):
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(700, 450)
dialog_layout = QVBoxLayout()
dialog.setLayout(dialog_layout)
if state.window_transparency and platform.system() != "Windows" and dialog:
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def close_settings():
dialog.reject()
update_checkbox_container = QWidget()
update_checkbox_layout = QHBoxLayout()
# ignore updates checkbox
if platform.system() == "Windows":
update_checkbox = QCheckBox()
update_checkbox_container.setLayout(update_checkbox_layout)
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
update_checkbox_layout.addStretch()
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
# auto-resume downloads checkbox
autoresume_container = QWidget()
autoresume_layout = QHBoxLayout()
autoresume_checkbox = QCheckBox()
autoresume_container.setLayout(autoresume_layout)
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume)
autoresume_layout.addWidget(autoresume_checkbox)
# transparent window checkbox
transparent_window_container = QWidget()
transparent_window_layout = QHBoxLayout()
transparent_window_checkbox = QCheckBox()
transparent_window_container.setLayout(transparent_window_layout)
transparent_window_layout.addWidget(QLabel("Window Transparency (requires restart) (Linux/MacOS only): "))
transparent_window_layout.addStretch()
transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox)
##################
# SERVER SETTING #
##################
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
api_url = QLineEdit()
api_url_layout.addWidget(QLabel("API Server URL:"))
api_url_layout.addWidget(api_url)
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url)
#################
# DOWNLOAD PATH #
#################
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
download_path = QLineEdit()
download_path_layout.addWidget(QLabel("Download Path:"))
download_path_layout.addWidget(download_path)
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path)
def browse_download_path():
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
if dir_path:
download_path.setText(dir_path)
browse_button = QPushButton()
browse_button.setFixedSize(36, 36)
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
###############
# IMAGE PATH #
###############
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart, experimental):"))
image_path_layout.addWidget(image_path)
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path)
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()
browse_button.setFixedSize(36, 36)
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
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_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout)
###############
# SAVE/CANCEL #
###############
layout = QHBoxLayout()
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
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)
layout.addWidget(cancel_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.exec()
-26
View File
@@ -1,26 +0,0 @@
from core.data.scrapers.rutracker import init_rutracker
from core.data.scrapers.uztracker import init_uztracker
from core.data.scrapers.steamrip import init_steamrip
from core.data.scrapers.monkrus import init_m0nkrus
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
init_rutracker()
init_uztracker()
init_m0nkrus()
init_steamrip()
def return_pressed(self):
self.show_empty_results(False)
search_text = self.searchbar.text()
if search_text == "":
consoleLog("Error: Can't search for nothing")
return
consoleLog(f"User searched for: {search_text}")
tracker = state.trackers[state.currenttracker]
scrapefunc = tracker["scrapeFunc"]
state.posts = scrapefunc(search_text)
from core.interface.gui import MainWindow
MainWindow._instance.search_results_signal.emit(tracker["headers"])
-56
View File
@@ -1,56 +0,0 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout
def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
tab = QWidget()
layout = QVBoxLayout()
if searchbar:
layout.addWidget(searchbar)
if layout2:
layout.addLayout(layout2)
else:
layout.addWidget(software_list)
if dlbutton:
layout.addWidget(dlbutton)
tab.setLayout(layout)
tabs.addTab(tab, title)
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
View File
View File
-80
View File
@@ -1,80 +0,0 @@
from core.utils.data.state import state
import configparser
import platform
import os
def create_config():
config = configparser.ConfigParser()
config["General"] = {
"debug": True,
"ignore_updates": f"{state.ignore_updates}",
"autoresume": f"{state.autoresume}",
"window_transparency": f"{state.window_transparency}"
}
config["Network"] = {
"api_url": f"{state.api_url}",
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
"download_speed_limit": f"{state.down_speed_limit}",
"upload_speed_limit": f"{state.up_speed_limit}",
"max_connections": f"{state.max_connections}",
"max_downloads": f"{state.max_downloads}"
}
config["Paths"] = {
"download_path": f"{state.download_path}",
"image_path": f"{state.image_path}"
}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(state.settings_path, "config.yml")
if not os.path.exists(config_file):
create_config()
return
config.read(config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
create_config()
-60
View File
@@ -1,60 +0,0 @@
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget
from typing import Any, List, Dict
from pathlib import Path
import threading
class AppState(QObject):
image_changed = Signal(str)
def __init__(self):
super().__init__()
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seederm leecher
self.version: str = "dev"
self._image_path: str = ""
self.ignore_updates: bool = False
self.debug: bool = False
self.autoresume: bool = True
self.currenttracker: str = "rutracker"
self.trackertable: QTableWidget
self.trackers: Dict[str,Dict[str,Any]] = {} # each tracker should add itself here
'''
an example:
"rutracker" : {
"name" : "rutracker", # name of the tracker
"headers" : ["author", "title"], # keys shown in the table
"scrapeFunc" : function,
}
'''
self.api_url: str = "https://api.michijackson.xyz"
self.seeded_magnets: set = set()
self.download_path: str = str(Path.home() / "Downloads")
self.up_speed_limit: int = 0
self.down_speed_limit: int = 0
self.max_connections: int = 200
self.max_downloads: int = 10
self.settings_path: str = ""
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.window_transparency: bool = False
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
self.log_buffer: List[str] = []
self.downloads_lock = threading.RLock()
self.main_window: Any = None
self.loop_running: bool = False
self.shutdown_event = threading.Event()
@property
def image_path(self) -> str:
return self._image_path
@image_path.setter
def image_path(self, new_path: str):
if new_path != self._image_path:
self._image_path = new_path
self.image_changed.emit(new_path)
state = AppState()
@@ -1,4 +1,4 @@
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
import requests
import re
+12
View File
@@ -0,0 +1,12 @@
from data.sources.rutracker import RutrackerScraper
from data.sources.uztracker import UztrackerScraper
from data.sources.steamrip import SteamripScraper
from data.sources.monkrus import MonkrusScraper
# Register Scrapers here
SCRAPERS = [
RutrackerScraper(),
SteamripScraper(),
MonkrusScraper(),
UztrackerScraper(),
]
+78
View File
@@ -0,0 +1,78 @@
from utils.data.tracker import get_magnet_link
from bs4 import BeautifulSoup
from typing import Dict
import requests
import time
class MonkrusScraper:
name = "m0nkrus"
headers = ["Post Title", "Author"]
is_magnet = True
def __init__(self):
self.cache = { "data": [], "last_fetched": 0 }
self.cache_expiry = 300
def _get_telegram_posts(self):
post = 100
max_posts = 250
posts = []
added = set()
current_time = time.time()
if self.cache["data"] and (current_time - self.cache["last_fetched"] < self.cache_expiry):
return self.cache["data"]
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
if not bubbles:
break
for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if post_txt is None or post_txt.b is None:
continue
title = post_txt.b.text
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
if link:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
title=title,
author="m0nkrus",
id=len(posts) + 1,
url=post_url
))
post += 22
posts.reverse()
self.cache["data"] = posts
self.cache["last_fetched"] = current_time
return(posts)
def search(self, query):
posts = self._get_telegram_posts()
filtered_posts = []
for post in posts:
if query.lower() in post["title"].lower():
filtered_post = post.copy()
filtered_post["id"] = len(filtered_posts) + 1
filtered_posts.append(filtered_post)
return filtered_posts
def get_download_link(self, post: Dict):
return get_magnet_link(post["url"])
+46
View File
@@ -0,0 +1,46 @@
from utils.network.jsonhandler import split_data, format_data
from utils.data.tracker import get_magnet_link
from utils.logging.logs import consoleLog
from utils.data.state import state
from typing import Dict
import requests
class RutrackerScraper:
name = "rutracker"
headers = ["Post Title", "Author", "Seeders", "Leechers"]
is_magnet = True
def search(self, query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
consoleLog("Sent request to server")
if search:
_, data, _, success, cached = split_data(search.text)
if cached:
consoleLog("Server response cached")
if success:
sorted_data = []
for entry in data:
sorted_data.append(
{
"title" : entry["title"],
"author" : entry["author"],
"seeders" : entry["seeders"],
"leechers" : entry["leechers"],
"url" : entry["url"],
"id" : entry["id"],
}
)
return sorted_data
else:
consoleLog("Scraping failed server-side, unable to fetch posts from rutracker")
return []
else:
consoleLog("No response from server, returning nothing")
return []
def get_download_link(self, post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
+107
View File
@@ -0,0 +1,107 @@
from data.hosts.buzzheavier import scrape_buzzheavier
from data.hosts.gofile import scrape_gofile
from utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
from typing import Dict
import webbrowser
import requests
import time
import re
class SteamripScraper:
name = "steamrip"
headers = ["Game"]
is_magnet = False
def __init__(self):
self.cache = { "data": [], "last_fetched": 0 }
self.cache_expiry = 300
def scrape_steamrip_links(self):
current_time = time.time()
if self.cache["data"] != [] and (current_time - self.cache["last_fetched"] < self.cache_expiry):
return self.cache["data"]
url = f"https://steamrip.com/games-list-page/"
response = requests.get(url)
text = response.text
soup = BeautifulSoup(text, "html.parser")
games = soup.find_all("li", class_="az-list-item")
if len(games) == 0:
return []
links = []
names = []
for gamehtml in games:
link = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
links += re.findall(r'(?<=href=")[^"]*', link.__str__())
name = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
names += re.findall(r'(?<=\/">)[^<]*', name.__str__())
# construct the list[dict[str,str]]
ret = []
for i in range(len(names)):
ret.append({"title" : names[i], "url" : links[i]})
return ret
def scrape_steamrip_game_downloads(self, gamelink):
url = "https://steamrip.com" + gamelink
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
download_link_elements = soup.find_all("a",class_="shortc-button")
download_links = ["buzzheavier", "gofile"]
for download_link in download_link_elements:
pure = download_link.attrs.get("href")
if pure[2] == "b":
download_links[0] = "https:" + pure
if pure[2] == "g":
download_links[1] = "https:" + pure
ret = []
for link in download_links:
if len(link) != 1:
ret.append(link)
self.cache["data"] = ret
return ret
def get_download_link(self, post: Dict):
url = post["url"]
links = self.scrape_steamrip_game_downloads(url)
best = ""
for link in links:
if link[0] == "h":
best = link
break
if links.index(best) == 0:
link = scrape_buzzheavier(best)
return link
elif links.index(best) == 1:
link, headers = scrape_gofile(best)
return link, headers
else:
consoleLog("Unable to retrieve download link due to captcha, launching browser...")
webbrowser.open(best)
return None, None
def search(self, query: str):
games = self.scrape_steamrip_links()
filtered_games = [
game for game in games
if query.lower() in game["title"].lower()
]
return filtered_games
+50
View File
@@ -0,0 +1,50 @@
from utils.data.tracker import get_magnet_link
from utils.logging.logs import consoleLog
from utils.data.state import state
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from typing import Dict
import requests
class UztrackerScraper:
name = "uztracker"
headers = ["Post Title", "Author"]
is_magnet = True
def search(self, query):
base_url="https://uztracker.net/"
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
consoleLog(search_url)
posts = []
try:
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
for link in links:
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
if not theme_link or not theme_link.b:
continue
url = urljoin(base_url, theme_link['href'])
title = theme_link.b.text
author_link = link.find('a', class_="med")
author = author_link.text.strip() if author_link else "Unknown"
posts.append(dict(
title=title,
author=author,
url=url,
))
return posts
except requests.RequestException as e:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
def get_download_link(self, post: Dict):
return get_magnet_link(post["url"])

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -1,7 +1,7 @@
from core.utils.logging.logs import consoleLog, remove_download_log
from utils.logging.logs import consoleLog, remove_download_log
from PySide6.QtWidgets import QMessageBox
from core.utils.data.state import state
from PySide6.QtCore import Qt, QPoint
from utils.data.state import state
from PySide6 import QtWidgets
import subprocess
import platform
@@ -1,9 +1,9 @@
from core.interface.dialogs.pauseresumedelegate import PauseResumeDelegate
from core.interface.dialogs.hoverrowdelegate import HoverRowDelegate
from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.dialogs.theme import _table_stylesheet
from interface.dialogs.pauseresumedelegate import PauseResumeDelegate
from interface.dialogs.hoverrowdelegate import HoverRowDelegate
from interface.dialogs.downloadmodel import DownloadModel
from interface.dialogs.theme import _table_stylesheet
from PySide6.QtWidgets import QTableView, QHeaderView
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
@@ -1,6 +1,6 @@
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import libtorrent as lt
import subprocess
import platform
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
from PySide6.QtCore import Qt
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import QEvent
def eventFilter(self, obj, event):
@@ -1,11 +1,11 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
from PySide6 import QtWidgets
class HoverRowDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
from core.interface.gui import MainWindow
from interface.gui import MainWindow
opt = QtWidgets.QStyleOptionViewItem(option)
option.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
+187
View File
@@ -0,0 +1,187 @@
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget
from PySide6.QtCore import Qt, QSize, QEvent, QObject
from PySide6.QtGui import QImage, QPixmap
from utils.data.state import state
import darkdetect
import os
class Image(QObject):
def __init__(self, parent):
super().__init__(parent)
self.application = parent
self.overlay_label = QLabel(parent)
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self.opacity_effect = QGraphicsOpacityEffect()
self.overlay_label.setGraphicsEffect(self.opacity_effect)
self._current_image_path = None
self._wallpaper_active = False
self._original_stylesheets = {}
parent.installEventFilter(self)
state.image_changed.connect(self.update_image_overlay)
if state.image_path and os.path.exists(state.image_path):
self._load_and_display(state.image_path)
def eventFilter(self, obj, event):
if obj == self.application and event.type() == QEvent.Type.Resize:
if self._current_image_path:
self._load_and_display(self._current_image_path)
return False
def _set_wallpaper_transparency(self, enabled):
if self._wallpaper_active == enabled:
return
self._wallpaper_active = enabled
parent = self.application
central = parent.centralWidget()
if central:
central.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
for attr in ['tab_wrapper', 'corner_widget', 'titlebar']:
w = getattr(parent, attr, None)
if w:
w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if hasattr(parent, 'tabs'):
tabs = parent.tabs
tabs.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
for i in range(tabs.count()):
page = tabs.widget(i)
if page:
page.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
stack = tabs.findChild(QStackedWidget)
if stack:
stack.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
tab_bar = tabs.tabBar()
if tab_bar:
tab_bar.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if 'tabs' not in self._original_stylesheets:
self._original_stylesheets['tabs'] = tabs.styleSheet()
if enabled:
tabs.setStyleSheet(
self._original_stylesheets['tabs']
+ "\nQTabWidget::pane { background: transparent; }"
+ "\nQTabBar { background: transparent; }"
+ "\nQTabBar::tab { background: transparent; }"
)
else:
tabs.setStyleSheet(self._original_stylesheets['tabs'])
# Searchbar
if hasattr(parent, 'searchbar'):
if 'searchbar' not in self._original_stylesheets:
self._original_stylesheets['searchbar'] = parent.searchbar.styleSheet()
if enabled:
parent.searchbar.setStyleSheet("QLineEdit { background-color: transparent; }")
else:
parent.searchbar.setStyleSheet(self._original_stylesheets['searchbar'])
# Download button
if hasattr(parent, 'dlbutton'):
if 'dlbutton' not in self._original_stylesheets:
self._original_stylesheets['dlbutton'] = parent.dlbutton.styleSheet()
if enabled:
parent.dlbutton.setStyleSheet("QPushButton { background-color: transparent; }")
else:
parent.dlbutton.setStyleSheet(self._original_stylesheets['dlbutton'])
# Labels
for attr in ['emptyResults', 'emptyDownload']:
w = getattr(parent, attr, None)
if w:
w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if hasattr(parent, 'tracker_list'):
if 'tracker_list' not in self._original_stylesheets:
self._original_stylesheets['tracker_list'] = parent.tracker_list.styleSheet()
if enabled:
popup_bg = '#1e1e1e' if darkdetect.isDark() else '#ffffff'
parent.tracker_list.setStyleSheet(
"QComboBox { background-color: transparent; }"
f" QComboBox QAbstractItemView {{ background-color: {popup_bg}; }}"
)
else:
parent.tracker_list.setStyleSheet(self._original_stylesheets['tracker_list'])
def _load_and_display(self, image_path):
if state.image_enabled is not True:
self.overlay_label.hide()
self._set_wallpaper_transparency(False)
return
self._current_image_path = image_path
parent = self.application
image = QImage(image_path)
if image.isNull():
self.overlay_label.hide()
return
if getattr(state, "image_as_wallpaper", False):
scaled_image = image.scaled(
parent.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation
)
crop_x = (scaled_image.width() - parent.width()) // 2
crop_y = (scaled_image.height() - parent.height()) // 2
scaled_image = scaled_image.copy(crop_x, crop_y, parent.width(), parent.height())
pixmap = QPixmap.fromImage(scaled_image)
self.overlay_label.setPixmap(pixmap)
self.overlay_label.setGeometry(0, 0, parent.width(), parent.height())
self.overlay_label.lower()
self.opacity_effect.setOpacity(state.image_opacity / 100)
self._set_wallpaper_transparency(True)
else:
self._set_wallpaper_transparency(False)
scaled_image = image.scaledToWidth(
state.image_width, Qt.TransformationMode.SmoothTransformation
)
max_width = parent.width()
max_height = parent.height()
if scaled_image.width() > max_width or scaled_image.height() > max_height:
scaled_image = scaled_image.scaled(
QSize(max_width, max_height),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
pixmap = QPixmap.fromImage(scaled_image)
self.overlay_label.setPixmap(pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
pos = state.image_position
off = int(state.image_offset)
w = self.overlay_label.width()
h = self.overlay_label.height()
if pos == "top-left":
x, y = off, off
elif pos == "top-right":
x, y = parent.width() - w - off, off
elif pos == "bottom-left":
x, y = off, parent.height() - h - off
elif pos == "center":
x, y = (parent.width() - w) // 2, (parent.height() - h) // 2
else: # bottom-right (default)
x, y = parent.width() - w - off, parent.height() - h - off
self.opacity_effect.setOpacity(state.image_opacity / 100)
self.overlay_label.move(x, y)
self.overlay_label.show()
def update_image_overlay(self, new_image_path):
self._load_and_display(new_image_path)
@@ -0,0 +1,17 @@
from PySide6.QtWidgets import QMessageBox
import qdarktheme
class NotificationPopup(QMessageBox):
def __init__(self, title, text, infotext=None, parent=None):
super().__init__(parent)
qdarktheme.setup_theme("auto")
self.setIcon(QMessageBox.Icon.Information)
self.setWindowTitle(title)
self.setText(text)
if infotext is not None:
self.setInformativeText(infotext)
self.setStandardButtons(QMessageBox.StandardButton.Ok)
@@ -1,8 +1,8 @@
from core.interface.dialogs.theme import _theme_colors, SVG_PLAY, SVG_PAUSE, SVG_FOLDER
from interface.dialogs.theme import _theme_colors, SVG_PLAY, SVG_PAUSE, SVG_FOLDER
from PySide6.QtWidgets import QStyledItemDelegate, QWidget, QHBoxLayout
from core.interface.utils.svghelper import svg_icon
from interface.utils.svghelper import svg_icon
from PySide6.QtCore import Qt, QSize, Signal
from core.utils.data.state import state
from utils.data.state import state
from PySide6 import QtWidgets
import libtorrent as lt
@@ -10,7 +10,7 @@ class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int)
def paint(self, painter, option, index):
from core.interface.gui import MainWindow
from interface.gui import MainWindow
opt = QtWidgets.QStyleOptionViewItem(option)
option.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
+328
View File
@@ -0,0 +1,328 @@
from interface.utils.tabhelper import create_tab
from utils.config.settings import save_settings
from interface.utils.svghelper import svg_icon
from utils.logging.logs import consoleLog
from PySide6.QtCore import Qt, QSize
from utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
QPushButton,
QWidget,
QVBoxLayout,
QDialog,
QLabel,
QHBoxLayout,
QSpinBox, QSlider, QCheckBox,
QFileDialog,
QComboBox,
)
import platform
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
def settings_dialog(self):
temp_image_path = state.image_path
temp_image_width = state.image_width
temp_image_offset = state.image_offset
temp_image_opacity = state.image_opacity
temp_image_enabled = state.image_enabled
temp_image_as_wallpaper = state.image_as_wallpaper
temp_image_position = state.image_position
def create_widget(widget_type, label_text, **kwargs):
container = QWidget()
layout = QHBoxLayout()
container.setLayout(layout)
layout.addWidget(QLabel(label_text))
widget = widget_type()
if widget_type in (QSpinBox,):
layout.addWidget(widget)
widget.setMinimum(kwargs.get("minimum", 0))
widget.setMaximum(kwargs.get("maximum", 10000000))
widget.setFixedWidth(kwargs.get("width", 180))
widget.setFixedHeight(kwargs.get("height", 30))
elif widget_type in (QCheckBox,):
layout.addStretch()
layout.addWidget(widget)
elif widget_type in (QLineEdit,):
layout.addWidget(widget)
container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
if "width" in kwargs:
widget.setFixedWidth(kwargs["width"])
if "height" in kwargs:
widget.setFixedHeight(kwargs["height"])
elif widget_type in (QComboBox,):
layout.addWidget(widget)
widget.setFixedWidth(kwargs.get("width", 180))
widget.setFixedHeight(kwargs.get("height", 30))
return container, widget
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(580, 400)
dialog_layout = QVBoxLayout()
dialog.setLayout(dialog_layout)
if state.window_transparency and platform.system() != "Windows" and dialog:
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Ignore Updates checkbox
update_checkbox_container, update_checkbox = create_widget(QCheckBox, "Ignore Updates: ")
if platform.system() == "Windows":
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
# Autoresume Container Checkbox
autoresume_container, autoresume_checkbox = create_widget(QCheckBox, "Auto-Resume Downloads: ")
autoresume_checkbox.setChecked(state.autoresume)
autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked))
# Transparent Window Checkbox
transparent_window_container, transparent_window_checkbox = create_widget(QCheckBox, "Window Transparency (requires restart) (Linux/MacOS only): ")
transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
# Accent Color
accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color (requires restart): ", width=180, height=30)
accent_color_input.setPlaceholderText("e.g. #fca7d7")
accent_color_input.setText(state.accent_color)
# API URL Widget
api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30)
api_url.setText(state.api_url)
# Download Path Widget
download_path_container, download_path = create_widget(QLineEdit, "Download Path: ")
download_path.setText(state.download_path)
download_path_layout = download_path_container.layout()
def browse_download_path():
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
if dir_path:
download_path.setText(dir_path)
browse_button = create_widget(QPushButton, "", width=36, height=36)[1]
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
# Image Path
image_path_container, image_path = create_widget(QLineEdit, "Image Path: ")
image_path.setText(state.image_path)
image_path_layout = image_path_container.layout()
image_path.textChanged.connect(lambda text: setattr(state, 'image_path', text))
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 = create_widget(QPushButton, "", width=36, height=36)[1]
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
image_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_image_path)
# Enable Image Checkbox
enable_image_container, enable_image_checkbox = create_widget(QCheckBox, "Enable Image (requires image path): ", width=180, height=30)
enable_image_checkbox.setChecked(state.image_enabled)
enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked))
# Image Mode Checkbox
image_mode_container, image_mode_checkbox = create_widget(QCheckBox, "Wallpaper Mode: ")
image_mode_checkbox.setChecked(state.image_as_wallpaper)
image_mode_checkbox.toggled.connect(lambda checked: setattr(state, 'image_as_wallpaper', checked))
# Image Position Preset
positions = ["bottom-right", "bottom-left", "top-right", "top-left", "center"]
image_position_container, image_position_combo = create_widget(QComboBox, "Position: ", width=180, height=30)
image_position_combo.addItems(positions)
idx = image_position_combo.findText(state.image_position)
image_position_combo.setCurrentIndex(idx if idx >= 0 else 0)
image_position_combo.currentTextChanged.connect(lambda val: setattr(state, 'image_position', val))
_slider_style = """
QSlider::groove:horizontal {
height: 3px;
background: palette(mid);
border-radius: 1px;
}
QSlider::handle:horizontal {
width: 10px;
height: 10px;
margin: -4px 0;
border-radius: 5px;
}
"""
# Image Width Slider
image_width_container = QWidget()
image_width_layout = QHBoxLayout(image_width_container)
image_width_layout.addWidget(QLabel("Image Width: "))
image_width = QSlider(Qt.Orientation.Horizontal)
image_width.setFixedWidth(180)
image_width.setStyleSheet(_slider_style)
image_width.setMinimum(0)
image_width.setMaximum(2500)
image_width.setValue(state.image_width)
image_width_val = QLabel(str(state.image_width))
image_width_val.setFixedWidth(36)
image_width.valueChanged.connect(lambda val: (setattr(state, 'image_width', val), image_width_val.setText(str(val))))
image_width_layout.addWidget(image_width_val)
image_width_layout.addWidget(image_width)
# Image Offset Slider
image_offset_container = QWidget()
image_offset_layout = QHBoxLayout(image_offset_container)
image_offset_layout.addWidget(QLabel("Corner Offset: "))
image_offset = QSlider(Qt.Orientation.Horizontal)
image_offset.setFixedWidth(180)
image_offset.setStyleSheet(_slider_style)
image_offset.setMinimum(0)
image_offset.setMaximum(500)
image_offset.setValue(state.image_offset)
image_offset_val = QLabel(str(state.image_offset))
image_offset_val.setFixedWidth(36)
image_offset.valueChanged.connect(lambda val: (setattr(state, 'image_offset', val), image_offset_val.setText(str(val))))
image_offset_layout.addWidget(image_offset_val)
image_offset_layout.addWidget(image_offset)
# Image Opacity Slider
image_opacity_container = QWidget()
image_opacity_layout = QHBoxLayout(image_opacity_container)
image_opacity_layout.addWidget(QLabel("Image Opacity: "))
image_opacity = QSlider(Qt.Orientation.Horizontal)
image_opacity.setFixedWidth(180)
image_opacity.setStyleSheet(_slider_style)
image_opacity.setMinimum(0)
image_opacity.setMaximum(100)
image_opacity.setValue(state.image_opacity)
image_opacity_val = QLabel(str(state.image_opacity))
image_opacity_val.setFixedWidth(36)
image_opacity.valueChanged.connect(lambda val: (setattr(state, 'image_opacity', val), image_opacity_val.setText(str(val))))
image_opacity_layout.addWidget(image_opacity_val)
image_opacity_layout.addWidget(image_opacity)
# Speed Limiting
down_speed_limit_container, down_speed_limit = create_widget(QSpinBox, "Max Download Speed (KiB, 0 for unlimited): ", width=180, height=30)
down_speed_limit.setMinimum(0)
down_speed_limit.setValue(state.down_speed_limit)
up_speed_limit_container, up_speed_limit = create_widget(QSpinBox, "Max Upload Speed (KiB, 0 for unlimited): ", width=180, height=30)
up_speed_limit.setMinimum(0)
up_speed_limit.setValue(state.up_speed_limit)
# Connection Configs
max_connections_container, max_connections = create_widget(QSpinBox, "Max Connections: ", width=180, height=30)
max_connections.setMinimum(0)
max_connections.setValue(state.max_connections)
# Download Configs
max_downloads_container, max_downloads = create_widget(QSpinBox, "Max Downloads: ", width=180, height=30)
max_downloads.setMinimum(0)
max_downloads.setValue(state.max_downloads)
# 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_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout)
# Save / Cancel buttons
layout = QHBoxLayout()
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: handle_save())
def handle_save():
save_settings(
dialog.accept,
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(),
image_width.value(),
image_offset.value(),
image_opacity.value(),
image_as_wallpaper=image_mode_checkbox.isChecked(),
image_position=image_position_combo.currentText(),
accent_color=accent_color_input.text().strip()
)
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
tabs = QtWidgets.QTabWidget()
create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container, accent_color_container], tabs=tabs, stretch=True)
create_tab("Image", [enable_image_container, image_mode_container, image_position_container, image_width_container, image_offset_container, image_opacity_container], tabs=tabs, stretch=True)
create_tab("Paths", [download_path_container, image_path_container], tabs=tabs, stretch=True)
create_tab("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container], tabs=tabs, stretch=True)
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
def on_dialog_finished(result): # Undo Image changes if "Save" button is not pressed
if result == QtWidgets.QDialog.DialogCode.Rejected:
state.image_path = temp_image_path
state.image_width = temp_image_width
state.image_offset = temp_image_offset
state.image_opacity = temp_image_opacity
state.image_enabled = temp_image_enabled
state.image_as_wallpaper = temp_image_as_wallpaper
state.image_position = temp_image_position
dialog.finished.connect(on_dialog_finished)
dialog.exec()
@@ -1,4 +1,6 @@
from PySide6.QtGui import QColor
from utils.logging.logs import consoleLog
from utils.data.state import state
import darkdetect
def _is_dark_mode():
@@ -27,15 +29,32 @@ def _theme_colors():
}
def _accent_selection_color(alpha: float = 0.35) -> str:
if not state.accent_color:
return None
try:
hex_color = state.accent_color.lstrip('#')
if len(hex_color) != 6:
return None
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
return f"rgba({r}, {g}, {b}, {alpha})"
except ValueError:
consoleLog(f"Invalid Color: {state.accent_color}")
return None
def _table_stylesheet(view_type="QTableWidget"):
c = _theme_colors()
dark = _is_dark_mode()
color_rule = "" if dark else f"color: {c['text']};"
selected_bg = _accent_selection_color() or c["selected"]
selection_color_rule = f"selection-background-color: {selected_bg};" if state.accent_color else ""
return f"""
{view_type} {{
border: none;
outline: 0;
font-size: 13px;
{selection_color_rule}
{color_rule}
}}
{view_type}::item {{
@@ -45,7 +64,7 @@ def _table_stylesheet(view_type="QTableWidget"):
{color_rule}
}}
{view_type}::item:selected {{
background: {c["selected"]};
background: {selected_bg};
outline: none;
border: none;
border-bottom: 1px solid {c["border"]};
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
class TrackerHoverDelegate(QStyledItemDelegate):
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _table_stylesheet
from interface.dialogs.theme import _table_stylesheet
from PySide6.QtWidgets import QTableWidget
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
@@ -1,8 +1,8 @@
from core.utils.network.update_checker import get_updates
from core.utils.network.updater import download_update
from utils.network.update_checker import get_updates
from PySide6.QtWidgets import QDialog, QMessageBox
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.network.updater import download_update
from utils.logging.logs import consoleLog
from utils.data.state import state
from pathlib import Path
import platform
import json
@@ -24,7 +24,7 @@ def _get_build_info_path() -> (Path | None):
def get_version() -> None:
# Get build info filepath
build_info_path = _get_build_info_path()
if os.path.exists(build_info_path):
if build_info_path and os.path.exists(build_info_path):
with open(build_info_path, "r") as f:
build_info = json.load(f)
state.version = build_info.get("version")
@@ -46,3 +46,4 @@ class UpdateDialog(QDialog):
response = msg.exec_()
if response == QMessageBox.StandardButton.Ok:
download_update(assets)
@@ -1,25 +1,26 @@
from core.interface.dialogs.trackerhoverdelegate import TrackerHoverDelegate
from core.interface.dialogs.elideditemdelegate import ElidedItemDelegate
from core.interface.dialogs.trackertable import _create_tracker_table
from core.interface.dialogs.downloadlist import _create_download_list
from core.interface.assets.base64_icons import settings_white_base64
from core.interface.assets.base64_icons import settings_black_base64
from core.interface.dialogs.downloadlist import download_list_update
from core.interface.dialogs.update import get_version, UpdateDialog
from core.utils.logging.logs import consoleLog, flush_log_buffer
from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog
from core.interface.assets.base64_icons import logo_base64
from core.interface.dialogs.eventfilter import eventFilter
from core.utils.network.download import download_selected
from core.interface.utils.tabhelper import create_tab
from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.interface.dialogs.image import Image
from core.utils.data.state import state
from interface.dialogs.trackerhoverdelegate import TrackerHoverDelegate
from interface.dialogs.elideditemdelegate import ElidedItemDelegate
from interface.dialogs.trackertable import _create_tracker_table
from interface.dialogs.downloadlist import _create_download_list
from interface.assets.base64_icons import settings_white_base64
from interface.assets.base64_icons import settings_black_base64
from interface.dialogs.downloadlist import download_list_update
from interface.dialogs.update import get_version, UpdateDialog
from interface.dialogs.theme import _accent_selection_color
from utils.logging.logs import consoleLog, flush_log_buffer
from interface.dialogs.downloadmodel import DownloadModel
from interface.dialogs.settings import settings_dialog
from interface.assets.base64_icons import logo_base64
from interface.dialogs.eventfilter import eventFilter
from utils.network.download import download_selected
from interface.utils.searchhelper import run_search
from interface.utils.tabhelper import create_tab
from utils.general.shutdown import closehelper
from utils.general.wrappers import run_thread
from interface.dialogs.image import Image
from utils.data.state import state
import core.interface.dialogs.contextmenu
import interface.dialogs.contextmenu
from PySide6 import QtWidgets
from PySide6.QtCore import (
@@ -86,7 +87,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
consoleLog(f"Could not set app ID: {e}")
# Get current version
# core.interface.dialogs.update
# interface.dialogs.update
get_version()
UpdateDialog()
@@ -137,7 +138,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(state.trackertable)
# core.interface.dialogs.hoverrowdelegate
# interface.dialogs.hoverrowdelegate
self.download_model = DownloadModel()
self.downloadList = _create_download_list(self)
self.downloadList.viewport().installEventFilter(self)
@@ -164,8 +165,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout.addWidget(state.trackertable)
# Tabs
self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
create_tab("Search", [self.searchbar, self.horizontal_layout, self.dlbutton], tabs=self.tabs, stretch=False)
create_tab("Downloads", [self.emptyDownload, self.downloadList], tabs=self.tabs, stretch=False)
# Corner Widget (Settings Button, Tracker list, Tab button container)
self.corner_widget = QWidget()
@@ -177,6 +178,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker)
if state.accent_color:
self.tracker_list.setStyleSheet(
f"QComboBox QAbstractItemView::item:selected {{ background: {_accent_selection_color()}; }}"
)
self.corner_layout.addWidget(self.tracker_list)
# Settings button
@@ -232,7 +237,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
# (\u2193) Arrow down, (\u2191) Arrow up
self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s")
self.speed_label = QLabel(" 0.0 kB/s 0.0 kB/s")
self.speed_label.setStyleSheet("padding-right: 6px;")
self.statusbar.addPermanentWidget(self.speed_label)
@@ -246,7 +251,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.active_timer.timeout.connect(self.show_empty_downloads)
self.active_timer.start(500)
self._context_menu = core.interface.dialogs.contextmenu.ContextMenu(self)
self._context_menu = interface.dialogs.contextmenu.ContextMenu(self)
self.image_overlay = Image(self)
def _apply_default_headers(self, table):
@@ -292,10 +297,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
table.setItemDelegateForColumn(i, self._tracker_hover_delegate)
def _start_search(self):
self.searchbar.setEnabled(False)
def _search_thread():
try:
return_pressed(self)
run_search(self)
except Exception as e:
consoleLog(f"Search error: {e}", True)
self.search_results_signal.emit([])
@@ -384,7 +388,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def closeEvent(self, event: QCloseEvent):
closehelper()
event.accept()
from core.utils.general.shutdown import force_exit
from utils.general.shutdown import force_exit
force_exit()
def _invalidate_hover_row(self, row):
+27
View File
@@ -0,0 +1,27 @@
from utils.logging.logs import consoleLog
from utils.data.state import state
from data.sources import SCRAPERS
for scraper in SCRAPERS:
state.trackers[scraper.name] = scraper
def run_search(self) -> None:
state.trackertable.setRowCount(0)
self.show_empty_results(False)
search_text = self.searchbar.text()
# Check for empty search
if search_text == "":
consoleLog("Error: Can't search for nothing")
return
state.posts = []
self.searchbar.setEnabled(False)
consoleLog(f"User searched for: {search_text}")
# Get current tracker and call its search function
scraper = state.trackers[state.currenttracker]
state.posts = scraper.search(search_text)
# Update GUI headers to match the current scraper
self.search_results_signal.emit(scraper.headers)
@@ -1,16 +1,13 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Qt, QByteArray
from PySide6.QtGui import QIcon, QPixmap, QPainter
from PySide6.QtCore import Qt, QByteArray
from PySide6.QtSvg import QSvgRenderer
from PySide6 import QtWidgets
import darkdetect
# Check if darkmode is enabled
def _is_dark_mode():
return darkdetect.isDark()
def svg_icon(svg_str, size=20):
app = QtWidgets.QApplication.instance()
if app:
+22
View File
@@ -0,0 +1,22 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLayout
# Tab creation helper function
def create_tab(title, items, tabs, stretch):
tab = QWidget()
layout = QVBoxLayout()
for item in items:
if item is None:
continue
if isinstance(item, QLayout):
layout.addLayout(item)
else:
layout.addWidget(item)
if stretch:
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
+26 -19
View File
@@ -1,16 +1,16 @@
from core.network.libtorrent_misc import send_notification, update_log, check_deleted_files
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces
from core.utils.logging.logs import get_download_logs
from core.utils.logging.logs import set_main_window
from core.network.libtorrent_int import check_space
from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.utils.general.shutdown import force_exit
from core.utils.config.config import read_config
from core.utils.logging.logs import consoleLog
from core.interface.gui import MainWindow
from core.utils.data.state import state
from network.libtorrent_misc import send_notification, update_log, check_deleted_files
from utils.logging.loghandler import split_data, check_completed, check_downloads
from network.interface import list_interfaces, init_interfaces
from utils.logging.logs import get_download_logs
from utils.logging.logs import set_main_window
from network.libtorrent_int import check_space
from utils.general.shutdown import closehelper
from utils.general.wrappers import run_thread
from utils.general.shutdown import force_exit
from utils.config.config import read_config
from utils.logging.logs import consoleLog
from interface.gui import MainWindow
from utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
import qdarktheme
@@ -20,15 +20,18 @@ import signal
import time
import sys
def run_gui():
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
qdarktheme.setup_theme("auto")
def run_gui(app):
custom_colors = {}
if state.accent_color:
custom_colors["primary"] = state.accent_color
qdarktheme.setup_theme("auto", custom_colors=custom_colors if custom_colors else None)
widget = MainWindow()
# Check OS for window transparency compatibility and apply
if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
transparent_colors = {"background": "#00000000", **custom_colors}
qdarktheme.setup_theme("auto", custom_colors=transparent_colors)
set_main_window(widget)
widget.show()
@@ -42,7 +45,11 @@ def keyboardinterrupthandler(signum, frame):
def main():
# Begin counting startup time
start_time = time.perf_counter()
# Parse saved files
# Initialize UI Engine
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
# Parse saved files
read_config()
logs = get_download_logs()
_, downloads = split_data(logs)
@@ -65,7 +72,7 @@ def main():
elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
# Launch GUI
run_gui()
run_gui(app)
if __name__ == "__main__":
main()
@@ -1,9 +1,10 @@
from utils.logging.logs import consoleLog, add_download_log
from utils.data.state import state
from typing import Optional
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog, add_download_log
from .handle import DirectDownloadHandle
from .utils import (
from network.direct_download.handle import DirectDownloadHandle
from network.direct_download.utils import (
sanitize_filename,
extract_filename_from_url,
detect_filename_from_headers,
@@ -1,8 +1,8 @@
from core.utils.logging.logs import consoleLog, update_download_completed
from network.direct_download.status import DirectDownloadStatus, ChunkSpec
from utils.logging.logs import consoleLog, update_download_completed
from concurrent.futures import ThreadPoolExecutor, as_completed
from .status import DirectDownloadStatus, ChunkSpec
from core.utils.data.state import state
from .utils import format_size
from network.direct_download.utils import format_size
from utils.data.state import state
from typing import Optional
import libtorrent as lt
import threading
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from urllib.parse import urlparse, unquote
from utils.logging.logs import consoleLog
from typing import Optional
import requests
import os
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import psutil
def get_net_interfaces():
@@ -1,7 +1,7 @@
from core.network.interface import get_interface_ip
from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.interface import get_interface_ip
from utils.general.wrappers import run_thread
from utils.logging.logs import consoleLog
from utils.data.state import state
import libtorrent as lt
import threading
import platform
@@ -23,7 +23,7 @@ def get_free_space_mb(dirname):
def check_space():
while not state.shutdown_event.is_set():
for magnet_uri, magnetdl in list(state.active_downloads.items()):
for _, magnetdl in list(state.active_downloads.items()):
try:
status = magnetdl.status()
except RuntimeError:
@@ -79,7 +79,6 @@ def add_download(magnet_uri):
state.active_downloads = {}
init_session()
free_space = get_free_space_mb(state.download_path)
if magnet_uri in state.active_downloads:
@@ -110,7 +109,6 @@ def add_download(magnet_uri):
params.save_path = state.download_path
handle = state.dl_session.add_torrent(params)
while not handle.has_metadata():
time.sleep(1)
@@ -1,6 +1,6 @@
from core.utils.logging.logs import update_download_completed_by_hash
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import update_download_completed_by_hash
from utils.logging.logs import consoleLog
from utils.data.state import state
from plyer import notification
import libtorrent as lt
import time
@@ -17,9 +17,6 @@ def cleanup_session():
state.dl_session = None
state.active_downloads.clear()
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
@@ -1,5 +1,5 @@
from core.network.libtorrent_int import add_download
from core.utils.logging.logs import consoleLog
from network.libtorrent_int import add_download
from utils.logging.logs import consoleLog
def add_magnet(uri):
+150
View File
@@ -0,0 +1,150 @@
from interface.dialogs.notificationpopup import NotificationPopup
from utils.logging.logs import consoleLog
from utils.data.state import state
import configparser
import platform
import time
import os
def create_config():
config = configparser.ConfigParser()
config["General"] = {
"debug": str(state.debug),
"ignore_updates": str(state.ignore_updates),
"autoresume": str(state.autoresume),
"window_transparency": str(state.window_transparency),
"accent_color": str(state.accent_color)
}
config["Network"] = {
"api_url": str(state.api_url),
"bound_interface": str(state.bound_interface) if state.bound_interface is not None else "None",
"download_speed_limit": str(state.down_speed_limit),
"upload_speed_limit": str(state.up_speed_limit),
"max_connections": str(state.max_connections),
"max_downloads": str(state.max_downloads)
}
config["Paths"] = {
"download_path": str(state.download_path),
"image_path": str(state.image_path)
}
config["Image"] = {
"enable_image": str(state.image_enabled),
"image_width": str(state.image_width),
"image_offset": str(state.image_offset),
"image_opacity": str(state.image_opacity),
"image_as_wallpaper": str(state.image_as_wallpaper),
"image_position": str(state.image_position)
}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.ini"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
# Temporary migration logic, keep for a while
old_config_file = os.path.join(state.settings_path, "config.yml")
new_config_file = os.path.join(state.settings_path, "config.ini")
if os.path.exists(old_config_file):
try:
os.replace(old_config_file, new_config_file)
consoleLog("Successfully migrated config.yml to config.ini")
except Exception as e:
consoleLog(f"Failed to migrate config file: {e}")
if not os.path.exists(new_config_file):
create_config()
return
try:
config.read(new_config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
state.accent_color = config.get("General", "accent_color", fallback=state.accent_color)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
# Image
state.image_enabled = config.getboolean("Image", "enable_image", fallback=state.image_enabled)
state.image_width = config.getint("Image", "image_width", fallback=state.image_width)
state.image_offset = config.getint("Image", "image_offset", fallback=state.image_offset)
state.image_opacity = config.getint("Image", "image_opacity", fallback=state.image_opacity)
state.image_as_wallpaper = config.getboolean("Image", "image_as_wallpaper", fallback=state.image_as_wallpaper)
state.image_position = config.get("Image", "image_position", fallback=state.image_position)
create_config()
except configparser.Error as e:
consoleLog(f"Error: Configuration file corrupted ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to a structural error.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
except ValueError as e:
consoleLog(f"Error: Invalid data types in configuration file ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to invalid data types.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
except Exception as e:
consoleLog(f"Unexpected error occurred while loading settings: {e}")
NotificationPopup(
title="Unexpected Error",
text="An unknown error occurred while loading your settings.",
infotext=f"System returned: {e}\n\nThe application may not function as expected."
).exec()
def backup_config(config_path):
if os.path.exists(config_path):
timestamp = time.strftime("%Y%m%d-%H%M%S")
backup_path = f"{config_path}.{timestamp}.bak"
try:
os.replace(config_path, backup_path)
consoleLog(f"Successfully created backup of corrupted config: {backup_path}")
except Exception as e:
consoleLog(f"Failed to create backup of corrupted config: {e}")
@@ -1,10 +1,11 @@
from core.network.libtorrent_int import update_settings
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from .config import create_config
from network.libtorrent_int import update_settings
from utils.config.config import create_config
from utils.logging.logs import consoleLog
from utils.data.state import state
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None):
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, image_width=None, image_offset=None, image_opacity=None, image_as_wallpaper=None, image_position=None, accent_color=None):
if apiurl is not None:
state.api_url = apiurl
if download_path is not None:
@@ -23,8 +24,20 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
state.max_downloads = max_downloads
if bound_interface is not None:
state.bound_interface = None if bound_interface == "None" else bound_interface
if image_width is not None:
state.image_width = image_width
if image_offset is not None:
state.image_offset = image_offset
if image_opacity is not None:
state.image_opacity = image_opacity
if image_as_wallpaper is not None:
state.image_as_wallpaper = image_as_wallpaper
if image_position is not None:
state.image_position = image_position
if accent_color is not None:
state.accent_color = accent_color
update_settings()
update_settings() # Update LibTorrent Session Settings
consoleLog("Saved Settings")
create_config()
close()
+160
View File
@@ -0,0 +1,160 @@
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QTableWidget
from typing import Any, List, Dict
from pathlib import Path
import threading
class AppState(QObject):
image_changed = Signal(str)
def __init__(self):
super().__init__()
# General
self.version: str = "Unknown"
self.debug: bool = False
self.main_window: Any = None
self.loop_running: bool = False
self.shutdown_event = threading.Event()
self.log_buffer: List[str] = []
# Paths
self.settings_path: str = ""
self._image_path: str = ""
self.download_path: str = str(Path.home() / "Downloads")
# GUI
self.window_transparency: bool = False
self.accent_color: str = ""
self.trackertable: QTableWidget
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
# Image
self._image_enabled: bool = False
self._image_width: int = 300 # Default to 300px
self._image_offset: int = 50
self._image_opacity: int = 100
self._image_as_wallpaper: bool = False
self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center
# Trackers / Scraping
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
self.currenttracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz"
self.trackers: Dict[str,Dict[str,Any]] = {}
# LibTorrent / Download related stuff
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.seeded_magnets: set = set()
self.ignore_updates: bool = False
self.autoresume: bool = True
self.up_speed_limit: int = 0
self.down_speed_limit: int = 0
self.max_connections: int = 200
self.max_downloads: int = 10
self.downloads_lock = threading.RLock()
@property
def image_path(self) -> str:
return self._image_path
@image_path.setter
def image_path(self, new_path: str):
if new_path != self._image_path:
self._image_path = new_path
self.image_changed.emit(new_path)
@property
def image_offset(self) -> int:
return self._image_offset
@image_offset.setter
def image_offset(self, new_offset: int):
if new_offset != self._image_offset:
self._image_offset = new_offset
self.image_changed.emit(self._image_path)
@property
def image_width(self) -> int:
return self._image_width
@image_width.setter
def image_width(self, new_width: int):
if new_width != self._image_width:
self._image_width = new_width
self.image_changed.emit(self._image_path)
@property
def image_opacity(self) -> int:
return self._image_opacity
@image_opacity.setter
def image_opacity(self, new_opacity: int):
if new_opacity != self._image_opacity:
self._image_opacity = new_opacity
self.image_changed.emit(self._image_path)
@property
def image_enabled(self) -> bool:
return self._image_enabled
@image_enabled.setter
def image_enabled(self, new_state: bool):
if new_state != self._image_enabled:
self._image_enabled = new_state
self.image_changed.emit(self._image_path)
@property
def image_as_wallpaper(self) -> bool:
return self._image_as_wallpaper
@image_as_wallpaper.setter
def image_as_wallpaper(self, new_state: bool):
if new_state != self._image_as_wallpaper:
self._image_as_wallpaper = new_state
self.image_changed.emit(self._image_path)
@property
def image_position(self) -> str:
return self._image_position
@image_position.setter
def image_position(self, new_pos: str):
if new_pos != self._image_position:
self._image_position = new_pos
self.image_changed.emit(self._image_path)
@property
def image_custom_position(self) -> bool:
return self._image_custom_position
@image_custom_position.setter
def image_custom_position(self, new_state: bool):
if new_state != self._image_custom_position:
self._image_custom_position = new_state
self.image_changed.emit(self._image_path)
@property
def image_x(self) -> int:
return self._image_x
@image_x.setter
def image_x(self, val: int):
if val != self._image_x:
self._image_x = val
self.image_changed.emit(self._image_path)
@property
def image_y(self) -> int:
return self._image_y
@image_y.setter
def image_y(self, val: int):
if val != self._image_y:
self._image_y = val
self.image_changed.emit(self._image_path)
state = AppState()
@@ -1,4 +1,4 @@
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
import requests
@@ -1,6 +1,6 @@
from core.network.libtorrent_misc import cleanup_session
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.libtorrent_misc import cleanup_session
from utils.logging.logs import consoleLog
from utils.data.state import state
import os
def closehelper():
@@ -1,4 +1,4 @@
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
def run_thread(thread):
target_name = thread._target.__name__
@@ -1,9 +1,8 @@
from core.utils.network.download import run_download_direct, seed_magnet
from core.utils.logging.logs import consoleLog, remove_download_log
from utils.network.download import run_download_direct, seed_magnet
from utils.logging.logs import consoleLog, remove_download_log
import os
def split_data(data):
count = data.count
downloads = data.data
@@ -18,7 +17,7 @@ def check_completed(downloads, resume):
run_download_direct(download.magnet_uri, download.title)
consoleLog(f"Resuming Magnet: {download.title}")
elif download.url:
from core.network.direct_download import add_direct_download
from network.direct_download import add_direct_download
add_direct_download(download.url, download.title)
consoleLog(f"Resuming Direct Download: {download.title}")
@@ -1,5 +1,5 @@
from core.utils.data.models import Download, DownloadList
from core.utils.data.state import state
from utils.data.models import Download, DownloadList
from utils.data.state import state
from dataclasses import asdict
from datetime import datetime
import json
@@ -243,7 +243,7 @@ def set_main_window(window):
def flush_log_buffer(): # credits to claude
if state.log_buffer:
try:
from core.interface.gui import MainWindow
from interface.gui import MainWindow
for log_entry in state.log_buffer:
MainWindow.add_log(log_entry)
state.log_buffer = []
@@ -256,7 +256,7 @@ def consoleLog(text, printAnyways = False):
formatted_text = f"[{current_time}] {text}"
try:
from core.interface.gui import MainWindow
from interface.gui import MainWindow
if not MainWindow.add_log(formatted_text):
state.log_buffer.append(formatted_text)
except Exception:
@@ -1,11 +1,11 @@
from core.network.direct_download import add_direct_download
from core.network.libtorrent_wrapper import add_magnet
from core.utils.logging.logs import add_download_log
from core.utils.general.wrappers import run_thread
from core.network.libtorrent_int import add_seed
from network.direct_download import add_direct_download
from network.libtorrent_wrapper import add_magnet
from PySide6.QtWidgets import QTableWidgetItem
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import add_download_log
from utils.general.wrappers import run_thread
from network.libtorrent_int import add_seed
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
from typing import Optional
import threading
@@ -31,9 +31,9 @@ def download_selected(items: list[QTableWidgetItem]):
run_thread(threading.Thread(target=run_download, args=(post,)))
def run_download(post, headers: Optional[dict] = None):
linkfunc = state.trackers[state.currenttracker]["linkFunc"]
ismagnet = state.trackers[state.currenttracker]["isMagnet"]
result = linkfunc(post)
link_method = state.trackers[state.currenttracker].get_download_link
ismagnet = state.trackers[state.currenttracker].is_magnet
result = link_method(post)
if ismagnet:
link = result
@@ -1,9 +1,8 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import requests
def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
try:
@@ -1,6 +1,7 @@
from core.network.direct_download.handle import DirectDownloadHandle
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.direct_download.handle import DirectDownloadHandle
from utils.general.shutdown import closehelper
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
from PySide6 import QtWidgets
import libtorrent as lt
@@ -91,6 +92,7 @@ def download_update(assets: list):
progress.setValue(100)
QtWidgets.QApplication.processEvents()
closehelper()
subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"])
time.sleep(1)
sys.exit(0)
os._exit(0)