├── .bumpversion.cfg ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── dockerhub-description.yml │ ├── nightly.yml │ ├── pre-commit.yaml │ ├── pull_requests.yml │ └── release.yml ├── .gitignore ├── .grenrc.yml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── logo.png ├── logo.psd └── logov2.png ├── build.spec ├── config.example.toml ├── pyproject.toml ├── qBitrr ├── __init__.py ├── arss.py ├── bundled_data.py ├── config.py ├── env_config.py ├── errors.py ├── ffprobe.py ├── gen_config.py ├── home_path.py ├── logger.py ├── main.py ├── tables.py └── utils.py ├── qBitrr2.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── entry_points.txt ├── requires.txt └── top_level.txt ├── requirements.all.txt ├── requirements.dev.txt ├── requirements.fast.txt ├── requirements.txt ├── setup.cfg └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.10.23 3 | tag = false 4 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 5 | serialize = 6 | {major}.{minor}.{patch} 7 | 8 | [bumpversion:part:build] 9 | 10 | [bumpversion:file:setup.cfg] 11 | 12 | [bumpversion:file:qBitrr/bundled_data.py] 13 | 14 | [bumpversion:file:Dockerfile] 15 | 16 | [bumpversion:file:pyproject.toml] 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .gitattributes 5 | 6 | .bumpversion.cfg 7 | .grenrc.yml 8 | .pre-commit-config.yaml 9 | 10 | CHANGELOG.md 11 | README.md 12 | config.example.toml 13 | requirements.all.txt 14 | requirements.dev.txt 15 | requirements.txt 16 | 17 | dist/ 18 | build/ 19 | qBitrr.egg-info/ 20 | venv/ 21 | .venv/ 22 | .sourcery.yaml 23 | test.py 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior for all files. 2 | * text=auto eol=lf 3 | 4 | # Normalized and converts to native line endings on checkout. 5 | *.py text 6 | *.pyx text 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [feramance] 4 | patreon: qBitrr 5 | ko_fi: feramance 6 | custom: ["https://www.paypal.me/feramance"] -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: daily 9 | 10 | - package-ecosystem: pip-compile 11 | directory: / 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: 15 0 * * 5 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: [python] 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v3 28 | with: 29 | languages: ${{ matrix.language }} 30 | - name: Autobuild 31 | uses: github/codeql-action/autobuild@v3 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v3 35 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - README.md 8 | - .github/workflows/dockerhub-description.yml 9 | workflow_dispatch: 10 | jobs: 11 | dockerHubDescription: 12 | name: Update Docker Hub Description 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Docker Hub Description 18 | uses: peter-evans/dockerhub-description@v4 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_TOKEN }} 22 | repository: feramance/qbitrr 23 | short-description: ${{ github.event.repository.description }} 24 | enable-url-completion: true 25 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | env: 13 | project-name: qBitrr 14 | 15 | jobs: 16 | docker_image: 17 | name: Build Docker Image 18 | runs-on: ubuntu-latest 19 | permissions: 20 | packages: write 21 | contents: read 22 | if: ${{ ! startsWith(github.event.head_commit.message, '[patch]') && ! startsWith(github.event.head_commit.message, '[minor]') && ! startsWith(github.event.head_commit.message, '[major]') }} 23 | steps: 24 | - id: string 25 | uses: ASzc/change-string-case-action@v6 26 | with: 27 | string: ${{ github.repository }} 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v3 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | - name: Login to DockerHub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | - name: Login to Container registry 42 | uses: docker/login-action@v3 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.REG_TOKEN }} 47 | - name: Extract metadata (tags, labels) for Docker 48 | id: meta 49 | uses: docker/metadata-action@v5 50 | with: 51 | images: | 52 | feramance/qbitrr 53 | ghcr.io/${{ steps.string.outputs.lowercase }} 54 | tags: | 55 | type=edge 56 | - name: Build and push Docker images 57 | env: 58 | DOCKER_BUILDKIT: 1 59 | uses: docker/build-push-action@v6 60 | with: 61 | context: . 62 | platforms: linux/amd64 63 | push: true 64 | tags: feramance/qbitrr:nightly,ghcr.io/${{ steps.string.outputs.lowercase }}:nightly 65 | labels: ${{ steps.meta.outputs.labels }} 66 | cache-from: type=gha 67 | cache-to: type=gha,mode=max 68 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main, master] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.10' 17 | - uses: pre-commit/action@v3.0.1 18 | -------------------------------------------------------------------------------- /.github/workflows/pull_requests.yml: -------------------------------------------------------------------------------- 1 | name: PR Build Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - releases/** 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | env: 13 | project-name: qBitrr 14 | 15 | jobs: 16 | package: 17 | if: github.event.pull_request.draft == false 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python: 23 | - '3.10' 24 | os: 25 | - windows-latest 26 | - macOS-latest 27 | - ubuntu-latest 28 | arch: 29 | - x86 30 | - x64 31 | - arm64 32 | exclude: 33 | - os: ubuntu-latest 34 | arch: x86 35 | - os: ubuntu-latest 36 | arch: arm64 37 | - os: macOS-latest 38 | arch: x64 39 | - os: macOS-latest 40 | arch: x86 41 | - os: windows-latest 42 | arch: arm64 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | ref: ${{ github.event.pull_request.head.sha }} 49 | - name: Set up Python ${{ matrix.python }} 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python}} 53 | architecture: ${{ matrix.arch }} 54 | - name: Install APT dependencies 55 | if: runner.os == 'Linux' 56 | run: | 57 | sudo apt-get update 58 | sudo apt-get install libsdl2-dev 59 | - name: Get git hash 60 | run: | 61 | echo "Current Hash: $(git rev-parse --short HEAD)" 62 | echo "HASH=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 63 | id: git_hash 64 | - name: Set archive name 65 | run: | 66 | ARCHIVE_NAME=${{ env.project-name }}-${{ steps.git_hash.outputs.HASH }}-${{ matrix.os }}-${{ matrix.arch }} 67 | echo "Archive name set to: $ARCHIVE_NAME" 68 | echo "NAME=$ARCHIVE_NAME" >> $GITHUB_OUTPUT 69 | id: archieve 70 | - name: Update git hash 71 | run: | 72 | sed -i -e 's/git_hash = \".*\"/git_hash = \"${{ steps.git_hash.outputs.HASH }}\"/g' ./qBitrr/bundled_data.py 73 | - name: Retrieve current version 74 | run: | 75 | echo "Current version: $(python setup.py --version)" 76 | echo "VERSION=$(python setup.py --version)" >> $GITHUB_OUTPUT 77 | id: current_version 78 | - name: Install Python dependencies 79 | run: | 80 | python -m pip install -U pip 81 | python -m pip install -U setuptools==69.5.1 82 | python -m pip install -U wheel 83 | python -m pip install -r requirements.dev.txt 84 | - name: Run PyInstaller 85 | env: 86 | PYTHONOPTIMIZE: 1 # Enable optimizations as if the -O flag is given. 87 | PYTHONHASHSEED: 42 # Try to ensure deterministic results. 88 | PYTHONUNBUFFERED: 1 89 | run: | 90 | pyinstaller build.spec 91 | # This step exists for debugging. Such as checking if data files were included correctly by PyInstaller. 92 | - name: List distribution files 93 | run: | 94 | find dist 95 | # Archive the PyInstaller build using the appropriate tool for the platform. 96 | - name: Tar files 97 | if: runner.os != 'Windows' 98 | run: | 99 | tar --format=ustar -czvf ${{ steps.archieve.outputs.NAME }}.tar.gz dist/ 100 | - name: Archive files 101 | if: runner.os == 'Windows' 102 | shell: pwsh 103 | run: | 104 | Compress-Archive dist/* ${{ steps.archieve.outputs.NAME }}.zip 105 | # Upload archives as artifacts, these can be downloaded from the GitHub actions page. 106 | - name: Upload Artifact 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: automated-build-${{ steps.archieve.outputs.NAME }} 110 | path: ${{ steps.archieve.outputs.NAME }}.* 111 | if-no-files-found: error 112 | docker_image: 113 | name: Build Docker Image 114 | runs-on: ubuntu-latest 115 | permissions: 116 | packages: write 117 | contents: read 118 | steps: 119 | - id: string 120 | uses: ASzc/change-string-case-action@v6 121 | with: 122 | string: ${{ github.repository }} 123 | - name: Checkout 124 | uses: actions/checkout@v4 125 | with: 126 | ref: ${{ github.event.pull_request.head.ref }} 127 | repository: ${{ github.event.pull_request.head.repo.full_name }} 128 | token: ${{ secrets.PAT }} 129 | - name: Set up QEMU 130 | uses: docker/setup-qemu-action@v3 131 | - name: Set up Docker Buildx 132 | uses: docker/setup-buildx-action@v3 133 | - name: Login to DockerHub 134 | uses: docker/login-action@v3 135 | with: 136 | username: ${{ secrets.DOCKERHUB_USERNAME }} 137 | password: ${{ secrets.DOCKERHUB_TOKEN }} 138 | - name: Login to Container registry 139 | uses: docker/login-action@v3 140 | with: 141 | registry: ghcr.io 142 | username: ${{ github.actor }} 143 | password: ${{ secrets.REG_TOKEN }} 144 | - name: Extract metadata (tags, labels) for Docker 145 | id: meta 146 | uses: docker/metadata-action@v5 147 | with: 148 | images: | 149 | feramance/qbitrr 150 | ghcr.io/${{ steps.string.outputs.lowercase }} 151 | tags: | 152 | type=ref,event=pr 153 | - name: Build and push Docker images 154 | env: 155 | DOCKER_BUILDKIT: 1 156 | uses: docker/build-push-action@v6 157 | with: 158 | context: . 159 | platforms: linux/amd64 160 | push: true 161 | tags: ${{ steps.meta.outputs.tags }} 162 | labels: ${{ steps.meta.outputs.labels }} 163 | cache-from: type=gha 164 | cache-to: type=gha,mode=max 165 | docker_image_arm: 166 | name: Build ARM Docker Image 167 | runs-on: ubuntu-latest 168 | permissions: 169 | packages: write 170 | contents: read 171 | steps: 172 | - id: string 173 | uses: ASzc/change-string-case-action@v6 174 | with: 175 | string: ${{ github.repository }} 176 | - name: Checkout 177 | uses: actions/checkout@v4 178 | with: 179 | ref: ${{ github.event.pull_request.head.ref }} 180 | repository: ${{ github.event.pull_request.head.repo.full_name }} 181 | token: ${{ secrets.PAT }} 182 | - name: Set up QEMU 183 | uses: docker/setup-qemu-action@v3 184 | - name: Set up Docker Buildx 185 | uses: docker/setup-buildx-action@v3 186 | - name: Login to DockerHub 187 | uses: docker/login-action@v3 188 | with: 189 | username: ${{ secrets.DOCKERHUB_USERNAME }} 190 | password: ${{ secrets.DOCKERHUB_TOKEN }} 191 | - name: Login to Container registry 192 | uses: docker/login-action@v3 193 | with: 194 | registry: ghcr.io 195 | username: ${{ github.actor }} 196 | password: ${{ secrets.REG_TOKEN }} 197 | - name: Extract metadata (tags, labels) for ARM Docker 198 | id: meta-arm 199 | uses: docker/metadata-action@v5 200 | with: 201 | images: | 202 | feramance/qbitrr 203 | ghcr.io/${{ steps.string.outputs.lowercase }} 204 | tags: | 205 | type=ref,event=pr,suffix=-arm 206 | - name: Build and push ARM Docker images 207 | env: 208 | DOCKER_BUILDKIT: 1 209 | uses: docker/build-push-action@v6 210 | with: 211 | context: . 212 | platforms: linux/arm64 213 | push: true 214 | tags: ${{ steps.meta-arm.outputs.tags }} 215 | labels: ${{ steps.meta-arm.outputs.labels }} 216 | cache-from: type=gha 217 | cache-to: type=gha,mode=max 218 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create a Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | env: 12 | project-name: qBitrr 13 | GREN_GITHUB_TOKEN: ${{ secrets.PAT }} 14 | 15 | jobs: 16 | bump_version: 17 | name: Bump release version 18 | runs-on: ubuntu-latest 19 | if: ${{ startsWith(github.event.head_commit.message, '[patch]') || startsWith(github.event.head_commit.message, '[minor]')|| startsWith(github.event.head_commit.message, '[major]') }} 20 | env: 21 | RELEASE_TYPE: ${{ startsWith(github.event.head_commit.message, '[patch]') && 'patch' || startsWith(github.event.head_commit.message, '[minor]') && 'minor' || startsWith(github.event.head_commit.message, '[major]') && 'major' }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.PAT }} 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.x 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install bump2version setuptools wheel twine 34 | - name: Retrieve current version 35 | run: | 36 | echo "Current version: $(python setup.py --version)" 37 | echo "VERSION=$(python setup.py --version)" >> $GITHUB_OUTPUT 38 | id: current_version 39 | - name: Bump Patch Version 40 | run: | 41 | bump2version --current-version $(python setup.py --version) ${{ env.RELEASE_TYPE }} 42 | - name: Retrieve new version 43 | run: | 44 | echo "VERSION=$(python setup.py --version)" >> $GITHUB_OUTPUT 45 | id: new_version 46 | - name: Import GPG key 47 | uses: crazy-max/ghaction-import-gpg@v6.2.0 48 | with: 49 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 50 | passphrase: ${{ secrets.PASSPHRASE }} 51 | git_user_signingkey: true 52 | git_commit_gpgsign: true 53 | git_tag_gpgsign: true 54 | id: import_gpg 55 | - name: Git Auto Commit 56 | uses: stefanzweifel/git-auto-commit-action@v5.0.1 57 | with: 58 | commit_message: '[skip ci]Automated version bump: v${{ github.job.get_current_version.current_version.outputs.VERSION }} >> v${{ steps.new_version.outputs.VERSION }}' 59 | tagging_message: v${{ steps.new_version.outputs.VERSION }} 60 | commit_options: -S 61 | commit_user_name: ${{ steps.import_gpg.outputs.name }} 62 | commit_user_email: ${{ steps.import_gpg.outputs.email }} 63 | commit_author: ${{ steps.import_gpg.outputs.name }} <${{ steps.import_gpg.outputs.email }}> 64 | outputs: 65 | RELEASE_TYPE: ${{ env.RELEASE_TYPE }} 66 | NEW_RELEASE: ${{ steps.new_version.outputs.VERSION }} 67 | release: 68 | name: Create a GitHub Release 69 | needs: [bump_version] 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | with: 74 | ref: master 75 | token: ${{ secrets.PAT }} 76 | - name: Set up Python 77 | uses: actions/setup-python@v5 78 | with: 79 | python-version: 3.x 80 | - name: Install dependencies 81 | run: | 82 | python -m pip install --upgrade pip 83 | pip install setuptools wheel twine 84 | - name: Import GPG key 85 | uses: crazy-max/ghaction-import-gpg@v6.2.0 86 | with: 87 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 88 | passphrase: ${{ secrets.PASSPHRASE }} 89 | git_user_signingkey: true 90 | git_commit_gpgsign: true 91 | git_tag_gpgsign: true 92 | id: import_gpg 93 | - name: Create GitHub Release 94 | uses: softprops/action-gh-release@v1 95 | with: 96 | token: ${{ secrets.PAT }} 97 | tag_name: v${{ needs.bump_version.outputs.NEW_RELEASE }} 98 | name: v${{ needs.bump_version.outputs.NEW_RELEASE }} 99 | draft: false 100 | prerelease: false 101 | release_hash: 102 | name: Update The Version Hash 103 | needs: [bump_version, release] 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v4 107 | with: 108 | token: ${{ secrets.PAT }} 109 | fetch-depth: 0 110 | ref: master 111 | - name: Set up Python 112 | uses: actions/setup-python@v5 113 | with: 114 | python-version: 3.x 115 | - name: Install dependencies 116 | run: | 117 | python -m pip install --upgrade pip 118 | pip install setuptools wheel twine 119 | - name: Get git hash 120 | run: | 121 | echo "Current Hash: $(git rev-parse --short HEAD)" 122 | echo "HASH=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 123 | id: git_hash 124 | - name: Update git hash 125 | run: | 126 | sed -i -e 's/git_hash = \".*\"/git_hash = \"${{ steps.git_hash.outputs.HASH }}\"/g' ./qBitrr/bundled_data.py 127 | - name: Import GPG key 128 | uses: crazy-max/ghaction-import-gpg@v6.2.0 129 | with: 130 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 131 | passphrase: ${{ secrets.PASSPHRASE }} 132 | git_user_signingkey: true 133 | git_commit_gpgsign: true 134 | git_tag_gpgsign: true 135 | id: import_gpg 136 | - name: Git Auto Commit 137 | uses: stefanzweifel/git-auto-commit-action@v5.0.1 138 | with: 139 | commit_message: '[skip ci] Update Release Hash for v${{needs.bump_version.outputs.NEW_RELEASE}}' 140 | commit_options: -S 141 | commit_user_name: ${{ steps.import_gpg.outputs.name }} 142 | commit_user_email: ${{ steps.import_gpg.outputs.email }} 143 | commit_author: ${{ steps.import_gpg.outputs.name }} <${{ steps.import_gpg.outputs.email }}> 144 | outputs: 145 | RELEASE_HASH: ${{ steps.git_hash.outputs.HASH }} 146 | publish: 147 | name: Publish to PyPi 148 | needs: [bump_version, release, release_hash, docker_image] 149 | runs-on: ubuntu-latest 150 | steps: 151 | - uses: actions/checkout@v4 152 | with: 153 | fetch-depth: 0 154 | ref: master 155 | - name: Set up Python 156 | uses: actions/setup-python@v5 157 | with: 158 | python-version: 3.x 159 | - name: Install dependencies 160 | run: | 161 | python -m pip install --upgrade pip 162 | pip install build wheel 163 | - name: Build package 164 | run: python -m build 165 | - name: Publish package 166 | uses: pypa/gh-action-pypi-publish@release/v1 167 | with: 168 | user: __token__ 169 | password: ${{ secrets.PYPI_API_TOKEN }} 170 | package: 171 | name: Build Binaries 172 | needs: [bump_version, release, release_hash, docker_image] 173 | runs-on: ${{ matrix.os }} 174 | strategy: 175 | fail-fast: false 176 | matrix: 177 | python: 178 | - 3.10.11 179 | os: 180 | - windows-latest 181 | - macOS-latest 182 | - ubuntu-latest 183 | arch: 184 | - x86 185 | - x64 186 | - arm64 187 | exclude: 188 | - os: ubuntu-latest 189 | arch: x86 190 | - os: ubuntu-latest 191 | arch: arm64 192 | - os: macOS-latest 193 | arch: x64 194 | - os: macOS-latest 195 | arch: x86 196 | - os: windows-latest 197 | arch: arm64 198 | steps: 199 | - name: Checkout code 200 | uses: actions/checkout@v4 201 | with: 202 | ref: master 203 | fetch-depth: 0 204 | - name: Install Homebrew dependencies 205 | if: runner.os == 'MacOS' 206 | run: | 207 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 208 | - name: Set up Python ${{ matrix.python }} 209 | uses: actions/setup-python@v5 210 | with: 211 | python-version: ${{ matrix.python}} 212 | architecture: ${{ matrix.arch }} 213 | - name: Install APT dependencies 214 | if: runner.os == 'Linux' 215 | run: | 216 | sudo apt-get update 217 | sudo apt-get install libsdl2-dev 218 | - name: Set archive name 219 | run: | 220 | ARCHIVE_NAME=${{ env.project-name }}-${{ needs.release_hash.outputs.RELEASE_HASH }}-${{ matrix.os }}-${{ matrix.arch }} 221 | echo "Archive name set to: $ARCHIVE_NAME" 222 | echo "NAME=$ARCHIVE_NAME" >> $GITHUB_OUTPUT 223 | id: archieve 224 | - name: Install Python dependencies 225 | run: | 226 | python -m pip install -U pip 227 | python -m pip install -U setuptools==69.5.1 228 | python -m pip install -U wheel 229 | python -m pip install -r requirements.dev.txt 230 | - name: Run PyInstaller 231 | env: 232 | PYTHONOPTIMIZE: 1 # Enable optimizations as if the -O flag is given. 233 | PYTHONHASHSEED: 42 # Try to ensure deterministic results. 234 | PYTHONUNBUFFERED: 1 235 | run: | 236 | pyinstaller build.spec 237 | # This step exists for debugging. Such as checking if data files were included correctly by PyInstaller. 238 | - name: List distribution files 239 | run: | 240 | find dist 241 | # Archive the PyInstaller build using the appropriate tool for the platform. 242 | - name: Tar files 243 | if: runner.os != 'Windows' 244 | run: | 245 | tar --format=ustar -czvf ${{ steps.archieve.outputs.NAME }}.tar.gz dist/ 246 | - name: Archive files 247 | if: runner.os == 'Windows' 248 | shell: pwsh 249 | run: | 250 | Compress-Archive dist/* ${{ steps.archieve.outputs.NAME }}.zip 251 | # Upload archives as artifacts, these can be downloaded from the GitHub actions page. 252 | - name: Upload Artifact 253 | uses: actions/upload-artifact@v4 254 | with: 255 | name: automated-build-${{ steps.archieve.outputs.NAME }} 256 | path: ${{ steps.archieve.outputs.NAME }}.* 257 | retention-days: 7 258 | if-no-files-found: error 259 | - name: Upload release 260 | uses: svenstaro/upload-release-action@v2 261 | with: 262 | repo_token: ${{ secrets.PAT }} 263 | file: ${{ steps.archieve.outputs.NAME }}.* 264 | file_glob: true 265 | tag: v${{needs.bump_version.outputs.NEW_RELEASE}} 266 | overwrite: true 267 | docker_image_arm: 268 | name: Build ARM Docker Image 269 | needs: [bump_version, release, release_hash] 270 | runs-on: ubuntu-latest 271 | steps: 272 | - id: string 273 | uses: ASzc/change-string-case-action@v6 274 | with: 275 | string: ${{ github.repository }} 276 | - name: Checkout 277 | uses: actions/checkout@v4 278 | with: 279 | fetch-depth: 0 280 | ref: master 281 | - name: Set up QEMU 282 | uses: docker/setup-qemu-action@v3 283 | - name: Set up Docker Buildx 284 | uses: docker/setup-buildx-action@v3 285 | - name: Login to DockerHub 286 | uses: docker/login-action@v3 287 | with: 288 | username: ${{ secrets.DOCKERHUB_USERNAME }} 289 | password: ${{ secrets.DOCKERHUB_TOKEN }} 290 | - name: Login to Container registry 291 | uses: docker/login-action@v3 292 | with: 293 | registry: ghcr.io 294 | username: ${{ github.actor }} 295 | password: ${{ secrets.REG_TOKEN }} 296 | - name: Extract metadata (tags, labels) for Docker 297 | id: meta 298 | uses: docker/metadata-action@v5 299 | with: 300 | images: | 301 | feramance/qbitrr 302 | ghcr.io/${{ steps.string.outputs.lowercase }} 303 | tags: | 304 | type=edge 305 | - name: Build and push 306 | env: 307 | DOCKER_BUILDKIT: 1 308 | uses: docker/build-push-action@v6 309 | with: 310 | context: . 311 | platforms: linux/arm64 312 | push: true 313 | tags: feramance/qbitrr:nightly-arm,feramance/qbitrr:latest-arm,feramance/qbitrr:v${{needs.bump_version.outputs.NEW_RELEASE}}-arm,ghcr.io/${{ steps.string.outputs.lowercase }}:nightly-arm,ghcr.io/${{ steps.string.outputs.lowercase }}:latest-arm,ghcr.io/${{ steps.string.outputs.lowercase }}:v${{needs.bump_version.outputs.NEW_RELEASE}}-arm 314 | labels: ${{ steps.meta.outputs.labels }} 315 | cache-from: type=gha 316 | cache-to: type=gha,mode=max 317 | docker_image: 318 | name: Build Docker Image 319 | needs: [bump_version, release, release_hash] 320 | runs-on: ubuntu-latest 321 | steps: 322 | - id: string 323 | uses: ASzc/change-string-case-action@v6 324 | with: 325 | string: ${{ github.repository }} 326 | - name: Checkout 327 | uses: actions/checkout@v4 328 | with: 329 | fetch-depth: 0 330 | ref: master 331 | - name: Set up QEMU 332 | uses: docker/setup-qemu-action@v3 333 | - name: Set up Docker Buildx 334 | uses: docker/setup-buildx-action@v3 335 | - name: Login to DockerHub 336 | uses: docker/login-action@v3 337 | with: 338 | username: ${{ secrets.DOCKERHUB_USERNAME }} 339 | password: ${{ secrets.DOCKERHUB_TOKEN }} 340 | - name: Login to Container registry 341 | uses: docker/login-action@v3 342 | with: 343 | registry: ghcr.io 344 | username: ${{ github.actor }} 345 | password: ${{ secrets.REG_TOKEN }} 346 | - name: Extract metadata (tags, labels) for Docker 347 | id: meta 348 | uses: docker/metadata-action@v5 349 | with: 350 | images: | 351 | feramance/qbitrr 352 | ghcr.io/${{ steps.string.outputs.lowercase }} 353 | tags: | 354 | type=edge 355 | - name: Build and push 356 | env: 357 | DOCKER_BUILDKIT: 1 358 | uses: docker/build-push-action@v6 359 | with: 360 | context: . 361 | platforms: linux/amd64 362 | push: true 363 | tags: feramance/qbitrr:nightly,feramance/qbitrr:latest,feramance/qbitrr:v${{needs.bump_version.outputs.NEW_RELEASE}},ghcr.io/${{ steps.string.outputs.lowercase }}:nightly,ghcr.io/${{ steps.string.outputs.lowercase }}:latest,ghcr.io/${{ steps.string.outputs.lowercase }}:v${{needs.bump_version.outputs.NEW_RELEASE}} 364 | labels: ${{ steps.meta.outputs.labels }} 365 | cache-from: type=gha 366 | cache-to: type=gha,mode=max 367 | change_logs: 368 | name: Generate Change Logs and Release Notes 369 | needs: [bump_version, release, release_hash, docker_image, publish] 370 | runs-on: ubuntu-latest 371 | steps: 372 | - uses: actions/checkout@v4 373 | with: 374 | fetch-depth: 0 375 | ref: master 376 | - name: Set up Python 377 | uses: actions/setup-python@v5 378 | with: 379 | python-version: 3.x 380 | - uses: actions/setup-node@v4 381 | with: 382 | node-version: latest 383 | - run: npm install github-release-notes -g 384 | - name: Release Notes and Change logs 385 | run: | 386 | gren release 387 | gren changelog 388 | - name: Import GPG key 389 | uses: crazy-max/ghaction-import-gpg@v6.2.0 390 | with: 391 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 392 | passphrase: ${{ secrets.PASSPHRASE }} 393 | git_user_signingkey: true 394 | git_commit_gpgsign: true 395 | git_tag_gpgsign: true 396 | id: import_gpg 397 | - name: Git Auto Commit 398 | uses: stefanzweifel/git-auto-commit-action@v5.0.1 399 | with: 400 | commit_message: '[skip ci] Update CHANGELOG.md and Release Notes for v${{needs.bump_version.outputs.NEW_RELEASE}}' 401 | commit_options: -S 402 | commit_user_name: ${{ steps.import_gpg.outputs.name }} 403 | commit_user_email: ${{ steps.import_gpg.outputs.email }} 404 | commit_author: ${{ steps.import_gpg.outputs.name }} <${{ steps.import_gpg.outputs.email }}> 405 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config.toml 2 | .idea 3 | /qBitrr/testing/test.py 4 | /qBitrr.egg-info/ 5 | /build/ 6 | /dist/ 7 | .env 8 | .venv/ 9 | venv/ 10 | /.github/workflows/test.yml 11 | __pycache__/ 12 | *.py[cod] 13 | /qBitrr/testing/ 14 | build.bat 15 | /main.*/ 16 | /main.exe 17 | .run/ 18 | .vscode/ 19 | test.py 20 | .config 21 | -------------------------------------------------------------------------------- /.grenrc.yml: -------------------------------------------------------------------------------- 1 | dataSource: commits 2 | ignoreIssuesWith: 3 | - wontfix 4 | - duplicate 5 | prefix: '' 6 | includeMessages: all 7 | ignoreCommitsWith: 8 | - \[auto-changelogs\] 9 | - \[auto-release\] 10 | - \[auto-publish\] 11 | - \[skip ci\] 12 | - \[ci skip\] 13 | - \[pre-commit.ci\] 14 | changelogFilename: CHANGELOG.md 15 | override: true 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ^(CHANGELOG.md|bundled_data.py|.bumpversion.cfg|Dockerfile|setup.cfg|.github/FUNDING.yml) 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: requirements-txt-fixer 9 | - id: trailing-whitespace 10 | - id: mixed-line-ending 11 | args: [--fix, lf] 12 | - id: detect-private-key 13 | - id: check-toml 14 | - id: check-json 15 | - id: pretty-format-json 16 | args: [--autofix, --indent, '2'] 17 | - repo: https://github.com/asottile/pyupgrade 18 | rev: v3.10.1 19 | hooks: 20 | - id: pyupgrade 21 | args: [--py38-plus] 22 | - repo: https://github.com/pycqa/isort 23 | rev: 5.12.0 24 | hooks: 25 | - id: isort 26 | - repo: https://github.com/psf/black 27 | rev: 23.7.0 28 | hooks: 29 | - id: black 30 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 31 | rev: v2.10.0 32 | hooks: 33 | - id: pretty-format-yaml 34 | args: [--autofix, --indent, '2'] 35 | - repo: https://github.com/sirosen/texthooks 36 | rev: 0.5.0 37 | hooks: 38 | - id: fix-smartquotes 39 | - id: fix-ligatures 40 | # - repo: https://github.com/pre-commit/mirrors-autopep8 41 | # rev: v2.0.4 # Use the sha / tag you want to point at 42 | # hooks: 43 | # - id: autopep8 44 | - repo: https://github.com/PyCQA/autoflake 45 | rev: v2.2.1 46 | hooks: 47 | - id: autoflake 48 | args: [--remove-all-unused-imports, --recursive, --in-place, --remove-unused-variables, --ignore-init-module-imports, --remove-duplicate-keys] 49 | - repo: https://github.com/MarcoGorelli/absolufy-imports 50 | rev: v0.3.1 51 | hooks: 52 | - id: absolufy-imports 53 | ci: 54 | autofix_commit_msg: | 55 | [pre-commit.ci] auto fixes from pre-commit.com hooks 56 | 57 | for more information, see https://pre-commit.ci 58 | autofix_prs: true 59 | autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' 60 | autoupdate_schedule: weekly 61 | submodules: false 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.10.23 (29/05/2025) 4 | - [[patch] Hotfix](https://github.com/Feramance/qBitrr/commit/ca816463818878164bee3b31f9c04576c300aeeb) - @Feramance 5 | - [Update readme](https://github.com/Feramance/qBitrr/commit/cec04e271a9501a0b9f1fce46ab4d43563e9292e) - @Feramance 6 | 7 | --- 8 | 9 | ## v4.10.22 (29/05/2025) 10 | - [[patch] Retry release workflow](https://github.com/Feramance/qBitrr/commit/20ffbe4d2dc678a33bce56e60456130739d803c9) - @Feramance 11 | - [Update release workflow](https://github.com/Feramance/qBitrr/commit/967e1bf002aba1d3a6a89159f72530b9ed0c05db) - @Feramance 12 | - [[patch] Updated tagging to handle all tags appropriately](https://github.com/Feramance/qBitrr/commit/7580e73d3889c687b1316987b5cc0aa81c43cc03) - @Feramance 13 | 14 | --- 15 | 16 | ## v4.10.21 (21/04/2025) 17 | - [[patch] Merge pull request #162 from Feramance:160-question-regarding-the-initial-_monitored_tracker_urls-loading](https://github.com/Feramance/qBitrr/commit/ceb162cd1eee785c969cc07f0e9483b8bb74cab7) - @Feramance 18 | - [Fix per @overlord73](https://github.com/Feramance/qBitrr/commit/f38baf50338b5a20db9657a574425f59b5fe68d0) - @Feramance 19 | - [Update custom format unmet handling](https://github.com/Feramance/qBitrr/commit/189428a38f36b9ffd3273051fcdf872a8715273a) - @Feramance 20 | - [Dependency bump and pyarr downgrade](https://github.com/Feramance/qBitrr/commit/254de97188a2f1ce3b98f2a0ecfe90fe003b7c0b) - @Feramance 21 | - [Custom format logging](https://github.com/Feramance/qBitrr/commit/b543d394607e0e0caf6db8ab4735f5144e9121cd) - @Feramance 22 | - [Merge pull request #159 from bruvv/patch-2](https://github.com/Feramance/qBitrr/commit/57b1f33148ee985d62b376722e9790bea045148c) - @Feramance 23 | - [Update gen_config.py](https://github.com/Feramance/qBitrr/commit/2467ba9a639548e154c92d58d28c1ee40ef8a654) - @bruvv 24 | - [Fixed some typos](https://github.com/Feramance/qBitrr/commit/184f06550b360d46fdf1213577f9fad77eeb427d) - @bruvv 25 | - [Further logging updates for improved logs](https://github.com/Feramance/qBitrr/commit/d06c83c860251972cc4b05140db0ebc92a67248b) - @Feramance 26 | - [Removed ansi colour formatting for neatness](https://github.com/Feramance/qBitrr/commit/83d0fc6fed9dcd1c4c37534bb4989e3c12db8870) - @Feramance 27 | - [Coloured log files](https://github.com/Feramance/qBitrr/commit/1d7fd695eeb655a8fb628adcaae94b699b2c692b) - @Feramance 28 | - [Logging changes](https://github.com/Feramance/qBitrr/commit/091d6234aa425833b59fa5b3dc3eb969773cce74) - @Feramance 29 | - [Attempting to fix connection error](https://github.com/Feramance/qBitrr/commit/fb916f50881d65cbd32596e29d5c7336f6f889e7) - @Feramance 30 | 31 | --- 32 | 33 | ## v4.10.20 (12/03/2025) 34 | - [[patch] Adjusted stalled delay fallback, logs, and stalled delay now acts on the last activity, rather than added on](https://github.com/Feramance/qBitrr/commit/a61080f740835600c02383376134b6157a024346) - @Feramance 35 | 36 | --- 37 | 38 | ## v4.10.19 (12/03/2025) 39 | - [[patch] Enabled stalled delay default](https://github.com/Feramance/qBitrr/commit/e9df62d84372b997f399e52e0870150438191104) - @Feramance 40 | 41 | --- 42 | 43 | ## v4.10.18 (12/03/2025) 44 | - [[patch] Free space fixes and remove stalled tag if recent](https://github.com/Feramance/qBitrr/commit/8bf7bb4366c0dc38fd2ff2ae430c7c8bf8a79cfb) - @Feramance 45 | 46 | --- 47 | 48 | ## v4.10.17 (12/03/2025) 49 | - [[patch] Updated paused torrent handling](https://github.com/Feramance/qBitrr/commit/1b4be8ff27ec72132e29db1a96ff612dad8d9df5) - @Feramance 50 | - [Free space adjustements](https://github.com/Feramance/qBitrr/commit/7805b27bc78a19ae27db7e0e19abe6c102ec396b) - @Feramance 51 | 52 | --- 53 | 54 | ## v4.10.16 (11/03/2025) 55 | - [[patch] Stalled delay fixes](https://github.com/Feramance/qBitrr/commit/fd119edf95348a255b27968e9dc55304f0228389) - @Feramance 56 | - [Further conditional adjustements for younger torrents and stalled delay](https://github.com/Feramance/qBitrr/commit/07a881e3496f49d93b63cf38549f8ba848eaf348) - @Feramance 57 | - [Adjusted younger than conditions](https://github.com/Feramance/qBitrr/commit/737f295b58fc2be41a97e80fb21db20ebda82a1b) - @Feramance 58 | - [Further stall handling checks](https://github.com/Feramance/qBitrr/commit/7c976a40b378243de5c7702deb0c1d41f106fb50) - @Feramance 59 | - [Log updates](https://github.com/Feramance/qBitrr/commit/03fe74e35e85669e484c406045b776f60aa54f9b) - @Feramance 60 | - [Adjusted stalled delay conditions](https://github.com/Feramance/qBitrr/commit/4b71295c58b502b8bcfe7da3ba4f1e4784165dbe) - @Feramance 61 | - [Logging update](https://github.com/Feramance/qBitrr/commit/09fd554546c71968fd5a6df2e42755bd998da060) - @Feramance 62 | - [Further update changes](https://github.com/Feramance/qBitrr/commit/83f10f33cb9260b889c2c54ccda31d1590297862) - @Feramance 63 | - [Update log](https://github.com/Feramance/qBitrr/commit/ee30569a045482ec0c2f68b8ce81f6722cf24a7f) - @Feramance 64 | - [Logging to check vars](https://github.com/Feramance/qBitrr/commit/386332cd00e33c4f5b9fa1936cde72bda7490727) - @Feramance 65 | - [Removed recent queue to use added on torrent property instead](https://github.com/Feramance/qBitrr/commit/2a12cb4ae098dcf2d63e044ce7775b7914bc3867) - @Feramance 66 | - [Adjusted recent queue check](https://github.com/Feramance/qBitrr/commit/cf7082fd937d91296d4c2b3f8da43bf5dfd69e47) - @Feramance 67 | - [Adjust stalled delay behaviour](https://github.com/Feramance/qBitrr/commit/d9a76442fca5c2c41b643c6a8461cd3efe8d2f27) - @Feramance 68 | - [Update README.md](https://github.com/Feramance/qBitrr/commit/eb98c8b5edee9ef846d79c0cbc9de41767c4512a) - @Feramance 69 | 70 | --- 71 | 72 | ## v4.10.15 (07/03/2025) 73 | - [[patch] stalled activity is now checked against last activity](https://github.com/Feramance/qBitrr/commit/1c6fe184a39e730931e7477bf1161dc803f75df9) - @Feramance 74 | - [Small fixes](https://github.com/Feramance/qBitrr/commit/1261cd4dd206a502ad9a695afaf977cea271d215) - @Feramance 75 | - [Adjusted stale download handling to allow buffer of activity prior to deletion](https://github.com/Feramance/qBitrr/commit/770f40554eb72282c8437ecdc2a8af0ff6322b1d) - @Feramance 76 | - [Adjusted ratio/seed handling](https://github.com/Feramance/qBitrr/commit/902c823d9f87c0954ec45d946b894caa94a8c02e) - @Feramance 77 | 78 | --- 79 | 80 | ## v4.10.14 (04/03/2025) 81 | - [[patch] Catch quality profile data errors](https://github.com/Feramance/qBitrr/commit/1de777789d4300409db863a1a6aa0afb1b720781) - @Feramance 82 | - [Dependency bump](https://github.com/Feramance/qBitrr/commit/63e2e619415d29d79bf7c8f0cbc004d7da79e217) - @Feramance 83 | 84 | --- 85 | 86 | ## v4.10.13 (26/02/2025) 87 | - [[patch] Fix max eta handling](https://github.com/Feramance/qBitrr/commit/cb4e892c097d5dd0ff547c7f0f34e90ca420b00c) - @Feramance 88 | 89 | --- 90 | 91 | ## v4.10.12 (23/02/2025) 92 | - [[patch] Update arss.py](https://github.com/Feramance/qBitrr/commit/8bad377188bbdd09ddbfaf8decb1b0bc42982545) - @Feramance 93 | - [Update README.md](https://github.com/Feramance/qBitrr/commit/25f0f37dd0b872d9f3ac4685b57eb1e305d43538) - @Feramance 94 | 95 | --- 96 | 97 | ## v4.10.11 (03/02/2025) 98 | - [[patch] Keep temp profile config](https://github.com/Feramance/qBitrr/commit/6742616fd4aaa5191bf481016c57c7a4e394a48d) - @Feramance 99 | - [New logo upload](https://github.com/Feramance/qBitrr/commit/c2076a7bdc24385d9617dc093ac8ffe5f39bee78) - @Feramance 100 | 101 | --- 102 | 103 | ## v4.10.10 (26/12/2024) 104 | - [[patch] Removed bad variable calls](https://github.com/Feramance/qBitrr/commit/dbec9dcecc2a8e092ca0fc3714448b0ca98b1cf9) - @Feramance 105 | - [Removed redundant logging](https://github.com/Feramance/qBitrr/commit/d0883c7ae03fb02e8134b394e9d5ad86ce487d2d) - @Feramance 106 | 107 | --- 108 | 109 | ## v4.10.9 (26/12/2024) 110 | - [[patch] Loop fixes](https://github.com/Feramance/qBitrr/commit/f934c77661a5f6276f46e0146cc132fa0714b35b) - @Feramance 111 | - [Remove redundant variable](https://github.com/Feramance/qBitrr/commit/3d4a3f90e7a78aacf9b47a4c1e95cf5267110952) - @Feramance 112 | - [Further loop changes](https://github.com/Feramance/qBitrr/commit/434dade175b290a37cd397fa4067edd62743eb79) - @Feramance 113 | - [Loop timer adjustment](https://github.com/Feramance/qBitrr/commit/2aeb48b6367c7c6b450d17496f6955d1cf253ae0) - @Feramance 114 | - [Loop debugging](https://github.com/Feramance/qBitrr/commit/0c6f377cb99b161abaedb02d5648b2cdc2190b6c) - @Feramance 115 | 116 | --- 117 | 118 | ## v4.10.8 (18/12/2024) 119 | - [[patch] Hotfix Free space config changes](https://github.com/Feramance/qBitrr/commit/072bffdca3af9b85c34c89f7f2a09fea4f530596) - @Feramance 120 | - [Changed log messages for clarity](https://github.com/Feramance/qBitrr/commit/631ec2dce2e710fdba7dc9a90558111817292a3e) - @Feramance 121 | - [Add logs for temp profile switching testing](https://github.com/Feramance/qBitrr/commit/9b137fd9eee21323f4762aa0e7ccbc234cfb09d3) - @Feramance 122 | - [Update README.md](https://github.com/Feramance/qBitrr/commit/a0c2a9622af9fdff5efed6eb8c58896b9b3dba55) - @Feramance 123 | - [Update README.md](https://github.com/Feramance/qBitrr/commit/8d62851db045e1daadf868a947b641d57f4c94f1) - @Feramance 124 | - [Update README.md](https://github.com/Feramance/qBitrr/commit/82adf8f79ed05da38057fcc49e988afedd7015b6) - @Feramance 125 | 126 | --- 127 | 128 | ## v4.10.7 (18/12/2024) 129 | - [[patch] Hotfix 2](https://github.com/Feramance/qBitrr/commit/7e355b9d4977b8daed95da58323f1c6ffcadf521) - @Feramance 130 | - [Merge branch 'master' of https://github.com/Feramance/qBitrr](https://github.com/Feramance/qBitrr/commit/0bb99509bbfe8949543108667caa1b2560436b93) - @Feramance 131 | - [[patch] Hotfix](https://github.com/Feramance/qBitrr/commit/b301ea02b586a52af65959fa653135b815b4b435) - @Feramance 132 | 133 | --- 134 | 135 | ## v4.10.6 (18/12/2024) 136 | - [[patch] Free space config folder added](https://github.com/Feramance/qBitrr/commit/9286bdc67402a13b213254cf6e742905f1004e25) - @Feramance 137 | 138 | --- 139 | 140 | ## v4.10.5 (10/12/2024) 141 | - [[patch] Minor changes and temporarily disabling separate request search](https://github.com/Feramance/qBitrr/commit/b4654957964b622e1eea8a984aebb6cb2cfd1566) - @Feramance 142 | - [Further testing](https://github.com/Feramance/qBitrr/commit/b024cf324bdef262d67c1b10ff5fd961144edfa8) - @Feramance 143 | - [Change searched update](https://github.com/Feramance/qBitrr/commit/0029967628b6e1378e3fb2b5c4b2b94c39934e06) - @Feramance 144 | - [Further testing upgrade searches](https://github.com/Feramance/qBitrr/commit/6f886ac18acfe034a0dba2164e49117c6f004fe7) - @Feramance 145 | - [Adjust upgrade behaviour](https://github.com/Feramance/qBitrr/commit/863d79e6a311dd839716fd19ec1ffb81bc299707) - @Feramance 146 | - [More logging for further debugging](https://github.com/Feramance/qBitrr/commit/e40ed6c1a2420ec857380c46286eb9d759f998f1) - @Feramance 147 | - [Added logging to check loop flow](https://github.com/Feramance/qBitrr/commit/f9ca818d2b13829aed067a0d0e16cfcbedf9556c) - @Feramance 148 | - [Merge pull request #145 from bruvv/patch-1](https://github.com/Feramance/qBitrr/commit/132d0816abe114c8c3c8be3dcada8899f2d297b1) - @Feramance 149 | - [Small typos in config file](https://github.com/Feramance/qBitrr/commit/52d49066375de050747582aeb2c70ddfb004a143) - @bruvv 150 | - [Removed some log tags](https://github.com/Feramance/qBitrr/commit/4e415d93cea1ceb46b74e341c96a953206a5ffa3) - @Feramance 151 | - [Logging adjustements](https://github.com/Feramance/qBitrr/commit/f89325de2bcc571bdd24a1e82e5895d89235f312) - @Feramance 152 | - [Logging and a few performance changes](https://github.com/Feramance/qBitrr/commit/5fee30041fa5ac9b99e8deed699efefb45b2ce12) - @Feramance 153 | - [Adjust search loop timer](https://github.com/Feramance/qBitrr/commit/4a1af891ce0d9d8996c6f94d06ebf37c1b193593) - @Feramance 154 | - [Request search adjustements](https://github.com/Feramance/qBitrr/commit/19f6521aab911d2e821a8143ec12aa625825b65b) - @Feramance 155 | - [Decrement command count for requests](https://github.com/Feramance/qBitrr/commit/daf60f511d8f3bf159fe9ceb27272bc3e039a319) - @Feramance 156 | - [Adjusted request conditions further](https://github.com/Feramance/qBitrr/commit/d69afa42dd3329dd61c5f20d7155db7b39517caa) - @Feramance 157 | - [Adjusted request get files conditions](https://github.com/Feramance/qBitrr/commit/beb71697ee036f5b6240050ad737c16bdac7ec85) - @Feramance 158 | - [Radarr request query adjustements](https://github.com/Feramance/qBitrr/commit/a19e08cbf3fd3cfbbe94a245b7631f5ae5a85409) - @Feramance 159 | - [Logging](https://github.com/Feramance/qBitrr/commit/48ffcbf21d9d3922067aa85ee842a16fe765742b) - @Feramance 160 | - [Adjust has_internet check](https://github.com/Feramance/qBitrr/commit/449b1aa77ea171a739d70dfe201d203a7e573359) - @Feramance 161 | - [Change request search filtering](https://github.com/Feramance/qBitrr/commit/e6e489ede8a787dfb4743be133af4a9a1c563442) - @Feramance 162 | - [Further fixes](https://github.com/Feramance/qBitrr/commit/91588932b728ed0abe7bb6d7b51b5b5f330af682) - @Feramance 163 | - [Updated get request files return type](https://github.com/Feramance/qBitrr/commit/1888f8bbfcc50d63a9e52bd17ca9dad4e9ced9d5) - @Feramance 164 | - [Fixed error](https://github.com/Feramance/qBitrr/commit/f7ee6552716a43ef7dcc58a0752bd3f3f40667a4) - @Feramance 165 | - [Logging](https://github.com/Feramance/qBitrr/commit/797282530535174dd8a5020594d5b1578b221a6c) - @Feramance 166 | - [Added logging to check where the flow is crashing](https://github.com/Feramance/qBitrr/commit/22bf52c1d8674d65dd4f0e8874e437bb52a1e119) - @Feramance 167 | - [Request search testing](https://github.com/Feramance/qBitrr/commit/a69262b2c6bcfd0c920e2a6f511e66ed1aeb4b95) - @Feramance 168 | - [Revert some request changes](https://github.com/Feramance/qBitrr/commit/6a338244585971e6843aaff42c95aa01a5c61b01) - @Feramance 169 | - [Request search run condition adjustements](https://github.com/Feramance/qBitrr/commit/ed63e087e3e719d30462207423664fe1f9cc66bb) - @Feramance 170 | - [Timing updates for request search](https://github.com/Feramance/qBitrr/commit/b519e3e97b9a8e83efed53e894b7df0d13669d68) - @Feramance 171 | - [Get request files changes](https://github.com/Feramance/qBitrr/commit/c382304e46c9da5edcad78028f2aeb157b3e456e) - @Feramance 172 | - [Request search loop overhaul](https://github.com/Feramance/qBitrr/commit/c2d77b75e77540a01d2a3e63007cf9a722f88850) - @Feramance 173 | 174 | --- 175 | 176 | ## v4.10.4 (05/12/2024) 177 | - [[patch] Quality profile change error handling when defined incorrectly](https://github.com/Feramance/qBitrr/commit/bc2666214800f87d229f90317eb07097e55e403d) - @Feramance 178 | 179 | --- 180 | 181 | ## v4.10.3 (05/12/2024) 182 | - [[patch] Minor changes to better handle temp profile config edge cases](https://github.com/Feramance/qBitrr/commit/b4f58311406d797956c3fa8aecc88b7ac18bdd09) - @Feramance 183 | - [Merge branch 'master' of https://github.com/Feramance/qBitrr](https://github.com/Feramance/qBitrr/commit/6a8c6224cd0b68a4c82ec87e773477bd071b601a) - @Feramance 184 | - [[patch] Hotfix 3](https://github.com/Feramance/qBitrr/commit/fefb8dab5335af88992f0d50c8d7a9ba8b1bb2ea) - @Feramance 185 | 186 | --- 187 | 188 | ## v4.10.2 (04/12/2024) 189 | - [[patch] Hotfix bad variable](https://github.com/Feramance/qBitrr/commit/e880563a91ca6a5f681bb66a49a6f247f42d3124) - @Feramance 190 | 191 | --- 192 | 193 | ## v4.10.1 (04/12/2024) 194 | - [[patch] Hotfix backwards compatibility for temp profile configs](https://github.com/Feramance/qBitrr/commit/4ac7c016e7470b168005d0f468535ab4a0abb67e) - @Feramance 195 | 196 | --- 197 | 198 | ## v4.10.0 (04/12/2024) 199 | - [[minor] Merge pull request #140 from Feramance:128-multiple-fixes-and-temp-profile-changes](https://github.com/Feramance/qBitrr/commit/5792f12c8bfbf31748b3a6aee6a7db9f5469f6a5) - @Feramance 200 | - [Config description updates](https://github.com/Feramance/qBitrr/commit/41b7b306e2db0e01254b6c701f1aa6f0b92df73d) - @Feramance 201 | - [Logging fixes](https://github.com/Feramance/qBitrr/commit/e362672313d6eb7a6615efd0b6a007328fa5f35f) - @Feramance 202 | - [Multiple temp profiles changes](https://github.com/Feramance/qBitrr/commit/ec9338c527aeee9bbfb81a106b717c2746664d6a) - @Feramance 203 | - [Hotfix](https://github.com/Feramance/qBitrr/commit/0b88fcfa4029eb144926728cc4a7e69087d585ce) - @Feramance 204 | - [Multiple temporary profiles rework](https://github.com/Feramance/qBitrr/commit/6a9eb81b8c68294357be01038501a22b933dc9db) - @Feramance 205 | - [Further unmonitord feature change](https://github.com/Feramance/qBitrr/commit/bdaf792527fc0bfee95fa86d21fda1d47334d8d8) - @Feramance 206 | - [EntryId fix](https://github.com/Feramance/qBitrr/commit/97af33713120b199e07572b1c7f88c5631624240) - @Feramance 207 | - [Fix qbittorrent client parameter](https://github.com/Feramance/qBitrr/commit/6602c35514c0194534f792cffea42446f0a7f2c2) - @Feramance 208 | - [Added profiles fallback](https://github.com/Feramance/qBitrr/commit/0a558d4244993f79271805ed35ccffb77202d831) - @Feramance 209 | - [Logging profiles](https://github.com/Feramance/qBitrr/commit/17ba6e1f2dea931084458144f920adc735d0b101) - @Feramance 210 | - [Unmonitored search updates and initial test of mutliple temp profiles](https://github.com/Feramance/qBitrr/commit/feb54476d0f7f8d7ea74b884aeacff99c3a75686) - @Feramance 211 | - [Unmonitored config option implementation](https://github.com/Feramance/qBitrr/commit/ce52dc6c9e401c940f8bcb8eb06df83a33b9a116) - @Feramance 212 | - [has internet changes](https://github.com/Feramance/qBitrr/commit/8665d44c63cb165227ff0535f60f1f601143be12) - @Feramance 213 | 214 | --- 215 | 216 | ## v4.9.20 (27/11/2024) 217 | - [[patch] Added config option to allow qbittorrent v5](https://github.com/Feramance/qBitrr/commit/7262b39754025c7c40a969e4300638b88402c438) - @Feramance 218 | 219 | --- 220 | 221 | ## v4.9.19 (25/11/2024) 222 | - [[patch] added api logging and set allow stalled behaviour to ignore the `IgnoreTorrentsYoungerThan` config](https://github.com/Feramance/qBitrr/commit/d371e3aa68f2038a59194faae9348ba1f5a0d4fe) - @Feramance 223 | - [Headless mode fix and limited qbittorrent version](https://github.com/Feramance/qBitrr/commit/431e5513b89475e5493e042378471c196af5ad77) - @Feramance 224 | - [Config notes update](https://github.com/Feramance/qBitrr/commit/358f1fe0b98467f77e066c952af6b27af7373177) - @Feramance 225 | 226 | --- 227 | 228 | ## v4.9.18 (14/11/2024) 229 | - [[patch] Merge pull request #131 from Feramance/127-attributeerror-in-headless-mode](https://github.com/Feramance/qBitrr/commit/4506eb9c6dc8c9ef9cf9e882e26c1498ae03d4a1) - @Feramance 230 | - [Fix headless mode category path checks](https://github.com/Feramance/qBitrr/commit/52e6cb9130ae03a6d308a6c0976d12710cd6707f) - @Feramance 231 | - [Merge branch 'master' of https://github.com/Feramance/qBitrr](https://github.com/Feramance/qBitrr/commit/fd06a192608a440301ef87a7132cb2958b42edce) - @Feramance 232 | - [Config updates](https://github.com/Feramance/qBitrr/commit/4bbd2d679919ad2b2e10f7e363c4753137da247e) - @Feramance 233 | 234 | --- 235 | 236 | ## v4.9.17 (13/11/2024) 237 | - [[patch] Improved search by year logging](https://github.com/Feramance/qBitrr/commit/d3d6178c9bb1013e47f2ab1a95a4192abcc8e1ef) - @Feramance 238 | - [Pre-commit changes](https://github.com/Feramance/qBitrr/commit/691f6c3d6a3084cd4f793c1e35d6ff1423a18f6c) - @Feramance 239 | - [Dependabot adjustmenets](https://github.com/Feramance/qBitrr/commit/45a7cd022f1de0c2df0efc37341cb404d641c23d) - @Feramance 240 | - [Dependabot config update](https://github.com/Feramance/qBitrr/commit/e6f14d0c70158dd93b4cdbad0cc5e2824bba6c09) - @Feramance 241 | - [Merge branch 'master' of https://github.com/Feramance/qBitrr](https://github.com/Feramance/qBitrr/commit/f16088b2f2c6d8bd2c44ef8ec2f4a99c125786e1) - @Feramance 242 | - [Dependabot yml config updates](https://github.com/Feramance/qBitrr/commit/099c2e5edb66260498875317a2724dca55da6a08) - @Feramance 243 | 244 | --- 245 | 246 | ## v4.9.16 (12/11/2024) 247 | - [[patch] Dependency updates and newenv changes](https://github.com/Feramance/qBitrr/commit/693bde3c28ca429888acf179ae41d6910e6ce65f) - @Feramance 248 | - [Merge branch 'master' of https://github.com/Feramance/qBitrr](https://github.com/Feramance/qBitrr/commit/08568267a777a78e76253f23e02421b8b9ccab93) - @Feramance 249 | - [[patch] Dependency update](https://github.com/Feramance/qBitrr/commit/2c4d169800f93c825a2a73d083b759b75472d77f) - @Feramance 250 | 251 | --- 252 | 253 | ## v4.9.15 (12/11/2024) 254 | - [[patch] Update ujson version](https://github.com/Feramance/qBitrr/commit/18b96cf12de8415f5b8774205e0632fd56e05098) - @Feramance 255 | - [Merge pull request #124 from Feramance/dependabot/github_actions/crazy-max/ghaction-import-gpg-6.2.0](https://github.com/Feramance/qBitrr/commit/1f95077994afc3158e43d7413379950289ecd51d) - @Feramance 256 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Pin Python to the latest supported version 2 | # (This avoid it auto updating to a higher untested version) 3 | FROM python:3.10 4 | 5 | LABEL Name="qBitrr" 6 | LABEL Maintainer="feramance" 7 | LABEL Version="4.10.23" 8 | LABEL org.opencontainers.image.source=https://github.com/feramance/qbitrr 9 | 10 | # Env used by the script to determine if its inside a docker - 11 | # if this is set to 69420 it will change the working dir for docker specific values 12 | ENV QBITRR_DOCKER_RUNNING=69420 13 | ENV PYTHONDONTWRITEBYTECODE=1 14 | ENV PYTHONUNBUFFERED=1 15 | ENV PYTHONOPTIMIZE=1 16 | 17 | RUN pip install --quiet -U pip wheel 18 | WORKDIR /app 19 | ADD ./requirements.fast.txt /app/requirements.fast.txt 20 | RUN pip install --quiet -r requirements.fast.txt 21 | COPY . /app 22 | RUN pip install --quiet . 23 | 24 | WORKDIR /config 25 | 26 | ENTRYPOINT ["python", "-m", "qBitrr.main"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Feramance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | PYTHON ?= python 4 | 5 | ROOT_DIR:=$(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 6 | 7 | ifneq ($(wildcard $(ROOT_DIR)/.venv/.),) 8 | VENV_PYTHON = $(ROOT_DIR)/.venv/bin/python 9 | else 10 | VENV_PYTHON = $(PYTHON) 11 | endif 12 | 13 | define HELP_BODY 14 | Usage: 15 | make 16 | 17 | Commands: 18 | reformat Reformat all files being tracked by git. 19 | bumpdeps Run script bumping dependencies. 20 | newenv Create or replace this project's virtual environment. 21 | syncenv Sync this project's virtual environment to Red's latest dependencies. 22 | endef 23 | export HELP_BODY 24 | 25 | # Python Code Style 26 | reformat: 27 | pre-commit run --all-files 28 | 29 | # Dependencies 30 | bumpdeps: 31 | pip-compile -o requirements.txt --upgrade --strip-extras 32 | pip-compile -o requirements.dev.txt --extra dev --upgrade --strip-extras 33 | pip-compile -o requirements.fast.txt --extra fast --upgrade --strip-extras 34 | pip-compile -o requirements.all.txt --extra all --upgrade --strip-extras 35 | 36 | # Development environment 37 | newenv: 38 | $(PYTHON) -m venv --clear .venv 39 | .venv/bin/pip install -U pip 40 | .venv/bin/pip install -U setuptools==69.5.1 41 | .venv/bin/pip install -U wheel 42 | .venv/bin/pip install -U pre-commit 43 | $(MAKE) syncenv 44 | pre-commit install 45 | syncenv: 46 | python.exe -m pip install --upgrade pip 47 | pip install -Ur requirements.all.txt 48 | pre-commit install 49 | help: 50 | @echo "$$HELP_BODY" 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qBitrr 2 | 3 | [![PyPI - License](https://img.shields.io/pypi/l/qbitrr)](https://github.com/Feramance/Qbitrr/blob/master/LICENSE) 4 | [![PyPI](https://img.shields.io/pypi/v/qBitrr2?label=PyPI)](https://pypi.org/project/qBitrr2/) 5 | [![Downloads](https://img.shields.io/pypi/dm/qbitrr2)](https://pypi.org/project/qBitrr2/) 6 | [![Pulls](https://img.shields.io/docker/pulls/feramance/qbitrr.svg)](https://hub.docker.com/r/feramance/qbitrr) 7 | 8 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qbitrr) 9 | [![Platforms](https://img.shields.io/badge/platform-linux--64%20%7C%20osx--64%20%7C%20win--32%20%7C%20win--64-lightgrey)](https://github.com/Feramance/qBitrr/releases/latest) 10 | 11 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Feramance/qBitrr/master.svg)](https://results.pre-commit.ci/latest/github/Feramance/qBitrr/master) 12 | [![CodeQL](https://github.com/Feramance/qBitrr/actions/workflows/codeql.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/codeql.yml) 13 | [![Create a Release](https://github.com/Feramance/qBitrr/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/release.yml) 14 | [![Nightly Build](https://github.com/Feramance/qBitrr/actions/workflows/nightly.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/nightly.yml) 15 | 16 | [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 17 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 18 | 19 | A simple script to monitor [qBit](https://github.com/qbittorrent/qBittorrent) and communicate with [Radarr](https://github.com/Radarr/Radarr) and [Sonarr](https://github.com/Sonarr/Sonarr) 20 | 21 | # POLL 22 | 23 | [Request searches?](https://github.com/Feramance/qBitrr/discussions/149) 24 | 25 | ## Notice (slowly getting there, will take some time) 26 | 27 | I am starting development on qBitrr+ which will be C# based for better overall performance and will also include a WebUI for better refined control on setting and what to search/upgrade etc. Hoping this will be the be all and end all application to manage your Radarr/Sonarr, Overseerr/Ombi and qBittorrent instances in one UI. This is still in it's very early stages and will likely be a couple months before a concrete alpha is rolled out (from start of February 2024). Once I have something solid I will remove this notice and add a link to the new qBitrr+, in the meantime I will be sharing periodic updates on my [Patreon](https://patreon.com/qBitrr) 28 | 29 | ## Features 30 | 31 | - Monitor qBit for Stalled/bad entries and delete them then blacklist them on Arrs (Option to also trigger a re-search action). 32 | - Monitor qBit for completed entries and tell the appropriate Arr instance to import it: 33 | - `qbitrr DownloadedMoviesScan` for Radarr 34 | - `qbitrr DownloadedEpisodesScan` for Sonarr 35 | - Skip files in qBit entries by extension, folder or regex. 36 | - Monitor completed folder and clean it up. 37 | - Usage of [ffprobe](https://github.com/FFmpeg/FFmpeg) to ensure downloaded entries are valid media. 38 | - Trigger periodic Rss Syncs on the appropriate Arr instances. 39 | - Trigger Queue update on appropriate Arr instances. 40 | - Search requests from [Overseerr](https://github.com/sct/overseerr) or [Ombi](https://github.com/Ombi-app/Ombi). 41 | - Auto add/remove trackers 42 | - Set per tracker values 43 | - **Sonarr v4 support** 44 | - **Radarr v4 and v5 support** 45 | - Monitor Arr's to trigger missing episode searches. 46 | - Searches Radarr missing movies based on Minimum Availability 47 | - Customizable searching by series or singular episodes 48 | - Optionally searches year by year is ascending or descending order (config option available) 49 | - Search for CF Score unmet and cancel torrents base on CF Score or Quality unmet search 50 | - Set minimum free space in download directory and pause torrent downloads accordingly 51 | - Change quality profile temporarily for missing items until found 52 | 53 | ## Tested with 54 | 55 | Some things to know before using it. 56 | 57 | - **Latest supported qbittorrent version is 4.6.7** 58 | - qbittorrent v5 is supported via a config value (this will be removed later on) 59 | - qbittorrent >= 4.5.x 60 | - [Sonarr](https://github.com/Sonarr/Sonarr) and [Radarr](https://github.com/Radarr/Radarr) both setup to add tags to all downloads. 61 | - qBit set to create sub-folders for tag. 62 | 63 | ## Usage 64 | 65 | ### Native 66 | 67 | - `python -m pip install qBitrr2` (I would recommend in a dedicated [venv](https://docs.python.org/3.3/library/venv.html) but that's out of scope) 68 | 69 | Alternatively: 70 | 71 | - Download the [latest release](https://github.com/Feramance/Qbitrr/releases/latest) 72 | 73 | #### Run the script 74 | 75 | 1. Activate your venv 76 | 2. Run `qBitrr2` to generate a config file 77 | 3. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 78 | 4. Run `qBitrr2` if installed through pip again to start the script 79 | 80 | Alternatively: 81 | 82 | 1. Unzip the downloaded release and run it 83 | 2. Run `qBitrr` to generate a config file 84 | 3. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 85 | 4. Run `qBitrr` if installed through pip again to start the script 86 | 87 | #### How to update the script 88 | 89 | 1. Activate your venv 90 | 2. Run `python -m pip install -U qBitrr2` 91 | 92 | Alternatively: 93 | 94 | 1. Download on the [latest release](https://github.com/Feramance/Qbitrr/releases/latest) 95 | 2. Unzip the downloaded release and run it 96 | 3. Run `qBitrr` to generate a config file 97 | 4. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 98 | 5. Run `qBitrr` if installed through pip again to start the script 99 | 100 | ***There is no auto-update feature, you will need to manually download the latest release and replace the old one.*** 101 | 102 | ### Docker 103 | 104 | #### Docker Image 105 | 106 | - The docker image can be found on [DockerHub](https://hub.docker.com/r/feramance/qbitrr) or [Github](https://github.com/Feramance/qBitrr/pkgs/container/qbitrr) 107 | 108 | #### Docker Run 109 | 110 | ```bash 111 | docker run -d \ 112 | --name=qbitrr \ 113 | -e TZ=Europe/London \ 114 | -v /etc/localtime:/etc/localtime:ro \ 115 | -v /path/to/appdata/qbitrr:/config \ 116 | -v /path/to/completed/downloads/folder:/completed_downloads:rw \ 117 | --restart unless-stopped \ 118 | feramance/qbitrr:latest 119 | ``` 120 | 121 | #### Docker Compose 122 | 123 | ```yaml 124 | version: "3" 125 | services: 126 | qbitrr: 127 | image: feramance/qbitrr:latest 128 | user: 1000:1000 # Required to ensure the container is run as the user who has perms to see the 2 mount points and the ability to write to the CompletedDownloadFolder mount 129 | tty: true # Ensure the output of docker-compose logs qBitrr are properly colored. 130 | restart: unless-stopped 131 | # networks: This container MUST share a network with your Sonarr/Radarr instances 132 | environment: 133 | - TZ=Europe/London 134 | volumes: 135 | - /etc/localtime:/etc/localtime:ro 136 | - /path/to/appdata/qbitrr:/config # Config folder for qBitrr 137 | - /path/to/completed/downloads/folder:/completed_downloads:rw # The script will ALWAYS require write permission in this folder if mounted, this folder is used to monitor completed downloads and if not present will cause the script to ignore downloaded file monitoring. 138 | # Now just to make sure it is clean, when using this script in a docker you will need to ensure you config.toml values reflect the mounted folders. 139 | # The same would apply to Settings.CompletedDownloadFolder 140 | # e.g CompletedDownloadFolder = /completed_downloads/folder/in/container 141 | 142 | logging: # this script will generate a LOT of logs - so it is up to you to decide how much of it you want to store 143 | driver: "json-file" 144 | options: 145 | max-size: "50m" 146 | max-file: 3 147 | depends_on: # Not needed but this ensures qBitrr only starts if the dependencies are up and running 148 | - qbittorrent 149 | - radarr-1080p 150 | - radarr-4k 151 | - sonarr-1080p 152 | - sonarr-anime 153 | - overseerr 154 | - ombi 155 | ``` 156 | 157 | ##### Important mentions for docker 158 | 159 | - The script will always expect a completed config.toml file 160 | - When you first start the container a "config.rename_me.toml" will be added to `/path/to/appdata/qbitrr` 161 | - Make sure to rename it to 'config.toml' then edit it to your desired values 162 | 163 | ## Feature Suggestions 164 | 165 | Please do not hesitate to open an issue for feature requests or any suggestions you may have. I plan on periodically adding any features I might feel I want to add but welcome to other suggestions I might not have thought of yet. 166 | 167 | ## Reporting an Issue 168 | 169 | When reporting an issue, please ensure that log files are enabled while running qBitrr and attach them to the issue. Thank you. 170 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Feramance/qBitrr/c4cc9b3ebe8e6ecda56bca944ab0b8041a5420fa/assets/logo.png -------------------------------------------------------------------------------- /assets/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Feramance/qBitrr/c4cc9b3ebe8e6ecda56bca944ab0b8041a5420fa/assets/logo.psd -------------------------------------------------------------------------------- /assets/logov2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Feramance/qBitrr/c4cc9b3ebe8e6ecda56bca944ab0b8041a5420fa/assets/logov2.png -------------------------------------------------------------------------------- /build.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | PROJECT_NAME = "qBitrr" 6 | 7 | 8 | a = Analysis(['qBitrr/main.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[], 12 | hiddenimports=[], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=[], 16 | excludes=[], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False) 21 | 22 | pyz = PYZ(a.pure, a.zipped_data, 23 | cipher=block_cipher) 24 | 25 | exe = EXE(pyz, 26 | a.scripts, 27 | a.binaries, 28 | a.zipfiles, 29 | a.datas, 30 | [], 31 | name=PROJECT_NAME, 32 | debug=False, 33 | bootloader_ignore_signals=False, 34 | strip=False, 35 | upx=True, 36 | upx_exclude=[], 37 | runtime_tmpdir=None, 38 | console=True, 39 | disable_windowed_traceback=False 40 | ) 41 | -------------------------------------------------------------------------------- /config.example.toml: -------------------------------------------------------------------------------- 1 | # This is a config file for the qBitrr Script - Make sure to change all entries of "CHANGE_ME". 2 | # This is a config file should be moved to "/config". 3 | 4 | 5 | [Settings] 6 | # Level of logging; One of CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACE 7 | ConsoleLevel = "INFO" 8 | 9 | # Enable logging to files 10 | Logging = true 11 | 12 | # Folder where your completed downloads are put into. Can be found in qBitTorrent -> Options -> Downloads -> Default Save Path (Please note, replace all '\' with '/') 13 | CompletedDownloadFolder = "CHANGE_ME" 14 | 15 | #The desired amount of free space in the downloads directory [K=kilobytes, M=megabytes, G=gigabytes, T=terabytes] (set to -1 to disable, this bypasses AutoPauseResume) 16 | FreeSpace = "-1" 17 | 18 | # Folder where the free space handler will check for free space (Please note, replace all '\' with '/') 19 | FreeSpaceFolder = "CHANGE_ME" 20 | 21 | # Enable automation of pausing and resuming torrents as needed (Required enabled for the FreeSpace logic to function) 22 | AutoPauseResume = true 23 | 24 | # Time to sleep for if there is no internet (in seconds: 600 = 10 Minutes) 25 | NoInternetSleepTimer = 15 26 | 27 | # Time to sleep between reprocessing torrents (in seconds: 600 = 10 Minutes) 28 | LoopSleepTimer = 5 29 | 30 | # Time to sleep between posting search commands (in seconds: 600 = 10 Minutes) 31 | SearchLoopDelay = -1 32 | 33 | # Add torrents to this category to mark them as failed 34 | FailedCategory = "failed" 35 | 36 | # Add torrents to this category to trigger them to be rechecked properly 37 | RecheckCategory = "recheck" 38 | 39 | # Tagless operation 40 | Tagless = false 41 | 42 | # Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes) 43 | # Only applicable to Re-check and failed categories 44 | IgnoreTorrentsYoungerThan = 600 45 | 46 | # URL to be pinged to check if you have a valid internet connection 47 | # These will be pinged a **LOT** make sure the service is okay with you sending all the continuous pings. 48 | PingURLS = ["one.one.one.one", "dns.google.com"] 49 | 50 | # FFprobe auto updates, binaries are downloaded from https://ffbinaries.com/downloads 51 | # If this is disabled and you want ffprobe to work 52 | # Ensure that you add the ffprobe binary to the folder"/config/qBitManager/ffprobe.exe" 53 | # If no `ffprobe` binary is found in the folder above all ffprobe functionality will be disabled. 54 | # By default this will always be on even if config does not have these key - to disable you need to explicitly set it to `False` 55 | FFprobeAutoUpdate = true 56 | 57 | 58 | [qBit] 59 | # If this is enabled qBitrr can run in headless mode where it will only process searches. 60 | # If media search is enabled in their individual categories 61 | # This is useful if you use for example Sabnzbd/NZBGet for downloading content but still want the faster media searches provided by qbit 62 | Disabled = false 63 | 64 | # qBittorrent WebUI url/ip - Can be found in Options > Web UI (called "IP Address") 65 | Host = "CHANGE_ME" 66 | 67 | # qBittorrent WebUI Port - Can be found in Options > Web UI (called "Port" on top right corner of the window) 68 | Port = 8080 69 | 70 | # qBittorrent WebUI Authentication - Can be found in Options > Web UI > Authentication 71 | UserName = "CHANGE_ME" 72 | 73 | # If you set "Bypass authentication on localhost or whitelisted IPs" remove this field. 74 | Password = "CHANGE_ME" 75 | 76 | # Set to true to allow qbittorrent v5 (Some API calls will not work as expected due to qbittorrent API issues not qBitrr) 77 | v5 = false 78 | 79 | 80 | [Sonarr-TV] 81 | # Toggle whether to manage the Servarr instance torrents. 82 | Managed = true 83 | 84 | # The URL used to access Servarr interface eg. http://ip:port (if you use a domain enter the domain without a port) 85 | URI = "CHANGE_ME" 86 | 87 | # The Servarr API Key, Can be found it Settings > General > Security 88 | APIKey = "CHANGE_ME" 89 | 90 | # Category applied by Servarr to torrents in qBitTorrent, can be found in Settings > Download Clients > qBit > Category 91 | Category = "sonarr-tv" 92 | 93 | # Toggle whether to send a query to Servarr to search any failed torrents 94 | ReSearch = true 95 | 96 | # The Servarr's Import Mode(one of Move, Copy or Auto) 97 | importMode = "Auto" 98 | 99 | # Timer to call RSSSync (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 100 | RssSyncTimer = 5 101 | 102 | # Timer to call RefreshDownloads to update the queue. (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 103 | RefreshDownloadsTimer = 5 104 | 105 | # Error messages shown my the Arr instance which should be considered failures. 106 | # This entry should be a list, leave it empty if you want to disable this error handling. 107 | # If enabled qBitrr will remove the failed files and tell the Arr instance the download failed 108 | ArrErrorCodesToBlocklist = ["Not an upgrade for existing episode file(s)", "Not a preferred word upgrade for existing episode file(s)", "Unable to determine if file is a sample"] 109 | 110 | 111 | [Sonarr-TV.EntrySearch] 112 | # All these settings depends on SearchMissing being True and access to the Servarr database file. 113 | 114 | # Should search for Missing files? 115 | SearchMissing = true 116 | 117 | # Should search for specials episodes? (Season 00) 118 | AlsoSearchSpecials = false 119 | 120 | # Should search for unmonitored episodes/series? 121 | Unmonitored = false 122 | 123 | # Maximum allowed Searches at any one points (I wouldn't recommend settings this too high) 124 | # Sonarr has a hardcoded cap of 3 simultaneous tasks 125 | SearchLimit = 5 126 | 127 | # It will order searches by the year the EPISODE was first aired 128 | SearchByYear = true 129 | 130 | # Reverse search order (Start searching oldest to newest) 131 | SearchInReverse = false 132 | 133 | # Delay between request searches in seconds 134 | SearchRequestsEvery = 300 135 | 136 | # Search movies which already have a file in the database in hopes of finding a better quality version. 137 | DoUpgradeSearch = false 138 | 139 | # Do a quality unmet search for existing entries. 140 | QualityUnmetSearch = false 141 | 142 | # Do a minimum custom format score unmet search for existing entries. 143 | CustomFormatUnmetSearch = false 144 | 145 | # Automatically remove torrents that do not mee the minimum custom format score. 146 | ForceMinimumCustomFormat = false 147 | 148 | # Once you have search all files on your specified year range restart the loop and search again. 149 | SearchAgainOnSearchCompletion = true 150 | 151 | # Use Temp profile for missing 152 | UseTempForMissing = false 153 | 154 | # Don't change back to main profile 155 | KeepTempProfile = false 156 | 157 | # Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles) 158 | MainQualityProfile = [] 159 | 160 | # Temp quality profile (To pair quality profiles, ensure they are in the same order as in the main profiles) 161 | TempQualityProfile = [] 162 | 163 | # Search by series instead of by episode (This ignored the QualityUnmetSearch and CustomFormatUnmetSearch setting) 164 | SearchBySeries = true 165 | 166 | # Prioritize Today's releases (Similar effect as RSS Sync, where it searches today's release episodes first, only works on Sonarr). 167 | PrioritizeTodaysReleases = true 168 | 169 | 170 | [Sonarr-TV.EntrySearch.Ombi] 171 | # Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.) 172 | SearchOmbiRequests = false 173 | 174 | # Ombi URI eg. http://ip:port (Note that this has to be the instance of Ombi which manage the Arr instance request (If you have multiple Ombi instances) 175 | OmbiURI = "CHANGE_ME" 176 | 177 | # Ombi's API Key 178 | OmbiAPIKey = "CHANGE_ME" 179 | 180 | # Only process approved requests 181 | ApprovedOnly = true 182 | 183 | 184 | [Sonarr-TV.EntrySearch.Overseerr] 185 | # Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.) 186 | # If this and Ombi are both enable, Ombi will be ignored 187 | SearchOverseerrRequests = false 188 | 189 | # Overseerr's URI eg. http://ip:port 190 | OverseerrURI = "CHANGE_ME" 191 | 192 | # Overseerr's API Key 193 | OverseerrAPIKey = "CHANGE_ME" 194 | 195 | # Only process approved requests 196 | ApprovedOnly = true 197 | 198 | # Only for 4K Instances 199 | # Only for 4K Instances 200 | Is4K = false 201 | 202 | 203 | [Sonarr-TV.Torrent] 204 | # Set it to regex matches to respect/ignore case. 205 | CaseSensitiveMatches = false 206 | 207 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 208 | # These regex need to be escaped, that's why you see so many backslashes. 209 | FolderExclusionRegex = ["\\bextras?\\b", "\\bfeaturettes?\\b", "\\bsamples?\\b", "\\bscreens?\\b", "\\bnc(ed|op)?(\\\\d+)?\\b"] 210 | 211 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 212 | # These regex need to be escaped, that's why you see so many backslashes. 213 | FileNameExclusionRegex = ["\\bncop\\\\d+?\\b", "\\bnced\\\\d+?\\b", "\\bsample\\b", "brarbg.com\\b", "\\btrailer\\b", "music video", "comandotorrents.com"] 214 | 215 | # Only files with these extensions will be allowed to be downloaded, comma separated strings or regex, leave it empty to allow all extensions 216 | FileExtensionAllowlist = [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"] 217 | 218 | # Auto delete files that can't be playable (i.e .exe, .png) 219 | AutoDelete = false 220 | 221 | # Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes) 222 | IgnoreTorrentsYoungerThan = 600 223 | 224 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 225 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 226 | # Note that if you set the MaximumETA on a tracker basis that value is favoured over this value 227 | MaximumETA = 604800 228 | 229 | # Do not delete torrents with higher completion percentage than this setting (0.5 = 50%, 1.0 = 100%) 230 | MaximumDeletablePercentage = 0.99 231 | 232 | # Ignore slow torrents. 233 | DoNotRemoveSlow = true 234 | 235 | # Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite) 236 | StalledDelay = 15 237 | 238 | # Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed. 239 | ReSearchStalled = false 240 | 241 | 242 | [Sonarr-TV.Torrent.SeedingMode] 243 | # Set the maximum allowed download rate for torrents 244 | # Set this value to -1 to disabled it 245 | # Note that if you set the DownloadRateLimit on a tracker basis that value is favoured over this value 246 | DownloadRateLimitPerTorrent = -1 247 | 248 | # Set the maximum allowed upload rate for torrents 249 | # Set this value to -1 to disabled it 250 | # Note that if you set the UploadRateLimit on a tracker basis that value is favoured over this value 251 | UploadRateLimitPerTorrent = -1 252 | 253 | # Set the maximum allowed upload ratio for torrents 254 | # Set this value to -1 to disabled it 255 | # Note that if you set the MaxUploadRatio on a tracker basis that value is favoured over this value 256 | MaxUploadRatio = -1 257 | 258 | # Set the maximum seeding time in seconds for torrents 259 | # Set this value to -1 to disabled it 260 | # Note that if you set the MaxSeedingTime on a tracker basis that value is favoured over this value 261 | MaxSeedingTime = -1 262 | 263 | # Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime) 264 | RemoveTorrent = -1 265 | 266 | # Enable if you want to remove dead trackers 267 | RemoveDeadTrackers = false 268 | 269 | # If "RemoveDeadTrackers" is set to true then remove trackers with the following messages 270 | RemoveTrackerWithMessage = ["skipping tracker announce (unreachable)", "No such host is known", "unsupported URL protocol", "info hash is not authorized with this tracker"] 271 | 272 | # You can have multiple trackers set here or none just add more subsections. 273 | 274 | [[Sonarr-TV.Torrent.Trackers]] 275 | Name = "Nyaa" 276 | Priority = 10 277 | URI = "http://nyaa.tracker.wf:7777/announce" 278 | MaximumETA = 18000 279 | DownloadRateLimit = -1 280 | UploadRateLimit = -1 281 | MaxUploadRatio = -1 282 | MaxSeedingTime = -1 283 | AddTrackerIfMissing = false 284 | RemoveIfExists = false 285 | SuperSeedMode = false 286 | AddTags = ["qBitrr-anime"] 287 | 288 | 289 | [Sonarr-Anime] 290 | # Toggle whether to manage the Servarr instance torrents. 291 | Managed = true 292 | 293 | # The URL used to access Servarr interface eg. http://ip:port (if you use a domain enter the domain without a port) 294 | URI = "CHANGE_ME" 295 | 296 | # The Servarr API Key, Can be found it Settings > General > Security 297 | APIKey = "CHANGE_ME" 298 | 299 | # Category applied by Servarr to torrents in qBitTorrent, can be found in Settings > Download Clients > qBit > Category 300 | Category = "sonarr-anime" 301 | 302 | # Toggle whether to send a query to Servarr to search any failed torrents 303 | ReSearch = true 304 | 305 | # The Servarr's Import Mode(one of Move, Copy or Auto) 306 | importMode = "Auto" 307 | 308 | # Timer to call RSSSync (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 309 | RssSyncTimer = 5 310 | 311 | # Timer to call RefreshDownloads to update the queue. (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 312 | RefreshDownloadsTimer = 5 313 | 314 | # Error messages shown my the Arr instance which should be considered failures. 315 | # This entry should be a list, leave it empty if you want to disable this error handling. 316 | # If enabled qBitrr will remove the failed files and tell the Arr instance the download failed 317 | ArrErrorCodesToBlocklist = ["Not an upgrade for existing episode file(s)", "Not a preferred word upgrade for existing episode file(s)", "Unable to determine if file is a sample"] 318 | 319 | 320 | [Sonarr-Anime.EntrySearch] 321 | # All these settings depends on SearchMissing being True and access to the Servarr database file. 322 | 323 | # Should search for Missing files? 324 | SearchMissing = true 325 | 326 | # Should search for specials episodes? (Season 00) 327 | AlsoSearchSpecials = false 328 | 329 | # Should search for unmonitored episodes/series? 330 | Unmonitored = false 331 | 332 | # Maximum allowed Searches at any one points (I wouldn't recommend settings this too high) 333 | # Sonarr has a hardcoded cap of 3 simultaneous tasks 334 | SearchLimit = 5 335 | 336 | # It will order searches by the year the EPISODE was first aired 337 | SearchByYear = true 338 | 339 | # Reverse search order (Start searching oldest to newest) 340 | SearchInReverse = false 341 | 342 | # Delay between request searches in seconds 343 | SearchRequestsEvery = 300 344 | 345 | # Search movies which already have a file in the database in hopes of finding a better quality version. 346 | DoUpgradeSearch = false 347 | 348 | # Do a quality unmet search for existing entries. 349 | QualityUnmetSearch = false 350 | 351 | # Do a minimum custom format score unmet search for existing entries. 352 | CustomFormatUnmetSearch = false 353 | 354 | # Automatically remove torrents that do not mee the minimum custom format score. 355 | ForceMinimumCustomFormat = false 356 | 357 | # Once you have search all files on your specified year range restart the loop and search again. 358 | SearchAgainOnSearchCompletion = true 359 | 360 | # Use Temp profile for missing 361 | UseTempForMissing = false 362 | 363 | # Don't change back to main profile 364 | KeepTempProfile = false 365 | 366 | # Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles) 367 | MainQualityProfile = [] 368 | 369 | # Temp quality profile (To pair quality profiles, ensure they are in the same order as in the main profiles) 370 | TempQualityProfile = [] 371 | 372 | # Search by series instead of by episode (This ignored the QualityUnmetSearch and CustomFormatUnmetSearch setting) 373 | SearchBySeries = true 374 | 375 | # Prioritize Today's releases (Similar effect as RSS Sync, where it searches today's release episodes first, only works on Sonarr). 376 | PrioritizeTodaysReleases = true 377 | 378 | 379 | [Sonarr-Anime.EntrySearch.Ombi] 380 | # Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.) 381 | SearchOmbiRequests = false 382 | 383 | # Ombi URI eg. http://ip:port (Note that this has to be the instance of Ombi which manage the Arr instance request (If you have multiple Ombi instances) 384 | OmbiURI = "CHANGE_ME" 385 | 386 | # Ombi's API Key 387 | OmbiAPIKey = "CHANGE_ME" 388 | 389 | # Only process approved requests 390 | ApprovedOnly = true 391 | 392 | 393 | [Sonarr-Anime.EntrySearch.Overseerr] 394 | # Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.) 395 | # If this and Ombi are both enable, Ombi will be ignored 396 | SearchOverseerrRequests = false 397 | 398 | # Overseerr's URI eg. http://ip:port 399 | OverseerrURI = "CHANGE_ME" 400 | 401 | # Overseerr's API Key 402 | OverseerrAPIKey = "CHANGE_ME" 403 | 404 | # Only process approved requests 405 | ApprovedOnly = true 406 | 407 | # Only for 4K Instances 408 | # Only for 4K Instances 409 | Is4K = false 410 | 411 | 412 | [Sonarr-Anime.Torrent] 413 | # Set it to regex matches to respect/ignore case. 414 | CaseSensitiveMatches = false 415 | 416 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 417 | # These regex need to be escaped, that's why you see so many backslashes. 418 | FolderExclusionRegex = ["\\bextras?\\b", "\\bfeaturettes?\\b", "\\bsamples?\\b", "\\bscreens?\\b", "\\bspecials?\\b", "\\bova\\b", "\\bnc(ed|op)?(\\\\d+)?\\b"] 419 | 420 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 421 | # These regex need to be escaped, that's why you see so many backslashes. 422 | FileNameExclusionRegex = ["\\bncop\\\\d+?\\b", "\\bnced\\\\d+?\\b", "\\bsample\\b", "brarbg.com\\b", "\\btrailer\\b", "music video", "comandotorrents.com"] 423 | 424 | # Only files with these extensions will be allowed to be downloaded, comma separated strings or regex, leave it empty to allow all extensions 425 | FileExtensionAllowlist = [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"] 426 | 427 | # Auto delete files that can't be playable (i.e .exe, .png) 428 | AutoDelete = false 429 | 430 | # Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes) 431 | IgnoreTorrentsYoungerThan = 600 432 | 433 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 434 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 435 | # Note that if you set the MaximumETA on a tracker basis that value is favoured over this value 436 | MaximumETA = 604800 437 | 438 | # Do not delete torrents with higher completion percentage than this setting (0.5 = 50%, 1.0 = 100%) 439 | MaximumDeletablePercentage = 0.99 440 | 441 | # Ignore slow torrents. 442 | DoNotRemoveSlow = true 443 | 444 | # Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite) 445 | StalledDelay = 15 446 | 447 | # Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed. 448 | ReSearchStalled = false 449 | 450 | 451 | [Sonarr-Anime.Torrent.SeedingMode] 452 | # Set the maximum allowed download rate for torrents 453 | # Set this value to -1 to disabled it 454 | # Note that if you set the DownloadRateLimit on a tracker basis that value is favoured over this value 455 | DownloadRateLimitPerTorrent = -1 456 | 457 | # Set the maximum allowed upload rate for torrents 458 | # Set this value to -1 to disabled it 459 | # Note that if you set the UploadRateLimit on a tracker basis that value is favoured over this value 460 | UploadRateLimitPerTorrent = -1 461 | 462 | # Set the maximum allowed upload ratio for torrents 463 | # Set this value to -1 to disabled it 464 | # Note that if you set the MaxUploadRatio on a tracker basis that value is favoured over this value 465 | MaxUploadRatio = -1 466 | 467 | # Set the maximum seeding time in seconds for torrents 468 | # Set this value to -1 to disabled it 469 | # Note that if you set the MaxSeedingTime on a tracker basis that value is favoured over this value 470 | MaxSeedingTime = -1 471 | 472 | # Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime) 473 | RemoveTorrent = -1 474 | 475 | # Enable if you want to remove dead trackers 476 | RemoveDeadTrackers = false 477 | 478 | # If "RemoveDeadTrackers" is set to true then remove trackers with the following messages 479 | RemoveTrackerWithMessage = ["skipping tracker announce (unreachable)", "No such host is known", "unsupported URL protocol", "info hash is not authorized with this tracker"] 480 | 481 | # You can have multiple trackers set here or none just add more subsections. 482 | 483 | [[Sonarr-Anime.Torrent.Trackers]] 484 | Name = "Nyaa" 485 | Priority = 10 486 | URI = "http://nyaa.tracker.wf:7777/announce" 487 | MaximumETA = 18000 488 | DownloadRateLimit = -1 489 | UploadRateLimit = -1 490 | MaxUploadRatio = -1 491 | MaxSeedingTime = -1 492 | AddTrackerIfMissing = false 493 | RemoveIfExists = false 494 | SuperSeedMode = false 495 | AddTags = ["qBitrr-anime"] 496 | 497 | 498 | [Radarr-1080] 499 | # Toggle whether to manage the Servarr instance torrents. 500 | Managed = true 501 | 502 | # The URL used to access Servarr interface eg. http://ip:port (if you use a domain enter the domain without a port) 503 | URI = "CHANGE_ME" 504 | 505 | # The Servarr API Key, Can be found it Settings > General > Security 506 | APIKey = "CHANGE_ME" 507 | 508 | # Category applied by Servarr to torrents in qBitTorrent, can be found in Settings > Download Clients > qBit > Category 509 | Category = "radarr-1080" 510 | 511 | # Toggle whether to send a query to Servarr to search any failed torrents 512 | ReSearch = true 513 | 514 | # The Servarr's Import Mode(one of Move, Copy or Auto) 515 | importMode = "Auto" 516 | 517 | # Timer to call RSSSync (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 518 | RssSyncTimer = 5 519 | 520 | # Timer to call RefreshDownloads to update the queue. (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 521 | RefreshDownloadsTimer = 5 522 | 523 | # Error messages shown my the Arr instance which should be considered failures. 524 | # This entry should be a list, leave it empty if you want to disable this error handling. 525 | # If enabled qBitrr will remove the failed files and tell the Arr instance the download failed 526 | ArrErrorCodesToBlocklist = ["Not an upgrade for existing movie file(s)", "Not a preferred word upgrade for existing movie file(s)", "Unable to determine if file is a sample"] 527 | 528 | 529 | [Radarr-1080.EntrySearch] 530 | # All these settings depends on SearchMissing being True and access to the Servarr database file. 531 | 532 | # Should search for Missing files? 533 | SearchMissing = true 534 | 535 | # Should search for unmonitored movies? 536 | Unmonitored = false 537 | 538 | # Radarr has a default of 3 simultaneous tasks, which can be increased up to 10 tasks 539 | # If you set the environment variable of "THREAD_LIMIT" to a number between and including 2-10 540 | # Radarr devs have stated that this is an unsupported feature so you will not get any support for doing so from them. 541 | # That being said I've been daily driving 10 simultaneous tasks for quite a while now with no issues. 542 | SearchLimit = 5 543 | 544 | # It will order searches by the year the EPISODE was first aired 545 | SearchByYear = true 546 | 547 | # Reverse search order (Start searching oldest to newest) 548 | SearchInReverse = false 549 | 550 | # Delay between request searches in seconds 551 | SearchRequestsEvery = 300 552 | 553 | # Search movies which already have a file in the database in hopes of finding a better quality version. 554 | DoUpgradeSearch = false 555 | 556 | # Do a quality unmet search for existing entries. 557 | QualityUnmetSearch = false 558 | 559 | # Do a minimum custom format score unmet search for existing entries. 560 | CustomFormatUnmetSearch = false 561 | 562 | # Automatically remove torrents that do not mee the minimum custom format score. 563 | ForceMinimumCustomFormat = false 564 | 565 | # Once you have search all files on your specified year range restart the loop and search again. 566 | SearchAgainOnSearchCompletion = true 567 | 568 | # Use Temp profile for missing 569 | UseTempForMissing = false 570 | 571 | # Don't change back to main profile 572 | KeepTempProfile = false 573 | 574 | # Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles) 575 | MainQualityProfile = [] 576 | 577 | # Temp quality profile (To pair quality profiles, ensure they are in the same order as in the main profiles) 578 | TempQualityProfile = [] 579 | 580 | 581 | [Radarr-1080.EntrySearch.Ombi] 582 | # Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.) 583 | SearchOmbiRequests = false 584 | 585 | # Ombi URI eg. http://ip:port (Note that this has to be the instance of Ombi which manage the Arr instance request (If you have multiple Ombi instances) 586 | OmbiURI = "CHANGE_ME" 587 | 588 | # Ombi's API Key 589 | OmbiAPIKey = "CHANGE_ME" 590 | 591 | # Only process approved requests 592 | ApprovedOnly = true 593 | 594 | 595 | [Radarr-1080.EntrySearch.Overseerr] 596 | # Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.) 597 | # If this and Ombi are both enable, Ombi will be ignored 598 | SearchOverseerrRequests = false 599 | 600 | # Overseerr's URI eg. http://ip:port 601 | OverseerrURI = "CHANGE_ME" 602 | 603 | # Overseerr's API Key 604 | OverseerrAPIKey = "CHANGE_ME" 605 | 606 | # Only process approved requests 607 | ApprovedOnly = true 608 | 609 | # Only for 4K Instances 610 | # Only for 4K Instances 611 | Is4K = false 612 | 613 | 614 | [Radarr-1080.Torrent] 615 | # Set it to regex matches to respect/ignore case. 616 | CaseSensitiveMatches = false 617 | 618 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 619 | # These regex need to be escaped, that's why you see so many backslashes. 620 | FolderExclusionRegex = ["\\bextras?\\b", "\\bfeaturettes?\\b", "\\bsamples?\\b", "\\bscreens?\\b", "\\bnc(ed|op)?(\\\\d+)?\\b"] 621 | 622 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 623 | # These regex need to be escaped, that's why you see so many backslashes. 624 | FileNameExclusionRegex = ["\\bncop\\\\d+?\\b", "\\bnced\\\\d+?\\b", "\\bsample\\b", "brarbg.com\\b", "\\btrailer\\b", "music video", "comandotorrents.com"] 625 | 626 | # Only files with these extensions will be allowed to be downloaded, comma separated strings or regex, leave it empty to allow all extensions 627 | FileExtensionAllowlist = [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"] 628 | 629 | # Auto delete files that can't be playable (i.e .exe, .png) 630 | AutoDelete = false 631 | 632 | # Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes) 633 | IgnoreTorrentsYoungerThan = 600 634 | 635 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 636 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 637 | # Note that if you set the MaximumETA on a tracker basis that value is favoured over this value 638 | MaximumETA = 604800 639 | 640 | # Do not delete torrents with higher completion percentage than this setting (0.5 = 50%, 1.0 = 100%) 641 | MaximumDeletablePercentage = 0.99 642 | 643 | # Ignore slow torrents. 644 | DoNotRemoveSlow = true 645 | 646 | # Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite) 647 | StalledDelay = 15 648 | 649 | # Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed. 650 | ReSearchStalled = false 651 | 652 | 653 | [Radarr-1080.Torrent.SeedingMode] 654 | # Set the maximum allowed download rate for torrents 655 | # Set this value to -1 to disabled it 656 | # Note that if you set the DownloadRateLimit on a tracker basis that value is favoured over this value 657 | DownloadRateLimitPerTorrent = -1 658 | 659 | # Set the maximum allowed upload rate for torrents 660 | # Set this value to -1 to disabled it 661 | # Note that if you set the UploadRateLimit on a tracker basis that value is favoured over this value 662 | UploadRateLimitPerTorrent = -1 663 | 664 | # Set the maximum allowed upload ratio for torrents 665 | # Set this value to -1 to disabled it 666 | # Note that if you set the MaxUploadRatio on a tracker basis that value is favoured over this value 667 | MaxUploadRatio = -1 668 | 669 | # Set the maximum seeding time in seconds for torrents 670 | # Set this value to -1 to disabled it 671 | # Note that if you set the MaxSeedingTime on a tracker basis that value is favoured over this value 672 | MaxSeedingTime = -1 673 | 674 | # Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime) 675 | RemoveTorrent = -1 676 | 677 | # Enable if you want to remove dead trackers 678 | RemoveDeadTrackers = false 679 | 680 | # If "RemoveDeadTrackers" is set to true then remove trackers with the following messages 681 | RemoveTrackerWithMessage = ["skipping tracker announce (unreachable)", "No such host is known", "unsupported URL protocol", "info hash is not authorized with this tracker"] 682 | 683 | # You can have multiple trackers set here or none just add more subsections. 684 | 685 | [[Radarr-1080.Torrent.Trackers]] 686 | Name = "Rarbg-2810" 687 | Priority = 1 688 | URI = "udp://9.rarbg.com:2810/announce" 689 | MaximumETA = 18000 690 | DownloadRateLimit = -1 691 | UploadRateLimit = -1 692 | MaxUploadRatio = -1 693 | MaxSeedingTime = -1 694 | AddTrackerIfMissing = false 695 | RemoveIfExists = false 696 | SuperSeedMode = false 697 | AddTags = ["qBitrr-Rarbg", "Movies and TV"] 698 | 699 | [[Radarr-1080.Torrent.Trackers]] 700 | Name = "Rarbg-2740" 701 | Priority = 2 702 | URI = "udp://9.rarbg.to:2740/announce" 703 | MaximumETA = 18000 704 | DownloadRateLimit = -1 705 | UploadRateLimit = -1 706 | MaxUploadRatio = -1 707 | MaxSeedingTime = -1 708 | AddTrackerIfMissing = false 709 | RemoveIfExists = false 710 | SuperSeedMode = false 711 | 712 | 713 | [Radarr-4K] 714 | # Toggle whether to manage the Servarr instance torrents. 715 | Managed = true 716 | 717 | # The URL used to access Servarr interface eg. http://ip:port (if you use a domain enter the domain without a port) 718 | URI = "CHANGE_ME" 719 | 720 | # The Servarr API Key, Can be found it Settings > General > Security 721 | APIKey = "CHANGE_ME" 722 | 723 | # Category applied by Servarr to torrents in qBitTorrent, can be found in Settings > Download Clients > qBit > Category 724 | Category = "radarr-4k" 725 | 726 | # Toggle whether to send a query to Servarr to search any failed torrents 727 | ReSearch = true 728 | 729 | # The Servarr's Import Mode(one of Move, Copy or Auto) 730 | importMode = "Auto" 731 | 732 | # Timer to call RSSSync (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 733 | RssSyncTimer = 5 734 | 735 | # Timer to call RefreshDownloads to update the queue. (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires) 736 | RefreshDownloadsTimer = 5 737 | 738 | # Error messages shown my the Arr instance which should be considered failures. 739 | # This entry should be a list, leave it empty if you want to disable this error handling. 740 | # If enabled qBitrr will remove the failed files and tell the Arr instance the download failed 741 | ArrErrorCodesToBlocklist = ["Not an upgrade for existing movie file(s)", "Not a preferred word upgrade for existing movie file(s)", "Unable to determine if file is a sample"] 742 | 743 | 744 | [Radarr-4K.EntrySearch] 745 | # All these settings depends on SearchMissing being True and access to the Servarr database file. 746 | 747 | # Should search for Missing files? 748 | SearchMissing = true 749 | 750 | # Should search for unmonitored movies? 751 | Unmonitored = false 752 | 753 | # Radarr has a default of 3 simultaneous tasks, which can be increased up to 10 tasks 754 | # If you set the environment variable of "THREAD_LIMIT" to a number between and including 2-10 755 | # Radarr devs have stated that this is an unsupported feature so you will not get any support for doing so from them. 756 | # That being said I've been daily driving 10 simultaneous tasks for quite a while now with no issues. 757 | SearchLimit = 5 758 | 759 | # It will order searches by the year the EPISODE was first aired 760 | SearchByYear = true 761 | 762 | # Reverse search order (Start searching oldest to newest) 763 | SearchInReverse = false 764 | 765 | # Delay between request searches in seconds 766 | SearchRequestsEvery = 300 767 | 768 | # Search movies which already have a file in the database in hopes of finding a better quality version. 769 | DoUpgradeSearch = false 770 | 771 | # Do a quality unmet search for existing entries. 772 | QualityUnmetSearch = false 773 | 774 | # Do a minimum custom format score unmet search for existing entries. 775 | CustomFormatUnmetSearch = false 776 | 777 | # Automatically remove torrents that do not mee the minimum custom format score. 778 | ForceMinimumCustomFormat = false 779 | 780 | # Once you have search all files on your specified year range restart the loop and search again. 781 | SearchAgainOnSearchCompletion = true 782 | 783 | # Use Temp profile for missing 784 | UseTempForMissing = false 785 | 786 | # Don't change back to main profile 787 | KeepTempProfile = false 788 | 789 | # Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles) 790 | MainQualityProfile = [] 791 | 792 | # Temp quality profile (To pair quality profiles, ensure they are in the same order as in the main profiles) 793 | TempQualityProfile = [] 794 | 795 | 796 | [Radarr-4K.EntrySearch.Ombi] 797 | # Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.) 798 | SearchOmbiRequests = false 799 | 800 | # Ombi URI eg. http://ip:port (Note that this has to be the instance of Ombi which manage the Arr instance request (If you have multiple Ombi instances) 801 | OmbiURI = "CHANGE_ME" 802 | 803 | # Ombi's API Key 804 | OmbiAPIKey = "CHANGE_ME" 805 | 806 | # Only process approved requests 807 | ApprovedOnly = true 808 | 809 | 810 | [Radarr-4K.EntrySearch.Overseerr] 811 | # Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.) 812 | # If this and Ombi are both enable, Ombi will be ignored 813 | SearchOverseerrRequests = false 814 | 815 | # Overseerr's URI eg. http://ip:port 816 | OverseerrURI = "CHANGE_ME" 817 | 818 | # Overseerr's API Key 819 | OverseerrAPIKey = "CHANGE_ME" 820 | 821 | # Only process approved requests 822 | ApprovedOnly = true 823 | 824 | # Only for 4K Instances 825 | # Only for 4K Instances 826 | Is4K = true 827 | 828 | 829 | [Radarr-4K.Torrent] 830 | # Set it to regex matches to respect/ignore case. 831 | CaseSensitiveMatches = false 832 | 833 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 834 | # These regex need to be escaped, that's why you see so many backslashes. 835 | FolderExclusionRegex = ["\\bextras?\\b", "\\bfeaturettes?\\b", "\\bsamples?\\b", "\\bscreens?\\b", "\\bnc(ed|op)?(\\\\d+)?\\b"] 836 | 837 | # These regex values will match any folder where the full name matches the specified values here, comma separated strings. 838 | # These regex need to be escaped, that's why you see so many backslashes. 839 | FileNameExclusionRegex = ["\\bncop\\\\d+?\\b", "\\bnced\\\\d+?\\b", "\\bsample\\b", "brarbg.com\\b", "\\btrailer\\b", "music video", "comandotorrents.com"] 840 | 841 | # Only files with these extensions will be allowed to be downloaded, comma separated strings or regex, leave it empty to allow all extensions 842 | FileExtensionAllowlist = [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"] 843 | 844 | # Auto delete files that can't be playable (i.e .exe, .png) 845 | AutoDelete = false 846 | 847 | # Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes) 848 | IgnoreTorrentsYoungerThan = 600 849 | 850 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 851 | # Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour) 852 | # Note that if you set the MaximumETA on a tracker basis that value is favoured over this value 853 | MaximumETA = 604800 854 | 855 | # Do not delete torrents with higher completion percentage than this setting (0.5 = 50%, 1.0 = 100%) 856 | MaximumDeletablePercentage = 0.99 857 | 858 | # Ignore slow torrents. 859 | DoNotRemoveSlow = true 860 | 861 | # Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite) 862 | StalledDelay = 15 863 | 864 | # Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed. 865 | ReSearchStalled = false 866 | 867 | 868 | [Radarr-4K.Torrent.SeedingMode] 869 | # Set the maximum allowed download rate for torrents 870 | # Set this value to -1 to disabled it 871 | # Note that if you set the DownloadRateLimit on a tracker basis that value is favoured over this value 872 | DownloadRateLimitPerTorrent = -1 873 | 874 | # Set the maximum allowed upload rate for torrents 875 | # Set this value to -1 to disabled it 876 | # Note that if you set the UploadRateLimit on a tracker basis that value is favoured over this value 877 | UploadRateLimitPerTorrent = -1 878 | 879 | # Set the maximum allowed upload ratio for torrents 880 | # Set this value to -1 to disabled it 881 | # Note that if you set the MaxUploadRatio on a tracker basis that value is favoured over this value 882 | MaxUploadRatio = -1 883 | 884 | # Set the maximum seeding time in seconds for torrents 885 | # Set this value to -1 to disabled it 886 | # Note that if you set the MaxSeedingTime on a tracker basis that value is favoured over this value 887 | MaxSeedingTime = -1 888 | 889 | # Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime) 890 | RemoveTorrent = -1 891 | 892 | # Enable if you want to remove dead trackers 893 | RemoveDeadTrackers = false 894 | 895 | # If "RemoveDeadTrackers" is set to true then remove trackers with the following messages 896 | RemoveTrackerWithMessage = ["skipping tracker announce (unreachable)", "No such host is known", "unsupported URL protocol", "info hash is not authorized with this tracker"] 897 | 898 | # You can have multiple trackers set here or none just add more subsections. 899 | 900 | [[Radarr-4K.Torrent.Trackers]] 901 | Name = "Rarbg-2810" 902 | Priority = 1 903 | URI = "udp://9.rarbg.com:2810/announce" 904 | MaximumETA = 18000 905 | DownloadRateLimit = -1 906 | UploadRateLimit = -1 907 | MaxUploadRatio = -1 908 | MaxSeedingTime = -1 909 | AddTrackerIfMissing = false 910 | RemoveIfExists = false 911 | SuperSeedMode = false 912 | AddTags = ["qBitrr-Rarbg", "Movies and TV", "4K"] 913 | 914 | [[Radarr-4K.Torrent.Trackers]] 915 | Name = "Rarbg-2740" 916 | Priority = 2 917 | URI = "udp://9.rarbg.to:2740/announce" 918 | MaximumETA = 18000 919 | DownloadRateLimit = -1 920 | UploadRateLimit = -1 921 | MaxUploadRatio = -1 922 | MaxSeedingTime = -1 923 | AddTrackerIfMissing = false 924 | RemoveIfExists = false 925 | SuperSeedMode = false 926 | AddTags = ["4K"] 927 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | line_length = 99 4 | py_version = 38 5 | known_third_party = [ 6 | "cachetools", 7 | "colorama", 8 | "coloredlogs", 9 | "environ", 10 | "ffmpeg", 11 | "jaraco.docker", 12 | "packaging", 13 | "pathos", 14 | "peewee", 15 | "ping3", 16 | "pyarr", 17 | "qbittorrentapi", 18 | "requests", 19 | "tomlkit", 20 | "ujson", 21 | ] 22 | known_local_folder = ["qBitrr"] 23 | 24 | [tool.black] 25 | line-length = 99 26 | target-version = ['py38'] 27 | 28 | [tool.poetry] 29 | name = "pypi-public" 30 | version = "4.10.23" 31 | description = "A simple script to monitor qBit and communicate with Radarr and Sonarr" 32 | authors = ["Drapersniper", "Feramance"] 33 | readme = "README.md" 34 | repository = "https://github.com/Feramance/qBitrr" 35 | url = "https://pypi.org/simple/" 36 | 37 | [tool.autopep8] 38 | max_line_length = 99 39 | ignore = "E712" 40 | in-place = true 41 | recursive = true 42 | aggressive = 3 43 | -------------------------------------------------------------------------------- /qBitrr/__init__.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import platform 3 | 4 | import qBitrr.logger # noqa 5 | 6 | with contextlib.suppress(ImportError): 7 | if platform.python_implementation() == "CPython": 8 | # Only replace complexjson on CPython 9 | # On PyPy it shows a SystemError when attempting to 10 | # decode the responses from qbittorrentapi 11 | import requests 12 | import ujson 13 | 14 | requests.models.complexjson = ujson 15 | -------------------------------------------------------------------------------- /qBitrr/bundled_data.py: -------------------------------------------------------------------------------- 1 | version = "4.10.23" 2 | git_hash = "bc1d052" 3 | license_text = ( 4 | "Licence can be found on:\n\nhttps://github.com/Feramance/qBitrr/blob/master/LICENSE" 5 | ) 6 | patched_version = f"{version}-{git_hash}" 7 | -------------------------------------------------------------------------------- /qBitrr/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import contextlib 5 | import pathlib 6 | import shutil 7 | import sys 8 | 9 | from qBitrr.bundled_data import license_text, patched_version 10 | from qBitrr.env_config import ENVIRO_CONFIG 11 | from qBitrr.gen_config import MyConfig, _write_config_file, generate_doc 12 | from qBitrr.home_path import APPDATA_FOLDER, HOME_PATH 13 | 14 | 15 | def process_flags() -> argparse.Namespace | bool: 16 | parser = argparse.ArgumentParser(description="An interface to interact with qBit and *arrs.") 17 | parser.add_argument( 18 | "--gen-config", 19 | "-gc", 20 | dest="gen_config", 21 | help="Generate a config file in the current working directory", 22 | action="store_true", 23 | ) 24 | parser.add_argument( 25 | "-v", "--version", action="version", version=f"qBitrr version: {patched_version}" 26 | ) 27 | 28 | parser.add_argument( 29 | "-l", 30 | "--license", 31 | dest="license", 32 | action="store_const", 33 | const=license_text, 34 | help="Show the qBitrr's licence", 35 | ) 36 | parser.add_argument( 37 | "-s", 38 | "--source", 39 | action="store_const", 40 | dest="source", 41 | const="Source code can be found on: https://github.com/Feramance/qBitrr", 42 | help="Shows a link to qBitrr's source", 43 | ) 44 | 45 | args = parser.parse_args() 46 | 47 | if args.gen_config: 48 | from qBitrr.gen_config import _write_config_file 49 | 50 | _write_config_file() 51 | return True 52 | elif args.license: 53 | print(args.license) 54 | return True 55 | elif args.source: 56 | print(args.source) 57 | return True 58 | return args 59 | 60 | 61 | COPIED_TO_NEW_DIR = False 62 | file = "config.toml" 63 | CONFIG_EXISTS = True 64 | CONFIG_FILE = HOME_PATH.joinpath(file) 65 | CONFIG_PATH = pathlib.Path(f"./{file}") 66 | if any( 67 | a in sys.argv 68 | for a in [ 69 | "--gen-config", 70 | "-gc", 71 | "--version", 72 | "-v", 73 | "--license", 74 | "-l", 75 | "--source", 76 | "-s", 77 | "-h", 78 | "--help", 79 | ] 80 | ): 81 | CONFIG = MyConfig(CONFIG_FILE, config=generate_doc()) 82 | COPIED_TO_NEW_DIR = None 83 | elif (not CONFIG_FILE.exists()) and (not CONFIG_PATH.exists()): 84 | print(f"{file} has not been found") 85 | 86 | CONFIG_FILE = _write_config_file(docker=True) 87 | print(f"'{CONFIG_FILE.name}' has been generated") 88 | print('Rename it to "config.toml" then edit it and restart the container') 89 | 90 | CONFIG_EXISTS = False 91 | 92 | elif CONFIG_FILE.exists(): 93 | CONFIG = MyConfig(CONFIG_FILE) 94 | else: 95 | with contextlib.suppress( 96 | Exception 97 | ): # If file already exist or can't copy to APPDATA_FOLDER ignore the exception 98 | shutil.copy(CONFIG_PATH, CONFIG_FILE) 99 | COPIED_TO_NEW_DIR = True 100 | CONFIG = MyConfig("./config.toml") 101 | 102 | if COPIED_TO_NEW_DIR is not None: 103 | # print(f"STARTING QBITRR | {CONFIG.path} |\n{CONFIG}") 104 | print("STARTING QBITRR") 105 | else: 106 | print(f"STARTING QBITRR | CONFIG_FILE={CONFIG_FILE} | CONFIG_PATH={CONFIG_PATH}") 107 | 108 | FFPROBE_AUTO_UPDATE = ( 109 | CONFIG.get("Settings.FFprobeAutoUpdate", fallback=True) 110 | if ENVIRO_CONFIG.settings.ffprobe_auto_update is None 111 | else ENVIRO_CONFIG.settings.ffprobe_auto_update 112 | ) 113 | FAILED_CATEGORY = ENVIRO_CONFIG.settings.failed_category or CONFIG.get( 114 | "Settings.FailedCategory", fallback="failed" 115 | ) 116 | RECHECK_CATEGORY = ENVIRO_CONFIG.settings.recheck_category or CONFIG.get( 117 | "Settings.RecheckCategory", fallback="recheck" 118 | ) 119 | TAGLESS = ENVIRO_CONFIG.settings.tagless or CONFIG.get("Settings.Tagless", fallback=False) 120 | CONSOLE_LOGGING_LEVEL_STRING = ENVIRO_CONFIG.settings.console_level or CONFIG.get( 121 | "Settings.ConsoleLevel", fallback="INFO" 122 | ) 123 | ENABLE_LOGS = ENVIRO_CONFIG.settings.logging or CONFIG.get("Settings.Logging", fallback=True) 124 | COMPLETED_DOWNLOAD_FOLDER = ( 125 | ENVIRO_CONFIG.settings.completed_download_folder 126 | or CONFIG.get_or_raise("Settings.CompletedDownloadFolder") 127 | ) 128 | FREE_SPACE = ENVIRO_CONFIG.settings.free_space or CONFIG.get("Settings.FreeSpace", fallback="-1") 129 | FREE_SPACE_FOLDER = ( 130 | (ENVIRO_CONFIG.settings.free_space_folder or CONFIG.get_or_raise("Settings.FreeSpaceFolder")) 131 | if FREE_SPACE != "-1" 132 | else None 133 | ) 134 | NO_INTERNET_SLEEP_TIMER = ENVIRO_CONFIG.settings.no_internet_sleep_timer or CONFIG.get( 135 | "Settings.NoInternetSleepTimer", fallback=60 136 | ) 137 | LOOP_SLEEP_TIMER = ENVIRO_CONFIG.settings.loop_sleep_timer or CONFIG.get( 138 | "Settings.LoopSleepTimer", fallback=5 139 | ) 140 | SEARCH_LOOP_DELAY = ENVIRO_CONFIG.settings.search_loop_delay or CONFIG.get( 141 | "Settings.SearchLoopDelay", fallback=-1 142 | ) 143 | AUTO_PAUSE_RESUME = ENVIRO_CONFIG.settings.auto_pause_resume or CONFIG.get( 144 | "Settings.AutoPauseResume", fallback=True 145 | ) 146 | PING_URLS = ENVIRO_CONFIG.settings.ping_urls or CONFIG.get( 147 | "Settings.PingURLS", fallback=["one.one.one.one", "dns.google.com"] 148 | ) 149 | IGNORE_TORRENTS_YOUNGER_THAN = ENVIRO_CONFIG.settings.ignore_torrents_younger_than or CONFIG.get( 150 | "Settings.IgnoreTorrentsYoungerThan", fallback=600 151 | ) 152 | QBIT_DISABLED = ( 153 | CONFIG.get("qBit.Disabled", fallback=False) 154 | if ENVIRO_CONFIG.qbit.disabled is None 155 | else ENVIRO_CONFIG.qbit.disabled 156 | ) 157 | SEARCH_ONLY = ENVIRO_CONFIG.overrides.search_only 158 | PROCESS_ONLY = ENVIRO_CONFIG.overrides.processing_only 159 | 160 | if QBIT_DISABLED and PROCESS_ONLY: 161 | print("qBittorrent is disabled yet QBITRR_OVERRIDES_PROCESSING_ONLY is enabled") 162 | print( 163 | "Processing monitors qBitTorrents downloads " 164 | "therefore it depends on a health qBitTorrent connection" 165 | ) 166 | print("Exiting...") 167 | sys.exit(1) 168 | 169 | if SEARCH_ONLY and QBIT_DISABLED is False: 170 | QBIT_DISABLED = True 171 | print("QBITRR_OVERRIDES_SEARCH_ONLY is enabled, forcing qBitTorrent setting off") 172 | 173 | # Settings Config Values 174 | FF_VERSION = APPDATA_FOLDER.joinpath("ffprobe_info.json") 175 | FF_PROBE = APPDATA_FOLDER.joinpath("ffprobe") 176 | -------------------------------------------------------------------------------- /qBitrr/env_config.py: -------------------------------------------------------------------------------- 1 | from distutils.util import strtobool 2 | from typing import Optional 3 | 4 | import environ 5 | 6 | 7 | class Converter: 8 | @staticmethod 9 | def int(value: Optional[str]) -> Optional[int]: 10 | return None if value is None else int(value) 11 | 12 | @staticmethod 13 | def list(value: Optional[str], delimiter=",", converter=str) -> Optional[list]: 14 | return None if value is None else list(map(converter, value.split(delimiter))) 15 | 16 | @staticmethod 17 | def bool(value: Optional[str]) -> Optional[bool]: 18 | return None if value is None else strtobool(value) == 1 19 | 20 | 21 | @environ.config(prefix="QBITRR", frozen=True) 22 | class AppConfig: 23 | @environ.config(prefix="OVERRIDES", frozen=True) 24 | class Overrides: 25 | search_only = environ.var(None, converter=Converter.bool) 26 | processing_only = environ.var(None, converter=Converter.bool) 27 | data_path = environ.var(None) 28 | 29 | @environ.config(prefix="SETTINGS", frozen=True) 30 | class Settings: 31 | console_level = environ.var(None) 32 | logging = environ.var(None, converter=Converter.bool) 33 | completed_download_folder = environ.var(None) 34 | free_space = environ.var(None) 35 | free_space_folder = environ.var(None) 36 | no_internet_sleep_timer = environ.var(None, converter=Converter.int) 37 | loop_sleep_timer = environ.var(None, converter=Converter.int) 38 | search_loop_delay = environ.var(None, converter=Converter.int) 39 | auto_pause_resume = environ.var(None, converter=Converter.bool) 40 | failed_category = environ.var(None) 41 | recheck_category = environ.var(None) 42 | tagless = environ.var(None, converter=Converter.bool) 43 | ignore_torrents_younger_than = environ.var(None, converter=Converter.int) 44 | ping_urls = environ.var(None, converter=Converter.list) 45 | ffprobe_auto_update = environ.var(None, converter=Converter.bool) 46 | 47 | @environ.config(prefix="QBIT", frozen=True) 48 | class qBit: 49 | disabled = environ.var(None, converter=Converter.bool) 50 | host = environ.var(None) 51 | port = environ.var(None, converter=Converter.int) 52 | username = environ.var(None) 53 | password = environ.var(None) 54 | v5 = environ.var(False) 55 | 56 | overrides: Overrides = environ.group(Overrides) 57 | settings: Settings = environ.group(Settings) 58 | qbit: qBit = environ.group(qBit) 59 | 60 | 61 | ENVIRO_CONFIG: AppConfig = environ.to_config(AppConfig) 62 | -------------------------------------------------------------------------------- /qBitrr/errors.py: -------------------------------------------------------------------------------- 1 | class qBitManagerError(Exception): 2 | """Base Exception""" 3 | 4 | 5 | class UnhandledError(qBitManagerError): 6 | """Use to raise when there an unhandled edge case""" 7 | 8 | 9 | class ConfigException(qBitManagerError): 10 | """Base Exception for Config related exceptions""" 11 | 12 | 13 | class ArrManagerException(qBitManagerError): 14 | """Base Exception for Arr related Exceptions""" 15 | 16 | 17 | class SkipException(qBitManagerError): 18 | """Dummy error to skip actions""" 19 | 20 | 21 | class RequireConfigValue(qBitManagerError): 22 | """Exception raised when a config value requires a value.""" 23 | 24 | def __init__(self, config_class: str, config_key: str): 25 | self.message = f"Config key '{config_key}' in '{config_class}' requires a value." 26 | 27 | 28 | class NoConnectionrException(qBitManagerError): 29 | def __init__(self, message: str, type: str = "delay"): 30 | self.message = message 31 | self.type = type 32 | 33 | 34 | class DelayLoopException(qBitManagerError): 35 | def __init__(self, length: int, type: str): 36 | self.type = type 37 | self.length = length 38 | 39 | 40 | class RestartLoopException(ArrManagerException): 41 | """Exception to trigger a loop restart""" 42 | -------------------------------------------------------------------------------- /qBitrr/ffprobe.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | import os 5 | import platform 6 | import sys 7 | import zipfile 8 | 9 | import requests 10 | 11 | from qBitrr.config import FF_PROBE, FF_VERSION, FFPROBE_AUTO_UPDATE 12 | from qBitrr.logger import run_logs 13 | 14 | 15 | class FFprobeDownloader: 16 | def __init__(self): 17 | self.api = "https://ffbinaries.com/api/v1/version/latest" 18 | self.version_file = FF_VERSION 19 | self.logger = logging.getLogger("qBitrr.FFprobe") 20 | run_logs(self.logger) 21 | self.platform = platform.system() 22 | if self.platform == "Windows": 23 | self.probe_path = FF_PROBE.with_suffix(".exe") 24 | else: 25 | self.probe_path = FF_PROBE 26 | 27 | def get_upstream_version(self) -> dict: 28 | with requests.Session() as session: 29 | with session.get(self.api) as response: 30 | if response.status_code != 200: 31 | self.logger.warning("Failed to retrieve ffprobe version from API.") 32 | return {} 33 | return response.json() 34 | 35 | def get_current_version(self): 36 | try: 37 | with self.version_file.open(mode="r") as file: 38 | data = json.load(file) 39 | return data.get("version") 40 | except Exception: # If file can't be found or read or parsed 41 | self.logger.warning("Failed to retrieve current ffprobe version.") 42 | return "" 43 | 44 | def update(self): 45 | if not FFPROBE_AUTO_UPDATE: 46 | return 47 | current_version = self.get_current_version() 48 | upstream_data = self.get_upstream_version() 49 | upstream_version = upstream_data.get("version") 50 | if upstream_version is None: 51 | self.logger.debug( 52 | "Failed to retrieve ffprobe version from API.'upstream_version' is None" 53 | ) 54 | return 55 | probe_file_exists = self.probe_path.exists() 56 | if current_version == upstream_version and probe_file_exists: 57 | self.logger.debug("Current FFprobe is up to date.") 58 | return 59 | arch_key = self.get_arch() 60 | urls = upstream_data.get("bin", {}).get(arch_key) 61 | if urls is None: 62 | self.logger.debug("Failed to retrieve ffprobe version from API.'urls' is None") 63 | return 64 | ffprobe_url = urls.get("ffprobe") 65 | self.logger.debug("Downloading newer FFprobe: %s", ffprobe_url) 66 | self.download_and_extract(ffprobe_url) 67 | self.logger.debug("Updating local version of FFprobe: %s", upstream_version) 68 | self.version_file.write_text(json.dumps({"version": upstream_version})) 69 | try: 70 | os.chmod(self.probe_path, 0o777) 71 | self.logger.debug("Successfully changed permissions for ffprobe") 72 | except Exception as e: 73 | self.logger.debug("Failed to change permissions for ffprobe, %s", e) 74 | 75 | def download_and_extract(self, ffprobe_url): 76 | r = requests.get(ffprobe_url) 77 | z = zipfile.ZipFile(io.BytesIO(r.content)) 78 | self.logger.debug("Extracting downloaded FFprobe to: %s", FF_PROBE.parent) 79 | z.extract(member=self.probe_path.name, path=FF_PROBE.parent) 80 | 81 | def get_arch(self): 82 | part1 = None 83 | is_64bits = sys.maxsize > 2**32 84 | part2 = "64" if is_64bits else "32" 85 | if self.platform == "Windows": 86 | part1 = "windows-" 87 | elif self.platform == "Linux": 88 | part1 = "linux-" 89 | machine = platform.machine() 90 | if machine == "armv6l": 91 | part2 = "armhf" 92 | elif ("arm" in machine and is_64bits) or machine == "aarch64": 93 | part2 = "arm64" 94 | # Else just 32/64, Not armel - because just no 95 | elif self.platform == "Darwin": 96 | part1 = "osx-" 97 | part2 = "64" 98 | if part1 is None: 99 | raise RuntimeError( 100 | "You are running in an unsupported platform, " 101 | "if you expect this to be supported please open an issue on GitHub " 102 | "https://github.com/Feramance/qBitrr." 103 | ) 104 | 105 | return part1 + part2 106 | -------------------------------------------------------------------------------- /qBitrr/gen_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | from functools import reduce 5 | from typing import Any, TypeVar 6 | 7 | from tomlkit import comment, document, nl, parse, table 8 | from tomlkit.items import Table 9 | from tomlkit.toml_document import TOMLDocument 10 | 11 | from qBitrr.env_config import ENVIRO_CONFIG 12 | from qBitrr.home_path import APPDATA_FOLDER, HOME_PATH 13 | 14 | T = TypeVar("T") 15 | 16 | 17 | def generate_doc() -> TOMLDocument: 18 | config = document() 19 | config.add( 20 | comment( 21 | "This is a config file for the qBitrr Script - " 22 | 'Make sure to change all entries of "CHANGE_ME".' 23 | ) 24 | ) 25 | config.add(comment('This is a config file should be moved to "' f'{HOME_PATH}".')) 26 | config.add(nl()) 27 | _add_settings_section(config) 28 | _add_qbit_section(config) 29 | _add_category_sections(config) 30 | return config 31 | 32 | 33 | def _add_settings_section(config: TOMLDocument): 34 | settings = table() 35 | _gen_default_line( 36 | settings, 37 | "Level of logging; One of CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACE", 38 | "ConsoleLevel", 39 | ENVIRO_CONFIG.settings.console_level or "INFO", 40 | ) 41 | _gen_default_line( 42 | settings, "Enable logging to files", "Logging", ENVIRO_CONFIG.settings.logging or True 43 | ) 44 | _gen_default_line( 45 | settings, 46 | "Folder where your completed downloads are put into. Can be found in qBitTorrent -> Options -> Downloads -> Default Save Path (Please note, replace all '\\' with '/')", 47 | "CompletedDownloadFolder", 48 | ENVIRO_CONFIG.settings.completed_download_folder or "CHANGE_ME", 49 | ) 50 | _gen_default_line( 51 | settings, 52 | "The desired amount of free space in the downloads directory [K=kilobytes, M=megabytes, G=gigabytes, T=terabytes] (set to -1 to disable, this bypasses AutoPauseResume)", 53 | "FreeSpace", 54 | ENVIRO_CONFIG.settings.free_space or "-1", 55 | ) 56 | _gen_default_line( 57 | settings, 58 | "Folder where the free space handler will check for free space (Please note, replace all '' with '/')", 59 | "FreeSpaceFolder", 60 | ENVIRO_CONFIG.settings.free_space_folder or "CHANGE_ME", 61 | ) 62 | _gen_default_line( 63 | settings, 64 | "Enable automation of pausing and resuming torrents as needed (Required enabled for the FreeSpace logic to function)", 65 | "AutoPauseResume", 66 | ENVIRO_CONFIG.settings.auto_pause_resume or True, 67 | ) 68 | _gen_default_line( 69 | settings, 70 | "Time to sleep for if there is no internet (in seconds: 600 = 10 Minutes)", 71 | "NoInternetSleepTimer", 72 | ENVIRO_CONFIG.settings.no_internet_sleep_timer or 15, 73 | ) 74 | _gen_default_line( 75 | settings, 76 | "Time to sleep between reprocessing torrents (in seconds: 600 = 10 Minutes)", 77 | "LoopSleepTimer", 78 | ENVIRO_CONFIG.settings.loop_sleep_timer or 5, 79 | ) 80 | _gen_default_line( 81 | settings, 82 | "Time to sleep between posting search commands (in seconds: 600 = 10 Minutes)", 83 | "SearchLoopDelay", 84 | ENVIRO_CONFIG.settings.search_loop_delay or -1, 85 | ) 86 | _gen_default_line( 87 | settings, 88 | "Add torrents to this category to mark them as failed", 89 | "FailedCategory", 90 | ENVIRO_CONFIG.settings.failed_category or "failed", 91 | ) 92 | _gen_default_line( 93 | settings, 94 | "Add torrents to this category to trigger them to be rechecked properly", 95 | "RecheckCategory", 96 | ENVIRO_CONFIG.settings.recheck_category or "recheck", 97 | ) 98 | _gen_default_line( 99 | settings, "Tagless operation", "Tagless", ENVIRO_CONFIG.settings.tagless or False 100 | ) 101 | _gen_default_line( 102 | settings, 103 | [ 104 | "Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes)", 105 | "Only applicable to Re-check and failed categories", 106 | ], 107 | "IgnoreTorrentsYoungerThan", 108 | ENVIRO_CONFIG.settings.ignore_torrents_younger_than or 180, 109 | ) 110 | _gen_default_line( 111 | settings, 112 | [ 113 | "URL to be pinged to check if you have a valid internet connection", 114 | "These will be pinged a **LOT** make sure the service is okay with you sending all the continuous pings.", 115 | ], 116 | "PingURLS", 117 | ENVIRO_CONFIG.settings.ping_urls or ["one.one.one.one", "dns.google.com"], 118 | ) 119 | _gen_default_line( 120 | settings, 121 | [ 122 | "FFprobe auto updates, binaries are downloaded from https://ffbinaries.com/downloads", 123 | "If this is disabled and you want ffprobe to work", 124 | "Ensure that you add the ffprobe binary to the folder" 125 | f"\"{APPDATA_FOLDER.joinpath('ffprobe.exe')}\"", 126 | "If no `ffprobe` binary is found in the folder above all ffprobe functionality will be disabled.", 127 | "By default this will always be on even if config does not have these key - to disable you need to explicitly set it to `False`", 128 | ], 129 | "FFprobeAutoUpdate", 130 | True if ENVIRO_CONFIG.settings.ping_urls is None else ENVIRO_CONFIG.settings.ping_urls, 131 | ) 132 | config.add("Settings", settings) 133 | 134 | 135 | def _add_qbit_section(config: TOMLDocument): 136 | qbit = table() 137 | _gen_default_line( 138 | qbit, 139 | [ 140 | "If this is enabled qBitrr can run in headless mode where it will only process searches.", 141 | "If media search is enabled in their individual categories", 142 | "This is useful if you use for example Sabnzbd/NZBGet for downloading content but still want the faster media searches provided by qbit", 143 | ], 144 | "Disabled", 145 | False if ENVIRO_CONFIG.qbit.disabled is None else ENVIRO_CONFIG.qbit.disabled, 146 | ) 147 | _gen_default_line( 148 | qbit, 149 | 'qbittorrent WebUI URL/IP - Can be found in Options > Web UI (called "IP Address")', 150 | "Host", 151 | ENVIRO_CONFIG.qbit.host or "CHANGE_ME", 152 | ) 153 | _gen_default_line( 154 | qbit, 155 | 'qbittorrent WebUI Port - Can be found in Options > Web UI (called "Port" on top right corner of the window)', 156 | "Port", 157 | ENVIRO_CONFIG.qbit.port or 8080, 158 | ) 159 | _gen_default_line( 160 | qbit, 161 | "qbittorrent WebUI Authentication - Can be found in Options > Web UI > Authentication", 162 | "UserName", 163 | ENVIRO_CONFIG.qbit.username or "CHANGE_ME", 164 | ) 165 | _gen_default_line( 166 | qbit, 167 | 'If you set "Bypass authentication on localhost or whitelisted IPs" remove this field.', 168 | "Password", 169 | ENVIRO_CONFIG.qbit.password or "CHANGE_ME", 170 | ) 171 | _gen_default_line( 172 | qbit, 173 | "Set to true to allow qbittorrent v5 (Some API calls will not work as expected due to qbittorrent API issues not qBitrr)", 174 | "v5", 175 | ENVIRO_CONFIG.qbit.v5 or False, 176 | ) 177 | config.add("qBit", qbit) 178 | 179 | 180 | def _add_category_sections(config: TOMLDocument): 181 | for c in ["Sonarr-TV", "Sonarr-Anime", "Radarr-1080", "Radarr-4K"]: 182 | _gen_default_cat(c, config) 183 | 184 | 185 | def _gen_default_cat(category: str, config: TOMLDocument): 186 | cat_default = table() 187 | cat_default.add(nl()) 188 | _gen_default_line( 189 | cat_default, "Toggle whether to manage the Servarr instance torrents.", "Managed", True 190 | ) 191 | _gen_default_line( 192 | cat_default, 193 | "The URL used to access Servarr interface eg. http://ip:port" 194 | "(if you use a domain enter the domain without a port)", 195 | "URI", 196 | "CHANGE_ME", 197 | ) 198 | _gen_default_line( 199 | cat_default, 200 | "The Servarr API Key, Can be found it Settings > General > Security", 201 | "APIKey", 202 | "CHANGE_ME", 203 | ) 204 | _gen_default_line( 205 | cat_default, 206 | "Category applied by Servarr to torrents in qBitTorrent, can be found in Settings > Download Clients > qBit > Category", 207 | "Category", 208 | category.lower(), 209 | ) 210 | _gen_default_line( 211 | cat_default, 212 | "Toggle whether to send a query to Servarr to search any failed torrents", 213 | "ReSearch", 214 | True, 215 | ) 216 | _gen_default_line( 217 | cat_default, "The Servarr's Import Mode(one of Move, Copy or Auto)", "importMode", "Auto" 218 | ) 219 | _gen_default_line( 220 | cat_default, 221 | "Timer to call RSSSync (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires)", 222 | "RssSyncTimer", 223 | 1, 224 | ) 225 | _gen_default_line( 226 | cat_default, 227 | "Timer to call RefreshDownloads to update the queue. (In minutes) - Set to 0 to disable (Values below 5 can cause errors for maximum retires)", 228 | "RefreshDownloadsTimer", 229 | 1, 230 | ) 231 | messages = [] 232 | if "radarr" in category.lower(): 233 | messages.extend( 234 | [ 235 | "Not a preferred word upgrade for existing movie file(s)", 236 | "Not an upgrade for existing movie file(s)", 237 | "Unable to determine if file is a sample", 238 | ] 239 | ) 240 | elif "sonarr" in category.lower(): 241 | messages.extend( 242 | [ 243 | "Not a preferred word upgrade for existing episode file(s)", 244 | "Not an upgrade for existing episode file(s)", 245 | "Unable to determine if file is a sample", 246 | ] 247 | ) 248 | _gen_default_line( 249 | cat_default, 250 | [ 251 | "Error messages shown my the Arr instance which should be considered failures.", 252 | "This entry should be a list, leave it empty if you want to disable this error handling.", 253 | "If enabled qBitrr will remove the failed files and tell the Arr instance the download failed", 254 | ], 255 | "ArrErrorCodesToBlocklist", 256 | list(set(messages)), 257 | ) 258 | _gen_default_search_table(category, cat_default) 259 | _gen_default_torrent_table(category, cat_default) 260 | config.add(category, cat_default) 261 | 262 | 263 | def _gen_default_torrent_table(category: str, cat_default: Table): 264 | torrent_table = table() 265 | _gen_default_line( 266 | torrent_table, 267 | "Set it to regex matches to respect/ignore case.", 268 | "CaseSensitiveMatches", 269 | False, 270 | ) 271 | if "anime" not in category.lower(): 272 | _gen_default_line( 273 | torrent_table, 274 | [ 275 | "These regex values will match any folder where the full name matches the specified values here, comma separated strings.", 276 | "These regex need to be escaped, that's why you see so many backslashes.", 277 | ], 278 | "FolderExclusionRegex", 279 | [ 280 | r"\bextras?\b", 281 | r"\bfeaturettes?\b", 282 | r"\bsamples?\b", 283 | r"\bscreens?\b", 284 | r"\bnc(ed|op)?(\\d+)?\b", 285 | ], 286 | ) 287 | else: 288 | _gen_default_line( 289 | torrent_table, 290 | [ 291 | "These regex values will match any folder where the full name matches the specified values here, comma separated strings.", 292 | "These regex need to be escaped, that's why you see so many backslashes.", 293 | ], 294 | "FolderExclusionRegex", 295 | [ 296 | r"\bextras?\b", 297 | r"\bfeaturettes?\b", 298 | r"\bsamples?\b", 299 | r"\bscreens?\b", 300 | r"\bspecials?\b", 301 | r"\bova\b", 302 | r"\bnc(ed|op)?(\\d+)?\b", 303 | ], 304 | ) 305 | _gen_default_line( 306 | torrent_table, 307 | [ 308 | "These regex values will match any folder where the full name matches the specified values here, comma separated strings.", 309 | "These regex need to be escaped, that's why you see so many backslashes.", 310 | ], 311 | "FileNameExclusionRegex", 312 | [ 313 | r"\bncop\\d+?\b", 314 | r"\bnced\\d+?\b", 315 | r"\bsample\b", 316 | r"brarbg.com\b", 317 | r"\btrailer\b", 318 | r"music video", 319 | r"comandotorrents.com", 320 | ], 321 | ) 322 | _gen_default_line( 323 | torrent_table, 324 | "Only files with these extensions will be allowed to be downloaded, comma separated strings or regex, leave it empty to allow all extensions", 325 | "FileExtensionAllowlist", 326 | [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"], 327 | ) 328 | _gen_default_line( 329 | torrent_table, 330 | "Auto delete files that can't be playable (i.e .exe, .png)", 331 | "AutoDelete", 332 | False, 333 | ) 334 | _gen_default_line( 335 | torrent_table, 336 | "Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes)", 337 | "IgnoreTorrentsYoungerThan", 338 | 180, 339 | ) 340 | _gen_default_line( 341 | torrent_table, 342 | [ 343 | "Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour)", 344 | "Note that if you set the MaximumETA on a tracker basis that value is favoured over this value", 345 | ], 346 | "MaximumETA", 347 | -1, 348 | ) 349 | _gen_default_line( 350 | torrent_table, 351 | "Do not delete torrents with higher completion percentage than this setting (0.5 = 50%, 1.0 = 100%)", 352 | "MaximumDeletablePercentage", 353 | 0.99, 354 | ) 355 | _gen_default_line(torrent_table, "Ignore slow torrents.", "DoNotRemoveSlow", True) 356 | _gen_default_line( 357 | torrent_table, 358 | "Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite)", 359 | "StalledDelay", 360 | 15, 361 | ) 362 | _gen_default_line( 363 | torrent_table, 364 | "Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed.", 365 | "ReSearchStalled", 366 | False, 367 | ) 368 | _gen_default_seeding_table(category, torrent_table) 369 | _gen_default_tracker_tables(category, torrent_table) 370 | 371 | cat_default.add("Torrent", torrent_table) 372 | 373 | 374 | def _gen_default_seeding_table(category: str, torrent_table: Table): 375 | seeding_table = table() 376 | _gen_default_line( 377 | seeding_table, 378 | [ 379 | "Set the maximum allowed download rate for torrents", 380 | "Set this value to -1 to disabled it", 381 | "Note that if you set the DownloadRateLimit on a tracker basis that value is favoured over this value", 382 | ], 383 | "DownloadRateLimitPerTorrent", 384 | -1, 385 | ) 386 | _gen_default_line( 387 | seeding_table, 388 | [ 389 | "Set the maximum allowed upload rate for torrents", 390 | "Set this value to -1 to disabled it", 391 | "Note that if you set the UploadRateLimit on a tracker basis that value is favoured over this value", 392 | ], 393 | "UploadRateLimitPerTorrent", 394 | -1, 395 | ) 396 | _gen_default_line( 397 | seeding_table, 398 | [ 399 | "Set the maximum allowed upload ratio for torrents", 400 | "Set this value to -1 to disabled it", 401 | "Note that if you set the MaxUploadRatio on a tracker basis that value is favoured over this value", 402 | ], 403 | "MaxUploadRatio", 404 | -1, 405 | ) 406 | _gen_default_line( 407 | seeding_table, 408 | [ 409 | "Set the maximum seeding time in seconds for torrents", 410 | "Set this value to -1 to disabled it", 411 | "Note that if you set the MaxSeedingTime on a tracker basis that value is favoured over this value", 412 | ], 413 | "MaxSeedingTime", 414 | -1, 415 | ) 416 | _gen_default_line( 417 | seeding_table, 418 | "Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime)", 419 | "RemoveTorrent", 420 | -1, 421 | ) 422 | _gen_default_line( 423 | seeding_table, "Enable if you want to remove dead trackers", "RemoveDeadTrackers", False 424 | ) 425 | _gen_default_line( 426 | seeding_table, 427 | 'If "RemoveDeadTrackers" is set to true then remove trackers with the following messages', 428 | "RemoveTrackerWithMessage", 429 | [ 430 | "skipping tracker announce (unreachable)", 431 | "No such host is known", 432 | "unsupported URL protocol", 433 | "info hash is not authorized with this tracker", 434 | ], 435 | ) 436 | 437 | torrent_table.add("SeedingMode", seeding_table) 438 | 439 | 440 | def _gen_default_tracker_tables(category: str, torrent_table: Table): 441 | tracker_table_list = [] 442 | tracker_list = [] 443 | if "anime" in category.lower(): 444 | tracker_list.append(("Nyaa", "http://nyaa.tracker.wf:7777/announce", ["qBitrr-anime"], 10)) 445 | elif "radarr" in category.lower(): 446 | t = ["qBitrr-Rarbg", "Movies and TV"] 447 | t2 = [] 448 | if "4k" in category.lower(): 449 | t.append("4K") 450 | t2.append("4K") 451 | tracker_list.extend( 452 | ( 453 | ("Rarbg-2810", "udp://9.rarbg.com:2810/announce", t, 1), 454 | ("Rarbg-2740", "udp://9.rarbg.to:2740/announce", t2, 2), 455 | ) 456 | ) 457 | for name, url, tags, priority in tracker_list: 458 | tracker_table = table() 459 | _gen_default_line( 460 | tracker_table, 461 | "This is only for your own benefit, it is not currently used anywhere, but one day it may be.", 462 | "Name", 463 | name, 464 | ) 465 | tracker_table.add( 466 | comment("This is used when multiple trackers are in one single torrent.") 467 | ) 468 | _gen_default_line( 469 | tracker_table, 470 | "the tracker with the highest priority will have all its settings applied to the torrent.", 471 | "Priority", 472 | priority, 473 | ) 474 | _gen_default_line(tracker_table, "The tracker URI used by qBit.", "URI", url) 475 | _gen_default_line( 476 | tracker_table, 477 | "Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour).", 478 | "MaximumETA", 479 | 18000, 480 | ) 481 | tracker_table.add(comment("Set the maximum allowed download rate for torrents")) 482 | _gen_default_line( 483 | tracker_table, "Set this value to -1 to disabled it", "DownloadRateLimit", -1 484 | ) 485 | tracker_table.add(comment("Set the maximum allowed upload rate for torrents")) 486 | _gen_default_line( 487 | tracker_table, "Set this value to -1 to disabled it", "UploadRateLimit", -1 488 | ) 489 | tracker_table.add(comment("Set the maximum allowed download rate for torrents")) 490 | _gen_default_line( 491 | tracker_table, "Set this value to -1 to disabled it", "MaxUploadRatio", -1 492 | ) 493 | tracker_table.add(comment("Set the maximum allowed download rate for torrents")) 494 | _gen_default_line( 495 | tracker_table, "Set this value to -1 to disabled it", "MaxSeedingTime", -1 496 | ) 497 | _gen_default_line( 498 | tracker_table, 499 | "Add this tracker from any torrent that does not contains it.", 500 | "AddTrackerIfMissing", 501 | False, 502 | ) 503 | _gen_default_line( 504 | tracker_table, 505 | "Remove this tracker from any torrent that contains it.", 506 | "RemoveIfExists", 507 | False, 508 | ) 509 | _gen_default_line( 510 | tracker_table, 511 | "Enable Super Seeding setting for torrents with this tracker.", 512 | "SuperSeedMode", 513 | False, 514 | ) 515 | if tags: 516 | _gen_default_line( 517 | tracker_table, 518 | "Adds these tags to any torrents containing this tracker.", 519 | "AddTags", 520 | tags, 521 | ) 522 | tracker_table_list.append(tracker_table) 523 | torrent_table.add( 524 | comment("You can have multiple trackers set here or none just add more subsections.") 525 | ) 526 | torrent_table.add("Trackers", tracker_table_list) 527 | 528 | 529 | def _gen_default_line(table, comments, field, value): 530 | if isinstance(comments, list): 531 | for c in comments: 532 | table.add(comment(c)) 533 | else: 534 | table.add(comment(comments)) 535 | table.add(field, value) 536 | table.add(nl()) 537 | 538 | 539 | def _gen_default_search_table(category: str, cat_default: Table): 540 | search_table = table() 541 | _gen_default_line(search_table, "Should search for Missing files?", "SearchMissing", True) 542 | if "sonarr" in category.lower(): 543 | _gen_default_line( 544 | search_table, 545 | "Should search for specials episodes? (Season 00)", 546 | "AlsoSearchSpecials", 547 | False, 548 | ) 549 | _gen_default_line( 550 | search_table, 551 | "Should search for unmonitored episodes/series?", 552 | "Unmonitored", 553 | False, 554 | ) 555 | _gen_default_line( 556 | search_table, 557 | [ 558 | "Maximum allowed Searches at any one points (I wouldn't recommend settings this too high)", 559 | "Sonarr has a hardcoded cap of 3 simultaneous tasks", 560 | ], 561 | "SearchLimit", 562 | 5, 563 | ) 564 | elif "radarr" in category.lower(): 565 | _gen_default_line( 566 | search_table, 567 | "Should search for unmonitored movies?", 568 | "Unmonitored", 569 | False, 570 | ) 571 | _gen_default_line( 572 | search_table, 573 | [ 574 | "Radarr has a default of 3 simultaneous tasks, which can be increased up to 10 tasks", 575 | 'If you set the environment variable of "THREAD_LIMIT" to a number between and including 2-10', 576 | "Radarr devs have stated that this is an unsupported feature so you will not get any support for doing so from them.", 577 | "That being said I've been daily driving 10 simultaneous tasks for quite a while now with no issues.", 578 | ], 579 | "SearchLimit", 580 | 5, 581 | ) 582 | _gen_default_line( 583 | search_table, 584 | "It will order searches by the year the EPISODE was first aired", 585 | "SearchByYear", 586 | True, 587 | ) 588 | _gen_default_line( 589 | search_table, 590 | "Reverse search order (Start searching oldest to newest)", 591 | "SearchInReverse", 592 | False, 593 | ) 594 | _gen_default_line( 595 | search_table, "Delay between request searches in seconds", "SearchRequestsEvery", 300 596 | ) 597 | _gen_default_line( 598 | search_table, 599 | "Search movies which already have a file in the database in hopes of finding a " 600 | "better quality version.", 601 | "DoUpgradeSearch", 602 | False, 603 | ) 604 | _gen_default_line( 605 | search_table, 606 | "Do a quality unmet search for existing entries.", 607 | "QualityUnmetSearch", 608 | False, 609 | ) 610 | _gen_default_line( 611 | search_table, 612 | "Do a minimum custom format score unmet search for existing entries.", 613 | "CustomFormatUnmetSearch", 614 | False, 615 | ) 616 | _gen_default_line( 617 | search_table, 618 | "Automatically remove torrents that do not mee the minimum custom format score.", 619 | "ForceMinimumCustomFormat", 620 | False, 621 | ) 622 | _gen_default_line( 623 | search_table, 624 | "Once you have search all files on your specified year range restart the loop and " 625 | "search again.", 626 | "SearchAgainOnSearchCompletion", 627 | True, 628 | ) 629 | _gen_default_line(search_table, "Use Temp profile for missing", "UseTempForMissing", False) 630 | _gen_default_line(search_table, "Don't change back to main profile", "KeepTempProfile", False) 631 | _gen_default_line( 632 | search_table, 633 | "Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles)", 634 | "MainQualityProfile", 635 | [], 636 | ) 637 | _gen_default_line( 638 | search_table, 639 | "Temp quality profile (To pair quality profiles, ensure they are in the same order as in the main profiles)", 640 | "TempQualityProfile", 641 | [], 642 | ) 643 | if "sonarr" in category.lower(): 644 | _gen_default_line( 645 | search_table, 646 | "Search by series instead of by episode (This ignored the QualityUnmetSearch and CustomFormatUnmetSearch setting)", 647 | "SearchBySeries", 648 | True, 649 | ) 650 | _gen_default_line( 651 | search_table, 652 | "Prioritize Today's releases (Similar effect as RSS Sync, where it searches " 653 | "today's release episodes first, only works on Sonarr).", 654 | "PrioritizeTodaysReleases", 655 | True, 656 | ) 657 | _gen_default_ombi_table(category, search_table) 658 | _gen_default_overseerr_table(category, search_table) 659 | cat_default.add("EntrySearch", search_table) 660 | 661 | 662 | def _gen_default_ombi_table(category: str, search_table: Table): 663 | ombi_table = table() 664 | _gen_default_line( 665 | ombi_table, 666 | "Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.)", 667 | "SearchOmbiRequests", 668 | False, 669 | ) 670 | _gen_default_line( 671 | ombi_table, 672 | "Ombi URI eg. http://ip:port (Note that this has to be the instance of Ombi which manage the Arr instance request (If you have multiple Ombi instances)", 673 | "OmbiURI", 674 | "CHANGE_ME", 675 | ) 676 | _gen_default_line(ombi_table, "Ombi's API Key", "OmbiAPIKey", "CHANGE_ME") 677 | _gen_default_line(ombi_table, "Only process approved requests", "ApprovedOnly", True) 678 | search_table.add("Ombi", ombi_table) 679 | 680 | 681 | def _gen_default_overseerr_table(category: str, search_table: Table): 682 | overseerr_table = table() 683 | _gen_default_line( 684 | overseerr_table, 685 | [ 686 | "Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.)", 687 | "If this and Ombi are both enable, Ombi will be ignored", 688 | ], 689 | "SearchOverseerrRequests", 690 | False, 691 | ) 692 | _gen_default_line( 693 | overseerr_table, "Overseerr's URI eg. http://ip:port", "OverseerrURI", "CHANGE_ME" 694 | ) 695 | _gen_default_line(overseerr_table, "Overseerr's API Key", "OverseerrAPIKey", "CHANGE_ME") 696 | _gen_default_line(overseerr_table, "Only process approved requests", "ApprovedOnly", True) 697 | overseerr_table.add(comment("Only for 4K Instances")) 698 | if "radarr-4k" in category.lower(): 699 | _gen_default_line(overseerr_table, "Only for 4K Instances", "Is4K", True) 700 | else: 701 | _gen_default_line(overseerr_table, "Only for 4K Instances", "Is4K", False) 702 | search_table.add("Overseerr", overseerr_table) 703 | 704 | 705 | class MyConfig: 706 | # Original code taken from https://github.com/SemenovAV/toml_config 707 | # Licence is MIT, can be located at 708 | # https://github.com/SemenovAV/toml_config/blob/master/LICENSE.txt 709 | 710 | path: pathlib.Path 711 | config: TOMLDocument 712 | defaults_config: TOMLDocument 713 | 714 | def __init__(self, path: pathlib.Path | str, config: TOMLDocument | None = None): 715 | self.path = pathlib.Path(path) 716 | self._giving_data = bool(config) 717 | self.config = config or document() 718 | self.defaults_config = generate_doc() 719 | self.err = None 720 | self.state = True 721 | self.load() 722 | 723 | def __str__(self): 724 | return self.config.as_string() 725 | 726 | def load(self) -> MyConfig: 727 | if self.state: 728 | try: 729 | if self._giving_data: 730 | return self 731 | with self.path.open() as file: 732 | self.config = parse(file.read()) 733 | return self 734 | except (OSError, TypeError) as err: 735 | self.state = False 736 | self.err = err 737 | return self 738 | 739 | def save(self) -> MyConfig: 740 | if self.state: 741 | try: 742 | with open(self.path, "w", encoding="utf8") as file: 743 | file.write(self.config.as_string()) 744 | return self 745 | except OSError as err: 746 | self._value_error( 747 | err, "Possible permissions while attempting to read the config file.\n" 748 | ) 749 | except TypeError as err: 750 | self._value_error(err, "While attempting to read the config file.\n") 751 | return self 752 | 753 | def _value_error(self, err, arg1): 754 | self.state = False 755 | self.err = err 756 | raise ValueError(f"{arg1}{err}") 757 | 758 | def get(self, section: str, fallback: Any = None) -> T: 759 | return self._deep_get(section, default=fallback) 760 | 761 | def get_or_raise(self, section: str) -> T: 762 | if (r := self._deep_get(section, default=KeyError)) is KeyError: 763 | raise KeyError(f"{section} does not exist") 764 | return r 765 | 766 | def sections(self): 767 | return self.config.keys() 768 | 769 | def _deep_get(self, keys, default=...): 770 | values = reduce( 771 | lambda d, key: d.get(key, ...) if isinstance(d, dict) else ..., 772 | keys.split("."), 773 | self.config, 774 | ) 775 | 776 | return values if values is not ... else default 777 | 778 | 779 | def _write_config_file(docker=False) -> pathlib.Path: 780 | doc = generate_doc() 781 | file_name = "config.rename_me.toml" if docker else "config.toml" 782 | config_file = HOME_PATH.joinpath(file_name) 783 | if config_file.exists() and not docker: 784 | print(f"{config_file} already exists, File is not being replaced.") 785 | config_file = pathlib.Path.cwd().joinpath("config_new.toml") 786 | config = MyConfig(config_file, config=doc) 787 | config.save() 788 | print(f'New config file has been saved to "{config_file}"') 789 | return config_file 790 | -------------------------------------------------------------------------------- /qBitrr/home_path.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from jaraco.docker import is_docker 4 | 5 | from qBitrr.env_config import ENVIRO_CONFIG 6 | 7 | if ( 8 | ENVIRO_CONFIG.overrides.data_path is None 9 | or not (p := pathlib.Path(ENVIRO_CONFIG.overrides.data_path)).exists() 10 | ): 11 | if is_docker(): 12 | ON_DOCKER = True 13 | HOME_PATH = pathlib.Path("/config") 14 | else: 15 | ON_DOCKER = False 16 | HOME_PATH = pathlib.Path().absolute().joinpath(".config") 17 | HOME_PATH.mkdir(parents=True, exist_ok=True) 18 | else: 19 | HOME_PATH = p 20 | 21 | APPDATA_FOLDER = HOME_PATH.joinpath("qBitManager") 22 | APPDATA_FOLDER.mkdir(parents=True, exist_ok=True) 23 | APPDATA_FOLDER.chmod(mode=0o777) 24 | -------------------------------------------------------------------------------- /qBitrr/logger.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import pathlib 5 | import time 6 | from logging import Logger 7 | 8 | import coloredlogs 9 | 10 | from qBitrr.config import ( 11 | AUTO_PAUSE_RESUME, 12 | COMPLETED_DOWNLOAD_FOLDER, 13 | CONFIG, 14 | CONSOLE_LOGGING_LEVEL_STRING, 15 | COPIED_TO_NEW_DIR, 16 | ENABLE_LOGS, 17 | FAILED_CATEGORY, 18 | FREE_SPACE, 19 | HOME_PATH, 20 | IGNORE_TORRENTS_YOUNGER_THAN, 21 | LOOP_SLEEP_TIMER, 22 | NO_INTERNET_SLEEP_TIMER, 23 | PING_URLS, 24 | RECHECK_CATEGORY, 25 | SEARCH_LOOP_DELAY, 26 | TAGLESS, 27 | ) 28 | 29 | __all__ = ("run_logs",) 30 | 31 | TRACE = 5 32 | VERBOSE = 7 33 | NOTICE = 23 34 | HNOTICE = 24 35 | SUCCESS = 25 36 | 37 | 38 | class VerboseLogger(Logger): 39 | def _init__(self, *args, **kwargs): 40 | super().__init__(*args, **kwargs) 41 | if self.name.startswith("qBitrr"): 42 | self.set_config_level() 43 | 44 | def success(self, message, *args, **kwargs): 45 | if self.isEnabledFor(SUCCESS): 46 | self._log(SUCCESS, message, args, **kwargs) 47 | 48 | def hnotice(self, message, *args, **kwargs): 49 | if self.isEnabledFor(HNOTICE): 50 | self._log(HNOTICE, message, args, **kwargs) 51 | 52 | def notice(self, message, *args, **kwargs): 53 | if self.isEnabledFor(NOTICE): 54 | self._log(NOTICE, message, args, **kwargs) 55 | 56 | def verbose(self, message, *args, **kwargs): 57 | if self.isEnabledFor(VERBOSE): 58 | self._log(VERBOSE, message, args, **kwargs) 59 | 60 | def trace(self, message, *args, **kwargs): 61 | if self.isEnabledFor(TRACE): 62 | self._log(TRACE, message, args, **kwargs) 63 | 64 | def set_config_level(self): 65 | self.setLevel(CONSOLE_LOGGING_LEVEL_STRING) 66 | 67 | 68 | logging.addLevelName(SUCCESS, "SUCCESS") 69 | logging.addLevelName(HNOTICE, "HNOTICE") 70 | logging.addLevelName(NOTICE, "NOTICE") 71 | logging.addLevelName(VERBOSE, "VERBOSE") 72 | logging.addLevelName(TRACE, "TRACE") 73 | logging.setLoggerClass(VerboseLogger) 74 | 75 | 76 | def getLogger(name: str | None = None): 77 | return VerboseLogger.manager.getLogger(name) if name else logging.root 78 | 79 | 80 | logging.getLogger = getLogger 81 | 82 | 83 | logger = logging.getLogger("qBitrr.Misc") 84 | 85 | 86 | HAS_RUN = False 87 | 88 | 89 | def run_logs(logger: Logger, _name: str = None) -> None: 90 | global HAS_RUN 91 | try: 92 | configkeys = {f"qBitrr.{i}" for i in CONFIG.sections()} 93 | key_length = max(len(max(configkeys, key=len)), 10) 94 | except BaseException: 95 | key_length = 10 96 | coloredlogs.install( 97 | logger=logger, 98 | level=logging._nameToLevel.get(CONSOLE_LOGGING_LEVEL_STRING), 99 | fmt="[%(asctime)-15s] [pid:%(process)8d][tid:%(thread)8d] " 100 | f"%(levelname)-8s: %(name)-{key_length}s: %(message)s", 101 | level_styles={ 102 | "trace": {"color": "black", "bold": True}, 103 | "debug": {"color": "magenta", "bold": True}, 104 | "verbose": {"color": "blue", "bold": True}, 105 | "info": {"color": "white"}, 106 | "notice": {"color": "cyan"}, 107 | "hnotice": {"color": "cyan", "bold": True}, 108 | "warning": {"color": "yellow", "bold": True}, 109 | "success": {"color": "green", "bold": True}, 110 | "error": {"color": "red"}, 111 | "critical": {"color": "red", "bold": True}, 112 | }, 113 | field_styles={ 114 | "asctime": {"color": "green"}, 115 | "process": {"color": "magenta"}, 116 | "levelname": {"color": "red", "bold": True}, 117 | "name": {"color": "blue", "bold": True}, 118 | "thread": {"color": "cyan"}, 119 | }, 120 | reconfigure=True, 121 | ) 122 | if ENABLE_LOGS and _name: 123 | logs_folder = HOME_PATH.joinpath("logs") 124 | logs_folder.mkdir(parents=True, exist_ok=True) 125 | logs_folder.chmod(mode=0o777) 126 | logfile = logs_folder.joinpath(_name + ".log") 127 | if pathlib.Path(logfile).is_file(): 128 | logold = logs_folder.joinpath(_name + ".log.old") 129 | if pathlib.Path(logold).exists(): 130 | logold.unlink() 131 | logfile.rename(logold) 132 | fh = logging.FileHandler(logfile) 133 | fh.setFormatter( 134 | logging.Formatter( 135 | fmt="[%(asctime)-15s] " f"%(levelname)-8s: %(name)-{key_length}s: %(message)s" 136 | ) 137 | ) 138 | logger.addHandler(fh) 139 | if HAS_RUN is False: 140 | HAS_RUN = True 141 | log_debugs(logger) 142 | 143 | 144 | def log_debugs(logger): 145 | logger.debug("Log Level: %s", CONSOLE_LOGGING_LEVEL_STRING) 146 | logger.debug("Ping URLs: %s", PING_URLS) 147 | logger.debug("Script Config: Logging=%s", ENABLE_LOGS) 148 | logger.debug("Script Config: FailedCategory=%s", FAILED_CATEGORY) 149 | logger.debug("Script Config: RecheckCategory=%s", RECHECK_CATEGORY) 150 | logger.debug("Script Config: Tagless=%s", TAGLESS) 151 | logger.debug("Script Config: CompletedDownloadFolder=%s", COMPLETED_DOWNLOAD_FOLDER) 152 | logger.debug("Script Config: FreeSpace=%s", FREE_SPACE) 153 | logger.debug("Script Config: LoopSleepTimer=%s", LOOP_SLEEP_TIMER) 154 | logger.debug("Script Config: SearchLoopDelay=%s", SEARCH_LOOP_DELAY) 155 | logger.debug("Script Config: AutoPauseResume=%s", AUTO_PAUSE_RESUME) 156 | logger.debug("Script Config: NoInternetSleepTimer=%s", NO_INTERNET_SLEEP_TIMER) 157 | logger.debug("Script Config: IgnoreTorrentsYoungerThan=%s", IGNORE_TORRENTS_YOUNGER_THAN) 158 | 159 | 160 | if COPIED_TO_NEW_DIR is False and not HOME_PATH.joinpath("config.toml").exists(): 161 | logger.warning( 162 | "Config.toml should exist in '%s', in a future update this will be a requirement.", 163 | HOME_PATH, 164 | ) 165 | time.sleep(5) 166 | if COPIED_TO_NEW_DIR: 167 | logger.warning("Config.toml new location is %s", HOME_PATH) 168 | time.sleep(5) 169 | run_logs(logger) 170 | -------------------------------------------------------------------------------- /qBitrr/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import atexit 4 | import itertools 5 | import logging 6 | import sys 7 | import time 8 | from multiprocessing import freeze_support 9 | 10 | import pathos 11 | import qbittorrentapi 12 | import requests 13 | from packaging import version as version_parser 14 | from packaging.version import Version as VersionClass 15 | from qbittorrentapi import APINames 16 | from qbittorrentapi.decorators import login_required # , response_text 17 | 18 | from qBitrr.arss import ArrManager 19 | from qBitrr.bundled_data import patched_version 20 | from qBitrr.config import ( 21 | APPDATA_FOLDER, 22 | CONFIG, 23 | CONFIG_EXISTS, 24 | QBIT_DISABLED, 25 | SEARCH_ONLY, 26 | process_flags, 27 | ) 28 | from qBitrr.env_config import ENVIRO_CONFIG 29 | from qBitrr.ffprobe import FFprobeDownloader 30 | from qBitrr.logger import run_logs 31 | from qBitrr.utils import ExpiringSet, absolute_file_paths 32 | 33 | if CONFIG_EXISTS: 34 | from qBitrr.arss import ArrManager 35 | else: 36 | sys.exit(0) 37 | 38 | CHILD_PROCESSES = [] 39 | 40 | logger = logging.getLogger("qBitrr") 41 | run_logs(logger, "Main") 42 | 43 | 44 | class qBitManager: 45 | min_supported_version = VersionClass("4.3.9") 46 | soft_not_supported_supported_version = VersionClass("4.4.4") 47 | max_supported_version = VersionClass("4.6.7") 48 | _head_less_mode = False 49 | 50 | def __init__(self): 51 | self._name = "Manager" 52 | self.qBit_Host = CONFIG.get("qBit.Host", fallback="localhost") 53 | self.qBit_Port = CONFIG.get("qBit.Port", fallback=8105) 54 | self.qBit_UserName = CONFIG.get("qBit.UserName", fallback=None) 55 | self.qBit_Password = CONFIG.get("qBit.Password", fallback=None) 56 | self.qBit_v5 = CONFIG.get("qBit.v5", fallback=False) 57 | self.logger = logging.getLogger(f"qBitrr.{self._name}") 58 | run_logs(self.logger, self._name) 59 | self.logger.debug( 60 | "qBitTorrent Config: Host: %s Port: %s, Username: %s, Password: %s", 61 | self.qBit_Host, 62 | self.qBit_Port, 63 | self.qBit_UserName, 64 | self.qBit_Password, 65 | ) 66 | self._validated_version = False 67 | self.client = None 68 | self.current_qbit_version = None 69 | if not any([QBIT_DISABLED, SEARCH_ONLY]): 70 | self.client = qbittorrentapi.Client( 71 | host=self.qBit_Host, 72 | port=self.qBit_Port, 73 | username=self.qBit_UserName, 74 | password=self.qBit_Password, 75 | SIMPLE_RESPONSES=False, 76 | ) 77 | try: 78 | self.current_qbit_version = version_parser.parse(self.client.app_version()) 79 | self._validated_version = True 80 | except BaseException: 81 | self.current_qbit_version = self.min_supported_version 82 | self.logger.error( 83 | "Could not establish qBitTorrent version, " 84 | "you may experience errors, please report this error." 85 | ) 86 | self._version_validator() 87 | self.expiring_bool = ExpiringSet(max_age_seconds=10) 88 | self.cache = {} 89 | self.name_cache = {} 90 | self.should_delay_torrent_scan = False # If true torrent scan is delayed by 5 minutes. 91 | self.child_processes = [] 92 | self.ffprobe_downloader = FFprobeDownloader() 93 | try: 94 | if not any([QBIT_DISABLED, SEARCH_ONLY]): 95 | self.ffprobe_downloader.update() 96 | except Exception as e: 97 | self.logger.error( 98 | "FFprobe manager error: %s while attempting to download/update FFprobe", e 99 | ) 100 | self.arr_manager = ArrManager(self).build_arr_instances() 101 | run_logs(self.logger) 102 | 103 | def _version_validator(self): 104 | validated = False 105 | if self.qBit_v5: 106 | if self.min_supported_version <= self.current_qbit_version: 107 | validated = True 108 | else: 109 | if ( 110 | self.min_supported_version 111 | <= self.current_qbit_version 112 | <= self.max_supported_version 113 | ): 114 | validated = True 115 | 116 | if self._validated_version and validated: 117 | self.logger.info( 118 | "Current qBitTorrent version is supported: %s", self.current_qbit_version 119 | ) 120 | elif not self._validated_version and validated: 121 | self.logger.warning( 122 | "Could not validate current qBitTorrent version, assuming: %s", 123 | self.current_qbit_version, 124 | ) 125 | time.sleep(10) 126 | else: 127 | self.logger.critical( 128 | "You are currently running qBitTorrent version %s, " 129 | "Supported version range is %s to < %s", 130 | self.current_qbit_version, 131 | self.min_supported_version, 132 | self.max_supported_version, 133 | ) 134 | sys.exit(1) 135 | 136 | # @response_text(str) 137 | @login_required 138 | def app_version(self, **kwargs): 139 | return self.client._get( 140 | _name=APINames.Application, 141 | _method="version", 142 | _retries=0, 143 | _retry_backoff_factor=0, 144 | **kwargs, 145 | ) 146 | 147 | @property 148 | def is_alive(self) -> bool: 149 | try: 150 | if 1 in self.expiring_bool or self.client is None: 151 | return True 152 | self.client.app_version() 153 | self.logger.trace("Successfully connected to %s:%s", self.qBit_Host, self.qBit_Port) 154 | self.expiring_bool.add(1) 155 | return True 156 | except requests.RequestException: 157 | self.logger.warning("Could not connect to %s:%s", self.qBit_Host, self.qBit_Port) 158 | self.should_delay_torrent_scan = True 159 | return False 160 | 161 | def get_child_processes(self) -> list[pathos.helpers.mp.Process]: 162 | run_logs(self.logger) 163 | self.logger.debug("Managing %s categories", len(self.arr_manager.managed_objects)) 164 | count = 0 165 | procs = [] 166 | for arr in self.arr_manager.managed_objects.values(): 167 | numb, processes = arr.spawn_child_processes() 168 | count += numb 169 | procs.extend(processes) 170 | return procs 171 | 172 | def run(self): 173 | try: 174 | self.logger.debug("Starting %s child processes", len(self.child_processes)) 175 | [p.start() for p in self.child_processes] 176 | [p.join() for p in self.child_processes] 177 | except KeyboardInterrupt: 178 | self.logger.info("Detected Ctrl+C - Terminating process") 179 | sys.exit(0) 180 | except BaseException as e: 181 | self.logger.info("Detected Ctrl+C - Terminating process: %r", e) 182 | sys.exit(1) 183 | 184 | 185 | def run(): 186 | global CHILD_PROCESSES 187 | early_exit = process_flags() 188 | if early_exit is True: 189 | sys.exit(0) 190 | logger.info("Starting qBitrr: Version: %s.", patched_version) 191 | try: 192 | manager = qBitManager() 193 | except NameError: 194 | sys.exit(0) 195 | run_logs(logger) 196 | logger.debug("Environment variables: %r", ENVIRO_CONFIG) 197 | try: 198 | if CHILD_PROCESSES := manager.get_child_processes(): 199 | manager.run() 200 | else: 201 | logger.warning( 202 | "No tasks to perform, if this is unintended double check your config file." 203 | ) 204 | except KeyboardInterrupt: 205 | logger.info("Detected Ctrl+C - Terminating process") 206 | sys.exit(0) 207 | except Exception: 208 | logger.info("Attempting to terminate child processes, please wait a moment.") 209 | for child in manager.child_processes: 210 | child.kill() 211 | 212 | 213 | def cleanup(): 214 | for p in CHILD_PROCESSES: 215 | p.kill() 216 | p.terminate() 217 | 218 | 219 | def file_cleanup(): 220 | extensions = [".db", ".db-shm", ".db-wal"] 221 | all_files_in_folder = list(absolute_file_paths(APPDATA_FOLDER)) 222 | for file, ext in itertools.product(all_files_in_folder, extensions): 223 | if file.name.endswith(ext): 224 | APPDATA_FOLDER.joinpath(file).unlink(missing_ok=True) 225 | 226 | 227 | atexit.register(cleanup) 228 | 229 | 230 | if __name__ == "__main__": 231 | freeze_support() 232 | file_cleanup() 233 | run() 234 | -------------------------------------------------------------------------------- /qBitrr/tables.py: -------------------------------------------------------------------------------- 1 | from peewee import BooleanField, CharField, DateTimeField, IntegerField, Model, TextField 2 | 3 | 4 | class FilesQueued(Model): 5 | EntryId = IntegerField(primary_key=True, null=False, unique=True) 6 | 7 | 8 | class MoviesFilesModel(Model): 9 | Title = CharField() 10 | Monitored = BooleanField() 11 | TmdbId = IntegerField() 12 | Year = IntegerField() 13 | EntryId = IntegerField(unique=True) 14 | Searched = BooleanField(default=False) 15 | MovieFileId = IntegerField() 16 | IsRequest = BooleanField(default=False) 17 | QualityMet = BooleanField(default=False) 18 | Upgrade = BooleanField(default=False) 19 | CustomFormatScore = IntegerField(null=True) 20 | MinCustomFormatScore = IntegerField(null=True) 21 | CustomFormatMet = BooleanField(default=False) 22 | Reason = TextField(null=True) 23 | 24 | 25 | class EpisodeFilesModel(Model): 26 | EntryId = IntegerField(primary_key=True) 27 | SeriesTitle = TextField(null=True) 28 | Title = TextField(null=True) 29 | SeriesId = IntegerField(null=False) 30 | EpisodeFileId = IntegerField(null=True) 31 | EpisodeNumber = IntegerField(null=False) 32 | SeasonNumber = IntegerField(null=False) 33 | AbsoluteEpisodeNumber = IntegerField(null=True) 34 | SceneAbsoluteEpisodeNumber = IntegerField(null=True) 35 | AirDateUtc = DateTimeField(formats=["%Y-%m-%d %H:%M:%S.%f"], null=True) 36 | Monitored = BooleanField(null=True) 37 | Searched = BooleanField(default=False) 38 | IsRequest = BooleanField(default=False) 39 | QualityMet = BooleanField(default=False) 40 | Upgrade = BooleanField(default=False) 41 | CustomFormatScore = IntegerField(null=True) 42 | MinCustomFormatScore = IntegerField(null=True) 43 | CustomFormatMet = BooleanField(default=False) 44 | Reason = TextField(null=True) 45 | 46 | 47 | class SeriesFilesModel(Model): 48 | EntryId = IntegerField(primary_key=True) 49 | Title = TextField(null=True) 50 | Monitored = BooleanField(null=True) 51 | Searched = BooleanField(default=False) 52 | Upgrade = BooleanField(default=False) 53 | MinCustomFormatScore = IntegerField(null=True) 54 | 55 | 56 | class MovieQueueModel(Model): 57 | EntryId = IntegerField(unique=True) 58 | Completed = BooleanField(default=False) 59 | 60 | 61 | class EpisodeQueueModel(Model): 62 | EntryId = IntegerField(unique=True) 63 | Completed = BooleanField(default=False) 64 | 65 | 66 | class TorrentLibrary(Model): 67 | Hash = TextField(null=False) 68 | Category = TextField(null=False) 69 | AllowedSeeding = BooleanField(default=False) 70 | Imported = BooleanField(default=False) 71 | AllowedStalled = BooleanField(default=False) 72 | FreeSpacePaused = BooleanField(default=False) 73 | -------------------------------------------------------------------------------- /qBitrr/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import pathlib 5 | import random 6 | import re 7 | import socket 8 | import time 9 | from typing import Iterator 10 | 11 | import ping3 12 | import qbittorrentapi 13 | from cachetools import TTLCache 14 | 15 | ping3.EXCEPTIONS = True 16 | 17 | logger = logging.getLogger("qBitrr.Utils") 18 | 19 | CACHE = TTLCache(maxsize=50, ttl=60) 20 | 21 | UNITS = {"k": 1024, "m": 1048576, "g": 1073741824, "t": 1099511627776} 22 | 23 | 24 | def absolute_file_paths(directory: pathlib.Path | str) -> Iterator[pathlib.Path]: 25 | file_counter = 0 26 | error = True 27 | while error: 28 | try: 29 | if file_counter == 50: 30 | error = False 31 | yield from pathlib.Path(directory).glob("**/*") 32 | error = False 33 | file_counter = 0 34 | except FileNotFoundError as e: 35 | file_counter += 1 36 | if file_counter == 1: 37 | logger.warning("%s - %s", e.strerror, e.filename) 38 | 39 | 40 | def validate_and_return_torrent_file(file: str) -> pathlib.Path: 41 | path = pathlib.Path(file) 42 | if path.is_file(): 43 | path = path.parent.absolute() 44 | count = 9 45 | while not path.exists(): 46 | logger.debug( 47 | "Attempt %s/10: File does not yet exists! (Possibly being moved?) | " 48 | "%s | Sleeping for 0.1s", 49 | path, 50 | 10 - count, 51 | ) 52 | time.sleep(0.1) 53 | if count == 0: 54 | break 55 | count -= 1 56 | else: 57 | count = 0 58 | while str(path) == ".": 59 | path = pathlib.Path(file) 60 | if path.is_file(): 61 | path = path.parent.absolute() 62 | while not path.exists(): 63 | logger.debug( 64 | "Attempt %s/10:File does not yet exists! (Possibly being moved?) | " 65 | "%s | Sleeping for 0.1s", 66 | path, 67 | 10 - count, 68 | ) 69 | time.sleep(0.1) 70 | if count == 0: 71 | break 72 | count -= 1 73 | else: 74 | count = 0 75 | if count == 0: 76 | break 77 | count -= 1 78 | return path 79 | 80 | 81 | def has_internet(client: qbittorrentapi.Client): 82 | from qBitrr.config import PING_URLS 83 | 84 | url = random.choice(PING_URLS) 85 | try: 86 | if not is_connected(url) and client.transfer_info()["connection_status"] == "disconnected": 87 | return False 88 | except: 89 | logger.error("Error getting qbittorrent transfer info %s", client.transfer_info()) 90 | logger.debug("Successfully connected to %s", url) 91 | return True 92 | 93 | 94 | def _basic_ping(hostname): 95 | host = "N/A" 96 | try: 97 | # if this hostname was called within the last 10 seconds skip it 98 | # if it was previous successful 99 | # Reducing the number of call to it and the likelihood of rate-limits. 100 | if hostname in CACHE: 101 | return CACHE[hostname] 102 | # see if we can resolve the host name -- tells us if there is 103 | # a DNS listening 104 | host = socket.gethostbyname(hostname) 105 | # connect to the host -- tells us if the host is actually 106 | # reachable 107 | s = socket.create_connection((host, 80), 5) 108 | s.close() 109 | CACHE[hostname] = True 110 | return True 111 | except Exception as e: 112 | logger.debug("Error when connecting to host: %s %s %s", hostname, host, e) 113 | return False 114 | 115 | 116 | def is_connected(hostname): 117 | try: 118 | # if this hostname was called within the last 10 seconds skip it 119 | # if it was previous successful 120 | # Reducing the number of call to it and the likelihood of rate-limits. 121 | if hostname in CACHE: 122 | return CACHE[hostname] 123 | ping3.ping(hostname, timeout=5) 124 | CACHE[hostname] = True 125 | return True 126 | except ping3.errors.PingError as e: # All ping3 errors are subclasses of `PingError`. 127 | logger.debug("Error when connecting to host: %s %s", hostname, e) 128 | except ( 129 | Exception 130 | ): # Ping3 is far more robust but may requite root access, if root access is not available then run the basic mode 131 | return _basic_ping(hostname) 132 | 133 | 134 | def parse_size(size): 135 | m = re.match(r"^([0-9]+(?:\.[0-9]+)?)([kmgt]?)$", size, re.IGNORECASE) 136 | if not m: 137 | raise ValueError("Unsupported value for leave_free_space") 138 | val = float(m.group(1)) 139 | unit = m.group(2) 140 | if unit: 141 | val *= UNITS[unit.lower()] 142 | return val 143 | 144 | 145 | class ExpiringSet: 146 | def __init__(self, *args: list, **kwargs): 147 | max_age_seconds = kwargs.get("max_age_seconds", 0) 148 | assert max_age_seconds > 0 149 | self.age = max_age_seconds 150 | self.container = {} 151 | for arg in args: 152 | self.add(arg) 153 | 154 | def __repr__(self): 155 | self.__update__() 156 | return f"{self.__class__.__name__}({', '.join(self.container.keys())})" 157 | 158 | def extend(self, args): 159 | """Add several items at once.""" 160 | for arg in args: 161 | self.add(arg) 162 | 163 | def add(self, value): 164 | self.container[value] = time.time() 165 | 166 | def remove(self, item): 167 | del self.container[item] 168 | 169 | def contains(self, value): 170 | if value not in self.container: 171 | return False 172 | if time.time() - self.container[value] > self.age: 173 | del self.container[value] 174 | return False 175 | return True 176 | 177 | __contains__ = contains 178 | 179 | def __getitem__(self, index): 180 | self.__update__() 181 | return list(self.container.keys())[index] 182 | 183 | def __iter__(self): 184 | self.__update__() 185 | return iter(self.container.copy()) 186 | 187 | def __len__(self): 188 | self.__update__() 189 | return len(self.container) 190 | 191 | def __copy__(self): 192 | self.__update__() 193 | temp = ExpiringSet(max_age_seconds=self.age) 194 | temp.container = self.container.copy() 195 | return temp 196 | 197 | def __update__(self): 198 | for k, b in self.container.copy().items(): 199 | if time.time() - b > self.age: 200 | del self.container[k] 201 | return False 202 | 203 | def __hash__(self): 204 | return hash(*(self.container.keys())) 205 | 206 | def __eq__(self, other): 207 | return self.__hash__() == other.__hash__() 208 | -------------------------------------------------------------------------------- /qBitrr2.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.4 2 | Name: qBitrr2 3 | Version: 4.10.20 4 | Summary: "A simple Python script to talk to qBittorrent and Arr's" 5 | Home-page: https://github.com/Feramance/qBitrr 6 | Author: Feramance 7 | Author-email: fera@fera.wtf 8 | License: MIT 9 | Project-URL: Issue Tracker, https://github.com/Feramance/qBitrr/issues 10 | Project-URL: Source Code, https://github.com/Feramance/qBitrr 11 | Classifier: Development Status :: 5 - Production/Stable 12 | Classifier: Intended Audience :: Developers 13 | Classifier: Intended Audience :: End Users/Desktop 14 | Classifier: License :: OSI Approved :: MIT License 15 | Classifier: Natural Language :: English 16 | Classifier: Operating System :: MacOS :: MacOS X 17 | Classifier: Operating System :: Microsoft :: Windows 18 | Classifier: Operating System :: POSIX :: Linux 19 | Classifier: Programming Language :: Python :: 3 :: Only 20 | Classifier: Programming Language :: Python :: 3.8 21 | Classifier: Programming Language :: Python :: 3.9 22 | Classifier: Programming Language :: Python :: 3.10 23 | Classifier: Programming Language :: Python :: Implementation :: CPython 24 | Classifier: Programming Language :: Python :: Implementation :: PyPy 25 | Classifier: Topic :: Terminals 26 | Classifier: Topic :: Utilities 27 | Classifier: Typing :: Typed 28 | Requires-Python: <4,>=3.8.3 29 | Description-Content-Type: text/markdown 30 | License-File: LICENSE 31 | Requires-Dist: cachetools==5.3.2 32 | Requires-Dist: colorama==0.4.4 33 | Requires-Dist: coloredlogs==15.0.1 34 | Requires-Dist: environ-config==23.2.0 35 | Requires-Dist: ffmpeg-python==0.2.0 36 | Requires-Dist: jaraco.docker==2.0 37 | Requires-Dist: packaging==22.0 38 | Requires-Dist: pathos==0.2.8 39 | Requires-Dist: peewee==3.14.7 40 | Requires-Dist: ping3==3.0.2 41 | Requires-Dist: pyarr==5.1.2 42 | Requires-Dist: qbittorrent-api==2023.7.52 43 | Requires-Dist: requests==2.32.0 44 | Requires-Dist: tomlkit==0.7.2 45 | Provides-Extra: dev 46 | Requires-Dist: black==24.3.0; extra == "dev" 47 | Requires-Dist: bump2version==1.0.1; extra == "dev" 48 | Requires-Dist: isort==5.10.1; extra == "dev" 49 | Requires-Dist: pip-tools==7.3.0; extra == "dev" 50 | Requires-Dist: pre-commit==3.3.3; extra == "dev" 51 | Requires-Dist: pyinstaller==5.13.1; extra == "dev" 52 | Requires-Dist: pyupgrade==2.31.0; extra == "dev" 53 | Requires-Dist: twine==3.7.1; extra == "dev" 54 | Requires-Dist: ujson==5.10.0; extra == "dev" 55 | Requires-Dist: upgrade-pip==0.1.4; extra == "dev" 56 | Provides-Extra: fast 57 | Requires-Dist: ujson==5.10.0; extra == "fast" 58 | Provides-Extra: all 59 | Requires-Dist: black==24.3.0; extra == "all" 60 | Requires-Dist: bump2version==1.0.1; extra == "all" 61 | Requires-Dist: isort==5.10.1; extra == "all" 62 | Requires-Dist: pip-tools==7.3.0; extra == "all" 63 | Requires-Dist: pre-commit==3.3.3; extra == "all" 64 | Requires-Dist: pyinstaller==5.13.1; extra == "all" 65 | Requires-Dist: pyupgrade==2.31.0; extra == "all" 66 | Requires-Dist: twine==3.7.1; extra == "all" 67 | Requires-Dist: ujson==5.10.0; extra == "all" 68 | Requires-Dist: upgrade-pip==0.1.4; extra == "all" 69 | Requires-Dist: ujson==5.10.0; extra == "all" 70 | Dynamic: license-file 71 | 72 | # qBitrr 73 | 74 | [![PyPI - License](https://img.shields.io/pypi/l/qbitrr)](https://github.com/Feramance/Qbitrr/blob/master/LICENSE) 75 | [![PyPI](https://img.shields.io/pypi/v/qBitrr2?label=PyPI)](https://pypi.org/project/qBitrr2/) 76 | [![Downloads](https://img.shields.io/pypi/dm/qbitrr2)](https://pypi.org/project/qBitrr2/) 77 | [![Pulls](https://img.shields.io/docker/pulls/feramance/qbitrr.svg)](https://hub.docker.com/r/feramance/qbitrr) 78 | 79 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qbitrr) 80 | [![Platforms](https://img.shields.io/badge/platform-linux--64%20%7C%20osx--64%20%7C%20win--32%20%7C%20win--64-lightgrey)](https://github.com/Feramance/qBitrr/releases/latest) 81 | 82 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Feramance/qBitrr/master.svg)](https://results.pre-commit.ci/latest/github/Feramance/qBitrr/master) 83 | [![CodeQL](https://github.com/Feramance/qBitrr/actions/workflows/codeql.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/codeql.yml) 84 | [![Create a Release](https://github.com/Feramance/qBitrr/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/release.yml) 85 | [![Nightly Build](https://github.com/Feramance/qBitrr/actions/workflows/nightly.yml/badge.svg?branch=master)](https://github.com/Feramance/qBitrr/actions/workflows/nightly.yml) 86 | 87 | [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 88 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 89 | 90 | A simple script to monitor [qBit](https://github.com/qbittorrent/qBittorrent) and communicate with [Radarr](https://github.com/Radarr/Radarr) and [Sonarr](https://github.com/Sonarr/Sonarr) 91 | 92 | # POLL 93 | 94 | [Request searches?](https://github.com/Feramance/qBitrr/discussions/149) 95 | 96 | ## Notice (slowly getting there, will take some time) 97 | 98 | I am starting development on qBitrr+ which will be C# based for better overall performance and will also include a WebUI for better refined control on setting and what to search/upgrade etc. Hoping this will be the be all and end all application to manage your Radarr/Sonarr, Overseerr/Ombi and qBittorrent instances in one UI. This is still in it's very early stages and will likely be a couple months before a concrete alpha is rolled out (from start of February 2024). Once I have something solid I will remove this notice and add a link to the new qBitrr+, in the meantime I will be sharing periodic updates on my [Patreon](https://patreon.com/qBitrr) 99 | 100 | ## Features 101 | 102 | - Monitor qBit for Stalled/bad entries and delete them then blacklist them on Arrs (Option to also trigger a re-search action). 103 | - Monitor qBit for completed entries and tell the appropriate Arr instance to import it: 104 | - `qbitrr DownloadedMoviesScan` for Radarr 105 | - `qbitrr DownloadedEpisodesScan` for Sonarr 106 | - Skip files in qBit entries by extension, folder or regex. 107 | - Monitor completed folder and clean it up. 108 | - Usage of [ffprobe](https://github.com/FFmpeg/FFmpeg) to ensure downloaded entries are valid media. 109 | - Trigger periodic Rss Syncs on the appropriate Arr instances. 110 | - Trigger Queue update on appropriate Arr instances. 111 | - Search requests from [Overseerr](https://github.com/sct/overseerr) or [Ombi](https://github.com/Ombi-app/Ombi). 112 | - Auto add/remove trackers 113 | - Set per tracker values 114 | - **Sonarr v4 support** 115 | - **Radarr v4 and v5 support** 116 | - Monitor Arr's to trigger missing episode searches. 117 | - Searches Radarr missing movies based on Minimum Availability 118 | - Customizable searching by series or singular episodes 119 | - Optionally searches year by year is ascending or descending order (config option available) 120 | - Search for CF Score unmet and cancel torrents base on CF Score or Quality unmet search 121 | - Set minimum free space in download directory and pause torrent downloads accordingly 122 | - Change quality profile temporarily for missing items until found 123 | 124 | ## Tested with 125 | 126 | Some things to know before using it. 127 | 128 | - **Latest supported qbittorrent version is 4.6.7** 129 | - qbittorrent v5 is supported via a config value (this will be removed later on) 130 | - qbittorrent >= 4.5.x 131 | - [Sonarr](https://github.com/Sonarr/Sonarr) and [Radarr](https://github.com/Radarr/Radarr) both setup to add tags to all downloads. 132 | - qBit set to create sub-folders for tag. 133 | 134 | ## Usage 135 | 136 | ### Native 137 | 138 | - `python -m pip install qBitrr2` (I would recommend in a dedicated [venv](https://docs.python.org/3.3/library/venv.html) but that's out of scope) 139 | 140 | Alternatively: 141 | 142 | - Download the [latest release](https://github.com/Feramance/Qbitrr/releases/latest) 143 | 144 | #### Run the script 145 | 146 | 1. Activate your venv 147 | 2. Run `qBitrr2` to generate a config file 148 | 3. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 149 | 4. Run `qBitrr2` if installed through pip again to start the script 150 | 151 | Alternatively: 152 | 153 | 1. Unzip the downloaded release and run it 154 | 2. Run `qBitrr` to generate a config file 155 | 3. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 156 | 4. Run `qBitrr` if installed through pip again to start the script 157 | 158 | #### How to update the script 159 | 160 | 1. Activate your venv 161 | 2. Run `python -m pip install -U qBitrr2` 162 | 163 | Alternatively: 164 | 165 | 1. Download on the [latest release](https://github.com/Feramance/Qbitrr/releases/latest) 166 | 2. Unzip the downloaded release and run it 167 | 3. Run `qBitrr` to generate a config file 168 | 4. Edit the config file (located at `~/config/config.toml` (~ is your current directory) 169 | 5. Run `qBitrr` if installed through pip again to start the script 170 | 171 | ***There is no auto-update feature, you will need to manually download the latest release and replace the old one.*** 172 | 173 | ### Docker 174 | 175 | #### Docker Image 176 | 177 | - The docker image can be found on [DockerHub](https://hub.docker.com/r/feramance/qbitrr) or [Github](https://github.com/Feramance/qBitrr/pkgs/container/qbitrr) 178 | 179 | #### Docker Run 180 | 181 | ```bash 182 | docker run -d \ 183 | --name=qbitrr \ 184 | -e TZ=Europe/London \ 185 | -v /etc/localtime:/etc/localtime:ro \ 186 | -v /path/to/appdata/qbitrr:/config \ 187 | -v /path/to/completed/downloads/folder:/completed_downloads:rw \ 188 | --restart unless-stopped \ 189 | feramance/qbitrr:latest 190 | ``` 191 | 192 | #### Docker Compose 193 | 194 | ```yaml 195 | version: "3" 196 | services: 197 | qbitrr: 198 | image: feramance/qbitrr:latest 199 | user: 1000:1000 # Required to ensure the container is run as the user who has perms to see the 2 mount points and the ability to write to the CompletedDownloadFolder mount 200 | tty: true # Ensure the output of docker-compose logs qBitrr are properly colored. 201 | restart: unless-stopped 202 | # networks: This container MUST share a network with your Sonarr/Radarr instances 203 | environment: 204 | - TZ=Europe/London 205 | volumes: 206 | - /etc/localtime:/etc/localtime:ro 207 | - /path/to/appdata/qbitrr:/config # Config folder for qBitrr 208 | - /path/to/completed/downloads/folder:/completed_downloads:rw # The script will ALWAYS require write permission in this folder if mounted, this folder is used to monitor completed downloads and if not present will cause the script to ignore downloaded file monitoring. 209 | # Now just to make sure it is clean, when using this script in a docker you will need to ensure you config.toml values reflect the mounted folders. 210 | # The same would apply to Settings.CompletedDownloadFolder 211 | # e.g CompletedDownloadFolder = /completed_downloads/folder/in/container 212 | 213 | logging: # this script will generate a LOT of logs - so it is up to you to decide how much of it you want to store 214 | driver: "json-file" 215 | options: 216 | max-size: "50m" 217 | max-file: 3 218 | depends_on: # Not needed but this ensures qBitrr only starts if the dependencies are up and running 219 | - qbittorrent 220 | - radarr-1080p 221 | - radarr-4k 222 | - sonarr-1080p 223 | - sonarr-anime 224 | - overseerr 225 | - ombi 226 | ``` 227 | 228 | ##### Important mentions for docker 229 | 230 | - The script will always expect a completed config.toml file 231 | - When you first start the container a "config.rename_me.toml" will be added to `/path/to/appdata/qbitrr` 232 | - Make sure to rename it to 'config.toml' then edit it to your desired values 233 | 234 | ## Feature Suggestions 235 | 236 | Please do not hesitate to open an issue for feature requests or any suggestions you may have. I plan on periodically adding any features I might feel I want to add but welcome to other suggestions I might not have thought of yet. 237 | 238 | ## Reporting an Issue 239 | 240 | When reporting an issue, please ensure that log files are enabled while running qBitrr and attach them to the issue. Thank you. 241 | -------------------------------------------------------------------------------- /qBitrr2.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README.md 3 | pyproject.toml 4 | setup.cfg 5 | setup.py 6 | qBitrr/__init__.py 7 | qBitrr/arss.py 8 | qBitrr/bundled_data.py 9 | qBitrr/config.py 10 | qBitrr/env_config.py 11 | qBitrr/errors.py 12 | qBitrr/ffprobe.py 13 | qBitrr/gen_config.py 14 | qBitrr/home_path.py 15 | qBitrr/logger.py 16 | qBitrr/main.py 17 | qBitrr/tables.py 18 | qBitrr/utils.py 19 | qBitrr2.egg-info/PKG-INFO 20 | qBitrr2.egg-info/SOURCES.txt 21 | qBitrr2.egg-info/dependency_links.txt 22 | qBitrr2.egg-info/entry_points.txt 23 | qBitrr2.egg-info/requires.txt 24 | qBitrr2.egg-info/top_level.txt 25 | -------------------------------------------------------------------------------- /qBitrr2.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Feramance/qBitrr/c4cc9b3ebe8e6ecda56bca944ab0b8041a5420fa/qBitrr2.egg-info/dependency_links.txt -------------------------------------------------------------------------------- /qBitrr2.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [console_scripts] 2 | qbitrr = qBitrr.main:run 3 | -------------------------------------------------------------------------------- /qBitrr2.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | cachetools==5.3.2 2 | colorama==0.4.4 3 | coloredlogs==15.0.1 4 | environ-config==23.2.0 5 | ffmpeg-python==0.2.0 6 | jaraco.docker==2.0 7 | packaging==22.0 8 | pathos==0.2.8 9 | peewee==3.14.7 10 | ping3==3.0.2 11 | pyarr==5.1.2 12 | qbittorrent-api==2023.7.52 13 | requests==2.32.0 14 | tomlkit==0.7.2 15 | 16 | [all] 17 | black==24.3.0 18 | bump2version==1.0.1 19 | isort==5.10.1 20 | pip-tools==7.3.0 21 | pre-commit==3.3.3 22 | pyinstaller==5.13.1 23 | pyupgrade==2.31.0 24 | twine==3.7.1 25 | ujson==5.10.0 26 | upgrade-pip==0.1.4 27 | 28 | [dev] 29 | black==24.3.0 30 | bump2version==1.0.1 31 | isort==5.10.1 32 | pip-tools==7.3.0 33 | pre-commit==3.3.3 34 | pyinstaller==5.13.1 35 | pyupgrade==2.31.0 36 | twine==3.7.1 37 | ujson==5.10.0 38 | upgrade-pip==0.1.4 39 | 40 | [fast] 41 | ujson==5.10.0 42 | -------------------------------------------------------------------------------- /qBitrr2.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | qBitrr 2 | -------------------------------------------------------------------------------- /requirements.all.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --extra=all --output-file=requirements.all.txt --strip-extras 6 | # 7 | altgraph==0.17.4 8 | # via pyinstaller 9 | attrs==25.3.0 10 | # via environ-config 11 | backports-tarfile==1.2.0 12 | # via jaraco-context 13 | black==24.3.0 14 | # via qBitrr2 (setup.py) 15 | build==1.2.2.post1 16 | # via pip-tools 17 | bump2version==1.0.1 18 | # via qBitrr2 (setup.py) 19 | cachetools==5.3.2 20 | # via qBitrr2 (setup.py) 21 | certifi==2025.1.31 22 | # via requests 23 | cfgv==3.4.0 24 | # via pre-commit 25 | charset-normalizer==3.4.1 26 | # via requests 27 | click==8.1.8 28 | # via 29 | # black 30 | # pip-tools 31 | colorama==0.4.4 32 | # via 33 | # build 34 | # click 35 | # qBitrr2 (setup.py) 36 | # tqdm 37 | # twine 38 | coloredlogs==15.0.1 39 | # via qBitrr2 (setup.py) 40 | dill==0.3.9 41 | # via 42 | # multiprocess 43 | # pathos 44 | distlib==0.3.9 45 | # via virtualenv 46 | docutils==0.21.2 47 | # via readme-renderer 48 | environ-config==23.2.0 49 | # via qBitrr2 (setup.py) 50 | ffmpeg-python==0.2.0 51 | # via qBitrr2 (setup.py) 52 | filelock==3.18.0 53 | # via virtualenv 54 | future==1.0.0 55 | # via ffmpeg-python 56 | humanfriendly==10.0 57 | # via coloredlogs 58 | identify==2.6.9 59 | # via pre-commit 60 | idna==3.10 61 | # via requests 62 | importlib-metadata==8.6.1 63 | # via 64 | # keyring 65 | # twine 66 | isort==5.10.1 67 | # via qBitrr2 (setup.py) 68 | jaraco-classes==3.4.0 69 | # via keyring 70 | jaraco-context==6.0.1 71 | # via 72 | # jaraco-docker 73 | # keyring 74 | jaraco-docker==2.0 75 | # via qBitrr2 (setup.py) 76 | jaraco-functools==4.1.0 77 | # via 78 | # jaraco-docker 79 | # keyring 80 | keyring==25.6.0 81 | # via twine 82 | more-itertools==10.6.0 83 | # via 84 | # jaraco-classes 85 | # jaraco-functools 86 | multiprocess==0.70.17 87 | # via pathos 88 | mypy-extensions==1.0.0 89 | # via black 90 | nh3==0.2.21 91 | # via readme-renderer 92 | nodeenv==1.9.1 93 | # via pre-commit 94 | overrides==7.7.0 95 | # via pyarr 96 | packaging==22.0 97 | # via 98 | # black 99 | # build 100 | # pyinstaller-hooks-contrib 101 | # qBitrr2 (setup.py) 102 | # qbittorrent-api 103 | pathos==0.2.8 104 | # via qBitrr2 (setup.py) 105 | pathspec==0.12.1 106 | # via black 107 | peewee==3.14.7 108 | # via qBitrr2 (setup.py) 109 | pefile==2024.8.26 110 | # via pyinstaller 111 | ping3==3.0.2 112 | # via qBitrr2 (setup.py) 113 | pip-tools==7.3.0 114 | # via qBitrr2 (setup.py) 115 | pkginfo==1.12.1.2 116 | # via twine 117 | platformdirs==4.3.7 118 | # via 119 | # black 120 | # virtualenv 121 | pox==0.3.5 122 | # via pathos 123 | ppft==1.7.6.9 124 | # via pathos 125 | pre-commit==3.3.3 126 | # via qBitrr2 (setup.py) 127 | pyarr==5.1.2 128 | # via qBitrr2 (setup.py) 129 | pygments==2.19.1 130 | # via readme-renderer 131 | pyinstaller==5.13.1 132 | # via qBitrr2 (setup.py) 133 | pyinstaller-hooks-contrib==2025.2 134 | # via pyinstaller 135 | pyproject-hooks==1.2.0 136 | # via build 137 | pyreadline3==3.5.4 138 | # via humanfriendly 139 | pyupgrade==2.31.0 140 | # via qBitrr2 (setup.py) 141 | pywin32-ctypes==0.2.3 142 | # via 143 | # keyring 144 | # pyinstaller 145 | pyyaml==6.0.2 146 | # via pre-commit 147 | qbittorrent-api==2023.7.52 148 | # via qBitrr2 (setup.py) 149 | readme-renderer==44.0 150 | # via twine 151 | requests==2.32.0 152 | # via 153 | # pyarr 154 | # qBitrr2 (setup.py) 155 | # qbittorrent-api 156 | # requests-toolbelt 157 | # twine 158 | requests-toolbelt==1.0.0 159 | # via twine 160 | rfc3986==2.0.0 161 | # via twine 162 | six==1.17.0 163 | # via qbittorrent-api 164 | tokenize-rt==6.1.0 165 | # via pyupgrade 166 | tomlkit==0.7.2 167 | # via qBitrr2 (setup.py) 168 | tqdm==4.67.1 169 | # via twine 170 | twine==3.7.1 171 | # via qBitrr2 (setup.py) 172 | types-requests==2.32.0.20250328 173 | # via pyarr 174 | ujson==5.10.0 175 | # via qBitrr2 (setup.py) 176 | upgrade-pip==0.1.4 177 | # via qBitrr2 (setup.py) 178 | urllib3==2.3.0 179 | # via 180 | # qbittorrent-api 181 | # requests 182 | # types-requests 183 | virtualenv==20.30.0 184 | # via pre-commit 185 | wheel==0.45.1 186 | # via pip-tools 187 | zipp==3.21.0 188 | # via importlib-metadata 189 | 190 | # The following packages are considered to be unsafe in a requirements file: 191 | # pip 192 | # setuptools 193 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --extra=dev --output-file=requirements.dev.txt --strip-extras 6 | # 7 | altgraph==0.17.4 8 | # via pyinstaller 9 | attrs==25.3.0 10 | # via environ-config 11 | backports-tarfile==1.2.0 12 | # via jaraco-context 13 | black==24.3.0 14 | # via qBitrr2 (setup.py) 15 | build==1.2.2.post1 16 | # via pip-tools 17 | bump2version==1.0.1 18 | # via qBitrr2 (setup.py) 19 | cachetools==5.3.2 20 | # via qBitrr2 (setup.py) 21 | certifi==2025.1.31 22 | # via requests 23 | cfgv==3.4.0 24 | # via pre-commit 25 | charset-normalizer==3.4.1 26 | # via requests 27 | click==8.1.8 28 | # via 29 | # black 30 | # pip-tools 31 | colorama==0.4.4 32 | # via 33 | # build 34 | # click 35 | # qBitrr2 (setup.py) 36 | # tqdm 37 | # twine 38 | coloredlogs==15.0.1 39 | # via qBitrr2 (setup.py) 40 | dill==0.3.9 41 | # via 42 | # multiprocess 43 | # pathos 44 | distlib==0.3.9 45 | # via virtualenv 46 | docutils==0.21.2 47 | # via readme-renderer 48 | environ-config==23.2.0 49 | # via qBitrr2 (setup.py) 50 | ffmpeg-python==0.2.0 51 | # via qBitrr2 (setup.py) 52 | filelock==3.18.0 53 | # via virtualenv 54 | future==1.0.0 55 | # via ffmpeg-python 56 | humanfriendly==10.0 57 | # via coloredlogs 58 | identify==2.6.9 59 | # via pre-commit 60 | idna==3.10 61 | # via requests 62 | importlib-metadata==8.6.1 63 | # via 64 | # keyring 65 | # twine 66 | isort==5.10.1 67 | # via qBitrr2 (setup.py) 68 | jaraco-classes==3.4.0 69 | # via keyring 70 | jaraco-context==6.0.1 71 | # via 72 | # jaraco-docker 73 | # keyring 74 | jaraco-docker==2.0 75 | # via qBitrr2 (setup.py) 76 | jaraco-functools==4.1.0 77 | # via 78 | # jaraco-docker 79 | # keyring 80 | keyring==25.6.0 81 | # via twine 82 | more-itertools==10.6.0 83 | # via 84 | # jaraco-classes 85 | # jaraco-functools 86 | multiprocess==0.70.17 87 | # via pathos 88 | mypy-extensions==1.0.0 89 | # via black 90 | nh3==0.2.21 91 | # via readme-renderer 92 | nodeenv==1.9.1 93 | # via pre-commit 94 | overrides==7.7.0 95 | # via pyarr 96 | packaging==22.0 97 | # via 98 | # black 99 | # build 100 | # pyinstaller-hooks-contrib 101 | # qBitrr2 (setup.py) 102 | # qbittorrent-api 103 | pathos==0.2.8 104 | # via qBitrr2 (setup.py) 105 | pathspec==0.12.1 106 | # via black 107 | peewee==3.14.7 108 | # via qBitrr2 (setup.py) 109 | pefile==2024.8.26 110 | # via pyinstaller 111 | ping3==3.0.2 112 | # via qBitrr2 (setup.py) 113 | pip-tools==7.3.0 114 | # via qBitrr2 (setup.py) 115 | pkginfo==1.12.1.2 116 | # via twine 117 | platformdirs==4.3.7 118 | # via 119 | # black 120 | # virtualenv 121 | pox==0.3.5 122 | # via pathos 123 | ppft==1.7.6.9 124 | # via pathos 125 | pre-commit==3.3.3 126 | # via qBitrr2 (setup.py) 127 | pyarr==5.1.2 128 | # via qBitrr2 (setup.py) 129 | pygments==2.19.1 130 | # via readme-renderer 131 | pyinstaller==5.13.1 132 | # via qBitrr2 (setup.py) 133 | pyinstaller-hooks-contrib==2025.2 134 | # via pyinstaller 135 | pyproject-hooks==1.2.0 136 | # via build 137 | pyreadline3==3.5.4 138 | # via humanfriendly 139 | pyupgrade==2.31.0 140 | # via qBitrr2 (setup.py) 141 | pywin32-ctypes==0.2.3 142 | # via 143 | # keyring 144 | # pyinstaller 145 | pyyaml==6.0.2 146 | # via pre-commit 147 | qbittorrent-api==2023.7.52 148 | # via qBitrr2 (setup.py) 149 | readme-renderer==44.0 150 | # via twine 151 | requests==2.32.0 152 | # via 153 | # pyarr 154 | # qBitrr2 (setup.py) 155 | # qbittorrent-api 156 | # requests-toolbelt 157 | # twine 158 | requests-toolbelt==1.0.0 159 | # via twine 160 | rfc3986==2.0.0 161 | # via twine 162 | six==1.17.0 163 | # via qbittorrent-api 164 | tokenize-rt==6.1.0 165 | # via pyupgrade 166 | tomlkit==0.7.2 167 | # via qBitrr2 (setup.py) 168 | tqdm==4.67.1 169 | # via twine 170 | twine==3.7.1 171 | # via qBitrr2 (setup.py) 172 | types-requests==2.32.0.20250328 173 | # via pyarr 174 | ujson==5.10.0 175 | # via qBitrr2 (setup.py) 176 | upgrade-pip==0.1.4 177 | # via qBitrr2 (setup.py) 178 | urllib3==2.3.0 179 | # via 180 | # qbittorrent-api 181 | # requests 182 | # types-requests 183 | virtualenv==20.30.0 184 | # via pre-commit 185 | wheel==0.45.1 186 | # via pip-tools 187 | zipp==3.21.0 188 | # via importlib-metadata 189 | 190 | # The following packages are considered to be unsafe in a requirements file: 191 | # pip 192 | # setuptools 193 | -------------------------------------------------------------------------------- /requirements.fast.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --extra=fast --output-file=requirements.fast.txt --strip-extras 6 | # 7 | attrs==25.3.0 8 | # via environ-config 9 | backports-tarfile==1.2.0 10 | # via jaraco-context 11 | cachetools==5.3.2 12 | # via qBitrr2 (setup.py) 13 | certifi==2025.1.31 14 | # via requests 15 | charset-normalizer==3.4.1 16 | # via requests 17 | colorama==0.4.4 18 | # via qBitrr2 (setup.py) 19 | coloredlogs==15.0.1 20 | # via qBitrr2 (setup.py) 21 | dill==0.3.9 22 | # via 23 | # multiprocess 24 | # pathos 25 | environ-config==23.2.0 26 | # via qBitrr2 (setup.py) 27 | ffmpeg-python==0.2.0 28 | # via qBitrr2 (setup.py) 29 | future==1.0.0 30 | # via ffmpeg-python 31 | humanfriendly==10.0 32 | # via coloredlogs 33 | idna==3.10 34 | # via requests 35 | jaraco-context==6.0.1 36 | # via jaraco-docker 37 | jaraco-docker==2.0 38 | # via qBitrr2 (setup.py) 39 | jaraco-functools==4.1.0 40 | # via jaraco-docker 41 | more-itertools==10.6.0 42 | # via jaraco-functools 43 | multiprocess==0.70.17 44 | # via pathos 45 | overrides==7.7.0 46 | # via pyarr 47 | packaging==22.0 48 | # via 49 | # qBitrr2 (setup.py) 50 | # qbittorrent-api 51 | pathos==0.2.8 52 | # via qBitrr2 (setup.py) 53 | peewee==3.14.7 54 | # via qBitrr2 (setup.py) 55 | ping3==3.0.2 56 | # via qBitrr2 (setup.py) 57 | pox==0.3.5 58 | # via pathos 59 | ppft==1.7.6.9 60 | # via pathos 61 | pyarr==5.1.2 62 | # via qBitrr2 (setup.py) 63 | pyreadline3==3.5.4 64 | # via humanfriendly 65 | qbittorrent-api==2023.7.52 66 | # via qBitrr2 (setup.py) 67 | requests==2.32.0 68 | # via 69 | # pyarr 70 | # qBitrr2 (setup.py) 71 | # qbittorrent-api 72 | six==1.17.0 73 | # via qbittorrent-api 74 | tomlkit==0.7.2 75 | # via qBitrr2 (setup.py) 76 | types-requests==2.32.0.20250328 77 | # via pyarr 78 | ujson==5.10.0 79 | # via qBitrr2 (setup.py) 80 | urllib3==2.3.0 81 | # via 82 | # qbittorrent-api 83 | # requests 84 | # types-requests 85 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements.txt --strip-extras 6 | # 7 | attrs==25.3.0 8 | # via environ-config 9 | backports-tarfile==1.2.0 10 | # via jaraco-context 11 | cachetools==5.3.2 12 | # via qBitrr2 (setup.py) 13 | certifi==2025.1.31 14 | # via requests 15 | charset-normalizer==3.4.1 16 | # via requests 17 | colorama==0.4.4 18 | # via qBitrr2 (setup.py) 19 | coloredlogs==15.0.1 20 | # via qBitrr2 (setup.py) 21 | dill==0.3.9 22 | # via 23 | # multiprocess 24 | # pathos 25 | environ-config==23.2.0 26 | # via qBitrr2 (setup.py) 27 | ffmpeg-python==0.2.0 28 | # via qBitrr2 (setup.py) 29 | future==1.0.0 30 | # via ffmpeg-python 31 | humanfriendly==10.0 32 | # via coloredlogs 33 | idna==3.10 34 | # via requests 35 | jaraco-context==6.0.1 36 | # via jaraco-docker 37 | jaraco-docker==2.0 38 | # via qBitrr2 (setup.py) 39 | jaraco-functools==4.1.0 40 | # via jaraco-docker 41 | more-itertools==10.6.0 42 | # via jaraco-functools 43 | multiprocess==0.70.17 44 | # via pathos 45 | overrides==7.7.0 46 | # via pyarr 47 | packaging==22.0 48 | # via 49 | # qBitrr2 (setup.py) 50 | # qbittorrent-api 51 | pathos==0.2.8 52 | # via qBitrr2 (setup.py) 53 | peewee==3.14.7 54 | # via qBitrr2 (setup.py) 55 | ping3==3.0.2 56 | # via qBitrr2 (setup.py) 57 | pox==0.3.5 58 | # via pathos 59 | ppft==1.7.6.9 60 | # via pathos 61 | pyarr==5.1.2 62 | # via qBitrr2 (setup.py) 63 | pyreadline3==3.5.4 64 | # via humanfriendly 65 | qbittorrent-api==2023.7.52 66 | # via qBitrr2 (setup.py) 67 | requests==2.32.0 68 | # via 69 | # pyarr 70 | # qBitrr2 (setup.py) 71 | # qbittorrent-api 72 | six==1.17.0 73 | # via qbittorrent-api 74 | tomlkit==0.7.2 75 | # via qBitrr2 (setup.py) 76 | types-requests==2.32.0.20250328 77 | # via pyarr 78 | urllib3==2.3.0 79 | # via 80 | # qbittorrent-api 81 | # requests 82 | # types-requests 83 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = qBitrr2 3 | version = 4.10.23 4 | description = "A simple Python script to talk to qBittorrent and Arr's" 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/Feramance/qBitrr 8 | author = Feramance 9 | author_email = fera@fera.wtf 10 | license = MIT 11 | license_files = 12 | LICENSE 13 | classifiers = 14 | Development Status :: 5 - Production/Stable 15 | Intended Audience :: Developers 16 | Intended Audience :: End Users/Desktop 17 | License :: OSI Approved :: MIT License 18 | Natural Language :: English 19 | Operating System :: MacOS :: MacOS X 20 | Operating System :: Microsoft :: Windows 21 | Operating System :: POSIX :: Linux 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: 3.8 24 | Programming Language :: Python :: 3.9 25 | Programming Language :: Python :: 3.10 26 | Programming Language :: Python :: Implementation :: CPython 27 | Programming Language :: Python :: Implementation :: PyPy 28 | Topic :: Terminals 29 | Topic :: Utilities 30 | Typing :: Typed 31 | description_file = README.md 32 | project_urls = 33 | Issue Tracker = https://github.com/Feramance/qBitrr/issues 34 | Source Code = https://github.com/Feramance/qBitrr 35 | 36 | [options] 37 | packages = find_namespace: 38 | install_requires = 39 | cachetools==5.3.2 40 | colorama==0.4.4 41 | coloredlogs==15.0.1 42 | environ-config==23.2.0 43 | ffmpeg-python==0.2.0 44 | jaraco.docker==2.0 45 | packaging==22.0 46 | pathos==0.2.8 47 | peewee==3.14.7 48 | ping3==3.0.2 49 | pyarr==5.1.2 50 | qbittorrent-api==2023.7.52 51 | requests==2.32.0 52 | tomlkit==0.7.2 53 | python_requires = >=3.8.3,<4 54 | include_package_data = True 55 | 56 | [options.packages.find] 57 | include = 58 | qBitrr 59 | config.example.toml 60 | 61 | [options.entry_points] 62 | console_scripts = 63 | qbitrr=qBitrr.main:run 64 | 65 | [options.extras_require] 66 | dev = 67 | black==24.3.0 68 | bump2version==1.0.1 69 | isort==5.10.1 70 | pip-tools==7.3.0 71 | pre-commit==3.3.3 72 | pyinstaller==5.13.1 73 | pyupgrade==2.31.0 74 | twine==3.7.1 75 | ujson==5.10.0 76 | upgrade-pip==0.1.4 77 | fast = 78 | ujson==5.10.0 79 | all = 80 | %(dev)s 81 | %(fast)s 82 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | --------------------------------------------------------------------------------