mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd5ccc7688 | |||
| b2a30af5cd | |||
| fbc2225878 | |||
| 4d1a639651 | |||
| 8d1b3ba68f | |||
| dcc12c0b28 | |||
| 925fe21d03 | |||
| ec462afde6 | |||
| e50ef32c66 | |||
| d6e418d045 | |||
| a7c71b37f5 | |||
| 8fb1bb9e8d | |||
| 89fd8b4ebb | |||
| 3915e114a3 | |||
| 34a50e1a1b | |||
| 90124fd9c8 | |||
| 4175f82967 | |||
| 4bf2e9af81 | |||
| a6bb879db3 | |||
| 7653748599 | |||
| 9bec3f9189 | |||
| 04d93b870a | |||
| 664e660198 | |||
| 60019e0d2e | |||
| 8238cfbe56 | |||
| 7ccff5c5a1 | |||
| 8abe92fc4f | |||
| 0a3b5eeee2 | |||
| 7662c861ea | |||
| 2a8f305a76 | |||
| 12223d18de | |||
| 544733ce6b | |||
| 7aee2bcfe9 | |||
| d274e64841 | |||
| 23c74f6487 | |||
| 19ea09d035 | |||
| 92359d75e5 | |||
| 315a68c000 | |||
| 4b0ef44efd | |||
| 6dc28615a0 | |||
| 2ea9a329d1 | |||
| 9f8111ac73 | |||
| e5b809d6d4 | |||
| b573ca8fed | |||
| 8a45d71655 | |||
| 0c7bc426db | |||
| 1f06a19c17 | |||
| da92c4ab0a | |||
| f6e3ff4f27 | |||
| 88042b2d3d | |||
| 9265a9cd1c | |||
| adc9f6f6d4 | |||
| a3a6ef5eec |
+92
-32
@@ -15,7 +15,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.12"
|
||||
APP_NAME: "SoftwareManager"
|
||||
|
||||
jobs:
|
||||
@@ -57,7 +57,13 @@ jobs:
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install pyinstaller backports.tarfile pillow
|
||||
uv pip install Pillow
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
- name: Install Nuitka (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
run: uv pip install nuitka
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
@@ -82,36 +88,58 @@ jobs:
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Build executable with PyInstaller (Windows)
|
||||
- name: Install UPX (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
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
|
||||
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
|
||||
7z x upx.zip -oupx-dir
|
||||
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
|
||||
echo "$UPX_DIR" >> $GITHUB_PATH
|
||||
export PATH="$UPX_DIR:$PATH"
|
||||
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
|
||||
upx --version | head -1
|
||||
|
||||
- name: Build executable with PyInstaller (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
- name: Convert icon for Windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -c "
|
||||
from PIL import Image
|
||||
img = Image.open('src/core/interface/assets/logo.png')
|
||||
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
|
||||
print('Icon converted successfully')
|
||||
"
|
||||
|
||||
- name: Build with Nuitka (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -m nuitka \
|
||||
--onefile \
|
||||
--onefile-no-compression \
|
||||
--onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
|
||||
--assume-yes-for-downloads \
|
||||
--windows-console-mode=disable \
|
||||
--windows-icon-from-ico=app_icon.ico \
|
||||
--enable-plugin=pyside6 \
|
||||
--enable-plugin=upx \
|
||||
--upx-binary="${{ env.UPX_BINARY }}" \
|
||||
--noinclude-qt-translations \
|
||||
--include-data-files=build_info.json=build_info.json \
|
||||
--include-data-files=build_info.json=src/build_info.json \
|
||||
--include-data-files=build_info.json=src/core/interface/build_info.json \
|
||||
--output-filename=${{ env.APP_NAME }}.exe \
|
||||
src/main.py
|
||||
|
||||
- name: Build with PyInstaller (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
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" \
|
||||
--add-data "build_info.json:src/core" \
|
||||
--add-data "build_info.json:src/core/interface" \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
--hidden-import=backports.tarfile \
|
||||
@@ -120,24 +148,43 @@ jobs:
|
||||
--hidden-import=jaraco.text \
|
||||
--hidden-import=jaraco.functools \
|
||||
--name ${{ env.APP_NAME }} \
|
||||
main.py
|
||||
src/main.py
|
||||
|
||||
- name: Make executable (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: chmod +x dist/${{ env.APP_NAME }}
|
||||
- name: Build with PyInstaller (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile \
|
||||
--add-data "build_info.json:." \
|
||||
--add-data "build_info.json:src/core" \
|
||||
--add-data "build_info.json:src/core/interface" \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
--hidden-import=backports.tarfile \
|
||||
--hidden-import=backports \
|
||||
--hidden-import=jaraco.context \
|
||||
--hidden-import=jaraco.text \
|
||||
--hidden-import=jaraco.functools \
|
||||
--name ${{ env.APP_NAME }} \
|
||||
src/main.py
|
||||
|
||||
- name: Rename artifact
|
||||
shell: bash
|
||||
run: |
|
||||
cd dist
|
||||
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }}
|
||||
ls -la
|
||||
mkdir -p dist-final
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
mv ${{ env.APP_NAME }}.exe dist-final/${{ matrix.artifact_name }}
|
||||
else
|
||||
chmod +x dist/${{ env.APP_NAME }}
|
||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
|
||||
fi
|
||||
ls -la dist-final/
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-build
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
path: dist-final/${{ matrix.artifact_name }}
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
@@ -162,9 +209,16 @@ jobs:
|
||||
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
|
||||
ls -la release/
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd release
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
cat SHA256SUMS.txt
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||
CHECKSUMS=$(cat release/SHA256SUMS.txt)
|
||||
cat > release_notes.md << EOF
|
||||
## Development Build
|
||||
|
||||
@@ -177,6 +231,11 @@ jobs:
|
||||
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
|
||||
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
|
||||
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
|
||||
|
||||
### SHA-256 Checksums
|
||||
\`\`\`
|
||||
${CHECKSUMS}
|
||||
\`\`\`
|
||||
EOF
|
||||
cat release_notes.md
|
||||
|
||||
@@ -190,5 +249,6 @@ jobs:
|
||||
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 }}-macos
|
||||
release/SHA256SUMS.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# SoftwareManager - A Download Manager/Launcher
|
||||
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
||||
# SoftwareManager
|
||||
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
|
||||
- VPN Binding (impl. soon)
|
||||
- Bandwidth/Download speed limiting
|
||||
- Start downloads via a predefined magnet URI.
|
||||
- View detailed download information (name, speed, and status).
|
||||
- Searching & Downloading Software from various pages
|
||||
- Network Interface Binding
|
||||
- Simple UI, Speed Limiting etc.
|
||||
- No Logins required
|
||||
|
||||
## Installation
|
||||
1. Download the binary file for your operating system from releases
|
||||
@@ -14,18 +14,40 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
||||
|
||||
## Manual Installation
|
||||
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:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Run main.py:
|
||||
```bash
|
||||
python3 main.py
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Notes
|
||||
- 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.
|
||||
- This tool is for educational purposes only. Use it responsibly.
|
||||
## Additional Information
|
||||
- SoftwareManager is still in Development and most likely contains issues. Please report any bugs you find via the Issues tab. Contributions are also greatly appreciated.
|
||||
- MacOS Builds haven't been properly tested - We're thankful to receive feedback.
|
||||
- If you want to host your own server, clone our [server repository](https://github.com/KeksPirates/SoftwareManager-Server), enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of the server repo.
|
||||
|
||||
## Python Dependencies
|
||||
(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
|
||||
|
||||
@@ -41,6 +63,11 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
||||
</picture>
|
||||
</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.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"Version": "dev"
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import requests
|
||||
import asyncio
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
|
||||
global url_uztracker
|
||||
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
||||
|
||||
async def init_uztracker():
|
||||
global up
|
||||
global soup
|
||||
try:
|
||||
response = requests.get(url_uztracker, timeout=10)
|
||||
if response.status_code == 200:
|
||||
up = True
|
||||
else:
|
||||
up = False
|
||||
consoleLog(f"Uztracker seems down, status code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
consoleLog(f"\nRequest Exception on {url_uztracker}:")
|
||||
consoleLog(str(e))
|
||||
consoleLog("\nIs the Site down?")
|
||||
up = False
|
||||
|
||||
asyncio.run(init_uztracker())
|
||||
|
||||
def scrape_uztracker(search):
|
||||
if up:
|
||||
search_url = url_uztracker + search
|
||||
consoleLog(search_url)
|
||||
result = False
|
||||
global results
|
||||
global resulttitles
|
||||
results = []
|
||||
resulttitles = []
|
||||
|
||||
try:
|
||||
resultCount = 0
|
||||
response = requests.get(search_url)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
|
||||
for link in links:
|
||||
if link.b:
|
||||
resultCount += 1
|
||||
results.append(link['href'])
|
||||
resulttitles.append(link.b.text)
|
||||
result = True
|
||||
|
||||
if result:
|
||||
return resulttitles, results
|
||||
|
||||
if not result:
|
||||
# add popup in gui for no result
|
||||
return None
|
||||
|
||||
except requests.RequestException as e:
|
||||
if result:
|
||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||
return None
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None, None
|
||||
|
||||
def get_post_title(post_url):
|
||||
if up:
|
||||
response = requests.get(post_url)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
maintitle = soup.find(class_='tt-text')
|
||||
if maintitle:
|
||||
return maintitle.text
|
||||
else:
|
||||
consoleLog("Program not found")
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,83 +0,0 @@
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.data.scrapers.uztracker import scrape_uztracker
|
||||
from core.data.scrapers.rutracker import scrape_rutracker
|
||||
from core.data.scrapers.monkrus import scrape_monkrus_telegram
|
||||
from core.utils.network.jsonhandler import split_data, format_data
|
||||
from core.utils.data.state import state
|
||||
|
||||
def return_pressed(self):
|
||||
search_text = self.searchbar.text()
|
||||
if search_text == "":
|
||||
consoleLog("Error: Can't search for nothing")
|
||||
return
|
||||
consoleLog(f"User searched for: {search_text}")
|
||||
|
||||
if state.tracker == "uztracker":
|
||||
response = scrape_uztracker(search_text)
|
||||
if response:
|
||||
state.post_titles, state.post_urls = response
|
||||
self.qtablewidget.clear()
|
||||
if state.post_titles and len(state.post_titles) > 0:
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||
self.show_empty_results(False)
|
||||
else:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
consoleLog(f"No response from uztracker")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "rutracker":
|
||||
response = scrape_rutracker(search_text)
|
||||
if response:
|
||||
_, state.posts, _, _, cached = split_data(response)
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author, state.post_seeders, state.post_leechers = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
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:
|
||||
consoleLog(f"Response Cached: {cached}")
|
||||
else:
|
||||
consoleLog(f"No response from rutracker")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "m0nkrus":
|
||||
state.posts = scrape_monkrus_telegram(search_text)
|
||||
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||
@@ -1,21 +0,0 @@
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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_magnet_link
|
||||
from core.network.libtorrent_wrapper import add_download
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.general.logs import add_download_log
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
def download_selected(item, posts, post_titles):
|
||||
if item is not None:
|
||||
consoleLog(f"Downloading {item.text()}")
|
||||
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
|
||||
else:
|
||||
consoleLog("No item selected for download.")
|
||||
|
||||
def run_download(item, posts, post_titles):
|
||||
post_url = get_item_url(item, posts, post_titles)
|
||||
consoleLog(f"Selected URL: {post_url}")
|
||||
magnet_uri = get_magnet_link(post_url)
|
||||
|
||||
add_download_log(item, post_url, magnet_uri, False)
|
||||
add_download(magnet_uri)
|
||||
|
||||
def run_download_direct(magnet_uri):
|
||||
add_download(magnet_uri)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
requests==2.32.2
|
||||
PySide6==6.10.1
|
||||
PySide6-Essentials==6.10.1
|
||||
beautifulsoup4==4.13.5
|
||||
darkdetect==0.7.1
|
||||
pyinstaller==6.15.0
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "dev"
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from core.utils.general.logs import consoleLog
|
||||
import time
|
||||
|
||||
def scrape_monkrus_telegram(query):
|
||||
cache = {
|
||||
"data": [],
|
||||
"last_fetched": 0
|
||||
}
|
||||
|
||||
cache_expiry = 300
|
||||
|
||||
def _get_telegram_posts():
|
||||
post = 100
|
||||
max_posts = 250
|
||||
posts: list[dict] = []
|
||||
posts = []
|
||||
added = set()
|
||||
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
if cache["data"] and (current_time - cache["last_fetched"] < cache_expiry):
|
||||
return cache["data"]
|
||||
|
||||
while post <= max_posts:
|
||||
url = f"https://t.me/s/real_monkrus/{post}"
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, timeout=10)
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
|
||||
|
||||
@@ -25,7 +37,7 @@ def scrape_monkrus_telegram(query):
|
||||
title = post_txt.b.text
|
||||
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
|
||||
|
||||
if query.lower() in title.lower() and link:
|
||||
if link:
|
||||
post_url = link["href"]
|
||||
if post_url not in added:
|
||||
added.add(post_url)
|
||||
@@ -39,4 +51,20 @@ def scrape_monkrus_telegram(query):
|
||||
post += 22
|
||||
|
||||
posts.reverse()
|
||||
|
||||
cache["data"] = posts
|
||||
cache["last_fetched"] = current_time
|
||||
|
||||
return(posts)
|
||||
|
||||
def scrape_m0nkrus(query):
|
||||
posts = _get_telegram_posts()
|
||||
filtered_posts = []
|
||||
|
||||
for post in posts:
|
||||
if query.lower() in post["title"].lower():
|
||||
filtered_post = post.copy()
|
||||
filtered_post["id"] = len(filtered_posts) + 1
|
||||
filtered_posts.append(filtered_post)
|
||||
|
||||
return filtered_posts
|
||||
@@ -1,10 +1,9 @@
|
||||
import requests
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
|
||||
|
||||
def scrape_rutracker(search_text):
|
||||
search = requests.get(f"{state.api_url}/search?q={search_text}")
|
||||
def scrape_rutracker(query):
|
||||
search = requests.get(f"{state.api_url}/search?q={query}")
|
||||
consoleLog("Sent request to server")
|
||||
if search:
|
||||
try:
|
||||
@@ -12,10 +11,9 @@ def scrape_rutracker(search_text):
|
||||
except Exception:
|
||||
consoleLog("No results found / No response from server")
|
||||
return None
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
# This function utilizes the SoftwareManager server - source code can be found under the "server" branch.
|
||||
# This function utilizes the SoftwareManager server - source code can be found under the SoftwareManager-Server repository.
|
||||
@@ -0,0 +1,35 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin
|
||||
from core.utils.logging.logs import consoleLog
|
||||
|
||||
def scrape_uztracker(query):
|
||||
base_url="https://uztracker.net/"
|
||||
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
|
||||
consoleLog(search_url)
|
||||
posts = []
|
||||
|
||||
try:
|
||||
response = requests.get(search_url)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
|
||||
for link in links:
|
||||
|
||||
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
|
||||
url = urljoin(base_url, theme_link['href'])
|
||||
title = theme_link.b.text
|
||||
author_link = link.find('a', class_="med")
|
||||
author = author_link.text.strip() if author_link else "Unknown"
|
||||
|
||||
posts.append(dict(
|
||||
title=title,
|
||||
url=url,
|
||||
author=author
|
||||
))
|
||||
|
||||
return posts
|
||||
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||
return None
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -1,10 +1,11 @@
|
||||
from core.utils.config.settings import save_settings
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.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.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
@@ -31,6 +32,9 @@ def settings_dialog(self):
|
||||
|
||||
dialog.setLayout(QVBoxLayout())
|
||||
|
||||
if state.window_transparency and platform.system() != "Windows" and dialog:
|
||||
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
def close_settings():
|
||||
dialog.reject()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
|
||||
from PySide6.QtCore import QPersistentModelIndex, Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize, QByteArray
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QTableView,
|
||||
@@ -10,15 +10,16 @@ from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QComboBox,
|
||||
QTabWidget,
|
||||
QProgressBar,
|
||||
QHeaderView,
|
||||
QMessageBox,
|
||||
QTableWidget,
|
||||
QTextEdit,
|
||||
QStyledItemDelegate,
|
||||
QStatusBar
|
||||
)
|
||||
|
||||
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent
|
||||
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication, QColor, QBrush
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
import darkdetect
|
||||
import threading
|
||||
import platform
|
||||
@@ -29,7 +30,8 @@ import libtorrent as lt
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
from core.utils.general.logs import consoleLog, remove_download_log, flush_log_buffer
|
||||
import base64
|
||||
from core.utils.logging.logs import consoleLog, remove_download_log, flush_log_buffer
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.data.state import state
|
||||
from core.utils.network.download import download_selected
|
||||
@@ -37,10 +39,28 @@ from core.utils.network.update_checker import check_for_updates
|
||||
from core.interface.utils.tabhelper import create_tab
|
||||
from core.interface.utils.searchhelper import return_pressed
|
||||
from core.interface.dialogs.settings import settings_dialog
|
||||
from core.network.libtorrent_misc import cleanup_session
|
||||
from core.interface.assets.base64_icons import settings_black_base64
|
||||
from core.interface.assets.base64_icons import settings_white_base64
|
||||
from core.interface.assets.base64_icons import logo_base64
|
||||
from core.utils.general.shutdown import closehelper
|
||||
|
||||
|
||||
def _svg_icon(svg_str, size=20):
|
||||
renderer = QSvgRenderer(QByteArray(svg_str.encode()))
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
from PySide6.QtGui import QPainter
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
|
||||
|
||||
SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" fill="white"/></svg>'
|
||||
SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="5" y="3" width="4" height="18" rx="1" fill="white"/><rect x="15" y="3" width="4" height="18" rx="1" fill="white"/></svg>'
|
||||
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="white"/></svg>'
|
||||
|
||||
|
||||
def download_update(latest_version):
|
||||
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"
|
||||
@@ -64,6 +84,9 @@ def download_update(latest_version):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def windowCloseHelper():
|
||||
QGuiApplication.quit()
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
log_signal = Signal(str) # Thread-safe signal for logging
|
||||
|
||||
@@ -72,20 +95,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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")))
|
||||
|
||||
pixmap = QPixmap()
|
||||
image_data = base64.b64decode(logo_base64)
|
||||
pixmap.loadFromData(image_data)
|
||||
self.setWindowIcon(QIcon(pixmap))
|
||||
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
@@ -102,23 +115,22 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
state.version = build_info.get("version")
|
||||
|
||||
# Check for updates on Windows
|
||||
if state.ignore_updates is False:
|
||||
if platform.system() == "Windows":
|
||||
result = check_for_updates()
|
||||
if result != (None, None):
|
||||
assets, latest_version = result
|
||||
if state.ignore_updates is False and platform.system() == "Windows":
|
||||
result = check_for_updates()
|
||||
if result != (None, None):
|
||||
assets, latest_version = result
|
||||
|
||||
if assets:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText("A new version is available.")
|
||||
msg.setInformativeText("Press Ok to download the update.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||
if assets:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText("A new version is available.")
|
||||
msg.setInformativeText("Press Ok to download the update.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(latest_version)
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(latest_version)
|
||||
|
||||
self.setWindowTitle("Software Manager")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
@@ -143,60 +155,127 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
self.download_model = None
|
||||
self.downloadList = QTableView()
|
||||
self.downloadList.setMouseTracking(True)
|
||||
self._hovered_row = -1
|
||||
self._tracker_hovered_row = -1
|
||||
self._tracker_hovered_table = None
|
||||
self.downloadList.viewport().installEventFilter(self)
|
||||
self.emptyLibrary = QLabel("No items in library.")
|
||||
self.emptyDownload = QLabel("No items in downloads.")
|
||||
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.consoleLog = QTextEdit()
|
||||
self.progressbar = QProgressBar()
|
||||
self.statusbar = QStatusBar()
|
||||
|
||||
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.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
class TrackerHoverDelegate(QStyledItemDelegate):
|
||||
def paint(self, painter, option, index):
|
||||
if index.row() == MainWindow._instance._tracker_hovered_row:
|
||||
painter.save()
|
||||
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
|
||||
painter.restore()
|
||||
super().paint(painter, option, index)
|
||||
|
||||
|
||||
self._tracker_hover_delegate = TrackerHoverDelegate(self)
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
header.setStretchLastSection(False)
|
||||
def create_tracker_table(headers):
|
||||
table = QTableWidget()
|
||||
table.setColumnCount(len(headers))
|
||||
table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.setHorizontalHeaderLabels(headers)
|
||||
table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
self.qtablewidget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.qtablewidget.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
header = table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
header.resizeSection(1, 500)
|
||||
header.setStretchLastSection(False)
|
||||
header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
header.setHighlightSections(False)
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
header.resizeSection(1, 500)
|
||||
table.setShowGrid(False)
|
||||
table.setStyleSheet("""
|
||||
QTableWidget {
|
||||
border: none;
|
||||
outline: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
QTableWidget::item {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 6px 14px;
|
||||
}
|
||||
QTableWidget::item:selected {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
QHeaderView {
|
||||
background: transparent;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: normal;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 14px;
|
||||
}
|
||||
QHeaderView::section:checked {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: normal;
|
||||
}
|
||||
""")
|
||||
|
||||
table.setMouseTracking(True)
|
||||
table.viewport().setMouseTracking(True)
|
||||
table.setItemDelegate(self._tracker_hover_delegate)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
self.rutrackerlist = create_tracker_table(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
self.uztrackerlist = create_tracker_table(["Post Title", "Author"])
|
||||
self.monkruslist = create_tracker_table(["Post Title", "Author"])
|
||||
|
||||
state.tracker_list.update({"rutracker": self.rutrackerlist, "uztracker": self.uztrackerlist, "m0nkrus": self.monkruslist})
|
||||
|
||||
self.rutrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.rutrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
self.uztrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.uztrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
self.monkruslist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.monkruslist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
|
||||
tbl.viewport().installEventFilter(self)
|
||||
|
||||
container = QWidget()
|
||||
containerLayout = QVBoxLayout()
|
||||
containerLayout.addWidget(self.searchbar)
|
||||
containerLayout.addWidget(self.qtablewidget)
|
||||
containerLayout.addWidget(state.tracker_list[state.tracker])
|
||||
|
||||
class DownloadModel(QAbstractTableModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"]
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
|
||||
return len(state.active_downloads)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()):
|
||||
return len(self.headers)
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||
def headerData(self, section, orientation, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
||||
return self.headers[section]
|
||||
return None
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole:
|
||||
def data(self, index: QModelIndex | QPersistentModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
col = index.column()
|
||||
|
||||
if index.row() >= len(state.active_downloads) or index.row() < 0:
|
||||
@@ -212,22 +291,23 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
elif col == 1:
|
||||
return status.name if status.has_metadata else "Fetching metadata..."
|
||||
elif col == 2:
|
||||
if status.state == lt.torrent_status.downloading:
|
||||
if status.state == lt.torrent_status.downloading:
|
||||
return "Downloading"
|
||||
elif status.state == lt.torrent_status.seeding:
|
||||
elif status.state == lt.torrent_status.seeding:
|
||||
return "Seeding"
|
||||
elif status.paused:
|
||||
return "Paused"
|
||||
is_auto = bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
|
||||
return "Queued" if is_auto else "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"
|
||||
down_kb = status.download_rate / 1024
|
||||
up_kb = status.upload_rate / 1024
|
||||
down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
|
||||
up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
|
||||
return f"↓ {down_text} ↑ {up_text}"
|
||||
elif col == 5:
|
||||
downloaded_mb = status.total_wanted_done / (1024 * 1024)
|
||||
if downloaded_mb > 1024:
|
||||
@@ -258,7 +338,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
else:
|
||||
return "∞" if status.paused else "Stalled"
|
||||
|
||||
if role == Qt.UserRole and index.column() == 0:
|
||||
if role == Qt.ItemDataRole.UserRole and index.column() == 0:
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
return magnetdl.status().paused
|
||||
@@ -285,19 +365,37 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
subprocess.Popen(["open", save_path])
|
||||
return
|
||||
|
||||
if status.paused:
|
||||
is_paused = bool(magnetdl.flags() & lt.torrent_flags.paused)
|
||||
if is_paused:
|
||||
magnetdl.set_flags(lt.torrent_flags.auto_managed)
|
||||
magnetdl.resume()
|
||||
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 HoverRowDelegate(QStyledItemDelegate):
|
||||
def paint(self, painter, option, index):
|
||||
if index.row() == MainWindow._instance._hovered_row:
|
||||
painter.save()
|
||||
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
|
||||
painter.restore()
|
||||
super().paint(painter, option, index)
|
||||
|
||||
class PauseResumeDelegate(QStyledItemDelegate):
|
||||
clicked = Signal(int)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
if index.row() == MainWindow._instance._hovered_row:
|
||||
painter.save()
|
||||
painter.fillRect(option.rect, QColor(255, 255, 255, 15))
|
||||
painter.restore()
|
||||
super().paint(painter, option, index)
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
button = editor.findChild(QtWidgets.QPushButton)
|
||||
|
||||
@@ -307,9 +405,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
if button:
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
button.setText("📁")
|
||||
button.setIcon(_svg_icon(SVG_FOLDER, 18))
|
||||
button.setText("")
|
||||
else:
|
||||
button.setText("▶︎" if status.paused else "⏸︎")
|
||||
is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
|
||||
button.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
|
||||
button.setText("")
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
@@ -317,14 +418,26 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
status = magnetdl.status()
|
||||
|
||||
widget = QWidget(parent)
|
||||
widget.setStyleSheet("border: none;")
|
||||
widget.setStyleSheet("border: none; background: transparent;")
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
btnPause = QtWidgets.QPushButton("📁")
|
||||
btnPause = QtWidgets.QPushButton()
|
||||
btnPause.setIcon(_svg_icon(SVG_FOLDER, 18))
|
||||
else:
|
||||
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎")
|
||||
btnPause.setFixedSize(40, 30)
|
||||
btnPause = QtWidgets.QPushButton()
|
||||
is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
|
||||
btnPause.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
|
||||
btnPause.setIconSize(QSize(18, 18))
|
||||
btnPause.setFixedSize(30, 30)
|
||||
btnPause.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
btnPause.setStyleSheet("""
|
||||
QPushButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0px;
|
||||
}
|
||||
""")
|
||||
btnPause.clicked.connect(lambda: self.clicked.emit(index.row()))
|
||||
layout.addStretch()
|
||||
layout.addWidget(btnPause)
|
||||
@@ -332,16 +445,58 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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.downloadList.setModel(self.download_model)
|
||||
self.downloadList.horizontalHeader().setStretchLastSection(False)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.downloadList.horizontalHeader().resizeSection(0, 70)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.downloadList.horizontalHeader().setMinimumSectionSize(60)
|
||||
self.downloadList.verticalHeader().setDefaultSectionSize(40)
|
||||
self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
self.downloadList.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.downloadList.verticalHeader().setVisible(False)
|
||||
self.downloadList.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
self.downloadList.horizontalHeader().setHighlightSections(False)
|
||||
self.downloadList.setMouseTracking(True)
|
||||
self.downloadList.setShowGrid(False)
|
||||
self.downloadList.setStyleSheet("""
|
||||
QTableView {
|
||||
border: none;
|
||||
outline: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
QTableView::item {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 6px 14px;
|
||||
}
|
||||
QTableView::item:selected {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
QHeaderView {
|
||||
background: transparent;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: normal;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 14px;
|
||||
}
|
||||
QHeaderView::section:checked {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: normal;
|
||||
}
|
||||
""")
|
||||
self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
@@ -350,12 +505,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
def on_pause_resume_clicked(row):
|
||||
download_model.toggle_pause_resume(row)
|
||||
|
||||
self.downloadList.viewport().setMouseTracking(True)
|
||||
hover_delegate = HoverRowDelegate(self.downloadList)
|
||||
self.downloadList.setItemDelegate(hover_delegate)
|
||||
|
||||
delegate = PauseResumeDelegate(self.downloadList)
|
||||
self.downloadList.setItemDelegateForColumn(0, delegate)
|
||||
delegate.clicked.connect(on_pause_resume_clicked)
|
||||
|
||||
# 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=(state.tracker_list[state.tracker].selectedItems(), state.posts, state.post_titles))))
|
||||
|
||||
container.setLayout(containerLayout)
|
||||
self.setCentralWidget(container)
|
||||
@@ -375,24 +534,26 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
self.horizontal_layout = QHBoxLayout()
|
||||
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
|
||||
self.horizontal_layout.addWidget(self.qtablewidget)
|
||||
self.tracker_widget = state.tracker_list[state.tracker]
|
||||
self.horizontal_layout.addWidget(self.tracker_widget)
|
||||
|
||||
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
|
||||
self.tab1 = create_tab("Search", self.searchbar, state.tracker_list[state.tracker], self.tabs, self.dlbutton, self.horizontal_layout)
|
||||
# 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)
|
||||
|
||||
if state.image_path is not None and os.path.exists(state.image_path):
|
||||
self.image = QImage(state.image_path)
|
||||
self.image = self.image.scaledToWidth(300, Qt.TransformationMode.SmoothTransformation)
|
||||
self.pixmap = QPixmap.fromImage(self.image)
|
||||
self.overlay_label = 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
|
||||
# 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
|
||||
@@ -413,10 +574,18 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
self.settings_btn = QtWidgets.QToolButton()
|
||||
self.settings_btn.setIconSize(QSize(21, 21))
|
||||
self.settings_btn.setStyleSheet("background: transparent;")
|
||||
|
||||
if darkdetect.isDark():
|
||||
self.settings_btn.setIcon(QIcon(get_asset_path("settings_white.png")))
|
||||
settings_white_pixmap = QPixmap()
|
||||
settings_white = base64.b64decode(settings_white_base64)
|
||||
settings_white_pixmap.loadFromData(settings_white)
|
||||
self.settings_btn.setIcon(QIcon(settings_white_pixmap))
|
||||
else:
|
||||
self.settings_btn.setIcon(QIcon(get_asset_path("settings_black.png")))
|
||||
settings_black_pixmap = QPixmap()
|
||||
settings_black = base64.b64decode(settings_black_base64)
|
||||
settings_black_pixmap.loadFromData(settings_black)
|
||||
self.settings_btn.setIcon(QIcon(settings_black_pixmap))
|
||||
|
||||
self.settings_btn.setToolTip("Settings")
|
||||
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
|
||||
@@ -449,8 +618,21 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
self.consoleLog.setReadOnly(True)
|
||||
self.consoleLog.setFixedHeight(150)
|
||||
self.consoleLog.setStyleSheet("margin-bottom: 0px; background: transparent;")
|
||||
containerLayout.addWidget(self.consoleLog)
|
||||
|
||||
self.statusbar.show()
|
||||
self.statusbar.setStyleSheet("background: transparent; border: none; padding-right: 1px; padding-left: 6px; padding-bottom: 1px; margin-top: -8px")
|
||||
|
||||
self.version = QLabel(f"Version: {state.version}")
|
||||
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
self.speed_label = QLabel("↓ 0.0 kB/s ↑ 0.0 kB/s")
|
||||
self.speed_label.setStyleSheet("padding-right: 6px;")
|
||||
self.statusbar.addPermanentWidget(self.speed_label)
|
||||
|
||||
self.setStatusBar(self.statusbar)
|
||||
|
||||
self.download_timer = QTimer()
|
||||
self.download_timer.timeout.connect(self.download_list_update)
|
||||
self.download_timer.start(500)
|
||||
@@ -471,10 +653,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
MainWindow._instance.log_signal.emit(text)
|
||||
|
||||
def _on_log_signal(self, text):
|
||||
self.consoleLog.append(text)
|
||||
self.consoleLog.verticalScrollBar().setValue(
|
||||
self.consoleLog.verticalScrollBar().maximum()
|
||||
)
|
||||
if hasattr(self, 'consoleLog'):
|
||||
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)
|
||||
@@ -485,27 +668,120 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
def download_list_update(self):
|
||||
if self.download_model:
|
||||
self.download_model.layoutAboutToBeChanged.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)
|
||||
row_count = self.download_model.rowCount()
|
||||
if row_count > 0:
|
||||
top_left = self.download_model.index(0, 0)
|
||||
bottom_right = self.download_model.index(row_count - 1, self.download_model.columnCount() - 1)
|
||||
self.download_model.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
|
||||
|
||||
if not hasattr(self, '_last_dl_row_count'):
|
||||
self._last_dl_row_count = 0
|
||||
if row_count != self._last_dl_row_count:
|
||||
|
||||
for row in range(row_count, self._last_dl_row_count):
|
||||
idx = self.download_model.index(row, 0)
|
||||
self.downloadList.closePersistentEditor(idx)
|
||||
self._last_dl_row_count = row_count
|
||||
self.download_model.layoutAboutToBeChanged.emit()
|
||||
self.download_model.layoutChanged.emit()
|
||||
for row in range(row_count):
|
||||
idx = self.download_model.index(row, 0)
|
||||
self.downloadList.closePersistentEditor(idx)
|
||||
self.downloadList.openPersistentEditor(idx)
|
||||
else:
|
||||
|
||||
delegate = self.downloadList.itemDelegateForColumn(0)
|
||||
for row in range(row_count):
|
||||
idx = self.download_model.index(row, 0)
|
||||
editor = self.downloadList.indexWidget(idx)
|
||||
if editor and delegate:
|
||||
delegate.setEditorData(editor, idx)
|
||||
self._update_speed_label()
|
||||
|
||||
def _update_speed_label(self):
|
||||
total_down = 0
|
||||
total_up = 0
|
||||
for handle in state.active_downloads.values():
|
||||
try:
|
||||
s = handle.status()
|
||||
total_down += s.download_rate
|
||||
total_up += s.upload_rate
|
||||
except Exception:
|
||||
pass
|
||||
down_kb = total_down / 1024
|
||||
up_kb = total_up / 1024
|
||||
down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
|
||||
up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
|
||||
self.speed_label.setText(f"↓ {down_text} ↑ {up_text}")
|
||||
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.rutrackerlist.clearSelection()
|
||||
self.uztrackerlist.clearSelection()
|
||||
self.monkruslist.clearSelection()
|
||||
self.downloadList.clearSelection()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
closehelper()
|
||||
event.accept()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
try:
|
||||
for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
|
||||
if obj == tbl.viewport():
|
||||
if event.type() == QEvent.Type.MouseMove:
|
||||
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
|
||||
idx = tbl.indexAt(pos)
|
||||
new_row = idx.row() if idx.isValid() else -1
|
||||
if new_row != self._tracker_hovered_row or tbl is not self._tracker_hovered_table:
|
||||
self._tracker_hovered_row = new_row
|
||||
self._tracker_hovered_table = tbl
|
||||
tbl.viewport().update()
|
||||
elif event.type() == QEvent.Type.Leave:
|
||||
self._tracker_hovered_row = -1
|
||||
self._tracker_hovered_table = None
|
||||
tbl.viewport().update()
|
||||
break
|
||||
if obj == self.downloadList.viewport():
|
||||
if event.type() == QEvent.Type.MouseMove:
|
||||
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
|
||||
idx = self.downloadList.indexAt(pos)
|
||||
new_row = idx.row() if idx.isValid() else -1
|
||||
if new_row != self._hovered_row:
|
||||
old_row = self._hovered_row
|
||||
self._hovered_row = new_row
|
||||
self._invalidate_hover_row(old_row)
|
||||
self._invalidate_hover_row(new_row)
|
||||
elif event.type() == QEvent.Type.Leave:
|
||||
old_row = self._hovered_row
|
||||
self._hovered_row = -1
|
||||
self._invalidate_hover_row(old_row)
|
||||
except RuntimeError:
|
||||
pass
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def _invalidate_hover_row(self, row):
|
||||
if row >= 0 and self.download_model:
|
||||
self.downloadList.viewport().update()
|
||||
|
||||
def set_tracker(self, _):
|
||||
old_tracker = state.tracker
|
||||
state.tracker = self.tracker_list.currentText()
|
||||
|
||||
old_widget = state.tracker_list[old_tracker]
|
||||
self.horizontal_layout.removeWidget(old_widget)
|
||||
old_widget.setParent(None)
|
||||
|
||||
tracker_widget = state.tracker_list[state.tracker]
|
||||
self.horizontal_layout.addWidget(tracker_widget)
|
||||
|
||||
def show_empty_results(self, show: bool):
|
||||
if show:
|
||||
self.qtablewidget.hide()
|
||||
state.tracker_list[state.tracker].hide()
|
||||
self.emptyResults.show()
|
||||
else:
|
||||
self.qtablewidget.show()
|
||||
state.tracker_list[state.tracker].show()
|
||||
self.emptyResults.hide()
|
||||
|
||||
def show_empty_downloads(self):
|
||||
@@ -519,8 +795,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
# 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))
|
||||
table_width = state.tracker_list[state.tracker].viewport().width()
|
||||
state.tracker_list[state.tracker].setColumnWidth(1, int(table_width * 0.3))
|
||||
|
||||
def contextMenuEvent(self, event: QContextMenuEvent):
|
||||
if self.downloadList.underMouse():
|
||||
@@ -0,0 +1,66 @@
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.data.scrapers.uztracker import scrape_uztracker
|
||||
from core.data.scrapers.rutracker import scrape_rutracker
|
||||
from core.data.scrapers.monkrus import scrape_m0nkrus
|
||||
from core.utils.network.jsonhandler import split_data, format_data, format_data_minimal
|
||||
from core.utils.data.state import state
|
||||
|
||||
scrapers = {
|
||||
"uztracker": scrape_uztracker,
|
||||
"rutracker": scrape_rutracker,
|
||||
"m0nkrus": scrape_m0nkrus
|
||||
}
|
||||
|
||||
def return_pressed(self):
|
||||
search_text = self.searchbar.text()
|
||||
if search_text == "":
|
||||
consoleLog("Error: Can't search for nothing")
|
||||
return
|
||||
consoleLog(f"User searched for: {search_text}")
|
||||
|
||||
if state.tracker == "rutracker":
|
||||
response = scrape_rutracker(search_text)
|
||||
if response:
|
||||
_, state.posts, _, _, cached = split_data(response)
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author, state.post_seeders, state.post_leechers = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
state.tracker_list[state.tracker].clear()
|
||||
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
state.tracker_list[state.tracker].setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
state.tracker_list[state.tracker].setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
state.tracker_list[state.tracker].setItem(i, 0, QTableWidgetItem(title))
|
||||
for i, seeders in enumerate(state.post_seeders):
|
||||
state.tracker_list[state.tracker].setItem(i, 2, QTableWidgetItem(seeders))
|
||||
for i, leechers in enumerate(state.post_leechers):
|
||||
state.tracker_list[state.tracker].setItem(i, 3, QTableWidgetItem(leechers))
|
||||
if state.debug == True:
|
||||
consoleLog(f"Response Cached: {cached}")
|
||||
else:
|
||||
consoleLog(f"No response from rutracker")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker is not None:
|
||||
state.posts = scrapers[state.tracker](search_text)
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
state.tracker_list[state.tracker].clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_author, state.post_titles, state.post_urls = format_data_minimal(state.posts)
|
||||
self.show_empty_results(False)
|
||||
state.tracker_list[state.tracker].clear()
|
||||
state.tracker_list[state.tracker].setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
state.tracker_list[state.tracker].setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
state.tracker_list[state.tracker].setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
state.tracker_list[state.tracker].setItem(i, 0, QTableWidgetItem(title))
|
||||
@@ -1,4 +1,4 @@
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.utils.data.state import state
|
||||
import psutil
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_active_interfaces():
|
||||
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:
|
||||
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up:
|
||||
active.append(interface)
|
||||
consoleLog(f"Found Active: {interface}")
|
||||
|
||||
@@ -33,7 +33,7 @@ def list_interfaces() -> None:
|
||||
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:
|
||||
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up:
|
||||
status = "ACTIVE"
|
||||
else:
|
||||
status = "INACTIVE"
|
||||
@@ -1,10 +1,11 @@
|
||||
import time
|
||||
import os
|
||||
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
|
||||
from core.utils.logging.logs import consoleLog
|
||||
|
||||
|
||||
global loop_running
|
||||
@@ -36,7 +37,7 @@ def init_session():
|
||||
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}")
|
||||
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
|
||||
|
||||
state.dl_session.apply_settings(settings)
|
||||
|
||||
@@ -51,8 +52,26 @@ def add_download(magnet_uri, dl_path=state.download_path):
|
||||
init_session()
|
||||
|
||||
if magnet_uri in state.active_downloads:
|
||||
consoleLog("Skipping, download already running...")
|
||||
return
|
||||
try:
|
||||
handle = state.active_downloads[magnet_uri]
|
||||
status = handle.status()
|
||||
except RuntimeError as e:
|
||||
consoleLog(f"Error in LibTorrent Handle: {e}")
|
||||
|
||||
if status.has_metadata:
|
||||
filepath = os.path.join(status.save_path, status.name)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
|
||||
consoleLog(f"File Deleted, redownloading: {status.name}")
|
||||
state.dl_session.remove_torrent(handle)
|
||||
del state.active_downloads[magnet_uri]
|
||||
else:
|
||||
consoleLog("Skipping Downloading, download already running...")
|
||||
return False
|
||||
else:
|
||||
consoleLog("Skipping Downloading, download already running... ")
|
||||
return False
|
||||
|
||||
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
||||
magnetdl.save_path = dl_path
|
||||
@@ -62,8 +81,27 @@ def add_download(magnet_uri, dl_path=state.download_path):
|
||||
consoleLog(f"Added {magnet_uri} to downloads")
|
||||
|
||||
run_thread(threading.Thread(target=dl_status_loop))
|
||||
return True
|
||||
|
||||
|
||||
def add_seed(magnet_uri, file_path):
|
||||
if state.active_downloads is None:
|
||||
state.active_downloads = {}
|
||||
|
||||
init_session()
|
||||
|
||||
if magnet_uri in state.active_downloads:
|
||||
consoleLog("Already seeding this torrent")
|
||||
return False
|
||||
|
||||
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
||||
magnetdl.save_path = os.path.dirname(file_path)
|
||||
|
||||
handle = state.dl_session.add_torrent(magnetdl)
|
||||
state.active_downloads[magnet_uri] = handle
|
||||
state.seeded_magnets.add(magnet_uri)
|
||||
return True
|
||||
|
||||
|
||||
def dl_status_loop():
|
||||
global loop_running
|
||||
@@ -114,7 +152,7 @@ def update_settings():
|
||||
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}")
|
||||
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
|
||||
|
||||
state.dl_session.apply_settings(settings)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 core.utils.logging.logs import update_download_completed_by_hash
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from plyer import notification
|
||||
import libtorrent as lt
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
@@ -50,11 +51,31 @@ def update_log(shutdown_event):
|
||||
|
||||
status = magnetdl.status()
|
||||
|
||||
if status.state == lt.torrent_status.seeding and magnet_uri not in updated:
|
||||
if status.state == lt.torrent_status.seeding and magnet_uri not in updated and magnet_uri not in state.seeded_magnets:
|
||||
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)
|
||||
|
||||
def check_deleted_files(shutdown_event):
|
||||
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 status.has_metadata:
|
||||
file_path = os.path.join(status.save_path, status.name)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
consoleLog(f"Registered File Deletion: {status.name}")
|
||||
state.dl_session.remove_torrent(magnetdl)
|
||||
del state.active_downloads[magnet_uri]
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(5)
|
||||
@@ -1,4 +1,4 @@
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.network.libtorrent_int import add_download
|
||||
|
||||
def add_magnet(uri):
|
||||
@@ -8,16 +8,23 @@ def create_config():
|
||||
|
||||
config["General"] = {
|
||||
"debug": True,
|
||||
"ignore_updates": f"{state.ignore_updates}",
|
||||
"autoresume": f"{state.autoresume}",
|
||||
"window_transparency": f"{state.window_transparency}"
|
||||
}
|
||||
|
||||
config["Network"] = {
|
||||
"api_url": f"{state.api_url}",
|
||||
"download_path": f"{state.download_path}",
|
||||
"download_speed_limit": f"{state.down_speed_limit}",
|
||||
"upload_speed_limit": f"{state.up_speed_limit}",
|
||||
"ignore_updates": f"{state.ignore_updates}",
|
||||
"image_path": f"{state.image_path}",
|
||||
"autoresume": f"{state.autoresume}",
|
||||
"upload_speed_limit": f"{state.up_speed_limit}",
|
||||
"max_connections": f"{state.max_connections}",
|
||||
"max_downloads": f"{state.max_downloads}",
|
||||
"window_transparency": f"{state.window_transparency}",
|
||||
"max_downloads": f"{state.max_downloads}"
|
||||
}
|
||||
|
||||
config["Paths"] = {
|
||||
"bound_interface": f"{state.bound_interface}",
|
||||
"image_path": f"{state.image_path}"
|
||||
}
|
||||
|
||||
if platform.system() == "Windows":
|
||||
@@ -48,17 +55,23 @@ def read_config():
|
||||
return
|
||||
|
||||
config.read(config_file)
|
||||
|
||||
|
||||
# General
|
||||
state.debug = config.getboolean("General", "debug", fallback=state.debug)
|
||||
state.api_url = config.get("General", "api_url", fallback=state.api_url)
|
||||
state.download_path = config.get("General", "download_path", fallback=state.download_path)
|
||||
state.down_speed_limit = config.getint("General", "download_speed_limit", fallback=state.down_speed_limit)
|
||||
state.up_speed_limit = config.getint("General", "upload_speed_limit", fallback=state.up_speed_limit)
|
||||
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
|
||||
state.image_path = config.get("General", "image_path", fallback=state.image_path)
|
||||
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
|
||||
state.max_connections = config.getint("General", "max_connections", fallback=state.max_connections)
|
||||
state.max_downloads = config.getint("General", "max_downloads", fallback=state.max_downloads)
|
||||
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
|
||||
|
||||
# Network
|
||||
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
|
||||
state.download_path = config.get("Network", "download_path", fallback=state.download_path)
|
||||
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
|
||||
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
|
||||
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
|
||||
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
|
||||
|
||||
# Paths
|
||||
state.bound_interface = config.get("Paths", "bound_interface", fallback=state.bound_interface)
|
||||
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
|
||||
|
||||
create_config()
|
||||
@@ -1,6 +1,6 @@
|
||||
from core.network.libtorrent_int import update_settings
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from .config import create_config
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class Download:
|
||||
title: str
|
||||
url: str
|
||||
magnet_uri: str
|
||||
path: str
|
||||
completed: bool
|
||||
|
||||
@dataclass
|
||||
@@ -1,4 +1,5 @@
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import QTableWidget
|
||||
from typing import Optional, Any, List, Dict
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,9 +8,9 @@ class AppState(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.posts: List[Dict[str, Any]] = []
|
||||
self.post_titles: List[str] = []
|
||||
self.post_urls: List[str] = []
|
||||
self.posts: list[Any] | None = None
|
||||
self.post_titles: Optional[List] = None
|
||||
self.post_urls: Optional[List] = None
|
||||
self.post_author: List[str] = []
|
||||
self.post_seeders: List[str] = []
|
||||
self.post_leechers: List[str] = []
|
||||
@@ -19,6 +20,7 @@ class AppState(QObject):
|
||||
self.debug: bool = False
|
||||
self.autoresume: bool = True
|
||||
self.tracker: str = "rutracker"
|
||||
self.tracker_list: dict[str, QTableWidget] = {}
|
||||
self.api_url: str = "https://api.michijackson.xyz"
|
||||
self.download_path: str = str(Path.home() / "Downloads")
|
||||
self.up_speed_limit: int = 0
|
||||
@@ -27,11 +29,12 @@ class AppState(QObject):
|
||||
self.max_downloads: int = 10
|
||||
self.settings_path: str = ""
|
||||
self.dl_session: Any = None
|
||||
self.active_downloads: List[str] = {}
|
||||
self.active_downloads: Dict = {}
|
||||
self.seeded_magnets: set = set()
|
||||
self.window_transparency: bool = False
|
||||
self.interfaces: List = []
|
||||
self.active_interfaces: List = []
|
||||
self.bound_interface: str = None
|
||||
self.bound_interface: Any = None
|
||||
|
||||
@property
|
||||
def image_path(self) -> str:
|
||||
@@ -1,7 +1,7 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.utils.network.jsonhandler import format_data
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ def get_item_url(item, posts, post_titles): # softwarelist currentitem, post lis
|
||||
return item
|
||||
if state.tracker == "rutracker":
|
||||
item_dict = posts[post_index]
|
||||
_, post_links, _ = format_data([item_dict])
|
||||
_, post_links, _, _, _ = format_data([item_dict])
|
||||
consoleLog(f"Found post URL: {post_links[0]}")
|
||||
return post_links[0]
|
||||
if state.tracker == "m0nkrus":
|
||||
item_dict = posts[post_index]
|
||||
_, post_links, _ = format_data([item_dict])
|
||||
_, post_links, _, _, _,= format_data([item_dict])
|
||||
consoleLog(f"Found post URL: {post_links[0]}")
|
||||
return post_links[0]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from core.utils.logging.logs import consoleLog, remove_download_log
|
||||
from core.utils.network.download import run_download_direct, seed_magnet
|
||||
|
||||
import os
|
||||
|
||||
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}")
|
||||
|
||||
def check_downloads(downloads):
|
||||
for download in downloads:
|
||||
if os.path.exists(download.path):
|
||||
consoleLog(f"Existing Download: {download.title}")
|
||||
seed_magnet(download.magnet_uri, download.path)
|
||||
else:
|
||||
consoleLog(f"Inexistent Download: {download.title}")
|
||||
remove_download_log(download.magnet_uri)
|
||||
|
||||
@@ -5,11 +5,33 @@ from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import threading
|
||||
|
||||
_log_buffer = []
|
||||
_downloads_lock = threading.RLock()
|
||||
|
||||
|
||||
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
# wait for metadata outside the lock to avoid blocking other threads
|
||||
magnetdl = state.active_downloads.get(magnet_uri)
|
||||
if magnetdl:
|
||||
status = magnetdl.status()
|
||||
timeout = 60
|
||||
start = time.time()
|
||||
while not status.has_metadata and (time.time() - start) < timeout:
|
||||
time.sleep(0.5)
|
||||
status = magnetdl.status()
|
||||
torrent_name = status.name
|
||||
save_path = status.save_path
|
||||
consoleLog(f"Torrent name (has_metadata={status.has_metadata}): {torrent_name}")
|
||||
path = os.path.join(save_path, torrent_name)
|
||||
else:
|
||||
path = os.path.join(state.download_path, title)
|
||||
with _downloads_lock:
|
||||
return _add_download_log_inner(title, url, magnet_uri, completed, path)
|
||||
|
||||
def _add_download_log_inner(title, url, magnet_uri, completed, path) -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
@@ -21,10 +43,14 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
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")
|
||||
if magnet_uri in state.active_downloads:
|
||||
consoleLog("Skipping Logging, download already running...")
|
||||
return
|
||||
consoleLog("File already in Log, updating Download State...")
|
||||
hash = extract_hash_from_magnet(magnet_uri)
|
||||
update_download_completed_by_hash(hash, False)
|
||||
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
|
||||
|
||||
|
||||
@@ -32,7 +58,8 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
title=title,
|
||||
url=url,
|
||||
magnet_uri=magnet_uri,
|
||||
completed=completed,
|
||||
path=path,
|
||||
completed=completed
|
||||
))
|
||||
|
||||
consoleLog(f"Added {title} to Log File")
|
||||
@@ -44,6 +71,10 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
return download_list
|
||||
|
||||
def remove_download_log(magnet_uri) -> DownloadList:
|
||||
with _downloads_lock:
|
||||
return _remove_download_log_inner(magnet_uri)
|
||||
|
||||
def _remove_download_log_inner(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:
|
||||
@@ -56,10 +87,12 @@ def remove_download_log(magnet_uri) -> DownloadList:
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
|
||||
magnet_link = (magnet_uri or "").strip()
|
||||
title = next((getattr(d, 'title', 'Unknown') for d in downloads if (getattr(d, 'magnet_uri', None) or '').strip() == magnet_link or (getattr(d, 'url', None) or '').strip() == magnet_link))
|
||||
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")
|
||||
consoleLog(f"Removed {title} from Log File")
|
||||
|
||||
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||
with open(downloads_file, "w") as file:
|
||||
@@ -68,6 +101,10 @@ def remove_download_log(magnet_uri) -> DownloadList:
|
||||
return download_list
|
||||
|
||||
def update_download_completed(magnet_uri, completed) -> DownloadList:
|
||||
with _downloads_lock:
|
||||
return _update_download_completed_inner(magnet_uri, completed)
|
||||
|
||||
def _update_download_completed_inner(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:
|
||||
@@ -120,6 +157,10 @@ def update_download_completed(magnet_uri, completed) -> DownloadList:
|
||||
|
||||
|
||||
def get_download_logs() -> DownloadList:
|
||||
with _downloads_lock:
|
||||
return _get_download_logs_inner()
|
||||
|
||||
def _get_download_logs_inner() -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
@@ -143,6 +184,10 @@ def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
|
||||
|
||||
|
||||
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
|
||||
with _downloads_lock:
|
||||
return _update_download_completed_by_hash_inner(info_hash, completed)
|
||||
|
||||
def _update_download_completed_by_hash_inner(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:
|
||||
@@ -0,0 +1,40 @@
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.utils.data.tracker import get_item_url
|
||||
from core.utils.data.tracker import get_magnet_link
|
||||
from core.network.libtorrent_wrapper import add_download
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.logging.logs import add_download_log
|
||||
from core.network.libtorrent_int import add_seed
|
||||
import threading
|
||||
|
||||
|
||||
def download_selected(items, posts, post_titles):
|
||||
if not items:
|
||||
consoleLog("No item selected for download.")
|
||||
return
|
||||
seen = set()
|
||||
for item in items:
|
||||
if item.column() != 0:
|
||||
continue
|
||||
text = item.text()
|
||||
if text and text not in seen:
|
||||
seen.add(text)
|
||||
consoleLog(f"Downloading {text}")
|
||||
run_thread(threading.Thread(target=run_download, args=(text, posts, post_titles)))
|
||||
|
||||
def run_download(item, posts, post_titles):
|
||||
post_url = get_item_url(item, posts, post_titles)
|
||||
consoleLog(f"Selected URL: {post_url}")
|
||||
magnet_uri = get_magnet_link(post_url)
|
||||
|
||||
if add_download(magnet_uri):
|
||||
add_download_log(item, post_url, magnet_uri, False)
|
||||
|
||||
def run_download_direct(magnet_uri):
|
||||
consoleLog(f"Direct download: {magnet_uri[:60]}")
|
||||
add_download(magnet_uri)
|
||||
add_download_log("Direct Download", "", magnet_uri, False)
|
||||
|
||||
def seed_magnet(magnet_uri, file_path):
|
||||
consoleLog(f"Seeding: {magnet_uri[:60]}")
|
||||
add_seed(magnet_uri, file_path)
|
||||
@@ -11,7 +11,7 @@ def split_data(data):
|
||||
|
||||
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]
|
||||
@@ -20,6 +20,16 @@ def format_data(data):
|
||||
|
||||
return post_titles, post_links, post_author, post_seeders, post_leechers
|
||||
|
||||
def format_data_minimal(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]
|
||||
|
||||
return post_author, post_titles, post_links
|
||||
|
||||
|
||||
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
|
||||
|
||||
|
||||
|
||||
|
||||
# https://open.spotify.com/track/0F0fMcIdL2FmcC9eYqwazX
|
||||
@@ -1,6 +1,6 @@
|
||||
import requests
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.logging.logs import consoleLog
|
||||
|
||||
def check_for_updates():
|
||||
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
|
||||
@@ -17,11 +17,7 @@ def check_for_updates():
|
||||
latest_version = latest_release["name"]
|
||||
assets = latest_release["assets"]
|
||||
|
||||
|
||||
if state.version == "dev":
|
||||
consoleLog("Dev release detected, skipping version check")
|
||||
return None, None
|
||||
elif latest_version != state.version:
|
||||
if latest_version != state.version:
|
||||
consoleLog(f"New release available: {latest_version}")
|
||||
if assets:
|
||||
consoleLog("Assets:")
|
||||
+19
-19
@@ -1,13 +1,12 @@
|
||||
from core.interface.gui import MainWindow
|
||||
from core.interface.gui import MainWindow, windowCloseHelper
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.network.libtorrent_misc import send_notification, update_log
|
||||
from core.utils.general.logs import get_download_logs
|
||||
from core.utils.logging.logs import consoleLog
|
||||
from core.network.libtorrent_misc import send_notification, update_log, check_deleted_files
|
||||
from core.utils.logging.logs import get_download_logs
|
||||
from core.utils.general.shutdown import closehelper, shutdown_event
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.general.loghandler import split_data, check_completed
|
||||
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
|
||||
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 PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
@@ -15,13 +14,8 @@ import qdarktheme
|
||||
import threading
|
||||
import platform
|
||||
import signal
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
args = parser.parse_args()
|
||||
import time
|
||||
|
||||
def run_gui():
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
@@ -32,25 +26,25 @@ def run_gui():
|
||||
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
|
||||
|
||||
from core.utils.general.logs import set_main_window
|
||||
from core.utils.logging.logs import set_main_window
|
||||
set_main_window(widget)
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
def keyboardinterrupthandler(signum, frame):
|
||||
closehelper()
|
||||
windowCloseHelper()
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
start_time = time.perf_counter()
|
||||
read_config()
|
||||
logs = get_download_logs()
|
||||
count, downloads = split_data(logs)
|
||||
if args.debug:
|
||||
state.debug = args.debug # override of read_config
|
||||
_, downloads = split_data(logs)
|
||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||
consoleLog("Starting SoftwareManager...")
|
||||
consoleLog("Fetching Network Interfaces...")
|
||||
list_interfaces()
|
||||
consoleLog("Initialiting Interface variables...")
|
||||
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))
|
||||
@@ -59,6 +53,12 @@ if __name__ == "__main__":
|
||||
consoleLog("Started Thread: update_log")
|
||||
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
|
||||
consoleLog("Started Thread: check_completed")
|
||||
consoleLog("Launching GUI")
|
||||
run_thread(threading.Thread(target=check_deleted_files, args=(shutdown_event,), daemon=True))
|
||||
consoleLog("Started Thread: check_deleted_files")
|
||||
check_downloads(downloads)
|
||||
elapsed = time.perf_counter() - start_time
|
||||
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
||||
run_gui()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user