qBittorrent Tracker Remover
A long-running daemon that polls qBittorrent and strips unwanted trackers from active downloads. It posts a Discord alert when it loses its connection to qBittorrent and another when the connection comes back.
Alerts are edge-triggered: one red embed when the connection drops, silence across every retry while it stays down, one green embed with the outage duration on recovery. A long outage produces two messages, not hundreds.
Per-torrent processing errors are logged but never sent to Discord — only genuine connectivity loss alerts.
| File | Purpose |
|---|---|
tracker_remover.py | The daemon |
install.sh | Installer — renders the unit and wires secrets |
qbt-tracker-remover.service | systemd unit template (placeholder tokens) |
requirements.txt | Python dependencies |
.env.example | Reference for every supported variable |
Configuration
Section titled “Configuration”All configuration is environment variables. Only QB_PASSWORD is required — the script
exits 1 with a printed variable reference if it is missing.
| Variable | Required | Default |
|---|---|---|
QB_PASSWORD | yes | — |
QB_DISCORD_WEBHOOK | no | unset → notifications disabled, service still runs |
QB_HOST | no | 192.168.1.29 |
QB_PORT | no | 21287 |
QB_USERNAME | no | admin |
QB_CHECK_INTERVAL | no | 2 (seconds) |
QB_UNWANTED_TRACKERS | no | the trackers listed in .env.example |
QB_LOG_LEVEL | no | INFO (DEBUG adds per-torrent detail) |
QB_LOG_DIR | no | ~/Automation/logs (the .deb sets /var/log/…) |
Logging
Section titled “Logging”Logs go to both journald and ~/Automation/logs/tracker_remover.log. That path
follows the service user’s home, not WorkingDirectory, so it sits outside the
checkout and is created on first run. Set QB_LOG_DIR to put it somewhere else — the
.deb uses that to log to /var/log/qbt-tracker-remover instead.
The file is rotated by the script itself — 10 MB per file, 5 kept, so it is capped
at about 60 MB. No logrotate config is needed or used. journald bounds its own copy of
stdout separately, under whatever SystemMaxUse the host sets.
At INFO the log carries only real events: connection loss and recovery, trackers
actually removed, and a Checking N active downloads line when N changes or every 5
minutes as a heartbeat. Per-torrent and per-tracker detail sits at DEBUG, since the
poll interval is seconds and logging every pass produced tens of thousands of identical
lines a day. Set QB_LOG_LEVEL=DEBUG while troubleshooting to get it back.
Install
Section titled “Install”This runs on any Linux host with python3 and systemd. It does not have to be the
qBittorrent host — only something that can reach QB_HOST:QB_PORT.
There are two ways in. Prefer apt on any host that just needs to run the service.
From the Nexus APT repo (preferred)
Section titled “From the Nexus APT repo (preferred)”sudo apt update && sudo apt install qbt-tracker-removersudo qbt-tracker-remover configureconfigure asks for every setting with the value in effect today in brackets, exactly
like install.sh does, then writes the env file and offers to start the service. The
prompts live in a command rather than in the package’s postinst because dpkg
maintainer scripts may not prompt — stdin is not a terminal during apt install, and a
script waiting on an answer would hold the dpkg lock. Re-run it any time to change a
setting; secrets are never echoed.
For Ansible or any scripted install, skip it and write /etc/qbt-tracker-remover/env
directly — it is a conffile, so your version survives package upgrades.
The package runs as its own system user, keeps its dependencies in a venv under
/usr/lib/qbt-tracker-remover, logs to /var/log/qbt-tracker-remover, and treats
/etc/qbt-tracker-remover/env as a conffile so your settings survive upgrades. It is
enabled but deliberately not started until a credential is present — starting an
unconfigured service only burns the restart limit. Source lives in
packages/debian-ubuntu/qbt-tracker-remover/.
Upgrades are apt upgrade. Nothing on the host needs a git checkout.
From a checkout with install.sh
Section titled “From a checkout with install.sh”Use this when you are developing the script and want the service running the working tree, or on a host with no access to the APT repo. It is the same daemon, wired differently: it runs in place out of the checkout, as whichever user you choose.
If a host has both, the installer’s unit in /etc/systemd/system shadows the
packaged one in /usr/lib/systemd/system — systemd precedence, and the package’s
postinst warns when it sees this. Pick one per host. To hand a host over to the
package:
sudo systemctl disable --now qbt-tracker-removersudo rm /etc/systemd/system/qbt-tracker-remover.servicesudo systemctl daemon-reload && sudo systemctl enable --now qbt-tracker-removerDebian and Ubuntu split ensurepip out of python3, so python3 -m venv fails on a
minimal image. The installer detects that and installs the matching
python3.<minor>-venv package via apt-get before creating the venv.
Nothing below is host- or user-specific. install.sh resolves the service user and the
checkout path itself, so the common case is one command.
1. Clone the repo on the host
Section titled “1. Clone the repo on the host”The unit runs the script in place from a checkout of this repo, so updates are a
git pull rather than a copy. Put it wherever you like:
git clone <repo-url> ~/GitRepos/infrastructure-toolkitcd ~/GitRepos/infrastructure-toolkit/utils/qbittorrent-automationNever edit
tracker_remover.pyon the host. Changes flow commit → push →git pull→ restart; a local edit makes the next pull conflict.
2. Run the installer
Section titled “2. Run the installer”Preview first — this writes nothing and prints the exact unit and env file it would create, with secrets masked:
./install.sh --dry-runThen install. BWS_ACCESS_TOKEN is read from the environment; leave it unset on an
interactive terminal and you will be prompted for it:
BWS_ACCESS_TOKEN='<token>' ./install.shThat resolves the service user with id -un and the checkout with
git rev-parse --show-toplevel, creates a venv outside the checkout, installs the
dependencies, writes /etc/qbt-tracker-remover/env as 640 root:<user>, renders and
validates the unit, then enables and starts it.
Without BWS, use the env-file mode — you will be prompted for the qBittorrent password
if QB_PASSWORD is not already in the environment:
./install.sh --secrets envfileOn an EU Bitwarden account add --bws-server-base https://vault.bitwarden.eu. The
default endpoint is vault.bitwarden.com, where an EU-issued token gets
400 invalid_client; the installer applies the setting as the service user, since bws
stores it per-user.
Options
Section titled “Options”Every parameter is optional. Anything you leave out keeps its default, and any
application setting you leave out is omitted from the env file entirely, so the
defaults inside tracker_remover.py stay the single source of truth.
| Flag | Default |
|---|---|
--user NAME | current user (id -un) |
--repo-dir PATH | git rev-parse --show-toplevel |
--unit-name NAME | qbt-tracker-remover |
--unit-dir PATH | /etc/systemd/system |
--config-dir PATH | /etc/<unit-name> |
--secrets bws|envfile | bws |
--bws-bin PATH | /usr/bin/bws |
--bws-server-base URL | https://vault.bitwarden.eu |
--venv PATH | <service-home>/.venvs/<unit-name> |
--system-python | off (a venv is created) |
--python PATH | /usr/bin/python3, implies --system-python |
--skip-deps | off |
--qb-host HOST | keep the script’s default |
--qb-port PORT | keep the script’s default |
--qb-username NAME | keep the script’s default |
--check-interval SECS | keep the script’s default |
--unwanted-trackers CSV | keep the script’s default |
--discord-webhook URL | unset (alerts disabled) |
--dry-run | off |
--status | report unit state and exit |
--uninstall | remove the unit |
--no-start | enable without starting |
--force | skip all prompts, including the settings ones |
Secrets are never flags — a flag is visible in ps and lands in shell history. They
come from BWS_ACCESS_TOKEN, QB_PASSWORD and QB_DISCORD_WEBHOOK in the environment,
or from an interactive prompt. --discord-webhook is the one exception, as a webhook
URL is a capability rather than a credential; use the env var if you would rather it not
appear in ps.
Interactive settings
Section titled “Interactive settings”On a terminal the installer asks for each application setting in turn, showing the value in effect today in brackets:
Application settings - Enter keeps the value in brackets.
qBittorrent host [192.168.1.29]: qBittorrent WebUI port [21287]: qBittorrent WebUI username [admin]: Seconds between polls [2]: Tracker patterns to strip (comma-separated) [tracker.p2p-world.net,www.sharewood.tv,...]:The bracketed value is the key’s current line in the env file if the service is already
installed, otherwise the default read straight out of tracker_remover.py — the
installer does not keep its own copy of those defaults, so a prompt cannot advertise a
value the script would not use.
Enter writes nothing. An existing value stays as it is, and an unconfigured key stays out of the env file so the script’s default keeps applying. That means accepting every prompt on a fresh install is exactly equivalent to passing no flags at all.
Anything supplied as a flag is not prompted for; it is echoed as (from flag) instead.
--force skips the prompts entirely, which is what scripted and Ansible-driven installs
want. Under --secrets bws the webhook is not prompted for, since it comes from
Bitwarden and a copy in the env file would shadow it.
Re-running the installer is safe: it merges only the keys you supply into the env file
and leaves the rest alone, so changing --qb-host will not discard a password set
earlier. Any existing unit is backed up to <unit>.service.backup first.
If you are replacing an existing hand-written unit under a different name, disable it first so both do not run at once:
sudo systemctl disable --now <old-unit-name>Verify
Section titled “Verify”./install.sh --status # resolved config, read back from the installed unitjournalctl -u qbt-tracker-remover -fA healthy start looks like this, and posts nothing to Discord — a clean first connect is not a “recovery”:
Starting qBittorrent tracker remover service on LinuxDiscord connection alerts enabledWaiting 10 seconds for qBittorrent to start...Connecting to qBittorrent at 192.168.1.29:21287Successfully connected to qBittorrentChecking N active downloadsIf you see QB_DISCORD_WEBHOOK is not set - Discord notifications disabled, the
variable did not reach the process: under --secrets bws the BWS secret is missing or
misnamed, under --secrets envfile it is absent from the env file.
./install.sh --status shows which file the unit actually reads.
End-to-end alert test. Point the service at a dead endpoint and confirm one red embed arrives, then revert:
sudo systemctl edit qbt-tracker-remover # add: [Service] / Environment=QB_HOST=127.0.0.1sudo systemctl restart qbt-tracker-remover# expect exactly one ❌ embed in Discord, then silence across the 60s retriessudo systemctl revert qbt-tracker-removersudo systemctl restart qbt-tracker-remover# expect one ✅ embed reporting the outage durationUpdate
Section titled “Update”Commit and push the change, then on the host:
cd <checkout>git status --porcelain # expect empty; anything here will block the pullgit pullsudo systemctl restart qbt-tracker-removerThe service keeps running the old code until the restart — git pull alone changes
nothing.
If the pull changed qbt-tracker-remover.service itself, re-run ./install.sh instead
of restarting: the unit is copied into /etc at install time, never read from the
checkout, so a pull alone cannot update it.
Uninstall
Section titled “Uninstall”./install.sh --uninstallStops, disables and removes the unit, then asks before deleting the config directory —
it holds the secrets you typed. The checkout, the venv and ~/Automation/logs are left
alone.
Manual install (appendix)
Section titled “Manual install (appendix)”install.sh is the supported path; this is the equivalent by hand if you need it. The
unit has four placeholder tokens:
SERVICE_USER=$(id -un)sed -e "s|@SERVICE_USER@|$SERVICE_USER|" \ -e "s|@REPO_DIR@|$(git rev-parse --show-toplevel)|" \ -e "s|@ENV_FILE@|/etc/qbt-tracker-remover/env|" \ -e "s|@EXEC_START@|/usr/bin/bws run -- /usr/bin/python3 tracker_remover.py|" \ qbt-tracker-remover.service \ | sudo tee /etc/systemd/system/qbt-tracker-remover.service >/dev/null
# no token may survive, or systemd fails later with an opaque errorgrep -q '@[A-Z_]\+@' /etc/systemd/system/qbt-tracker-remover.service \ && echo "PLACEHOLDER LEFT - fix before starting" \ || echo "substitution ok"
sudo mkdir -p /etc/qbt-tracker-removersudo cp .env.example /etc/qbt-tracker-remover/envsudo nano /etc/qbt-tracker-remover/env # fill in the secrets you needsudo chown "root:$SERVICE_USER" /etc/qbt-tracker-remover/envsudo chmod 640 /etc/qbt-tracker-remover/env
sudo systemd-analyze verify /etc/systemd/system/qbt-tracker-remover.servicesudo systemctl daemon-reloadsudo systemctl enable --now qbt-tracker-removerDependencies then have to be installed by hand as the service user — note that a
--user pip install into your own account is invisible to a different service user, and
Debian 12+ / Ubuntu 24.04+ refuse to touch the system Python at all (PEP 668):
SERVICE_HOME=$(getent passwd "$SERVICE_USER" | cut -d: -f6) # never $HOMEsudo -u "$SERVICE_USER" python3 -m venv "$SERVICE_HOME/.venvs/qbt-tracker-remover"sudo -u "$SERVICE_USER" "$SERVICE_HOME/.venvs/qbt-tracker-remover/bin/pip" install -r requirements.txtsudo -u "$SERVICE_USER" python3 -c "import qbittorrentapi, requests; print('ok')"Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
Required environment variable 'QB_PASSWORD' is not set, unit failed after 5 tries | Secrets never reached the process. Check ./install.sh --status for the real EnvironmentFile, and that the BWS secret is named exactly QB_PASSWORD. |
status=200/CHDIR | WorkingDirectory wrong or unreadable by the service user. Check ./install.sh --status, then re-run ./install.sh with the right --repo-dir. |
Failed to determine user credentials / unknown user | The unit names a user that does not exist. Re-run ./install.sh --user <name>; the installer rejects an unknown user up front. |
status=203/EXEC | /usr/bin/bws or /usr/bin/python3 not at that path — check with command -v bws python3. |
ModuleNotFoundError: No module named 'qbittorrentapi' | Installed for the wrong user. Re-run ./install.sh without --skip-deps, or check the venv path in ./install.sh --status. |
ensurepip is not available while creating the venv | python3.<minor>-venv missing and no apt-get to install it. Install it with your package manager, or re-run with --system-python. |
| Journal is minutes behind | Environment=PYTHONUNBUFFERED=1 missing from the unit — re-run ./install.sh. |
Connects, then Error during processing repeatedly, no Discord alert | Correct behaviour — the connection is alive, so this is a processing fault. Read the journal for the exception. |
| No alert during a real outage | QB_DISCORD_WEBHOOK unset (see the startup warning), or the alert already fired earlier in the same outage — it is sent once per outage by design. |
EnvironmentFile ... Permission denied | File not readable by the service user; re-run ./install.sh, which sets 640 root:<user>. |
400 invalid_client from bws | EU-issued token hitting the default US endpoint. Re-run ./install.sh --bws-server-base https://vault.bitwarden.eu, which applies it as the service user. |
Security note
Section titled “Security note”The qBittorrent password was previously hardcoded in tracker_remover.py and is still
present in git history at commit b81076c. Rotate it in qBittorrent and store the new
value in BWS — removing it from the working tree does not remediate the exposure.