Compare commits

..

5 Commits

Author SHA1 Message Date
shayaa 25d72dfbf9 Update README.md 2025-09-11 20:45:56 +02:00
Vxrtrauter b3f72406e0 Refactor download management: remove old code and implement new structure for aria2p integration 2025-07-24 02:17:50 +02:00
Vxrtrauter 4052e14f41 cleaned up code a bit, restructured download functions 2025-07-02 07:08:07 +02:00
Vxrtrauter 628e9ee2c4 Adds basic CLI input and output functionality
Introduces a simple command-line interface to capture user input and display it back. Lays groundwork for future command processing and interaction.
2025-06-30 16:22:25 +02:00
Vxrtrauter 42eea1f371 Adds basic CLI input and output functionality
Introduces a simple command-line interface to capture user input and display it back. Lays groundwork for future command processing and interaction.
2025-06-30 16:21:49 +02:00
34 changed files with 237 additions and 1430 deletions
-151
View File
@@ -1,151 +0,0 @@
name: Build Executables (Dev)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: "Dev release version (e.g., v1.0.0-dev)"
required: true
default: "v1.0.0"
permissions:
contents: write
env:
PYTHON_VERSION: "3.11"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
asset_name: SoftwareManager-dev-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
asset_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
- os: macos-latest
artifact_name: SoftwareManager-dev-${{ github.sha }}-macos
asset_name: SoftwareManager-dev-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
short_sha: ${{ steps.version.outputs.short_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libxcb-xinerama0 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-keysyms1 \
libxcb-render-util0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-shape0 \
libxcb-util1
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
- name: Generate version
id: version
shell: bash
run: |
COMMIT_SHA=${{ github.sha }}
SHORT_SHA=${COMMIT_SHA:0:7}
VERSION="${SHORT_SHA}-dev"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject commit hash into code (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$json = '{
"version": "' + $env:VERSION + '"
}'
$json | Out-File -Encoding utf8 build_info.json
- name: Build executable with PyInstaller (Windows)
if: runner.os == 'Windows'
run: pyinstaller --onefile --windowed --add-data "build_info.json:." --name ${{ env.APP_NAME }} main.py
- name: Build executable with PyInstaller (Unix)
if: runner.os != 'Windows'
run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py
- name: Make executable (Unix)
if: runner.os != 'Windows'
run: chmod +x dist/${{ env.APP_NAME }}
- name: Rename artifact
shell: bash
run: |
cd dist
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }}
ls -la
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist/${{ matrix.artifact_name }}
retention-days: 1
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
if: success()
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/ubuntu-latest-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
ls -la release/
- name: Create Release (Public)
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build.outputs.version }}
name: ${{ needs.build.outputs.version }}
files: |
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -216
View File
@@ -1,216 +1,2 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# IDE's and editors
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
*.code-workspace
*.swp
__pycache__
.venv
+5 -14
View File
@@ -1,11 +1,14 @@
# SoftwareManager - A Download Manager/Launcher
## Description
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
## Features
- VPN Binding (impl. soon)
- Bandwidth/Download speed limiting
- Bandwidth/Download speed limiting (impl. soon)
- UI Theming
- Start downloads via a predefined magnet URI.
- View detailed download information (name, speed, and status).
- View detailed download information (name, speed, and status). (improved impl. soon)
## Installation
1. Download the binary file for your operating system from releases
@@ -27,18 +30,6 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
- 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.
## Star History
<a href="https://www.star-history.com/#KeksPirates/SoftwareManager&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
<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
-41
View File
@@ -1,41 +0,0 @@
from bs4 import BeautifulSoup
import requests
def scrape_monkrus_telegram(query):
post = 100
max_posts = 250
posts: list[dict] = []
added = set()
while post <= max_posts:
url = f"https://t.me/s/real_monkrus/{post}"
response = requests.get(url)
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 not post_txt or not post_txt.b:
continue
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:
post_url = link["href"]
if post_url not in added:
added.add(post_url)
posts.append(dict(
author="m0nkrus",
id=len(posts) + 1,
title=title,
url=post_url
))
post += 22
posts.reverse()
return(posts)
-22
View File
@@ -1,22 +0,0 @@
import requests
from core.utils.data.state import state
def scrape_rutracker(search_text):
search = requests.get(f"{state.api_url}/search/{search_text}")
if state.debug:
print("Sent request to server")
if search:
try:
return search.text
except Exception:
if state.debug:
print("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.
-88
View File
@@ -1,88 +0,0 @@
import requests
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from core.utils.data.state import state
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
if state.debug:
print(f"Uztracker seems down, status code: {response.status_code}")
except requests.exceptions.RequestException as e:
if state.debug:
print(f"\nRequest Exception on {url_uztracker}:")
print(e)
print("\nIs the Site down?")
up = False
asyncio.run(init_uztracker())
def scrape_uztracker(search):
if up:
search_url = url_uztracker + search
if state.debug:
print(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 and state.debug:
print(f"Failed to fetch {search_url}: {e}")
return None
else:
if state.debug:
print("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:
if state.debug:
print("Program not found")
else:
if state.debug:
print("Error: Uztracker down")
return None
+21
View File
@@ -0,0 +1,21 @@
import aria2p
import os
import subprocess
def run_aria2p():
aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
return aria2
+19
View File
@@ -0,0 +1,19 @@
from .aria2p_server import run_aria2p
import aria2p
import subprocess
import os
# from .scraper import uri
def start_client():
global aria2
aria2 = run_aria2p()
def add_magnet(uri):
aria2.add_magnet(uri)
def terminate():
return
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

-133
View File
@@ -1,133 +0,0 @@
from core.utils.config.settings import save_settings
from core.utils.data.state import state
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QLineEdit,
QPushButton,
QWidget,
QVBoxLayout,
QDialog,
QLabel,
QHBoxLayout,
QSpinBox,
QCheckBox,
)
def settings_dialog(self):
if state.debug:
print("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(800, 400)
dialog.setLayout(QVBoxLayout())
dialog.layout().addWidget(QLabel("Settings"))
def close_settings():
dialog.reject()
checkbox_container = QWidget()
checkbox_layout = QHBoxLayout()
# ignore updates checkbox
checkbox = QCheckBox()
checkbox_container.setLayout(checkbox_layout)
checkbox_layout.addWidget(QLabel("Ignore Updates: "))
checkbox_layout.addStretch()
checkbox.setChecked(state.ignore_updates)
checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
checkbox_layout.addWidget(checkbox)
dialog.layout().addWidget(checkbox_container)
##################
# THREAD SETTING #
##################
thread_box = QSpinBox()
thread_box.setMinimum(1)
thread_box.setMaximum(16)
thread_box.setValue(state.aria2_threads)
# container for tight space
thread_container = QWidget()
thread_layout = QHBoxLayout()
thread_layout.addWidget(QLabel("Threads:"))
thread_layout.addWidget(thread_box)
thread_container.setLayout(thread_layout)
thread_container.setMaximumHeight(80)
# Dimensions
thread_box.setFixedWidth(90)
thread_box.setFixedHeight(30)
dialog.layout().addWidget(thread_container)
##################
# SERVER SETTING #
##################
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
api_url = QLineEdit()
api_url_layout.addWidget(QLabel("API Server URL:"))
api_url_layout.addWidget(api_url)
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url)
dialog.layout().addWidget(api_url_container)
#################
# DOWNLOAD PATH #
#################
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
download_path = QLineEdit()
download_path_layout.addWidget(QLabel("Download Path:"))
download_path_layout.addWidget(download_path)
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path)
dialog.layout().addWidget(download_path_container)
##################
# SPEED LIMITING #
##################
speed_limit_container = QWidget()
speed_limit_layout = QHBoxLayout()
speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
speed_limit = QSpinBox()
speed_limit.setMinimum(0)
speed_limit.setMaximum(10000000)
speed_limit.setValue(state.speed_limit)
speed_limit_container.setLayout(speed_limit_layout)
speed_limit_layout.addWidget(speed_limit)
speed_limit.setFixedWidth(180)
speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container)
###############
# SAVE/CANCEL #
###############
layout = QHBoxLayout()
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value()))
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
dialog.layout().addLayout(layout)
dialog.exec()
-251
View File
@@ -1,251 +0,0 @@
from PySide6 import QtWidgets
from PySide6 import QtCore
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel
from PySide6.QtWidgets import (
QLineEdit,
QTableView,
QWidget,
QVBoxLayout,
QListWidget,
QToolBar,
QLabel,
QHBoxLayout,
QComboBox,
QTabWidget,
QProgressBar,
QHeaderView,
QMessageBox,
QTableWidget,
)
from PySide6.QtGui import QIcon, QAction, QCloseEvent
import darkdetect
import threading
import platform
import requests as r
import os
import subprocess
import time
import sys
import json
from core.utils.general.wrappers import run_thread
from core.utils.data.state import state
from core.utils.network.download import download_selected
from core.utils.network.update_checker import check_for_updates
from core.utils.general.shutdown import closehelper
from core.interface.utils.tabhelper import create_tab
from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog
from core.network.aria2_integration import dlprogress
print(os.path.abspath(__file__))
def download_update(latest_version):
old_filename = f"SoftwareManager-dev-{state.version.replace('-dev', '')}-windows.exe"
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
print("Downloading update...")
if os.path.exists(old_filename):
os.remove(old_filename)
response = r.get(url, allow_redirects=True)
with open(new_filename, "wb") as f:
f.write(response.content)
if not os.path.exists(new_filename):
raise FileNotFoundError(f"Executable not found")
subprocess.Popen([new_filename], shell=True)
time.sleep(0.5)
sys.exit(0)
class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self):
super().__init__()
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):
with open(build_info_path, "r") as f:
build_info = json.load(f)
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()
assets, latest_version = result
if assets:
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("Update Available")
msg.setText(f"A new version is available.")
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
response = msg.exec_()
if response == QMessageBox.Ok:
download_update(latest_version)
self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600)
self.controls = QWidget()
self.controlsLayout = QVBoxLayout()
# Widgets
self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
self.dlbutton = QtWidgets.QPushButton("Download")
self.libraryList = QListWidget()
self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignCenter)
self.emptyResults.hide()
self.download_model = None
self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.")
self.progressbar = QProgressBar()
self.qtablewidget = QTableWidget()
self.qtablewidget.setColumnCount(2)
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.qtablewidget.verticalHeader().setVisible(False)
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 500)
container = QWidget()
containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(self.qtablewidget)
class DownloadModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self.headers = ["Name", "Status", "Progress", "Speed", "Size", "Total Size"]
def rowCount(self, parent=QModelIndex()):
return len(state.downloads)
def columnCount(self, parent=QModelIndex()):
return len(self.headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
return None
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
download = state.downloads[index.row()]
col = index.column()
if col == 0:
return download.name
elif col == 1:
return getattr(download, 'status', 'Downloading')
elif col == 2:
return f"{int(download.progress)}%"
elif col == 3:
return f"{download.download_speed_string()}"
elif col == 4:
pass
elif col == 5:
return f"{download.total_length_string()}"
return None
self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model)
self.downloadList.horizontalHeader().setStretchLastSection(False)
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.downloadList.setSelectionBehavior(QTableView.SelectRows)
self.downloadList.setAttribute(Qt.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground)
# download button triggers
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
container.setLayout(containerLayout)
self.setCentralWidget(container)
# Tabs
self.tabs = QTabWidget()
self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(self.qtablewidget)
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
containerLayout.addWidget(self.tabs)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft)
self.tracker_list = QComboBox()
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
self.tracker_list.activated.connect(self.set_tracker)
if darkdetect.isDark():
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self)
else:
settings_action = QAction(QIcon("core/interface/assets/settings_black.png"), "Settings", self)
settings_action.triggered.connect(lambda: settings_dialog(self))
toolbar.addAction(settings_action)
toolbar.addWidget(self.tracker_list)
toolbar.setMovable(False)
self.progressbar.setValue(0)
containerLayout.addWidget(self.progressbar)
self.progress_timer = QTimer()
self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress)))
self.progress_timer.start(1000)
self.download_timer = QTimer()
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
self.download_timer.start(500)
def download_list_update(self):
if self.download_model:
self.download_model.layoutAboutToBeChanged.emit()
self.download_model.layoutChanged.emit()
def closeEvent(self, event: QCloseEvent):
closehelper()
event.accept()
def set_tracker(self, _):
state.tracker = self.tracker_list.currentText()
def update_progress(self):
progress = dlprogress()
self.progressbar.setValue(progress)
def show_empty_results(self, show: bool):
if show:
self.qtablewidget.hide()
self.emptyResults.show()
else:
self.emptyResults.hide()
self.qtablewidget.show()
-85
View File
@@ -1,85 +0,0 @@
from PySide6.QtWidgets import QTableWidgetItem
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 == "":
if state.debug:
print("Error: Can't search for nothing")
return
if state.debug:
print("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:
if state.debug:
print(f"No Results for {search_text}")
self.qtablewidget.clear()
self.show_empty_results(True)
else:
if state.debug:
print(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 == []:
if state.debug:
print(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))
if state.debug == True:
print(f"Response Cached: {cached}")
else:
if state.debug:
print(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 == []:
if state.debug:
print(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))
-17
View File
@@ -1,17 +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
View File
-75
View File
@@ -1,75 +0,0 @@
import aria2p
import subprocess
from core.utils.data.state import state
from plyer import notification
import time
import sys
def run_aria2p():
state.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
return state.aria2
def aria2server():
download_path = state.download_path
speed_limit = state.speed_limit
cmd = [
"aria2c",
"--enable-rpc",
"--disable-ipv6", # added this since it caused problems with vpns
"--rpc-listen-all",
"--rpc-listen-port=6800",
f"--dir={download_path}",
f"--max-download-limit={speed_limit * 1024}",
"-x", str(state.aria2_threads),
"-s", str(state.aria2_threads),
]
aria2server = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
)
return aria2server
def dlprogress():
try:
state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata]
downloads = state.downloads
if downloads:
for download in downloads:
progress = download.progress_string(0)
progress_int = int(progress.strip('%'))
return progress_int
return 0
except:
return 0
def send_notification(shutdown_event):
notified = set()
while not shutdown_event.is_set():
try:
for d in state.aria2.get_downloads():
if d.is_metadata:
continue
if d.progress == 100 and d.gid not in notified:
notification.notify(
title="Download finished",
message=f"{d.name} has finished downloading.",
timeout=4
)
notified.add(d.gid)
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
except Exception:
pass
time.sleep(5)
-16
View File
@@ -1,16 +0,0 @@
from .aria2_integration import run_aria2p
from core.utils.data.state import state
def start_client():
global aria2
aria2 = run_aria2p()
if state.debug:
print("Started Aria2p")
def add_magnet(uri):
aria2.add_magnet(uri)
if state.debug:
print("Magnet URI added to Aria2")
+23
View File
@@ -0,0 +1,23 @@
from bs4 import BeautifulSoup
import requests
badphrases = {}
badphrases.update({chr(i) for i in range(ord('A'), ord('Z') + 1)})
rutracker_url = "https://rutracker.org/forum/index.php"
def fetch_rutracker_software(url):
response = requests.get(url)
if response.status_code == 200:
soup = BeautifulSoup(response.text, "html.parser")
links = soup.find_all("a")
games_dict = {}
for link in links:
href = link.get("href", "").lower()
text = link.text.strip()
if text:
games_dict[text] = href
return games_dict
else:
print(f"Failed to retrieve the Rutracker page. Status code: {response.status_code}")
return {}
View File
-45
View File
@@ -1,45 +0,0 @@
import os
import platform
import configparser
from core.utils.data.state import state
def create_config():
config = configparser.ConfigParser()
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}",
"ignore_updates": f"{state.ignore_updates}"}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(settings_path, exist_ok=True)
with open(os.path.join(settings_path, "config.yml"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(settings_path, "config.yml")
if not os.path.exists(config_file):
create_config()
config.read(config_file)
state.debug = config.getboolean("General", "debug")
state.api_url = config.get("General", "api_url")
state.aria2_threads = config.getint("General", "aria2_threads")
state.download_path = config.get("General", "download_path")
state.speed_limit = config.getint("General", "speed_limit")
state.ignore_updates = config.getboolean("General", "ignore_updates")
-28
View File
@@ -1,28 +0,0 @@
from core.utils.data.state import state
from core.utils.general.shutdown import kill_aria2server
from .config import create_config
def restart_aria2c():
import main # had to do this because of circle import :(
import signal
import atexit
kill_aria2server()
state.aria2process.wait()
state.aria2process = main.run_aria2server()
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None):
if thread_count is not None:
state.aria2_threads = thread_count
if apiurl is not None:
state.api_url = apiurl
if download_path is not None:
state.download_path = download_path
if speed_limit is not None:
state.speed_limit = speed_limit
create_config()
restart_aria2c()
close()
-24
View File
@@ -1,24 +0,0 @@
from dataclasses import dataclass
from typing import Optional, Any, List, Dict
from pathlib import Path
@dataclass
class AppState:
posts: List[Dict[str, Any]]
post_titles: List[str]
post_urls: List[str]
post_author: List[str]
downloads: List[str]
version: str
ignore_updates: bool = False
debug: bool = False
tracker: str = "rutracker"
api_url: str = "https://api.michijackson.xyz"
download_path: str = str(Path.home() / "Downloads")
speed_limit: int = 0
aria2process: Optional[Any] = None
aria2: Any = None
aria2p: Any = None
aria2_threads: int = 4
state = AppState(posts=[], post_titles=[], post_urls=[], post_author=[], downloads=[], download_path="", version="")
-52
View File
@@ -1,52 +0,0 @@
import requests
from bs4 import BeautifulSoup
from core.utils.data.state import state
from core.utils.network.jsonhandler import format_data
def get_item_url(item, posts, post_titles): # softwarelist currentitem, post list (dict), post titles list
post_index = post_titles.index(item)
if 0 <= post_index < len(post_titles):
if state.tracker == "uztracker":
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
if state.debug:
print("Found post URL: ", item)
return item
if state.tracker == "rutracker":
item_dict = posts[post_index]
_, post_links, _ = format_data([item_dict])
if state.debug:
print("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])
if state.debug:
print("Found post URL: ", post_links[0])
return post_links[0]
return None
def get_magnet_link(post_url):
try:
response = requests.get(post_url) # eventually impl. cloudscraper
if state.debug:
print("Sent Request to retrieve Magnet Link...")
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
if magnet_link:
if state.debug:
print("Magnet Link Retrieved: ", magnet_link['href'])
return magnet_link['href']
else:
if state.debug:
print("Magnet Link not Found!")
except requests.RequestException as e:
if state.debug:
print(f"Failed to fetch {post_url}: {e}")
return None
-31
View File
@@ -1,31 +0,0 @@
from core.utils.data.state import state
import threading
import psutil
import sys
shutdown_event = threading.Event()
def kill_aria2server():
if sys.platform.startswith("win"):
process = "aria2c.exe"
else:
process = "aria2c"
if state.aria2process:
state.aria2process.kill()
if state.debug:
print("Killed Aria2")
try:
for proc in psutil.process_iter():
if proc.name() == process:
proc.kill()
if state.debug:
print(f"Killed Aria2c (PID {proc.pid})")
except psutil.NoSuchProcess:
pass
def closehelper():
shutdown_event.set()
kill_aria2server()
-2
View File
@@ -1,2 +0,0 @@
def run_thread(thread):
thread.start()
-29
View File
@@ -1,29 +0,0 @@
from core.utils.data.state import state
from core.utils.data.tracker import get_item_url
from core.utils.data.tracker import get_magnet_link
from core.network.aria2_wrapper import start_client
from core.network.aria2_wrapper import add_magnet
from core.utils.general.wrappers import run_thread
import threading
def download_selected(item, posts, post_titles):
if item is not None:
if state.debug:
print(f"Downloading {item.text()}")
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
else:
if state.debug:
print("No item selected for download.")
def run_download(item, posts, post_titles):
post_url = get_item_url(item, posts, post_titles)
if state.debug:
print("Selected URL: ", post_url)
magnet_uri = get_magnet_link(post_url)
start_client()
add_magnet(magnet_uri)
-23
View File
@@ -1,23 +0,0 @@
import json
def split_data(data):
p_json = json.loads(data)
count = p_json["count"]
posts = p_json["data"]
query = p_json["query"]
success = p_json["success"]
cached = p_json["cached"]
return count, posts, query, success, cached
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]
return post_titles, post_links, post_author
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
-36
View File
@@ -1,36 +0,0 @@
import requests
from core.utils.data.state import state
def check_for_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
response = requests.get(url)
if response.status_code != 200:
print(f"Failed to fetch releases: {response.status_code}")
exit(1)
releases = response.json()
releases.sort(key=lambda r: r["published_at"], reverse=True)
latest_release = releases[0]
latest_version = latest_release["name"]
assets = latest_release["assets"]
if state.version == "dev":
if state.debug:
print("Dev release detected, skipping version check")
return None, None
elif latest_version != state.version:
if state.debug:
print(f"New release available: {latest_version}")
if assets:
if state.debug:
print("Assets:")
for asset in assets:
print(f" - {asset['name']}: {asset['browser_download_url']}")
return assets, latest_version
else:
if state.debug:
print("Already up-to-date.")
return None, None
+96
View File
@@ -0,0 +1,96 @@
import requests
from bs4 import BeautifulSoup
global url_uztracker
url_uztracker = "https://uztracker.net/tracker.php?nm=" # placeholder url for now
response = requests.get(url_uztracker) # get da content from da url
soup = BeautifulSoup(response.content, 'html.parser') # create bs object
def scrape_uztracker(search):
search_url = url_uztracker + search
print(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
# print(f"Result found: ({resultCount}) {link.b.text} | {link['href']}")
results.append(link['href'])
resulttitles.append(link.b.text)
result = True
if result:
return resulttitles, results
if not result:
print(f'No Results found for "{search}"')
return
except requests.RequestException as e:
print(f"Failed to fetch {search_url}: {e}")
return None
def get_post_title(post_url):
response = requests.get(post_url)
soup = BeautifulSoup(response.text, 'html.parser')
maintitle = soup.find(class_='tt-text')
if maintitle:
return maintitle.text
else:
print("Program not found")
def select_program():
selection = input("Enter the Number of the Program you want to download: ")
try:
index = int(selection) - 1
if 0 <= index < len(results): # note for myself: py starts counting at 0; check if number is not negative, check if number is not more than list length.
selected = "https://uztracker.net/" + results[index].lstrip("./")
post_url = selected
return selected
else:
print("Invalid Selection.")
except ValueError:
print(Exception)
return
def get_magnet_link(post_url):
try:
response = requests.get(post_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
if magnet_link:
return magnet_link['href']
else:
print("Magnet Link not Found!")
except requests.RequestException as e:
print(f"Failed to fetch {post_url}: {e}")
# What x and x.startswith('magnet:') Does
# x - check if x exists (not None/empty)
# and - if x exists, THEN check the next part
# x.startswith('magnet:') - does x start with "magnet:"?
+56 -41
View File
@@ -1,49 +1,64 @@
from core.interface.gui import MainWindow
from core.utils.data.state import state
from core.network.aria2_integration import aria2server
from core.network.aria2_integration import send_notification
from core.utils.general.shutdown import closehelper, shutdown_event
from core.utils.general.wrappers import run_thread
from core.utils.config.config import read_config
from PySide6 import QtWidgets
import qdarktheme
import darkdetect
import threading
import signal
import argparse
import sys
from ui.clinterface import asciititle;
from core.uztracker_scraper import scrape_uztracker
from core.uztracker_scraper import get_magnet_link
from core.downloading.download import start_client
from core.downloading.download import add_magnet
import os
def search():
asciititle()
searchquery = input("Enter Search Query: ")
result = scrape_uztracker(searchquery)
if result is None:
clear()
asciititle()
print("No results found for your Query.\n")
return
return result
def select_result(result):
resulttitles, resultlinks = result
clear()
asciititle()
for i, title in enumerate(resulttitles, 1):
print(f"({i}) {title}")
selection = input("\n Enter the Number of the Program you want to download: ")
try:
index = int(selection) - 1
if 0 <= index < len(resultlinks): # note for myself: py starts counting at 0; check if number is not negative, check if number is not more than list length.
selected = "https://uztracker.net/" + resultlinks[index].lstrip("./")
post_url = selected
return selected
else:
print("Invalid Selection.")
except ValueError:
print(Exception)
return
def clear():
os.system('cls' if os.name == 'nt' else 'clear')
parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
def run_gui():
app = QtWidgets.QApplication([])
if darkdetect.isDark:
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
else:
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
widget = MainWindow()
widget.show()
sys.exit(app.exec())
def run_aria2server():
aria2process = aria2server()
return aria2process
def keyboardinterrupthandler(signum, frame):
closehelper()
if __name__ == "__main__":
read_config()
state.debug = args.debug # override of read_config
if state.debug:
print("Starting Aria2 Server")
state.aria2process = run_aria2server()
signal.signal(signal.SIGINT, keyboardinterrupthandler)
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
if state.debug:
print("Launching GUI")
run_gui()
clear()
results = search()
selected = select_result(results)
print(selected)
uri = get_magnet_link(selected)
start_client()
add_magnet(uri)
-10
View File
@@ -1,10 +0,0 @@
requests==2.32.2
PySide6==6.9.2
beautifulsoup4==4.13.5
darkdetect==0.8.0
aria2p==0.12.1
pyinstaller==6.15.0
pyqtdarktheme==0.1.7
aiohttp==3.13.0
plyer==2.1.0
psutil==7.1.0
+15
View File
@@ -0,0 +1,15 @@
def asciititle():
print(f"""
_____ __ _ __ __
/ ____| / _| | | \/ |
| (___ ___ | |_| |___ ____ _ _ __ ___| \ / | __ _ _ __ __ _ __ _ ___ _ __
\___ \ / _ \| _| __\ \ /\ / / _` | '__/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
____) | (_) | | | |_ \ V V / (_| | | | __/ | | | (_| | | | | (_| | (_| | __/ |
|_____/ \___/|_| \__| \_/\_/ \__,_|_| \___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
__/ |
|___/
""")