diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9997bbe --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,185 @@ +name: Build Executables (Test) + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + PYTHON_VERSION: "3.12" + APP_NAME: "SoftwareManager" + +jobs: + build: + name: Build ${{ matrix.os }} + strategy: + fail-fast: false # keep other platforms building even if one fails + matrix: + include: + - os: ubuntu-22.04 + artifact_name: SoftwareManager-test-${{ github.sha }}-linux + - os: windows-latest + artifact_name: SoftwareManager-test-${{ github.sha }}-windows + - os: macos-latest + artifact_name: SoftwareManager-test-${{ github.sha }}-macos + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.9.7" + enable-cache: true + + - name: Install Python dependencies + run: | + uv pip install -r requirements.txt + uv pip install Pillow + env: + UV_SYSTEM_PYTHON: 1 + + - name: Install Nuitka (Windows only) + if: runner.os == 'Windows' + run: uv pip install nuitka + env: + UV_SYSTEM_PYTHON: 1 + + - name: Generate version + id: version + shell: bash + run: | + SHORT_SHA="${{ github.sha }}" + SHORT_SHA="${SHORT_SHA:0:7}" + BRANCH="${{ github.head_ref || github.ref_name }}" + BRANCH_SLUG="${BRANCH//\//-}" # replace slashes with dashes + VERSION="${BRANCH_SLUG}-${SHORT_SHA}-test" + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "Generated version: $VERSION" + + - name: Inject build info + shell: bash + run: | + cat > build_info.json << EOF + { + "version": "${{ env.VERSION }}" + } + EOF + + - name: Install UPX (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip + 7z x upx.zip -oupx-dir + UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64" + echo "$UPX_DIR" >> $GITHUB_PATH + export PATH="$UPX_DIR:$PATH" + echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV + upx --version | head -1 + + - name: Convert icon for Windows + if: runner.os == 'Windows' + shell: bash + run: | + python -c " + from PIL import Image + img = Image.open('src/interface/assets/logo.png') + img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)]) + print('Icon converted successfully') + " + + - name: Build with Nuitka (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + python -m nuitka \ + --standalone \ + --assume-yes-for-downloads \ + --windows-console-mode=disable \ + --windows-icon-from-ico=app_icon.ico \ + --enable-plugin=pyside6 \ + --enable-plugin=upx \ + --upx-binary="${{ env.UPX_BINARY }}" \ + --noinclude-qt-translations \ + --include-data-files=build_info.json=build_info.json \ + --output-filename=${{ env.APP_NAME }}.exe \ + --output-dir=nuitka-build \ + src/main.py + + - name: Build with PyInstaller (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + pyinstaller --onefile --windowed \ + --add-data "build_info.json:." \ + --exclude-module PyQt6 \ + --exclude-module PyQt5 \ + --hidden-import=backports.tarfile \ + --hidden-import=backports \ + --hidden-import=jaraco.context \ + --hidden-import=jaraco.text \ + --hidden-import=jaraco.functools \ + --name ${{ env.APP_NAME }} \ + src/main.py + + - name: Build with PyInstaller (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + pyinstaller --onefile \ + --add-data "build_info.json:." \ + --exclude-module PyQt6 \ + --exclude-module PyQt5 \ + --hidden-import=backports.tarfile \ + --hidden-import=backports \ + --hidden-import=jaraco.context \ + --hidden-import=jaraco.text \ + --hidden-import=jaraco.functools \ + --name ${{ env.APP_NAME }} \ + src/main.py + + - 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 + chmod +x dist/${{ env.APP_NAME }} + mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }} + fi + ls -la dist-final/ + + - name: Create ZIP (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip" + + - name: Upload artifact (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }}-build + path: dist-final/${{ matrix.artifact_name }}.zip + retention-days: 7 + + - name: Upload artifact (Linux/macOS) + if: runner.os != 'Windows' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }}-build + path: dist-final/${{ matrix.artifact_name }} + retention-days: 7 diff --git a/README.md b/README.md index ccc2eae..751f26e 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ SoftwareManager is a Python-based GUI tool that simplifies searching and downloa Star History Chart ---- **Disclaimer:** SoftwareManager is intended for legal and ethical use only. Ensure compliance with applicable laws and regulations when using this tool. diff --git a/src/main.py b/src/main.py index 84f9552..4cdcf70 100644 --- a/src/main.py +++ b/src/main.py @@ -59,6 +59,7 @@ def main(): # 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,))) + run_thread(threading.Thread(target=check_space, args=())) # Finish counting startup time elapsed = time.perf_counter() - start_time consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI") @@ -66,4 +67,4 @@ def main(): run_gui() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/network/libtorrent_int.py b/src/network/libtorrent_int.py index 42350ec..c1bb694 100644 --- a/src/network/libtorrent_int.py +++ b/src/network/libtorrent_int.py @@ -21,6 +21,25 @@ def get_free_space_mb(dirname): st = os.statvfs(dirname) return st.f_bavail * st.f_frsize +def check_space(): + while not state.shutdown_event.is_set(): + for magnet_uri, magnetdl in list(state.active_downloads.items()): + try: + status = magnetdl.status() + except RuntimeError: + continue + + if status.state == lt.torrent_status.downloading: + free_space = get_free_space_mb(state.download_path) + total_size = status.total_wanted + + if free_space < total_size: + consoleLog(f"Not enough free space to continue downloading: {status.name}") + magnetdl.pause() + break + + time.sleep(5) + def init_session(): if state.dl_session is not None: return