mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c09b1a994 | |||
| 1ffa1cacb9 | |||
| 207eb70f11 | |||
| 64144e5ed7 | |||
| d05fae7e87 | |||
| 9abdec6c33 | |||
| fb5eb61d05 | |||
| eff2164ecc | |||
| d8aa206ea2 | |||
| 6de74303ff | |||
| 5eb6314cb2 | |||
| 179dd0af83 | |||
| 365700900f | |||
| babbccdc9a | |||
| 367ffab86a | |||
| 71bad60617 | |||
| 27758f794c | |||
| d82afe8577 | |||
| 8592b3058e | |||
| 7a82e948b9 | |||
| 782b0b6bc9 | |||
| 71cc0d9f1f | |||
| 675dead1d9 | |||
| 64ca3c76a2 | |||
| d378ee8258 | |||
| e9b01426f8 | |||
| 631f6592ac | |||
| 468faff16b | |||
| 6b45c40aad | |||
| d6302fc43f | |||
| e768c7518b | |||
| 49c390131a | |||
| 9bc0e22ff7 | |||
| 982be09f6e | |||
| d5582c5c38 | |||
| 392d2a6f03 | |||
| 7310049246 | |||
| 04aaff3b29 | |||
| f5809c466a | |||
| 8f19703314 | |||
| 7d1f249797 | |||
| c20dc32de5 | |||
| b2bc97a6af | |||
| 15a771fe9f | |||
| 976d610baf | |||
| 42616f996d | |||
| 8601a1ed50 | |||
| a885576d8a | |||
| a8de8461bd | |||
| c1e4f3a688 | |||
| ca4020970a | |||
| 0956317eb2 | |||
| eed4af7705 | |||
| 82a2f2d4d7 | |||
| be1c9bfe45 |
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile \
|
||||
pyinstaller --windowed \
|
||||
--add-data "build_info.json:." \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
@@ -153,6 +153,10 @@ jobs:
|
||||
--name ${{ env.APP_NAME }} \
|
||||
src/main.py
|
||||
|
||||
- name: Install create-dmg (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install create-dmg
|
||||
|
||||
- name: Rename artifact
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -160,9 +164,20 @@ jobs:
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
||||
ls -la dist-final/${{ env.APP_NAME }}/
|
||||
else
|
||||
elif [ "$RUNNER_OS" == "Linux" ]; then
|
||||
chmod +x dist/${{ env.APP_NAME }}
|
||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
create-dmg \
|
||||
--volname "${{ env.APP_NAME }}" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 300 \
|
||||
--icon-size 100 \
|
||||
--icon "${{ env.APP_NAME }}.app" 150 150 \
|
||||
--hide-extension "${{ env.APP_NAME }}.app" \
|
||||
--app-drop-link 450 150 \
|
||||
"dist-final/${{ matrix.artifact_name }}.dmg" \
|
||||
"dist/"
|
||||
fi
|
||||
ls -la dist-final/
|
||||
|
||||
@@ -203,7 +218,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-build
|
||||
path: dist-final/${{ matrix.artifact_name }}
|
||||
path: dist-final/${{ matrix.artifact_name }}*
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
@@ -213,7 +228,7 @@ jobs:
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
@@ -225,8 +240,8 @@ jobs:
|
||||
cp artifacts/ubuntu-22.04-build/SoftwareManager-stable-*-linux release/SoftwareManager-stable-${SHORT_SHA}-linux
|
||||
cp artifacts/windows-latest-build/SoftwareManager-stable-*-windows.zip release/SoftwareManager-stable-${SHORT_SHA}-windows.zip
|
||||
cp artifacts/windows-latest-build/SoftwareManager-stable-*-windows-setup.exe release/SoftwareManager-stable-${SHORT_SHA}-windows-setup.exe
|
||||
cp artifacts/macos-latest-build/SoftwareManager-stable-*-macos release/SoftwareManager-stable-${SHORT_SHA}-macos
|
||||
chmod +x release/SoftwareManager-stable-*-linux release/SoftwareManager-stable-*-macos
|
||||
cp artifacts/macos-latest-build/SoftwareManager-stable-*-macos.dmg release/SoftwareManager-stable-${SHORT_SHA}-macos.dmg
|
||||
chmod +x release/SoftwareManager-stable-*-linux
|
||||
ls -la release/
|
||||
|
||||
- name: Generate checksums
|
||||
@@ -236,7 +251,7 @@ jobs:
|
||||
cat SHA256SUMS.txt
|
||||
|
||||
- name: Create Release (Public)
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
tag_name: ${{ needs.build.outputs.version }}
|
||||
@@ -246,5 +261,5 @@ jobs:
|
||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-linux
|
||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-windows.zip
|
||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-windows-setup.exe
|
||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-macos
|
||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-macos.dmg
|
||||
release/SHA256SUMS.txt
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -58,6 +58,15 @@ jobs:
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxcb-cursor0 libxcb-render-util0 libxcb-xkb1 \
|
||||
libxcb-icccm4 libxcb-keysyms1 libxcb-image0 \
|
||||
libxcb-shape0 libxcb-util1 libxkbcommon-x11-0
|
||||
|
||||
- name: Install Nuitka (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
run: uv pip install nuitka
|
||||
@@ -218,7 +227,7 @@ jobs:
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
@@ -266,7 +275,7 @@ jobs:
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create Release (Public)
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ needs.build.outputs.version }}
|
||||
name: ${{ needs.build.outputs.version }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: python scripts/sync_readme_dependencies.py
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
commit-message: "Sync README dependencies"
|
||||
branch: auto/sync-readme-dependencies
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false # keep other platforms building even if one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
pyinstaller --onefile \
|
||||
pyinstaller --windowed \
|
||||
--add-data "build_info.json:." \
|
||||
--exclude-module PyQt6 \
|
||||
--exclude-module PyQt5 \
|
||||
@@ -150,15 +150,30 @@ jobs:
|
||||
--name ${{ env.APP_NAME }} \
|
||||
src/main.py
|
||||
|
||||
- name: Install create-dmg (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install create-dmg
|
||||
|
||||
- name: Stage artifact
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist-final
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
||||
else
|
||||
elif [ "$RUNNER_OS" == "Linux" ]; then
|
||||
chmod +x dist/${{ env.APP_NAME }}
|
||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
create-dmg \
|
||||
--volname "${{ env.APP_NAME }} Test" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 300 \
|
||||
--icon-size 100 \
|
||||
--icon "${{ env.APP_NAME }}.app" 150 150 \
|
||||
--hide-extension "${{ env.APP_NAME }}.app" \
|
||||
--app-drop-link 450 150 \
|
||||
"dist-final/${{ matrix.artifact_name }}.dmg" \
|
||||
"dist/"
|
||||
fi
|
||||
ls -la dist-final/
|
||||
|
||||
@@ -181,5 +196,5 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-build
|
||||
path: dist-final/${{ matrix.artifact_name }}
|
||||
path: dist-final/${{ matrix.artifact_name }}*
|
||||
retention-days: 7
|
||||
|
||||
@@ -143,6 +143,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.python-version
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
@@ -41,24 +41,20 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
|
||||
|
||||
<!-- python-dependencies:start -->
|
||||
```text
|
||||
requests (2.33.1)
|
||||
PySide6-Essentials (6.11.0)
|
||||
requests (2.34.2)
|
||||
PySide6-Essentials (6.11.1)
|
||||
beautifulsoup4 (4.14.3)
|
||||
darkdetect (0.7.1)
|
||||
pyinstaller (6.19.0)
|
||||
PyQtDarkTheme-fork (2.3.4)
|
||||
pyinstaller (6.20.0)
|
||||
PyQtDarkTheme-fork (2.3.6)
|
||||
plyer (2.1.0)
|
||||
psutil (7.1.0)
|
||||
psutil (7.2.2)
|
||||
libtorrent (2.0.11)
|
||||
libtorrent-windows-dll (0.0.3)
|
||||
send2trash (2.1.0)
|
||||
```
|
||||
<!-- python-dependencies:end -->
|
||||
|
||||
## Activity
|
||||
|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#KeksPirates/SoftwareManager&type=date&legend=top-left">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Simply search and download software and games from various sources. Developed with ❤︎ by KeksPirates.">
|
||||
<meta name="darkreader-lock">
|
||||
<link rel="stylesheet" href="main.css"/>
|
||||
<title>SoftwareManager - Simply search and download software from various sources</title>
|
||||
@@ -10,6 +11,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://softwaremanager.xyz/</loc>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://softwaremanager.xyz/api</loc>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://softwaremanager.xyz/docs</loc>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
requests==2.33.1
|
||||
PySide6-Essentials==6.11.0
|
||||
requests==2.34.2
|
||||
PySide6-Essentials==6.11.1
|
||||
beautifulsoup4==4.14.3
|
||||
darkdetect==0.7.1
|
||||
pyinstaller==6.19.0
|
||||
PyQtDarkTheme-fork==2.3.4
|
||||
pyinstaller==6.20.0
|
||||
PyQtDarkTheme-fork==2.3.6
|
||||
plyer==2.1.0
|
||||
psutil==7.1.0
|
||||
psutil==7.2.2
|
||||
libtorrent==2.0.11
|
||||
libtorrent-windows-dll==0.0.3
|
||||
send2trash==2.1.0
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from utils.logging.logs import consoleLog
|
||||
import requests
|
||||
|
||||
def scrape_buzzheavier(url):
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
try:
|
||||
response = requests.get(url, timeout=15)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Buzzheavier: Failed to fetch page - {e}")
|
||||
return None
|
||||
|
||||
download_url = url + '/download'
|
||||
|
||||
@@ -13,6 +17,10 @@ def scrape_buzzheavier(url):
|
||||
'referer': url
|
||||
}
|
||||
|
||||
head_response = requests.head(download_url, headers=headers, allow_redirects=False)
|
||||
hx_redirect = head_response.headers.get('hx-redirect')
|
||||
return hx_redirect
|
||||
try:
|
||||
head_response = requests.head(download_url, headers=headers, allow_redirects=False, timeout=15)
|
||||
hx_redirect = head_response.headers.get('hx-redirect')
|
||||
return hx_redirect
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Buzzheavier: Failed to fetch download URL - {e}")
|
||||
return None
|
||||
+35
-33
@@ -23,42 +23,44 @@ def scrape_gofile(url):
|
||||
|
||||
content_id = match.group(1)
|
||||
session = requests.Session()
|
||||
try:
|
||||
# Create a guest account
|
||||
r = session.post(f"{_API_BASE}/accounts", headers={"User-Agent": _USER_AGENT}, timeout=15)
|
||||
data = r.json()
|
||||
if data.get("status") != "ok":
|
||||
consoleLog("GoFile: Failed to create guest account")
|
||||
return None
|
||||
|
||||
# Create a guest account
|
||||
r = session.post(f"{_API_BASE}/accounts", headers={"User-Agent": _USER_AGENT})
|
||||
data = r.json()
|
||||
if data.get("status") != "ok":
|
||||
consoleLog("GoFile: Failed to create guest account")
|
||||
return None
|
||||
account_token = data["data"]["token"]
|
||||
website_token = _generate_website_token(account_token)
|
||||
|
||||
account_token = data["data"]["token"]
|
||||
website_token = _generate_website_token(account_token)
|
||||
headers = {
|
||||
"User-Agent": _USER_AGENT,
|
||||
"Authorization": f"Bearer {account_token}",
|
||||
"X-Website-Token": website_token,
|
||||
"X-BL": "en-US",
|
||||
"Referer": "https://gofile.io/",
|
||||
"Origin": "https://gofile.io",
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": _USER_AGENT,
|
||||
"Authorization": f"Bearer {account_token}",
|
||||
"X-Website-Token": website_token,
|
||||
"X-BL": "en-US",
|
||||
"Referer": "https://gofile.io/",
|
||||
"Origin": "https://gofile.io",
|
||||
}
|
||||
# Fetch folder contents
|
||||
r = session.get(f"{_API_BASE}/contents/{content_id}", headers=headers, timeout=15)
|
||||
data = r.json()
|
||||
if data.get("status") != "ok":
|
||||
consoleLog(f"GoFile: API error - {data.get('status')}")
|
||||
return None
|
||||
|
||||
# Fetch folder contents
|
||||
r = session.get(f"{_API_BASE}/contents/{content_id}", headers=headers)
|
||||
data = r.json()
|
||||
if data.get("status") != "ok":
|
||||
consoleLog(f"GoFile: API error - {data.get('status')}")
|
||||
return None
|
||||
children = data["data"].get("children", {})
|
||||
if not children:
|
||||
consoleLog("GoFile: No files found")
|
||||
return None
|
||||
|
||||
children = data["data"].get("children", {})
|
||||
if not children:
|
||||
consoleLog("GoFile: No files found")
|
||||
return None
|
||||
first_child = next(iter(children.values()))
|
||||
link = first_child.get("link")
|
||||
if not link:
|
||||
consoleLog("GoFile: No download link in response")
|
||||
return None
|
||||
|
||||
first_child = next(iter(children.values()))
|
||||
link = first_child.get("link")
|
||||
if not link:
|
||||
consoleLog("GoFile: No download link in response")
|
||||
return None
|
||||
|
||||
return link, headers
|
||||
return link, headers
|
||||
finally:
|
||||
session.close()
|
||||
@@ -2,6 +2,7 @@ from utils.network.jsonhandler import split_data, format_data
|
||||
from utils.data.tracker import get_magnet_link
|
||||
from utils.logging.logs import consoleLog
|
||||
from utils.data.state import state
|
||||
from urllib.parse import quote
|
||||
from typing import Dict
|
||||
import requests
|
||||
|
||||
@@ -11,10 +12,18 @@ class RutrackerScraper:
|
||||
is_magnet = True
|
||||
|
||||
def search(self, query):
|
||||
search = requests.get(f"{state.api_url}/search?q={query}", timeout=15)
|
||||
try:
|
||||
search = requests.get(f"{state.api_url}/search?q={quote(query)}", timeout=15)
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"Failed to reach server: {e}")
|
||||
return []
|
||||
consoleLog("Sent request to server")
|
||||
if search:
|
||||
_, data, _, success, cached = split_data(search.text)
|
||||
try:
|
||||
_, data, _, success, cached = split_data(search.text)
|
||||
except (KeyError, ValueError) as e:
|
||||
consoleLog(f"Failed to parse server response: {e}")
|
||||
return []
|
||||
if cached:
|
||||
consoleLog("Server response cached")
|
||||
if success:
|
||||
|
||||
@@ -27,7 +27,7 @@ class SteamripScraper:
|
||||
|
||||
try:
|
||||
url = f"https://steamrip.com/games-list-page/"
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, timeout=15)
|
||||
response.raise_for_status()
|
||||
text = response.text
|
||||
except requests.RequestException as e:
|
||||
@@ -61,7 +61,7 @@ class SteamripScraper:
|
||||
|
||||
def scrape_steamrip_game_downloads(self, url):
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, timeout=15)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
consoleLog(f"SteamRip: Failed to fetch game page - {e}")
|
||||
@@ -110,23 +110,19 @@ class SteamripScraper:
|
||||
consoleLog("SteamRip: No valid download links found")
|
||||
return None, None
|
||||
|
||||
try:
|
||||
idx = links.index(best)
|
||||
except ValueError:
|
||||
consoleLog("SteamRip: Failed to resolve download host")
|
||||
return None, None
|
||||
|
||||
try:
|
||||
if idx == 0:
|
||||
consoleLog(f"Found download link: {best}")
|
||||
if "buzzheavier" or "bzzhr" in best:
|
||||
link = scrape_buzzheavier(best)
|
||||
return link
|
||||
elif idx == 1:
|
||||
elif "gofile" in best:
|
||||
link, headers = scrape_gofile(best)
|
||||
return link, headers
|
||||
elif idx == 2:
|
||||
elif "vikingfile" in best:
|
||||
scrape_vikingfile(best)
|
||||
return None, None
|
||||
elif idx == 3:
|
||||
elif "megadb" in best:
|
||||
scrape_megadb(best)
|
||||
return None, None
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,7 @@ class UztrackerScraper:
|
||||
posts = []
|
||||
|
||||
try:
|
||||
response = requests.get(search_url)
|
||||
response = requests.get(search_url, timeout=15)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
links = soup.find_all('tr', class_="tCenter hl-tr", id=lambda x: x and x.startswith('tor_'))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from PySide6.QtCore import Qt, QPoint, Signal, QThread
|
||||
from network.direct_download.handle import DirectDownloadHandle
|
||||
from utils.logging.logs import consoleLog, remove_download_log
|
||||
from utils.network.download import download_selected
|
||||
from utils.data.tracker import get_magnet_link
|
||||
@@ -11,7 +12,6 @@ import threading
|
||||
import webbrowser
|
||||
import subprocess
|
||||
import platform
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
@@ -121,7 +121,12 @@ class ContextMenu_Downloads:
|
||||
|
||||
remove_download_log(magnet_link)
|
||||
|
||||
if state.dl_session:
|
||||
if isinstance(magnetdl, DirectDownloadHandle):
|
||||
try:
|
||||
magnetdl.stop()
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while stopping direct download: {e}")
|
||||
elif state.dl_session:
|
||||
try:
|
||||
state.dl_session.remove_torrent(magnetdl)
|
||||
except Exception as e:
|
||||
@@ -179,29 +184,24 @@ class ContextMenu_Downloads:
|
||||
|
||||
remove_download_log(magnet_link)
|
||||
|
||||
if state.dl_session:
|
||||
if isinstance(magnetdl, DirectDownloadHandle):
|
||||
try:
|
||||
magnetdl.stop()
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while stopping direct download: {e}")
|
||||
elif state.dl_session:
|
||||
try:
|
||||
state.dl_session.remove_torrent(magnetdl)
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while removing download from LibTorrent: {e}")
|
||||
|
||||
if download_path and os.path.exists(download_path):
|
||||
last_error = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
if os.path.isfile(download_path) or os.path.isdir(download_path):
|
||||
send2trash(download_path)
|
||||
consoleLog(f"Deleted files for: {torrent_name}", True)
|
||||
last_error = None
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < 2:
|
||||
time.sleep(0.5)
|
||||
|
||||
if last_error:
|
||||
consoleLog(f"Error deleting files: {last_error}", True)
|
||||
try:
|
||||
if os.path.isfile(download_path) or os.path.isdir(download_path):
|
||||
send2trash(download_path)
|
||||
consoleLog(f"Deleted files for: {torrent_name}", True)
|
||||
except Exception as e:
|
||||
consoleLog(f"Error deleting files: {e}", True)
|
||||
else:
|
||||
consoleLog(f"Removed entry (files not found): {torrent_name}", True)
|
||||
|
||||
|
||||
@@ -79,9 +79,8 @@ def settings_dialog(self):
|
||||
|
||||
# Ignore Updates checkbox
|
||||
update_checkbox_container, update_checkbox = create_widget(QCheckBox, "Ignore Updates: ")
|
||||
if platform.system() == "Windows":
|
||||
update_checkbox.setChecked(state.ignore_updates)
|
||||
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
|
||||
update_checkbox.setChecked(state.ignore_updates)
|
||||
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
|
||||
|
||||
# Autoresume Container Checkbox
|
||||
autoresume_container, autoresume_checkbox = create_widget(QCheckBox, "Auto-Resume Downloads: ")
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox, QCheckBox
|
||||
from utils.network.update_checker import get_updates
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox
|
||||
from utils.network.updater import download_update
|
||||
from utils.config.config import create_config
|
||||
from utils.logging.logs import consoleLog
|
||||
from utils.data.state import state
|
||||
from pathlib import Path
|
||||
import webbrowser
|
||||
import platform
|
||||
import json
|
||||
import os
|
||||
|
||||
RELEASES_URL = "https://github.com/KeksPirates/SoftwareManager/releases/latest"
|
||||
|
||||
def _get_build_info_path() -> (Path | None):
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
|
||||
@@ -31,19 +35,49 @@ def get_version() -> None:
|
||||
else:
|
||||
consoleLog(f"Could not find build info file (Path: {build_info_path})")
|
||||
|
||||
|
||||
def _show_notify_dialog(latest: str) -> None:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText(f"A new version is available: <b>{latest}</b>")
|
||||
msg.setInformativeText("Visit the releases page to download it.")
|
||||
|
||||
dont_show = QCheckBox("Don't show again")
|
||||
msg.setCheckBox(dont_show)
|
||||
|
||||
open_btn = msg.addButton("Open Releases Page", QMessageBox.ButtonRole.AcceptRole)
|
||||
msg.addButton("Dismiss", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
msg.exec()
|
||||
|
||||
if dont_show.isChecked():
|
||||
state.ignore_updates = True
|
||||
create_config()
|
||||
|
||||
if msg.clickedButton() == open_btn:
|
||||
webbrowser.open(RELEASES_URL)
|
||||
|
||||
|
||||
class UpdateDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
if state.ignore_updates is False and platform.system() == "Windows":
|
||||
if state.ignore_updates:
|
||||
return
|
||||
|
||||
assets, latest = get_updates()
|
||||
if assets != None:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText(f"A new version is available\n({latest})")
|
||||
msg.setInformativeText("Press Ok to download.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(assets)
|
||||
assets, latest = get_updates()
|
||||
if assets is None:
|
||||
return
|
||||
|
||||
if platform.system() == "Windows":
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Icon.Information)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setText(f"A new version is available\n({latest})")
|
||||
msg.setInformativeText("Press Ok to download.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
|
||||
response = msg.exec_()
|
||||
if response == QMessageBox.StandardButton.Ok:
|
||||
download_update(assets)
|
||||
else:
|
||||
_show_notify_dialog(latest)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from interface.utils.searchhelper import run_search
|
||||
from interface.utils.tabhelper import create_tab
|
||||
from utils.general.shutdown import closehelper
|
||||
from utils.general.wrappers import run_thread
|
||||
from utils.logging.logs import log_emitter
|
||||
from interface.dialogs.image import Image
|
||||
from utils.data.state import state
|
||||
|
||||
@@ -68,14 +69,12 @@ class CenteredDelegate(QStyledItemDelegate):
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
eventFilter = eventFilter
|
||||
log_signal = Signal(str)
|
||||
search_results_signal = Signal(list)
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
MainWindow._instance = self
|
||||
self.log_signal.connect(self._on_log_signal)
|
||||
self.search_results_signal.connect(self._on_search_results)
|
||||
|
||||
pixmap = QPixmap()
|
||||
@@ -132,6 +131,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
self.consoleLog = QTextEdit()
|
||||
self.statusbar = QStatusBar()
|
||||
|
||||
log_emitter.new_log.connect(self._on_log_signal)
|
||||
flush_log_buffer()
|
||||
|
||||
self._tracker_elided_delegate = ElidedItemDelegate(lambda: self._tracker_hovered_row, self)
|
||||
@@ -307,6 +307,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
table.setItemDelegateForColumn(i, self._tracker_hover_delegate)
|
||||
|
||||
def _start_search(self):
|
||||
self.searchbar.setEnabled(False)
|
||||
state.trackertable.setRowCount(0)
|
||||
self.show_empty_results(False)
|
||||
|
||||
def _search_thread():
|
||||
try:
|
||||
run_search(self)
|
||||
@@ -315,13 +319,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
|
||||
self.search_results_signal.emit([])
|
||||
run_thread(threading.Thread(target=_search_thread))
|
||||
|
||||
@staticmethod
|
||||
def add_log(text):
|
||||
if hasattr(MainWindow, "_instance") and MainWindow._instance:
|
||||
MainWindow._instance.log_signal.emit(text)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_log_signal(self, text):
|
||||
if hasattr(self, 'consoleLog'):
|
||||
self.consoleLog.append(text)
|
||||
|
||||
@@ -6,9 +6,6 @@ for scraper in SCRAPERS:
|
||||
state.trackers[scraper.name] = scraper
|
||||
|
||||
def run_search(self) -> None:
|
||||
state.trackertable.setRowCount(0)
|
||||
self.show_empty_results(False)
|
||||
|
||||
search_text = self.searchbar.text()
|
||||
# Check for empty search
|
||||
if search_text == "":
|
||||
@@ -16,7 +13,6 @@ def run_search(self) -> None:
|
||||
return
|
||||
|
||||
state.posts = []
|
||||
self.searchbar.setEnabled(False)
|
||||
consoleLog(f"User searched for: {search_text}")
|
||||
|
||||
# Get current tracker and call its search function
|
||||
|
||||
+8
-6
@@ -1,4 +1,3 @@
|
||||
from network.libtorrent_misc import send_notification, update_log, check_deleted_files
|
||||
from utils.logging.loghandler import split_data, check_completed, check_downloads
|
||||
from network.interface import list_interfaces, init_interfaces
|
||||
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||
@@ -9,12 +8,13 @@ from utils.general.shutdown import closehelper
|
||||
from utils.general.wrappers import run_thread
|
||||
from interface.assets.base64_icons import logo_base64
|
||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu
|
||||
from network.libtorrent_misc import AppDaemons
|
||||
from utils.general.shutdown import force_exit
|
||||
from utils.config.config import read_config
|
||||
from PySide6.QtGui import QAction, QPixmap
|
||||
from utils.logging.logs import consoleLog
|
||||
from interface.gui import MainWindow
|
||||
from PySide6.QtCore import Qt, QObject
|
||||
from PySide6.QtCore import Qt, QObject, QThread
|
||||
from utils.data.state import state
|
||||
from PySide6 import QtWidgets
|
||||
import qdarktheme
|
||||
@@ -45,7 +45,6 @@ class SingleInstance(QObject):
|
||||
self.server.listen(SERVER_NAME)
|
||||
self.server.newConnection.connect(self.handle_connection)
|
||||
self.is_running = False
|
||||
self.is_running = False
|
||||
|
||||
def handle_connection(self):
|
||||
socket = self.server.nextPendingConnection()
|
||||
@@ -143,9 +142,12 @@ def main():
|
||||
init_interfaces()
|
||||
consoleLog(f"Current Bound: {state.bound_interface}")
|
||||
# Start background daemon threads
|
||||
run_thread(threading.Thread(target=send_notification, args=(state.shutdown_event,), daemon=True))
|
||||
run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True))
|
||||
run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True))
|
||||
app._daemon_thread = QThread()
|
||||
app._daemons = AppDaemons()
|
||||
app._daemons.moveToThread(app._daemon_thread)
|
||||
app._daemon_thread.started.connect(app._daemons.start_all)
|
||||
app.aboutToQuit.connect(app._daemon_thread.quit)
|
||||
app._daemon_thread.start()
|
||||
# Start background non-daemon threads
|
||||
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
|
||||
run_thread(threading.Thread(target=check_downloads, args=(downloads,)))
|
||||
|
||||
@@ -20,12 +20,14 @@ def add_direct_download(url: str, title: str, dl_path: Optional[str] = None, hea
|
||||
consoleLog(f"Download already active: {title}")
|
||||
return
|
||||
|
||||
filename = (
|
||||
raw_filename = (
|
||||
detect_filename_from_headers(url, DirectDownloadHandle.USER_AGENT)
|
||||
or extract_filename_from_url(url)
|
||||
or sanitize_filename(title) + ".zip"
|
||||
)
|
||||
|
||||
filename = sanitize_filename(raw_filename)
|
||||
|
||||
handle = DirectDownloadHandle(url, filename, dl_path, headers, single_threaded)
|
||||
with state.downloads_lock:
|
||||
state.active_downloads[url] = handle
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
@@ -231,8 +232,24 @@ class DirectDownloadHandle:
|
||||
def _preallocate_file(self, total_size: int):
|
||||
if os.path.exists(self._file_path) and os.path.getsize(self._file_path) == total_size:
|
||||
return
|
||||
|
||||
with open(self._file_path, "wb") as f:
|
||||
f.truncate(total_size)
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
import ctypes
|
||||
from ctypes.wintypes import DWORD
|
||||
|
||||
handle = msvcrt.get_osfhandle(f.fileno())
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
bytes_returned = DWORD()
|
||||
kernel32.DeviceIoControl(
|
||||
handle, 590020, None, 0, None, 0, ctypes.byref(bytes_returned), None
|
||||
)
|
||||
kernel32.SetFilePointerEx(handle, ctypes.c_int64(total_size), None, 0)
|
||||
kernel32.SetEndOfFile(handle)
|
||||
else:
|
||||
f.truncate(total_size)
|
||||
|
||||
def _compute_chunks(self, total_size: int) -> list[ChunkSpec]:
|
||||
num_chunks = min(self.NUM_THREADS, max(1, total_size // self.MIN_CHUNK_SIZE))
|
||||
|
||||
@@ -34,7 +34,7 @@ def detect_filename_from_headers(url: str, user_agent: str) -> Optional[str]:
|
||||
if "filename=" in cd:
|
||||
parts = cd.split("filename=")
|
||||
if len(parts) > 1:
|
||||
fname = parts[-1].strip().strip('"').strip("'")
|
||||
fname = parts[1].split(";")[0].strip().strip('"').strip("'")
|
||||
if fname:
|
||||
return fname
|
||||
except Exception as e:
|
||||
|
||||
@@ -163,7 +163,7 @@ def add_seed(magnet_uri, file_path):
|
||||
return False
|
||||
with state.downloads_lock:
|
||||
state.active_downloads[magnet_uri] = handle
|
||||
state.seeded_magnets.add(magnet_uri)
|
||||
state.seeded_magnets.add(magnet_uri)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from utils.logging.logs import update_download_completed_by_hash
|
||||
from utils.logging.logs import consoleLog
|
||||
from PySide6.QtCore import QObject, QTimer
|
||||
from utils.data.state import state
|
||||
from plyer import notification
|
||||
import libtorrent as lt
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
@@ -19,55 +19,28 @@ def cleanup_session():
|
||||
with state.downloads_lock:
|
||||
state.active_downloads.clear()
|
||||
|
||||
def send_notification(shutdown_event):
|
||||
notified = set()
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
with state.downloads_lock:
|
||||
items = list(state.active_downloads.items())
|
||||
for magnet_uri, magnetdl in items:
|
||||
if isinstance(magnetdl, dict):
|
||||
continue
|
||||
class AppDaemons(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
status = magnetdl.status()
|
||||
self.notified_magnets = set()
|
||||
self.updated_magnets = set()
|
||||
|
||||
if status.state == lt.torrent_status.seeding and magnet_uri not in notified:
|
||||
notification.notify(
|
||||
title="Download finished",
|
||||
message=f"{status.name} has finished downloading.",
|
||||
timeout=4
|
||||
)
|
||||
notified.add(magnet_uri)
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while sending notification: {e}")
|
||||
time.sleep(5)
|
||||
# Create Timers
|
||||
self.deleted_files_timer = QTimer(self)
|
||||
self.completed_downloads_timer = QTimer(self)
|
||||
|
||||
def update_log(shutdown_event):
|
||||
updated = set()
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
with state.downloads_lock:
|
||||
items = list(state.active_downloads.items())
|
||||
for magnet_uri, magnetdl in items:
|
||||
if isinstance(magnetdl, dict):
|
||||
continue
|
||||
# Connect Timer Signals
|
||||
self.deleted_files_timer.timeout.connect(self.check_deleted_files)
|
||||
self.completed_downloads_timer.timeout.connect(self.check_completed_downloads)
|
||||
|
||||
status = magnetdl.status()
|
||||
def start_all(self):
|
||||
consoleLog("Starting Timer: deleted_files")
|
||||
self.deleted_files_timer.start(2000)
|
||||
consoleLog("Starting Timer: completed_downloads")
|
||||
self.completed_downloads_timer.start(5000)
|
||||
|
||||
if status.state == lt.torrent_status.seeding and magnet_uri not in updated and magnet_uri not in state.seeded_magnets:
|
||||
consoleLog(f"Marking {status.name} as completed")
|
||||
if hasattr(status, 'info_hashes'):
|
||||
info_hash = str(status.info_hashes.v1)
|
||||
else:
|
||||
info_hash = str(status.info_hash)
|
||||
update_download_completed_by_hash(info_hash, True)
|
||||
updated.add(magnet_uri)
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while updating log file: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def check_deleted_files(shutdown_event):
|
||||
while not shutdown_event.is_set():
|
||||
def check_deleted_files(self):
|
||||
try:
|
||||
with state.downloads_lock:
|
||||
items = list(state.active_downloads.items())
|
||||
@@ -87,4 +60,36 @@ def check_deleted_files(shutdown_event):
|
||||
del state.active_downloads[magnet_uri]
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while checking for file deletions: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def check_completed_downloads(self):
|
||||
try:
|
||||
with state.downloads_lock:
|
||||
items = list(state.active_downloads.items())
|
||||
seeded = set(state.seeded_magnets)
|
||||
for magnet_uri, magnetdl in items:
|
||||
if isinstance(magnetdl, dict):
|
||||
continue
|
||||
|
||||
status = magnetdl.status()
|
||||
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
|
||||
if magnet_uri not in self.updated_magnets and magnet_uri not in seeded:
|
||||
consoleLog(f"Marking {status.name} as completed")
|
||||
if hasattr(status, 'info_hashes'):
|
||||
info_hash = str(status.info_hashes.v1)
|
||||
else:
|
||||
info_hash = str(status.info_hash)
|
||||
update_download_completed_by_hash(info_hash, True)
|
||||
self.updated_magnets.add(magnet_uri)
|
||||
|
||||
if magnet_uri not in self.notified_magnets:
|
||||
notification.notify(
|
||||
title="Download finished",
|
||||
message=f"{status.name} has finished downloading.",
|
||||
timeout=4
|
||||
)
|
||||
self.notified_magnets.add(magnet_uri)
|
||||
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while checking downloads: {e}")
|
||||
@@ -61,6 +61,7 @@ class AppState(QObject):
|
||||
self.max_connections: int = 200
|
||||
self.max_downloads: int = 10
|
||||
self.downloads_lock = threading.RLock()
|
||||
self._log_lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def image_path(self) -> str:
|
||||
|
||||
+14
-14
@@ -1,4 +1,5 @@
|
||||
from utils.data.models import Download, DownloadList
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from utils.data.state import state
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
@@ -7,6 +8,10 @@ import time
|
||||
import os
|
||||
import re
|
||||
|
||||
class LogSignal(QObject):
|
||||
new_log = Signal(str)
|
||||
|
||||
log_emitter = LogSignal()
|
||||
|
||||
def _downloads_file_path() -> str:
|
||||
return os.path.join(state.settings_path, "downloads.json")
|
||||
@@ -187,27 +192,22 @@ def _update_download_completed_by_hash_inner(info_hash, completed) -> DownloadLi
|
||||
def set_main_window(window):
|
||||
state.main_window = window
|
||||
|
||||
def flush_log_buffer(): # credits to claude
|
||||
if state.log_buffer:
|
||||
try:
|
||||
from interface.gui import MainWindow
|
||||
for log_entry in state.log_buffer:
|
||||
MainWindow.add_log(log_entry)
|
||||
state.log_buffer = []
|
||||
except Exception as e:
|
||||
consoleLog(f"Exception while flushing log buffer: {e}")
|
||||
def flush_log_buffer():
|
||||
with state._log_lock:
|
||||
for text in state.log_buffer:
|
||||
log_emitter.new_log.emit(text)
|
||||
|
||||
state.log_buffer.clear()
|
||||
|
||||
def consoleLog(text, printAnyways = False):
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M:%S")
|
||||
formatted_text = f"[{current_time}] {text}"
|
||||
|
||||
try:
|
||||
from interface.gui import MainWindow
|
||||
if not MainWindow.add_log(formatted_text):
|
||||
state.log_buffer.append(formatted_text)
|
||||
except Exception:
|
||||
with state._log_lock:
|
||||
state.log_buffer.append(formatted_text)
|
||||
|
||||
log_emitter.new_log.emit(formatted_text)
|
||||
|
||||
if state.debug or printAnyways:
|
||||
print(formatted_text)
|
||||
|
||||
@@ -2,13 +2,12 @@ from network.direct_download.handle import DirectDownloadHandle
|
||||
from utils.general.shutdown import closehelper
|
||||
from utils.logging.logs import consoleLog
|
||||
from utils.data.state import state
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, QTimer, QEventLoop
|
||||
from PySide6 import QtWidgets
|
||||
import libtorrent as lt
|
||||
import subprocess
|
||||
import tempfile
|
||||
import hashlib
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -42,7 +41,6 @@ def download_update(assets: list):
|
||||
progress = QtWidgets.QProgressDialog("Downloading update... (0.0 MB/s)", None, 0, 100)
|
||||
progress.setWindowTitle("Updating")
|
||||
progress.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
progress.setCancelButton(None)
|
||||
progress.setMinimumDuration(0)
|
||||
progress.setAutoClose(False)
|
||||
progress.setAutoReset(False)
|
||||
@@ -55,14 +53,19 @@ def download_update(assets: list):
|
||||
handle = DirectDownloadHandle(url, filename, tempfile.gettempdir())
|
||||
handle.start()
|
||||
|
||||
while True:
|
||||
QtWidgets.QApplication.processEvents()
|
||||
loop = QEventLoop()
|
||||
timer = QTimer()
|
||||
timer.setInterval(100)
|
||||
|
||||
def poll_progress():
|
||||
status = handle.status()
|
||||
|
||||
if status.error:
|
||||
state.down_speed_limit = original_limit
|
||||
progress.close()
|
||||
consoleLog(f"Update download failed: {status.error}")
|
||||
timer.stop()
|
||||
loop.quit()
|
||||
return
|
||||
|
||||
if status.total_wanted > 0:
|
||||
@@ -72,9 +75,25 @@ def download_update(assets: list):
|
||||
progress.setLabelText(f"Downloading update... ({speed_mb:.1f} MB/s)")
|
||||
|
||||
if status.state == lt.torrent_status.seeding:
|
||||
break
|
||||
timer.stop()
|
||||
loop.quit()
|
||||
|
||||
time.sleep(0.1)
|
||||
if progress.wasCanceled():
|
||||
state.down_speed_limit = original_limit
|
||||
consoleLog("Update download cancelled by user.")
|
||||
timer.stop()
|
||||
loop.quit()
|
||||
return
|
||||
|
||||
timer.timeout.connect(poll_progress)
|
||||
timer.start()
|
||||
loop.exec()
|
||||
|
||||
if progress.wasCanceled():
|
||||
return
|
||||
|
||||
if handle.status().error:
|
||||
return
|
||||
|
||||
state.down_speed_limit = original_limit
|
||||
|
||||
@@ -84,6 +103,11 @@ def download_update(assets: list):
|
||||
else:
|
||||
progress.close()
|
||||
consoleLog("Error: Invalid file hash, file may be corrupted")
|
||||
QtWidgets.QMessageBox.critical(
|
||||
None,
|
||||
"Update Failed",
|
||||
"Error: Invalid file hash, file may be corrupted"
|
||||
)
|
||||
sys.exit(0)
|
||||
else:
|
||||
consoleLog("Skipping hash verification (no hash found for release)")
|
||||
@@ -94,5 +118,4 @@ def download_update(assets: list):
|
||||
|
||||
closehelper()
|
||||
subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"])
|
||||
time.sleep(1)
|
||||
os._exit(0)
|
||||
Reference in New Issue
Block a user