mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8111ac73 | |||
| e5b809d6d4 | |||
| b573ca8fed | |||
| 8a45d71655 | |||
| 0c7bc426db | |||
| 1f06a19c17 | |||
| da92c4ab0a | |||
| f6e3ff4f27 | |||
| 88042b2d3d | |||
| 9265a9cd1c | |||
| adc9f6f6d4 | |||
| a3a6ef5eec | |||
| 8e5e0425ef | |||
| c2b329aed4 | |||
| c5f2be90af | |||
| 695b80c6de | |||
| b7188a515a | |||
| 7b2522438c | |||
| 0686c7aec2 | |||
| 6be1784903 | |||
| 4589322f0a | |||
| 8132d789ef | |||
| 3f39e67933 | |||
| d43d6e57cc | |||
| 28cffe59f5 | |||
| f1726f4e9a | |||
| b7ca895aeb | |||
| e95ad11672 | |||
| f2a79e5eed | |||
| 990135450f | |||
| 62c6afe350 | |||
| 1aaf56a4dc | |||
| 47b643d68b | |||
| de1c8577d6 | |||
| c835b8375c | |||
| e7291e8e99 | |||
| a4efe9185a | |||
| 86b4f023ac | |||
| 006da55aa0 | |||
| 214a389ad3 | |||
| d4005d7e86 | |||
| 08a45cb422 | |||
| c49726fe42 | |||
| ecc117dedc | |||
| c51fc9ea4a | |||
| 48ed11c248 |
+91
-26
@@ -15,7 +15,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.12"
|
||||
APP_NAME: "SoftwareManager"
|
||||
|
||||
jobs:
|
||||
@@ -57,7 +57,13 @@ jobs:
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install pyinstaller backports.tarfile pillow
|
||||
uv pip install Pillow
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
- name: Install Nuitka (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
run: uv pip install nuitka
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
@@ -82,28 +88,53 @@ jobs:
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Build executable with PyInstaller (Windows)
|
||||
- name: Install UPX (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 "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 }} \
|
||||
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
|
||||
7z x upx.zip -oupx-dir
|
||||
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
|
||||
echo "$UPX_DIR" >> $GITHUB_PATH
|
||||
export PATH="$UPX_DIR:$PATH"
|
||||
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
|
||||
upx --version | head -1
|
||||
|
||||
- name: Convert icon for Windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -c "
|
||||
from PIL import Image
|
||||
img = Image.open('core/interface/assets/logo.png')
|
||||
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
|
||||
print('Icon converted successfully')
|
||||
"
|
||||
|
||||
- name: Build with Nuitka (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -m nuitka \
|
||||
--onefile \
|
||||
--onefile-no-compression \
|
||||
--onefile-tempdir-spec="{CACHE_DIR}/${{ env.APP_NAME }}/${{ env.VERSION }}" \
|
||||
--assume-yes-for-downloads \
|
||||
--windows-console-mode=disable \
|
||||
--windows-icon-from-ico=app_icon.ico \
|
||||
--enable-plugin=pyside6 \
|
||||
--enable-plugin=upx \
|
||||
--upx-binary="${{ env.UPX_BINARY }}" \
|
||||
--noinclude-qt-translations \
|
||||
--include-data-dir=core/interface/assets=core/interface/assets \
|
||||
--include-data-files=build_info.json=build_info.json \
|
||||
--include-data-files=build_info.json=core/build_info.json \
|
||||
--include-data-files=build_info.json=core/interface/build_info.json \
|
||||
--output-filename=${{ env.APP_NAME }}.exe \
|
||||
main.py
|
||||
|
||||
- name: Build executable with PyInstaller (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
- name: Build with PyInstaller (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile --windowed \
|
||||
@@ -122,22 +153,43 @@ jobs:
|
||||
--name ${{ env.APP_NAME }} \
|
||||
main.py
|
||||
|
||||
- name: Make executable (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: chmod +x dist/${{ env.APP_NAME }}
|
||||
- name: Build with PyInstaller (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile \
|
||||
--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: Rename artifact
|
||||
shell: bash
|
||||
run: |
|
||||
cd dist
|
||||
mv ${{ env.APP_NAME }}${{ runner.os == 'Windows' && '.exe' || '' }} ${{ matrix.artifact_name }}
|
||||
ls -la
|
||||
mkdir -p dist-final
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
mv ${{ env.APP_NAME }}.exe dist-final/${{ matrix.artifact_name }}
|
||||
else
|
||||
chmod +x dist/${{ env.APP_NAME }}
|
||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
|
||||
fi
|
||||
ls -la dist-final/
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-build
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
path: dist-final/${{ matrix.artifact_name }}
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
@@ -162,9 +214,16 @@ jobs:
|
||||
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
|
||||
ls -la release/
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd release
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
cat SHA256SUMS.txt
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
|
||||
CHECKSUMS=$(cat release/SHA256SUMS.txt)
|
||||
cat > release_notes.md << EOF
|
||||
## Development Build
|
||||
|
||||
@@ -177,6 +236,11 @@ jobs:
|
||||
- Linux: \`SoftwareManager-dev-${SHORT_SHA}-linux\`
|
||||
- Windows: \`SoftwareManager-dev-${SHORT_SHA}-windows.exe\`
|
||||
- macOS: \`SoftwareManager-dev-${SHORT_SHA}-macos\`
|
||||
|
||||
### SHA-256 Checksums
|
||||
\`\`\`
|
||||
${CHECKSUMS}
|
||||
\`\`\`
|
||||
EOF
|
||||
cat release_notes.md
|
||||
|
||||
@@ -190,5 +254,6 @@ jobs:
|
||||
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
|
||||
release/SHA256SUMS.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# SoftwareManager - A Download Manager/Launcher
|
||||
SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and managing free software from various sources.
|
||||
# SoftwareManager
|
||||
SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker and the official M0nkrus Telegram channel.
|
||||
|
||||
## Features
|
||||
- VPN Binding (impl. soon)
|
||||
- Bandwidth/Download speed limiting
|
||||
- Start downloads via a predefined magnet URI.
|
||||
- View detailed download information (name, speed, and status).
|
||||
- Searching & Downloading Software from various pages
|
||||
- Network Interface Binding
|
||||
- Simple UI, Speed Limiting etc.
|
||||
- No Logins required
|
||||
|
||||
## Installation
|
||||
1. Download the binary file for your operating system from releases
|
||||
@@ -14,19 +14,44 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
||||
|
||||
## Manual Installation
|
||||
1. Ensure Python 3.x is installed on your system.
|
||||
2. Clone the Repository:
|
||||
```bash
|
||||
git clone https://github.com/KeksPirates/SoftwareManager.git && cd SoftwareManager
|
||||
```
|
||||
2. Install Dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Run main.py:
|
||||
```bash
|
||||
python3 main.py
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Notes
|
||||
- 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.
|
||||
## Additional Information
|
||||
- SoftwareManager is still in Development and most likely contains issues. Please report any bugs you find via the Issues tab. Contributions are also greatly appreciated.
|
||||
- MacOS Builds haven't been properly tested - We're thankful to receive feedback.
|
||||
- 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 the server branch.
|
||||
|
||||
## Python Dependencies
|
||||
(also included in requirements.txt)
|
||||
|
||||
```bash
|
||||
requests (2.32.2)
|
||||
PySide6 (6.10.1)
|
||||
beautifulsoup (44.13.5)
|
||||
darkdetect (0.7.1)
|
||||
pyinstaller (6.15.0)
|
||||
PyQtDarkTheme-fork (2.3.4)
|
||||
aiohttp (3.13.0)
|
||||
plyer (2.1.0)
|
||||
psutil (7.1.0)
|
||||
libtorrent (2.0.11)
|
||||
libtorrent-windows-dll (0.0.3)
|
||||
```
|
||||
|
||||
## Activity
|
||||
|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
@@ -38,6 +63,11 @@ SoftwareManager is a Python-based GUI/TUI tool that simplifies downloading and m
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/KeksPirates/SoftwareManager/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=KeksPirates/SoftwareManager" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
**Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool.
|
||||
|
||||
@@ -4,7 +4,7 @@ from core.utils.general.logs import consoleLog
|
||||
|
||||
|
||||
def scrape_rutracker(search_text):
|
||||
search = requests.get(f"{state.api_url}/search/{search_text}")
|
||||
search = requests.get(f"{state.api_url}/search?q={search_text}")
|
||||
consoleLog("Sent request to server")
|
||||
if search:
|
||||
try:
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import requests
|
||||
import asyncio
|
||||
import threading
|
||||
from bs4 import BeautifulSoup
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.general.wrappers import run_thread
|
||||
|
||||
|
||||
global url_uztracker
|
||||
url_uztracker = "https://uztracker.net/tracker.php?nm="
|
||||
|
||||
async def init_uztracker():
|
||||
def init_uztracker():
|
||||
global up
|
||||
global soup
|
||||
try:
|
||||
@@ -19,12 +21,12 @@ async def init_uztracker():
|
||||
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(f"Request Exception on {url_uztracker}:")
|
||||
consoleLog(str(e))
|
||||
consoleLog("\nIs the Site down?")
|
||||
consoleLog("Is the Site down?")
|
||||
up = False
|
||||
|
||||
asyncio.run(init_uztracker())
|
||||
run_thread(threading.Thread(target=init_uztracker))
|
||||
|
||||
def scrape_uztracker(search):
|
||||
if up:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from core.utils.config.settings import save_settings
|
||||
from core.utils.data.state import state
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.interface.utils.tabhelper import general_tab
|
||||
from core.interface.utils.tabhelper import paths_tab
|
||||
from core.interface.utils.tabhelper import network_tab
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
@@ -13,6 +16,8 @@ from PySide6.QtWidgets import (
|
||||
QSpinBox,
|
||||
QCheckBox,
|
||||
QFileDialog,
|
||||
QComboBox,
|
||||
QTabWidget,
|
||||
)
|
||||
import platform
|
||||
|
||||
@@ -22,10 +27,9 @@ def settings_dialog(self):
|
||||
consoleLog("Settings dialog opened")
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Settings")
|
||||
dialog.setFixedSize(800, 550)
|
||||
dialog.setFixedSize(700, 450)
|
||||
|
||||
dialog.setLayout(QVBoxLayout())
|
||||
dialog.layout().addWidget(QLabel("Settings"))
|
||||
|
||||
def close_settings():
|
||||
dialog.reject()
|
||||
@@ -56,39 +60,26 @@ def settings_dialog(self):
|
||||
|
||||
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)
|
||||
|
||||
#####################
|
||||
# DOWNLOAD SETTINGS #
|
||||
#####################
|
||||
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()
|
||||
# transparent window checkbox
|
||||
transparent_window_container = QWidget()
|
||||
transparent_window_layout = QHBoxLayout()
|
||||
|
||||
thread_layout.addWidget(QLabel("Threads:"))
|
||||
thread_layout.addWidget(thread_box)
|
||||
thread_container.setLayout(thread_layout)
|
||||
thread_container.setMaximumHeight(80)
|
||||
|
||||
transparent_window_checkbox = QCheckBox()
|
||||
transparent_window_container.setLayout(transparent_window_layout)
|
||||
transparent_window_layout.addWidget(QLabel("Window Transparency (requires restart) (Linux/MacOS only): "))
|
||||
|
||||
# Dimensions
|
||||
thread_box.setFixedWidth(90)
|
||||
thread_box.setFixedHeight(30)
|
||||
|
||||
dialog.layout().addWidget(thread_container)
|
||||
transparent_window_layout.addStretch()
|
||||
transparent_window_checkbox.setChecked(state.window_transparency)
|
||||
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
|
||||
transparent_window_layout.addWidget(transparent_window_checkbox)
|
||||
|
||||
##################
|
||||
# SERVER SETTING #
|
||||
##################
|
||||
|
||||
|
||||
api_url_container = QWidget()
|
||||
api_url_layout = QHBoxLayout()
|
||||
|
||||
@@ -98,7 +89,6 @@ def settings_dialog(self):
|
||||
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 #
|
||||
@@ -113,7 +103,6 @@ def settings_dialog(self):
|
||||
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)
|
||||
|
||||
def browse_download_path():
|
||||
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
|
||||
@@ -124,6 +113,29 @@ def settings_dialog(self):
|
||||
download_path_layout.addWidget(browse_button)
|
||||
browse_button.clicked.connect(browse_download_path)
|
||||
|
||||
###############
|
||||
# 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)
|
||||
|
||||
def browse_image_path():
|
||||
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
|
||||
if file_path:
|
||||
image_path.setText(file_path)
|
||||
|
||||
browse_button = QPushButton("📁")
|
||||
image_path_layout.addWidget(browse_button)
|
||||
browse_button.clicked.connect(browse_image_path)
|
||||
|
||||
##################
|
||||
# SPEED LIMITING #
|
||||
##################
|
||||
@@ -140,8 +152,7 @@ def settings_dialog(self):
|
||||
down_speed_limit_layout.addWidget(down_speed_limit)
|
||||
down_speed_limit.setFixedWidth(180)
|
||||
down_speed_limit.setFixedHeight(30)
|
||||
dialog.layout().addWidget(down_speed_limit_container)
|
||||
|
||||
|
||||
up_speed_limit_container = QWidget()
|
||||
up_speed_limit_layout = QHBoxLayout()
|
||||
|
||||
@@ -154,7 +165,6 @@ def settings_dialog(self):
|
||||
up_speed_limit_layout.addWidget(up_speed_limit)
|
||||
up_speed_limit.setFixedWidth(180)
|
||||
up_speed_limit.setFixedHeight(30)
|
||||
dialog.layout().addWidget(up_speed_limit_container)
|
||||
|
||||
######################
|
||||
# CONNECTION CONFIGS #
|
||||
@@ -172,7 +182,6 @@ def settings_dialog(self):
|
||||
max_connections_layout.addWidget(max_connections)
|
||||
max_connections.setFixedWidth(180)
|
||||
max_connections.setFixedHeight(30)
|
||||
dialog.layout().addWidget(max_connections_container)
|
||||
|
||||
####################
|
||||
# DOWNLOAD CONFIGS #
|
||||
@@ -190,32 +199,34 @@ def settings_dialog(self):
|
||||
max_downloads_layout.addWidget(max_downloads)
|
||||
max_downloads.setFixedWidth(180)
|
||||
max_downloads.setFixedHeight(30)
|
||||
dialog.layout().addWidget(max_downloads_container)
|
||||
|
||||
#####################
|
||||
# INTERFACE BINDING #
|
||||
#####################
|
||||
|
||||
###############
|
||||
# IMAGE PATH #
|
||||
###############
|
||||
interface_container = QWidget()
|
||||
interface_layout = QHBoxLayout()
|
||||
|
||||
image_path_container = QWidget()
|
||||
image_path_layout = QHBoxLayout()
|
||||
interface_label = QLabel("Network Interface:")
|
||||
interface_layout.addWidget(interface_label)
|
||||
interface_select = QComboBox()
|
||||
interface_select.addItems(["None"] + state.interfaces)
|
||||
|
||||
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)
|
||||
target = state.bound_interface if state.bound_interface else "None"
|
||||
|
||||
def browse_image_path():
|
||||
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
|
||||
if file_path:
|
||||
image_path.setText(file_path)
|
||||
index = interface_select.findText(target)
|
||||
if index >= 0:
|
||||
interface_select.setCurrentIndex(index)
|
||||
else:
|
||||
interface_select.setCurrentIndex(0)
|
||||
|
||||
interface_select.setFixedWidth(180)
|
||||
interface_select.setFixedHeight(30)
|
||||
interface_select.setFixedWidth(180)
|
||||
interface_select.setFixedHeight(30)
|
||||
interface_layout.addWidget(interface_select)
|
||||
interface_container.setLayout(interface_layout)
|
||||
|
||||
browse_button = QPushButton("📁")
|
||||
image_path_layout.addWidget(browse_button)
|
||||
browse_button.clicked.connect(browse_image_path)
|
||||
|
||||
###############
|
||||
# SAVE/CANCEL #
|
||||
@@ -225,12 +236,28 @@ def settings_dialog(self):
|
||||
|
||||
save_btn = QPushButton("Save")
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
save_btn.clicked.connect(lambda: save_settings(close_settings, api_url.text(), download_path.text(), down_speed_limit.value(), up_speed_limit.value(), image_path.text(), None, max_connections.value(), max_downloads.value()))
|
||||
save_btn.clicked.connect(lambda: save_settings(
|
||||
close_settings,
|
||||
api_url.text(),
|
||||
download_path.text(),
|
||||
down_speed_limit.value(),
|
||||
up_speed_limit.value(),
|
||||
image_path.text(),
|
||||
autoresume_checkbox.isChecked(),
|
||||
max_connections.value(),
|
||||
max_downloads.value(),
|
||||
interface_select.currentText()))
|
||||
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
layout.addWidget(cancel_btn)
|
||||
layout.addWidget(save_btn)
|
||||
|
||||
self.tabs = QTabWidget()
|
||||
self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs)
|
||||
self.tab2 = paths_tab("Paths", download_path_container, image_path_container, self.tabs)
|
||||
self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs)
|
||||
|
||||
dialog.layout().addWidget(self.tabs)
|
||||
dialog.layout().addLayout(layout)
|
||||
|
||||
|
||||
dialog.exec()
|
||||
|
||||
+231
-92
@@ -1,12 +1,11 @@
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent
|
||||
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QTableView,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QListWidget,
|
||||
QToolBar,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QComboBox,
|
||||
@@ -16,13 +15,10 @@ from PySide6.QtWidgets import (
|
||||
QMessageBox,
|
||||
QTableWidget,
|
||||
QTextEdit,
|
||||
QStyle,
|
||||
QStyleOptionButton,
|
||||
QStyledItemDelegate,
|
||||
QApplication,
|
||||
)
|
||||
|
||||
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
|
||||
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent
|
||||
import darkdetect
|
||||
import threading
|
||||
import platform
|
||||
@@ -33,7 +29,8 @@ import libtorrent as lt
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
from core.utils.general.logs import consoleLog
|
||||
import asyncio
|
||||
from core.utils.general.logs import consoleLog, remove_download_log, flush_log_buffer
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.utils.data.state import state
|
||||
from core.utils.network.download import download_selected
|
||||
@@ -77,15 +74,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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)
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
asset_path = os.path.join(base_path, 'assets', filename)
|
||||
if os.path.exists(asset_path):
|
||||
return asset_path
|
||||
|
||||
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||
asset_path = os.path.join(sys._MEIPASS, '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")))
|
||||
@@ -106,23 +104,22 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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 state.ignore_updates is False and 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)
|
||||
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)
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(latest_version)
|
||||
|
||||
self.setWindowTitle("Software Manager")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
@@ -138,39 +135,44 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.libraryList = QListWidget()
|
||||
|
||||
self.emptyResults = QLabel("No Results")
|
||||
self.emptyResults.setAlignment(Qt.AlignCenter)
|
||||
self.emptyResults.setAlignment(Qt.AlignmentFlag.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.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.consoleLog = QTextEdit()
|
||||
self.progressbar = QProgressBar()
|
||||
|
||||
flush_log_buffer()
|
||||
|
||||
# Table Widget for Item List
|
||||
self.qtablewidget = QTableWidget()
|
||||
|
||||
self.qtablewidget.setColumnCount(2)
|
||||
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
|
||||
self.qtablewidget.setColumnCount(4)
|
||||
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.qtablewidget.verticalHeader().setVisible(False)
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
|
||||
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
header.setStretchLastSection(False)
|
||||
|
||||
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.qtablewidget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.qtablewidget.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
header = self.qtablewidget.horizontalHeader()
|
||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
header.resizeSection(1, 500)
|
||||
|
||||
container = QWidget()
|
||||
@@ -189,32 +191,31 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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:
|
||||
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:
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole:
|
||||
col = index.column()
|
||||
|
||||
if index.row() >= len(state.active_downloads) or index.row() < 0:
|
||||
return None
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
|
||||
status = magnetdl.status()
|
||||
|
||||
if col == 0:
|
||||
is_paused = magnetdl.status().paused
|
||||
return "▶︎" if is_paused else "⏸︎"
|
||||
pass
|
||||
elif col == 1:
|
||||
return status.name if status.has_metadata else "Fetching metadata..."
|
||||
elif col == 2:
|
||||
if status.state == lt.torrent_status.downloading:
|
||||
if status.state == lt.torrent_status.downloading:
|
||||
return "Downloading"
|
||||
elif status.state == lt.torrent_status.seeding:
|
||||
elif status.state == lt.torrent_status.seeding:
|
||||
return "Seeding"
|
||||
elif status.paused:
|
||||
return "Paused"
|
||||
@@ -258,8 +259,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
else:
|
||||
return "∞" if status.paused else "Stalled"
|
||||
|
||||
if role == Qt.UserRole and index.column() == 0:
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
if role == Qt.UserRole and index.column() == 0:
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
return magnetdl.status().paused
|
||||
|
||||
@@ -270,24 +271,28 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
if row >= len(state.active_downloads) or row < 0:
|
||||
return
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
status = magnetdl.status()
|
||||
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
if state.download_path is not None and os.path.exists(state.download_path):
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
save_path = magnetdl.save_path()
|
||||
if save_path and os.path.exists(save_path):
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(os.path.normpath(state.download_path))
|
||||
os.startfile(os.path.normpath(save_path))
|
||||
elif platform.system() == "Linux":
|
||||
subprocess.Popen(["xdg-open", state.download_path])
|
||||
subprocess.Popen(["xdg-open", save_path])
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.Popen(["open", state.download_path])
|
||||
subprocess.Popen(["open", save_path])
|
||||
return
|
||||
|
||||
if status.paused:
|
||||
is_paused = bool(magnetdl.flags() & lt.torrent_flags.paused)
|
||||
if is_paused:
|
||||
magnetdl.set_flags(lt.torrent_flags.auto_managed)
|
||||
magnetdl.resume()
|
||||
consoleLog(f"Resumed download: {status.name}", True)
|
||||
else:
|
||||
magnetdl.unset_flags(lt.torrent_flags.auto_managed)
|
||||
magnetdl.pause()
|
||||
consoleLog(f"Paused download: {status.name}", True)
|
||||
|
||||
@@ -299,23 +304,19 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
button = editor.findChild(QtWidgets.QPushButton)
|
||||
|
||||
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
status = magnetdl.status()
|
||||
|
||||
if button:
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
button.setText("📁")
|
||||
else:
|
||||
button.setText("▶︎" if status.paused else "⏸︎")
|
||||
|
||||
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
status = magnetdl.status()
|
||||
|
||||
@@ -323,7 +324,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
widget.setStyleSheet("border: none;")
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
btnPause = QtWidgets.QPushButton("📁")
|
||||
else:
|
||||
btnPause = QtWidgets.QPushButton("▶︎" if status.paused else "⏸︎")
|
||||
@@ -334,7 +335,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
layout.addStretch()
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress:
|
||||
self.clicked.emit(index.row())
|
||||
@@ -344,10 +344,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
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)
|
||||
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
download_model = self.download_model
|
||||
|
||||
@@ -366,56 +366,92 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
|
||||
# Tabs
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setStyleSheet("""
|
||||
QTabBar::tab {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
height: 25px;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
margin-top: -20px;
|
||||
}
|
||||
""")
|
||||
|
||||
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.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.image = self.image.scaledToWidth(300, Qt.SmoothTransformation)
|
||||
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
|
||||
# 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)
|
||||
|
||||
self.corner_widget = QWidget()
|
||||
self.corner_layout = QHBoxLayout(self.corner_widget)
|
||||
self.corner_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
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.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.tracker_list.activated.connect(self.set_tracker)
|
||||
self.corner_layout.addWidget(self.tracker_list)
|
||||
|
||||
self.settings_btn = QtWidgets.QToolButton()
|
||||
self.settings_btn.setIconSize(QSize(21, 21))
|
||||
self.settings_btn.setStyleSheet("background: transparent;")
|
||||
if darkdetect.isDark():
|
||||
settings_action = QAction(QIcon(get_asset_path("settings_white.png")), "Settings", self)
|
||||
self.settings_btn.setIcon(QIcon(get_asset_path("settings_white.png")))
|
||||
else:
|
||||
settings_action = QAction(QIcon(get_asset_path("settings_black.png")), "Settings", self)
|
||||
self.settings_btn.setIcon(QIcon(get_asset_path("settings_black.png")))
|
||||
self.settings_btn.setToolTip("Settings")
|
||||
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
|
||||
|
||||
self.corner_layout.addWidget(self.settings_btn)
|
||||
|
||||
settings_action.triggered.connect(lambda: settings_dialog(self))
|
||||
toolbar.addAction(settings_action)
|
||||
toolbar.addWidget(self.tracker_list)
|
||||
self.tab_wrapper = QWidget()
|
||||
self.tab_layout = QVBoxLayout()
|
||||
self.tab_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.tab_layout.setSpacing(0)
|
||||
|
||||
|
||||
toolbar.setMovable(False)
|
||||
self.tab_bar_layout = QHBoxLayout()
|
||||
self.tab_bar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.tab_bar_layout.setSpacing(0)
|
||||
|
||||
self.tab_bar_layout.addWidget(self.tabs.tabBar(), stretch=0)
|
||||
self.tab_bar_layout.addStretch()
|
||||
self.tab_bar_layout.addWidget(self.corner_widget, stretch=0)
|
||||
|
||||
self.tab_layout.addLayout(self.tab_bar_layout)
|
||||
self.tab_layout.addWidget(self.tabs)
|
||||
|
||||
|
||||
self.tab_wrapper.setLayout(self.tab_layout)
|
||||
|
||||
|
||||
containerLayout.addWidget(self.tab_wrapper)
|
||||
containerLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# self.progressbar.setValue(0)
|
||||
self.consoleLog.setReadOnly(True)
|
||||
self.consoleLog.setFixedHeight(150)
|
||||
containerLayout.addWidget(self.consoleLog)
|
||||
@@ -428,16 +464,23 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
self.active_timer.timeout.connect(self.show_empty_downloads)
|
||||
self.active_timer.start(500)
|
||||
|
||||
self.context_menu = QtWidgets.QMenu(self)
|
||||
self.context_menu.addAction("Open Containing Folder", self.openFolderAction)
|
||||
self.context_menu.addAction("Copy Magnet URI", self.copyMagnetURIAction)
|
||||
self.context_menu.addAction("Cancel Download", self.cancelDownloadAction)
|
||||
self.context_menu.addAction("Delete File", self.deleteFileAction)
|
||||
|
||||
@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()
|
||||
)
|
||||
if hasattr(self, 'consoleLog'):
|
||||
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)
|
||||
@@ -484,3 +527,99 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
super().resizeEvent(event)
|
||||
table_width = self.qtablewidget.viewport().width()
|
||||
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
|
||||
|
||||
def contextMenuEvent(self, event: QContextMenuEvent):
|
||||
if self.downloadList.underMouse():
|
||||
pos = self.downloadList.viewport().mapFromGlobal(event.globalPos())
|
||||
index = self.downloadList.indexAt(pos)
|
||||
|
||||
if index.isValid():
|
||||
row = index.row()
|
||||
self._context_menu_row = row
|
||||
self.context_menu.exec(event.globalPos())
|
||||
else:
|
||||
super().contextMenuEvent(event)
|
||||
|
||||
def openFolderAction(self):
|
||||
if not hasattr(self, '_context_menu_row'):
|
||||
return
|
||||
|
||||
row = self._context_menu_row
|
||||
if row < 0 or row >= len(state.active_downloads):
|
||||
return
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
|
||||
download_path = magnetdl.save_path()
|
||||
|
||||
if download_path and os.path.exists(download_path):
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(os.path.normpath(download_path))
|
||||
elif platform.system() == "Linux":
|
||||
subprocess.Popen(["xdg-open", download_path])
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.Popen(["open", download_path])
|
||||
|
||||
def copyMagnetURIAction(self):
|
||||
if not hasattr(self, '_context_menu_row'):
|
||||
return
|
||||
|
||||
row = self._context_menu_row
|
||||
if row < 0 or row >= len(state.active_downloads):
|
||||
return
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(magnet_link)
|
||||
|
||||
def cancelDownloadAction(self):
|
||||
if not hasattr(self, '_context_menu_row'):
|
||||
return
|
||||
|
||||
row = self._context_menu_row
|
||||
if row < 0 or row >= len(state.active_downloads):
|
||||
return
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
|
||||
confirm = QMessageBox.question(self, "Cancel Download", f"Are you sure you want to cancel the download of '{magnetdl.status().name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
if confirm == QMessageBox.StandardButton.Yes:
|
||||
del state.active_downloads[magnet_link]
|
||||
remove_download_log(magnet_link)
|
||||
consoleLog(f"Cancelled download: {magnetdl.status().name}", True)
|
||||
|
||||
def deleteFileAction(self):
|
||||
if not hasattr(self, '_context_menu_row'):
|
||||
return
|
||||
|
||||
row = self._context_menu_row
|
||||
if row < 0 or row >= len(state.active_downloads):
|
||||
return
|
||||
|
||||
magnet_link = list(state.active_downloads.keys())[row]
|
||||
magnetdl = state.active_downloads[magnet_link]
|
||||
status = magnetdl.status()
|
||||
save_path = status.save_path
|
||||
torrent_name = status.name
|
||||
download_path = os.path.join(save_path, torrent_name)
|
||||
|
||||
confirm = QMessageBox.question(self, "Delete Files", f"Are you sure you want to delete the downloaded files of '{magnetdl.status().name}'? This action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
if confirm == QMessageBox.StandardButton.Yes:
|
||||
if download_path and os.path.exists(download_path):
|
||||
try:
|
||||
if os.path.isfile(download_path):
|
||||
os.remove(download_path)
|
||||
del state.active_downloads[magnet_link]
|
||||
remove_download_log(magnet_link)
|
||||
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
|
||||
else:
|
||||
import shutil
|
||||
shutil.rmtree(download_path)
|
||||
del state.active_downloads[magnet_link]
|
||||
remove_download_log(magnet_link)
|
||||
consoleLog(f"Deleted files for: {magnetdl.status().name}", True)
|
||||
except Exception as e:
|
||||
consoleLog(f"Error deleting files: {e}", True)
|
||||
|
||||
@@ -3,7 +3,7 @@ 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.network.jsonhandler import split_data, format_data, format_data_m0nkrus
|
||||
from core.utils.data.state import state
|
||||
|
||||
def return_pressed(self):
|
||||
@@ -44,15 +44,19 @@ def return_pressed(self):
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
||||
state.post_titles, _, state.post_author, state.post_seeders, state.post_leechers = format_data(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author", "Seeders", "Leechers"])
|
||||
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))
|
||||
for i, seeders in enumerate(state.post_seeders):
|
||||
self.qtablewidget.setItem(i, 2, QTableWidgetItem(seeders))
|
||||
for i, leechers in enumerate(state.post_leechers):
|
||||
self.qtablewidget.setItem(i, 3, QTableWidgetItem(leechers))
|
||||
if state.debug == True:
|
||||
consoleLog(f"Response Cached: {cached}")
|
||||
else:
|
||||
@@ -68,7 +72,7 @@ def return_pressed(self):
|
||||
self.qtablewidget.clear()
|
||||
self.show_empty_results(True)
|
||||
else:
|
||||
state.post_titles, _, state.post_author = format_data(state.posts)
|
||||
state.post_titles, _, state.post_author = format_data_m0nkrus(state.posts)
|
||||
self.show_empty_results(False)
|
||||
self.qtablewidget.clear()
|
||||
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
|
||||
|
||||
@@ -12,6 +12,45 @@ def create_tab(title, searchbar, software_list, tabs, dlbutton, layout2):
|
||||
layout.addWidget(software_list)
|
||||
if dlbutton:
|
||||
layout.addWidget(dlbutton)
|
||||
tab.setLayout(layout)
|
||||
tab.setLayout(layout)
|
||||
tabs.addTab(tab, title)
|
||||
return tab
|
||||
|
||||
#################
|
||||
# SETTINGS TABS #
|
||||
#################
|
||||
def general_tab(title, autoresume, update_checkbox, transparent_window, tabs):
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(autoresume)
|
||||
layout.addWidget(update_checkbox)
|
||||
layout.addWidget(transparent_window)
|
||||
layout.addStretch()
|
||||
tab.setLayout(layout)
|
||||
tabs.addTab(tab, title)
|
||||
return tab
|
||||
|
||||
def paths_tab(title, download_path, image_path, tabs):
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(download_path)
|
||||
layout.addWidget(image_path)
|
||||
layout.addStretch()
|
||||
tab.setLayout(layout)
|
||||
tabs.addTab(tab, title)
|
||||
return tab
|
||||
|
||||
|
||||
def network_tab(title, network_interface, max_connections, max_downloads, up_speed_limit, down_speed_limit, api_url, tabs):
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(network_interface)
|
||||
layout.addWidget(max_connections)
|
||||
layout.addWidget(max_downloads)
|
||||
layout.addWidget(up_speed_limit)
|
||||
layout.addWidget(down_speed_limit)
|
||||
layout.addWidget(api_url)
|
||||
layout.addStretch()
|
||||
tab.setLayout(layout)
|
||||
tabs.addTab(tab, title)
|
||||
return tab
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from core.utils.general.logs import consoleLog
|
||||
from core.utils.data.state import state
|
||||
import psutil
|
||||
|
||||
addrs = psutil.net_if_addrs()
|
||||
stats = psutil.net_if_stats()
|
||||
|
||||
def get_net_interfaces():
|
||||
for interface in addrs.keys():
|
||||
consoleLog(f"Found Interface: {interface}")
|
||||
return addrs.keys()
|
||||
|
||||
|
||||
def get_active_interfaces():
|
||||
active = []
|
||||
|
||||
for interface, addr_list in addrs.items():
|
||||
up = stats[interface].isup
|
||||
for addr in addr_list:
|
||||
if addr.family == 2: # ipv4
|
||||
ipv4 = addr.address
|
||||
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
|
||||
active.append(interface)
|
||||
consoleLog(f"Found Active: {interface}")
|
||||
|
||||
return active
|
||||
|
||||
|
||||
def list_interfaces() -> None:
|
||||
|
||||
for interface, addr_list in addrs.items():
|
||||
up = stats[interface].isup
|
||||
for addr in addr_list:
|
||||
if addr.family == 2: # ipv4
|
||||
ipv4 = addr.address
|
||||
if not ipv4.startswith("127.") and not ipv4.startswith("169.254") and up == True:
|
||||
status = "ACTIVE"
|
||||
else:
|
||||
status = "INACTIVE"
|
||||
consoleLog(f"Found Interface: {interface} [{status}]")
|
||||
|
||||
def init_interfaces():
|
||||
state.interfaces = list(addrs.keys())
|
||||
state.active_interfaces = get_active_interfaces()
|
||||
|
||||
def get_interface_ip(interface_name):
|
||||
if interface_name in addrs:
|
||||
for addr in addrs[interface_name]:
|
||||
if addr.family == 2: # ipv4
|
||||
if not addr.address.startswith("127.") and not addr.address.startswith("169.254"):
|
||||
return addr.address
|
||||
return None
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
from core.utils.general.wrappers import run_thread
|
||||
from core.network.interface import get_interface_ip
|
||||
import threading
|
||||
import libtorrent as lt
|
||||
from core.utils.data.state import state
|
||||
@@ -15,6 +16,7 @@ def init_session():
|
||||
|
||||
state.dl_session = lt.session()
|
||||
|
||||
|
||||
settings = {
|
||||
"upload_rate_limit": state.up_speed_limit,
|
||||
"download_rate_limit": state.down_speed_limit,
|
||||
@@ -26,8 +28,15 @@ def init_session():
|
||||
"connections_limit": state.max_connections,
|
||||
"active_downloads": state.max_downloads
|
||||
}
|
||||
|
||||
|
||||
if state.bound_interface:
|
||||
interface_ip = get_interface_ip(state.bound_interface)
|
||||
if interface_ip:
|
||||
settings["outgoing_interfaces"] = interface_ip
|
||||
settings["listen_interfaces"] = f"{interface_ip}:6881"
|
||||
consoleLog(f"Binding to Interface IP: {interface_ip}")
|
||||
else:
|
||||
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
|
||||
|
||||
state.dl_session.apply_settings(settings)
|
||||
|
||||
@@ -42,10 +51,9 @@ def add_download(magnet_uri, dl_path=state.download_path):
|
||||
init_session()
|
||||
|
||||
if magnet_uri in state.active_downloads:
|
||||
consoleLog("Skipping, download already running...")
|
||||
consoleLog("Skipping Downloading, download already running...")
|
||||
return
|
||||
|
||||
|
||||
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
||||
magnetdl.save_path = dl_path
|
||||
|
||||
@@ -98,6 +106,21 @@ def update_settings():
|
||||
"connections_limit": state.max_connections,
|
||||
"active_downloads": state.max_downloads
|
||||
}
|
||||
|
||||
if state.bound_interface:
|
||||
interface_ip = get_interface_ip(state.bound_interface)
|
||||
if interface_ip:
|
||||
settings["outgoing_interfaces"] = interface_ip
|
||||
settings["listen_interfaces"] = f"{interface_ip}:6881"
|
||||
consoleLog(f"Binding to Interface IP: {interface_ip}")
|
||||
else:
|
||||
consoleLog(f"Skipping Binding, no Interface set. ({state.bound_interface})")
|
||||
|
||||
state.dl_session.apply_settings(settings)
|
||||
|
||||
def update_bound_interface():
|
||||
|
||||
settings = { "outgoing_interfaces": state.bound_interface }
|
||||
|
||||
state.dl_session.apply_settings(settings)
|
||||
|
||||
|
||||
@@ -6,7 +6,4 @@ def add_magnet(uri):
|
||||
add_download(uri)
|
||||
consoleLog("Magnet URI added to LibTorrent")
|
||||
else:
|
||||
consoleLog(f"Invalid Magnet Link: {uri}")
|
||||
|
||||
|
||||
|
||||
consoleLog(f"Invalid Magnet Link: {uri}")
|
||||
+36
-11
@@ -6,8 +6,26 @@ from core.utils.data.state import state
|
||||
def create_config():
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "download_path": f"{state.download_path}", f"download_speed_limit": f"{state.down_speed_limit}", f"upload_speed_limit": f"{state.up_speed_limit}",
|
||||
"ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}", "autoresume": f"{state.autoresume}", "max_connections": f"{state.max_connections}", "max_downloads": f"{state.max_downloads}"}
|
||||
config["General"] = {
|
||||
"debug": True,
|
||||
"ignore_updates": f"{state.ignore_updates}",
|
||||
"autoresume": f"{state.autoresume}",
|
||||
"window_transparency": f"{state.window_transparency}"
|
||||
}
|
||||
|
||||
config["Network"] = {
|
||||
"api_url": f"{state.api_url}",
|
||||
"download_path": f"{state.download_path}",
|
||||
"download_speed_limit": f"{state.down_speed_limit}",
|
||||
"upload_speed_limit": f"{state.up_speed_limit}",
|
||||
"max_connections": f"{state.max_connections}",
|
||||
"max_downloads": f"{state.max_downloads}"
|
||||
}
|
||||
|
||||
config["Paths"] = {
|
||||
"bound_interface": f"{state.bound_interface}",
|
||||
"image_path": f"{state.image_path}"
|
||||
}
|
||||
|
||||
if platform.system() == "Windows":
|
||||
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
||||
@@ -37,16 +55,23 @@ def read_config():
|
||||
return
|
||||
|
||||
config.read(config_file)
|
||||
|
||||
|
||||
# General
|
||||
state.debug = config.getboolean("General", "debug", fallback=state.debug)
|
||||
state.api_url = config.get("General", "api_url", fallback=state.api_url)
|
||||
state.download_path = config.get("General", "download_path", fallback=state.download_path)
|
||||
state.down_speed_limit = config.getint("General", "download_speed_limit", fallback=state.down_speed_limit)
|
||||
state.up_speed_limit = config.getint("General", "upload_speed_limit", fallback=state.up_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)
|
||||
state.max_connections = config.getint("General", "max_connections", fallback=state.max_connections)
|
||||
state.max_downloads = config.getint("General", "max_downloads", fallback=state.max_downloads)
|
||||
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
|
||||
|
||||
create_config()
|
||||
# Network
|
||||
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
|
||||
state.download_path = config.get("Network", "download_path", fallback=state.download_path)
|
||||
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
|
||||
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
|
||||
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
|
||||
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
|
||||
|
||||
# Paths
|
||||
state.bound_interface = config.get("Paths", "bound_interface", fallback=state.bound_interface)
|
||||
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
|
||||
|
||||
create_config()
|
||||
|
||||
@@ -4,7 +4,7 @@ from core.utils.general.logs import consoleLog
|
||||
from .config import create_config
|
||||
|
||||
|
||||
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None):
|
||||
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None):
|
||||
if apiurl is not None:
|
||||
state.api_url = apiurl
|
||||
if download_path is not None:
|
||||
@@ -21,6 +21,8 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
|
||||
state.max_connections = max_connections
|
||||
if max_downloads is not None:
|
||||
state.max_downloads = max_downloads
|
||||
if bound_interface is not None:
|
||||
state.bound_interface = None if bound_interface == "None" else bound_interface
|
||||
|
||||
|
||||
update_settings()
|
||||
|
||||
@@ -11,6 +11,8 @@ class AppState(QObject):
|
||||
self.post_titles: List[str] = []
|
||||
self.post_urls: List[str] = []
|
||||
self.post_author: List[str] = []
|
||||
self.post_seeders: List[str] = []
|
||||
self.post_leechers: List[str] = []
|
||||
self.version: str = "dev"
|
||||
self._image_path: str = ""
|
||||
self.ignore_updates: bool = False
|
||||
@@ -23,13 +25,13 @@ class AppState(QObject):
|
||||
self.down_speed_limit: int = 0
|
||||
self.max_connections: int = 200
|
||||
self.max_downloads: int = 10
|
||||
self.aria2process: Optional[Any] = None
|
||||
self.aria2: Any = None
|
||||
self.aria2p: Any = None
|
||||
self.aria2_threads: int = 4
|
||||
self.settings_path: str = None
|
||||
self.settings_path: str = ""
|
||||
self.dl_session: Any = None
|
||||
self.active_downloads: List[str] = {}
|
||||
self.window_transparency: bool = False
|
||||
self.interfaces: List = []
|
||||
self.active_interfaces: List = []
|
||||
self.bound_interface: str = None
|
||||
|
||||
@property
|
||||
def image_path(self) -> str:
|
||||
|
||||
@@ -15,12 +15,12 @@ def get_item_url(item, posts, post_titles): # softwarelist currentitem, post lis
|
||||
return item
|
||||
if state.tracker == "rutracker":
|
||||
item_dict = posts[post_index]
|
||||
_, post_links, _ = format_data([item_dict])
|
||||
_, 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])
|
||||
_, post_links, _, _, _,= format_data([item_dict])
|
||||
consoleLog(f"Found post URL: {post_links[0]}")
|
||||
return post_links[0]
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
_log_buffer = []
|
||||
|
||||
|
||||
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
||||
@@ -22,7 +24,12 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
|
||||
|
||||
if any(d.magnet_uri == magnet_uri or d.url == url for d in downloads):
|
||||
consoleLog("Skipped Logging, download already in file")
|
||||
if magnet_uri in state.active_downloads:
|
||||
consoleLog("Skipping Logging, download already running...")
|
||||
return
|
||||
consoleLog("File already in Log, updating Download State...")
|
||||
hash = extract_hash_from_magnet(magnet_uri)
|
||||
update_download_completed_by_hash(hash, False)
|
||||
return DownloadList(data=downloads, count=len(downloads)) # thanks again claude (im stupid)
|
||||
|
||||
|
||||
@@ -41,6 +48,30 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||
|
||||
return download_list
|
||||
|
||||
def remove_download_log(magnet_uri) -> 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 = []
|
||||
|
||||
magnet_link = (magnet_uri or "").strip()
|
||||
downloads = [d for d in downloads if (getattr(d, 'magnet_uri', None) or "").strip() != magnet_link and (getattr(d, 'url', None) or "").strip() != magnet_link]
|
||||
|
||||
consoleLog(f"Removed {magnet_link} from 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")
|
||||
|
||||
@@ -165,15 +196,28 @@ def set_main_window(window):
|
||||
global _main_window
|
||||
_main_window = window
|
||||
|
||||
def flush_log_buffer(): # credits to claude
|
||||
global _log_buffer
|
||||
if _log_buffer:
|
||||
try:
|
||||
from core.interface.gui import MainWindow
|
||||
for log_entry in _log_buffer:
|
||||
MainWindow.add_log(log_entry)
|
||||
_log_buffer = []
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def consoleLog(text, printAnyways = False):
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M:%S")
|
||||
formatted_text = f"[{current_time}] {text}"
|
||||
|
||||
try:
|
||||
from core.interface.gui import MainWindow
|
||||
MainWindow.add_log(f"[{current_time}] {text}")
|
||||
MainWindow.add_log(formatted_text)
|
||||
except Exception:
|
||||
pass
|
||||
global _log_buffer
|
||||
_log_buffer.append(formatted_text)
|
||||
|
||||
if state.debug or printAnyways:
|
||||
print(f"[{current_time}] {text}")
|
||||
print(formatted_text)
|
||||
|
||||
@@ -11,7 +11,16 @@ def split_data(data):
|
||||
|
||||
return count, posts, query, success, cached
|
||||
|
||||
def format_data(data):
|
||||
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]
|
||||
post_seeders = [post["seeders"] for post in data]
|
||||
post_leechers = [post["leechers"] for post in data]
|
||||
|
||||
return post_titles, post_links, post_author, post_seeders, post_leechers
|
||||
|
||||
def format_data_m0nkrus(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]
|
||||
@@ -20,4 +29,7 @@ def format_data(data):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# https://open.spotify.com/track/70vjXI7oBou5buTtgiJeRW?si=25640cc4243c4bf3
|
||||
|
||||
@@ -6,31 +6,35 @@ 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.network.interface import list_interfaces, init_interfaces
|
||||
from core.network.libtorrent_int import update_bound_interface
|
||||
from core.utils.config.config import read_config
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
import qdarktheme
|
||||
import darkdetect
|
||||
import threading
|
||||
import platform
|
||||
import signal
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
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"))
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("auto")
|
||||
widget = MainWindow()
|
||||
|
||||
if state.window_transparency and platform.system() != "Windows":
|
||||
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
|
||||
|
||||
from core.utils.general.logs import set_main_window
|
||||
set_main_window(widget)
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -38,18 +42,26 @@ def keyboardinterrupthandler(signum, frame):
|
||||
closehelper()
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_time = time.perf_counter()
|
||||
read_config()
|
||||
logs = get_download_logs()
|
||||
count, downloads = split_data(logs)
|
||||
_, downloads = split_data(logs)
|
||||
if args.debug:
|
||||
state.debug = args.debug # override of read_config
|
||||
signal.signal(signal.SIGINT, keyboardinterrupthandler)
|
||||
consoleLog("Starting SoftwareManager...")
|
||||
consoleLog("Fetching Network Interfaces...")
|
||||
list_interfaces()
|
||||
consoleLog("Initializing Interface variables...")
|
||||
init_interfaces()
|
||||
consoleLog(f"Current Bound: {state.bound_interface}")
|
||||
run_thread(threading.Thread(target=send_notification, args=(shutdown_event,), daemon=True))
|
||||
consoleLog("Started send_notification thread")
|
||||
consoleLog("Started Thread: send_notification")
|
||||
run_thread(threading.Thread(target=update_log, args=(shutdown_event,), daemon=True))
|
||||
consoleLog("Started update_log thread")
|
||||
consoleLog("Started Thread: update_log")
|
||||
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
|
||||
consoleLog("Started check_completed thread")
|
||||
consoleLog("Launching GUI")
|
||||
consoleLog("Started Thread: check_completed")
|
||||
elapsed = time.perf_counter() - start_time
|
||||
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
||||
run_gui()
|
||||
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
requests==2.32.2
|
||||
PySide6==6.10.1
|
||||
PySide6-Essentials==6.10.1
|
||||
beautifulsoup4==4.13.5
|
||||
darkdetect==0.8.0
|
||||
darkdetect==0.7.1
|
||||
pyinstaller==6.15.0
|
||||
pyqtdarktheme==0.1.7
|
||||
PyQtDarkTheme-fork==2.3.4
|
||||
aiohttp==3.13.0
|
||||
plyer==2.1.0
|
||||
psutil==7.1.0
|
||||
libtorrent==2.0.11
|
||||
libtorrent-windows-dll==0.0.3
|
||||
libtorrent-windows-dll==0.0.3
|
||||
|
||||
Reference in New Issue
Block a user