Files
serverAppConfigs/serverAppConfigs-docs/FrankBook-Pro/qbittorrent.md
T
Frank 327e78b950 docs(vault): destroy the dead weight — 53 pages and the change diary
The vault was 155 pages / 31,210 lines, most of it measurement ledgers for
payloads that no longer run. A throughput figure for a runtime nobody operates is
not a record: AI moves faster than the vault, so the number is wrong before
anyone reads it, and a wrong number in a runbook gets believed. Per the new
purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed.

Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest):
- the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM,
  the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch
  campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two
  "Running Qwen3.6 …" pages;
- the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag)
  and the multi-device/x4-pair/hybrid/offload experiment pages;
- every vLLM page — no vLLM endpoint exists on this fleet;
- `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced
  owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff,
  self-hosted app census, the TTS survey) that were obsolete by construction;
- `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the
  page that owns the thing it governs, never to a diary.

Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras
tables keep the window, payload, runtime, reason and rollback pointer; the rate
columns are blanked. The live shapes stay verbatim — those numbers are what the
deployer and `models.yml` consume.
2026-10-19 10:56:37 -07:00

202 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: reference
title: qBittorrent
description: "The download client for the *arr stack: pinned image, shared WireGuard netns, WebUI credential recipe."
tags: [qbittorrent, arr-stack, frankbook-pro, gluetun]
timestamp: 2026-09-03T00:00:00Z
---
# qBittorrent
qBittorrent is the torrent download client for the *arr stack. The stack runs on
**FrankBook-Pro** (the Mac mini) as quadlet units inside its podman machine —
not on Dankserver. It receives download requests from Sonarr, Radarr, Lidarr,
and Readarr via category tagging.
**Host:** FrankBook-Pro (podman machine) — **Access:** WebUI on port `8080`
published by the [[Gluetun]] unit because qBittorrent shares that container's
network namespace (a netns-sharer publishes no ports of its own).
## Facts
| Item | Value |
|------|-------|
| Image | `lscr.io/linuxserver/qbittorrent:4.6.7` — **pinned**, see the pin rationale below |
| Network | `Network=gluetun.container` (torrent egress exits via the WireGuard/ProtonVPN tunnel) |
| Ports | 8080 (WebUI) and 6881 TCP+UDP published on the gluetun unit |
| Env | `PUID=0`, `PGID=0`, `TZ=America/Los_Angeles`, `UMASK_SET=022` |
| Volumes | `/Volumes/COMPLETE/qbittorrent-data:/config`, plus `/Volumes/COMPLETE` and `/Volumes/INCOMPLETE` mounted at identical paths |
| Auth | Web UI username/password (`admin` + generated password); LAN bypass not used |
## Why the 4.6.7 pin exists (2026-09-01)
Readarr's abandoned qBittorrent client rejects qB ≥ 5's **204 empty-body login
success** as `Authentication failure` — a runtime break, not just an API-create
issue. The fleet pins `lscr.io/linuxserver/qbittorrent:4.6.7` while Readarr is
in the stack. **Never unpin to `latest` while Readarr is in the fleet.**
Second trap on any version flip: qB 5.x's shutdown conf rewrite leaves a
`WebUI\Password_PBKDF2=@ByteArray(...)` line that 4.6.7 rejects — every login
then returns "invalid credentials" (the arrs included). Fix = rewrite the same
conf line with the same password while qB is stopped.
## Persistent WebUI credentials (verified 2026-08-30)
qBittorrent 5 mints a **new temporary password at every start** unless a
password is stored in `qBittorrent.conf`. That rotation silently breaks every
arr download client after each reboot. The Web-API has no password endpoint
any more (`/api/v2/app/setPassword` was removed), and the linuxserver image
has no `WEBUI_AUTH` handling — so persistence must be written to the conf
directly, in qb's own `salt:hash` format (`src/base/utils/password.cpp`:
PBKDF2-HMAC-SHA512, 100 000 iterations, 16-byte salt = 4×uint32, then
`base64(salt) + ":" + base64(hash)` as the raw value):
```python
import base64, hashlib, os
salt = os.urandom(16)
h = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000, 64)
val = (base64.b64encode(salt) + b":" + base64.b64encode(h)).decode()
# with the container STOPPED, in [Preferences]:
# WebUI\Username=admin
# WebUI\Password_PBKDF2=<val>
```
Then `podman start qbittorrent` and confirm login survives a
`podman restart` — the temp-password rotation stops the moment stored
credentials verify. The chosen value lives host-local in `/home/dank/.qbpass`
(mode 600, owner `dank`) — never in git; every arr download client and
[[Prowlarr]] reference it. (The classic `@ByteArray(salt+hash)` form is WRONG
for qb5: login 401s on it; verify() base64-decodes the two halves separately.)
## Deployment (current)
Quadlet unit `qbittorrent.container` from this repo
(`frankbook-pro/qbittorrent/quadlet/`), installed inside the podman machine at
`/home/core/.config/containers/systemd/qbittorrent.service` by the
frankbook-pro handler in `scripts/deploy-remote.sh`:
```ini
[Container]
ContainerName=qbittorrent
Image=lscr.io/linuxserver/qbittorrent:4.6.7
Network=gluetun.container
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/qbittorrent-data:/config
Volume=/Volumes/COMPLETE:/Volumes/COMPLETE
Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE
[Service]
TimeoutStartSec=900
Restart=always
```
## Gotchas (verified live)
- Temporary-password rotation on every boot is the default state of an
untouched qb5 — see the persistence recipe above; without it, all arr
download clients silently fail queue adds ("Couldn't add release … to
download queue" in the arr logs) after any restart.
- API login from `curl` needs a `Referer: http://<host>:<port>/` header
(CSRF protection) or POSTs get rejected even with a valid SID.
- Cookie-name quirk by version: qB 5.x names its WebUI session cookie
`QBT_SID_<port>`; 4.6.7 uses plain `SID`. Any automation must parse the
cookie generically (e.g. `grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*'`)
instead of hard-coding a port-suffixed name, or it fails with 403 cascades
that never reach qB as failed logins.
- Egress verification for this stack must run **inside the gluetun container**
(`podman exec gluetun wget -qO- https://ipinfo.io/json`); running the same
probe one ssh-context out returns the residential IP — a false leak alarm.
- Ensure umask is set to `022` so downloaded files are readable by arr apps.
## E2E verification after any pipeline change
Run after ANY change to the torrent pipeline — WireGuard credential rotation,
qB version change, gluetun reconfig, category re-seeding. Proves the full
grab path (tunnel egress → qB download → category save path → clean
removal). All commands from a dev box via `ssh frankbook-pro`; inside the
Mac shell export `PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"` for
`podman machine ssh`.
**Access note (2026-09-21):** the Mac's login user is **dank**@192.168.0.112
(NOT core — `core` is the podman-machine VM user, reached only
`podman machine ssh`). blankpad has key auth for `dank@` (bootstrapped
2026-09-21) and a `frankbook-pro` alias in `~/.ssh/config`. macOS sshd has a
low MaxAuthTries: when password-bootstrapping use `-o
PreferredAuthentications=password -o PubkeyAuthentication=no
-o IdentitiesOnly=yes` or the key offers exhaust the attempts before the
password is tried.
**1. Egress gate — MUST run inside gluetun:**
```bash
podman machine ssh 'podman exec gluetun wget -qO- https://ipinfo.io/json'
```
Require `org` = AS9009 (M247 — ProtonVPN Panama PA-12 config); judge by org,
never country alone. NEVER run this one ssh-context out (bare `wget` on the
Mac host): it silently returns the residential Comcast IP and reads as a
hard-gate leak that does not exist. A residential reading ⇒ re-run inside
the container before diagnosing.
**2. Login — generic cookie parse.** The QPW fetch is flaky (~1 in N returns
empty); assert length before use — an empty-password run once produced a
Forbidden loop that cost hours:
```bash
QPW=$(podman machine ssh 'cat /home/core/Containers/qbittorrent/env' | grep QBIT_WEBUI_PASSWORD | cut -d= -f2)
[ ${#QPW} -eq 20 ] || exit 1
SID=$(curl -si --data-urlencode "username=admin" --data-urlencode "password=$QPW" \
http://127.0.0.1:8080/api/v2/auth/login \
| grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*' | head -1)
curl -b "$SID" … # pass the WHOLE match
```
WebUI from the LAN: `http://192.168.0.112:8080` (published on the gluetun
unit). Username `admin`; password = the machine-local `QBIT_WEBUI_PASSWORD`
(20 chars, generated once at provisioning by the deploy handler — never in
the repo). Parse-bug 403s never reach qB as failed logins — no ban accrues;
server-side truth is `/Volumes/COMPLETE/qbittorrent-data/qBittorrent/config/qBittorrent/logs/qbittorrent.log`
(`WebAPI login success/failure` + source IP). Repeated REAL failures accrue
toward qB's 5-failures/3600 s IP ban.
**3. Add a legal test torrent.** Blender open-movie torrents (~300 MB, e.g.
Big Buck Bunny) via `POST /torrents/add` with the raw `.torrent` file
(`-F "file=@…"`) and `category=Finished Torrents`. Never leave a real-looking
grab behind.
**4. Poll and verify.** Poll `GET /torrents/info` (~20 s): state
`downloading` → `moving` (INCOMPLETE→COMPLETE move) → `stalledUP`/`uploading`
at progress=1. `save_path` must be `/Volumes/COMPLETE/Finished Torrents`
(qB 4.x auto-creates unknown categories at DefaultSavePath; 5.x did NOT
inherit category paths — one more reason for the 4.6.7 pin). Confirm on
disk: `ls "/Volumes/COMPLETE/Finished Torrents/"`. Throughput ~17 MB/s is
normal for the tunnel.
**5. Cleanup — delete only your artifact:**
```bash
curl -s -o /dev/null -w "%{http_code}" -b "$SID" \
--data-urlencode "hashes=<literal-hash>" \
--data-urlencode "deleteFiles=true" \
http://127.0.0.1:8080/api/v2/torrents/delete # expect 200
```
Target the known hash literally — the user's real grabs share the directory
and MUST be untouched. Verify: hash absent from `/torrents/info`, your dir
gone, all other entries intact.
**Version-coupling constraints:** qB is pinned to 4.6.7 (Readarr's EOL
client rejects qB ≥ 5's `204` login at runtime — see the pin rationale
above). Do not unpin while Readarr exists. After ANY qB version flip,
regenerate the `WebUI\Password_PBKDF2` line fresh (same password, 100k
iterations) — a 5.x-written hash line makes 4.6.7 reject every login.
Rewrite while qB is stopped.
## See Also
- [[ARR-Stack]] — full *arr stack documentation
- [[Gluetun]] — the WireGuard tunnel this container shares