mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aba367247e | |||
| 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 |
@@ -141,7 +141,7 @@ jobs:
|
|||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pyinstaller --onefile \
|
pyinstaller --windowed \
|
||||||
--add-data "build_info.json:." \
|
--add-data "build_info.json:." \
|
||||||
--exclude-module PyQt6 \
|
--exclude-module PyQt6 \
|
||||||
--exclude-module PyQt5 \
|
--exclude-module PyQt5 \
|
||||||
@@ -153,6 +153,10 @@ jobs:
|
|||||||
--name ${{ env.APP_NAME }} \
|
--name ${{ env.APP_NAME }} \
|
||||||
src/main.py
|
src/main.py
|
||||||
|
|
||||||
|
- name: Install create-dmg (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: brew install create-dmg
|
||||||
|
|
||||||
- name: Rename artifact
|
- name: Rename artifact
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -160,9 +164,20 @@ jobs:
|
|||||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||||
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
||||||
ls -la dist-final/${{ env.APP_NAME }}/
|
ls -la dist-final/${{ env.APP_NAME }}/
|
||||||
else
|
elif [ "$RUNNER_OS" == "Linux" ]; then
|
||||||
chmod +x dist/${{ env.APP_NAME }}
|
chmod +x dist/${{ env.APP_NAME }}
|
||||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_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
|
fi
|
||||||
ls -la dist-final/
|
ls -la dist-final/
|
||||||
|
|
||||||
@@ -203,7 +218,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v7
|
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 }}*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -225,8 +240,8 @@ jobs:
|
|||||||
cp artifacts/ubuntu-22.04-build/SoftwareManager-stable-*-linux release/SoftwareManager-stable-${SHORT_SHA}-linux
|
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.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/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
|
cp artifacts/macos-latest-build/SoftwareManager-stable-*-macos.dmg release/SoftwareManager-stable-${SHORT_SHA}-macos.dmg
|
||||||
chmod +x release/SoftwareManager-stable-*-linux release/SoftwareManager-stable-*-macos
|
chmod +x release/SoftwareManager-stable-*-linux
|
||||||
ls -la release/
|
ls -la release/
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
@@ -236,7 +251,7 @@ jobs:
|
|||||||
cat SHA256SUMS.txt
|
cat SHA256SUMS.txt
|
||||||
|
|
||||||
- name: Create Release (Public)
|
- name: Create Release (Public)
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.RELEASE_TOKEN }}
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
tag_name: ${{ needs.build.outputs.version }}
|
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 }}-linux
|
||||||
release/SoftwareManager-stable-${{ needs.build.outputs.short_sha }}-windows.zip
|
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 }}-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
|
release/SHA256SUMS.txt
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
UV_SYSTEM_PYTHON: 1
|
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)
|
- name: Install Nuitka (Windows only)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: uv pip install nuitka
|
run: uv pip install nuitka
|
||||||
@@ -266,7 +275,7 @@ jobs:
|
|||||||
cat release_notes.md
|
cat release_notes.md
|
||||||
|
|
||||||
- name: Create Release (Public)
|
- name: Create Release (Public)
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.build.outputs.version }}
|
tag_name: ${{ needs.build.outputs.version }}
|
||||||
name: ${{ needs.build.outputs.version }}
|
name: ${{ needs.build.outputs.version }}
|
||||||
@@ -278,4 +287,4 @@ jobs:
|
|||||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
|
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
run: python scripts/sync_readme_dependencies.py
|
run: python scripts/sync_readme_dependencies.py
|
||||||
|
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
commit-message: "Sync README dependencies"
|
commit-message: "Sync README dependencies"
|
||||||
branch: auto/sync-readme-dependencies
|
branch: auto/sync-readme-dependencies
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build ${{ matrix.os }}
|
name: Build ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false # keep other platforms building even if one fails
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pyinstaller --onefile \
|
pyinstaller --windowed \
|
||||||
--add-data "build_info.json:." \
|
--add-data "build_info.json:." \
|
||||||
--exclude-module PyQt6 \
|
--exclude-module PyQt6 \
|
||||||
--exclude-module PyQt5 \
|
--exclude-module PyQt5 \
|
||||||
@@ -150,15 +150,30 @@ jobs:
|
|||||||
--name ${{ env.APP_NAME }} \
|
--name ${{ env.APP_NAME }} \
|
||||||
src/main.py
|
src/main.py
|
||||||
|
|
||||||
|
- name: Install create-dmg (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: brew install create-dmg
|
||||||
|
|
||||||
- name: Stage artifact
|
- name: Stage artifact
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist-final
|
mkdir -p dist-final
|
||||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||||
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
|
||||||
else
|
elif [ "$RUNNER_OS" == "Linux" ]; then
|
||||||
chmod +x dist/${{ env.APP_NAME }}
|
chmod +x dist/${{ env.APP_NAME }}
|
||||||
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_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
|
fi
|
||||||
ls -la dist-final/
|
ls -la dist-final/
|
||||||
|
|
||||||
@@ -181,5 +196,5 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v7
|
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 }}*
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
.python-version
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
|||||||
@@ -41,24 +41,20 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa
|
|||||||
|
|
||||||
<!-- python-dependencies:start -->
|
<!-- python-dependencies:start -->
|
||||||
```text
|
```text
|
||||||
requests (2.33.1)
|
requests (2.34.2)
|
||||||
PySide6-Essentials (6.11.0)
|
PySide6-Essentials (6.11.1)
|
||||||
beautifulsoup4 (4.14.3)
|
beautifulsoup4 (4.14.3)
|
||||||
darkdetect (0.7.1)
|
darkdetect (0.7.1)
|
||||||
pyinstaller (6.19.0)
|
pyinstaller (6.20.0)
|
||||||
PyQtDarkTheme-fork (2.3.4)
|
PyQtDarkTheme-fork (2.3.6)
|
||||||
plyer (2.1.0)
|
plyer (2.1.0)
|
||||||
psutil (7.1.0)
|
psutil (7.2.2)
|
||||||
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 -->
|
<!-- python-dependencies:end -->
|
||||||
|
|
||||||
## Activity
|
|
||||||
|
|
||||||

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