Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
24beaea
fix: premature torrent removals, HnR clear mode, tests, .cursor in gi…
Feb 26, 2026
087a2e1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 26, 2026
0644e2a
fix(webui): avoid duplicate qBit prefix in process names; update docs…
Feb 26, 2026
5e22002
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 26, 2026
d5868d2
fix: pass qBit client to has_internet to avoid 'Instance default not …
Feb 26, 2026
8787c50
Added compose
Feb 26, 2026
25e8fc8
feat: automated startup database repair
Feb 26, 2026
ad82ba1
fix: reset download limit with set_download_limit when DownloadRateLi…
Feb 26, 2026
6d1d213
fix: catch DatabaseRecoveryError on startup; offer fresh DB when repa…
Feb 26, 2026
5cc1d2d
fix: reduce DB corruption on unclean shutdown; delete corrupt DB on f…
Feb 26, 2026
e5179f8
fix: FreeSpaceManager monitor qBit-managed categories too
Feb 26, 2026
c4ff7ec
docs: fix MkDocs build; document FSM category monitoring
Feb 26, 2026
a7967ef
ci: fix PR and docs workflows
Feb 26, 2026
b2ed922
fix: docs nav for strict build, ConfigView parse types, Configuration…
Feb 26, 2026
c47b9d8
fix(docs): disable parallel processing in git-revision-date plugin to…
Feb 26, 2026
4363e1a
fix(docs): include root CHANGELOG in docs/changelog.md for MkDocs build
Feb 26, 2026
bf3f298
fix(docs): ensure changelog.md exists in CI by copying CHANGELOG.md w…
Feb 26, 2026
157e897
fix(docs): remove symlink before copying changelog in CI
Feb 26, 2026
c15292c
webui: qBit process view - remove badges, one card per instance
Feb 26, 2026
d29785e
docs: fix health endpoint to /health and update Built With stack
Feb 26, 2026
1166131
docs: full doc vs code consistency review
Feb 26, 2026
b5045c2
fix: premature torrent removals in HNR clear mode
Feb 26, 2026
071be70
Update qbit_category_manager
Feb 26, 2026
35f49c2
fix: correct _should_leave_alone return type to 5-tuple (bool, int, b…
Feb 26, 2026
a10d3b8
Unify torrent behaviour for all categories; qBit-managed categories u…
Feb 26, 2026
368eccd
fix(database): wrap ArrInstance index creation in db.atomic() to fix …
Feb 26, 2026
e83d333
fix(arss): missing-files handling and per-instance removal for HnR/cl…
Feb 26, 2026
05b9be7
fix: premature torrent removals in HnR clear mode (arss + category ma…
Feb 26, 2026
76d331a
chore: remove unused qBit category removal helpers
cursoragent Feb 27, 2026
319bc47
chore: remove unused HnR helpers in category manager
cursoragent Feb 27, 2026
4943bc9
fix: resolve placeholder section mapping and windows path loop
cursoragent Feb 27, 2026
252d742
fix: apply placeholder qbit actions and cap compose log size
cursoragent Feb 27, 2026
0d84d36
fix: rate limit vars (dl_r/ul_r) in arss; dedupe Configuration nav in…
Feb 27, 2026
55ef11e
fix: FreeSpaceFolder, PlaceHolderArr category, ERROR/missing-files, _…
Feb 27, 2026
e07f2b1
fix: align placeholder defaults and retry deletion paths
cursoragent Feb 27, 2026
f82d1bb
fix(ci): correct python-version expression in PR workflow
Feb 27, 2026
f01b295
fix: remove accidental CLAUDE guide and init cleanup set
cursoragent Feb 27, 2026
a4c5deb
fix: remove stale sqlite wal and shm on fresh start
cursoragent Feb 27, 2026
b8a755b
Merge master into fix/premature-torrent-removals-hnr-clear-mode (fix …
Feb 27, 2026
173415f
fix(webui): defer setState in NumberInput effect to satisfy react-hoo…
Feb 27, 2026
934a3c3
Removed unecessary test
Feb 27, 2026
ee8b70d
Merge branch 'master' into fix/premature-torrent-removals-hnr-clear-mode
Feramance Feb 27, 2026
d0fb505
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 27, 2026
b8e558b
Fix pre-commit
Feb 27, 2026
01ff464
Merge branches 'fix/premature-torrent-removals-hnr-clear-mode' and 'f…
Feb 27, 2026
9485152
Removed redundant egg info
Feb 27, 2026
06a1e74
Updated git ignore
Feb 27, 2026
6f1df0f
feat(scripts): add post-merge hook to rebuild and deploy on git pull
Feb 27, 2026
810e1d6
feat(config): human-friendly duration in config and Web UI
Feb 28, 2026
b48b537
fix: handle sub-minute durations in minute parser
cursoragent Feb 28, 2026
a528989
chore: cross-platform rebuild script and WebUI duration labels
Feb 28, 2026
6ef76f5
fix: PlaceHolderArr TAGLESS crash and duration unit dropdown width
Feb 28, 2026
a1e0284
fix: comprehensive code review fixes across backend and frontend
Feb 28, 2026
9e81ba3
chore: codebase review fixes - security, quality, consistency
Feb 28, 2026
2c29b92
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
f669711
fix: Docker config volume permissions and WebUI/build errors
Feb 28, 2026
6fec591
feat(logs): tail-only API, Live checkbox, Refresh, load older logs
Mar 1, 2026
280a754
fix(webui): render changelog as markdown and improve Check Updates flow
Mar 1, 2026
66bfeb7
fix: DB locks, config path, logging, log file hardening
Mar 1, 2026
758981f
fix(webui): prevent qBit category processes from appearing as cards i…
Mar 1, 2026
977857d
fix(webui): hide configured qBit categories from Other section in Pro…
Mar 1, 2026
a6ea8ef
fix: handle sub-minute durations in minute parser
cursor[bot] Mar 1, 2026
cea8b33
fix(PlaceHolderArr): only cleanup cache for successfully deleted torr…
Mar 1, 2026
4805ed9
fix: only clean cache for successfully deleted torrents in PlaceHolde…
Mar 1, 2026
37b6dcf
fix: resolve CodeQL security findings
Mar 1, 2026
d089b8b
fix(webui): restore GET /web/config route for Config view
Mar 1, 2026
c5ba5f1
security: redact qBit passwords and Arr API keys in API/Web UI
Mar 1, 2026
50117cc
Added curl and fixed nightly workflow docker images
Mar 1, 2026
e6b4e75
Added favicon and some docker comments
Mar 1, 2026
29de695
docs: add qBitrr vs Huntarr comparison and From Huntarr migration
Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/autofix/auto_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import sys
import textwrap
import uuid
from collections.abc import Iterable
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
from typing import Iterable
from zipfile import ZipFile

import requests
Expand Down
5 changes: 2 additions & 3 deletions .github/scripts/update_releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
import re
import subprocess
import sys
from typing import Dict, List


def extract_changelog_entries() -> Dict[str, str]:
def extract_changelog_entries() -> dict[str, str]:
"""Extract all changelog entries from CHANGELOG.md"""
with open("CHANGELOG.md") as f:
content = f.read()
Expand All @@ -31,7 +30,7 @@ def extract_changelog_entries() -> Dict[str, str]:
return entries


def get_existing_releases() -> List[str]:
def get_existing_releases() -> list[str]:
"""Get list of existing GitHub releases"""
result = subprocess.run(
["gh", "release", "list", "--limit", "100"], capture_output=True, text=True, check=True
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
matrix:
language:
- python
- javascript-typescript
steps:
- name: Checkout repository
uses: actions/checkout@v6
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dockerhub-description.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
publish-descriptions:
name: Update Docker Hub and PyPI metadata
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@v6
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Ensure changelog page exists
run: |
if [ ! -f docs/changelog.md ] || [ -L docs/changelog.md ]; then
rm -f docs/changelog.md
cp CHANGELOG.md docs/changelog.md
fi

- name: Build documentation
run: mkdocs build --strict --verbose

Expand Down Expand Up @@ -99,6 +106,13 @@ jobs:
pip install -r requirements.docs.txt
pip install linkchecker

- name: Ensure changelog page exists
run: |
if [ ! -f docs/changelog.md ] || [ -L docs/changelog.md ]; then
rm -f docs/changelog.md
cp CHANGELOG.md docs/changelog.md
fi

- name: Build docs
run: mkdocs build

Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ jobs:
packages: write
contents: read
if: ${{ ! startsWith(github.event.head_commit.message, '[patch]') && ! startsWith(github.event.head_commit.message, '[minor]') && ! startsWith(github.event.head_commit.message, '[major]') }}
strategy:
matrix:
platform: [amd64, arm64]
steps:
- id: string
uses: ASzc/change-string-case-action@v6
Expand Down Expand Up @@ -65,7 +62,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: feramance/qbitrr:nightly,ghcr.io/${{ steps.string.outputs.lowercase }}:nightly
labels: ${{ steps.meta.outputs.labels }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ on:
jobs:
pre-commit:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install project (for absolufy-imports and import resolution)
run: pip install -e .
- uses: pre-commit/action@v3.0.1
8 changes: 5 additions & 3 deletions .github/workflows/pull_requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ jobs:
ref: ${{ github.event.pull_request.head.ref }}
- name: Install Homebrew dependencies
if: runner.os == 'macOS'
run: "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n"
run: |
brew install sdl2
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python}}
python-version: ${{ matrix.python }}
architecture: ${{ matrix.arch }}
cache: pip
- name: Install APT dependencies
Expand All @@ -64,7 +65,7 @@ jobs:
RELEASE_HASH: ${{ steps.git_hash.outputs.HASH }}
run: "python - <<'PY'\nimport os\nfrom pathlib import Path\nimport re\n\nrelease_hash = os.environ[\"RELEASE_HASH\"]\npath = Path(\"qBitrr/bundled_data.py\")\ncontent = path.read_text()\nupdated = re.sub(r'git_hash = \".*\"', f'git_hash = \"{release_hash}\"', content)\npath.write_text(updated)\nPY\n"
- name: Retrieve current version
run: "echo \"Current version: $(python setup.py --version)\"\necho \"VERSION=$(python setup.py --version)\" >> $GITHUB_OUTPUT\n"
run: "VERSION=$(python -c \"import configparser; c = configparser.ConfigParser(); c.read('setup.cfg'); print(c['metadata']['version'])\")\necho \"Current version: ${VERSION}\"\necho \"VERSION=${VERSION}\" >> $GITHUB_OUTPUT\n"
id: current_version
- name: Set up Node.js
uses: actions/setup-node@v6
Expand Down Expand Up @@ -102,6 +103,7 @@ jobs:
docker_image:
name: Build Docker Images
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
packages: write
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
run: "bump2version \"${RELEASE_TYPE}\"\n"
- name: Capture new version
id: version
run: "VERSION=$(python setup.py --version)\necho \"value=${VERSION}\" >> \"${GITHUB_OUTPUT}\"\necho \"New version: ${VERSION}\"\n"
run: "VERSION=$(python -c \"import configparser; c = configparser.ConfigParser(); c.read('setup.cfg'); print(c['metadata']['version'])\")\necho \"value=${VERSION}\" >> \"${GITHUB_OUTPUT}\"\necho \"New version: ${VERSION}\"\n"
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6.3.0
Expand Down
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.run/
.vscode/
.vs/
.cursor

# Testing
/qBitrr/testing/test.py
Expand All @@ -20,6 +21,7 @@ __pycache__/
*.so
*.egg
*.egg-info/
qBitrr2.egg-info/
.Python

# Build artifacts
Expand Down Expand Up @@ -69,7 +71,6 @@ test_webui.sh
/scripts/temp/

# Development
CLAUDE.md
compose.yml
opencodetmp/
.claude
CLAUDE.md
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
rev: v3.21.0
hooks:
- id: pyupgrade
args: [--py38-plus]
args: [--py311-plus]
- repo: https://github.com/pycqa/isort
rev: 7.0.0
hooks:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ Each release produces:
- **Tagging**: Radarr/Sonarr downloads MUST have tags matching the category configured for qBitrr to track them
- **Paths**: qBit's "Save Path" must be accessible to Radarr/Sonarr (common issue in Docker with mismatched volumes)
- **WebUI Token**: If `Settings.WebUIToken` is set, all `/api/*` calls require `X-API-Token` header
- **Database Locks**: Use `db_lock.py:locked_database()` context manager for all Peewee queries to avoid conflicts
- **Database Locks**: Use `db_lock.py:database_lock()` context manager for all Peewee queries to avoid conflicts

## Documentation Maintenance

Expand Down
20 changes: 15 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,39 @@ RUN cd webui && npm ci
COPY webui webui
RUN mkdir -p qBitrr/static && cd webui && npm run build

# Pin Python to the latest supported version we support in production
FROM python:3.14
FROM python:3.12-slim
Comment thread
Feramance marked this conversation as resolved.
Resolved

LABEL Name="qBitrr"
LABEL Maintainer="feramance"
LABEL Version="5.9.1"
LABEL org.opencontainers.image.source=https://github.com/feramance/qbitrr

# Install tini (PID 1, SIGTERM forwarding), gosu (drop privileges after chown), and curl (health checks)
RUN apt-get update && apt-get install -y --no-install-recommends tini gosu curl \
&& rm -rf /var/lib/apt/lists/*
Comment thread
Feramance marked this conversation as resolved.
Resolved
Comment thread
Feramance marked this conversation as resolved.
Resolved

# Env used by the script to determine if it's inside a docker -
# if this is set to 69420 it will change the working dir for docker specific values
ENV QBITRR_DOCKER_RUNNING=69420
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONOPTIMIZE=1

RUN pip install --quiet -U pip wheel
RUN pip install --no-cache-dir --quiet -U pip wheel
WORKDIR /app
COPY . /app
COPY --from=webui-build /src/qBitrr/static/ /app/qBitrr/static/
RUN rm -rf qBitrr2.egg-info *.egg-info && pip install --quiet ".[fast]"
RUN rm -rf qBitrr2.egg-info *.egg-info && pip install --no-cache-dir --quiet ".[fast]"

RUN groupadd -r qbitrr && useradd -r -g qbitrr -d /config -s /sbin/nologin qbitrr \
&& mkdir -p /config && chown -R qbitrr:qbitrr /config /app

COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

WORKDIR /config

EXPOSE 6969

ENTRYPOINT ["python", "-m", "qBitrr.main"]
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["python", "-m", "qBitrr.main"]
Comment thread
Feramance marked this conversation as resolved.
Resolved
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,4 @@ Released under the [MIT License](LICENSE). Use it, modify it, share it—commerc
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Feramance/qBitrr&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Feramance/qBitrr&type=date&legend=top-left" />
</picture>
</a>
</a>
4 changes: 2 additions & 2 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ MaxSeedingTime = -1
# 4 = Remove when both conditions are met (AND)
RemoveTorrent = -1

# Enable Hit and Run protection for managed category torrents
HitAndRunMode = false
# Hit and Run mode: and = require both ratio and time; or = either clears; disabled = no HnR
HitAndRunMode = "disabled"

# Minimum seed ratio before removal allowed (HnR protection)
MinSeedRatio = 1.0
Expand Down
32 changes: 32 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
services:
qbitrr:
container_name: qbitrr
build:
context: .
dockerfile: Dockerfile
environment:
- UMASK=000
- TZ=${TZ}
- PUID=${USER_ID}
- PGID=${USER_ID}
restart: always
tty: true
stop_grace_period: 30s
volumes:
- torrents:/torrents
- .config:/config
logging:
driver: json-file
options:
max-size: 50m
max-file: 1
ports:
- 6969:6969
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

volumes:
torrents:
driver: local
driver_opts:
type: cifs
device: ${CIFS_PATH}
o: username=${CIFS_USER},password=${CIFS_PASS},vers=3.1.1,iocharset=utf8
14 changes: 14 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh
set -e
# Default to 1000:1000 when PUID/PGID are not set (e.g. docker run without -e)
export PUID="${PUID:-1000}"
export PGID="${PGID:-1000}"

# When running as root, ensure /config is owned by PUID:PGID so the app can write
# (mounted volumes often appear as root-owned; this restores writability)
if [ "$(id -u)" = "0" ] && [ "${PUID}" != "0" ]; then
chown -R "${PUID}:${PGID}" /config 2>/dev/null || true
fi

# Run the container CMD as the specified user (tini + python)
exec gosu "${PUID}:${PGID}" /usr/bin/tini -- "$@"
Comment thread
Feramance marked this conversation as resolved.
Resolved
14 changes: 7 additions & 7 deletions docs/advanced/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ def run_loop(self):
time.sleep(LOOP_SLEEP_TIMER)

except DelayLoopException as e:
logger.warning(f"Delaying loop for {e.length}s: {e.type}")
logger.warning(f"Delaying loop for {e.length}s: {e.error_type}")
time.sleep(e.length)

except RestartLoopException:
Expand Down Expand Up @@ -970,12 +970,12 @@ if torrent['category'] not in self.categories:

```python
class DelayLoopException(qBitManagerError):
def __init__(self, length: int, type: str):
self.type = type # Reason for delay
self.length = length # Seconds to delay
def __init__(self, length: int, error_type: str):
self.error_type = error_type # Reason for delay
self.length = length # Seconds to delay

# Usage: raised when qBittorrent or an Arr API is unreachable
raise DelayLoopException(length=60, type="qbittorrent_offline")
raise DelayLoopException(length=60, error_type="qbittorrent_offline")
```

**RestartLoopException** -- restart from the beginning after a config reload:
Expand All @@ -989,9 +989,9 @@ class RestartLoopException(ArrManagerException):

```python
class NoConnectionrException(qBitManagerError):
def __init__(self, message: str, type: str = "delay"):
def __init__(self, message: str, error_type: str = "delay"):
self.message = message
self.type = type # "delay" or "fatal"
self.error_type = error_type # "delay" or "fatal"
```

### Torrent State Machine
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ External monitoring can check:

```bash
# Check if qBitrr is running
curl http://localhost:6969/api/health
curl http://localhost:6969/health

# Expected response:
{"status": "healthy", "version": "5.5.5"}
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.md
Loading
Loading