mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a45d71655 | |||
| 0c7bc426db | |||
| 1f06a19c17 | |||
| da92c4ab0a | |||
| f6e3ff4f27 | |||
| 88042b2d3d | |||
| 9265a9cd1c | |||
| adc9f6f6d4 | |||
| a3a6ef5eec | |||
| 8e5e0425ef | |||
| c2b329aed4 | |||
| c5f2be90af | |||
| 695b80c6de | |||
| b7188a515a | |||
| 7b2522438c | |||
| 0686c7aec2 | |||
| 6be1784903 | |||
| 4589322f0a | |||
| 8132d789ef | |||
| 3f39e67933 | |||
| d43d6e57cc | |||
| 28cffe59f5 | |||
| f1726f4e9a | |||
| b7ca895aeb | |||
| e95ad11672 | |||
| f2a79e5eed | |||
| 990135450f | |||
| 62c6afe350 | |||
| 1aaf56a4dc | |||
| 47b643d68b | |||
| de1c8577d6 | |||
| c835b8375c | |||
| e7291e8e99 | |||
| a4efe9185a | |||
| 86b4f023ac | |||
| 006da55aa0 | |||
| 214a389ad3 | |||
| d4005d7e86 | |||
| 08a45cb422 | |||
| c49726fe42 | |||
| ecc117dedc | |||
| c51fc9ea4a | |||
| 48ed11c248 | |||
| d672e3d85f | |||
| 564c0f2ac5 | |||
| 8a48c6c730 | |||
| 6432511639 | |||
| 054e1d0aa5 | |||
| 6f7a9e13f4 | |||
| 89de75c3d7 | |||
| f41520e595 | |||
| 79c8de1dbf | |||
| dc933d54f3 | |||
| f78d8780cf | |||
| 7f155547ec | |||
| 8e431f9826 | |||
| 748f0865b7 | |||
| a19cf469f6 | |||
| 8f9f77d63f | |||
| fa3a43887a | |||
| 685461e453 | |||
| f3379174b0 | |||
| b217a74fce | |||
| 1da62ef414 | |||
| 4b5c36030d | |||
| cc5f6a804c | |||
| b783d0a0cb | |||
| d057624548 | |||
| 81f7970c47 | |||
| 6a41ac44c9 | |||
| 26c5410f3f | |||
| 7067f243ae | |||
| d82008c9fe | |||
| 83ff2720c8 | |||
| 83adbb027f | |||
| 606c70efcc | |||
| 9970086901 | |||
| 0d631e0ab8 | |||
| 1c1ce3e69d | |||
| b5931f5590 | |||
| 84ea030e2a | |||
| 356f3bf070 | |||
| 6ff29e2ea6 | |||
| 847b50ab10 | |||
| 5bc81b5215 | |||
| efe83613d0 | |||
| 3597cba207 | |||
| 749051aa4f | |||
| bd26f073e4 | |||
| 027b4aff8d | |||
| a386269f29 | |||
| 9afced8a0b | |||
| db77bed3cf | |||
| cc3b201450 | |||
| 5a9d9067b9 | |||
| e0eaf250e5 | |||
| 1c4c010e4e | |||
| c1509f9172 | |||
| 97eabf6ec6 | |||
| 5eca1a678e | |||
| 7e59fdc2d3 | |||
| ad6fa428a0 | |||
| 56e8fada4d | |||
| e950d2f90d | |||
| 1a59c45ba4 | |||
| 293c1c3ad1 | |||
| f30cc5da5e | |||
| 004c7ad416 | |||
| 65f7c04936 | |||
| 9d0d5332bf | |||
| 6d9991188a | |||
| f397bc18ce |
+73
-37
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
|
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
|
||||||
asset_name: SoftwareManager-dev-${{ github.sha }}-linux
|
asset_name: SoftwareManager-dev-${{ github.sha }}-linux
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@@ -44,30 +44,22 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Install system dependencies (Linux)
|
- name: Install uv
|
||||||
if: runner.os == 'Linux'
|
uses: astral-sh/setup-uv@v7
|
||||||
run: |
|
with:
|
||||||
sudo apt-get update
|
version: "0.9.7"
|
||||||
sudo apt-get install -y \
|
enable-cache: true
|
||||||
libxcb-xinerama0 \
|
|
||||||
libxkbcommon-x11-0 \
|
|
||||||
libxcb-cursor0 \
|
|
||||||
libxcb-keysyms1 \
|
|
||||||
libxcb-render-util0 \
|
|
||||||
libxcb-icccm4 \
|
|
||||||
libxcb-image0 \
|
|
||||||
libxcb-shape0 \
|
|
||||||
libxcb-util1
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
uv pip install -r requirements.txt
|
||||||
pip install -r requirements.txt
|
uv pip install pyinstaller backports.tarfile pillow
|
||||||
pip install pyinstaller
|
env:
|
||||||
|
UV_SYSTEM_PYTHON: 1
|
||||||
|
|
||||||
- name: Generate version
|
- name: Generate version
|
||||||
id: version
|
id: version
|
||||||
@@ -81,29 +73,54 @@ jobs:
|
|||||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
echo "Generated version: $VERSION"
|
echo "Generated version: $VERSION"
|
||||||
|
|
||||||
- name: Inject commit hash into code (Windows)
|
- name: Inject commit hash into code
|
||||||
if: runner.os == 'Windows'
|
shell: bash
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
$json = '{
|
cat > build_info.json << EOF
|
||||||
"version": "' + $env:VERSION + '"
|
{
|
||||||
}'
|
"version": "${{ env.VERSION }}"
|
||||||
$json | Out-File -Encoding utf8 build_info.json
|
}
|
||||||
|
EOF
|
||||||
- name: Download aria2c (Windows)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
run: |
|
|
||||||
mkdir -p deps
|
|
||||||
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip
|
|
||||||
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/
|
|
||||||
|
|
||||||
- name: Build executable with PyInstaller (Windows)
|
- name: Build executable with PyInstaller (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: pyinstaller --onefile --windowed --add-data "build_info.json:." --add-data "deps/aria2c.exe:." --name ${{ env.APP_NAME }} main.py
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pyinstaller --onefile --windowed \
|
||||||
|
--add-data "build_info.json;." \
|
||||||
|
--add-data "build_info.json;core" \
|
||||||
|
--add-data "build_info.json;core/interface" \
|
||||||
|
--add-data "core/interface/assets;core/interface/assets" \
|
||||||
|
--icon "core/interface/assets/logo.png" \
|
||||||
|
--exclude-module PyQt6 \
|
||||||
|
--exclude-module PyQt5 \
|
||||||
|
--hidden-import=backports.tarfile \
|
||||||
|
--hidden-import=backports \
|
||||||
|
--hidden-import=jaraco.context \
|
||||||
|
--hidden-import=jaraco.text \
|
||||||
|
--hidden-import=jaraco.functools \
|
||||||
|
--name ${{ env.APP_NAME }} \
|
||||||
|
main.py
|
||||||
|
|
||||||
- name: Build executable with PyInstaller (Unix)
|
- name: Build executable with PyInstaller (Unix)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pyinstaller --onefile --windowed \
|
||||||
|
--add-data "build_info.json:." \
|
||||||
|
--add-data "build_info.json:core" \
|
||||||
|
--add-data "build_info.json:core/interface" \
|
||||||
|
--add-data "core/interface/assets:core/interface/assets" \
|
||||||
|
--icon "core/interface/assets/logo.png" \
|
||||||
|
--exclude-module PyQt6 \
|
||||||
|
--exclude-module PyQt5 \
|
||||||
|
--hidden-import=backports.tarfile \
|
||||||
|
--hidden-import=backports \
|
||||||
|
--hidden-import=jaraco.context \
|
||||||
|
--hidden-import=jaraco.text \
|
||||||
|
--hidden-import=jaraco.functools \
|
||||||
|
--name ${{ env.APP_NAME }} \
|
||||||
|
main.py
|
||||||
|
|
||||||
- name: Make executable (Unix)
|
- name: Make executable (Unix)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
@@ -126,7 +143,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
if: success()
|
if: success()
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -139,17 +156,36 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||||
cp artifacts/ubuntu-latest-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
|
cp artifacts/ubuntu-22.04-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
|
||||||
cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
|
cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
|
||||||
cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
|
cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
|
||||||
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
|
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
|
||||||
ls -la release/
|
ls -la release/
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
run: |
|
||||||
|
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||||
|
cat > release_notes.md << EOF
|
||||||
|
## Development Build
|
||||||
|
|
||||||
|
**Commit:** [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
$(git log -1 --pretty=format:"- %s" ${{ github.sha }})
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
|
||||||
|
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
|
||||||
|
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
|
||||||
|
EOF
|
||||||
|
cat release_notes.md
|
||||||
|
|
||||||
- name: Create Release (Public)
|
- name: Create Release (Public)
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.build.outputs.version }}
|
tag_name: ${{ needs.build.outputs.version }}
|
||||||
name: ${{ needs.build.outputs.version }}
|
name: ${{ needs.build.outputs.version }}
|
||||||
|
body_path: release_notes.md
|
||||||
files: |
|
files: |
|
||||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
|
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
|
||||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
|
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
|
||||||
|
|||||||
@@ -214,3 +214,7 @@ __marimo__/
|
|||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# Local build files
|
||||||
|
build-local.ps1
|
||||||
|
build-local.bat
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# SoftwareManager - A Download Manager/Launcher
|
# SoftwareManager
|
||||||
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker and the official M0nkrus Telegram channel.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- VPN Binding (impl. soon)
|
- Searching & Downloading Software from various pages
|
||||||
- Bandwidth/Download speed limiting
|
- Network Interface Binding
|
||||||
- Start downloads via a predefined magnet URI.
|
- Simple UI, Speed Limiting etc.
|
||||||
- View detailed download information (name, speed, and status).
|
- No Logins required
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
1. Download the binary file for your operating system from releases
|
1. Download the binary file for your operating system from releases
|
||||||
@@ -14,19 +14,44 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
|||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
1. Ensure Python 3.x is installed on your system.
|
1. Ensure Python 3.x is installed on your system.
|
||||||
|
2. Clone the Repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/KeksPirates/SoftwareManager.git && cd SoftwareManager
|
||||||
|
```
|
||||||
2. Install Dependencies:
|
2. Install Dependencies:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
3. Run main.py:
|
3. Run main.py:
|
||||||
```bash
|
```bash
|
||||||
python3 main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Additional Information
|
||||||
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of its branch.
|
- SoftwareManager is still in Development and most likely contains issues. Please report any bugs you find via the Issues tab. Contributions are also greatly appreciated.
|
||||||
- This tool is for educational purposes only. Use it responsibly.
|
- MacOS Builds haven't been properly tested - We're thankful to receive feedback.
|
||||||
|
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of the server branch.
|
||||||
|
|
||||||
|
## Python Dependencies
|
||||||
|
(also included in requirements.txt)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
requests (2.32.2)
|
||||||
|
PySide6 (6.10.1)
|
||||||
|
beautifulsoup (44.13.5)
|
||||||
|
darkdetect (0.7.1)
|
||||||
|
pyinstaller (6.15.0)
|
||||||
|
PyQtDarkTheme-fork (2.3.4)
|
||||||
|
aiohttp (3.13.0)
|
||||||
|
plyer (2.1.0)
|
||||||
|
psutil (7.1.0)
|
||||||
|
libtorrent (2.0.11)
|
||||||
|
libtorrent-windows-dll (0.0.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Activity
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
@@ -38,6 +63,11 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
<a href="https://github.com/KeksPirates/SoftwareManager/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=KeksPirates/SoftwareManager" />
|
||||||
|
</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
|
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"Version": "dev"
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import requests
|
import requests
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
|
||||||
def scrape_monkrus_telegram(query):
|
def scrape_monkrus_telegram(query):
|
||||||
post = 100
|
post = 100
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import requests
|
import requests
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
|
||||||
|
|
||||||
def scrape_rutracker(search_text):
|
def scrape_rutracker(search_text):
|
||||||
search = requests.get(f"{state.api_url}/search/{search_text}")
|
search = requests.get(f"{state.api_url}/search?q={search_text}")
|
||||||
if state.debug:
|
consoleLog("Sent request to server")
|
||||||
print("Sent request to server")
|
|
||||||
if search:
|
if search:
|
||||||
try:
|
try:
|
||||||
return search.text
|
return search.text
|
||||||
except Exception:
|
except Exception:
|
||||||
if state.debug:
|
consoleLog("No results found / No response from server")
|
||||||
print("No results found / No response from server")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import requests
|
import requests
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import threading
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from core.utils.general.wrappers import run_thread
|
||||||
|
|
||||||
|
|
||||||
global url_uztracker
|
global url_uztracker
|
||||||
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
||||||
|
|
||||||
async def init_uztracker():
|
def init_uztracker():
|
||||||
global up
|
global up
|
||||||
global soup
|
global soup
|
||||||
try:
|
try:
|
||||||
@@ -17,22 +19,19 @@ async def init_uztracker():
|
|||||||
up = True
|
up = True
|
||||||
else:
|
else:
|
||||||
up = False
|
up = False
|
||||||
if state.debug:
|
consoleLog(f"Uztracker seems down, status code: {response.status_code}")
|
||||||
print(f"Uztracker seems down, status code: {response.status_code}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if state.debug:
|
consoleLog(f"Request Exception on {url_uztracker}:")
|
||||||
print(f"\nRequest Exception on {url_uztracker}:")
|
consoleLog(str(e))
|
||||||
print(e)
|
consoleLog("Is the Site down?")
|
||||||
print("\nIs the Site down?")
|
|
||||||
up = False
|
up = False
|
||||||
|
|
||||||
asyncio.run(init_uztracker())
|
run_thread(threading.Thread(target=init_uztracker))
|
||||||
|
|
||||||
def scrape_uztracker(search):
|
def scrape_uztracker(search):
|
||||||
if up:
|
if up:
|
||||||
search_url = url_uztracker + search
|
search_url = url_uztracker + search
|
||||||
if state.debug:
|
consoleLog(search_url)
|
||||||
print(search_url)
|
|
||||||
result = False
|
result = False
|
||||||
global results
|
global results
|
||||||
global resulttitles
|
global resulttitles
|
||||||
@@ -60,12 +59,11 @@ def scrape_uztracker(search):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
if result and state.debug:
|
if result:
|
||||||
print(f"Failed to fetch {search_url}: {e}")
|
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog("Error: Uztracker down")
|
||||||
print("Error: Uztracker down")
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def get_post_title(post_url):
|
def get_post_title(post_url):
|
||||||
@@ -76,11 +74,9 @@ def get_post_title(post_url):
|
|||||||
if maintitle:
|
if maintitle:
|
||||||
return maintitle.text
|
return maintitle.text
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog("Program not found")
|
||||||
print("Program not found")
|
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog("Error: Uztracker down")
|
||||||
print("Error: Uztracker down")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -1,5 +1,9 @@
|
|||||||
from core.utils.config.settings import save_settings
|
from core.utils.config.settings import save_settings
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from core.interface.utils.tabhelper import general_tab
|
||||||
|
from core.interface.utils.tabhelper import paths_tab
|
||||||
|
from core.interface.utils.tabhelper import network_tab
|
||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
@@ -11,65 +15,71 @@ from PySide6.QtWidgets import (
|
|||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
)
|
QFileDialog,
|
||||||
|
QComboBox,
|
||||||
|
QTabWidget,
|
||||||
|
)
|
||||||
|
import platform
|
||||||
|
|
||||||
|
|
||||||
def settings_dialog(self):
|
def settings_dialog(self):
|
||||||
|
|
||||||
if state.debug:
|
consoleLog("Settings dialog opened")
|
||||||
print("Settings dialog opened")
|
|
||||||
dialog = QDialog(self)
|
dialog = QDialog(self)
|
||||||
dialog.setWindowTitle("Settings")
|
dialog.setWindowTitle("Settings")
|
||||||
dialog.setFixedSize(800, 400)
|
dialog.setFixedSize(700, 450)
|
||||||
|
|
||||||
dialog.setLayout(QVBoxLayout())
|
dialog.setLayout(QVBoxLayout())
|
||||||
dialog.layout().addWidget(QLabel("Settings"))
|
|
||||||
|
|
||||||
def close_settings():
|
def close_settings():
|
||||||
dialog.reject()
|
dialog.reject()
|
||||||
|
|
||||||
checkbox_container = QWidget()
|
update_checkbox_container = QWidget()
|
||||||
checkbox_layout = QHBoxLayout()
|
update_checkbox_layout = QHBoxLayout()
|
||||||
|
|
||||||
# ignore updates checkbox
|
# ignore updates checkbox
|
||||||
checkbox = QCheckBox()
|
|
||||||
checkbox_container.setLayout(checkbox_layout)
|
|
||||||
checkbox_layout.addWidget(QLabel("Ignore Updates: "))
|
|
||||||
checkbox_layout.addStretch()
|
|
||||||
checkbox.setChecked(state.ignore_updates)
|
|
||||||
checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
|
|
||||||
checkbox_layout.addWidget(checkbox)
|
|
||||||
dialog.layout().addWidget(checkbox_container)
|
|
||||||
|
|
||||||
##################
|
if platform.system() == "Windows":
|
||||||
# THREAD SETTING #
|
update_checkbox = QCheckBox()
|
||||||
##################
|
update_checkbox_container.setLayout(update_checkbox_layout)
|
||||||
thread_box = QSpinBox()
|
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
|
||||||
thread_box.setMinimum(1)
|
update_checkbox_layout.addStretch()
|
||||||
thread_box.setMaximum(16)
|
update_checkbox.setChecked(state.ignore_updates)
|
||||||
thread_box.setValue(state.aria2_threads)
|
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
|
||||||
|
update_checkbox_layout.addWidget(update_checkbox)
|
||||||
|
dialog.layout().addWidget(update_checkbox_container)
|
||||||
|
|
||||||
# container for tight space
|
# auto-resume downloads checkbox
|
||||||
thread_container = QWidget()
|
|
||||||
thread_layout = QHBoxLayout()
|
|
||||||
|
|
||||||
thread_layout.addWidget(QLabel("Threads:"))
|
autoresume_container = QWidget()
|
||||||
thread_layout.addWidget(thread_box)
|
autoresume_layout = QHBoxLayout()
|
||||||
thread_container.setLayout(thread_layout)
|
|
||||||
thread_container.setMaximumHeight(80)
|
|
||||||
|
|
||||||
|
|
||||||
# Dimensions
|
autoresume_checkbox = QCheckBox()
|
||||||
thread_box.setFixedWidth(90)
|
autoresume_container.setLayout(autoresume_layout)
|
||||||
thread_box.setFixedHeight(30)
|
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
|
||||||
|
|
||||||
dialog.layout().addWidget(thread_container)
|
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 #
|
# SERVER SETTING #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
api_url_container = QWidget()
|
api_url_container = QWidget()
|
||||||
api_url_layout = QHBoxLayout()
|
api_url_layout = QHBoxLayout()
|
||||||
|
|
||||||
@@ -79,7 +89,6 @@ def settings_dialog(self):
|
|||||||
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||||
api_url_container.setLayout(api_url_layout)
|
api_url_container.setLayout(api_url_layout)
|
||||||
api_url.setText(state.api_url)
|
api_url.setText(state.api_url)
|
||||||
dialog.layout().addWidget(api_url_container)
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
# DOWNLOAD PATH #
|
# DOWNLOAD PATH #
|
||||||
@@ -94,25 +103,130 @@ def settings_dialog(self):
|
|||||||
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||||
download_path_container.setLayout(download_path_layout)
|
download_path_container.setLayout(download_path_layout)
|
||||||
download_path.setText(state.download_path)
|
download_path.setText(state.download_path)
|
||||||
dialog.layout().addWidget(download_path_container)
|
|
||||||
|
def browse_download_path():
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
|
||||||
|
if dir_path:
|
||||||
|
download_path.setText(dir_path)
|
||||||
|
|
||||||
|
browse_button = QPushButton("📁")
|
||||||
|
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):"))
|
||||||
|
image_path_layout.addWidget(image_path)
|
||||||
|
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.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("📁")
|
||||||
|
image_path_layout.addWidget(browse_button)
|
||||||
|
browse_button.clicked.connect(browse_image_path)
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# SPEED LIMITING #
|
# SPEED LIMITING #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
speed_limit_container = QWidget()
|
down_speed_limit_container = QWidget()
|
||||||
speed_limit_layout = QHBoxLayout()
|
down_speed_limit_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
down_speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
|
||||||
|
down_speed_limit = QSpinBox()
|
||||||
|
down_speed_limit.setMinimum(0)
|
||||||
|
down_speed_limit.setMaximum(10000000)
|
||||||
|
down_speed_limit.setValue(state.down_speed_limit)
|
||||||
|
down_speed_limit_container.setLayout(down_speed_limit_layout)
|
||||||
|
down_speed_limit_layout.addWidget(down_speed_limit)
|
||||||
|
down_speed_limit.setFixedWidth(180)
|
||||||
|
down_speed_limit.setFixedHeight(30)
|
||||||
|
|
||||||
|
up_speed_limit_container = QWidget()
|
||||||
|
up_speed_limit_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
up_speed_limit_layout.addWidget(QLabel("Max Upload Speed (KiB, 0 for unlimited): "))
|
||||||
|
up_speed_limit = QSpinBox()
|
||||||
|
up_speed_limit.setMinimum(0)
|
||||||
|
up_speed_limit.setMaximum(10000000)
|
||||||
|
up_speed_limit.setValue(state.up_speed_limit)
|
||||||
|
up_speed_limit_container.setLayout(up_speed_limit_layout)
|
||||||
|
up_speed_limit_layout.addWidget(up_speed_limit)
|
||||||
|
up_speed_limit.setFixedWidth(180)
|
||||||
|
up_speed_limit.setFixedHeight(30)
|
||||||
|
|
||||||
|
######################
|
||||||
|
# CONNECTION CONFIGS #
|
||||||
|
######################
|
||||||
|
|
||||||
|
max_connections_container = QWidget()
|
||||||
|
max_connections_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
max_connections_layout.addWidget(QLabel("Max Connections: "))
|
||||||
|
max_connections = QSpinBox()
|
||||||
|
max_connections.setMinimum(0)
|
||||||
|
max_connections.setMaximum(10000000)
|
||||||
|
max_connections.setValue(state.max_connections)
|
||||||
|
max_connections_container.setLayout(max_connections_layout)
|
||||||
|
max_connections_layout.addWidget(max_connections)
|
||||||
|
max_connections.setFixedWidth(180)
|
||||||
|
max_connections.setFixedHeight(30)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# DOWNLOAD CONFIGS #
|
||||||
|
####################
|
||||||
|
|
||||||
|
max_downloads_container = QWidget()
|
||||||
|
max_downloads_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
max_downloads_layout.addWidget(QLabel("Max Downloads: "))
|
||||||
|
max_downloads = QSpinBox()
|
||||||
|
max_downloads.setMinimum(0)
|
||||||
|
max_downloads.setMaximum(10000000)
|
||||||
|
max_downloads.setValue(state.max_downloads)
|
||||||
|
max_downloads_container.setLayout(max_downloads_layout)
|
||||||
|
max_downloads_layout.addWidget(max_downloads)
|
||||||
|
max_downloads.setFixedWidth(180)
|
||||||
|
max_downloads.setFixedHeight(30)
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# INTERFACE BINDING #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
interface_container = QWidget()
|
||||||
|
interface_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
interface_label = QLabel("Network Interface:")
|
||||||
|
interface_layout.addWidget(interface_label)
|
||||||
|
interface_select = QComboBox()
|
||||||
|
interface_select.addItems(["None"] + state.interfaces)
|
||||||
|
|
||||||
|
target = state.bound_interface if state.bound_interface else "None"
|
||||||
|
|
||||||
|
index = interface_select.findText(target)
|
||||||
|
if index >= 0:
|
||||||
|
interface_select.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
interface_select.setCurrentIndex(0)
|
||||||
|
|
||||||
|
interface_select.setFixedWidth(180)
|
||||||
|
interface_select.setFixedHeight(30)
|
||||||
|
interface_select.setFixedWidth(180)
|
||||||
|
interface_select.setFixedHeight(30)
|
||||||
|
interface_layout.addWidget(interface_select)
|
||||||
|
interface_container.setLayout(interface_layout)
|
||||||
|
|
||||||
speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
|
|
||||||
speed_limit = QSpinBox()
|
|
||||||
speed_limit.setMinimum(0)
|
|
||||||
speed_limit.setMaximum(10000000)
|
|
||||||
speed_limit.setValue(state.speed_limit)
|
|
||||||
speed_limit_container.setLayout(speed_limit_layout)
|
|
||||||
speed_limit_layout.addWidget(speed_limit)
|
|
||||||
speed_limit.setFixedWidth(180)
|
|
||||||
speed_limit.setFixedHeight(30)
|
|
||||||
dialog.layout().addWidget(speed_limit_container)
|
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# SAVE/CANCEL #
|
# SAVE/CANCEL #
|
||||||
@@ -122,12 +236,28 @@ def settings_dialog(self):
|
|||||||
|
|
||||||
save_btn = QPushButton("Save")
|
save_btn = QPushButton("Save")
|
||||||
cancel_btn = QPushButton("Cancel")
|
cancel_btn = QPushButton("Cancel")
|
||||||
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value()))
|
save_btn.clicked.connect(lambda: save_settings(
|
||||||
|
close_settings,
|
||||||
|
api_url.text(),
|
||||||
|
download_path.text(),
|
||||||
|
down_speed_limit.value(),
|
||||||
|
up_speed_limit.value(),
|
||||||
|
image_path.text(),
|
||||||
|
autoresume_checkbox.isChecked(),
|
||||||
|
max_connections.value(),
|
||||||
|
max_downloads.value(),
|
||||||
|
interface_select.currentText()))
|
||||||
|
|
||||||
cancel_btn.clicked.connect(dialog.reject)
|
cancel_btn.clicked.connect(dialog.reject)
|
||||||
layout.addWidget(cancel_btn)
|
layout.addWidget(cancel_btn)
|
||||||
layout.addWidget(save_btn)
|
layout.addWidget(save_btn)
|
||||||
|
|
||||||
|
self.tabs = QTabWidget()
|
||||||
|
self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs)
|
||||||
|
self.tab2 = paths_tab("Paths", download_path_container, image_path_container, self.tabs)
|
||||||
|
self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs)
|
||||||
|
|
||||||
|
dialog.layout().addWidget(self.tabs)
|
||||||
dialog.layout().addLayout(layout)
|
dialog.layout().addLayout(layout)
|
||||||
|
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|||||||
+450
-77
@@ -1,13 +1,11 @@
|
|||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
from PySide6 import QtCore
|
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
|
||||||
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QTableView,
|
QTableView,
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
QToolBar,
|
|
||||||
QLabel,
|
QLabel,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
@@ -16,37 +14,38 @@ from PySide6.QtWidgets import (
|
|||||||
QHeaderView,
|
QHeaderView,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
)
|
QTextEdit,
|
||||||
|
QStyledItemDelegate,
|
||||||
|
)
|
||||||
|
|
||||||
from PySide6.QtGui import QIcon, QAction, QCloseEvent
|
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent
|
||||||
import darkdetect
|
import darkdetect
|
||||||
import threading
|
import threading
|
||||||
import platform
|
import platform
|
||||||
import requests as r
|
import requests as r
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import libtorrent as lt
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
from core.utils.general.logs import consoleLog, remove_download_log, flush_log_buffer
|
||||||
from core.utils.general.wrappers import run_thread
|
from core.utils.general.wrappers import run_thread
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
from core.utils.network.download import download_selected
|
from core.utils.network.download import download_selected
|
||||||
from core.utils.network.update_checker import check_for_updates
|
from core.utils.network.update_checker import check_for_updates
|
||||||
from core.utils.general.shutdown import closehelper
|
|
||||||
from core.interface.utils.tabhelper import create_tab
|
from core.interface.utils.tabhelper import create_tab
|
||||||
from core.interface.utils.searchhelper import return_pressed
|
from core.interface.utils.searchhelper import return_pressed
|
||||||
from core.interface.dialogs.settings import settings_dialog
|
from core.interface.dialogs.settings import settings_dialog
|
||||||
from core.network.aria2_integration import dlprogress
|
from core.network.libtorrent_misc import cleanup_session
|
||||||
|
from core.utils.general.shutdown import closehelper
|
||||||
|
|
||||||
|
|
||||||
def download_update(latest_version):
|
def download_update(latest_version):
|
||||||
old_filename = f"SoftwareManager-dev-{state.version.replace('-dev', '')}-windows.exe"
|
|
||||||
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
||||||
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
||||||
|
|
||||||
print("Downloading update...")
|
print("Downloading update...", True)
|
||||||
if os.path.exists(old_filename):
|
|
||||||
os.remove(old_filename)
|
|
||||||
response = r.get(url, allow_redirects=True)
|
response = r.get(url, allow_redirects=True)
|
||||||
with open(new_filename, "wb") as f:
|
with open(new_filename, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
@@ -54,12 +53,47 @@ def download_update(latest_version):
|
|||||||
raise FileNotFoundError("Executable not found")
|
raise FileNotFoundError("Executable not found")
|
||||||
subprocess.Popen([new_filename], shell=True)
|
subprocess.Popen([new_filename], shell=True)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
msg = QMessageBox()
|
||||||
|
msg.setIcon(QMessageBox.Icon.Information)
|
||||||
|
msg.setWindowTitle("New Version")
|
||||||
|
msg.setText("New version installed.")
|
||||||
|
msg.setInformativeText("Please remove the old exe.")
|
||||||
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||||
|
log_signal = Signal(str) # Thread-safe signal for logging
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
MainWindow._instance = self
|
||||||
|
self.log_signal.connect(self._on_log_signal)
|
||||||
|
|
||||||
|
def get_asset_path(filename):
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
base_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename)
|
||||||
|
if os.path.exists(asset_path):
|
||||||
|
return asset_path
|
||||||
|
|
||||||
|
return os.path.join('core', 'interface', 'assets', filename)
|
||||||
|
|
||||||
|
self.setWindowIcon(QIcon(get_asset_path("logo.png")))
|
||||||
|
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
appid = 'SoftwareManager'
|
||||||
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f"Could not set app ID: {e}")
|
||||||
|
|
||||||
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
|
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
|
||||||
if os.path.exists(build_info_path):
|
if os.path.exists(build_info_path):
|
||||||
@@ -71,19 +105,20 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
if state.ignore_updates is False:
|
if state.ignore_updates is False:
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
result = check_for_updates()
|
result = check_for_updates()
|
||||||
assets, latest_version = result
|
if result != (None, None):
|
||||||
|
assets, latest_version = result
|
||||||
|
|
||||||
if assets:
|
if assets:
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
msg.setIcon(QMessageBox.Information)
|
msg.setIcon(QMessageBox.Icon.Information)
|
||||||
msg.setWindowTitle("Update Available")
|
msg.setWindowTitle("Update Available")
|
||||||
msg.setText("A new version is available.")
|
msg.setText("A new version is available.")
|
||||||
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.")
|
msg.setInformativeText("Press Ok to download the update.")
|
||||||
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||||
|
|
||||||
response = msg.exec_()
|
response = msg.exec_()
|
||||||
if response == QMessageBox.Ok:
|
if response == QMessageBox.StandardButton.Ok:
|
||||||
download_update(latest_version)
|
download_update(latest_version)
|
||||||
|
|
||||||
self.setWindowTitle("Software Manager")
|
self.setWindowTitle("Software Manager")
|
||||||
self.setGeometry(100, 100, 800, 600)
|
self.setGeometry(100, 100, 800, 600)
|
||||||
@@ -99,29 +134,44 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
|
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
|
||||||
|
|
||||||
self.dlbutton = QtWidgets.QPushButton("Download")
|
self.dlbutton = QtWidgets.QPushButton("Download")
|
||||||
|
self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.libraryList = QListWidget()
|
self.libraryList = QListWidget()
|
||||||
|
|
||||||
self.emptyResults = QLabel("No Results")
|
self.emptyResults = QLabel("No Results")
|
||||||
self.emptyResults.setAlignment(Qt.AlignCenter)
|
self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.emptyResults.hide()
|
self.emptyResults.hide()
|
||||||
|
|
||||||
self.download_model = None
|
self.download_model = None
|
||||||
self.downloadList = QTableView()
|
self.downloadList = QTableView()
|
||||||
self.emptyLibrary = QLabel("No items in library.")
|
self.emptyLibrary = QLabel("No items in library.")
|
||||||
self.emptyDownload = QLabel("No items in downloads.")
|
self.emptyDownload = QLabel("No items in downloads.")
|
||||||
self.progressbar = QProgressBar()
|
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.qtablewidget = QTableWidget()
|
|
||||||
|
|
||||||
self.qtablewidget.setColumnCount(2)
|
self.consoleLog = QTextEdit()
|
||||||
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
self.progressbar = QProgressBar()
|
||||||
|
|
||||||
|
flush_log_buffer()
|
||||||
|
|
||||||
|
# Table Widget for Item List
|
||||||
|
self.qtablewidget = QTableWidget()
|
||||||
|
|
||||||
|
self.qtablewidget.setColumnCount(4)
|
||||||
|
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.qtablewidget.verticalHeader().setVisible(False)
|
self.qtablewidget.verticalHeader().setVisible(False)
|
||||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||||
self.qtablewidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
|
||||||
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
|
|
||||||
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
|
|
||||||
|
|
||||||
header = self.qtablewidget.horizontalHeader()
|
header = self.qtablewidget.horizontalHeader()
|
||||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
header.setStretchLastSection(False)
|
||||||
|
|
||||||
|
self.qtablewidget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
self.qtablewidget.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
header = self.qtablewidget.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
header.resizeSection(1, 500)
|
header.resizeSection(1, 500)
|
||||||
|
|
||||||
container = QWidget()
|
container = QWidget()
|
||||||
@@ -132,45 +182,180 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
class DownloadModel(QAbstractTableModel):
|
class DownloadModel(QAbstractTableModel):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.headers = ["Name", "Status", "Progress", "Speed", "Size", "Total Size"]
|
self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"]
|
||||||
|
|
||||||
def rowCount(self, parent=QModelIndex()):
|
def rowCount(self, parent=QModelIndex()):
|
||||||
return len(state.downloads)
|
return len(state.active_downloads)
|
||||||
|
|
||||||
def columnCount(self, parent=QModelIndex()):
|
def columnCount(self, parent=QModelIndex()):
|
||||||
return len(self.headers)
|
return len(self.headers)
|
||||||
|
|
||||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
return self.headers[section]
|
return self.headers[section]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def data(self, index, role=Qt.DisplayRole):
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
download = state.downloads[index.row()]
|
|
||||||
col = index.column()
|
col = index.column()
|
||||||
|
|
||||||
|
if index.row() >= len(state.active_downloads) or index.row() < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
if col == 0:
|
if col == 0:
|
||||||
return download.name
|
|
||||||
elif col == 1:
|
|
||||||
return getattr(download, 'status', 'Downloading')
|
|
||||||
elif col == 2:
|
|
||||||
return f"{int(download.progress)}%"
|
|
||||||
elif col == 3:
|
|
||||||
return f"{download.download_speed_string()}"
|
|
||||||
elif col == 4:
|
|
||||||
pass
|
pass
|
||||||
|
elif col == 1:
|
||||||
|
return status.name if status.has_metadata else "Fetching metadata..."
|
||||||
|
elif col == 2:
|
||||||
|
if status.state == lt.torrent_status.downloading:
|
||||||
|
return "Downloading"
|
||||||
|
elif status.state == lt.torrent_status.seeding:
|
||||||
|
return "Seeding"
|
||||||
|
elif status.paused:
|
||||||
|
return "Paused"
|
||||||
|
else:
|
||||||
|
return "Queued"
|
||||||
|
elif col == 3:
|
||||||
|
return f"{status.progress * 100:.1f}%"
|
||||||
|
elif col == 4:
|
||||||
|
speed_kbs = status.download_rate / 1024
|
||||||
|
if speed_kbs > 1024:
|
||||||
|
return f"{speed_kbs / 1024:.1f} MB/s"
|
||||||
|
else:
|
||||||
|
return f"{speed_kbs:.1f} kB/s"
|
||||||
elif col == 5:
|
elif col == 5:
|
||||||
return f"{download.total_length_string()}"
|
downloaded_mb = status.total_wanted_done / (1024 * 1024)
|
||||||
|
if downloaded_mb > 1024:
|
||||||
|
return f"{downloaded_mb / 1024:.2f} GB"
|
||||||
|
else:
|
||||||
|
return f"{downloaded_mb:.1f} MB"
|
||||||
|
elif col == 6:
|
||||||
|
total_mb = status.total_wanted / (1024 * 1024)
|
||||||
|
if total_mb > 1024:
|
||||||
|
return f"{total_mb / 1024:.2f} GB"
|
||||||
|
else:
|
||||||
|
return f"{total_mb:.1f} MB"
|
||||||
|
elif col == 7:
|
||||||
|
if status.download_rate > 0:
|
||||||
|
bytes_left = status.total_wanted - status.total_wanted_done
|
||||||
|
eta_seconds = bytes_left / status.download_rate
|
||||||
|
|
||||||
|
if eta_seconds < 60:
|
||||||
|
return f"{int(eta_seconds)}s"
|
||||||
|
elif eta_seconds < 3600:
|
||||||
|
minutes = int(eta_seconds / 60)
|
||||||
|
seconds = int(eta_seconds % 60)
|
||||||
|
return f"{minutes}m {seconds}s"
|
||||||
|
else:
|
||||||
|
hours = int(eta_seconds / 3600)
|
||||||
|
minutes = int((eta_seconds % 3600) / 60)
|
||||||
|
return f"{hours}h {minutes}m"
|
||||||
|
else:
|
||||||
|
return "∞" if status.paused else "Stalled"
|
||||||
|
|
||||||
|
if role == Qt.UserRole and index.column() == 0:
|
||||||
|
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
return magnetdl.status().paused
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def toggle_pause_resume(self, row):
|
||||||
|
|
||||||
|
if row >= len(state.active_downloads) or row < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[row]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
if status.state == lt.torrent_status.seeding:
|
||||||
|
save_path = magnetdl.save_path()
|
||||||
|
if save_path and os.path.exists(save_path):
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
os.startfile(os.path.normpath(save_path))
|
||||||
|
elif platform.system() == "Linux":
|
||||||
|
subprocess.Popen(["xdg-open", save_path])
|
||||||
|
elif platform.system() == "Darwin":
|
||||||
|
subprocess.Popen(["open", save_path])
|
||||||
|
return
|
||||||
|
|
||||||
|
is_paused = bool(magnetdl.flags() & lt.torrent_flags.paused)
|
||||||
|
if is_paused:
|
||||||
|
magnetdl.set_flags(lt.torrent_flags.auto_managed)
|
||||||
|
magnetdl.resume()
|
||||||
|
consoleLog(f"Resumed download: {status.name}", True)
|
||||||
|
else:
|
||||||
|
magnetdl.unset_flags(lt.torrent_flags.auto_managed)
|
||||||
|
magnetdl.pause()
|
||||||
|
consoleLog(f"Paused download: {status.name}", True)
|
||||||
|
|
||||||
|
idx = self.index(row, 0)
|
||||||
|
self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
|
||||||
|
|
||||||
|
class PauseResumeDelegate(QStyledItemDelegate):
|
||||||
|
clicked = Signal(int)
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
button = editor.findChild(QtWidgets.QPushButton)
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
if button:
|
||||||
|
if status.state == lt.torrent_status.seeding:
|
||||||
|
button.setText("📁")
|
||||||
|
else:
|
||||||
|
button.setText("▶︎" if status.paused else "⏸︎")
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
widget = QWidget(parent)
|
||||||
|
widget.setStyleSheet("border: none;")
|
||||||
|
layout = QHBoxLayout(widget)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
if status.state == lt.torrent_status.seeding:
|
||||||
|
btnPause = QtWidgets.QPushButton("📁")
|
||||||
|
else:
|
||||||
|
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎")
|
||||||
|
btnPause.setFixedSize(40, 30)
|
||||||
|
btnPause.clicked.connect(lambda: self.clicked.emit(index.row()))
|
||||||
|
layout.addStretch()
|
||||||
|
layout.addWidget(btnPause)
|
||||||
|
layout.addStretch()
|
||||||
|
widget.setLayout(layout)
|
||||||
|
return widget
|
||||||
|
def editorEvent(self, event, model, option, index):
|
||||||
|
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
|
||||||
|
self.clicked.emit(index.row())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
self.download_model = DownloadModel()
|
self.download_model = DownloadModel()
|
||||||
self.downloadList.setModel(self.download_model)
|
self.downloadList.setModel(self.download_model)
|
||||||
self.downloadList.horizontalHeader().setStretchLastSection(False)
|
self.downloadList.horizontalHeader().setStretchLastSection(False)
|
||||||
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
self.downloadList.setSelectionBehavior(QTableView.SelectRows)
|
self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||||
self.downloadList.setAttribute(Qt.WA_TranslucentBackground)
|
self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground)
|
self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
download_model = self.download_model
|
||||||
|
|
||||||
|
def on_pause_resume_clicked(row):
|
||||||
|
download_model.toggle_pause_resume(row)
|
||||||
|
|
||||||
|
delegate = PauseResumeDelegate(self.downloadList)
|
||||||
|
self.downloadList.setItemDelegateForColumn(0, delegate)
|
||||||
|
delegate.clicked.connect(on_pause_resume_clicked)
|
||||||
|
|
||||||
# download button triggers
|
# download button triggers
|
||||||
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
|
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
|
||||||
@@ -180,55 +365,137 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
|
|
||||||
# Tabs
|
# Tabs
|
||||||
self.tabs = QTabWidget()
|
self.tabs = QTabWidget()
|
||||||
|
self.tabs.setStyleSheet("""
|
||||||
|
QTabBar::tab {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
QTabWidget::pane {
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
self.horizontal_layout = QHBoxLayout()
|
self.horizontal_layout = QHBoxLayout()
|
||||||
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
|
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
|
||||||
self.horizontal_layout.addWidget(self.qtablewidget)
|
self.horizontal_layout.addWidget(self.qtablewidget)
|
||||||
|
|
||||||
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
|
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
|
||||||
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
|
# self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
|
||||||
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
|
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
|
||||||
|
|
||||||
|
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.SmoothTransformation)
|
||||||
|
self.pixmap = QPixmap.fromImage(self.image)
|
||||||
|
self.overlay_label = QLabel(self)
|
||||||
|
self.overlay_label.setPixmap(self.pixmap)
|
||||||
|
self.overlay_label.adjustSize()
|
||||||
|
self.overlay_label.raise_()
|
||||||
|
|
||||||
|
# offset_x = -1350
|
||||||
|
# offset_y = -550
|
||||||
|
x = self.width() - self.overlay_label.width() # - offset_x
|
||||||
|
y = self.height() - self.overlay_label.height() # - offset_y
|
||||||
|
self.overlay_label.move(x, y)
|
||||||
|
|
||||||
|
# temporarily disabled
|
||||||
|
# state.image_changed.connect(self.update_image_overlay)
|
||||||
|
|
||||||
containerLayout.addWidget(self.tabs)
|
|
||||||
|
self.corner_widget = QWidget()
|
||||||
|
self.corner_layout = QHBoxLayout(self.corner_widget)
|
||||||
|
self.corner_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
|
|
||||||
|
|
||||||
toolbar = QToolBar("Main Toolbar")
|
|
||||||
self.addToolBar(toolbar)
|
|
||||||
toolbar.setLayoutDirection(Qt.RightToLeft)
|
|
||||||
|
|
||||||
self.tracker_list = QComboBox()
|
self.tracker_list = QComboBox()
|
||||||
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
|
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
|
||||||
|
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.tracker_list.activated.connect(self.set_tracker)
|
self.tracker_list.activated.connect(self.set_tracker)
|
||||||
|
self.corner_layout.addWidget(self.tracker_list)
|
||||||
|
|
||||||
|
self.settings_btn = QtWidgets.QToolButton()
|
||||||
|
self.settings_btn.setIconSize(QSize(21, 21))
|
||||||
|
self.settings_btn.setStyleSheet("background: transparent;")
|
||||||
if darkdetect.isDark():
|
if darkdetect.isDark():
|
||||||
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self)
|
self.settings_btn.setIcon(QIcon(get_asset_path("settings_white.png")))
|
||||||
else:
|
else:
|
||||||
settings_action = QAction(QIcon("core/interface/assets/settings_black.png"), "Settings", self)
|
self.settings_btn.setIcon(QIcon(get_asset_path("settings_black.png")))
|
||||||
|
self.settings_btn.setToolTip("Settings")
|
||||||
|
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
|
||||||
|
|
||||||
|
self.corner_layout.addWidget(self.settings_btn)
|
||||||
|
|
||||||
settings_action.triggered.connect(lambda: settings_dialog(self))
|
self.tab_wrapper = QWidget()
|
||||||
toolbar.addAction(settings_action)
|
self.tab_layout = QVBoxLayout()
|
||||||
toolbar.addWidget(self.tracker_list)
|
self.tab_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.tab_layout.setSpacing(0)
|
||||||
|
|
||||||
|
|
||||||
toolbar.setMovable(False)
|
self.tab_bar_layout = QHBoxLayout()
|
||||||
|
self.tab_bar_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.tab_bar_layout.setSpacing(0)
|
||||||
|
|
||||||
self.progressbar.setValue(0)
|
self.tab_bar_layout.addWidget(self.tabs.tabBar(), stretch=0)
|
||||||
containerLayout.addWidget(self.progressbar)
|
self.tab_bar_layout.addStretch()
|
||||||
|
self.tab_bar_layout.addWidget(self.corner_widget, stretch=0)
|
||||||
|
|
||||||
self.progress_timer = QTimer()
|
self.tab_layout.addLayout(self.tab_bar_layout)
|
||||||
self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress)))
|
self.tab_layout.addWidget(self.tabs)
|
||||||
self.progress_timer.start(1000)
|
|
||||||
|
|
||||||
|
self.tab_wrapper.setLayout(self.tab_layout)
|
||||||
|
|
||||||
|
|
||||||
|
containerLayout.addWidget(self.tab_wrapper)
|
||||||
|
containerLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
self.consoleLog.setReadOnly(True)
|
||||||
|
self.consoleLog.setFixedHeight(150)
|
||||||
|
containerLayout.addWidget(self.consoleLog)
|
||||||
|
|
||||||
self.download_timer = QTimer()
|
self.download_timer = QTimer()
|
||||||
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
|
self.download_timer.timeout.connect(self.download_list_update)
|
||||||
self.download_timer.start(500)
|
self.download_timer.start(500)
|
||||||
|
|
||||||
|
self.active_timer = QTimer()
|
||||||
|
self.active_timer.timeout.connect(self.show_empty_downloads)
|
||||||
|
self.active_timer.start(500)
|
||||||
|
|
||||||
|
self.context_menu = QtWidgets.QMenu(self)
|
||||||
|
self.context_menu.addAction("Open Containing Folder", self.openFolderAction)
|
||||||
|
self.context_menu.addAction("Copy Magnet URI", self.copyMagnetURIAction)
|
||||||
|
self.context_menu.addAction("Cancel Download", self.cancelDownloadAction)
|
||||||
|
self.context_menu.addAction("Delete File", self.deleteFileAction)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_log(text):
|
||||||
|
if MainWindow._instance:
|
||||||
|
MainWindow._instance.log_signal.emit(text)
|
||||||
|
|
||||||
|
def _on_log_signal(self, text):
|
||||||
|
self.consoleLog.append(text)
|
||||||
|
self.consoleLog.verticalScrollBar().setValue(
|
||||||
|
self.consoleLog.verticalScrollBar().maximum()
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
def download_list_update(self):
|
def download_list_update(self):
|
||||||
if self.download_model:
|
if self.download_model:
|
||||||
self.download_model.layoutAboutToBeChanged.emit()
|
self.download_model.layoutAboutToBeChanged.emit()
|
||||||
self.download_model.layoutChanged.emit()
|
self.download_model.layoutChanged.emit()
|
||||||
|
for row in range(self.download_model.rowCount()):
|
||||||
|
idx = self.download_model.index(row, 0)
|
||||||
|
self.downloadList.closePersistentEditor(idx)
|
||||||
|
self.downloadList.openPersistentEditor(idx)
|
||||||
|
|
||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent):
|
def closeEvent(self, event: QCloseEvent):
|
||||||
closehelper()
|
closehelper()
|
||||||
@@ -237,14 +504,120 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
def set_tracker(self, _):
|
def set_tracker(self, _):
|
||||||
state.tracker = self.tracker_list.currentText()
|
state.tracker = self.tracker_list.currentText()
|
||||||
|
|
||||||
def update_progress(self):
|
|
||||||
progress = dlprogress()
|
|
||||||
self.progressbar.setValue(progress)
|
|
||||||
|
|
||||||
def show_empty_results(self, show: bool):
|
def show_empty_results(self, show: bool):
|
||||||
if show:
|
if show:
|
||||||
self.qtablewidget.hide()
|
self.qtablewidget.hide()
|
||||||
self.emptyResults.show()
|
self.emptyResults.show()
|
||||||
else:
|
else:
|
||||||
self.emptyResults.hide()
|
|
||||||
self.qtablewidget.show()
|
self.qtablewidget.show()
|
||||||
|
self.emptyResults.hide()
|
||||||
|
|
||||||
|
def show_empty_downloads(self):
|
||||||
|
if len(state.active_downloads) > 0:
|
||||||
|
self.emptyDownload.hide()
|
||||||
|
self.downloadList.show()
|
||||||
|
else:
|
||||||
|
self.emptyDownload.show()
|
||||||
|
self.downloadList.hide()
|
||||||
|
|
||||||
|
# thank you claude
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
table_width = self.qtablewidget.viewport().width()
|
||||||
|
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event: QContextMenuEvent):
|
||||||
|
if self.downloadList.underMouse():
|
||||||
|
pos = self.downloadList.viewport().mapFromGlobal(event.globalPos())
|
||||||
|
index = self.downloadList.indexAt(pos)
|
||||||
|
|
||||||
|
if index.isValid():
|
||||||
|
row = index.row()
|
||||||
|
self._context_menu_row = row
|
||||||
|
self.context_menu.exec(event.globalPos())
|
||||||
|
else:
|
||||||
|
super().contextMenuEvent(event)
|
||||||
|
|
||||||
|
def openFolderAction(self):
|
||||||
|
if not hasattr(self, '_context_menu_row'):
|
||||||
|
return
|
||||||
|
|
||||||
|
row = self._context_menu_row
|
||||||
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
|
return
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[row]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
|
||||||
|
download_path = magnetdl.save_path()
|
||||||
|
|
||||||
|
if download_path and os.path.exists(download_path):
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
os.startfile(os.path.normpath(download_path))
|
||||||
|
elif platform.system() == "Linux":
|
||||||
|
subprocess.Popen(["xdg-open", download_path])
|
||||||
|
elif platform.system() == "Darwin":
|
||||||
|
subprocess.Popen(["open", download_path])
|
||||||
|
|
||||||
|
def copyMagnetURIAction(self):
|
||||||
|
if not hasattr(self, '_context_menu_row'):
|
||||||
|
return
|
||||||
|
|
||||||
|
row = self._context_menu_row
|
||||||
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
|
return
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[row]
|
||||||
|
|
||||||
|
clipboard = QtWidgets.QApplication.clipboard()
|
||||||
|
clipboard.setText(magnet_link)
|
||||||
|
|
||||||
|
def cancelDownloadAction(self):
|
||||||
|
if not hasattr(self, '_context_menu_row'):
|
||||||
|
return
|
||||||
|
|
||||||
|
row = self._context_menu_row
|
||||||
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
|
return
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[row]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
|
||||||
|
confirm = QMessageBox.question(self, "Cancel Download", f"Are you sure you want to cancel the download of '{magnetdl.status().name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
if confirm == QMessageBox.StandardButton.Yes:
|
||||||
|
del state.active_downloads[magnet_link]
|
||||||
|
remove_download_log(magnet_link)
|
||||||
|
consoleLog(f"Cancelled download: {magnetdl.status().name}", True)
|
||||||
|
|
||||||
|
def deleteFileAction(self):
|
||||||
|
if not hasattr(self, '_context_menu_row'):
|
||||||
|
return
|
||||||
|
|
||||||
|
row = self._context_menu_row
|
||||||
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
|
return
|
||||||
|
|
||||||
|
magnet_link = list(state.active_downloads.keys())[row]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
|
status = magnetdl.status()
|
||||||
|
save_path = status.save_path
|
||||||
|
torrent_name = status.name
|
||||||
|
download_path = os.path.join(save_path, torrent_name)
|
||||||
|
|
||||||
|
confirm = QMessageBox.question(self, "Delete Files", f"Are you sure you want to delete the downloaded files of '{magnetdl.status().name}'? This action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
if confirm == QMessageBox.StandardButton.Yes:
|
||||||
|
if download_path and os.path.exists(download_path):
|
||||||
|
try:
|
||||||
|
if os.path.isfile(download_path):
|
||||||
|
os.remove(download_path)
|
||||||
|
del state.active_downloads[magnet_link]
|
||||||
|
remove_download_log(magnet_link)
|
||||||
|
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
|
||||||
|
else:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(download_path)
|
||||||
|
del state.active_downloads[magnet_link]
|
||||||
|
remove_download_log(magnet_link)
|
||||||
|
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f"Error deleting files: {e}", True)
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
from PySide6.QtWidgets import QTableWidgetItem
|
from PySide6.QtWidgets import QTableWidgetItem
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
from core.data.scrapers.uztracker import scrape_uztracker
|
from core.data.scrapers.uztracker import scrape_uztracker
|
||||||
from core.data.scrapers.rutracker import scrape_rutracker
|
from core.data.scrapers.rutracker import scrape_rutracker
|
||||||
from core.data.scrapers.monkrus import scrape_monkrus_telegram
|
from core.data.scrapers.monkrus import scrape_monkrus_telegram
|
||||||
from core.utils.network.jsonhandler import split_data, format_data
|
from core.utils.network.jsonhandler import split_data, format_data, format_data_m0nkrus
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
|
||||||
def return_pressed(self):
|
def return_pressed(self):
|
||||||
search_text = self.searchbar.text()
|
search_text = self.searchbar.text()
|
||||||
if search_text == "":
|
if search_text == "":
|
||||||
if state.debug:
|
consoleLog("Error: Can't search for nothing")
|
||||||
print("Error: Can't search for nothing")
|
|
||||||
return
|
return
|
||||||
if state.debug:
|
consoleLog(f"User searched for: {search_text}")
|
||||||
print("User searched for:", search_text)
|
|
||||||
|
|
||||||
if state.tracker == "uztracker":
|
if state.tracker == "uztracker":
|
||||||
response = scrape_uztracker(search_text)
|
response = scrape_uztracker(search_text)
|
||||||
@@ -28,13 +27,11 @@ def return_pressed(self):
|
|||||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||||
self.show_empty_results(False)
|
self.show_empty_results(False)
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog(f"No Results for {search_text}")
|
||||||
print(f"No Results for {search_text}")
|
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.show_empty_results(True)
|
self.show_empty_results(True)
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog(f"No response from uztracker")
|
||||||
print(f"No response from uztracker")
|
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.show_empty_results(True)
|
self.show_empty_results(True)
|
||||||
|
|
||||||
@@ -43,25 +40,27 @@ def return_pressed(self):
|
|||||||
if response:
|
if response:
|
||||||
_, state.posts, _, _, cached = split_data(response)
|
_, state.posts, _, _, cached = split_data(response)
|
||||||
if state.posts == []:
|
if state.posts == []:
|
||||||
if state.debug:
|
consoleLog(f"No Results for {search_text}")
|
||||||
print(f"No Results for {search_text}")
|
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.show_empty_results(True)
|
self.show_empty_results(True)
|
||||||
else:
|
else:
|
||||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
state.post_titles, _, state.post_author, state.post_seeders, state.post_leechers = format_data(state.posts)
|
||||||
self.show_empty_results(False)
|
self.show_empty_results(False)
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||||
for i, author in enumerate(state.post_author):
|
for i, author in enumerate(state.post_author):
|
||||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||||
for i, title in enumerate(state.post_titles):
|
for i, title in enumerate(state.post_titles):
|
||||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||||
|
for i, seeders in enumerate(state.post_seeders):
|
||||||
|
self.qtablewidget.setItem(i, 2, QTableWidgetItem(seeders))
|
||||||
|
for i, leechers in enumerate(state.post_leechers):
|
||||||
|
self.qtablewidget.setItem(i, 3, QTableWidgetItem(leechers))
|
||||||
if state.debug == True:
|
if state.debug == True:
|
||||||
print(f"Response Cached: {cached}")
|
consoleLog(f"Response Cached: {cached}")
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog(f"No response from rutracker")
|
||||||
print(f"No response from rutracker")
|
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.show_empty_results(True)
|
self.show_empty_results(True)
|
||||||
|
|
||||||
@@ -69,12 +68,11 @@ def return_pressed(self):
|
|||||||
state.posts = scrape_monkrus_telegram(search_text)
|
state.posts = scrape_monkrus_telegram(search_text)
|
||||||
|
|
||||||
if state.posts == []:
|
if state.posts == []:
|
||||||
if state.debug:
|
consoleLog(f"No Results for {search_text}")
|
||||||
print(f"No Results for {search_text}")
|
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.show_empty_results(True)
|
self.show_empty_results(True)
|
||||||
else:
|
else:
|
||||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
state.post_titles, _, state.post_author = format_data_m0nkrus(state.posts)
|
||||||
self.show_empty_results(False)
|
self.show_empty_results(False)
|
||||||
self.qtablewidget.clear()
|
self.qtablewidget.clear()
|
||||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||||
|
|||||||
@@ -12,6 +12,45 @@ def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
|
|||||||
layout.addWidget(software_list)
|
layout.addWidget(software_list)
|
||||||
if dlbutton:
|
if dlbutton:
|
||||||
layout.addWidget(dlbutton)
|
layout.addWidget(dlbutton)
|
||||||
tab.setLayout(layout)
|
tab.setLayout(layout)
|
||||||
tabs.addTab(tab, title)
|
tabs.addTab(tab, title)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
#################
|
||||||
|
# SETTINGS TABS #
|
||||||
|
#################
|
||||||
|
def general_tab(title, autoresume, update_checkbox, transparent_window, tabs):
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(autoresume)
|
||||||
|
layout.addWidget(update_checkbox)
|
||||||
|
layout.addWidget(transparent_window)
|
||||||
|
layout.addStretch()
|
||||||
|
tab.setLayout(layout)
|
||||||
|
tabs.addTab(tab, title)
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def paths_tab(title, download_path, image_path, tabs):
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(download_path)
|
||||||
|
layout.addWidget(image_path)
|
||||||
|
layout.addStretch()
|
||||||
|
tab.setLayout(layout)
|
||||||
|
tabs.addTab(tab, title)
|
||||||
|
return tab
|
||||||
|
|
||||||
|
|
||||||
|
def network_tab(title, network_interface, max_connections, max_downloads, up_speed_limit, down_speed_limit, api_url, tabs):
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(network_interface)
|
||||||
|
layout.addWidget(max_connections)
|
||||||
|
layout.addWidget(max_downloads)
|
||||||
|
layout.addWidget(up_speed_limit)
|
||||||
|
layout.addWidget(down_speed_limit)
|
||||||
|
layout.addWidget(api_url)
|
||||||
|
layout.addStretch()
|
||||||
|
tab.setLayout(layout)
|
||||||
|
tabs.addTab(tab, title)
|
||||||
|
return tab
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import aria2p
|
|
||||||
import subprocess
|
|
||||||
from core.utils.data.state import state
|
|
||||||
from plyer import notification
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def run_aria2p():
|
|
||||||
|
|
||||||
state.aria2 = aria2p.API(
|
|
||||||
aria2p.Client(
|
|
||||||
host="http://localhost",
|
|
||||||
port=6800,
|
|
||||||
secret=""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return state.aria2
|
|
||||||
|
|
||||||
|
|
||||||
def aria2server():
|
|
||||||
download_path = state.download_path
|
|
||||||
speed_limit = state.speed_limit
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
"aria2c",
|
|
||||||
"--enable-rpc",
|
|
||||||
"--disable-ipv6", # added this since it caused problems with vpns
|
|
||||||
"--rpc-listen-all",
|
|
||||||
"--rpc-listen-port=6800",
|
|
||||||
f"--dir={download_path}",
|
|
||||||
f"--max-download-limit={speed_limit * 1024}",
|
|
||||||
"-x", str(state.aria2_threads),
|
|
||||||
"-s", str(state.aria2_threads),
|
|
||||||
]
|
|
||||||
|
|
||||||
aria2server = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
|
||||||
)
|
|
||||||
return aria2server
|
|
||||||
|
|
||||||
def dlprogress():
|
|
||||||
try:
|
|
||||||
state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata]
|
|
||||||
downloads = state.downloads
|
|
||||||
if downloads:
|
|
||||||
for download in downloads:
|
|
||||||
progress = download.progress_string(0)
|
|
||||||
progress_int = int(progress.strip('%'))
|
|
||||||
return progress_int
|
|
||||||
return 0
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def send_notification(shutdown_event):
|
|
||||||
notified = set()
|
|
||||||
while not shutdown_event.is_set():
|
|
||||||
try:
|
|
||||||
for d in state.aria2.get_downloads():
|
|
||||||
if d.is_metadata:
|
|
||||||
continue
|
|
||||||
if d.progress == 100 and d.gid not in notified:
|
|
||||||
notification.notify(
|
|
||||||
title="Download finished",
|
|
||||||
message=f"{d.name} has finished downloading.",
|
|
||||||
timeout=4
|
|
||||||
)
|
|
||||||
notified.add(d.gid)
|
|
||||||
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(5)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from .aria2_integration import run_aria2p
|
|
||||||
from core.utils.data.state import state
|
|
||||||
|
|
||||||
def start_client():
|
|
||||||
global aria2
|
|
||||||
aria2 = run_aria2p()
|
|
||||||
if state.debug:
|
|
||||||
print("Started Aria2p")
|
|
||||||
|
|
||||||
def add_magnet(uri):
|
|
||||||
aria2.add_magnet(uri)
|
|
||||||
if state.debug:
|
|
||||||
print("Magnet URI added to Aria2")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from core.utils.data.state import state
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
addrs = psutil.net_if_addrs()
|
||||||
|
stats = psutil.net_if_stats()
|
||||||
|
|
||||||
|
def get_net_interfaces():
|
||||||
|
for interface in addrs.keys():
|
||||||
|
consoleLog(f"Found Interface: {interface}")
|
||||||
|
return addrs.keys()
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_interfaces():
|
||||||
|
active = []
|
||||||
|
|
||||||
|
for interface, addr_list in addrs.items():
|
||||||
|
up = stats[interface].isup
|
||||||
|
for addr in addr_list:
|
||||||
|
if addr.family == 2: # ipv4
|
||||||
|
ipv4 = addr.address
|
||||||
|
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
|
||||||
|
active.append(interface)
|
||||||
|
consoleLog(f"Found Active: {interface}")
|
||||||
|
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
def list_interfaces() -> None:
|
||||||
|
|
||||||
|
for interface, addr_list in addrs.items():
|
||||||
|
up = stats[interface].isup
|
||||||
|
for addr in addr_list:
|
||||||
|
if addr.family == 2: # ipv4
|
||||||
|
ipv4 = addr.address
|
||||||
|
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
|
||||||
|
status = "ACTIVE"
|
||||||
|
else:
|
||||||
|
status = "INACTIVE"
|
||||||
|
consoleLog(f"Found Interface: {interface} [{status}]")
|
||||||
|
|
||||||
|
def init_interfaces():
|
||||||
|
state.interfaces = list(addrs.keys())
|
||||||
|
state.active_interfaces = get_active_interfaces()
|
||||||
|
|
||||||
|
def get_interface_ip(interface_name):
|
||||||
|
if interface_name in addrs:
|
||||||
|
for addr in addrs[interface_name]:
|
||||||
|
if addr.family == 2: # ipv4
|
||||||
|
if not addr.address.startswith("127.") and not addr.address.startswith("169.254"):
|
||||||
|
return addr.address
|
||||||
|
return None
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import time
|
||||||
|
from core.utils.general.wrappers import run_thread
|
||||||
|
from core.network.interface import get_interface_ip
|
||||||
|
import threading
|
||||||
|
import libtorrent as lt
|
||||||
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
|
||||||
|
|
||||||
|
global loop_running
|
||||||
|
loop_running = False
|
||||||
|
|
||||||
|
def init_session():
|
||||||
|
if state.dl_session is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
state.dl_session = lt.session()
|
||||||
|
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
"upload_rate_limit": state.up_speed_limit,
|
||||||
|
"download_rate_limit": state.down_speed_limit,
|
||||||
|
"enable_dht": True,
|
||||||
|
"enable_lsd": True,
|
||||||
|
"enable_upnp": True,
|
||||||
|
"enable_natpmp": True,
|
||||||
|
"dht_bootstrap_nodes": "router.bittorrent.com:6881,dht.transmissionbt.com:6881",
|
||||||
|
"connections_limit": state.max_connections,
|
||||||
|
"active_downloads": state.max_downloads
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.bound_interface:
|
||||||
|
interface_ip = get_interface_ip(state.bound_interface)
|
||||||
|
if interface_ip:
|
||||||
|
settings["outgoing_interfaces"] = interface_ip
|
||||||
|
settings["listen_interfaces"] = f"{interface_ip}:6881"
|
||||||
|
consoleLog(f"Binding to Interface IP: {interface_ip}")
|
||||||
|
else:
|
||||||
|
consoleLog(f"Error finding IP for Interface {state.bound_interface}")
|
||||||
|
|
||||||
|
state.dl_session.apply_settings(settings)
|
||||||
|
|
||||||
|
consoleLog("Initialized Session")
|
||||||
|
|
||||||
|
|
||||||
|
def add_download(magnet_uri, dl_path=state.download_path):
|
||||||
|
|
||||||
|
if state.active_downloads is None:
|
||||||
|
state.active_downloads = {}
|
||||||
|
|
||||||
|
init_session()
|
||||||
|
|
||||||
|
if magnet_uri in state.active_downloads:
|
||||||
|
consoleLog("Skipping, download already running...")
|
||||||
|
return
|
||||||
|
|
||||||
|
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
||||||
|
magnetdl.save_path = dl_path
|
||||||
|
|
||||||
|
download = state.dl_session.add_torrent(magnetdl)
|
||||||
|
state.active_downloads[magnet_uri] = download
|
||||||
|
consoleLog(f"Added {magnet_uri} to downloads")
|
||||||
|
|
||||||
|
run_thread(threading.Thread(target=dl_status_loop))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def dl_status_loop():
|
||||||
|
global loop_running
|
||||||
|
if loop_running == True:
|
||||||
|
return
|
||||||
|
|
||||||
|
loop_running = True
|
||||||
|
completed_set = set()
|
||||||
|
|
||||||
|
if not state.active_downloads:
|
||||||
|
consoleLog("No active downloads")
|
||||||
|
loop_running = False
|
||||||
|
return
|
||||||
|
|
||||||
|
while state.active_downloads:
|
||||||
|
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set:
|
||||||
|
consoleLog(f"Download completed: {status.name}")
|
||||||
|
|
||||||
|
completed_set.add(magnet_uri)
|
||||||
|
|
||||||
|
if not state.active_downloads:
|
||||||
|
loop_running = False
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
loop_running = False
|
||||||
|
|
||||||
|
def update_settings():
|
||||||
|
|
||||||
|
if state.dl_session is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
"upload_rate_limit": state.up_speed_limit,
|
||||||
|
"download_rate_limit": state.down_speed_limit,
|
||||||
|
"connections_limit": state.max_connections,
|
||||||
|
"active_downloads": state.max_downloads
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.bound_interface:
|
||||||
|
interface_ip = get_interface_ip(state.bound_interface)
|
||||||
|
if interface_ip:
|
||||||
|
settings["outgoing_interfaces"] = interface_ip
|
||||||
|
settings["listen_interfaces"] = f"{interface_ip}:6881"
|
||||||
|
consoleLog(f"Binding to Interface IP: {interface_ip}")
|
||||||
|
else:
|
||||||
|
consoleLog(f"Error finding IP for Interface {state.bound_interface}")
|
||||||
|
|
||||||
|
state.dl_session.apply_settings(settings)
|
||||||
|
|
||||||
|
def update_bound_interface():
|
||||||
|
|
||||||
|
settings = { "outgoing_interfaces": state.bound_interface }
|
||||||
|
|
||||||
|
state.dl_session.apply_settings(settings)
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import update_download_completed_by_hash
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from plyer import notification
|
||||||
|
import libtorrent as lt
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_session():
|
||||||
|
if state.dl_session is not None:
|
||||||
|
for magnetdl in state.active_downloads.values():
|
||||||
|
if hasattr(magnetdl, 'pause'): # Check it's a handle
|
||||||
|
magnetdl.pause()
|
||||||
|
|
||||||
|
del state.dl_session
|
||||||
|
state.dl_session = None
|
||||||
|
state.active_downloads.clear()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def send_notification(shutdown_event):
|
||||||
|
notified = set()
|
||||||
|
while not shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
||||||
|
if isinstance(magnetdl, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
if status.state == lt.torrent_status.seeding and magnet_uri not in notified:
|
||||||
|
notification.notify(
|
||||||
|
title="Download finished",
|
||||||
|
message=f"{status.name} has finished downloading.",
|
||||||
|
timeout=4
|
||||||
|
)
|
||||||
|
notified.add(magnet_uri)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def update_log(shutdown_event):
|
||||||
|
updated = set()
|
||||||
|
while not shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
||||||
|
if isinstance(magnetdl, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = magnetdl.status()
|
||||||
|
|
||||||
|
if status.state == lt.torrent_status.seeding and magnet_uri not in updated:
|
||||||
|
consoleLog(f"Marking {status.name} as completed")
|
||||||
|
info_hash = str(status.info_hash)
|
||||||
|
update_download_completed_by_hash(info_hash, True)
|
||||||
|
updated.add(magnet_uri)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(5)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from core.network.libtorrent_int import add_download
|
||||||
|
|
||||||
|
def add_magnet(uri):
|
||||||
|
if uri is not None and uri.startswith("magnet:?"):
|
||||||
|
add_download(uri)
|
||||||
|
consoleLog("Magnet URI added to LibTorrent")
|
||||||
|
else:
|
||||||
|
consoleLog(f"Invalid Magnet Link: {uri}")
|
||||||
+47
-15
@@ -6,18 +6,36 @@ from core.utils.data.state import state
|
|||||||
def create_config():
|
def create_config():
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}",
|
config["General"] = {
|
||||||
"ignore_updates": f"{state.ignore_updates}"}
|
"debug": True,
|
||||||
|
"ignore_updates": f"{state.ignore_updates}",
|
||||||
|
"autoresume": f"{state.autoresume}",
|
||||||
|
"window_transparency": f"{state.window_transparency}"
|
||||||
|
}
|
||||||
|
|
||||||
|
config["Network"] = {
|
||||||
|
"api_url": f"{state.api_url}",
|
||||||
|
"download_path": f"{state.download_path}",
|
||||||
|
"download_speed_limit": f"{state.down_speed_limit}",
|
||||||
|
"upload_speed_limit": f"{state.up_speed_limit}",
|
||||||
|
"max_connections": f"{state.max_connections}",
|
||||||
|
"max_downloads": f"{state.max_downloads}"
|
||||||
|
}
|
||||||
|
|
||||||
|
config["Paths"] = {
|
||||||
|
"bound_interface": f"{state.bound_interface}",
|
||||||
|
"image_path": f"{state.image_path}"
|
||||||
|
}
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
||||||
else:
|
else:
|
||||||
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||||
|
|
||||||
settings_path = os.path.join(config_dir, "SoftwareManager")
|
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||||
os.makedirs(settings_path, exist_ok=True)
|
os.makedirs(state.settings_path, exist_ok=True)
|
||||||
|
|
||||||
with open(os.path.join(settings_path, "config.yml"), 'w') as cf:
|
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
|
||||||
config.write(cf)
|
config.write(cf)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,20 +44,34 @@ def read_config():
|
|||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
||||||
else:
|
else:
|
||||||
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||||
settings_path = os.path.join(config_dir, "SoftwareManager")
|
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||||
config_file = os.path.join(settings_path, "config.yml")
|
config_file = os.path.join(state.settings_path, "config.yml")
|
||||||
|
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
create_config()
|
create_config()
|
||||||
|
return
|
||||||
|
|
||||||
config.read(config_file)
|
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)
|
||||||
|
|
||||||
state.debug = config.getboolean("General", "debug")
|
# Network
|
||||||
state.api_url = config.get("General", "api_url")
|
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
|
||||||
state.aria2_threads = config.getint("General", "aria2_threads")
|
state.download_path = config.get("Network", "download_path", fallback=state.download_path)
|
||||||
state.download_path = config.get("General", "download_path")
|
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
|
||||||
state.speed_limit = config.getint("General", "speed_limit")
|
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
|
||||||
state.ignore_updates = config.getboolean("General", "ignore_updates")
|
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
|
||||||
|
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
state.bound_interface = config.get("Paths", "bound_interface", fallback=state.bound_interface)
|
||||||
|
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
|
||||||
|
|
||||||
|
create_config()
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
|
from core.network.libtorrent_int import update_settings
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
from core.utils.general.shutdown import kill_aria2server
|
from core.utils.general.logs import consoleLog
|
||||||
from .config import create_config
|
from .config import create_config
|
||||||
|
|
||||||
def restart_aria2c():
|
|
||||||
import main # had to do this because of circle import :(
|
|
||||||
import signal
|
|
||||||
import atexit
|
|
||||||
kill_aria2server()
|
|
||||||
state.aria2process.wait()
|
|
||||||
state.aria2process = main.run_aria2server()
|
|
||||||
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
|
|
||||||
atexit.unregister(kill_aria2server)
|
|
||||||
atexit.register(kill_aria2server)
|
|
||||||
|
|
||||||
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None):
|
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None):
|
||||||
if thread_count is not None:
|
|
||||||
state.aria2_threads = thread_count
|
|
||||||
if apiurl is not None:
|
if apiurl is not None:
|
||||||
state.api_url = apiurl
|
state.api_url = apiurl
|
||||||
if download_path is not None:
|
if download_path is not None:
|
||||||
state.download_path = download_path
|
state.download_path = download_path
|
||||||
if speed_limit is not None:
|
if down_speed_limit is not None:
|
||||||
state.speed_limit = speed_limit
|
state.down_speed_limit = down_speed_limit
|
||||||
|
if up_speed_limit is not None:
|
||||||
|
state.up_speed_limit = up_speed_limit
|
||||||
|
if image_path is not None:
|
||||||
|
state.image_path = image_path
|
||||||
|
if autoresume is not None:
|
||||||
|
state.autoresume = autoresume
|
||||||
|
if max_connections is not None:
|
||||||
|
state.max_connections = max_connections
|
||||||
|
if max_downloads is not None:
|
||||||
|
state.max_downloads = max_downloads
|
||||||
|
if bound_interface is not None:
|
||||||
|
state.bound_interface = None if bound_interface == "None" else bound_interface
|
||||||
|
|
||||||
|
|
||||||
|
update_settings()
|
||||||
|
consoleLog("Saved Settings")
|
||||||
create_config()
|
create_config()
|
||||||
restart_aria2c()
|
|
||||||
close()
|
close()
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Download:
|
||||||
|
title: str
|
||||||
|
url: str
|
||||||
|
magnet_uri: str
|
||||||
|
completed: bool
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DownloadList:
|
||||||
|
count: int
|
||||||
|
data: List[Download]
|
||||||
+42
-20
@@ -1,24 +1,46 @@
|
|||||||
from dataclasses import dataclass
|
from PySide6.QtCore import QObject, Signal
|
||||||
from typing import Optional, Any, List, Dict
|
from typing import Optional, Any, List, Dict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@dataclass
|
class AppState(QObject):
|
||||||
class AppState:
|
image_changed = Signal(str)
|
||||||
posts: List[Dict[str, Any]]
|
|
||||||
post_titles: List[str]
|
|
||||||
post_urls: List[str]
|
|
||||||
post_author: List[str]
|
|
||||||
downloads: List[str]
|
|
||||||
version: str
|
|
||||||
ignore_updates: bool = False
|
|
||||||
debug: bool = False
|
|
||||||
tracker: str = "rutracker"
|
|
||||||
api_url: str = "https://api.michijackson.xyz"
|
|
||||||
download_path: str = str(Path.home() / "Downloads")
|
|
||||||
speed_limit: int = 0
|
|
||||||
aria2process: Optional[Any] = None
|
|
||||||
aria2: Any = None
|
|
||||||
aria2p: Any = None
|
|
||||||
aria2_threads: int = 4
|
|
||||||
|
|
||||||
state = AppState(posts=[], post_titles=[], post_urls=[], post_author=[], downloads=[], download_path="", version="")
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.posts: List[Dict[str, Any]] = []
|
||||||
|
self.post_titles: List[str] = []
|
||||||
|
self.post_urls: List[str] = []
|
||||||
|
self.post_author: List[str] = []
|
||||||
|
self.post_seeders: List[str] = []
|
||||||
|
self.post_leechers: List[str] = []
|
||||||
|
self.version: str = "dev"
|
||||||
|
self._image_path: str = ""
|
||||||
|
self.ignore_updates: bool = False
|
||||||
|
self.debug: bool = False
|
||||||
|
self.autoresume: bool = True
|
||||||
|
self.tracker: str = "rutracker"
|
||||||
|
self.api_url: str = "https://api.michijackson.xyz"
|
||||||
|
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: List[str] = {}
|
||||||
|
self.window_transparency: bool = False
|
||||||
|
self.interfaces: List = []
|
||||||
|
self.active_interfaces: List = []
|
||||||
|
self.bound_interface: str = None
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|||||||
+10
-16
@@ -1,6 +1,7 @@
|
|||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
from core.utils.network.jsonhandler import format_data
|
from core.utils.network.jsonhandler import format_data
|
||||||
|
|
||||||
|
|
||||||
@@ -10,20 +11,17 @@ def get_item_url(item, posts, post_titles): # softwarelist currentitem, post lis
|
|||||||
if 0 <= post_index < len(post_titles):
|
if 0 <= post_index < len(post_titles):
|
||||||
if state.tracker == "uztracker":
|
if state.tracker == "uztracker":
|
||||||
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
|
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
|
||||||
if state.debug:
|
consoleLog(f"Found post URL: {item}")
|
||||||
print("Found post URL: ", item)
|
|
||||||
return item
|
return item
|
||||||
if state.tracker == "rutracker":
|
if state.tracker == "rutracker":
|
||||||
item_dict = posts[post_index]
|
item_dict = posts[post_index]
|
||||||
_, post_links, _ = format_data([item_dict])
|
_, post_links, _, _, _ = format_data([item_dict])
|
||||||
if state.debug:
|
consoleLog(f"Found post URL: {post_links[0]}")
|
||||||
print("Found post URL: ", post_links[0])
|
|
||||||
return post_links[0]
|
return post_links[0]
|
||||||
if state.tracker == "m0nkrus":
|
if state.tracker == "m0nkrus":
|
||||||
item_dict = posts[post_index]
|
item_dict = posts[post_index]
|
||||||
_, post_links, _ = format_data([item_dict])
|
_, post_links, _, _, _,= format_data([item_dict])
|
||||||
if state.debug:
|
consoleLog(f"Found post URL: {post_links[0]}")
|
||||||
print("Found post URL: ", post_links[0])
|
|
||||||
return post_links[0]
|
return post_links[0]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -33,20 +31,16 @@ def get_item_url(item, posts, post_titles): # softwarelist currentitem, post lis
|
|||||||
def get_magnet_link(post_url):
|
def get_magnet_link(post_url):
|
||||||
try:
|
try:
|
||||||
response = requests.get(post_url) # eventually impl. cloudscraper
|
response = requests.get(post_url) # eventually impl. cloudscraper
|
||||||
if state.debug:
|
consoleLog("Sent Request to retrieve Magnet Link...")
|
||||||
print("Sent Request to retrieve Magnet Link...")
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
|
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
|
||||||
if magnet_link:
|
if magnet_link:
|
||||||
if state.debug:
|
consoleLog(f"Magnet Link Retrieved: {magnet_link['href']}")
|
||||||
print("Magnet Link Retrieved: ", magnet_link['href'])
|
|
||||||
return magnet_link['href']
|
return magnet_link['href']
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog("Magnet Link not Found!")
|
||||||
print("Magnet Link not Found!")
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
if state.debug:
|
consoleLog(f"Failed to fetch {post_url}: {e}")
|
||||||
print(f"Failed to fetch {post_url}: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
from core.utils.network.download import run_download_direct
|
||||||
|
|
||||||
|
def split_data(data):
|
||||||
|
|
||||||
|
count = data.count
|
||||||
|
downloads = data.data
|
||||||
|
|
||||||
|
return count, downloads
|
||||||
|
|
||||||
|
def check_completed(downloads, resume):
|
||||||
|
for download in downloads:
|
||||||
|
if download.completed == False:
|
||||||
|
consoleLog(f"Found unfinished download: {download.title}")
|
||||||
|
if resume == True:
|
||||||
|
run_download_direct(download.magnet_uri)
|
||||||
|
consoleLog(f"Resuming {download.title}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
from core.utils.data.models import Download, DownloadList
|
||||||
|
from core.utils.data.state import state
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
_log_buffer = []
|
||||||
|
|
||||||
|
|
||||||
|
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||||
|
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||||
|
try:
|
||||||
|
with open(downloads_file, "r") as file:
|
||||||
|
existing_data = json.load(file)
|
||||||
|
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
downloads = []
|
||||||
|
else:
|
||||||
|
downloads = []
|
||||||
|
|
||||||
|
|
||||||
|
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
|
||||||
|
consoleLog("Skipped Logging, download already in file")
|
||||||
|
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
|
||||||
|
|
||||||
|
|
||||||
|
downloads.append(Download(
|
||||||
|
title=title,
|
||||||
|
url=url,
|
||||||
|
magnet_uri=magnet_uri,
|
||||||
|
completed=completed,
|
||||||
|
))
|
||||||
|
|
||||||
|
consoleLog(f"Added {title} to Log File")
|
||||||
|
|
||||||
|
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||||
|
with open(downloads_file, "w") as file:
|
||||||
|
json.dump(asdict(download_list), file, indent=4)
|
||||||
|
|
||||||
|
return download_list
|
||||||
|
|
||||||
|
def remove_download_log(magnet_uri) -> DownloadList:
|
||||||
|
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||||
|
try:
|
||||||
|
with open(downloads_file, "r") as file:
|
||||||
|
existing_data = json.load(file)
|
||||||
|
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
downloads = []
|
||||||
|
else:
|
||||||
|
downloads = []
|
||||||
|
|
||||||
|
magnet_link = (magnet_uri or "").strip()
|
||||||
|
downloads = [d for d in downloads if (getattr(d, 'magnet_uri', None) or "").strip() != magnet_link and (getattr(d, 'url', None) or "").strip() != magnet_link]
|
||||||
|
|
||||||
|
consoleLog(f"Removed {magnet_link} from Log File")
|
||||||
|
|
||||||
|
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||||
|
with open(downloads_file, "w") as file:
|
||||||
|
json.dump(asdict(download_list), file, indent=4)
|
||||||
|
|
||||||
|
return download_list
|
||||||
|
|
||||||
|
def update_download_completed(magnet_uri, completed) -> DownloadList:
|
||||||
|
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||||
|
try:
|
||||||
|
with open(downloads_file, "r") as file:
|
||||||
|
existing_data = json.load(file)
|
||||||
|
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
consoleLog(f"Error loading downloads.json: {e}")
|
||||||
|
downloads = []
|
||||||
|
else:
|
||||||
|
downloads = []
|
||||||
|
|
||||||
|
identifier = (magnet_uri or "").strip()
|
||||||
|
if state.debug:
|
||||||
|
consoleLog(f"Updating download completed for identifier: {identifier!r}")
|
||||||
|
consoleLog(f"Total downloads in file: {len(downloads)}")
|
||||||
|
for i, d in enumerate(downloads):
|
||||||
|
try:
|
||||||
|
mag = (getattr(d, 'magnet_uri', None) or '')[:50]
|
||||||
|
url = (getattr(d, 'url', None) or '')[:50]
|
||||||
|
consoleLog(f" [{i}] magnet: {mag}... url: {url}...")
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f" [{i}] Error reading download: {e}")
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for download in downloads:
|
||||||
|
try:
|
||||||
|
stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip()
|
||||||
|
stored_url = (getattr(download, 'url', None) or "").strip()
|
||||||
|
consoleLog(f"Comparing with magnet: {stored_magnet[:50] if stored_magnet else 'None'}...")
|
||||||
|
if identifier and (stored_magnet == identifier or stored_url == identifier):
|
||||||
|
consoleLog(f"Match found! Setting completed={completed}")
|
||||||
|
download.completed = completed
|
||||||
|
found = True
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f"Error comparing download: {e}")
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
consoleLog("No matching download found to update")
|
||||||
|
return DownloadList(data=downloads, count=len(downloads))
|
||||||
|
|
||||||
|
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||||
|
with open(downloads_file, "w") as file:
|
||||||
|
json.dump(asdict(download_list), file, indent=4)
|
||||||
|
|
||||||
|
consoleLog("Updated download log")
|
||||||
|
|
||||||
|
return download_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_logs() -> DownloadList:
|
||||||
|
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||||
|
try:
|
||||||
|
with open(downloads_file, "r") as file:
|
||||||
|
existing_data = json.load(file)
|
||||||
|
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
downloads = []
|
||||||
|
else:
|
||||||
|
downloads = []
|
||||||
|
|
||||||
|
return DownloadList(data=downloads, count=len(downloads))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
|
||||||
|
match = re.search(r'urn:btih:([A-F0-9]+)', magnet_uri, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).upper()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
|
||||||
|
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||||
|
try:
|
||||||
|
with open(downloads_file, "r") as file:
|
||||||
|
existing_data = json.load(file)
|
||||||
|
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
consoleLog(f"Error loading downloads.json: {e}")
|
||||||
|
downloads = []
|
||||||
|
else:
|
||||||
|
downloads = []
|
||||||
|
|
||||||
|
info_hash_upper = (info_hash or "").upper().strip()
|
||||||
|
consoleLog(f"Updating download by hash: {info_hash_upper}")
|
||||||
|
consoleLog(f"Total downloads in file: {len(downloads)}")
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for download in downloads:
|
||||||
|
try:
|
||||||
|
magnet_uri = getattr(download, 'magnet_uri', None)
|
||||||
|
if magnet_uri:
|
||||||
|
stored_hash = extract_hash_from_magnet(magnet_uri)
|
||||||
|
if stored_hash and stored_hash == info_hash_upper:
|
||||||
|
download.completed = completed
|
||||||
|
found = True
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f"Error comparing download: {e}")
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
consoleLog("No matching download found to update")
|
||||||
|
return DownloadList(data=downloads, count=len(downloads))
|
||||||
|
|
||||||
|
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||||
|
with open(downloads_file, "w") as file:
|
||||||
|
json.dump(asdict(download_list), file, indent=4)
|
||||||
|
|
||||||
|
consoleLog("Updated download log")
|
||||||
|
|
||||||
|
return download_list
|
||||||
|
|
||||||
|
|
||||||
|
_main_window = None
|
||||||
|
|
||||||
|
def set_main_window(window):
|
||||||
|
global _main_window
|
||||||
|
_main_window = window
|
||||||
|
|
||||||
|
def flush_log_buffer(): # credits to claude
|
||||||
|
global _log_buffer
|
||||||
|
if _log_buffer:
|
||||||
|
try:
|
||||||
|
from core.interface.gui import MainWindow
|
||||||
|
for log_entry in _log_buffer:
|
||||||
|
MainWindow.add_log(log_entry)
|
||||||
|
_log_buffer = []
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def consoleLog(text, printAnyways = False):
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M:%S")
|
||||||
|
formatted_text = f"[{current_time}] {text}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.interface.gui import MainWindow
|
||||||
|
MainWindow.add_log(formatted_text)
|
||||||
|
except Exception:
|
||||||
|
global _log_buffer
|
||||||
|
_log_buffer.append(formatted_text)
|
||||||
|
|
||||||
|
if state.debug or printAnyways:
|
||||||
|
print(formatted_text)
|
||||||
@@ -1,31 +1,9 @@
|
|||||||
from core.utils.data.state import state
|
|
||||||
import threading
|
import threading
|
||||||
import psutil
|
from core.network.libtorrent_misc import cleanup_session
|
||||||
import sys
|
from core.utils.general.wrappers import run_thread
|
||||||
|
|
||||||
|
|
||||||
shutdown_event = threading.Event()
|
shutdown_event = threading.Event()
|
||||||
|
|
||||||
def kill_aria2server():
|
|
||||||
if sys.platform.startswith("win"):
|
|
||||||
process = "aria2c.exe"
|
|
||||||
else:
|
|
||||||
process = "aria2c"
|
|
||||||
|
|
||||||
if state.aria2process:
|
|
||||||
state.aria2process.kill()
|
|
||||||
if state.debug:
|
|
||||||
print("Killed Aria2")
|
|
||||||
|
|
||||||
try:
|
|
||||||
for proc in psutil.process_iter():
|
|
||||||
if proc.name() == process:
|
|
||||||
proc.kill()
|
|
||||||
if state.debug:
|
|
||||||
print(f"Killed Aria2c (PID {proc.pid})")
|
|
||||||
except psutil.NoSuchProcess:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def closehelper():
|
def closehelper():
|
||||||
shutdown_event.set()
|
run_thread(threading.Thread(target=cleanup_session))
|
||||||
kill_aria2server()
|
shutdown_event.set()
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
from core.utils.data.tracker import get_item_url
|
from core.utils.data.tracker import get_item_url
|
||||||
from core.utils.data.tracker import get_magnet_link
|
from core.utils.data.tracker import get_magnet_link
|
||||||
from core.network.aria2_wrapper import start_client
|
from core.network.libtorrent_wrapper import add_download
|
||||||
from core.network.aria2_wrapper import add_magnet
|
|
||||||
from core.utils.general.wrappers import run_thread
|
from core.utils.general.wrappers import run_thread
|
||||||
|
from core.utils.general.logs import add_download_log
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
def download_selected(item, posts, post_titles):
|
def download_selected(item, posts, post_titles):
|
||||||
if item is not None:
|
if item is not None:
|
||||||
if state.debug:
|
consoleLog(f"Downloading {item.text()}")
|
||||||
print(f"Downloading {item.text()}")
|
|
||||||
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
|
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if state.debug:
|
consoleLog("No item selected for download.")
|
||||||
print("No item selected for download.")
|
|
||||||
|
|
||||||
def run_download(item, posts, post_titles):
|
def run_download(item, posts, post_titles):
|
||||||
post_url = get_item_url(item, posts, post_titles)
|
post_url = get_item_url(item, posts, post_titles)
|
||||||
if state.debug:
|
consoleLog(f"Selected URL: {post_url}")
|
||||||
print("Selected URL: ", post_url)
|
|
||||||
magnet_uri = get_magnet_link(post_url)
|
magnet_uri = get_magnet_link(post_url)
|
||||||
start_client()
|
|
||||||
add_magnet(magnet_uri)
|
add_download_log(item, post_url, magnet_uri, False)
|
||||||
|
add_download(magnet_uri)
|
||||||
|
|
||||||
|
def run_download_direct(magnet_uri):
|
||||||
|
add_download(magnet_uri)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ def split_data(data):
|
|||||||
|
|
||||||
return count, posts, query, success, cached
|
return count, posts, query, success, cached
|
||||||
|
|
||||||
def format_data(data):
|
def format_data(data):
|
||||||
|
post_author = [post["author"] for post in data]
|
||||||
|
post_titles = [post["title"] for post in data]
|
||||||
|
post_links = [post["url"] for post in data]
|
||||||
|
post_seeders = [post["seeders"] for post in data]
|
||||||
|
post_leechers = [post["leechers"] for post in data]
|
||||||
|
|
||||||
|
return post_titles, post_links, post_author, post_seeders, post_leechers
|
||||||
|
|
||||||
|
def format_data_m0nkrus(data):
|
||||||
post_author = [post["author"] for post in data]
|
post_author = [post["author"] for post in data]
|
||||||
post_titles = [post["title"] for post in data]
|
post_titles = [post["title"] for post in data]
|
||||||
post_links = [post["url"] for post in data]
|
post_links = [post["url"] for post in data]
|
||||||
@@ -20,4 +29,7 @@ def format_data(data):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
|
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import requests
|
import requests
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
|
from core.utils.general.logs import consoleLog
|
||||||
|
|
||||||
def check_for_updates():
|
def check_for_updates():
|
||||||
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
|
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"Failed to fetch releases: {response.status_code}")
|
consoleLog(f"Failed to fetch releases: {response.status_code}")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
releases = response.json()
|
releases = response.json()
|
||||||
@@ -18,19 +19,17 @@ def check_for_updates():
|
|||||||
|
|
||||||
|
|
||||||
if state.version == "dev":
|
if state.version == "dev":
|
||||||
if state.debug:
|
consoleLog("Dev release detected, skipping version check")
|
||||||
print("Dev release detected, skipping version check")
|
return None, None
|
||||||
return None, None
|
elif latest_version != state.version:
|
||||||
elif latest_version != state.version:
|
consoleLog(f"New release available: {latest_version}")
|
||||||
if state.debug:
|
if assets:
|
||||||
print(f"New release available: {latest_version}")
|
consoleLog("Assets:")
|
||||||
if assets:
|
for asset in assets:
|
||||||
if state.debug:
|
consoleLog(f" - {asset['name']}: {asset['browser_download_url']}")
|
||||||
print("Assets:")
|
return assets, latest_version
|
||||||
for asset in assets:
|
else:
|
||||||
print(f" - {asset['name']}: {asset['browser_download_url']}")
|
return None, None
|
||||||
return assets, latest_version
|
else:
|
||||||
else:
|
consoleLog("Already up-to-date.")
|
||||||
if state.debug:
|
|
||||||
print("Already up-to-date.")
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|||||||
@@ -1,49 +1,67 @@
|
|||||||
from core.interface.gui import MainWindow
|
from core.interface.gui import MainWindow
|
||||||
from core.utils.data.state import state
|
from core.utils.data.state import state
|
||||||
from core.network.aria2_integration import aria2server
|
from core.utils.general.logs import consoleLog
|
||||||
from core.network.aria2_integration import send_notification
|
from core.network.libtorrent_misc import send_notification, update_log
|
||||||
|
from core.utils.general.logs import get_download_logs
|
||||||
from core.utils.general.shutdown import closehelper, shutdown_event
|
from core.utils.general.shutdown import closehelper, shutdown_event
|
||||||
from core.utils.general.wrappers import run_thread
|
from core.utils.general.wrappers import run_thread
|
||||||
|
from core.utils.general.loghandler import split_data, check_completed
|
||||||
|
from core.network.interface import list_interfaces, init_interfaces
|
||||||
|
from core.network.libtorrent_int import update_bound_interface
|
||||||
from core.utils.config.config import read_config
|
from core.utils.config.config import read_config
|
||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
import qdarktheme
|
import qdarktheme
|
||||||
import darkdetect
|
|
||||||
import threading
|
import threading
|
||||||
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--debug", action="store_true")
|
parser.add_argument("--debug", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
def run_gui():
|
def run_gui():
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||||
if darkdetect.isDark:
|
qdarktheme.setup_theme("auto")
|
||||||
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
|
|
||||||
else:
|
|
||||||
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
|
|
||||||
widget = MainWindow()
|
widget = MainWindow()
|
||||||
|
|
||||||
|
if state.window_transparency and platform.system() != "Windows":
|
||||||
|
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
|
||||||
|
|
||||||
|
from core.utils.general.logs import set_main_window
|
||||||
|
set_main_window(widget)
|
||||||
widget.show()
|
widget.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
def run_aria2server():
|
|
||||||
aria2process = aria2server()
|
|
||||||
return aria2process
|
|
||||||
|
|
||||||
def keyboardinterrupthandler(signum, frame):
|
def keyboardinterrupthandler(signum, frame):
|
||||||
closehelper()
|
closehelper()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
start_time = time.perf_counter()
|
||||||
read_config()
|
read_config()
|
||||||
state.debug = args.debug # override of read_config
|
logs = get_download_logs()
|
||||||
if state.debug:
|
_, downloads = split_data(logs)
|
||||||
print("Starting Aria2 Server")
|
if args.debug:
|
||||||
state.aria2process = run_aria2server()
|
state.debug = args.debug # override of read_config
|
||||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||||
|
consoleLog("Starting SoftwareManager...")
|
||||||
|
consoleLog("Fetching Network Interfaces...")
|
||||||
|
list_interfaces()
|
||||||
|
consoleLog("Initializing Interface variables...")
|
||||||
|
init_interfaces()
|
||||||
|
consoleLog(f"Current Bound: {state.bound_interface}")
|
||||||
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
||||||
if state.debug:
|
consoleLog("Started Thread: send_notification")
|
||||||
print("Launching GUI")
|
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
|
||||||
|
consoleLog("Started Thread: update_log")
|
||||||
|
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
|
||||||
|
consoleLog("Started Thread: check_completed")
|
||||||
|
elapsed = time.perf_counter() - start_time
|
||||||
|
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
||||||
run_gui()
|
run_gui()
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,10 +1,11 @@
|
|||||||
requests==2.32.2
|
requests==2.32.2
|
||||||
PySide6==6.9.2
|
PySide6==6.10.1
|
||||||
beautifulsoup4==4.13.5
|
beautifulsoup4==4.13.5
|
||||||
darkdetect==0.8.0
|
darkdetect==0.7.1
|
||||||
aria2p==0.12.1
|
|
||||||
pyinstaller==6.15.0
|
pyinstaller==6.15.0
|
||||||
pyqtdarktheme==0.1.7
|
PyQtDarkTheme-fork==2.3.4
|
||||||
aiohttp==3.13.0
|
aiohttp==3.13.0
|
||||||
plyer==2.1.0
|
plyer==2.1.0
|
||||||
psutil==7.1.0
|
psutil==7.1.0
|
||||||
|
libtorrent==2.0.11
|
||||||
|
libtorrent-windows-dll==0.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user