mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Merge branch 'main' into feat/contextmenu
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Keeps your GitHub Actions (CI/CD) up to date daily
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
# Keeps your Python dependencies (requirements.txt / pyproject.toml) up to date daily
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "darkdetect"
|
||||||
|
versions: [">=0.8.0"]
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -190,7 +190,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build artifact (Windows)
|
- name: Upload build artifact (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: |
|
path: |
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build artifact (Linux/macOS)
|
- name: Upload build artifact (Linux/macOS)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: dist-final/${{ matrix.artifact_name }}
|
path: dist-final/${{ matrix.artifact_name }}
|
||||||
@@ -213,8 +213,8 @@ jobs:
|
|||||||
if: success()
|
if: success()
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
@@ -248,4 +248,4 @@ jobs:
|
|||||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-macos
|
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-macos
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build artifact (Windows)
|
- name: Upload build artifact (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: |
|
path: |
|
||||||
@@ -205,7 +205,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload build artifact (Linux/macOS)
|
- name: Upload build artifact (Linux/macOS)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: dist-final/${{ matrix.artifact_name }}
|
path: dist-final/${{ matrix.artifact_name }}
|
||||||
@@ -218,8 +218,8 @@ jobs:
|
|||||||
if: success()
|
if: success()
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Sync README Dependencies
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- requirements.txt
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Update README dependencies
|
||||||
|
run: python scripts/sync_readme_dependencies.py
|
||||||
|
|
||||||
|
- name: Create pull request
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
commit-message: "Sync README dependencies"
|
||||||
|
branch: auto/sync-readme-dependencies
|
||||||
|
title: "Sync README dependencies"
|
||||||
|
body: "Automated update of the Python Dependencies section in README.md to match `requirements.txt`."
|
||||||
|
labels: documentation
|
||||||
|
token: ${{ secrets.PR_TOKEN }}
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -170,7 +170,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifact (Windows)
|
- name: Upload artifact (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: dist-final/${{ matrix.artifact_name }}.zip
|
path: dist-final/${{ matrix.artifact_name }}.zip
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifact (Linux/macOS)
|
- name: Upload artifact (Linux/macOS)
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-build
|
name: ${{ matrix.os }}-build
|
||||||
path: dist-final/${{ matrix.artifact_name }}
|
path: dist-final/${{ matrix.artifact_name }}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# SoftwareManager
|
# 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.
|
SoftwareManager is a Python-based GUI tool that simplifies searching and downloading software from various sources, including Rutracker, Uztracker, Steamrip and the official M0nkrus Telegram channel.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Our Steamrip implementation is currently very experimental and potentially unstable.<br>
|
||||||
|
> Downloads will fail if there's no BuzzHeavier download available. We're working on this.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Searching & Downloading Software from various pages
|
- Searching & Downloading Software from various pages
|
||||||
@@ -35,20 +39,21 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
|
|||||||
## Python Dependencies
|
## Python Dependencies
|
||||||
(also included in requirements.txt)
|
(also included in requirements.txt)
|
||||||
|
|
||||||
```bash
|
<!-- python-dependencies:start -->
|
||||||
requests (2.32.2)
|
```text
|
||||||
PySide6 (6.10.1)
|
requests (2.33.1)
|
||||||
beautifulsoup (44.13.5)
|
PySide6-Essentials (6.11.0)
|
||||||
|
beautifulsoup4 (4.14.3)
|
||||||
darkdetect (0.7.1)
|
darkdetect (0.7.1)
|
||||||
pyinstaller (6.15.0)
|
pyinstaller (6.19.0)
|
||||||
PyQtDarkTheme-fork (2.3.4)
|
PyQtDarkTheme-fork (2.3.4)
|
||||||
aiohttp (3.13.0)
|
|
||||||
plyer (2.1.0)
|
plyer (2.1.0)
|
||||||
psutil (7.1.0)
|
psutil (7.1.0)
|
||||||
libtorrent (2.0.11)
|
libtorrent (2.0.11)
|
||||||
libtorrent-windows-dll (0.0.3)
|
libtorrent-windows-dll (0.0.3)
|
||||||
send2trash (2.1.0)
|
send2trash (2.1.0)
|
||||||
```
|
```
|
||||||
|
<!-- python-dependencies:end -->
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="darkreader-lock">
|
||||||
<link rel="stylesheet" href="main.css"/>
|
<link rel="stylesheet" href="main.css"/>
|
||||||
<title>SoftwareManager - Main</title>
|
<title>SoftwareManager - Main</title>
|
||||||
<!-- <link rel="icon" type="image/x-icon" href="/assets/icons/favicon.ico"> -->
|
<!-- <link rel="icon" type="image/x-icon" href="/assets/icons/favicon.ico"> -->
|
||||||
@@ -225,9 +226,11 @@
|
|||||||
openTab(null, 'api', false);
|
openTab(null, 'api', false);
|
||||||
} else if (currentPath === '/docs') {
|
} else if (currentPath === '/docs') {
|
||||||
openTab(null, 'docs', false);
|
openTab(null, 'docs', false);
|
||||||
} else {
|
} else if (currentPath === '/') {
|
||||||
history.replaceState({ tab: 'start' }, "", "/");
|
history.replaceState({ tab: 'start' }, "", "/");
|
||||||
openTab(null, 'start', false);
|
openTab(null, 'start', false);
|
||||||
|
} else {
|
||||||
|
openTab(null, 'notfound', false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+4
-5
@@ -1,10 +1,9 @@
|
|||||||
requests==2.32.2
|
requests==2.33.1
|
||||||
PySide6-Essentials==6.10.1
|
PySide6-Essentials==6.11.0
|
||||||
beautifulsoup4==4.13.5
|
beautifulsoup4==4.14.3
|
||||||
darkdetect==0.7.1
|
darkdetect==0.7.1
|
||||||
pyinstaller==6.15.0
|
pyinstaller==6.19.0
|
||||||
PyQtDarkTheme-fork==2.3.4
|
PyQtDarkTheme-fork==2.3.4
|
||||||
aiohttp==3.13.0
|
|
||||||
plyer==2.1.0
|
plyer==2.1.0
|
||||||
psutil==7.1.0
|
psutil==7.1.0
|
||||||
libtorrent==2.0.11
|
libtorrent==2.0.11
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
README_PATH = ROOT_DIR / "README.md"
|
||||||
|
REQUIREMENTS_PATH = ROOT_DIR / "requirements.txt"
|
||||||
|
|
||||||
|
START_MARKER = "<!-- python-dependencies:start -->"
|
||||||
|
END_MARKER = "<!-- python-dependencies:end -->"
|
||||||
|
REQUIREMENT_PATTERN = re.compile(
|
||||||
|
r"^(?P<name>[A-Za-z0-9_.\-\[\]]+)\s*(?P<specifier>(==|~=|>=|<=|!=|>|<).+)?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_requirements(requirements_text: str) -> list[str]:
|
||||||
|
formatted_lines: list[str] = []
|
||||||
|
|
||||||
|
for raw_line in requirements_text.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = REQUIREMENT_PATTERN.match(line)
|
||||||
|
if not match:
|
||||||
|
formatted_lines.append(f" {line}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = match.group("name")
|
||||||
|
specifier = (match.group("specifier") or "").strip()
|
||||||
|
|
||||||
|
if specifier.startswith("=="):
|
||||||
|
formatted_lines.append(f" {name} ({specifier[2:]})")
|
||||||
|
elif specifier:
|
||||||
|
formatted_lines.append(f" {name} {specifier}")
|
||||||
|
else:
|
||||||
|
formatted_lines.append(f" {name}")
|
||||||
|
|
||||||
|
return formatted_lines
|
||||||
|
|
||||||
|
|
||||||
|
def render_dependency_block(requirements_text: str) -> str:
|
||||||
|
dependency_lines = iter_requirements(requirements_text)
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
START_MARKER,
|
||||||
|
" ```text",
|
||||||
|
*dependency_lines,
|
||||||
|
" ```",
|
||||||
|
END_MARKER,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_managed_block(readme_text: str, rendered_block: str) -> str:
|
||||||
|
start_index = readme_text.find(START_MARKER)
|
||||||
|
end_index = readme_text.find(END_MARKER)
|
||||||
|
|
||||||
|
if start_index == -1 or end_index == -1 or end_index < start_index:
|
||||||
|
raise RuntimeError("README dependency markers are missing or invalid.")
|
||||||
|
|
||||||
|
end_index += len(END_MARKER)
|
||||||
|
return readme_text[:start_index] + rendered_block + readme_text[end_index:]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
requirements_text = REQUIREMENTS_PATH.read_text(encoding="utf-8")
|
||||||
|
readme_text = README_PATH.read_text(encoding="utf-8")
|
||||||
|
rendered_block = render_dependency_block(requirements_text)
|
||||||
|
updated_readme = replace_managed_block(readme_text, rendered_block)
|
||||||
|
|
||||||
|
if updated_readme != readme_text:
|
||||||
|
README_PATH.write_text(updated_readme, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -49,6 +49,9 @@ class SteamripScraper:
|
|||||||
for i in range(len(names)):
|
for i in range(len(names)):
|
||||||
ret.append({"title" : names[i], "url" : links[i]})
|
ret.append({"title" : names[i], "url" : links[i]})
|
||||||
|
|
||||||
|
self.cache["data"] = ret
|
||||||
|
self.cache["last_fetched"] = current_time
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def scrape_steamrip_game_downloads(self, url):
|
def scrape_steamrip_game_downloads(self, url):
|
||||||
@@ -70,8 +73,6 @@ class SteamripScraper:
|
|||||||
if len(link) != 1:
|
if len(link) != 1:
|
||||||
ret.append(link)
|
ret.append(link)
|
||||||
|
|
||||||
self.cache["data"] = ret
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_download_link(self, post: Dict):
|
def get_download_link(self, post: Dict):
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class UztrackerScraper:
|
|||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
consoleLog(f"Failed to fetch {search_url}: {e}")
|
consoleLog(f"Failed to fetch {search_url}: {e}")
|
||||||
return None
|
return []
|
||||||
|
|
||||||
def get_download_link(self, post: Dict):
|
def get_download_link(self, post: Dict):
|
||||||
return get_magnet_link(post["url"])
|
return get_magnet_link(post["url"])
|
||||||
|
|||||||
@@ -53,10 +53,12 @@ class ContextMenu_Downloads:
|
|||||||
if not hasattr(self, '_context_menu_row'):
|
if not hasattr(self, '_context_menu_row'):
|
||||||
return
|
return
|
||||||
row = self._context_menu_row
|
row = self._context_menu_row
|
||||||
if row < 0 or row >= len(state.active_downloads):
|
with state.downloads_lock:
|
||||||
return
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
magnet_link = list(state.active_downloads.keys())[row]
|
return
|
||||||
magnetdl = state.active_downloads[magnet_link]
|
keys = list(state.active_downloads.keys())
|
||||||
|
magnet_link = keys[row]
|
||||||
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
download_path = magnetdl.save_path()
|
download_path = magnetdl.save_path()
|
||||||
if download_path and os.path.exists(download_path):
|
if download_path and os.path.exists(download_path):
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
@@ -70,9 +72,11 @@ class ContextMenu_Downloads:
|
|||||||
if not hasattr(self, '_context_menu_row'):
|
if not hasattr(self, '_context_menu_row'):
|
||||||
return
|
return
|
||||||
row = self._context_menu_row
|
row = self._context_menu_row
|
||||||
if row < 0 or row >= len(state.active_downloads):
|
with state.downloads_lock:
|
||||||
return
|
if row < 0 or row >= len(state.active_downloads):
|
||||||
magnet_link = list(state.active_downloads.keys())[row]
|
return
|
||||||
|
keys = list(state.active_downloads.keys())
|
||||||
|
magnet_link = keys[row]
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
clipboard = QtWidgets.QApplication.clipboard()
|
||||||
clipboard.setText(magnet_link)
|
clipboard.setText(magnet_link)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ class DownloadModel(QAbstractTableModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
magnet_link = list(state.active_downloads.keys())[index.row()]
|
keys = list(state.active_downloads.keys())
|
||||||
|
magnet_link = keys[index.row()]
|
||||||
magnetdl = state.active_downloads[magnet_link]
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
status = magnetdl.status()
|
status = magnetdl.status()
|
||||||
except (IndexError, KeyError, RuntimeError):
|
except (IndexError, KeyError, RuntimeError):
|
||||||
@@ -101,7 +102,8 @@ class DownloadModel(QAbstractTableModel):
|
|||||||
if row >= len(state.active_downloads) or row < 0:
|
if row >= len(state.active_downloads) or row < 0:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
magnet_link = list(state.active_downloads.keys())[row]
|
keys = list(state.active_downloads.keys())
|
||||||
|
magnet_link = keys[row]
|
||||||
magnetdl = state.active_downloads[magnet_link]
|
magnetdl = state.active_downloads[magnet_link]
|
||||||
status = magnetdl.status()
|
status = magnetdl.status()
|
||||||
except (IndexError, KeyError, RuntimeError):
|
except (IndexError, KeyError, RuntimeError):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget
|
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget
|
||||||
from PySide6.QtCore import Qt, QSize, QEvent, QObject
|
from PySide6.QtCore import Qt, QSize, QEvent, QObject, QTimer
|
||||||
from PySide6.QtGui import QImage, QPixmap
|
from PySide6.QtGui import QImage, QPixmap
|
||||||
from utils.data.state import state
|
from utils.data.state import state
|
||||||
import darkdetect
|
import darkdetect
|
||||||
@@ -17,8 +17,15 @@ class Image(QObject):
|
|||||||
self.overlay_label.setGraphicsEffect(self.opacity_effect)
|
self.overlay_label.setGraphicsEffect(self.opacity_effect)
|
||||||
|
|
||||||
self._current_image_path = None
|
self._current_image_path = None
|
||||||
|
self._cached_qimage = None
|
||||||
self._wallpaper_active = False
|
self._wallpaper_active = False
|
||||||
self._original_stylesheets = {}
|
self._original_stylesheets = {}
|
||||||
|
|
||||||
|
self._resize_timer = QTimer(self)
|
||||||
|
self._resize_timer.setSingleShot(True)
|
||||||
|
self._resize_timer.setInterval(100)
|
||||||
|
self._resize_timer.timeout.connect(self._on_resize_finished)
|
||||||
|
|
||||||
parent.installEventFilter(self)
|
parent.installEventFilter(self)
|
||||||
state.image_changed.connect(self.update_image_overlay)
|
state.image_changed.connect(self.update_image_overlay)
|
||||||
|
|
||||||
@@ -28,9 +35,14 @@ class Image(QObject):
|
|||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
if obj == self.application and event.type() == QEvent.Type.Resize:
|
if obj == self.application and event.type() == QEvent.Type.Resize:
|
||||||
if self._current_image_path:
|
if self._current_image_path:
|
||||||
self._load_and_display(self._current_image_path)
|
self._apply_layout_fast()
|
||||||
|
self._resize_timer.start()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _on_resize_finished(self):
|
||||||
|
if self._current_image_path:
|
||||||
|
self._apply_layout()
|
||||||
|
|
||||||
def _set_wallpaper_transparency(self, enabled):
|
def _set_wallpaper_transparency(self, enabled):
|
||||||
if self._wallpaper_active == enabled:
|
if self._wallpaper_active == enabled:
|
||||||
return
|
return
|
||||||
@@ -117,12 +129,62 @@ class Image(QObject):
|
|||||||
self._set_wallpaper_transparency(False)
|
self._set_wallpaper_transparency(False)
|
||||||
return
|
return
|
||||||
self._current_image_path = image_path
|
self._current_image_path = image_path
|
||||||
|
|
||||||
|
# Only reload from disk when the path actually changed
|
||||||
|
if self._cached_qimage is None or image_path != getattr(self, '_cached_path', None):
|
||||||
|
image = QImage(image_path)
|
||||||
|
if image.isNull():
|
||||||
|
self.overlay_label.hide()
|
||||||
|
self._cached_qimage = None
|
||||||
|
return
|
||||||
|
self._cached_qimage = image
|
||||||
|
self._cached_path = image_path
|
||||||
|
|
||||||
|
self._apply_layout()
|
||||||
|
|
||||||
|
def _apply_layout_fast(self):
|
||||||
|
if self._cached_qimage is None or not state.image_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
parent = self.application
|
parent = self.application
|
||||||
image = QImage(image_path)
|
image = self._cached_qimage
|
||||||
if image.isNull():
|
|
||||||
|
if getattr(state, "image_as_wallpaper", False):
|
||||||
|
scaled = image.scaled(
|
||||||
|
parent.size(),
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
||||||
|
Qt.TransformationMode.FastTransformation
|
||||||
|
)
|
||||||
|
cx = (scaled.width() - parent.width()) // 2
|
||||||
|
cy = (scaled.height() - parent.height()) // 2
|
||||||
|
cropped = scaled.copy(cx, cy, parent.width(), parent.height())
|
||||||
|
self.overlay_label.setPixmap(QPixmap.fromImage(cropped))
|
||||||
|
self.overlay_label.setGeometry(0, 0, parent.width(), parent.height())
|
||||||
|
else:
|
||||||
|
w = self.overlay_label.width()
|
||||||
|
h = self.overlay_label.height()
|
||||||
|
pos = state.image_position
|
||||||
|
off = int(state.image_offset)
|
||||||
|
if pos == "top-left":
|
||||||
|
x, y = off, off
|
||||||
|
elif pos == "top-right":
|
||||||
|
x, y = parent.width() - w - off, off
|
||||||
|
elif pos == "bottom-left":
|
||||||
|
x, y = off, parent.height() - h - off
|
||||||
|
elif pos == "center":
|
||||||
|
x, y = (parent.width() - w) // 2, (parent.height() - h) // 2
|
||||||
|
else:
|
||||||
|
x, y = parent.width() - w - off, parent.height() - h - off
|
||||||
|
self.overlay_label.move(x, y)
|
||||||
|
|
||||||
|
def _apply_layout(self):
|
||||||
|
if self._cached_qimage is None or not state.image_enabled:
|
||||||
self.overlay_label.hide()
|
self.overlay_label.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
parent = self.application
|
||||||
|
image = self._cached_qimage
|
||||||
|
|
||||||
if getattr(state, "image_as_wallpaper", False):
|
if getattr(state, "image_as_wallpaper", False):
|
||||||
scaled_image = image.scaled(
|
scaled_image = image.scaled(
|
||||||
parent.size(),
|
parent.size(),
|
||||||
@@ -184,4 +246,6 @@ class Image(QObject):
|
|||||||
self.overlay_label.show()
|
self.overlay_label.show()
|
||||||
|
|
||||||
def update_image_overlay(self, new_image_path):
|
def update_image_overlay(self, new_image_path):
|
||||||
|
if new_image_path != getattr(self, '_cached_path', None):
|
||||||
|
self._cached_qimage = None
|
||||||
self._load_and_display(new_image_path)
|
self._load_and_display(new_image_path)
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
from interface.utils.tabhelper import create_tab
|
from interface.utils.tabhelper import create_tab
|
||||||
|
from interface.dialogs.theme import _accent_selection_color
|
||||||
from utils.config.settings import save_settings
|
from utils.config.settings import save_settings
|
||||||
from interface.utils.svghelper import svg_icon
|
from interface.utils.svghelper import svg_icon
|
||||||
from utils.logging.logs import consoleLog
|
from utils.logging.logs import consoleLog
|
||||||
from PySide6.QtCore import Qt, QSize
|
from PySide6.QtCore import Qt, QSize
|
||||||
from utils.data.state import state
|
from utils.data.state import state
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
@@ -13,9 +15,10 @@ from PySide6.QtWidgets import (
|
|||||||
QDialog,
|
QDialog,
|
||||||
QLabel,
|
QLabel,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QSpinBox, QSlider, QCheckBox,
|
QSpinBox, QSlider, QCheckBox,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
|
QColorDialog
|
||||||
)
|
)
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
@@ -91,9 +94,31 @@ def settings_dialog(self):
|
|||||||
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
|
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
|
||||||
|
|
||||||
# Accent Color
|
# Accent Color
|
||||||
accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color (requires restart): ", width=180, height=30)
|
accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color: ", width=180, height=30)
|
||||||
|
accent_color_button = QPushButton("")
|
||||||
|
accent_color_button.setFixedSize(21, 21)
|
||||||
|
accent_color_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
|
def update_accent_button_color(color_str):
|
||||||
|
c = color_str.strip()
|
||||||
|
accent_color_button.setStyleSheet(
|
||||||
|
f"QPushButton {{ background-color: {c if c else 'transparent'}; border: 1px solid palette(mid); border-radius: 3px; }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def browse_accent_color():
|
||||||
|
current = accent_color_input.text().strip()
|
||||||
|
initial = QColor(current) if current else QColor()
|
||||||
|
color = QColorDialog.getColor(initial, dialog, "Select Accent Color")
|
||||||
|
if color.isValid():
|
||||||
|
accent_color_input.setText(color.name())
|
||||||
|
|
||||||
|
accent_color_input.textChanged.connect(update_accent_button_color)
|
||||||
|
accent_color_button.clicked.connect(browse_accent_color)
|
||||||
accent_color_input.setPlaceholderText("e.g. #fca7d7")
|
accent_color_input.setPlaceholderText("e.g. #fca7d7")
|
||||||
accent_color_input.setText(state.accent_color)
|
accent_color_input.setText(state.accent_color)
|
||||||
|
update_accent_button_color(state.accent_color)
|
||||||
|
accent_color_container.layout().insertWidget(1, accent_color_button)
|
||||||
|
|
||||||
|
|
||||||
# API URL Widget
|
# API URL Widget
|
||||||
api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30)
|
api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30)
|
||||||
@@ -305,6 +330,13 @@ def settings_dialog(self):
|
|||||||
accent_color=accent_color_input.text().strip(),
|
accent_color=accent_color_input.text().strip(),
|
||||||
close_to_tray=close_to_tray_checkbox.isChecked()
|
close_to_tray=close_to_tray_checkbox.isChecked()
|
||||||
)
|
)
|
||||||
|
color = _accent_selection_color()
|
||||||
|
if color:
|
||||||
|
self.tracker_list.setStyleSheet(
|
||||||
|
f"QComboBox QAbstractItemView::item:selected {{ background: {color}; }}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.tracker_list.setStyleSheet("")
|
||||||
|
|
||||||
cancel_btn.clicked.connect(dialog.reject)
|
cancel_btn.clicked.connect(dialog.reject)
|
||||||
layout.addWidget(cancel_btn)
|
layout.addWidget(cancel_btn)
|
||||||
|
|||||||
@@ -317,6 +317,15 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
|||||||
def _on_log_signal(self, text):
|
def _on_log_signal(self, text):
|
||||||
if hasattr(self, 'consoleLog'):
|
if hasattr(self, 'consoleLog'):
|
||||||
self.consoleLog.append(text)
|
self.consoleLog.append(text)
|
||||||
|
# Delete lines if exceeding 500
|
||||||
|
doc = self.consoleLog.document()
|
||||||
|
if doc.blockCount() > 500:
|
||||||
|
cursor = self.consoleLog.textCursor()
|
||||||
|
cursor.movePosition(cursor.MoveOperation.Start)
|
||||||
|
cursor.movePosition(cursor.MoveOperation.Down, cursor.MoveMode.KeepAnchor, doc.blockCount() - 500)
|
||||||
|
cursor.removeSelectedText()
|
||||||
|
cursor.deleteChar()
|
||||||
|
|
||||||
self.consoleLog.verticalScrollBar().setValue(
|
self.consoleLog.verticalScrollBar().setValue(
|
||||||
self.consoleLog.verticalScrollBar().maximum()
|
self.consoleLog.verticalScrollBar().maximum()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ def add_direct_download(url: str, title: str, dl_path: Optional[str] = None, hea
|
|||||||
if dl_path is None:
|
if dl_path is None:
|
||||||
dl_path = state.download_path
|
dl_path = state.download_path
|
||||||
|
|
||||||
if url in state.active_downloads:
|
with state.downloads_lock:
|
||||||
consoleLog(f"Download already active: {title}")
|
if url in state.active_downloads:
|
||||||
return
|
consoleLog(f"Download already active: {title}")
|
||||||
|
return
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
detect_filename_from_headers(url, DirectDownloadHandle.USER_AGENT)
|
detect_filename_from_headers(url, DirectDownloadHandle.USER_AGENT)
|
||||||
@@ -26,7 +27,8 @@ def add_direct_download(url: str, title: str, dl_path: Optional[str] = None, hea
|
|||||||
)
|
)
|
||||||
|
|
||||||
handle = DirectDownloadHandle(url, filename, dl_path, headers, single_threaded)
|
handle = DirectDownloadHandle(url, filename, dl_path, headers, single_threaded)
|
||||||
state.active_downloads[url] = handle
|
with state.downloads_lock:
|
||||||
|
state.active_downloads[url] = handle
|
||||||
add_download_log(title, url, "", False)
|
add_download_log(title, url, "", False)
|
||||||
handle.start()
|
handle.start()
|
||||||
consoleLog(f"Started direct download: {filename}")
|
consoleLog(f"Started direct download: {filename}")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from collections import deque
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import libtorrent as lt
|
import libtorrent as lt
|
||||||
import threading
|
import threading
|
||||||
@@ -31,7 +32,7 @@ class DirectDownloadStatus:
|
|||||||
self._upload_rate = 0
|
self._upload_rate = 0
|
||||||
|
|
||||||
self._chunk_bytes: dict[int, int] = {}
|
self._chunk_bytes: dict[int, int] = {}
|
||||||
self._speed_window: list[tuple[float, int]] = []
|
self._speed_window: deque[tuple[float, int]] = deque()
|
||||||
self._speed_window_size = 3.0
|
self._speed_window_size = 3.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -65,9 +66,8 @@ class DirectDownloadStatus:
|
|||||||
self._speed_window.append((now, self._total_wanted_done))
|
self._speed_window.append((now, self._total_wanted_done))
|
||||||
|
|
||||||
cutoff = now - self._speed_window_size
|
cutoff = now - self._speed_window_size
|
||||||
self._speed_window = [
|
while self._speed_window and self._speed_window[0][0] < cutoff:
|
||||||
(t, b) for t, b in self._speed_window if t >= cutoff
|
self._speed_window.popleft()
|
||||||
]
|
|
||||||
if len(self._speed_window) >= 2:
|
if len(self._speed_window) >= 2:
|
||||||
oldest_time, oldest_bytes = self._speed_window[0]
|
oldest_time, oldest_bytes = self._speed_window[0]
|
||||||
dt = now - oldest_time
|
dt = now - oldest_time
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import time
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
loop_running = False
|
_loop_lock = threading.Lock()
|
||||||
|
|
||||||
def get_free_space_mb(dirname):
|
def get_free_space_mb(dirname):
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
@@ -23,7 +23,9 @@ def get_free_space_mb(dirname):
|
|||||||
|
|
||||||
def check_space():
|
def check_space():
|
||||||
while not state.shutdown_event.is_set():
|
while not state.shutdown_event.is_set():
|
||||||
for _, magnetdl in list(state.active_downloads.items()):
|
with state.downloads_lock:
|
||||||
|
items = list(state.active_downloads.items())
|
||||||
|
for _, magnetdl in items:
|
||||||
try:
|
try:
|
||||||
status = magnetdl.status()
|
status = magnetdl.status()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -81,9 +83,11 @@ def add_download(magnet_uri):
|
|||||||
init_session()
|
init_session()
|
||||||
free_space = get_free_space_mb(state.download_path)
|
free_space = get_free_space_mb(state.download_path)
|
||||||
|
|
||||||
if magnet_uri in state.active_downloads:
|
with state.downloads_lock:
|
||||||
|
already_active = magnet_uri in state.active_downloads
|
||||||
|
handle = state.active_downloads.get(magnet_uri) if already_active else None
|
||||||
|
if already_active:
|
||||||
try:
|
try:
|
||||||
handle = state.active_downloads[magnet_uri]
|
|
||||||
status = handle.status()
|
status = handle.status()
|
||||||
|
|
||||||
if status.has_metadata:
|
if status.has_metadata:
|
||||||
@@ -93,7 +97,8 @@ def add_download(magnet_uri):
|
|||||||
|
|
||||||
consoleLog(f"File Deleted, redownloading: {status.name}")
|
consoleLog(f"File Deleted, redownloading: {status.name}")
|
||||||
state.dl_session.remove_torrent(handle)
|
state.dl_session.remove_torrent(handle)
|
||||||
del state.active_downloads[magnet_uri]
|
with state.downloads_lock:
|
||||||
|
del state.active_downloads[magnet_uri]
|
||||||
else:
|
else:
|
||||||
consoleLog("Skipping Downloading, download already running...")
|
consoleLog("Skipping Downloading, download already running...")
|
||||||
return False
|
return False
|
||||||
@@ -102,23 +107,26 @@ def add_download(magnet_uri):
|
|||||||
return False
|
return False
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
consoleLog(f"Error in LibTorrent Handle: {e}")
|
consoleLog(f"Error in LibTorrent Handle: {e}")
|
||||||
del state.active_downloads[magnet_uri]
|
with state.downloads_lock:
|
||||||
|
del state.active_downloads[magnet_uri]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = lt.parse_magnet_uri(magnet_uri)
|
params = lt.parse_magnet_uri(magnet_uri)
|
||||||
params.save_path = state.download_path
|
params.save_path = state.download_path
|
||||||
|
|
||||||
handle = state.dl_session.add_torrent(params)
|
handle = state.dl_session.add_torrent(params)
|
||||||
|
metadata_timeout = 60
|
||||||
|
metadata_start = time.time()
|
||||||
while not handle.has_metadata():
|
while not handle.has_metadata():
|
||||||
|
if time.time() - metadata_start > metadata_timeout:
|
||||||
|
state.dl_session.remove_torrent(handle)
|
||||||
|
consoleLog("Timed out waiting for torrent metadata")
|
||||||
|
return False
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
total_size = handle.get_torrent_info().total_size()
|
total_size = handle.get_torrent_info().total_size()
|
||||||
|
|
||||||
if free_space > total_size:
|
if free_space <= total_size:
|
||||||
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
|
||||||
magnetdl.save_path = state.download_path
|
|
||||||
download = state.dl_session.add_torrent(magnetdl)
|
|
||||||
else:
|
|
||||||
state.dl_session.remove_torrent(handle)
|
state.dl_session.remove_torrent(handle)
|
||||||
consoleLog("Not enough free space to download this item.")
|
consoleLog("Not enough free space to download this item.")
|
||||||
return False
|
return False
|
||||||
@@ -126,8 +134,9 @@ def add_download(magnet_uri):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
consoleLog(f"Failed to add torrent or fetch info: {e}")
|
consoleLog(f"Failed to add torrent or fetch info: {e}")
|
||||||
return False
|
return False
|
||||||
if download:
|
if handle:
|
||||||
state.active_downloads[magnet_uri] = download
|
with state.downloads_lock:
|
||||||
|
state.active_downloads[magnet_uri] = handle
|
||||||
consoleLog(f"Added {magnet_uri} to downloads")
|
consoleLog(f"Added {magnet_uri} to downloads")
|
||||||
|
|
||||||
run_thread(threading.Thread(target=dl_status_loop))
|
run_thread(threading.Thread(target=dl_status_loop))
|
||||||
@@ -140,9 +149,10 @@ def add_seed(magnet_uri, file_path):
|
|||||||
|
|
||||||
init_session()
|
init_session()
|
||||||
|
|
||||||
if magnet_uri in state.active_downloads:
|
with state.downloads_lock:
|
||||||
consoleLog("Already seeding this torrent")
|
if magnet_uri in state.active_downloads:
|
||||||
return False
|
consoleLog("Already seeding this torrent")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
magnetdl = lt.parse_magnet_uri(magnet_uri)
|
||||||
@@ -151,43 +161,43 @@ def add_seed(magnet_uri, file_path):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
consoleLog(f"Failed to add seed: {e}")
|
consoleLog(f"Failed to add seed: {e}")
|
||||||
return False
|
return False
|
||||||
state.active_downloads[magnet_uri] = handle
|
with state.downloads_lock:
|
||||||
|
state.active_downloads[magnet_uri] = handle
|
||||||
state.seeded_magnets.add(magnet_uri)
|
state.seeded_magnets.add(magnet_uri)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def dl_status_loop():
|
def dl_status_loop():
|
||||||
global loop_running
|
if not _loop_lock.acquire(blocking=False):
|
||||||
if loop_running == True:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
loop_running = True
|
try:
|
||||||
completed_set = set()
|
completed_set = set()
|
||||||
|
|
||||||
if not state.active_downloads:
|
|
||||||
consoleLog("No active downloads")
|
|
||||||
loop_running = False
|
|
||||||
return
|
|
||||||
|
|
||||||
while state.active_downloads and not state.shutdown_event.is_set():
|
|
||||||
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
|
||||||
try:
|
|
||||||
status = magnetdl.status()
|
|
||||||
except RuntimeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set:
|
|
||||||
consoleLog(f"Download completed: {status.name}")
|
|
||||||
|
|
||||||
completed_set.add(magnet_uri)
|
|
||||||
|
|
||||||
if not state.active_downloads:
|
if not state.active_downloads:
|
||||||
loop_running = False
|
consoleLog("No active downloads")
|
||||||
break
|
return
|
||||||
|
|
||||||
time.sleep(1)
|
while state.active_downloads and not state.shutdown_event.is_set():
|
||||||
|
with state.downloads_lock:
|
||||||
loop_running = False
|
items = list(state.active_downloads.items())
|
||||||
|
for magnet_uri, magnetdl in items:
|
||||||
|
try:
|
||||||
|
status = magnetdl.status()
|
||||||
|
except RuntimeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if status.state == lt.torrent_status.seeding and magnet_uri not in completed_set:
|
||||||
|
consoleLog(f"Download completed: {status.name}")
|
||||||
|
|
||||||
|
completed_set.add(magnet_uri)
|
||||||
|
|
||||||
|
if not state.active_downloads:
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
finally:
|
||||||
|
_loop_lock.release()
|
||||||
|
|
||||||
def update_settings():
|
def update_settings():
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ import os
|
|||||||
|
|
||||||
def cleanup_session():
|
def cleanup_session():
|
||||||
if state.dl_session is not None:
|
if state.dl_session is not None:
|
||||||
for magnetdl in state.active_downloads.values():
|
with state.downloads_lock:
|
||||||
if hasattr(magnetdl, 'pause'): # Check it's a handle
|
for magnetdl in state.active_downloads.values():
|
||||||
magnetdl.pause()
|
if hasattr(magnetdl, 'pause'): # check it's a handle
|
||||||
|
magnetdl.pause()
|
||||||
|
|
||||||
del state.dl_session
|
del state.dl_session
|
||||||
state.dl_session = None
|
state.dl_session = None
|
||||||
state.active_downloads.clear()
|
with state.downloads_lock:
|
||||||
|
state.active_downloads.clear()
|
||||||
|
|
||||||
def send_notification(shutdown_event):
|
def send_notification(shutdown_event):
|
||||||
notified = set()
|
notified = set()
|
||||||
while not shutdown_event.is_set():
|
while not shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
with state.downloads_lock:
|
||||||
|
items = list(state.active_downloads.items())
|
||||||
|
for magnet_uri, magnetdl in items:
|
||||||
if isinstance(magnetdl, dict):
|
if isinstance(magnetdl, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -42,7 +46,9 @@ def update_log(shutdown_event):
|
|||||||
updated = set()
|
updated = set()
|
||||||
while not shutdown_event.is_set():
|
while not shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
with state.downloads_lock:
|
||||||
|
items = list(state.active_downloads.items())
|
||||||
|
for magnet_uri, magnetdl in items:
|
||||||
if isinstance(magnetdl, dict):
|
if isinstance(magnetdl, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -63,7 +69,9 @@ def update_log(shutdown_event):
|
|||||||
def check_deleted_files(shutdown_event):
|
def check_deleted_files(shutdown_event):
|
||||||
while not shutdown_event.is_set():
|
while not shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
for magnet_uri, magnetdl in list(state.active_downloads.items()):
|
with state.downloads_lock:
|
||||||
|
items = list(state.active_downloads.items())
|
||||||
|
for magnet_uri, magnetdl in items:
|
||||||
if isinstance(magnetdl, dict):
|
if isinstance(magnetdl, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -75,7 +83,8 @@ def check_deleted_files(shutdown_event):
|
|||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
consoleLog(f"Registered File Deletion: {status.name}")
|
consoleLog(f"Registered File Deletion: {status.name}")
|
||||||
state.dl_session.remove_torrent(magnetdl)
|
state.dl_session.remove_torrent(magnetdl)
|
||||||
del state.active_downloads[magnet_uri]
|
with state.downloads_lock:
|
||||||
|
del state.active_downloads[magnet_uri]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
consoleLog(f"Exception while checking for file deletions: {e}")
|
consoleLog(f"Exception while checking for file deletions: {e}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
@@ -2,6 +2,9 @@ from network.libtorrent_int import update_settings
|
|||||||
from utils.config.config import create_config
|
from utils.config.config import create_config
|
||||||
from utils.logging.logs import consoleLog
|
from utils.logging.logs import consoleLog
|
||||||
from utils.data.state import state
|
from utils.data.state import state
|
||||||
|
import qdarktheme
|
||||||
|
import platform
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +39,10 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
|
|||||||
state.image_position = image_position
|
state.image_position = image_position
|
||||||
if accent_color is not None:
|
if accent_color is not None:
|
||||||
state.accent_color = accent_color
|
state.accent_color = accent_color
|
||||||
|
custom_colors = {"primary": state.accent_color} if state.accent_color else {}
|
||||||
|
if state.window_transparency and platform.system() != "Windows":
|
||||||
|
custom_colors["background"] = "#00000000"
|
||||||
|
qdarktheme.setup_theme("auto", custom_colors=custom_colors if custom_colors else None)
|
||||||
if close_to_tray is not None:
|
if close_to_tray is not None:
|
||||||
state.close_to_tray = close_to_tray
|
state.close_to_tray = close_to_tray
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class AppState(QObject):
|
|||||||
self._image_opacity: int = 100
|
self._image_opacity: int = 100
|
||||||
self._image_as_wallpaper: bool = False
|
self._image_as_wallpaper: bool = False
|
||||||
self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center
|
self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center
|
||||||
|
self._image_custom_position: bool = False
|
||||||
|
self._image_x: int = 0
|
||||||
|
self._image_y: int = 0
|
||||||
|
|
||||||
# Trackers / Scraping
|
# Trackers / Scraping
|
||||||
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
|
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
|
||||||
|
|||||||
+30
-83
@@ -8,6 +8,27 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _downloads_file_path() -> str:
|
||||||
|
return os.path.join(state.settings_path, "downloads.json")
|
||||||
|
|
||||||
|
def _load_downloads() -> list[Download]:
|
||||||
|
downloads_file = _downloads_file_path()
|
||||||
|
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)
|
||||||
|
return [Download(**d) for d in existing_data.get("data", [])]
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
consoleLog(f"Error loading downloads.json: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _save_downloads(downloads: list[Download]) -> DownloadList:
|
||||||
|
download_list = DownloadList(data=downloads, count=len(downloads))
|
||||||
|
with open(_downloads_file_path(), "w") as file:
|
||||||
|
json.dump(asdict(download_list), file, indent=4)
|
||||||
|
return download_list
|
||||||
|
|
||||||
|
|
||||||
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
||||||
# wait for metadata outside the lock to avoid blocking other threads
|
# wait for metadata outside the lock to avoid blocking other threads
|
||||||
magnetdl = state.active_downloads.get(magnet_uri)
|
magnetdl = state.active_downloads.get(magnet_uri)
|
||||||
@@ -28,17 +49,7 @@ def add_download_log(title, url, magnet_uri, completed) -> DownloadList:
|
|||||||
return _add_download_log_inner(title, url, magnet_uri, completed, path)
|
return _add_download_log_inner(title, url, magnet_uri, completed, path)
|
||||||
|
|
||||||
def _add_download_log_inner(title, url, magnet_uri, completed, path) -> DownloadList:
|
def _add_download_log_inner(title, url, magnet_uri, completed, path) -> DownloadList:
|
||||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
downloads = _load_downloads()
|
||||||
|
|
||||||
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((magnet_uri and d.magnet_uri == magnet_uri) or (url and d.url == url) for d in downloads):
|
if any((magnet_uri and d.magnet_uri == magnet_uri) or (url and d.url == url) for d in downloads):
|
||||||
if magnet_uri and magnet_uri in state.active_downloads:
|
if magnet_uri and magnet_uri in state.active_downloads:
|
||||||
@@ -60,30 +71,14 @@ def _add_download_log_inner(title, url, magnet_uri, completed, path) -> Download
|
|||||||
))
|
))
|
||||||
|
|
||||||
consoleLog(f"Added {title} to Log File")
|
consoleLog(f"Added {title} to Log File")
|
||||||
|
return _save_downloads(downloads)
|
||||||
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 remove_download_log(magnet_uri) -> DownloadList:
|
def remove_download_log(magnet_uri) -> DownloadList:
|
||||||
with state.downloads_lock:
|
with state.downloads_lock:
|
||||||
return _remove_download_log_inner(magnet_uri)
|
return _remove_download_log_inner(magnet_uri)
|
||||||
|
|
||||||
def _remove_download_log_inner(magnet_uri) -> DownloadList:
|
def _remove_download_log_inner(magnet_uri) -> DownloadList:
|
||||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
downloads = _load_downloads()
|
||||||
|
|
||||||
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()
|
magnet_link = (magnet_uri or "").strip()
|
||||||
if not magnet_link:
|
if not magnet_link:
|
||||||
@@ -93,30 +88,14 @@ def _remove_download_log_inner(magnet_uri) -> DownloadList:
|
|||||||
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]
|
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 {title} from Log File")
|
consoleLog(f"Removed {title} from Log File")
|
||||||
|
return _save_downloads(downloads)
|
||||||
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:
|
def update_download_completed(magnet_uri, completed) -> DownloadList:
|
||||||
with state.downloads_lock:
|
with state.downloads_lock:
|
||||||
return _update_download_completed_inner(magnet_uri, completed)
|
return _update_download_completed_inner(magnet_uri, completed)
|
||||||
|
|
||||||
def _update_download_completed_inner(magnet_uri, completed) -> DownloadList:
|
def _update_download_completed_inner(magnet_uri, completed) -> DownloadList:
|
||||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
downloads = _load_downloads()
|
||||||
|
|
||||||
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()
|
identifier = (magnet_uri or "").strip()
|
||||||
if state.debug:
|
if state.debug:
|
||||||
@@ -145,13 +124,8 @@ def _update_download_completed_inner(magnet_uri, completed) -> DownloadList:
|
|||||||
consoleLog("No matching download found to update")
|
consoleLog("No matching download found to update")
|
||||||
return DownloadList(data=downloads, count=len(downloads))
|
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")
|
consoleLog("Updated download log")
|
||||||
|
return _save_downloads(downloads)
|
||||||
return download_list
|
|
||||||
|
|
||||||
|
|
||||||
def get_download_logs() -> DownloadList:
|
def get_download_logs() -> DownloadList:
|
||||||
@@ -159,18 +133,7 @@ def get_download_logs() -> DownloadList:
|
|||||||
return _get_download_logs_inner()
|
return _get_download_logs_inner()
|
||||||
|
|
||||||
def _get_download_logs_inner() -> DownloadList:
|
def _get_download_logs_inner() -> DownloadList:
|
||||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
downloads = _load_downloads()
|
||||||
|
|
||||||
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))
|
return DownloadList(data=downloads, count=len(downloads))
|
||||||
|
|
||||||
|
|
||||||
@@ -195,18 +158,7 @@ def update_download_completed_by_hash(info_hash, completed) -> DownloadList:
|
|||||||
return _update_download_completed_by_hash_inner(info_hash, completed)
|
return _update_download_completed_by_hash_inner(info_hash, completed)
|
||||||
|
|
||||||
def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadList:
|
def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadList:
|
||||||
downloads_file = os.path.join(state.settings_path, "downloads.json")
|
downloads = _load_downloads()
|
||||||
|
|
||||||
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()
|
info_hash_upper = (info_hash or "").upper().strip()
|
||||||
consoleLog(f"Updating download by hash: {info_hash_upper}")
|
consoleLog(f"Updating download by hash: {info_hash_upper}")
|
||||||
@@ -228,13 +180,8 @@ def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadLi
|
|||||||
consoleLog("No matching download found to update")
|
consoleLog("No matching download found to update")
|
||||||
return DownloadList(data=downloads, count=len(downloads))
|
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")
|
consoleLog("Updated download log")
|
||||||
|
return _save_downloads(downloads)
|
||||||
return download_list
|
|
||||||
|
|
||||||
|
|
||||||
def set_main_window(window):
|
def set_main_window(window):
|
||||||
|
|||||||
@@ -37,10 +37,16 @@ def run_download(post, headers: Optional[dict] = None):
|
|||||||
|
|
||||||
if ismagnet:
|
if ismagnet:
|
||||||
link = result
|
link = result
|
||||||
|
if not link:
|
||||||
|
consoleLog("Failed to retrieve magnet link")
|
||||||
|
return
|
||||||
add_magnet(link)
|
add_magnet(link)
|
||||||
add_download_log(post.get("title", "Unknown"), "", link, False)
|
add_download_log(post.get("title", "Unknown"), "", link, False)
|
||||||
else:
|
else:
|
||||||
link, link_headers = result if isinstance(result, tuple) else (result, None)
|
link, link_headers = result if isinstance(result, tuple) else (result, None)
|
||||||
|
if not link:
|
||||||
|
consoleLog("Failed to retrieve download link")
|
||||||
|
return
|
||||||
final_headers = headers or link_headers
|
final_headers = headers or link_headers
|
||||||
add_direct_download(link, post.get("title", "Unknown"), headers=final_headers, single_threaded=final_headers is not None)
|
add_direct_download(link, post.get("title", "Unknown"), headers=final_headers, single_threaded=final_headers is not None)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user