qbit-ops is a small qBittorrent operations CLI for homelab usage.
It helps run bulk qBittorrent operations with safe defaults, explicit dry-runs and readable summaries.
- Check qBittorrent connectivity from
.env. - Diagnose qbit-ops configuration and qBittorrent API access.
- List torrents with basic audit fields.
- List torrent categories and filter torrents by category.
- Inspect a torrent by hash with tracker details.
- Export a full instance backup with torrents, trackers and metadata.
- Compare two export files before applying bulk changes.
- List trackers used by a qBittorrent instance.
- Analyze tracker health, disabled trackers and dynamic query variants.
- Inspect torrents using a specific tracker.
- Export the active tracker state as JSON.
- Add a tracker only when another tracker is already present.
- Remove a tracker in bulk.
- Replace a tracker in bulk without creating duplicate target trackers.
- Pause, resume, start or reannounce torrents in bulk by category, tracker, name, completed or all.
- Match trackers exactly or without query parameters.
- Ignore disabled qBittorrent pseudo-trackers such as DHT, PeX and LSD.
- Use
--output jsonon all audit commands for scripting. - Keep
--dry-runenabled by default for modifying commands.
qbit-ops is designed to avoid accidental destructive operations.
--dry-runis enabled by default for bulk modifications.- Real changes require
--no-dry-run. - Bulk torrent actions require exactly one filter:
--category,--tracker,--name,--allor--completed(onstartonly). - Tracker URLs are normalized before comparison.
--match exactis the default matching mode.--match without-querymust be requested explicitly.- Raw qBittorrent tracker URLs are preserved for API operations.
- Linux
- Git
- Make
- Python 3.12
- Poetry
- qBittorrent Web UI/API enabled
Check local tooling:
make doctorgit clone https://github.com/LECOQQ/qbit-ops.git
cd qbit-ops
make installThis installs Poetry dependencies and local Git hooks.
Run the CLI from the repository with:
poetry run qbit-ops --helpUse pipx when you want qbit-ops available as a regular command outside the
repository.
Install pipx on Ubuntu/Debian:
sudo apt update
sudo apt install pipx
pipx ensurepathRestart your shell if pipx ensurepath asks you to.
Install qbit-ops from the repository:
git clone https://github.com/LECOQQ/qbit-ops.git
cd qbit-ops
pipx install .Then run:
qbit-ops --helpUpdate an existing pipx installation from the repository:
cd qbit-ops
pipx reinstall qbit-opsCreate a local .env file:
cp .env.example .envThen configure your qBittorrent connection:
QBIT_HOST=http://localhost:8080
QBIT_USER=admin
QBIT_PASSWORD=change-meNever commit .env; it contains local secrets and is ignored by Git.
When qbit-ops is installed as an application, for example with pipx, prefer
a user-level configuration file:
mkdir -p ~/.config/qbit-ops
cp .env.example ~/.config/qbit-ops/.envThen edit:
nano ~/.config/qbit-ops/.envConfiguration lookup order:
- existing environment variables;
- file pointed to by
QBIT_OPS_ENV_FILE, when set; .envin the current working directory;~/.config/qbit-ops/.env.
Check connectivity:
qbit-ops connection checkRun configuration diagnostics:
qbit-ops config doctorList torrents:
qbit-ops torrents listList categories:
qbit-ops torrents categoriesList torrents in a category:
qbit-ops torrents list --category sonarrList torrents using a tracker:
qbit-ops torrents list --tracker "https://tracker-a.example/announce"Inspect a torrent by hash:
qbit-ops torrents inspect --hash "TORRENT_HASH"Search torrents by name:
qbit-ops torrents inspect --name "L.amour.est.dans.le.pre.S20E02"List trackers:
qbit-ops trackers listInspect torrents using a tracker:
qbit-ops trackers inspect --tracker "https://tracker-a.example/announce"Export the active tracker state:
qbit-ops trackers exportGroup dynamic tracker URLs without query parameters:
qbit-ops trackers list --match without-queryAnalyze tracker health:
qbit-ops trackers healthExport a full backup with torrents, trackers and metadata:
qbit-ops backup export --output jsonCompare two export files:
qbit-ops backup diff backup-before.json backup-after.jsonPause torrents in a category:
qbit-ops torrents pause --category sonarr --dry-runWhen working from a Poetry development environment, prefix commands with
poetry run, for example:
poetry run qbit-ops connection checkThe examples below use poetry run for development. If qbit-ops is installed
with pipx, remove the poetry run prefix.
Show the CLI help:
poetry run qbit-ops --helpCheck connection settings:
poetry run qbit-ops connection checkCheck connection settings as JSON:
poetry run qbit-ops connection check --output jsonRun configuration diagnostics:
poetry run qbit-ops config doctorRun configuration diagnostics as JSON:
poetry run qbit-ops config doctor --output jsonList torrents:
poetry run qbit-ops torrents listList torrents as JSON:
poetry run qbit-ops torrents list --output jsonList categories:
poetry run qbit-ops torrents categoriesList categories as JSON:
poetry run qbit-ops torrents categories --output jsonList torrents in a category:
poetry run qbit-ops torrents list --category sonarrList uncategorized torrents:
poetry run qbit-ops torrents list --category "(uncategorized)"List torrents using a tracker:
poetry run qbit-ops torrents list \
--tracker "https://tracker-a.example/announce"List torrents using a tracker without query parameters:
poetry run qbit-ops torrents list \
--tracker "http://connect.maxp2p.org:8080/passkey/announce" \
--match without-queryInspect a torrent by hash:
poetry run qbit-ops torrents inspect --hash "TORRENT_HASH"Search torrents by name:
poetry run qbit-ops torrents inspect --name "L.amour.est.dans.le.pre"Search torrents by name as JSON:
poetry run qbit-ops torrents inspect \
--name "L.amour.est.dans.le.pre" \
--output jsonInspect a torrent as JSON:
poetry run qbit-ops torrents inspect \
--hash "TORRENT_HASH" \
--output jsonList trackers with exact matching:
poetry run qbit-ops trackers listList trackers grouped without query parameters:
poetry run qbit-ops trackers list --match without-queryList trackers as JSON:
poetry run qbit-ops trackers list --output jsonAnalyze tracker health:
poetry run qbit-ops trackers healthAnalyze tracker health as JSON:
poetry run qbit-ops trackers health --output jsonInspect torrents using a tracker:
poetry run qbit-ops trackers inspect \
--tracker "https://tracker-a.example/announce"Inspect torrents using a tracker as JSON:
poetry run qbit-ops trackers inspect \
--tracker "https://tracker-a.example/announce" \
--output jsonExport the active tracker state as JSON:
poetry run qbit-ops trackers export --output jsonExport a full backup with torrents, trackers and metadata:
poetry run qbit-ops backup export --output jsonCompare two export files:
poetry run qbit-ops backup diff backup-before.json backup-after.jsonCompare two export files as JSON:
poetry run qbit-ops backup diff backup-before.json backup-after.json \
--output jsonAdd a tracker if another tracker is already present:
poetry run qbit-ops trackers add-if-present \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce"Apply the add operation:
poetry run qbit-ops trackers add-if-present \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--no-dry-runRemove a tracker in bulk:
poetry run qbit-ops trackers remove \
--tracker "https://tracker-a.example/announce"Replace a tracker in bulk:
poetry run qbit-ops trackers replace \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce"Apply the remove operation:
poetry run qbit-ops trackers remove \
--tracker "https://tracker-a.example/announce" \
--no-dry-runApply the replace operation:
poetry run qbit-ops trackers replace \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--no-dry-runPause torrents in a category:
poetry run qbit-ops torrents pause --category sonarr --dry-runResume torrents in a category:
poetry run qbit-ops torrents resume --category sonarr --no-dry-runReannounce torrents using a tracker:
poetry run qbit-ops torrents reannounce \
--tracker "https://tracker-a.example/announce" \
--dry-run \
--verboseUse these commands to inspect the qBittorrent instance before changing it:
poetry run qbit-ops connection check
poetry run qbit-ops config doctor
poetry run qbit-ops torrents list
poetry run qbit-ops torrents categories
poetry run qbit-ops torrents list --category sonarr
poetry run qbit-ops torrents inspect --hash "TORRENT_HASH"
poetry run qbit-ops trackers list
poetry run qbit-ops trackers list --match without-query
poetry run qbit-ops trackers health
poetry run qbit-ops trackers inspect \
--tracker "https://tracker-a.example/announce"
poetry run qbit-ops trackers export --output json
poetry run qbit-ops backup export --output json
poetry run qbit-ops backup diff backup-before.json backup-after.jsonUse this when you want to add a target tracker only to torrents that already use a known source tracker.
Dry-run:
poetry run qbit-ops trackers add-if-present \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--dry-run \
--verboseApply:
poetry run qbit-ops trackers add-if-present \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--no-dry-runSome trackers include dynamic query parameters such as sig or announce_ts.
Use --match without-query to compare only the stable tracker URL identity.
List grouped variants:
poetry run qbit-ops trackers list --match without-queryAdd conditionally with query parameters ignored:
poetry run qbit-ops trackers add-if-present \
--source "http://connect.maxp2p.org:8080/passkey/announce" \
--target "https://tracker-b.example/announce" \
--match without-query \
--dry-run \
--verboseRemove dynamic variants with query parameters ignored:
poetry run qbit-ops trackers remove \
--tracker "http://connect.maxp2p.org:8080/passkey/announce" \
--match without-query \
--dry-run \
--verboseReplace dynamic variants with query parameters ignored:
poetry run qbit-ops trackers replace \
--source "http://connect.maxp2p.org:8080/passkey/announce" \
--target "https://tracker-b.example/announce" \
--match without-query \
--dry-run \
--verboseUse this when a tracker should be removed from every torrent using it.
Dry-run:
poetry run qbit-ops trackers remove \
--tracker "https://tracker-a.example/announce" \
--dry-run \
--verboseApply:
poetry run qbit-ops trackers remove \
--tracker "https://tracker-a.example/announce" \
--no-dry-runUse this when a tracker should be migrated to another tracker.
If the target tracker is already present on a torrent, qbit-ops removes the
source tracker instead of adding a duplicate target.
Dry-run:
poetry run qbit-ops trackers replace \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--dry-run \
--verboseApply:
poetry run qbit-ops trackers replace \
--source "https://tracker-a.example/announce" \
--target "https://tracker-b.example/announce" \
--no-dry-runUse these commands to act on torrents filtered by category, tracker, name,
completed or all. Exactly one filter is required, except --completed which
can also be combined with --category, --tracker or --name.
Dry-run pause by category:
poetry run qbit-ops torrents pause --category sonarr --dry-run --verboseStart all stopped completed torrents (qBittorrent Web UI equivalent):
poetry run qbit-ops torrents start --completed --dry-run --verbose
poetry run qbit-ops torrents start --completed --no-dry-runApply resume to all stopped torrents:
poetry run qbit-ops torrents resume --all --no-dry-runApply resume by tracker:
poetry run qbit-ops torrents resume \
--tracker "https://tracker-a.example/announce" \
--no-dry-runReannounce torrents matching a name:
poetry run qbit-ops torrents reannounce \
--name "L.amour.est.dans.le.pre" \
--dry-runpause, resume and start are idempotent:
pauseskips torrents already stopped (paused*orstopped*states).resumeandstartskip torrents that are not stopped; active torrents are never restarted.start --completedonly targets torrents withprogress=100%.
exact: compares the full normalized tracker URL. This is the default.without-query: ignores query parameters when comparing trackers.
Both modes preserve the raw qBittorrent URLs for API operations. This matters for remove operations, because qBittorrent expects the original tracker URLs.
All audit commands accept --output text|json:
connection checkconfig doctortorrents listtorrents categoriestorrents inspecttrackers listtrackers healthtrackers inspecttrackers exportbackup exportbackup diff
Text output is the default and prints a human-readable summary. JSON output is intended for scripting and backups.
backup export --output json produces a full payload with:
- export metadata (
exported_at, qBittorrent versions, configured host); - torrent metadata and tracker details for every torrent;
- normalized tracker identities;
- aggregated tracker usage counts.
Example:
qbit-ops backup export --output json > backup.jsontorrents inspect --name ranks matches by relevance:
- exact name match;
- name starts with the query;
- name contains the query;
- fuzzy similarity fallback.
Use --hash when you already know the torrent hash and need full tracker
details. Use --name to find candidate torrents first, then inspect the hash
you selected.
backup diff compares two export files produced by backup export or
trackers export. It reports:
- torrents added, removed or changed;
- normalized tracker additions and removals per torrent;
- tracker usage additions, removals and count changes.
Example:
qbit-ops backup diff backup-before.json backup-after.jsontrackers health reports:
- scanned torrents;
- active tracker occurrences;
- disabled tracker occurrences;
- unique exact tracker URLs;
- unique logical tracker URLs without query parameters;
- query variant groups;
- disabled tracker URLs such as DHT, PeX or LSD.
Use JSON output when the report should be consumed by another tool:
qbit-ops trackers health --output jsonModifying commands print a final summary:
Summary:
- scanned: X
- matched_source: X
- already_had_target: X
- modified: X
- dry_run: true/false
Tracker removal uses a dedicated summary:
Summary:
- scanned: X
- matched_tracker: X
- modified: X
- removed_urls: X
- dry_run: true/false
Tracker replacement uses a dedicated summary:
Summary:
- scanned: X
- matched_source: X
- already_had_target: X
- modified: X
- replaced_urls: X
- removed_urls: X
- dry_run: true/false
Bulk torrent actions use a dedicated summary:
Summary:
- action: pause|resume|start|reannounce
- filter: category|tracker|name|completed|all
- value: ...
- match: exact|without-query
- scanned: X
- matched: X
- modified: X
- skipped: X
- dry_run: true/false
When --verbose is passed to a bulk modification command, impacted torrents are
printed after the summary.
0: command completed successfully.1: configuration, connection, authentication or API error.2: targeted command completed but matched no torrent.
Exit code 2 is used when a command completes successfully but reports a
non-error outcome that may still require attention:
torrents inspect,torrents list --tracker,torrents list --category,torrents pause,torrents resume,torrents start,torrents reannounce,trackers inspect,trackers add-if-present,trackers remove,trackers replace: no torrent matched the requested criteria;backup diff: the two exports differ.
Format and validate the project:
make format
make checkIssues and pull requests are welcome.
Before opening a pull request:
make format
make checkPlease keep changes small, explicit and aligned with the safety-first behavior of the CLI.
qbit-ops is licensed under the MIT License. See LICENSE.