Skip to content

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.

FilePurpose
tracker_remover.pyThe daemon
install.shInstaller — renders the unit and wires secrets
qbt-tracker-remover.servicesystemd unit template (placeholder tokens)
requirements.txtPython dependencies
.env.exampleReference for every supported variable

All configuration is environment variables. Only QB_PASSWORD is required — the script exits 1 with a printed variable reference if it is missing.

VariableRequiredDefault
QB_PASSWORDyes
QB_DISCORD_WEBHOOKnounset → notifications disabled, service still runs
QB_HOSTno192.168.1.29
QB_PORTno21287
QB_USERNAMEnoadmin
QB_CHECK_INTERVALno2 (seconds)
QB_UNWANTED_TRACKERSnothe trackers listed in .env.example
QB_LOG_LEVELnoINFO (DEBUG adds per-torrent detail)
QB_LOG_DIRno~/Automation/logs (the .deb sets /var/log/…)

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.

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.

Terminal window
sudo apt update && sudo apt install qbt-tracker-remover
sudo qbt-tracker-remover configure

configure 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.

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:

Terminal window
sudo systemctl disable --now qbt-tracker-remover
sudo rm /etc/systemd/system/qbt-tracker-remover.service
sudo systemctl daemon-reload && sudo systemctl enable --now qbt-tracker-remover

Debian 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.

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:

Terminal window
git clone <repo-url> ~/GitRepos/infrastructure-toolkit
cd ~/GitRepos/infrastructure-toolkit/utils/qbittorrent-automation

Never edit tracker_remover.py on the host. Changes flow commit → push → git pull → restart; a local edit makes the next pull conflict.

Preview first — this writes nothing and prints the exact unit and env file it would create, with secrets masked:

Terminal window
./install.sh --dry-run

Then install. BWS_ACCESS_TOKEN is read from the environment; leave it unset on an interactive terminal and you will be prompted for it:

Terminal window
BWS_ACCESS_TOKEN='<token>' ./install.sh

That 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:

Terminal window
./install.sh --secrets envfile

On 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.

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.

FlagDefault
--user NAMEcurrent user (id -un)
--repo-dir PATHgit rev-parse --show-toplevel
--unit-name NAMEqbt-tracker-remover
--unit-dir PATH/etc/systemd/system
--config-dir PATH/etc/<unit-name>
--secrets bws|envfilebws
--bws-bin PATH/usr/bin/bws
--bws-server-base URLhttps://vault.bitwarden.eu
--venv PATH<service-home>/.venvs/<unit-name>
--system-pythonoff (a venv is created)
--python PATH/usr/bin/python3, implies --system-python
--skip-depsoff
--qb-host HOSTkeep the script’s default
--qb-port PORTkeep the script’s default
--qb-username NAMEkeep the script’s default
--check-interval SECSkeep the script’s default
--unwanted-trackers CSVkeep the script’s default
--discord-webhook URLunset (alerts disabled)
--dry-runoff
--statusreport unit state and exit
--uninstallremove the unit
--no-startenable without starting
--forceskip 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.

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:

Terminal window
sudo systemctl disable --now <old-unit-name>
Terminal window
./install.sh --status # resolved config, read back from the installed unit
journalctl -u qbt-tracker-remover -f

A healthy start looks like this, and posts nothing to Discord — a clean first connect is not a “recovery”:

Starting qBittorrent tracker remover service on Linux
Discord connection alerts enabled
Waiting 10 seconds for qBittorrent to start...
Connecting to qBittorrent at 192.168.1.29:21287
Successfully connected to qBittorrent
Checking N active downloads

If 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:

Terminal window
sudo systemctl edit qbt-tracker-remover # add: [Service] / Environment=QB_HOST=127.0.0.1
sudo systemctl restart qbt-tracker-remover
# expect exactly one ❌ embed in Discord, then silence across the 60s retries
sudo systemctl revert qbt-tracker-remover
sudo systemctl restart qbt-tracker-remover
# expect one ✅ embed reporting the outage duration

Commit and push the change, then on the host:

Terminal window
cd <checkout>
git status --porcelain # expect empty; anything here will block the pull
git pull
sudo systemctl restart qbt-tracker-remover

The 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.

Terminal window
./install.sh --uninstall

Stops, 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.

install.sh is the supported path; this is the equivalent by hand if you need it. The unit has four placeholder tokens:

Terminal window
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 error
grep -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-remover
sudo cp .env.example /etc/qbt-tracker-remover/env
sudo nano /etc/qbt-tracker-remover/env # fill in the secrets you need
sudo chown "root:$SERVICE_USER" /etc/qbt-tracker-remover/env
sudo chmod 640 /etc/qbt-tracker-remover/env
sudo systemd-analyze verify /etc/systemd/system/qbt-tracker-remover.service
sudo systemctl daemon-reload
sudo systemctl enable --now qbt-tracker-remover

Dependencies 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):

Terminal window
SERVICE_HOME=$(getent passwd "$SERVICE_USER" | cut -d: -f6) # never $HOME
sudo -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.txt
sudo -u "$SERVICE_USER" python3 -c "import qbittorrentapi, requests; print('ok')"
SymptomCause
Required environment variable 'QB_PASSWORD' is not set, unit failed after 5 triesSecrets never reached the process. Check ./install.sh --status for the real EnvironmentFile, and that the BWS secret is named exactly QB_PASSWORD.
status=200/CHDIRWorkingDirectory 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 userThe 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 venvpython3.<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 behindEnvironment=PYTHONUNBUFFERED=1 missing from the unit — re-run ./install.sh.
Connects, then Error during processing repeatedly, no Discord alertCorrect behaviour — the connection is alive, so this is a processing fault. Read the journal for the exception.
No alert during a real outageQB_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 deniedFile not readable by the service user; re-run ./install.sh, which sets 640 root:<user>.
400 invalid_client from bwsEU-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.

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.