mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-04 01:49:42 +02:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f0f454578 | |||
| 45e614ed86 | |||
| 43baeed7d3 | |||
| e34ff32dbd | |||
| 7990480843 | |||
| 000278ec6b | |||
| 931faacb0b | |||
| 6de5337e77 | |||
| 991ea47b40 | |||
| adc0d1c552 | |||
| 5215fb2e1b | |||
| de2fc1ca42 | |||
| 6e2860cb67 | |||
| 65a5b8cdbf | |||
| 30e920675d | |||
| 7d77a88245 | |||
| d9314c979e | |||
| 4bd6761592 | |||
| e41b142023 | |||
| 97b604eec1 | |||
| 94c0673714 | |||
| 00c6babc40 | |||
| 7cd342effc | |||
| ac8f48ed56 | |||
| f59c8227c2 | |||
| 8c787db331 | |||
| a0f772def8 | |||
| 0537bc23e3 | |||
| 962ce8984c | |||
| f358d5462f | |||
| 251f9f40de | |||
| 9d0033dcb5 | |||
| 92a6fbc9b5 | |||
| 77343e6300 | |||
| c151b87da9 | |||
| a9450435d7 | |||
| e4c50e20df | |||
| b5d944278b | |||
| 223dcd9a4c | |||
| 12f7712af3 | |||
| 108ba9ab31 | |||
| 9a7985696c | |||
| ea9288dcf7 | |||
| 2659b1da79 | |||
| 1af30922d2 | |||
| 335845b9dd | |||
| 573889721f | |||
| d7d454a213 | |||
| b45a4895f5 | |||
| f1c92448e7 | |||
| c06fac0f55 | |||
| b0596e9710 | |||
| 62b8506d13 | |||
| 53b2bb912e | |||
| 5da7989157 | |||
| 13d0384a64 | |||
| 171a61b8ae | |||
| 1f4fbb845a | |||
| 1b0a00068c | |||
| 70035cc255 | |||
| 26b091319b | |||
| d2a2212b15 | |||
| 0184b10a5f | |||
| 662751d7db | |||
| cdb9257859 | |||
| 03ee9c299a | |||
| 6c54692155 | |||
| 199b59b7da | |||
| 2428bddb9f | |||
| 2cc4b9e815 | |||
| dfc44e3534 | |||
| a568e14db4 | |||
| 817ea56001 | |||
| d3063cd148 | |||
| 2d8876868f | |||
| 1c5460de94 | |||
| 6a12cb4314 | |||
| 2d264d7d1c | |||
| 1ac9496e46 | |||
| 0c61ac5319 | |||
| 6a34c15457 | |||
| b925503562 | |||
| bae3667cd9 | |||
| ca28097dd9 | |||
| 7c9bad2451 | |||
| b8af01927d | |||
| b284a06708 | |||
| 9394b5fa01 | |||
| 3c7450cee1 | |||
| eedb5ded17 | |||
| 8a856f0dad | |||
| 01217c21af | |||
| 59f092f937 | |||
| 154d06caaf | |||
| 4ee1f366aa | |||
| 9fbaca0b2b | |||
| 75ff4e4ecd | |||
| 66d43faad1 | |||
| 1c28d8e232 | |||
| 71ab31b537 | |||
| af1488d980 | |||
| 32c9f4a391 | |||
| 664ac9b48f | |||
| 7035eec0b5 | |||
| bdad958e25 | |||
| a95cc197be | |||
| 05c60104c4 | |||
| c1ee2b7c60 | |||
| 8033c56145 | |||
| a3471fd100 | |||
| cf46ee1c78 | |||
| e3ca73e600 | |||
| 8eacbc4bc5 | |||
| 9b9b50db9b | |||
| 764002f4de | |||
| 72a892d707 | |||
| 01303234d6 | |||
| 87380f57af | |||
| b701e75bdf | |||
| 922897ebc5 | |||
| 9578326a1a | |||
| bf6e638519 | |||
| addd432229 | |||
| 9d6e54b451 | |||
| 6aa43ae2c5 | |||
| 5929c218b7 | |||
| 7edec4ace5 | |||
| 7cc79e69cb | |||
| 4ddccc2341 | |||
| 09a3a5a7a2 | |||
| 391e4c372b | |||
| 9793558b7c | |||
| 5bfa06239a | |||
| 25e1647a13 | |||
| e39c386e55 | |||
| 70c7042b09 | |||
| 8716000057 | |||
| 0307f0fa82 | |||
| 091a534ceb | |||
| aa31b85365 | |||
| 908e559805 | |||
| cdde3e12d9 | |||
| 60fd806f1e | |||
| e80bd9569f | |||
| ebdedb2536 | |||
| a94048188e | |||
| 69892861a5 | |||
| bf3516ee02 | |||
| e48375bce1 |
@@ -0,0 +1,143 @@
|
|||||||
|
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 }}
|
||||||
+215
-1
@@ -1,2 +1,216 @@
|
|||||||
__pycache__
|
# 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
|
.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,14 +1,11 @@
|
|||||||
# 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 (impl. soon)
|
- Bandwidth/Download speed limiting
|
||||||
- UI Theming
|
|
||||||
- Start downloads via a predefined magnet URI.
|
- Start downloads via a predefined magnet URI.
|
||||||
- View detailed download information (name, speed, and status). (improved impl. soon)
|
- View detailed download information (name, speed, and status).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
1. Download the binary file for your operating system from releases
|
1. Download the binary file for your operating system from releases
|
||||||
@@ -33,3 +30,4 @@ 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.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,123 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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}")
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .aria2_integration import run_aria2p
|
||||||
|
|
||||||
|
def start_client():
|
||||||
|
global aria2
|
||||||
|
aria2 = run_aria2p()
|
||||||
|
|
||||||
|
def add_magnet(uri):
|
||||||
|
aria2.add_magnet(uri)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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")
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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="")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
def run_thread(thread):
|
||||||
|
thread.start()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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,64 +1,47 @@
|
|||||||
from ui.clinterface import asciititle;
|
from core.interface.gui import MainWindow
|
||||||
from core.uztracker_scraper import scrape_uztracker
|
from core.utils.data.state import state
|
||||||
from core.uztracker_scraper import get_magnet_link
|
from core.network.aria2_integration import aria2server
|
||||||
from core.downloading.download import start_client
|
from core.network.aria2_integration import send_notification
|
||||||
from core.downloading.download import add_magnet
|
from core.utils.general.shutdown import kill_aria2server, closehelper, shutdown_event
|
||||||
import os
|
from core.utils.general.wrappers import run_thread
|
||||||
|
from core.utils.config.config import read_config
|
||||||
|
from PySide6 import QtWidgets
|
||||||
|
import qdarktheme
|
||||||
def search():
|
import darkdetect
|
||||||
asciititle()
|
import threading
|
||||||
searchquery = input("Enter Search Query: ")
|
import signal
|
||||||
result = scrape_uztracker(searchquery)
|
import atexit
|
||||||
if result is None:
|
import sys
|
||||||
clear()
|
|
||||||
asciititle()
|
# Debug Output
|
||||||
print("No results found for your Query.\n")
|
state.debug = True
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
clear()
|
read_config()
|
||||||
results = search()
|
if state.debug:
|
||||||
selected = select_result(results)
|
print("Starting Aria2 Server")
|
||||||
print(selected)
|
state.aria2process = run_aria2server()
|
||||||
uri = get_magnet_link(selected)
|
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||||
start_client()
|
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
||||||
add_magnet(uri)
|
if state.debug:
|
||||||
|
print("Launching GUI")
|
||||||
|
run_gui()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
def asciititle():
|
|
||||||
print(f"""
|
|
||||||
|
|
||||||
_____ __ _ __ __
|
|
||||||
/ ____| / _| | | \/ |
|
|
||||||
| (___ ___ | |_| |___ ____ _ _ __ ___| \ / | __ _ _ __ __ _ __ _ ___ _ __
|
|
||||||
\___ \ / _ \| _| __\ \ /\ / / _` | '__/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
|
|
||||||
____) | (_) | | | |_ \ V V / (_| | | | __/ | | | (_| | | | | (_| | (_| | __/ |
|
|
||||||
|_____/ \___/|_| \__| \_/\_/ \__,_|_| \___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
|
|
||||||
__/ |
|
|
||||||
|___/
|
|
||||||
|
|
||||||
""")
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user