mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Compare commits
61 Commits
204ef0f-stable
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4c6c3381a | |||
| 68a9e870cf | |||
| 02de32719d | |||
| c5a78a2da3 | |||
| 32f02d856d | |||
| 4edb1baa0b | |||
| 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 | |||
| 02bb77ea1e | |||
| 82a2f2d4d7 | |||
| be1c9bfe45 |
@@ -46,6 +46,21 @@ jobs:
|
||||
version: "0.9.7"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxcb-xinerama0 \
|
||||
libxkbcommon-x11-0 \
|
||||
libxcb-cursor0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-shape0 \
|
||||
libxcb-util1
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv pip install -r requirements.txt
|
||||
@@ -141,7 +156,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 +168,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 +179,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 +233,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:
|
||||
@@ -225,8 +255,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 +266,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 +276,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
|
||||
|
||||
@@ -51,6 +51,21 @@ jobs:
|
||||
version: "0.9.7"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxcb-xinerama0 \
|
||||
libxkbcommon-x11-0 \
|
||||
libxcb-cursor0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-shape0 \
|
||||
libxcb-util1
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv pip install -r requirements.txt
|
||||
@@ -58,6 +73,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
|
||||
@@ -266,7 +290,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 }}
|
||||
@@ -278,4 +302,4 @@ jobs:
|
||||
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
|
||||
release/SHA256SUMS.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -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
|
||||
@@ -41,6 +41,21 @@ jobs:
|
||||
version: "0.9.7"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libxcb-xinerama0 \
|
||||
libxkbcommon-x11-0 \
|
||||
libxcb-cursor0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-shape0 \
|
||||
libxcb-util1
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv pip install -r requirements.txt
|
||||
@@ -138,7 +153,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 +165,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 +211,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">
|
||||
|
||||
+53
-5
@@ -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">
|
||||
|
||||
@@ -37,9 +39,10 @@
|
||||
<h1>KeksPirates</h1>
|
||||
</div>
|
||||
<nav class="tab">
|
||||
<a class="tablinks" onclick="openTab(event, 'start')" id="defaultOpen">Home</a>
|
||||
<a class="tablinks" onclick="openTab(event, 'api')">API</a>
|
||||
<a class="tablinks" onclick="openTab(event, 'docs')">Docs</a>
|
||||
<a class="tablinks" onclick="openTab(event, 'start')" id="navHome">Home</a>
|
||||
<a class="tablinks" onclick="openTab(event, 'api')" id="navApi">API</a>
|
||||
<a class="tablinks" onclick="openTab(event, 'docs')" id="navDocs">Docs</a>
|
||||
<a class="tablinks" id="navVst" href="https://vstsearch.softwaremanager.xyz">VST Search</a>
|
||||
<!-- <a class="tablinks" onclick="openTab(event, 'notfound')">404</a> -->
|
||||
</nav>
|
||||
</header>
|
||||
@@ -185,6 +188,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="vstsearch" class="tabcontent">
|
||||
<div class="cse-search">
|
||||
<h3 class="vst-logo"><span style="color: #db65d5">VST Search</span></h3>
|
||||
<div class="gcse-searchbox"></div>
|
||||
|
||||
<div class="gcse-searchresults"></div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notfound" class="tabcontent">
|
||||
<div class="notfound-frame">
|
||||
<h1 class="notfound-title">404</h1>
|
||||
@@ -201,6 +215,20 @@
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const MAIN_ORIGIN = 'https://softwaremanager.xyz';
|
||||
const VST_ORIGIN = 'https://vstsearch.softwaremanager.xyz';
|
||||
const onVstDomain = window.location.hostname.startsWith('vstsearch.');
|
||||
|
||||
let cseLoaded = false;
|
||||
function loadCSE() {
|
||||
if (cseLoaded) return;
|
||||
cseLoaded = true;
|
||||
var s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = 'https://cse.google.com/cse.js?cx=e7334cef72a8b42e7';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function openTab(evt, tabName, pushToHistory = true) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
@@ -216,6 +244,8 @@
|
||||
|
||||
var target = document.getElementById(tabName);
|
||||
target.style.display = "block";
|
||||
|
||||
if (tabName === 'vstsearch') loadCSE();
|
||||
// restart animation
|
||||
target.classList.remove('tab-animate');
|
||||
void target.offsetWidth;
|
||||
@@ -245,6 +275,10 @@
|
||||
|
||||
// Determine the correct tab immediately (before full load) to prevent flash
|
||||
(function() {
|
||||
if (onVstDomain) {
|
||||
document.getElementById('vstsearch').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
let currentPath = window.location.pathname;
|
||||
let tabName = 'start';
|
||||
if (currentPath === '/api') tabName = 'api';
|
||||
@@ -255,9 +289,23 @@
|
||||
})();
|
||||
|
||||
window.onload = function() {
|
||||
if (onVstDomain) {
|
||||
document.getElementById('navHome').href = MAIN_ORIGIN + '/';
|
||||
document.getElementById('navApi').href = MAIN_ORIGIN + '/api';
|
||||
document.getElementById('navDocs').href = MAIN_ORIGIN + '/docs';
|
||||
['navHome', 'navApi', 'navDocs'].forEach(function(id) {
|
||||
document.getElementById(id).removeAttribute('onclick');
|
||||
});
|
||||
openTab(null, 'vstsearch', false);
|
||||
document.getElementById('navVst').className += " active";
|
||||
return;
|
||||
}
|
||||
|
||||
let currentPath = window.location.pathname;
|
||||
|
||||
if (currentPath === '/api') {
|
||||
|
||||
if (currentPath === '/vstsearch') {
|
||||
window.location.replace(VST_ORIGIN);
|
||||
} else if (currentPath === '/api') {
|
||||
openTab(null, 'api', false);
|
||||
} else if (currentPath === '/docs') {
|
||||
openTab(null, 'docs', false);
|
||||
|
||||
@@ -378,6 +378,84 @@ pre.api-response {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.cse-search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 70vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cse-search .vst-logo {
|
||||
font-family: var(--main-font);
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 1.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cse-search .gsc-control-cse,
|
||||
.cse-search .gsc-results-wrapper-overlay,
|
||||
.cse-search .gsc-results-wrapper-visible {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.cse-search .gsc-input-box,
|
||||
.cse-search .gsc-input-box.gsc-input-box-hover,
|
||||
.cse-search .gsc-input-box.gsc-input-box-focus {
|
||||
background: #1a1b1d !important;
|
||||
border-color: #1a1b1d !important;
|
||||
}
|
||||
|
||||
.cse-search .gsc-input,
|
||||
.cse-search input.gsc-input,
|
||||
.cse-search .gsib_a input.gsc-input {
|
||||
background: transparent !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.cse-search .gsib_a {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'><path fill-rule='evenodd' clip-rule='evenodd' d='M14.76 13.27L20.49 19L19 20.49L13.27 14.76C12.2 15.53 10.91 16 9.5 16C5.91 16 3 13.09 3 9.5C3 5.91 5.91 3 9.5 3C13.09 3 16 5.91 16 9.5C16 10.91 15.53 12.2 14.76 13.27ZM9.5 5C7.01 5 5 7.01 5 9.5C5 11.99 7.01 14 9.5 14C11.99 14 14 11.99 14 9.5C14 7.01 11.99 5 9.5 5Z' fill='%23ffffff'/></svg>") !important;
|
||||
}
|
||||
|
||||
.cse-search .gsc-search-button-v2 {
|
||||
background: #db65d5 !important;
|
||||
border-color: #db65d5 !important;
|
||||
}
|
||||
|
||||
.cse-search .gsc-search-button-v2:hover {
|
||||
background: #c94dc3 !important;
|
||||
border-color: #c94dc3 !important;
|
||||
}
|
||||
|
||||
.cse-search .gcsc-more-maybe-branding-root,
|
||||
.gcsc-find-more-on-google {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.gsc-completion-container,
|
||||
.gssb_c, .gssb_e,
|
||||
table.gssb_c, table.gssb_e {
|
||||
background: #1a1b1d !important;
|
||||
border-color: #333 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.gsc-completion-container td,
|
||||
.gsc-completion-container .gsc-completion-suggestion,
|
||||
.gssb_a, .gssb_a td, .gssb_i {
|
||||
background: #1a1b1d !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.gsc-completion-container .gsc-completion-selected,
|
||||
.gsc-completion-container tr:hover td,
|
||||
.gssb_a:hover td, .gssb_e-hover {
|
||||
background: #2a2b2d !important;
|
||||
}
|
||||
|
||||
@media(max-width: 560px) {
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,55 @@
|
||||
// Cloudflare Pages middleware.
|
||||
//
|
||||
// Both softwaremanager.xyz and vstsearch.softwaremanager.xyz serve the same
|
||||
// index.html. Crawlers (Discord, Twitter, etc.) read the raw HTML and do NOT
|
||||
// run JavaScript, so we rewrite the <title> and Open Graph / Twitter meta tags
|
||||
// server-side when the request is for the VST Search subdomain. This gives the
|
||||
// subdomain its own link embed without a separate deploy.
|
||||
|
||||
const VST = {
|
||||
title: "VST Search - SoftwareManager",
|
||||
description: "Search for plugins easily",
|
||||
url: "https://vstsearch.softwaremanager.xyz/",
|
||||
};
|
||||
|
||||
class AttributeSetter {
|
||||
constructor(attribute, value) {
|
||||
this.attribute = attribute;
|
||||
this.value = value;
|
||||
}
|
||||
element(element) {
|
||||
element.setAttribute(this.attribute, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class ContentSetter {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
element(element) {
|
||||
element.setInnerContent(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, next } = context;
|
||||
const response = await next();
|
||||
|
||||
const onVstDomain = new URL(request.url).hostname.startsWith("vstsearch.");
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
|
||||
// Leave everything untouched on the main domain and for non-HTML responses.
|
||||
if (!onVstDomain || !contentType.includes("text/html")) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return new HTMLRewriter()
|
||||
.on("title", new ContentSetter(VST.title))
|
||||
.on("meta[name='description']", new AttributeSetter("content", VST.description))
|
||||
.on("meta[property='og:title']", new AttributeSetter("content", VST.title))
|
||||
.on("meta[property='og:description']", new AttributeSetter("content", VST.description))
|
||||
.on("meta[property='og:url']", new AttributeSetter("content", VST.url))
|
||||
.on("meta[name='twitter:title']", new AttributeSetter("content", VST.title))
|
||||
.on("meta[name='twitter:description']", new AttributeSetter("content", VST.description))
|
||||
.transform(response);
|
||||
}
|
||||
+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'
|
||||
|
||||
@@ -12,7 +16,11 @@ def scrape_buzzheavier(url):
|
||||
'hx-request': 'true',
|
||||
'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,12 +13,11 @@ 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
|
||||
scraper = state.trackers[state.currenttracker]
|
||||
state.posts = scraper.search(search_text)
|
||||
|
||||
|
||||
# Update GUI headers to match the current scraper
|
||||
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 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
|
||||
|
||||
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)
|
||||
class AppDaemons(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
self.notified_magnets = set()
|
||||
self.updated_magnets = set()
|
||||
|
||||
def check_deleted_files(shutdown_event):
|
||||
while not shutdown_event.is_set():
|
||||
# Create Timers
|
||||
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:
|
||||
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}")
|
||||
@@ -25,7 +25,7 @@ class AppState(QObject):
|
||||
|
||||
# GUI
|
||||
self.window_transparency: bool = False
|
||||
self.accent_color: str = ""
|
||||
self.accent_color: str = "#fda7d7"
|
||||
self.trackertable: QTableWidget
|
||||
self.interfaces: List = []
|
||||
self.active_interfaces: List = []
|
||||
@@ -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