mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-04 01:49:42 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25d72dfbf9 | |||
| b3f72406e0 | |||
| 4052e14f41 | |||
| 628e9ee2c4 | |||
| 42eea1f371 |
@@ -1,143 +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 }}
|
|
||||||
|
|
||||||
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: Build executable with PyInstaller
|
|
||||||
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:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Display downloaded artifacts
|
|
||||||
run: |
|
|
||||||
echo "Downloaded artifacts:"
|
|
||||||
find artifacts -type f
|
|
||||||
ls -la artifacts/
|
|
||||||
|
|
||||||
- name: Generate version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
COMMIT_SHA=${{ github.sha }}
|
|
||||||
SHORT_SHA=${COMMIT_SHA:0:7}
|
|
||||||
VERSION="${SHORT_SHA}-dev"
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
||||||
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Generated version: ${VERSION}"
|
|
||||||
|
|
||||||
- name: Prepare release files
|
|
||||||
run: |
|
|
||||||
mkdir -p release
|
|
||||||
SHORT_SHA="${{ steps.version.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 Prerelease (Public)
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.version.outputs.version }}
|
|
||||||
name: ${{ steps.version.outputs.version }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
files: |
|
|
||||||
release/SoftwareManager-dev-${{ steps.version.outputs.short_sha }}-linux
|
|
||||||
release/SoftwareManager-dev-${{ steps.version.outputs.short_sha }}-windows.exe
|
|
||||||
release/SoftwareManager-dev-${{ steps.version.outputs.short_sha }}-macos
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
+1
-215
@@ -1,216 +1,2 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
__pycache__
|
||||||
__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
|
.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
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
# SoftwareManager - A Download Manager/Launcher
|
# SoftwareManager - A Download Manager/Launcher
|
||||||
|
|
||||||
|
## Description
|
||||||
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- VPN Binding (impl. soon)
|
- VPN Binding (impl. soon)
|
||||||
- Bandwidth/Download speed limiting
|
- Bandwidth/Download speed limiting (impl. soon)
|
||||||
|
- UI Theming
|
||||||
- Start downloads via a predefined magnet URI.
|
- 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
|
## Installation
|
||||||
1. Download the binary file for your operating system from releases
|
1. Download the binary file for your operating system from releases
|
||||||
@@ -30,4 +33,3 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
|
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +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 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.
|
|
||||||
@@ -1,93 +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
|
|
||||||
print(f"Uztracker seems down, status code: {response.status_code}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"\nRequest Exception on {url_uztracker}:")
|
|
||||||
print(e)
|
|
||||||
print("\nIs the Site down?")
|
|
||||||
up = False
|
|
||||||
|
|
||||||
asyncio.run(init_uztracker())
|
|
||||||
|
|
||||||
async def scrape_uztracker(search, max_results=450):
|
|
||||||
if up:
|
|
||||||
result = False
|
|
||||||
global results
|
|
||||||
global resulttitles
|
|
||||||
results = []
|
|
||||||
resulttitles = []
|
|
||||||
|
|
||||||
per_page = 50
|
|
||||||
|
|
||||||
for start in range(0, max_results, per_page):
|
|
||||||
search_url = url_uztracker + search + f"&start={start}"
|
|
||||||
if state.debug:
|
|
||||||
print(search_url)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
try:
|
|
||||||
async with session.get(search_url) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
text = await response.text()
|
|
||||||
soup = BeautifulSoup(text, 'html.parser')
|
|
||||||
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
|
|
||||||
resultCount = 0
|
|
||||||
for link in links:
|
|
||||||
if link.b:
|
|
||||||
resultCount += 1
|
|
||||||
results.append(link['href'])
|
|
||||||
resulttitles.append(link.b.text)
|
|
||||||
result = True
|
|
||||||
|
|
||||||
if resultCount == 0:
|
|
||||||
# add popup in gui for no result
|
|
||||||
break
|
|
||||||
|
|
||||||
except aiohttp.ClientError as e:
|
|
||||||
if result:
|
|
||||||
print(f"Failed to fetch {search_url}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Error: Uztracker down")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
if not results:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
return resulttitles, results
|
|
||||||
|
|
||||||
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:
|
|
||||||
print("Error: Uztracker down")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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 |
@@ -1,123 +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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
##################
|
|
||||||
# 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)
|
|
||||||
print(thread_box.value())
|
|
||||||
layout.addWidget(cancel_btn)
|
|
||||||
layout.addWidget(save_btn)
|
|
||||||
|
|
||||||
dialog.layout().addLayout(layout)
|
|
||||||
|
|
||||||
dialog.exec()
|
|
||||||
@@ -1,190 +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,
|
|
||||||
)
|
|
||||||
from PySide6.QtGui import QIcon, QAction, QCloseEvent
|
|
||||||
import darkdetect
|
|
||||||
import threading
|
|
||||||
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.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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
searchresults = []
|
|
||||||
|
|
||||||
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.softwareList = QListWidget()
|
|
||||||
self.post_author_list = QListWidget()
|
|
||||||
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()
|
|
||||||
|
|
||||||
container = QWidget()
|
|
||||||
containerLayout = QVBoxLayout()
|
|
||||||
containerLayout.addWidget(self.searchbar)
|
|
||||||
|
|
||||||
containerLayout.addWidget(self.softwareList)
|
|
||||||
containerLayout.addWidget(self.post_author_list)
|
|
||||||
self.softwareList.addItems(searchresults)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# download button triggers
|
|
||||||
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.softwareList.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.softwareList, stretch=3)
|
|
||||||
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
|
|
||||||
self.horizontal_layout.addWidget(self.post_author_list)
|
|
||||||
|
|
||||||
self.tab1 = create_tab("Search", self.searchbar, self.softwareList, 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"])
|
|
||||||
self.tracker_list.activated.connect(self.set_tracker)
|
|
||||||
|
|
||||||
|
|
||||||
if darkdetect.isDark():
|
|
||||||
settings_action = QAction(QIcon("core/interface/assets/settings_dark.png"), "Settings", self)
|
|
||||||
else:
|
|
||||||
settings_action = QAction(QIcon("core/interface/assets/settings.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.softwareList.hide()
|
|
||||||
self.emptyResults.show()
|
|
||||||
else:
|
|
||||||
self.emptyResults.hide()
|
|
||||||
self.softwareList.show()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from core.data.scrapers.uztracker import scrape_uztracker
|
|
||||||
from core.data.scrapers.rutracker import scrape_rutracker
|
|
||||||
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 = asyncio.run(scrape_uztracker(search_text))
|
|
||||||
if state.tracker == "rutracker":
|
|
||||||
response = scrape_rutracker(search_text)
|
|
||||||
if response:
|
|
||||||
if state.tracker == "uztracker":
|
|
||||||
state.post_titles, state.post_urls, = response
|
|
||||||
self.softwareList.clear()
|
|
||||||
if state.post_titles:
|
|
||||||
self.softwareList.addItems(state.post_titles)
|
|
||||||
self.post_author_list.addItems(state.post_author)
|
|
||||||
if state.tracker == "rutracker":
|
|
||||||
_, state.posts, _, _, cached = split_data(response)
|
|
||||||
if state.posts == []:
|
|
||||||
if state.debug:
|
|
||||||
print(f"No Results found for \"{search_text}\"")
|
|
||||||
self.softwareList.clear()
|
|
||||||
self.show_empty_results(True)
|
|
||||||
else:
|
|
||||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
|
||||||
self.show_empty_results(False)
|
|
||||||
self.post_author_list.clear()
|
|
||||||
self.post_author_list.addItems(state.post_author)
|
|
||||||
self.softwareList.clear()
|
|
||||||
self.softwareList.addItems(state.post_titles)
|
|
||||||
if state.debug == True:
|
|
||||||
print(f"Cached: {cached}")
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import aria2p
|
|
||||||
import subprocess
|
|
||||||
from core.utils.data.state import state
|
|
||||||
from plyer import notification
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def run_aria2p():
|
|
||||||
|
|
||||||
state.aria2 = aria2p.API(
|
|
||||||
aria2p.Client(
|
|
||||||
host="http://localhost",
|
|
||||||
port=6800,
|
|
||||||
secret=""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return state.aria2
|
|
||||||
|
|
||||||
|
|
||||||
def aria2server():
|
|
||||||
download_path = state.download_path
|
|
||||||
speed_limit = state.speed_limit
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
"aria2c",
|
|
||||||
"--enable-rpc",
|
|
||||||
"--disable-ipv6", # added this since it caused problems with vpns
|
|
||||||
"--rpc-listen-all",
|
|
||||||
"--rpc-listen-port=6800",
|
|
||||||
f"--dir={download_path}",
|
|
||||||
f"--max-download-limit={speed_limit * 1024}",
|
|
||||||
"-x", str(state.aria2_threads),
|
|
||||||
"-s", str(state.aria2_threads),
|
|
||||||
]
|
|
||||||
|
|
||||||
aria2server = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
|
||||||
)
|
|
||||||
return aria2server
|
|
||||||
|
|
||||||
def dlprogress():
|
|
||||||
try:
|
|
||||||
state.downloads = [download for download in state.aria2.get_downloads() if not download.is_metadata]
|
|
||||||
downloads = state.downloads
|
|
||||||
if downloads:
|
|
||||||
for download in downloads:
|
|
||||||
progress = download.progress_string(0)
|
|
||||||
progress_int = int(progress.strip('%'))
|
|
||||||
return progress_int
|
|
||||||
return 0
|
|
||||||
except:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def send_notification(shutdown_event):
|
|
||||||
notified = set()
|
|
||||||
while not shutdown_event.is_set():
|
|
||||||
try:
|
|
||||||
for d in state.aria2.get_downloads():
|
|
||||||
if d.is_metadata:
|
|
||||||
continue
|
|
||||||
if d.progress == 100 and d.gid not in notified:
|
|
||||||
notification.notify(
|
|
||||||
title="Download finished",
|
|
||||||
message=f"{d.name} has finished downloading.",
|
|
||||||
timeout=4
|
|
||||||
)
|
|
||||||
notified.add(d.gid)
|
|
||||||
state.downloads = [d for d in state.aria2.get_downloads() if d.is_active and not d.is_metadata]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(5)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from .aria2_integration import run_aria2p
|
|
||||||
|
|
||||||
def start_client():
|
|
||||||
global aria2
|
|
||||||
aria2 = run_aria2p()
|
|
||||||
|
|
||||||
def add_magnet(uri):
|
|
||||||
aria2.add_magnet(uri)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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 {}
|
||||||
@@ -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}"}
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from core.utils.data.state import state
|
|
||||||
from .config import create_config
|
|
||||||
|
|
||||||
def restart_aria2c():
|
|
||||||
import main # had to do this because of circle import :(
|
|
||||||
import signal
|
|
||||||
import atexit
|
|
||||||
main.kill_aria2server()
|
|
||||||
state.aria2process.wait()
|
|
||||||
state.aria2process = main.run_aria2server()
|
|
||||||
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
|
|
||||||
atexit.unregister(main.kill_aria2server)
|
|
||||||
atexit.register(main.kill_aria2server)
|
|
||||||
|
|
||||||
def save_settings(thread_count, close, apiurl, download_path, speed_limit):
|
|
||||||
state.aria2_threads = thread_count
|
|
||||||
state.api_url = apiurl
|
|
||||||
state.download_path = download_path
|
|
||||||
state.speed_limit = speed_limit
|
|
||||||
|
|
||||||
create_config()
|
|
||||||
restart_aria2c()
|
|
||||||
close()
|
|
||||||
@@ -1,22 +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]
|
|
||||||
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="")
|
|
||||||
@@ -1,39 +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): # oftwarelist 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("./")
|
|
||||||
return item
|
|
||||||
if state.tracker == "rutracker":
|
|
||||||
item_dict = posts[post_index]
|
|
||||||
_, post_links, _ = format_data([item_dict])
|
|
||||||
return post_links[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_magnet_link(post_url):
|
|
||||||
try:
|
|
||||||
response = requests.get(post_url) # eventually impl. cloudscraper
|
|
||||||
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
|
|
||||||
|
|
||||||
@@ -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("Killed Aria2c")
|
|
||||||
except psutil.NoSuchProcess:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def closehelper():
|
|
||||||
shutdown_event.set()
|
|
||||||
kill_aria2server()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
def run_thread(thread):
|
|
||||||
thread.start()
|
|
||||||
@@ -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"network {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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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:"?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,47 +1,64 @@
|
|||||||
from core.interface.gui import MainWindow
|
from ui.clinterface import asciititle;
|
||||||
from core.utils.data.state import state
|
from core.uztracker_scraper import scrape_uztracker
|
||||||
from core.network.aria2_integration import aria2server
|
from core.uztracker_scraper import get_magnet_link
|
||||||
from core.network.aria2_integration import send_notification
|
from core.downloading.download import start_client
|
||||||
from core.utils.general.shutdown import kill_aria2server, closehelper, shutdown_event
|
from core.downloading.download import add_magnet
|
||||||
from core.utils.general.wrappers import run_thread
|
import os
|
||||||
from core.utils.config.config import read_config
|
|
||||||
from PySide6 import QtWidgets
|
|
||||||
import qdarktheme
|
|
||||||
import darkdetect
|
|
||||||
import threading
|
|
||||||
import signal
|
|
||||||
import atexit
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Debug Output
|
|
||||||
state.debug = True
|
|
||||||
|
|
||||||
|
|
||||||
def run_gui():
|
|
||||||
app = QtWidgets.QApplication([])
|
def search():
|
||||||
if darkdetect.isDark:
|
asciititle()
|
||||||
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
|
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:
|
else:
|
||||||
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
|
print("Invalid Selection.")
|
||||||
widget = MainWindow()
|
|
||||||
widget.show()
|
except ValueError:
|
||||||
sys.exit(app.exec())
|
print(Exception)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_aria2server():
|
|
||||||
aria2process = aria2server()
|
|
||||||
return aria2process
|
|
||||||
|
|
||||||
def keyboardinterrupthandler(signum, frame):
|
|
||||||
closehelper()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
read_config()
|
clear()
|
||||||
if state.debug:
|
results = search()
|
||||||
print("Starting Aria2 Server")
|
selected = select_result(results)
|
||||||
state.aria2process = run_aria2server()
|
print(selected)
|
||||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
uri = get_magnet_link(selected)
|
||||||
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
start_client()
|
||||||
if state.debug:
|
add_magnet(uri)
|
||||||
print("Launching GUI")
|
|
||||||
run_gui()
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
def asciititle():
|
||||||
|
print(f"""
|
||||||
|
|
||||||
|
_____ __ _ __ __
|
||||||
|
/ ____| / _| | | \/ |
|
||||||
|
| (___ ___ | |_| |___ ____ _ _ __ ___| \ / | __ _ _ __ __ _ __ _ ___ _ __
|
||||||
|
\___ \ / _ \| _| __\ \ /\ / / _` | '__/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
|
||||||
|
____) | (_) | | | |_ \ V V / (_| | | | __/ | | | (_| | | | | (_| | (_| | __/ |
|
||||||
|
|_____/ \___/|_| \__| \_/\_/ \__,_|_| \___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
Reference in New Issue
Block a user