mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
249 Commits
main-cli
...
b783d0a-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| b783d0a0cb | |||
| d057624548 | |||
| 81f7970c47 | |||
| 6a41ac44c9 | |||
| 26c5410f3f | |||
| 7067f243ae | |||
| d82008c9fe | |||
| 83ff2720c8 | |||
| 83adbb027f | |||
| 606c70efcc | |||
| 9970086901 | |||
| 0d631e0ab8 | |||
| 1c1ce3e69d | |||
| b5931f5590 | |||
| 84ea030e2a | |||
| 356f3bf070 | |||
| 6ff29e2ea6 | |||
| 847b50ab10 | |||
| 5bc81b5215 | |||
| efe83613d0 | |||
| 3597cba207 | |||
| 749051aa4f | |||
| bd26f073e4 | |||
| 027b4aff8d | |||
| a386269f29 | |||
| 9afced8a0b | |||
| db77bed3cf | |||
| cc3b201450 | |||
| 5a9d9067b9 | |||
| e0eaf250e5 | |||
| 1c4c010e4e | |||
| c1509f9172 | |||
| 97eabf6ec6 | |||
| 5eca1a678e | |||
| 7e59fdc2d3 | |||
| ad6fa428a0 | |||
| 56e8fada4d | |||
| e950d2f90d | |||
| 1a59c45ba4 | |||
| 293c1c3ad1 | |||
| f30cc5da5e | |||
| 004c7ad416 | |||
| 65f7c04936 | |||
| 9d0d5332bf | |||
| 6d9991188a | |||
| f397bc18ce | |||
| 59e45efbb6 | |||
| 27f0a3f71d | |||
| 1d500305f7 | |||
| 20a310264f | |||
| aee8f16353 | |||
| 2354adb349 | |||
| 4c69a17f0e | |||
| 2bab6c3321 | |||
| 91482760b6 | |||
| 83fa23df58 | |||
| 41d28095c2 | |||
| de2e49e60b | |||
| dd6d16d833 | |||
| 7d0c8d16ec | |||
| 80750f010f | |||
| c93f6345c8 | |||
| a7132ed1cd | |||
| a2cff15d0d | |||
| 2eb384f7cf | |||
| 05a5b04507 | |||
| 87c974f124 | |||
| 95a687ddd9 | |||
| 813d2b31c4 | |||
| deb5ce5b88 | |||
| a11c1d7129 | |||
| dcc78ade44 | |||
| 10e80ab4b6 | |||
| 3290ff3fac | |||
| d5d99d6a0e | |||
| 3960612f94 | |||
| a37a47b65b | |||
| 76b26664f2 | |||
| c8a1e72107 | |||
| 7c505f1402 | |||
| 55e4f86a39 | |||
| bd71b0e645 | |||
| 2a86e4b774 | |||
| d17b4bd44a | |||
| 10c935e852 | |||
| bd8320a26c | |||
| c99ce339e0 | |||
| 1822a150d7 | |||
| 7e5e3033e6 | |||
| 1cab671a13 | |||
| 5637e80657 | |||
| 2f78b812ba | |||
| 5af9e79ecc | |||
| 4dadc9ba50 | |||
| 719c4e86e8 | |||
| 999c8e9ba4 | |||
| 3f6889da56 | |||
| 69ac739be6 | |||
| 141cc8fadc | |||
| 80c6ece72e | |||
| 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,210 @@
|
||||
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-22.04
|
||||
artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
|
||||
asset_name: SoftwareManager-dev-${{ github.sha }}-linux
|
||||
- os: windows-latest
|
||||
artifact_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
|
||||
asset_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
|
||||
- os: macos-latest
|
||||
artifact_name: SoftwareManager-dev-${{ github.sha }}-macos
|
||||
asset_name: SoftwareManager-dev-${{ github.sha }}-macos
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
short_sha: ${{ steps.version.outputs.short_sha }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxcb-xinerama0 \
|
||||
libxkbcommon-x11-0 \
|
||||
libxcb-cursor0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-shape0 \
|
||||
libxcb-util1
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller backports.tarfile pillow
|
||||
|
||||
- name: Generate version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
COMMIT_SHA=${{ github.sha }}
|
||||
SHORT_SHA=${COMMIT_SHA:0:7}
|
||||
VERSION="${SHORT_SHA}-dev"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "Generated version: $VERSION"
|
||||
|
||||
- name: Inject commit hash into code
|
||||
shell: bash
|
||||
run: |
|
||||
cat > build_info.json << EOF
|
||||
{
|
||||
"version": "${{ env.VERSION }}"
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Download aria2c (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
mkdir -p deps
|
||||
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip
|
||||
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/
|
||||
|
||||
- name: Build executable with PyInstaller (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile --windowed \
|
||||
--add-data "build_info.json;." \
|
||||
--add-data "build_info.json;core" \
|
||||
--add-data "build_info.json;core/interface" \
|
||||
--add-data "deps/aria2c.exe;." \
|
||||
--add-data "core/interface/assets;core/interface/assets" \
|
||||
--icon "core/interface/assets/logo.png" \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
--hidden-import=backports.tarfile \
|
||||
--hidden-import=backports \
|
||||
--hidden-import=jaraco.context \
|
||||
--hidden-import=jaraco.text \
|
||||
--hidden-import=jaraco.functools \
|
||||
--name ${{ env.APP_NAME }} \
|
||||
main.py
|
||||
|
||||
- name: Build executable with PyInstaller (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile --windowed \
|
||||
--add-data "build_info.json:." \
|
||||
--add-data "build_info.json:core" \
|
||||
--add-data "build_info.json:core/interface" \
|
||||
--add-data "core/interface/assets:core/interface/assets" \
|
||||
--icon "core/interface/assets/logo.png" \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
--hidden-import=backports.tarfile \
|
||||
--hidden-import=backports \
|
||||
--hidden-import=jaraco.context \
|
||||
--hidden-import=jaraco.text \
|
||||
--hidden-import=jaraco.functools \
|
||||
--name ${{ env.APP_NAME }} \
|
||||
main.py
|
||||
|
||||
- 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-22.04
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||
cp artifacts/ubuntu-22.04-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: Generate release notes
|
||||
run: |
|
||||
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||
cat > release_notes.md << EOF
|
||||
## Development Build
|
||||
|
||||
**Commit:** [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||
|
||||
### Changes
|
||||
$(git log -1 --pretty=format:"- %s" ${{ github.sha }})
|
||||
|
||||
### Downloads
|
||||
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
|
||||
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
|
||||
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
|
||||
EOF
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create Release (Public)
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.build.outputs.version }}
|
||||
name: ${{ needs.build.outputs.version }}
|
||||
body_path: release_notes.md
|
||||
files: |
|
||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
|
||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
|
||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+220
-2
@@ -1,2 +1,220 @@
|
||||
__pycache__
|
||||
.venv
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
|
||||
# IDE's and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
*.code-workspace
|
||||
*.swp
|
||||
|
||||
# Local build files
|
||||
build-local.ps1
|
||||
build-local.bat
|
||||
@@ -1,63 +1,44 @@
|
||||
# PirateLauncher - A Download Manager/Launcher
|
||||
|
||||
## Description
|
||||
PirateLauncher is a Python-based GUI/TUI tool that simplifies downloading and managing pirated software from various sources.
|
||||
# SoftwareManager - A Download Manager/Launcher
|
||||
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
||||
|
||||
## Features
|
||||
- VPN Binding (impl. soon)
|
||||
- Bandwidth/Download speed limiting
|
||||
- Start downloads via a predefined magnet URI.
|
||||
- View detailed download information (name, speed, and status).
|
||||
|
||||
## Requirements
|
||||
- Python 3.x
|
||||
- `aria2c` (installed and available in your PATH)
|
||||
- Python libraries:
|
||||
- `aria2p`
|
||||
- `keyboard`
|
||||
|
||||
## Installation
|
||||
1. Download the binary file for your operating system from releases
|
||||
https://github.com/KeksPirates/SoftwareManager/releases
|
||||
2. Run the file
|
||||
|
||||
## Manual Installation
|
||||
1. Ensure Python 3.x is installed on your system.
|
||||
2. Install Aria2:
|
||||
2. Install Dependencies:
|
||||
```bash
|
||||
sudo apt install aria2 # For Debian/Ubuntu/Mint
|
||||
paru/yay -S aria2 # For Arch from AUR
|
||||
brew install aria2 # For macOS
|
||||
choco install aria2 # For Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
4. Install the required Python libraries:
|
||||
3. Run main.py:
|
||||
```bash
|
||||
pip install aria2p keyboard
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Usage
|
||||
1. Clone or download PirateLauncher to your local machine.
|
||||
2. Run the launcher:
|
||||
```bash
|
||||
python pirate_launcher.py
|
||||
```
|
||||
3. Use the following keyboard controls:
|
||||
- Press **`d`** to start/resume a download.
|
||||
- Press **`s`** to pause all downloads.
|
||||
- Press **`q`** to quit the launcher.
|
||||
|
||||
|
||||
## Configuration
|
||||
- To use a different magnet URI, update the `magnet_uri` variable in the script.
|
||||
- Ensure the RPC port (default: 6800) is open and accessible.
|
||||
|
||||
## Example
|
||||
Run the following command to launch PirateLauncher and control downloads:
|
||||
|
||||
```bash
|
||||
python pirate_launcher.py
|
||||
```
|
||||
|
||||
Press `d` to start the predefined download, `s` to pause, and `q` to quit.
|
||||
|
||||
## Notes
|
||||
- Aria2 must be installed and accessible from your command line.
|
||||
- If you want to host your own server, clone the server branch of this repo, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of its branch.
|
||||
- This tool is for educational purposes only. Use it responsibly.
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#KeksPirates/SoftwareManager&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=KeksPirates/SoftwareManager&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
**Disclaimer:** PirateLauncher 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,3 @@
|
||||
{
|
||||
"Version": "dev"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
def scrape_monkrus_telegram(query):
|
||||
post = 100
|
||||
max_posts = 250
|
||||
posts: list[dict] = []
|
||||
added = set()
|
||||
|
||||
while post <= max_posts:
|
||||
url = f"https://t.me/s/real_monkrus/{post}"
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
bubbles = soup.find_all("div", class_="tgme_widget_message_bubble")
|
||||
|
||||
if not bubbles:
|
||||
break
|
||||
|
||||
for bubble in bubbles:
|
||||
post_txt = bubble.find("div", class_="tgme_widget_message_text js-message_text")
|
||||
if not post_txt or not post_txt.b:
|
||||
continue
|
||||
|
||||
title = post_txt.b.text
|
||||
link = bubble.find("a", href=lambda x: x and x.startswith("https://uztracker.net"))
|
||||
|
||||
if query.lower() in title.lower() and link:
|
||||
post_url = link["href"]
|
||||
if post_url not in added:
|
||||
added.add(post_url)
|
||||
posts.append(dict(
|
||||
author="m0nkrus",
|
||||
id=len(posts) + 1,
|
||||
title=title,
|
||||
url=post_url
|
||||
))
|
||||
|
||||
post += 22
|
||||
|
||||
posts.reverse()
|
||||
return(posts)
|
||||
@@ -0,0 +1,21 @@
|
||||
import requests
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
|
||||
def scrape_rutracker(search_text):
|
||||
search = requests.get(f"{state.api_url}/search/{search_text}")
|
||||
consoleLog("Sent request to server")
|
||||
if search:
|
||||
try:
|
||||
return search.text
|
||||
except Exception:
|
||||
consoleLog("No results found / No response from server")
|
||||
return None
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
# This function utilizes the SoftwareManager server - source code can be found under the "server" branch.
|
||||
@@ -0,0 +1,82 @@
|
||||
import requests
|
||||
import asyncio
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
|
||||
global url_uztracker
|
||||
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
||||
|
||||
async def init_uztracker():
|
||||
global up
|
||||
global soup
|
||||
try:
|
||||
response = requests.get(url_uztracker, timeout=10)
|
||||
if response.status_code == 200:
|
||||
up = True
|
||||
else:
|
||||
up = False
|
||||
consoleLog(f"Uztracker seems down, status code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
consoleLog(f"\nRequest Exception on {url_uztracker}:")
|
||||
consoleLog(str(e))
|
||||
consoleLog("\nIs the Site down?")
|
||||
up = False
|
||||
|
||||
asyncio.run(init_uztracker())
|
||||
|
||||
def scrape_uztracker(search):
|
||||
if up:
|
||||
search_url = url_uztracker + search
|
||||
consoleLog(search_url)
|
||||
result = False
|
||||
global results
|
||||
global resulttitles
|
||||
results = []
|
||||
resulttitles = []
|
||||
|
||||
try:
|
||||
resultCount = 0
|
||||
response = requests.get(search_url)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
links = soup.find_all('a', class_="genmed tLink", href=lambda x: x and x.startswith('./viewtopic'))
|
||||
for link in links:
|
||||
if link.b:
|
||||
resultCount += 1
|
||||
results.append(link['href'])
|
||||
resulttitles.append(link.b.text)
|
||||
result = True
|
||||
|
||||
if result:
|
||||
return resulttitles, results
|
||||
|
||||
if not result:
|
||||
# add popup in gui for no result
|
||||
return None
|
||||
|
||||
except requests.RequestException as e:
|
||||
if result:
|
||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||
return None
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None, None
|
||||
|
||||
def get_post_title(post_url):
|
||||
if up:
|
||||
response = requests.get(post_url)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
maintitle = soup.find(class_='tt-text')
|
||||
if maintitle:
|
||||
return maintitle.text
|
||||
else:
|
||||
consoleLog("Program not found")
|
||||
else:
|
||||
consoleLog("Error: Uztracker down")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import aria2p
|
||||
import subprocess
|
||||
import keyboard
|
||||
import os
|
||||
# from .scraper import uri
|
||||
|
||||
aria2 = aria2p.API(
|
||||
aria2p.Client(
|
||||
host="http://localhost",
|
||||
port=6800,
|
||||
secret=""
|
||||
)
|
||||
)
|
||||
|
||||
download_paused = False
|
||||
def pauseDownload(_):
|
||||
global download_paused
|
||||
if download_paused == True:
|
||||
print("Error: Download already paused.")
|
||||
else:
|
||||
print("Download is being paused...")
|
||||
download_paused = True
|
||||
aria2.pause_all()
|
||||
|
||||
def startDownload(_):
|
||||
global download_paused
|
||||
if download_paused == True:
|
||||
aria2.resume_all()
|
||||
print("Download Resumed!")
|
||||
download_paused = False
|
||||
else:
|
||||
print("Download Started!")
|
||||
download = aria2.add_magnet(uri)
|
||||
download_paused = False
|
||||
|
||||
def terminate():
|
||||
return
|
||||
|
||||
# print("Enter Download URL: ")
|
||||
# uri = input()
|
||||
|
||||
#Magnet Download
|
||||
# downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
|
||||
# subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
|
||||
# magnet_uri = "magnet:?xt=urn:btih:7FBF03CCC9115260DC50102E5A31E83E91422A79&dn=Ready+or+Not%3A+LSPD+Bundle+%28v66884+%2B+4+DLCs%2FBonuses+%2B+Windows+7+Fix%2C+MULTi12%29+%5BFitGirl+Repack%2C+Selective+Download+-+from+34.7+GB%5D&tr=udp%3A%2F%2Fopentor.net%3A6969&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccp.ovh%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce"
|
||||
|
||||
# #Torrent Download
|
||||
# downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
|
||||
# subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
|
||||
# torrent_path = "C:/Users/Downloads/Ready or Not.torrent"
|
||||
|
||||
#Direct Download
|
||||
downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
|
||||
subprocess.Popen(["aria2c", "--enable-rpc", "--rpc-listen-all=true", f"--dir={downloads_dir}"])
|
||||
uri
|
||||
|
||||
downloads = aria2.get_downloads()
|
||||
|
||||
for download in downloads:
|
||||
print(download.name, downloads.download_speed, download.status)
|
||||
|
||||
keyboard.on_press_key("d", startDownload, suppress=False)
|
||||
keyboard.on_press_key("s", pauseDownload, suppress=False)
|
||||
keyboard.on_press_key("q", pauseDownload, terminate)
|
||||
keyboard.wait("q")
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QLayoutItem, QLineEdit, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QLabel, QPushButton
|
||||
import sys
|
||||
from uztracker_scraper import scrape_uztracker
|
||||
|
||||
|
||||
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(self.return_pressed)
|
||||
|
||||
self.button = QtWidgets.QPushButton("Download")
|
||||
self.softwareList = QListWidget()
|
||||
|
||||
container = QWidget()
|
||||
containerLayout = QVBoxLayout()
|
||||
containerLayout.addWidget(self.searchbar)
|
||||
|
||||
containerLayout.addWidget(self.softwareList)
|
||||
self.softwareList.addItems(searchresults)
|
||||
|
||||
|
||||
containerLayout.addWidget(self.button)
|
||||
self.button.clicked.connect(lambda: print(f"Downloading {self.softwareList.currentItem().text()}"), )
|
||||
|
||||
|
||||
container.setLayout(containerLayout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
|
||||
|
||||
def return_pressed(self):
|
||||
search_text = self.searchbar.text()
|
||||
self.postnames, self.postlinks = scrape_uztracker(search_text)
|
||||
self.softwareList.clear()
|
||||
if self.postnames:
|
||||
self.softwareList.addItems(self.postnames)
|
||||
print("User searched for:", search_text)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def run_gui():
|
||||
app = QtWidgets.QApplication([])
|
||||
|
||||
widget = MainWindow()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_gui()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,166 @@
|
||||
from core.utils.config.settings import save_settings
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QDialog,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QSpinBox,
|
||||
QCheckBox,
|
||||
)
|
||||
import platform
|
||||
|
||||
|
||||
def settings_dialog(self):
|
||||
|
||||
consoleLog("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()
|
||||
|
||||
update_checkbox_container = QWidget()
|
||||
update_checkbox_layout = QHBoxLayout()
|
||||
|
||||
# ignore updates checkbox
|
||||
|
||||
if platform.system() == "Windows":
|
||||
update_checkbox = QCheckBox()
|
||||
update_checkbox_container.setLayout(update_checkbox_layout)
|
||||
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
|
||||
update_checkbox_layout.addStretch()
|
||||
update_checkbox.setChecked(state.ignore_updates)
|
||||
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
|
||||
update_checkbox_layout.addWidget(update_checkbox)
|
||||
dialog.layout().addWidget(update_checkbox_container)
|
||||
|
||||
# auto-resume downloads checkbox
|
||||
|
||||
autoresume_container = QWidget()
|
||||
autoresume_layout = QHBoxLayout()
|
||||
|
||||
autoresume_checkbox = QCheckBox()
|
||||
autoresume_container.setLayout(autoresume_layout)
|
||||
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
|
||||
|
||||
autoresume_layout.addStretch()
|
||||
autoresume_checkbox.setChecked(state.autoresume)
|
||||
autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked))
|
||||
autoresume_layout.addWidget(autoresume_checkbox)
|
||||
dialog.layout().addWidget(autoresume_container)
|
||||
|
||||
##################
|
||||
# THREAD SETTING #
|
||||
##################
|
||||
thread_box = QSpinBox()
|
||||
thread_box.setMinimum(1)
|
||||
thread_box.setMaximum(16)
|
||||
thread_box.setValue(state.aria2_threads)
|
||||
|
||||
|
||||
# container for tight space
|
||||
thread_container = QWidget()
|
||||
thread_layout = QHBoxLayout()
|
||||
|
||||
thread_layout.addWidget(QLabel("Threads:"))
|
||||
thread_layout.addWidget(thread_box)
|
||||
thread_container.setLayout(thread_layout)
|
||||
thread_container.setMaximumHeight(80)
|
||||
|
||||
|
||||
# Dimensions
|
||||
thread_box.setFixedWidth(90)
|
||||
thread_box.setFixedHeight(30)
|
||||
|
||||
dialog.layout().addWidget(thread_container)
|
||||
|
||||
##################
|
||||
# SERVER SETTING #
|
||||
##################
|
||||
|
||||
api_url_container = QWidget()
|
||||
api_url_layout = QHBoxLayout()
|
||||
|
||||
api_url = QLineEdit()
|
||||
api_url_layout.addWidget(QLabel("API Server URL:"))
|
||||
api_url_layout.addWidget(api_url)
|
||||
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
api_url_container.setLayout(api_url_layout)
|
||||
api_url.setText(state.api_url)
|
||||
dialog.layout().addWidget(api_url_container)
|
||||
|
||||
#################
|
||||
# DOWNLOAD PATH #
|
||||
#################
|
||||
|
||||
download_path_container = QWidget()
|
||||
download_path_layout = QHBoxLayout()
|
||||
|
||||
download_path = QLineEdit()
|
||||
download_path_layout.addWidget(QLabel("Download Path:"))
|
||||
download_path_layout.addWidget(download_path)
|
||||
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
download_path_container.setLayout(download_path_layout)
|
||||
download_path.setText(state.download_path)
|
||||
dialog.layout().addWidget(download_path_container)
|
||||
|
||||
##################
|
||||
# SPEED LIMITING #
|
||||
##################
|
||||
|
||||
speed_limit_container = QWidget()
|
||||
speed_limit_layout = QHBoxLayout()
|
||||
|
||||
speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
|
||||
speed_limit = QSpinBox()
|
||||
speed_limit.setMinimum(0)
|
||||
speed_limit.setMaximum(10000000)
|
||||
speed_limit.setValue(state.speed_limit)
|
||||
speed_limit_container.setLayout(speed_limit_layout)
|
||||
speed_limit_layout.addWidget(speed_limit)
|
||||
speed_limit.setFixedWidth(180)
|
||||
speed_limit.setFixedHeight(30)
|
||||
dialog.layout().addWidget(speed_limit_container)
|
||||
|
||||
###############
|
||||
# IMAGE PATH #
|
||||
###############
|
||||
|
||||
image_path_container = QWidget()
|
||||
image_path_layout = QHBoxLayout()
|
||||
|
||||
image_path = QLineEdit()
|
||||
image_path_layout.addWidget(QLabel("Image Path (requires restart):"))
|
||||
image_path_layout.addWidget(image_path)
|
||||
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
image_path_container.setLayout(image_path_layout)
|
||||
image_path.setText(state.image_path)
|
||||
dialog.layout().addWidget(image_path_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(), image_path.text()))
|
||||
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
layout.addWidget(cancel_btn)
|
||||
layout.addWidget(save_btn)
|
||||
|
||||
dialog.layout().addLayout(layout)
|
||||
|
||||
dialog.exec()
|
||||
@@ -0,0 +1,410 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QTableView,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QListWidget,
|
||||
QToolBar,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QComboBox,
|
||||
QTabWidget,
|
||||
QProgressBar,
|
||||
QHeaderView,
|
||||
QMessageBox,
|
||||
QTableWidget,
|
||||
QTextEdit,
|
||||
QStyle,
|
||||
QStyleOptionButton,
|
||||
QStyledItemDelegate,
|
||||
QApplication,
|
||||
)
|
||||
|
||||
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
|
||||
import darkdetect
|
||||
import threading
|
||||
import platform
|
||||
import requests as r
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.data.state import state
|
||||
from core.utils.network.download import download_selected
|
||||
from core.utils.network.update_checker import check_for_updates
|
||||
from core.utils.general.shutdown import closehelper
|
||||
from core.interface.utils.tabhelper import create_tab
|
||||
from core.interface.utils.searchhelper import return_pressed
|
||||
from core.interface.dialogs.settings import settings_dialog
|
||||
from core.network.aria2_integration import dlprogress
|
||||
|
||||
|
||||
def download_update(latest_version):
|
||||
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
||||
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
|
||||
|
||||
consoleLog("Downloading update...", True)
|
||||
response = r.get(url, allow_redirects=True)
|
||||
with open(new_filename, "wb") as f:
|
||||
f.write(response.content)
|
||||
if not os.path.exists(new_filename):
|
||||
raise FileNotFoundError("Executable not found")
|
||||
subprocess.Popen([new_filename], shell=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("New Version")
|
||||
msg.setText("New version installed.")
|
||||
msg.setInformativeText("Please remove the old exe.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
log_signal = Signal(str) # Thread-safe signal for logging
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
MainWindow._instance = self
|
||||
self.log_signal.connect(self._on_log_signal)
|
||||
|
||||
def get_asset_path(filename):
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.dirname(__file__)
|
||||
|
||||
asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename)
|
||||
if os.path.exists(asset_path):
|
||||
return asset_path
|
||||
|
||||
return os.path.join('core', 'interface', 'assets', filename)
|
||||
|
||||
self.setWindowIcon(QIcon(get_asset_path("logo.png")))
|
||||
|
||||
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import ctypes
|
||||
appid = 'SoftwareManager'
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||
except Exception as e:
|
||||
consoleLog(f"Could not set app ID: {e}")
|
||||
|
||||
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
|
||||
if os.path.exists(build_info_path):
|
||||
with open(build_info_path, "r") as f:
|
||||
build_info = json.load(f)
|
||||
state.version = build_info.get("version")
|
||||
|
||||
# Check for updates on Windows
|
||||
if state.ignore_updates is False:
|
||||
if platform.system() == "Windows":
|
||||
result = check_for_updates()
|
||||
if result != (None, None):
|
||||
assets, latest_version = result
|
||||
|
||||
if assets:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText("A new version is available.")
|
||||
msg.setInformativeText("Press Ok to download the update.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(latest_version)
|
||||
|
||||
self.setWindowTitle("Software Manager")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
self.controls = QWidget()
|
||||
self.controlsLayout = QVBoxLayout()
|
||||
|
||||
# Widgets
|
||||
self.searchbar = QLineEdit()
|
||||
self.searchbar.setPlaceholderText("Search for software...")
|
||||
self.searchbar.setClearButtonEnabled(True)
|
||||
self.searchbar.setMinimumHeight(30)
|
||||
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
|
||||
|
||||
self.dlbutton = QtWidgets.QPushButton("Download")
|
||||
self.libraryList = QListWidget()
|
||||
|
||||
self.emptyResults = QLabel("No Results")
|
||||
self.emptyResults.setAlignment(Qt.AlignCenter)
|
||||
self.emptyResults.hide()
|
||||
|
||||
self.download_model = None
|
||||
self.downloadList = QTableView()
|
||||
self.emptyLibrary = QLabel("No items in library.")
|
||||
self.emptyDownload = QLabel("No items in downloads.")
|
||||
self.emptyDownload.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.consoleLog = QTextEdit()
|
||||
self.progressbar = QProgressBar()
|
||||
|
||||
# Table Widget for Item List
|
||||
self.qtablewidget = QTableWidget()
|
||||
|
||||
self.qtablewidget.setColumnCount(2)
|
||||
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.qtablewidget.verticalHeader().setVisible(False)
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||
header.setStretchLastSection(False)
|
||||
|
||||
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||
header.resizeSection(1, 500)
|
||||
|
||||
container = QWidget()
|
||||
containerLayout = QVBoxLayout()
|
||||
containerLayout.addWidget(self.searchbar)
|
||||
containerLayout.addWidget(self.qtablewidget)
|
||||
|
||||
class DownloadModel(QAbstractTableModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.headers = ["Action", "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):
|
||||
col = index.column()
|
||||
download = state.downloads[index.row()]
|
||||
if role == Qt.DisplayRole:
|
||||
col = index.column()
|
||||
|
||||
if col == 0:
|
||||
return "▶︎" if download.is_paused else "⏸︎"
|
||||
elif col == 1:
|
||||
return download.name
|
||||
elif col == 2:
|
||||
return getattr(download, 'status', 'Downloading')
|
||||
elif col == 3:
|
||||
return f"{int(download.progress)}%"
|
||||
elif col == 4:
|
||||
return f"{download.download_speed_string()}"
|
||||
elif col == 5:
|
||||
return f"{download.completed_length_string()}"
|
||||
elif col == 6:
|
||||
return f"{download.total_length_string()}"
|
||||
elif col == 7:
|
||||
return f"{download.eta_string()}"
|
||||
|
||||
if role == Qt.UserRole and col == 0:
|
||||
return download.is_paused
|
||||
|
||||
return None
|
||||
|
||||
def toggle_pause_resume(self, row):
|
||||
download = state.downloads[row]
|
||||
if download.is_paused:
|
||||
download.resume()
|
||||
consoleLog(f"Resumed download: {download.name}", True)
|
||||
else:
|
||||
download.pause()
|
||||
consoleLog(f"Paused download: {download.name}", True)
|
||||
idx = self.index(row, 0)
|
||||
self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.UserRole])
|
||||
|
||||
class PauseResumeDelegate(QStyledItemDelegate):
|
||||
clicked = Signal(int)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
if index.column() != 0:
|
||||
super().paint(painter, option, index)
|
||||
return
|
||||
|
||||
paused = index.data(Qt.UserRole)
|
||||
|
||||
button = QStyleOptionButton()
|
||||
button.rect = option.rect
|
||||
button.text = "▶︎" if paused else "⏸︎"
|
||||
button.state = QStyle.State_Enabled
|
||||
|
||||
QApplication.style().drawControl(
|
||||
QStyle.CE_PushButton, button, painter
|
||||
)
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
|
||||
self.clicked.emit(index.row())
|
||||
return True
|
||||
return False
|
||||
|
||||
self.download_model = DownloadModel()
|
||||
self.downloadList.setModel(self.download_model)
|
||||
self.downloadList.horizontalHeader().setStretchLastSection(False)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
self.downloadList.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.downloadList.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
download_model = self.download_model
|
||||
|
||||
def on_pause_resume_clicked(row):
|
||||
download_model.toggle_pause_resume(row)
|
||||
|
||||
delegate = PauseResumeDelegate(self.downloadList)
|
||||
self.downloadList.setItemDelegateForColumn(0, delegate)
|
||||
delegate.clicked.connect(on_pause_resume_clicked)
|
||||
|
||||
# download button triggers
|
||||
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
|
||||
|
||||
container.setLayout(containerLayout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
# Tabs
|
||||
self.tabs = QTabWidget()
|
||||
|
||||
self.horizontal_layout = QHBoxLayout()
|
||||
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
|
||||
self.horizontal_layout.addWidget(self.qtablewidget)
|
||||
|
||||
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
|
||||
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
|
||||
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
|
||||
|
||||
if state.image_path is not None and os.path.exists(state.image_path):
|
||||
self.image = QImage(state.image_path)
|
||||
self.pixmap = QPixmap.fromImage(self.image)
|
||||
self.overlay_label = QLabel(self)
|
||||
self.overlay_label.setPixmap(self.pixmap)
|
||||
self.overlay_label.adjustSize()
|
||||
self.overlay_label.raise_()
|
||||
|
||||
offset_x = -1350
|
||||
offset_y = -550
|
||||
x = self.width() - self.overlay_label.width() - offset_x
|
||||
y = self.height() - self.overlay_label.height() - offset_y
|
||||
self.overlay_label.move(x, y)
|
||||
|
||||
|
||||
# temporarily disabled
|
||||
# state.image_changed.connect(self.update_image_overlay)
|
||||
|
||||
containerLayout.addWidget(self.tabs)
|
||||
|
||||
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
|
||||
toolbar = QToolBar("Main Toolbar")
|
||||
self.addToolBar(toolbar)
|
||||
toolbar.setLayoutDirection(Qt.RightToLeft)
|
||||
|
||||
self.tracker_list = QComboBox()
|
||||
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
|
||||
self.tracker_list.activated.connect(self.set_tracker)
|
||||
|
||||
if darkdetect.isDark():
|
||||
settings_action = QAction(QIcon(get_asset_path("settings_white.png")), "Settings", self)
|
||||
else:
|
||||
settings_action = QAction(QIcon(get_asset_path("settings_black.png")), "Settings", self)
|
||||
|
||||
settings_action.triggered.connect(lambda: settings_dialog(self))
|
||||
toolbar.addAction(settings_action)
|
||||
toolbar.addWidget(self.tracker_list)
|
||||
|
||||
toolbar.setMovable(False)
|
||||
|
||||
# self.progressbar.setValue(0)
|
||||
self.consoleLog.setReadOnly(True)
|
||||
self.consoleLog.setFixedHeight(150)
|
||||
containerLayout.addWidget(self.consoleLog)
|
||||
|
||||
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)
|
||||
|
||||
self.active_timer = QTimer()
|
||||
self.active_timer.timeout.connect(self.show_empty_downloads)
|
||||
self.active_timer.start(500)
|
||||
|
||||
@staticmethod
|
||||
def add_log(text):
|
||||
if MainWindow._instance:
|
||||
MainWindow._instance.log_signal.emit(text)
|
||||
|
||||
def _on_log_signal(self, text):
|
||||
self.consoleLog.append(text)
|
||||
self.consoleLog.verticalScrollBar().setValue(
|
||||
self.consoleLog.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
def update_image_overlay(self, new_image_path):
|
||||
self.image = QImage(new_image_path)
|
||||
self.pixmap = QPixmap.fromImage(self.image)
|
||||
|
||||
self.overlay_label.setPixmap(self.pixmap)
|
||||
self.overlay_label.adjustSize()
|
||||
|
||||
def download_list_update(self):
|
||||
if self.download_model:
|
||||
self.download_model.layoutAboutToBeChanged.emit()
|
||||
self.download_model.layoutChanged.emit()
|
||||
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
closehelper()
|
||||
event.accept()
|
||||
|
||||
def set_tracker(self, _):
|
||||
state.tracker = self.tracker_list.currentText()
|
||||
|
||||
def update_progress(self):
|
||||
progress = dlprogress()
|
||||
self.progressbar.setValue(progress)
|
||||
|
||||
def show_empty_results(self, show: bool):
|
||||
if show:
|
||||
self.qtablewidget.hide()
|
||||
self.emptyResults.show()
|
||||
else:
|
||||
self.qtablewidget.show()
|
||||
self.emptyResults.hide()
|
||||
|
||||
def show_empty_downloads(self):
|
||||
if len(state.downloads) > 0:
|
||||
self.emptyDownload.hide()
|
||||
self.downloadList.show()
|
||||
else:
|
||||
self.emptyDownload.show()
|
||||
self.downloadList.hide()
|
||||
|
||||
# thank you claude
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
table_width = self.qtablewidget.viewport().width()
|
||||
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.data.scrapers.uztracker import scrape_uztracker
|
||||
from core.data.scrapers.rutracker import scrape_rutracker
|
||||
from core.data.scrapers.monkrus import scrape_monkrus_telegram
|
||||
from core.utils.network.jsonhandler import split_data, format_data
|
||||
from core.utils.data.state import state
|
||||
|
||||
def return_pressed(self):
|
||||
search_text = self.searchbar.text()
|
||||
if search_text == "":
|
||||
consoleLog("Error: Can't search for nothing")
|
||||
return
|
||||
consoleLog(f"User searched for: {search_text}")
|
||||
|
||||
if state.tracker == "uztracker":
|
||||
response = scrape_uztracker(search_text)
|
||||
if response:
|
||||
state.post_titles, state.post_urls = response
|
||||
self.qtablewidget.clear()
|
||||
if state.post_titles and len(state.post_titles) > 0:
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||
self.show_empty_results(False)
|
||||
else:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
consoleLog(f"No response from uztracker")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "rutracker":
|
||||
response = scrape_rutracker(search_text)
|
||||
if response:
|
||||
_, state.posts, _, _, cached = split_data(response)
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||
if state.debug == True:
|
||||
consoleLog(f"Response Cached: {cached}")
|
||||
else:
|
||||
consoleLog(f"No response from rutracker")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
|
||||
elif state.tracker == "m0nkrus":
|
||||
state.posts = scrape_monkrus_telegram(search_text)
|
||||
|
||||
if state.posts == []:
|
||||
consoleLog(f"No Results for {search_text}")
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setRowCount(len(state.post_titles))
|
||||
for i, author in enumerate(state.post_author):
|
||||
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
|
||||
for i, title in enumerate(state.post_titles):
|
||||
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
|
||||
@@ -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,109 @@
|
||||
import aria2p
|
||||
import subprocess
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import update_download_completed_by_hash
|
||||
from core.utils.general.logs import consoleLog
|
||||
from plyer import notification
|
||||
import socket
|
||||
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
|
||||
)
|
||||
|
||||
max_checks = 50
|
||||
for check in range(max_checks):
|
||||
try:
|
||||
sock = socket.socket()
|
||||
sock.settimeout(0.1)
|
||||
sock.connect(("localhost", 6800))
|
||||
sock.close()
|
||||
break
|
||||
except(ConnectionRefusedError, socket.timeout):
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError("Aria2 Server failed to start")
|
||||
|
||||
consoleLog("Aria2 Server started")
|
||||
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)
|
||||
|
||||
def update_log(shutdown_event):
|
||||
updated = 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 updated:
|
||||
consoleLog(f"Marking {d.name} as completed")
|
||||
update_download_completed_by_hash(d.info_hash, True)
|
||||
updated.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,18 @@
|
||||
from .aria2_integration import run_aria2p
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
def start_client():
|
||||
global aria2
|
||||
aria2 = run_aria2p()
|
||||
consoleLog("Started Aria2p")
|
||||
|
||||
def add_magnet(uri):
|
||||
if uri is not None and uri.startswith("magnet:?"):
|
||||
aria2.add_magnet(uri)
|
||||
consoleLog("Magnet URI added to Aria2")
|
||||
else:
|
||||
consoleLog(f"Invalid Magnet Link: {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,50 @@
|
||||
import os
|
||||
import platform
|
||||
import configparser
|
||||
from core.utils.data.state import state
|
||||
|
||||
def create_config():
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}",
|
||||
"ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}"}
|
||||
|
||||
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"))
|
||||
|
||||
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||
os.makedirs(state.settings_path, exist_ok=True)
|
||||
|
||||
with open(os.path.join(state.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"))
|
||||
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||
config_file = os.path.join(state.settings_path, "config.yml")
|
||||
|
||||
if not os.path.exists(config_file):
|
||||
create_config()
|
||||
return
|
||||
|
||||
config.read(config_file)
|
||||
|
||||
state.debug = config.getboolean("General", "debug", fallback=state.debug)
|
||||
state.api_url = config.get("General", "api_url", fallback=state.api_url)
|
||||
state.aria2_threads = config.getint("General", "aria2_threads", fallback=state.aria2_threads)
|
||||
state.download_path = config.get("General", "download_path", fallback=state.download_path)
|
||||
state.speed_limit = config.getint("General", "speed_limit", fallback=state.speed_limit)
|
||||
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
|
||||
state.image_path = config.get("General", "image_path", fallback=state.image_path)
|
||||
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
|
||||
|
||||
create_config()
|
||||
@@ -0,0 +1,33 @@
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.shutdown import kill_aria2server
|
||||
from .config import create_config
|
||||
|
||||
def restart_aria2c():
|
||||
import main # had to do this because of circle import :(
|
||||
import signal
|
||||
import atexit
|
||||
kill_aria2server()
|
||||
state.aria2process.wait()
|
||||
state.aria2process = main.run_aria2server()
|
||||
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
|
||||
atexit.unregister(kill_aria2server)
|
||||
atexit.register(kill_aria2server)
|
||||
|
||||
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None, autoresume=None):
|
||||
if thread_count is not None:
|
||||
state.aria2_threads = thread_count
|
||||
if apiurl is not None:
|
||||
state.api_url = apiurl
|
||||
if download_path is not None:
|
||||
state.download_path = download_path
|
||||
if speed_limit is not None:
|
||||
state.speed_limit = speed_limit
|
||||
if image_path is not None:
|
||||
state.image_path = image_path
|
||||
if autoresume is not None:
|
||||
state.autoresume = autoresume
|
||||
|
||||
|
||||
create_config()
|
||||
restart_aria2c()
|
||||
close()
|
||||
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Download:
|
||||
title: str
|
||||
url: str
|
||||
magnet_uri: str
|
||||
completed: bool
|
||||
|
||||
@dataclass
|
||||
class DownloadList:
|
||||
count: int
|
||||
data: List[Download]
|
||||
@@ -0,0 +1,40 @@
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from typing import Optional, Any, List, Dict
|
||||
from pathlib import Path
|
||||
|
||||
class AppState(QObject):
|
||||
image_changed = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.posts: List[Dict[str, Any]] = []
|
||||
self.post_titles: List[str] = []
|
||||
self.post_urls: List[str] = []
|
||||
self.post_author: List[str] = []
|
||||
self.downloads: List[str] = []
|
||||
self.version: str = "dev"
|
||||
self._image_path: str = ""
|
||||
self.ignore_updates: bool = False
|
||||
self.debug: bool = False
|
||||
self.autoresume: bool = True
|
||||
self.tracker: str = "rutracker"
|
||||
self.api_url: str = "https://api.michijackson.xyz"
|
||||
self.download_path: str = str(Path.home() / "Downloads")
|
||||
self.speed_limit: int = 0
|
||||
self.aria2process: Optional[Any] = None
|
||||
self.aria2: Any = None
|
||||
self.aria2p: Any = None
|
||||
self.aria2_threads: int = 4
|
||||
self.settings_path: str = None
|
||||
|
||||
@property
|
||||
def image_path(self) -> str:
|
||||
return self._image_path
|
||||
|
||||
@image_path.setter
|
||||
def image_path(self, new_path: str):
|
||||
if new_path != self._image_path:
|
||||
self._image_path = new_path
|
||||
self.image_changed.emit(new_path)
|
||||
|
||||
state = AppState()
|
||||
@@ -0,0 +1,46 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.network.jsonhandler import format_data
|
||||
|
||||
|
||||
|
||||
def get_item_url(item, posts, post_titles): # softwarelist currentitem, post list (dict), post titles list
|
||||
post_index = post_titles.index(item)
|
||||
if 0 <= post_index < len(post_titles):
|
||||
if state.tracker == "uztracker":
|
||||
item = "https://uztracker.net/" + state.post_urls[post_index].lstrip("./")
|
||||
consoleLog(f"Found post URL: {item}")
|
||||
return item
|
||||
if state.tracker == "rutracker":
|
||||
item_dict = posts[post_index]
|
||||
_, post_links, _ = format_data([item_dict])
|
||||
consoleLog(f"Found post URL: {post_links[0]}")
|
||||
return post_links[0]
|
||||
if state.tracker == "m0nkrus":
|
||||
item_dict = posts[post_index]
|
||||
_, post_links, _ = format_data([item_dict])
|
||||
consoleLog(f"Found post URL: {post_links[0]}")
|
||||
return post_links[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def get_magnet_link(post_url):
|
||||
try:
|
||||
response = requests.get(post_url) # eventually impl. cloudscraper
|
||||
consoleLog("Sent Request to retrieve Magnet Link...")
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
magnet_link = soup.find('a', href=lambda x: x and x.startswith('magnet:'))
|
||||
if magnet_link:
|
||||
consoleLog(f"Magnet Link Retrieved: {magnet_link['href']}")
|
||||
return magnet_link['href']
|
||||
else:
|
||||
consoleLog("Magnet Link not Found!")
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Failed to fetch {post_url}: {e}")
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.network.download import run_download_direct
|
||||
|
||||
def split_data(data):
|
||||
|
||||
count = data.count
|
||||
downloads = data.data
|
||||
|
||||
return count, downloads
|
||||
|
||||
def check_completed(downloads, resume):
|
||||
for download in downloads:
|
||||
if download.completed == False:
|
||||
consoleLog(f"Found unfinished download: {download.title}")
|
||||
if resume == True:
|
||||
run_download_direct(download.magnet_uri)
|
||||
consoleLog(f"Resuming {download.title}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
from core.utils.data.models import Download, DownloadList
|
||||
from core.utils.data.state import state
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
try:
|
||||
with open(downloads_file, "r") as file:
|
||||
existing_data = json.load(file)
|
||||
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||
except json.JSONDecodeError:
|
||||
downloads = []
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
|
||||
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
|
||||
consoleLog("Skipped Logging, download already in file")
|
||||
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
|
||||
|
||||
|
||||
downloads.append(Download(
|
||||
title=title,
|
||||
url=url,
|
||||
magnet_uri=magnet_uri,
|
||||
completed=completed,
|
||||
))
|
||||
|
||||
consoleLog(f"Added {title} to Log File")
|
||||
|
||||
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||
with open(downloads_file, "w") as file:
|
||||
json.dump(asdict(download_list), file, indent=4)
|
||||
|
||||
return download_list
|
||||
|
||||
def update_download_completed(magnet_uri, completed) -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
try:
|
||||
with open(downloads_file, "r") as file:
|
||||
existing_data = json.load(file)
|
||||
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
consoleLog(f"Error loading downloads.json: {e}")
|
||||
downloads = []
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
identifier = (magnet_uri or "").strip()
|
||||
if state.debug:
|
||||
consoleLog(f"Updating download completed for identifier: {identifier!r}")
|
||||
consoleLog(f"Total downloads in file: {len(downloads)}")
|
||||
for i, d in enumerate(downloads):
|
||||
try:
|
||||
mag = (getattr(d, 'magnet_uri', None) or '')[:50]
|
||||
url = (getattr(d, 'url', None) or '')[:50]
|
||||
consoleLog(f" [{i}] magnet: {mag}... url: {url}...")
|
||||
except Exception as e:
|
||||
consoleLog(f" [{i}] Error reading download: {e}")
|
||||
|
||||
found = False
|
||||
for download in downloads:
|
||||
try:
|
||||
stored_magnet = (getattr(download, 'magnet_uri', None) or "").strip()
|
||||
stored_url = (getattr(download, 'url', None) or "").strip()
|
||||
consoleLog(f"Comparing with magnet: {stored_magnet[:50] if stored_magnet else 'None'}...")
|
||||
if identifier and (stored_magnet == identifier or stored_url == identifier):
|
||||
consoleLog(f"Match found! Setting completed={completed}")
|
||||
download.completed = completed
|
||||
found = True
|
||||
except Exception as e:
|
||||
consoleLog(f"Error comparing download: {e}")
|
||||
|
||||
if not found:
|
||||
consoleLog("No matching download found to update")
|
||||
return DownloadList(data=downloads, count=len(downloads))
|
||||
|
||||
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||
with open(downloads_file, "w") as file:
|
||||
json.dump(asdict(download_list), file, indent=4)
|
||||
|
||||
consoleLog("Updated download log")
|
||||
|
||||
return download_list
|
||||
|
||||
|
||||
def get_download_logs() -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
try:
|
||||
with open(downloads_file, "r") as file:
|
||||
existing_data = json.load(file)
|
||||
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||
except json.JSONDecodeError:
|
||||
downloads = []
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
return DownloadList(data=downloads, count=len(downloads))
|
||||
|
||||
|
||||
def extract_hash_from_magnet(magnet_uri): # full credits to claude for this
|
||||
match = re.search(r'urn:btih:([A-F0-9]+)', magnet_uri, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).upper()
|
||||
return None
|
||||
|
||||
|
||||
def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
|
||||
if os.path.exists(downloads_file) and os.path.getsize(downloads_file) > 0:
|
||||
try:
|
||||
with open(downloads_file, "r") as file:
|
||||
existing_data = json.load(file)
|
||||
downloads = [Download(**d) for d in existing_data.get("data", [])]
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
consoleLog(f"Error loading downloads.json: {e}")
|
||||
downloads = []
|
||||
else:
|
||||
downloads = []
|
||||
|
||||
info_hash_upper = (info_hash or "").upper().strip()
|
||||
consoleLog(f"Updating download by hash: {info_hash_upper}")
|
||||
consoleLog(f"Total downloads in file: {len(downloads)}")
|
||||
|
||||
found = False
|
||||
for download in downloads:
|
||||
try:
|
||||
magnet_uri = getattr(download, 'magnet_uri', None)
|
||||
if magnet_uri:
|
||||
stored_hash = extract_hash_from_magnet(magnet_uri)
|
||||
if stored_hash and stored_hash == info_hash_upper:
|
||||
download.completed = completed
|
||||
found = True
|
||||
except Exception as e:
|
||||
consoleLog(f"Error comparing download: {e}")
|
||||
|
||||
if not found:
|
||||
consoleLog("No matching download found to update")
|
||||
return DownloadList(data=downloads, count=len(downloads))
|
||||
|
||||
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||
with open(downloads_file, "w") as file:
|
||||
json.dump(asdict(download_list), file, indent=4)
|
||||
|
||||
consoleLog("Updated download log")
|
||||
|
||||
return download_list
|
||||
|
||||
|
||||
_main_window = None
|
||||
|
||||
def set_main_window(window):
|
||||
global _main_window
|
||||
_main_window = window
|
||||
|
||||
def consoleLog(text, printAnyways = False):
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M:%S")
|
||||
|
||||
try:
|
||||
from core.interface.gui import MainWindow
|
||||
MainWindow.add_log(f"[{current_time}] {text}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if state.debug or printAnyways:
|
||||
print(f"[{current_time}] {text}")
|
||||
@@ -0,0 +1,30 @@
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
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()
|
||||
consoleLog("Killed Aria2")
|
||||
|
||||
try:
|
||||
for proc in psutil.process_iter():
|
||||
if proc.name() == process:
|
||||
proc.kill()
|
||||
consoleLog(f"Killed Aria2c (PID {proc.pid})")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
def closehelper():
|
||||
shutdown_event.set()
|
||||
kill_aria2server()
|
||||
@@ -0,0 +1,2 @@
|
||||
def run_thread(thread):
|
||||
thread.start()
|
||||
@@ -0,0 +1,33 @@
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.data.tracker import get_item_url
|
||||
from core.utils.data.tracker import get_magnet_link
|
||||
from core.network.aria2_wrapper import start_client
|
||||
from core.network.aria2_wrapper import add_magnet
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.general.logs import add_download_log
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
def download_selected(item, posts, post_titles):
|
||||
if item is not None:
|
||||
consoleLog(f"Downloading {item.text()}")
|
||||
run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
|
||||
else:
|
||||
consoleLog("No item selected for download.")
|
||||
|
||||
def run_download(item, posts, post_titles):
|
||||
post_url = get_item_url(item, posts, post_titles)
|
||||
consoleLog(f"Selected URL: {post_url}")
|
||||
magnet_uri = get_magnet_link(post_url)
|
||||
start_client()
|
||||
add_download_log(item, post_url, magnet_uri, False)
|
||||
add_magnet(magnet_uri)
|
||||
|
||||
def run_download_direct(magnet_uri):
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
import requests
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
|
||||
def check_for_updates():
|
||||
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases"
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
consoleLog(f"Failed to fetch releases: {response.status_code}")
|
||||
exit(1)
|
||||
|
||||
releases = response.json()
|
||||
releases.sort(key=lambda r: r["published_at"], reverse=True)
|
||||
|
||||
latest_release = releases[0]
|
||||
latest_version = latest_release["name"]
|
||||
assets = latest_release["assets"]
|
||||
|
||||
|
||||
if state.version == "dev":
|
||||
consoleLog("Dev release detected, skipping version check")
|
||||
return None, None
|
||||
elif latest_version != state.version:
|
||||
consoleLog(f"New release available: {latest_version}")
|
||||
if assets:
|
||||
consoleLog("Assets:")
|
||||
for asset in assets:
|
||||
consoleLog(f" - {asset['name']}: {asset['browser_download_url']}")
|
||||
return assets, latest_version
|
||||
else:
|
||||
return None, None
|
||||
else:
|
||||
consoleLog("Already up-to-date.")
|
||||
return None, None
|
||||
@@ -1,102 +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:"?
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
search_input = input("Enter the Name of the Program you want to search for: ")
|
||||
selected = scrape_uztracker(search_input)
|
||||
maintitle = get_post_title(selected)
|
||||
magnetlink = get_magnet_link(selected)
|
||||
print(maintitle)
|
||||
print(magnetlink)
|
||||
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
from core.gui import run_gui
|
||||
from core.interface.gui import MainWindow
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.network.aria2_integration import aria2server
|
||||
from core.network.aria2_integration import send_notification, update_log
|
||||
from core.utils.general.logs import get_download_logs
|
||||
from core.utils.general.shutdown import closehelper, shutdown_event
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.general.loghandler import split_data, check_completed
|
||||
from core.utils.config.config import read_config
|
||||
from PySide6 import QtWidgets
|
||||
import qdarktheme
|
||||
import darkdetect
|
||||
import threading
|
||||
import signal
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
def run_gui():
|
||||
app = QtWidgets.QApplication([])
|
||||
if darkdetect.isDark:
|
||||
app.setStyleSheet(qdarktheme.load_stylesheet("dark"))
|
||||
else:
|
||||
app.setStyleSheet(qdarktheme.load_stylesheet("light"))
|
||||
widget = MainWindow()
|
||||
|
||||
from core.utils.general.logs import set_main_window
|
||||
set_main_window(widget)
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
def run_aria2server():
|
||||
aria2process = aria2server()
|
||||
return aria2process
|
||||
|
||||
def keyboardinterrupthandler(signum, frame):
|
||||
closehelper()
|
||||
|
||||
if __name__ == "__main__":
|
||||
read_config()
|
||||
logs = get_download_logs()
|
||||
count, downloads = split_data(logs)
|
||||
if args.debug:
|
||||
state.debug = args.debug # override of read_config
|
||||
consoleLog("Starting Aria2 Server")
|
||||
state.aria2process = run_aria2server()
|
||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
||||
consoleLog("Started send_notification thread")
|
||||
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
|
||||
consoleLog("Started update_log thread")
|
||||
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
|
||||
consoleLog("Started check_completed thread")
|
||||
consoleLog("Launching GUI")
|
||||
run_gui()
|
||||
|
||||
run_gui()
|
||||
@@ -0,0 +1,10 @@
|
||||
requests==2.32.2
|
||||
PySide6==6.10.1
|
||||
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
|
||||
Reference in New Issue
Block a user