Compare commits

...

25 Commits

Author SHA1 Message Date
shayaa d4b7067484 Merge pull request #51 from KeksPirates/refactor/improve-code-quality
Refactor/improve code quality
2026-03-21 02:50:07 +01:00
Vxrtrauter 77c2e9cd9e fix: tab layout, clear search state, and cleanup
UI: remove trackertable from the Search tab and reorder widgets to correct layout. Tab helper: add QLayout directly instead of wrapping it in a temporary QWidget to avoid extra nesting. Search helper: clear the trackertable rows and reset state.posts when starting a new search to ensure fresh results. Minor: import check_space in main and simplify an unused loop variable in libtorrent_int for clarity/linting.
2026-03-21 02:48:48 +01:00
Vxrtrauter 290388a7dc Merge branch 'main' into refactor/improve-code-quality 2026-03-21 02:48:04 +01:00
KeksNino 3b51552313 check for free disk space at startup
resolve merge
2026-03-21 02:08:23 +01:00
Vxrtrauter 9e7607dd36 refactor: get_magnet to get_download_link
Rename scraper method get_magnet -> get_download_link in monkrus, rutracker and uztracker. Update downloader code to call .get_download_link and use the .is_magnet flag on tracker objects (replace dict access). Switch direct_download package imports to absolute paths and perform a small import reorder in direct_download/utils.py. These changes unify the download API and adapt call sites to the new tracker interface.
2026-03-21 02:03:49 +01:00
KeksNino 616552063d fix gui not starting, caused by tabhelper 2026-03-21 01:47:46 +01:00
KeksNino 9eb70af1a1 refactored tabhelper function 2026-03-21 01:14:25 +01:00
Vxrtrauter 4d4187ce63 fix: revert update.py changes 2026-03-21 01:09:31 +01:00
shayaa cac2ad3ab1 Revise disclaimer in README.md
Updated disclaimer for legal and ethical use of SoftwareManager.
2026-03-21 00:58:17 +01:00
shayaa cdbf9165d2 Update image path for logo in test workflow 2026-03-21 00:55:34 +01:00
KeksNino 23e44cb128 remove qt widget from updater.py and use class instead 2026-03-21 00:39:28 +01:00
Vxrtrauter 0a591e1cda refactor: update progress dialog and tidy UI 2026-03-21 00:27:56 +01:00
Vxrtrauter c0f5060451 fix: searchbar disabling itself when searching for nothing 2026-03-20 22:51:28 +01:00
Vxrtrauter c6940f0178 fix: add self argument to function 2026-03-20 02:26:42 +01:00
Vxrtrauter 2afb3e7716 refactor: improve scraper handling by utilizing classes for registration 2026-03-20 02:22:40 +01:00
Vxrtrauter 325eda54fa refactor: remove core/ folder 2026-03-19 20:42:28 +01:00
Vxrtrauter 53d4a240a0 refactor state.py and add comments to it, add another check to version getter. 2026-03-18 23:33:33 +01:00
shayaa a56969f19d Add GitHub Actions workflow for building testing executables 2026-03-17 23:43:47 +01:00
Vxrtrauter 2beabbb1ba refactor: improve search functionality and code organization in GUI components 2026-03-16 14:24:23 +01:00
shayaa 5bb732072e Merge pull request #48 from KeksPirates/fix/link-updater-to-gui
link updater to gui again
2026-03-16 03:04:35 +01:00
Vxrtrauter f50bfd0aa0 link updater to gui again 2026-03-16 03:04:14 +01:00
shayaa 71adbfcd96 Merge pull request #47 from KeksPirates/fix/invalid-version-detection
Fix invalid version detection
2026-03-16 02:45:46 +01:00
Vxrtrauter 270ee027f7 feat: add semi-dynamic discovering of build info file 2026-03-16 02:31:03 +01:00
Vxrtrauter a443f64501 refactor: move build_info.json to root folder, update workflow to support changes, rename function to more accurate name 2026-03-16 02:02:03 +01:00
Vxrtrauter 0e05f42741 Fix invalid version detection, refactor main.py and add comments for improved code understanding 2026-03-16 01:35:52 +01:00
63 changed files with 788 additions and 631 deletions
+2 -8
View File
@@ -106,7 +106,7 @@ jobs:
run: |
python -c "
from PIL import Image
img = Image.open('src/core/interface/assets/logo.png')
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
@@ -125,8 +125,6 @@ jobs:
--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 \
--output-dir=nuitka-build \
src/main.py
@@ -137,8 +135,6 @@ jobs:
run: |
pyinstaller --onefile --windowed \
--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 \
@@ -155,8 +151,6 @@ jobs:
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 \
@@ -287,4 +281,4 @@ jobs:
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+185
View File
@@ -0,0 +1,185 @@
name: Build Executables (Test)
on:
workflow_dispatch:
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
fail-fast: false # keep other platforms building even if one fails
matrix:
include:
- os: ubuntu-22.04
artifact_name: SoftwareManager-test-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-windows
- os: macos-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.7"
enable-cache: true
- name: Install Python dependencies
run: |
uv pip install -r requirements.txt
uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env:
UV_SYSTEM_PYTHON: 1
- name: Generate version
id: version
shell: bash
run: |
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
BRANCH="${{ github.head_ref || github.ref_name }}"
BRANCH_SLUG="${BRANCH//\//-}" # replace slashes with dashes
VERSION="${BRANCH_SLUG}-${SHORT_SHA}-test"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject build info
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Install UPX (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
7z x upx.zip -oupx-dir
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
upx --version | head -1
- name: Convert icon for Windows
if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
- name: Build with Nuitka (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
python -m nuitka \
--standalone \
--assume-yes-for-downloads \
--windows-console-mode=disable \
--windows-icon-from-ico=app_icon.ico \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Stage artifact
shell: bash
run: |
mkdir -p dist-final
if [ "$RUNNER_OS" == "Windows" ]; then
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
ls -la dist-final/
- name: Create ZIP (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip"
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}.zip
retention-days: 7
- name: Upload artifact (Linux/macOS)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}
retention-days: 7
-1
View File
@@ -62,7 +62,6 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
</picture>
</a>
---
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
View File
View File
-87
View File
@@ -1,87 +0,0 @@
from core.utils.data.tracker import get_magnet_link
from core.utils.data.state import state
from bs4 import BeautifulSoup
from typing import Dict
import requests
import time
cache = {
"data": [],
"last_fetched": 0
}
cache_expiry = 300
def _get_telegram_posts():
post = 100
max_posts = 250
posts = []
added = set()
current_time = time.time()
if cache["data"] and (current_time - cache["last_fetched"] < cache_expiry):
return cache["data"]
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
if not bubbles:
break
for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if post_txt is None or post_txt.b is None:
continue
title = post_txt.b.text
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
if link:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
title=title,
author="m0nkrus",
id=len(posts) + 1,
url=post_url
))
post += 22
posts.reverse()
cache["data"] = posts
cache["last_fetched"] = current_time
return(posts)
def scrape_m0nkrus(query):
posts = _get_telegram_posts()
filtered_posts = []
for post in posts:
if query.lower() in post["title"].lower():
filtered_post = post.copy()
filtered_post["id"] = len(filtered_posts) + 1
filtered_posts.append(filtered_post)
return filtered_posts
def get_magnet(post: Dict):
return get_magnet_link(post["url"])
Metadata = {
"name" : "m0nkrus",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_m0nkrus,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_m0nkrus():
state.trackers.update({Metadata["name"] : Metadata})
-52
View File
@@ -1,52 +0,0 @@
from core.utils.network.jsonhandler import split_data, format_data
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from typing import Dict
import requests
def scrape_rutracker(query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
consoleLog("Sent request to server")
if search:
_, data, _, success, cached = split_data(search.text)
if cached:
consoleLog("Server response cached")
if success:
sorted_data = []
for entry in data:
sorted_data.append(
{
"title" : entry["title"],
"author" : entry["author"],
"seeders" : entry["seeders"],
"leechers" : entry["leechers"],
"url" : entry["url"],
"id" : entry["id"],
}
)
return sorted_data
else:
consoleLog("Scraping failed server-side, unable to fetch posts from rutracker")
return []
else:
consoleLog("No response from server, returning nothing")
return []
def get_magnet(post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
Metadata = {
"name" : "rutracker",
"headers" : ["Post Title", "Author", "Seeders", "Leechers"],
"scrapeFunc" : scrape_rutracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_rutracker():
state.trackers.update({Metadata["name"] : Metadata})
-122
View File
@@ -1,122 +0,0 @@
from core.data.scrapers.provider.buzzheavier import scrape_buzzheavier
from core.data.scrapers.provider.gofile import scrape_gofile
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from bs4 import BeautifulSoup
from typing import Dict
import webbrowser
import requests
import time
import re
cache = {
"data": [],
"last_fetched": 0
}
cache_expiry = 300
def get_Metadata():
return Metadata
def scrape_steamrip_links():
current_time = time.time()
if cache["data"] != [] and (current_time - cache["last_fetched"] < cache_expiry):
return cache["data"]
url = f"https://steamrip.com/games-list-page/"
response = requests.get(url)
text = response.text
soup = BeautifulSoup(text, "html.parser")
games = soup.find_all("li", class_="az-list-item")
if len(games) == 0:
return []
links = []
names = []
for gamehtml in games:
link = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
links += re.findall(r'(?<=href=")[^"]*', link.__str__())
name = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
names += re.findall(r'(?<=\/">)[^<]*', name.__str__())
# construct the list[dict[str,str]]
ret = []
for i in range(len(names)):
ret.append({"title" : names[i], "url" : links[i]})
return ret
def scrape_steamrip_game_downloads(gamelink):
url = "https://steamrip.com" + gamelink
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
download_link_elements = soup.find_all("a",class_="shortc-button")
download_links = ["buzzheavier", "gofile"]
for download_link in download_link_elements:
pure = download_link.attrs.get("href")
if pure[2] == "b":
download_links[0] = "https:" + pure
if pure[2] == "g":
download_links[1] = "https:" + pure
ret = []
for link in download_links:
if len(link) != 1:
ret.append(link)
cache["data"] = ret
return ret
def get_download_link(post: Dict):
url = post["url"]
links = scrape_steamrip_game_downloads(url)
best = ""
for link in links:
if link[0] == "h":
best = link
break
if links.index(best) == 0:
link = scrape_buzzheavier(best)
return link
elif links.index(best) == 1:
link, headers = scrape_gofile(best)
return link, headers
else:
consoleLog("Unable to retrieve download link due to captcha, launching browser...")
webbrowser.open(best)
return None, None
def filter_steamrip(query: str):
games = scrape_steamrip_links()
filtered_games = [
game for game in games
if query.lower() in game["title"].lower()
]
return filtered_games
Metadata = {
"name" : "steamrip",
"headers" : ["Game"],
"scrapeFunc" : filter_steamrip,
"linkFunc" : get_download_link,
"isMagnet" : False,
}
def init_steamrip():
state.trackers.update({Metadata["name"] : Metadata})
-55
View File
@@ -1,55 +0,0 @@
from core.utils.data.tracker import get_magnet_link
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from typing import Dict
import requests
def scrape_uztracker(query):
base_url="https://uztracker.net/"
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
consoleLog(search_url)
posts = []
try:
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
for link in links:
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
if not theme_link or not theme_link.b:
continue
url = urljoin(base_url, theme_link['href'])
title = theme_link.b.text
author_link = link.find('a', class_="med")
author = author_link.text.strip() if author_link else "Unknown"
posts.append(dict(
title=title,
author=author,
url=url,
))
return posts
except requests.RequestException as e:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
def get_magnet(post: Dict):
return get_magnet_link(post["url"])
Metadata = {
"name" : "uztracker",
"headers" : ["Post Title", "Author"],
"scrapeFunc" : scrape_uztracker,
"linkFunc" : get_magnet,
"isMagnet" : True,
}
def init_uztracker():
state.trackers.update({Metadata["name"] : Metadata})
-26
View File
@@ -1,26 +0,0 @@
from core.data.scrapers.rutracker import init_rutracker
from core.data.scrapers.uztracker import init_uztracker
from core.data.scrapers.steamrip import init_steamrip
from core.data.scrapers.monkrus import init_m0nkrus
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
init_rutracker()
init_uztracker()
init_m0nkrus()
init_steamrip()
def return_pressed(self):
self.show_empty_results(False)
search_text = self.searchbar.text()
if search_text == "":
consoleLog("Error: Can't search for nothing")
return
consoleLog(f"User searched for: {search_text}")
tracker = state.trackers[state.currenttracker]
scrapefunc = tracker["scrapeFunc"]
state.posts = scrapefunc(search_text)
from core.interface.gui import MainWindow
MainWindow._instance.search_results_signal.emit(tracker["headers"])
-56
View File
@@ -1,56 +0,0 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout
def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
tab = QWidget()
layout = QVBoxLayout()
if searchbar:
layout.addWidget(searchbar)
if layout2:
layout.addLayout(layout2)
else:
layout.addWidget(software_list)
if dlbutton:
layout.addWidget(dlbutton)
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
#################
# SETTINGS TABS #
#################
def general_tab(title, autoresume, update_checkbox, transparent_window, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(autoresume)
layout.addWidget(update_checkbox)
layout.addWidget(transparent_window)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
def paths_tab(title, download_path, image_path, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(download_path)
layout.addWidget(image_path)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
def network_tab(title, network_interface, max_connections, max_downloads, up_speed_limit, down_speed_limit, api_url, tabs):
tab = QWidget()
layout = QVBoxLayout()
layout.addWidget(network_interface)
layout.addWidget(max_connections)
layout.addWidget(max_downloads)
layout.addWidget(up_speed_limit)
layout.addWidget(down_speed_limit)
layout.addWidget(api_url)
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
View File
View File
-2
View File
@@ -1,2 +0,0 @@
def run_thread(thread):
thread.start()
@@ -1,4 +1,4 @@
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
import requests
import re
+12
View File
@@ -0,0 +1,12 @@
from data.sources.rutracker import RutrackerScraper
from data.sources.uztracker import UztrackerScraper
from data.sources.steamrip import SteamripScraper
from data.sources.monkrus import MonkrusScraper
# Register Scrapers here
SCRAPERS = [
RutrackerScraper(),
SteamripScraper(),
MonkrusScraper(),
UztrackerScraper(),
]
+78
View File
@@ -0,0 +1,78 @@
from utils.data.tracker import get_magnet_link
from bs4 import BeautifulSoup
from typing import Dict
import requests
import time
class MonkrusScraper:
name = "m0nkrus"
headers = ["Post Title", "Author"]
is_magnet = True
def __init__(self):
self.cache = { "data": [], "last_fetched": 0 }
self.cache_expiry = 300
def _get_telegram_posts(self):
post = 100
max_posts = 250
posts = []
added = set()
current_time = time.time()
if self.cache["data"] and (current_time - self.cache["last_fetched"] < self.cache_expiry):
return self.cache["data"]
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
if not bubbles:
break
for bubble in bubbles:
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
if post_txt is None or post_txt.b is None:
continue
title = post_txt.b.text
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
if link:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
title=title,
author="m0nkrus",
id=len(posts) + 1,
url=post_url
))
post += 22
posts.reverse()
self.cache["data"] = posts
self.cache["last_fetched"] = current_time
return(posts)
def search(self, query):
posts = self._get_telegram_posts()
filtered_posts = []
for post in posts:
if query.lower() in post["title"].lower():
filtered_post = post.copy()
filtered_post["id"] = len(filtered_posts) + 1
filtered_posts.append(filtered_post)
return filtered_posts
def get_download_link(self, post: Dict):
return get_magnet_link(post["url"])
+46
View File
@@ -0,0 +1,46 @@
from utils.network.jsonhandler import split_data, format_data
from utils.data.tracker import get_magnet_link
from utils.logging.logs import consoleLog
from utils.data.state import state
from typing import Dict
import requests
class RutrackerScraper:
name = "rutracker"
headers = ["Post Title", "Author", "Seeders", "Leechers"]
is_magnet = True
def search(self, query):
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
consoleLog("Sent request to server")
if search:
_, data, _, success, cached = split_data(search.text)
if cached:
consoleLog("Server response cached")
if success:
sorted_data = []
for entry in data:
sorted_data.append(
{
"title" : entry["title"],
"author" : entry["author"],
"seeders" : entry["seeders"],
"leechers" : entry["leechers"],
"url" : entry["url"],
"id" : entry["id"],
}
)
return sorted_data
else:
consoleLog("Scraping failed server-side, unable to fetch posts from rutracker")
return []
else:
consoleLog("No response from server, returning nothing")
return []
def get_download_link(self, post: Dict):
_, post_links, _, _, _ = format_data([post])
return get_magnet_link(post_links[0])
+107
View File
@@ -0,0 +1,107 @@
from data.hosts.buzzheavier import scrape_buzzheavier
from data.hosts.gofile import scrape_gofile
from utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
from typing import Dict
import webbrowser
import requests
import time
import re
class SteamripScraper:
name = "steamrip"
headers = ["Game"]
is_magnet = False
def __init__(self):
self.cache = { "data": [], "last_fetched": 0 }
self.cache_expiry = 300
def scrape_steamrip_links(self):
current_time = time.time()
if self.cache["data"] != [] and (current_time - self.cache["last_fetched"] < self.cache_expiry):
return self.cache["data"]
url = f"https://steamrip.com/games-list-page/"
response = requests.get(url)
text = response.text
soup = BeautifulSoup(text, "html.parser")
games = soup.find_all("li", class_="az-list-item")
if len(games) == 0:
return []
links = []
names = []
for gamehtml in games:
link = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
links += re.findall(r'(?<=href=")[^"]*', link.__str__())
name = gamehtml.find("a", href=lambda x: x and x.startswith("/"))
names += re.findall(r'(?<=\/">)[^<]*', name.__str__())
# construct the list[dict[str,str]]
ret = []
for i in range(len(names)):
ret.append({"title" : names[i], "url" : links[i]})
return ret
def scrape_steamrip_game_downloads(self, gamelink):
url = "https://steamrip.com" + gamelink
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
download_link_elements = soup.find_all("a",class_="shortc-button")
download_links = ["buzzheavier", "gofile"]
for download_link in download_link_elements:
pure = download_link.attrs.get("href")
if pure[2] == "b":
download_links[0] = "https:" + pure
if pure[2] == "g":
download_links[1] = "https:" + pure
ret = []
for link in download_links:
if len(link) != 1:
ret.append(link)
self.cache["data"] = ret
return ret
def get_download_link(self, post: Dict):
url = post["url"]
links = self.scrape_steamrip_game_downloads(url)
best = ""
for link in links:
if link[0] == "h":
best = link
break
if links.index(best) == 0:
link = scrape_buzzheavier(best)
return link
elif links.index(best) == 1:
link, headers = scrape_gofile(best)
return link, headers
else:
consoleLog("Unable to retrieve download link due to captcha, launching browser...")
webbrowser.open(best)
return None, None
def search(self, query: str):
games = self.scrape_steamrip_links()
filtered_games = [
game for game in games
if query.lower() in game["title"].lower()
]
return filtered_games
+50
View File
@@ -0,0 +1,50 @@
from utils.data.tracker import get_magnet_link
from utils.logging.logs import consoleLog
from utils.data.state import state
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from typing import Dict
import requests
class UztrackerScraper:
name = "uztracker"
headers = ["Post Title", "Author"]
is_magnet = True
def search(self, query):
base_url="https://uztracker.net/"
search_url = f"{base_url.rstrip('/')}/tracker.php?nm={query}"
consoleLog(search_url)
posts = []
try:
response = requests.get(search_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
for link in links:
theme_link = link.find('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
if not theme_link or not theme_link.b:
continue
url = urljoin(base_url, theme_link['href'])
title = theme_link.b.text
author_link = link.find('a', class_="med")
author = author_link.text.strip() if author_link else "Unknown"
posts.append(dict(
title=title,
author=author,
url=url,
))
return posts
except requests.RequestException as e:
consoleLog(f"Failed to fetch {search_url}: {e}")
return None
def get_download_link(self, post: Dict):
return get_magnet_link(post["url"])

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -1,7 +1,7 @@
from core.utils.logging.logs import consoleLog, remove_download_log
from utils.logging.logs import consoleLog, remove_download_log
from PySide6.QtWidgets import QMessageBox
from core.utils.data.state import state
from PySide6.QtCore import Qt, QPoint
from utils.data.state import state
from PySide6 import QtWidgets
import subprocess
import platform
@@ -1,9 +1,9 @@
from core.interface.dialogs.pauseresumedelegate import PauseResumeDelegate
from core.interface.dialogs.hoverrowdelegate import HoverRowDelegate
from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.dialogs.theme import _table_stylesheet
from interface.dialogs.pauseresumedelegate import PauseResumeDelegate
from interface.dialogs.hoverrowdelegate import HoverRowDelegate
from interface.dialogs.downloadmodel import DownloadModel
from interface.dialogs.theme import _table_stylesheet
from PySide6.QtWidgets import QTableView, QHeaderView
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
@@ -1,6 +1,6 @@
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import libtorrent as lt
import subprocess
import platform
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
from PySide6.QtCore import Qt
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import QEvent
def eventFilter(self, obj, event):
@@ -1,11 +1,11 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
from PySide6 import QtWidgets
class HoverRowDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
from core.interface.gui import MainWindow
from interface.gui import MainWindow
opt = QtWidgets.QStyleOptionViewItem(option)
option.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
@@ -1,6 +1,6 @@
from PySide6.QtGui import QImage, QPixmap
from core.utils.data.state import state
from PySide6.QtWidgets import QLabel
from utils.data.state import state
from PySide6.QtCore import Qt
import os
@@ -1,8 +1,8 @@
from core.interface.dialogs.theme import _theme_colors, SVG_PLAY, SVG_PAUSE, SVG_FOLDER
from interface.dialogs.theme import _theme_colors, SVG_PLAY, SVG_PAUSE, SVG_FOLDER
from PySide6.QtWidgets import QStyledItemDelegate, QWidget, QHBoxLayout
from core.interface.utils.svghelper import svg_icon
from interface.utils.svghelper import svg_icon
from PySide6.QtCore import Qt, QSize, Signal
from core.utils.data.state import state
from utils.data.state import state
from PySide6 import QtWidgets
import libtorrent as lt
@@ -10,7 +10,7 @@ class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int)
def paint(self, painter, option, index):
from core.interface.gui import MainWindow
from interface.gui import MainWindow
opt = QtWidgets.QStyleOptionViewItem(option)
option.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
@@ -1,11 +1,9 @@
from core.interface.utils.tabhelper import general_tab
from core.interface.utils.tabhelper import network_tab
from core.utils.config.settings import save_settings
from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.svghelper import svg_icon
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from interface.utils.tabhelper import create_tab
from utils.config.settings import save_settings
from interface.utils.svghelper import svg_icon
from utils.logging.logs import consoleLog
from PySide6.QtCore import Qt, QSize
from utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
@@ -19,7 +17,6 @@ from PySide6.QtWidgets import (
QCheckBox,
QFileDialog,
QComboBox,
QTabWidget,
)
import platform
@@ -46,7 +43,7 @@ def settings_dialog(self):
update_checkbox_container = QWidget()
update_checkbox_layout = QHBoxLayout()
# ignore updates checkbox
# Ignore updates checkbox
if platform.system() == "Windows":
update_checkbox = QCheckBox()
@@ -57,7 +54,7 @@ def settings_dialog(self):
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
# auto-resume downloads checkbox
# Auto-resume downloads checkbox
autoresume_container = QWidget()
autoresume_layout = QHBoxLayout()
@@ -71,7 +68,7 @@ def settings_dialog(self):
autoresume_layout.addWidget(autoresume_checkbox)
# transparent window checkbox
# Transparent window checkbox
transparent_window_container = QWidget()
transparent_window_layout = QHBoxLayout()
@@ -84,10 +81,8 @@ def settings_dialog(self):
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox)
##################
# SERVER SETTING #
##################
# API Server Setting
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
@@ -98,10 +93,8 @@ def settings_dialog(self):
api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url)
#################
# DOWNLOAD PATH #
#################
# Download Path
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
@@ -133,10 +126,8 @@ def settings_dialog(self):
download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
###############
# IMAGE PATH #
###############
# Image Path
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
@@ -167,10 +158,8 @@ def settings_dialog(self):
image_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_image_path)
##################
# SPEED LIMITING #
##################
# Speed Limiting
down_speed_limit_container = QWidget()
down_speed_limit_layout = QHBoxLayout()
@@ -197,10 +186,8 @@ def settings_dialog(self):
up_speed_limit.setFixedWidth(180)
up_speed_limit.setFixedHeight(30)
######################
# CONNECTION CONFIGS #
######################
# Connection Configs
max_connections_container = QWidget()
max_connections_layout = QHBoxLayout()
@@ -214,10 +201,8 @@ def settings_dialog(self):
max_connections.setFixedWidth(180)
max_connections.setFixedHeight(30)
####################
# DOWNLOAD CONFIGS #
####################
# Download Configs
max_downloads_container = QWidget()
max_downloads_layout = QHBoxLayout()
@@ -231,10 +216,8 @@ def settings_dialog(self):
max_downloads.setFixedWidth(180)
max_downloads.setFixedHeight(30)
#####################
# INTERFACE BINDING #
#####################
# Interface Binding
interface_container = QWidget()
interface_layout = QHBoxLayout()
@@ -257,10 +240,7 @@ def settings_dialog(self):
interface_container.setLayout(interface_layout)
###############
# SAVE/CANCEL #
###############
# Save / Cancel buttons
layout = QHBoxLayout()
save_btn = QPushButton("Save")
@@ -281,12 +261,12 @@ def settings_dialog(self):
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
self.tabs = QTabWidget()
self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs)
self.tab2 = paths_tab("Paths", download_path_container, image_path_container, self.tabs)
self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs)
tabs = QtWidgets.QTabWidget()
create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container], tabs=tabs, stretch=True)
create_tab("Paths", [download_path_container, image_path_container], tabs=tabs, stretch=True)
create_tab("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container], tabs=tabs, stretch=True)
dialog_layout.addWidget(self.tabs)
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
dialog.exec()
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _theme_colors
from interface.dialogs.theme import _theme_colors
from PySide6.QtWidgets import QStyledItemDelegate
class TrackerHoverDelegate(QStyledItemDelegate):
@@ -1,4 +1,4 @@
from core.interface.dialogs.theme import _table_stylesheet
from interface.dialogs.theme import _table_stylesheet
from PySide6.QtWidgets import QTableWidget
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
@@ -1,18 +1,35 @@
from core.utils.network.update_checker import get_updates
from core.utils.network.updater import download_update
from utils.network.update_checker import get_updates
from PySide6.QtWidgets import QDialog, QMessageBox
from core.utils.data.state import state
from utils.network.updater import download_update
from utils.logging.logs import consoleLog
from utils.data.state import state
from pathlib import Path
import platform
import json
import os
def check_version():
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):
def _get_build_info_path() -> (Path | None):
current_dir = Path(__file__).resolve().parent
for _ in range(6): # Climb max 6 directories
file = current_dir / "build_info.json"
if file.exists():
return file
# Go up one directory if file isn't found
current_dir = current_dir.parent
return None
def get_version() -> None:
# Get build info filepath
build_info_path = _get_build_info_path()
if build_info_path and os.path.exists(build_info_path):
with open(build_info_path, "r") as f:
build_info = json.load(f)
state.version = build_info.get("version")
else:
consoleLog(f"Could not find build info file (Path: {build_info_path})")
class UpdateDialog(QDialog):
def __init__(self, parent=None):
@@ -29,3 +46,4 @@ class UpdateDialog(QDialog):
response = msg.exec_()
if response == QMessageBox.StandardButton.Ok:
download_update(assets)
@@ -1,26 +1,25 @@
from core.interface.dialogs.trackerhoverdelegate import TrackerHoverDelegate
from core.interface.dialogs.elideditemdelegate import ElidedItemDelegate
from core.interface.dialogs.trackertable import _create_tracker_table
from core.interface.dialogs.downloadlist import _create_download_list
from core.interface.assets.base64_icons import settings_white_base64
from core.interface.assets.base64_icons import settings_black_base64
from core.interface.dialogs.downloadlist import download_list_update
from core.utils.logging.logs import consoleLog, flush_log_buffer
from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog
from core.interface.assets.base64_icons import logo_base64
from core.interface.dialogs.eventfilter import eventFilter
from core.utils.network.download import download_selected
from core.interface.utils.tabhelper import create_tab
from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.interface.dialogs.image import Image
from core.utils.data.state import state
from interface.dialogs.trackerhoverdelegate import TrackerHoverDelegate
from interface.dialogs.elideditemdelegate import ElidedItemDelegate
from interface.dialogs.trackertable import _create_tracker_table
from interface.dialogs.downloadlist import _create_download_list
from interface.assets.base64_icons import settings_white_base64
from interface.assets.base64_icons import settings_black_base64
from interface.dialogs.downloadlist import download_list_update
from interface.dialogs.update import get_version, UpdateDialog
from utils.logging.logs import consoleLog, flush_log_buffer
from interface.dialogs.downloadmodel import DownloadModel
from interface.dialogs.settings import settings_dialog
from interface.assets.base64_icons import logo_base64
from interface.dialogs.eventfilter import eventFilter
from utils.network.download import download_selected
from interface.utils.searchhelper import run_search
from interface.utils.tabhelper import create_tab
from utils.general.shutdown import closehelper
from utils.general.wrappers import run_thread
from interface.dialogs.image import Image
from utils.data.state import state
import core.interface.dialogs.hoverrowdelegate
import core.interface.dialogs.contextmenu
import core.interface.dialogs.update
import interface.dialogs.contextmenu
from PySide6 import QtWidgets
from PySide6.QtCore import (
@@ -77,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
pixmap.loadFromData(image_data)
self.setWindowIcon(QIcon(pixmap))
# set appid on windows
# Set AppID on Windows
if platform.system() == "Windows":
try:
import ctypes
@@ -86,23 +85,30 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
except Exception as e:
consoleLog(f"Could not set app ID: {e}")
# core.interface.dialogs.update
# Get current version
# interface.dialogs.update
get_version()
UpdateDialog()
# Initialize Window
self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600)
self.controls = QWidget()
self.controlsLayout = QVBoxLayout()
# Searchbar
self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(self._start_search)
# Download Button
self.dlbutton = QtWidgets.QPushButton("Download")
self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
# Dialog for empty results
self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.emptyResults.hide()
@@ -131,7 +137,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(state.trackertable)
# core.interface.dialogs.hoverrowdelegate
# interface.dialogs.hoverrowdelegate
self.download_model = DownloadModel()
self.downloadList = _create_download_list(self)
self.downloadList.viewport().installEventFilter(self)
@@ -157,23 +163,32 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(state.trackertable)
self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
# Tabs
create_tab("Search", [self.searchbar, self.horizontal_layout, self.dlbutton], tabs=self.tabs, stretch=False)
create_tab("Downloads", [self.emptyDownload, self.downloadList], tabs=self.tabs, stretch=False)
# Corner Widget (Settings Button, Tracker list, Tab button container)
self.corner_widget = QWidget()
self.corner_layout = QHBoxLayout(self.corner_widget)
self.corner_layout.setContentsMargins(0, 0, 0, 0)
# Tracker list
self.tracker_list = QComboBox()
self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker)
self.corner_layout.addWidget(self.tracker_list)
# Settings button
self.settings_btn = QtWidgets.QToolButton()
self.settings_btn.setIconSize(QSize(21, 21))
self.settings_btn.setStyleSheet("background: transparent;")
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)
# Adjust theme based on system preferences
if darkdetect.isDark():
settings_white_pixmap = QPixmap()
settings_white = base64.b64decode(settings_white_base64)
@@ -185,11 +200,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
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))
self.corner_layout.addWidget(self.settings_btn)
self.tab_wrapper = QWidget()
self.tab_layout = QVBoxLayout()
self.tab_layout.setContentsMargins(0, 0, 0, 0)
@@ -221,7 +231,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.version = QLabel(f"Version: {state.version}")
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s")
# (\u2193) Arrow down, (\u2191) Arrow up
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)
@@ -235,7 +246,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.active_timer.timeout.connect(self.show_empty_downloads)
self.active_timer.start(500)
self._context_menu = core.interface.dialogs.contextmenu.ContextMenu(self)
self._context_menu = interface.dialogs.contextmenu.ContextMenu(self)
self.image_overlay = Image(self)
def _apply_default_headers(self, table):
@@ -281,10 +292,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
table.setItemDelegateForColumn(i, self._tracker_hover_delegate)
def _start_search(self):
self.searchbar.setEnabled(False)
def _search_thread():
try:
return_pressed(self)
run_search(self)
except Exception as e:
consoleLog(f"Search error: {e}", True)
self.search_results_signal.emit([])
@@ -373,7 +383,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def closeEvent(self, event: QCloseEvent):
closehelper()
event.accept()
from core.utils.general.shutdown import force_exit
from utils.general.shutdown import force_exit
force_exit()
def _invalidate_hover_row(self, row):
+27
View File
@@ -0,0 +1,27 @@
from utils.logging.logs import consoleLog
from utils.data.state import state
from data.sources import SCRAPERS
for scraper in SCRAPERS:
state.trackers[scraper.name] = scraper
def run_search(self) -> None:
state.trackertable.setRowCount(0)
self.show_empty_results(False)
search_text = self.searchbar.text()
# Check for empty search
if search_text == "":
consoleLog("Error: Can't search for nothing")
return
state.posts = []
self.searchbar.setEnabled(False)
consoleLog(f"User searched for: {search_text}")
# Get current tracker and call its search function
scraper = state.trackers[state.currenttracker]
state.posts = scraper.search(search_text)
# Update GUI headers to match the current scraper
self.search_results_signal.emit(scraper.headers)
@@ -1,16 +1,13 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Qt, QByteArray
from PySide6.QtGui import QIcon, QPixmap, QPainter
from PySide6.QtCore import Qt, QByteArray
from PySide6.QtSvg import QSvgRenderer
from PySide6 import QtWidgets
import darkdetect
# Check if darkmode is enabled
def _is_dark_mode():
return darkdetect.isDark()
def svg_icon(svg_str, size=20):
app = QtWidgets.QApplication.instance()
if app:
+22
View File
@@ -0,0 +1,22 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLayout
# Tab creation helper function
def create_tab(title, items, tabs, stretch):
tab = QWidget()
layout = QVBoxLayout()
for item in items:
if item is None:
continue
if isinstance(item, QLayout):
layout.addLayout(item)
else:
layout.addWidget(item)
if stretch:
layout.addStretch()
tab.setLayout(layout)
tabs.addTab(tab, title)
return tab
+26 -21
View File
@@ -1,13 +1,16 @@
from core.network.libtorrent_misc import send_notification, update_log, check_deleted_files
from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces
from core.utils.logging.logs import get_download_logs
from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread
from core.utils.config.config import read_config
from core.utils.logging.logs import consoleLog
from core.interface.gui import MainWindow
from core.utils.data.state import state
from network.libtorrent_misc import send_notification, update_log, check_deleted_files
from utils.logging.loghandler import split_data, check_completed, check_downloads
from network.interface import list_interfaces, init_interfaces
from utils.logging.logs import get_download_logs
from utils.logging.logs import set_main_window
from network.libtorrent_int import check_space
from utils.general.shutdown import closehelper
from utils.general.wrappers import run_thread
from utils.general.shutdown import force_exit
from utils.config.config import read_config
from utils.logging.logs import consoleLog
from interface.gui import MainWindow
from utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
import qdarktheme
@@ -22,45 +25,47 @@ def run_gui():
qdarktheme.setup_theme("auto")
widget = MainWindow()
# Check OS for window transparency compatibility and apply
if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
from core.utils.logging.logs import set_main_window
set_main_window(widget)
widget.show()
sys.exit(app.exec())
def keyboardinterrupthandler(signum, frame):
closehelper()
from core.utils.general.shutdown import force_exit
force_exit()
def main():
# Begin counting startup time
start_time = time.perf_counter()
# Parse saved files
read_config()
logs = get_download_logs()
_, downloads = split_data(logs)
# Initialize Keyboardinterrupt signal
signal.signal(signal.SIGINT, keyboardinterrupthandler)
consoleLog("Starting SoftwareManager...")
consoleLog("Fetching Network Interfaces...")
# Get network interface info
list_interfaces()
consoleLog("Initializing Interface variables...")
init_interfaces()
consoleLog(f"Current Bound: {state.bound_interface}")
# Start background daemon threads
run_thread(threading.Thread(target=send_notification, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: send_notification")
run_thread(threading.Thread(target=update_log, args=(state.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")
run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: check_deleted_files")
# Start background non-daemon threads
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
run_thread(threading.Thread(target=check_downloads, args=(downloads,)))
consoleLog("Started Thread: check_downloads")
run_thread(threading.Thread(target=check_space, args=()))
# Finish counting startup time
elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
# Launch GUI
run_gui()
if __name__ == "__main__":
main()
main()
@@ -1,9 +1,10 @@
from utils.logging.logs import consoleLog, add_download_log
from utils.data.state import state
from typing import Optional
from core.utils.data.state import state
from core.utils.logging.logs import consoleLog, add_download_log
from .handle import DirectDownloadHandle
from .utils import (
from network.direct_download.handle import DirectDownloadHandle
from network.direct_download.utils import (
sanitize_filename,
extract_filename_from_url,
detect_filename_from_headers,
@@ -1,8 +1,8 @@
from core.utils.logging.logs import consoleLog, update_download_completed
from network.direct_download.status import DirectDownloadStatus, ChunkSpec
from utils.logging.logs import consoleLog, update_download_completed
from concurrent.futures import ThreadPoolExecutor, as_completed
from .status import DirectDownloadStatus, ChunkSpec
from core.utils.data.state import state
from .utils import format_size
from network.direct_download.utils import format_size
from utils.data.state import state
from typing import Optional
import libtorrent as lt
import threading
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from urllib.parse import urlparse, unquote
from utils.logging.logs import consoleLog
from typing import Optional
import requests
import os
@@ -1,5 +1,5 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import psutil
def get_net_interfaces():
@@ -29,6 +29,7 @@ def get_active_interfaces():
def list_interfaces() -> None:
consoleLog("Fetching Network Interfaces...")
addrs = psutil.net_if_addrs()
stats = psutil.net_if_stats()
@@ -45,6 +46,7 @@ def list_interfaces() -> None:
consoleLog(f"Found Interface: {interface} [{status}]")
def init_interfaces():
consoleLog("Initializing Interface variables...")
addrs = psutil.net_if_addrs()
state.interfaces = list(addrs.keys())
state.active_interfaces = get_active_interfaces()
@@ -1,7 +1,7 @@
from core.network.interface import get_interface_ip
from core.utils.general.wrappers import run_thread
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.interface import get_interface_ip
from utils.general.wrappers import run_thread
from utils.logging.logs import consoleLog
from utils.data.state import state
import libtorrent as lt
import threading
import platform
@@ -21,6 +21,25 @@ def get_free_space_mb(dirname):
st = os.statvfs(dirname)
return st.f_bavail * st.f_frsize
def check_space():
while not state.shutdown_event.is_set():
for _, magnetdl in list(state.active_downloads.items()):
try:
status = magnetdl.status()
except RuntimeError:
continue
if status.state == lt.torrent_status.downloading:
free_space = get_free_space_mb(state.download_path)
total_size = status.total_wanted
if free_space < total_size:
consoleLog(f"Not enough free space to continue downloading: {status.name}")
magnetdl.pause()
break
time.sleep(5)
def init_session():
if state.dl_session is not None:
return
@@ -60,7 +79,6 @@ def add_download(magnet_uri):
state.active_downloads = {}
init_session()
free_space = get_free_space_mb(state.download_path)
if magnet_uri in state.active_downloads:
@@ -91,7 +109,6 @@ def add_download(magnet_uri):
params.save_path = state.download_path
handle = state.dl_session.add_torrent(params)
while not handle.has_metadata():
time.sleep(1)
@@ -1,6 +1,6 @@
from core.utils.logging.logs import update_download_completed_by_hash
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import update_download_completed_by_hash
from utils.logging.logs import consoleLog
from utils.data.state import state
from plyer import notification
import libtorrent as lt
import time
@@ -17,9 +17,6 @@ def cleanup_session():
state.dl_session = None
state.active_downloads.clear()
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
@@ -1,5 +1,5 @@
from core.network.libtorrent_int import add_download
from core.utils.logging.logs import consoleLog
from network.libtorrent_int import add_download
from utils.logging.logs import consoleLog
def add_magnet(uri):
@@ -1,4 +1,4 @@
from core.utils.data.state import state
from utils.data.state import state
import configparser
import platform
import os
@@ -1,7 +1,8 @@
from core.network.libtorrent_int import update_settings
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from .config import create_config
from network.libtorrent_int import update_settings
from utils.config.config import create_config
from utils.logging.logs import consoleLog
from utils.data.state import state
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None):
@@ -9,43 +9,44 @@ class AppState(QObject):
def __init__(self):
super().__init__()
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seederm leecher
self.version: str = "dev"
self._image_path: str = ""
self.ignore_updates: bool = False
self.debug: bool = False
self.autoresume: bool = True
self.currenttracker: str = "rutracker"
self.trackertable: QTableWidget
self.trackers: Dict[str,Dict[str,Any]] = {} # each tracker should add itself here
'''
an example:
"rutracker" : {
"name" : "rutracker", # name of the tracker
"headers" : ["author", "title"], # keys shown in the table
"scrapeFunc" : function,
}
'''
self.api_url: str = "https://api.michijackson.xyz"
self.seeded_magnets: set = set()
# General
self.version: str = "Unknown"
self.debug: bool = False
self.main_window: Any = None
self.loop_running: bool = False
self.shutdown_event = threading.Event()
self.log_buffer: List[str] = []
# Paths
self.settings_path: str = ""
self._image_path: str = ""
self.download_path: str = str(Path.home() / "Downloads")
# GUI
self.window_transparency: bool = False
self.trackertable: QTableWidget
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
# Trackers / Scraping
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
self.currenttracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz"
self.trackers: Dict[str,Dict[str,Any]] = {}
# LibTorrent / Download related stuff
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.seeded_magnets: set = set()
self.ignore_updates: bool = False
self.autoresume: bool = True
self.up_speed_limit: int = 0
self.down_speed_limit: int = 0
self.max_connections: int = 200
self.max_downloads: int = 10
self.settings_path: str = ""
self.dl_session: Any = None
self.active_downloads: Dict = {}
self.window_transparency: bool = False
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
self.log_buffer: List[str] = []
self.downloads_lock = threading.RLock()
self.main_window: Any = None
self.loop_running: bool = False
self.shutdown_event = threading.Event()
@property
def image_path(self) -> str:
@@ -1,4 +1,4 @@
from core.utils.logging.logs import consoleLog
from utils.logging.logs import consoleLog
from bs4 import BeautifulSoup
import requests
@@ -1,11 +1,11 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.libtorrent_misc import cleanup_session
from utils.logging.logs import consoleLog
from utils.data.state import state
import os
def closehelper():
state.shutdown_event.set()
try:
from core.network.libtorrent_misc import cleanup_session
cleanup_session()
except Exception as e:
consoleLog(f"Exception while cleaning up LibTorrent Session: {e}")
+9
View File
@@ -0,0 +1,9 @@
from utils.logging.logs import consoleLog
def run_thread(thread):
target_name = thread._target.__name__
try:
thread.start()
consoleLog(f"Started Thread: {target_name}")
except Exception as e:
consoleLog(f"Exception while starting thread {target_name}: {e}")
@@ -1,5 +1,5 @@
from core.utils.network.download import run_download_direct, seed_magnet
from core.utils.logging.logs import consoleLog, remove_download_log
from utils.network.download import run_download_direct, seed_magnet
from utils.logging.logs import consoleLog, remove_download_log
import os
def split_data(data):
@@ -18,7 +18,7 @@ def check_completed(downloads, resume):
run_download_direct(download.magnet_uri, download.title)
consoleLog(f"Resuming Magnet: {download.title}")
elif download.url:
from core.network.direct_download import add_direct_download
from network.direct_download import add_direct_download
add_direct_download(download.url, download.title)
consoleLog(f"Resuming Direct Download: {download.title}")
@@ -1,5 +1,5 @@
from core.utils.data.models import Download, DownloadList
from core.utils.data.state import state
from utils.data.models import Download, DownloadList
from utils.data.state import state
from dataclasses import asdict
from datetime import datetime
import json
@@ -243,7 +243,7 @@ def set_main_window(window):
def flush_log_buffer(): # credits to claude
if state.log_buffer:
try:
from core.interface.gui import MainWindow
from interface.gui import MainWindow
for log_entry in state.log_buffer:
MainWindow.add_log(log_entry)
state.log_buffer = []
@@ -256,7 +256,7 @@ def consoleLog(text, printAnyways = False):
formatted_text = f"[{current_time}] {text}"
try:
from core.interface.gui import MainWindow
from interface.gui import MainWindow
if not MainWindow.add_log(formatted_text):
state.log_buffer.append(formatted_text)
except Exception:
@@ -1,11 +1,11 @@
from core.network.direct_download import add_direct_download
from core.network.libtorrent_wrapper import add_magnet
from core.utils.logging.logs import add_download_log
from core.utils.general.wrappers import run_thread
from core.network.libtorrent_int import add_seed
from network.direct_download import add_direct_download
from network.libtorrent_wrapper import add_magnet
from PySide6.QtWidgets import QTableWidgetItem
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import add_download_log
from utils.general.wrappers import run_thread
from network.libtorrent_int import add_seed
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
from typing import Optional
import threading
@@ -31,9 +31,9 @@ def download_selected(items: list[QTableWidgetItem]):
run_thread(threading.Thread(target=run_download, args=(post,)))
def run_download(post, headers: Optional[dict] = None):
linkfunc = state.trackers[state.currenttracker]["linkFunc"]
ismagnet = state.trackers[state.currenttracker]["isMagnet"]
result = linkfunc(post)
link_method = state.trackers[state.currenttracker].get_download_link
ismagnet = state.trackers[state.currenttracker].is_magnet
result = link_method(post)
if ismagnet:
link = result
@@ -1,9 +1,8 @@
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from utils.logging.logs import consoleLog
from utils.data.state import state
import requests
def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
try:
@@ -1,6 +1,6 @@
from core.network.direct_download.handle import DirectDownloadHandle
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state
from network.direct_download.handle import DirectDownloadHandle
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
from PySide6 import QtWidgets
import libtorrent as lt