docs(arr-chain): Prowlarr feed contract cracked, qB PBKDF2 persistence recipe, chain proof

This commit is contained in:
Frank
2026-08-30 00:38:09 -07:00
parent a04c9d7d02
commit 3626c7f40d
3 changed files with 134 additions and 17 deletions
+61 -11
View File
@@ -1,6 +1,8 @@
# Prowlarr
Prowlarr is the indexer management layer for the *arr stack. It aggregates indexers (torrent and Usenet) and exposes a unified API that Sonarr, Radarr, Lidarr, and Readarr consume.
Prowlarr is the indexer management layer for the *arr stack. It hosts the
indexer definitions and exposes them as Torznab feeds and downloads;
Sonarr, Radarr, Lidarr, and Readarr query those feeds and receive results.
**Host:** Dankserver — **Hostname:** `prowlarr.dankserver.local` — **Docs:** https://wiki.servarr.com/prowlarr
@@ -11,14 +13,35 @@ Prowlarr is the indexer management layer for the *arr stack. It aggregates index
| Image | `lscr.io/linuxserver/prowlarr:latest` |
| Port | 9696 (host) → 9696 (container) |
| Min RAM | 256 MB |
| Auth | API key (Settings → General) |
| Auth | Forms (web UI) + API key (`/api/v1/...` and feed routes) |
## Configuration
## Live wiring (verified end-to-end 2026-08-30)
1. Add indexers: Settings → Indexers → Add.
2. Test each indexer with the **Test** button.
3. Assign tags (e.g., `movies`, `tv`, `music`) to indexers for routing.
4. Add download client: Settings → Download Clients → Add qBittorrent.
Indexer queries flow **through** Prowlarr; a grab hands off to the querying
app's own qBittorrent download client. Feeds are per-indexer:
```
http://prowlarr:9696/{indexerId}/api?apikey={GLOBAL_KEY}&t=caps|search|tvsearch|movie|music|book&...
http://prowlarr:9696/{indexerId}/download?link=...&file=... (torrent handoff)
```
| App | Indexer row | Prowlarr id | Categories | Status |
|---|---|---|---|---|
| Radarr | Prowlarr-YTS | 7 | [2000] | ✅ grabbed + imported 2160p Dark Knight 2026-08-30 |
| Sonarr | Prowlarr-Nyaa | 4 | [5000] | ✅ connection validated (202) |
| Lidarr | Prowlarr-Nyaa | 4 | [3000] | ✅ connection validated (202) |
| Readarr | Prowlarr-Byrutor | 3 | [7000] | indexer valid; download client parked |
The global API key is in `config.xml` (`<ApiKey>`); per-application keys are
only used by Prowlarr's own sync calls, NOT by the feeds. The arr download
clients authenticate to qBittorrent with the permanent WebUI password stored
in `/home/dank/.qbpass` (mode 600) on Dankserver.
**Readarr caveat:** Readarr is abandoned upstream (last release 0.4.x); its
qBittorrent client cannot authenticate against qB 5 (`Authentication
failure` on the connection test while Sonarr/Lidarr/Radarr pass with the
same credentials). The books pipeline is parked pending a Readarr successor;
the indexer row itself is healthy.
## Docker Compose
@@ -40,11 +63,38 @@ prowlarr:
networks: [appnet]
```
## Gotchas
## Gotchas (all verified live 2026-08-30)
- Prowlarr database grows with indexer data. Set up periodic vacuum/backup.
- Indexer sync has a 15–60 minute delay. Search results may lag.
- Rotate API key: update it in every arr app.
- **The feed route is `/{indexerId}/api`.** There is no `/torznab/{id}/feed`
— that path (and any other unmatched path) hits the Forms-auth catch-all
and gets `302 → /login?returnUrl=…`. An arr following the redirect parses
the login HTML and fails with a cryptic
`'/' is an unexpected token … Line 54` XML error that looks like an auth
problem. It is a path typo. Source: `NewznabController`
(`[HttpGet("{id:int}/api")]` + absolute
`/api/v1/indexer/{id}/newznab`).
- **The arr's Torznab joiner is `TrimEnd(baseUrl,'/') + apiPath` with NO
separator.** `baseUrl="…/7/" + apiPath="api"` silently becomes `…/7api`.
Either keep the leading slash on `apiPath` (`"/api"`) or end the baseUrl
with `/api`.
- Categories in the indexer row must be a subset of what the feed's `caps`
advertises; extras log "does not support the following categories" and
some feeds reject them.
- The v1 API collection is `/api/v1/applications` (not `/api/v1/app`);
GET responses carry `fields: []` — real settings values live in
`prowlarr.db` → `Applications.Settings` JSON (read-only inspection).
- Instance name/`InstanceName=Prowlarr` in `config.xml` does NOT prefix the
API/feed routes on this build (they answer at root; the `/Prowlarr/...`
prefix 302s to the SPA).
- **Application auto-sync cannot manage these rows** (they carry
`SyncLevel=2`): `POST /api/v1/applications/test` fails with
`Unable to cast object of type 'NzbDrone.Core.Thing…'` — Prowlarr v1
cannot deserialize Radarr 6.3's newer response types. The manual Torznab
rows above are therefore the real integration; sync neither duplicates
nor clobbers them.
- Prowlarr runs `AuthenticationMethod=Forms` + `AuthenticationRequired=Enabled`
healthily — the DryIoc crash combo was `None`+`Enabled` only (see
[[radarr]] auth notes).
## See Also
@@ -15,10 +15,39 @@ qBittorrent is the torrent download client for the *arr stack on Dankserver. It
## Configuration
1. Change the default Web UI password (Settings → Web UI).
2. Enable "No auth mode for LAN" for arr app connectivity (Settings → Web UI → uncheck "HTTP authentication required for clients").
3. Create download categories: `movies`, `tv`, `music`, `books`.
4. Set max active downloads: Settings → Downloads (recommended: 5 global, 3 per category).
1. Keep Web UI authentication ON (LAN bypass not needed — the arr apps
authenticate with the API) and give them a permanent password.
2. Create download categories per app (`radarr`, `sonarr`, `lidarr`,
`readarr`): the arr download clients route category-per-app, and Prowlarr
feeds hand grabs to each app's own client.
3. Set max active downloads: Settings → Downloads (recommended: 5 global,
3 per category).
## 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 classic `@ByteArray(salt+hash)` form is WRONG for
qb5: login 401s on it; verify() base64-decodes the two halves separately.)
## Docker Compose
@@ -41,9 +70,14 @@ qbittorrent:
networks: [appnet]
```
## Gotchas
## Gotchas (verified live)
- qBittorrent Web UI password changes on first run (check `config/qBittorrent.conf`).
- 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.
- Ensure umask is set to `022` so downloaded files are readable by arr apps.
- Monitor bandwidth limits to avoid saturating the network.
+33
View File
@@ -11,6 +11,39 @@ timestamp: 2026-09-14
# Bundle Change Log
## 2026-08-30 (late) — download chain proven end to end; qB credentials persisted
- **Cracked the arr↔Prowlarr feed contract**: route is `/{indexerId}/api`
(Prowlarr `NewznabController`; absolute `/api/v1/indexer/{id}/newznab`),
auth = the Prowlarr **global** key. The previously hand-rolled
`/torznab/{id}/feed` paths were Forms-auth catch-alls returning
`302 → /login` — which the arrs XML-parsed as "unexpected token '/'".
A source-verified librarian answer settled it in one shot.
- **Re-pointed all four arr indexer rows** (Radarr→YTS id7,
Sonarr/Lidarr→Nyaa id4, Readarr→Byrutor id3) via each app's own API;
every PUT passed the app's live connection validator (202); RSS enabled.
- **First real grab**: `MoviesSearch` → 3 YTS releases → *The Dark Knight
(2008) 2160p* (7.5 GiB) → qBittorrent 100 % → **imported**
(`hasFile: true`, 8,078,103,833 B at `/movies/The Dark Knight (2008)`,
hardlinked from the shared `/mnt/entropy` filesystem), queue empty.
- **Prowlarr Application auto-sync found broken by version skew**
(`Unable to cast object of type 'NzbDrone.Core.Thing…'` against Radarr
6.3) — the manual rows are the integration; documented.
- **qBittorrent permanent credentials installed**: the WebUI temp password
rotated on every boot (it had silently broken the arr clients once this
session). qb5 removed the API password endpoint and the lscr image has
no env support — wrote `WebUI\Password_PBKDF2` (source-verified
`base64(salt):base64(PBKDF2-HMAC-SHA512/100k)` form) into the conf with
the container stopped; login survives `podman restart` (204). All arr
download clients updated to it; value kept host-local in
`/home/dank/.qbpass` (0600), never in git.
- **Readarr parked with reason**: abandoned upstream; its qB client cannot
authenticate against qB 5 while three sibling apps pass with identical
credentials. Indexer row healthy; books pipeline awaits a successor.
- **Docs**: rewrote `Dankserver/prowlarr.md` (live wiring + six verified
gotchas incl. the joiner bug and the 302-masquerading-as-auth failure)
and `Dankserver/qbittorrent.md` (persistent-credential recipe, Referer
CSRF note).
## 2026-08-29 (late) — guarded-host sign-in fixed end to end
- **Diagnosed** both user symptoms: `Invalid next URL` = the flow being entered with an absolute cross-host `next=` (open-redirect guard rejects at completion); phone "hostname could not be found" = the public DNS zone had **no `auth` A record** (added via Porkbun API, `auth A 71.227.227.172 TTL 300`).
- **Disproved meta-header routing** live (full meta-header set still 404s; matching `Host` alone answers). The 2026.8 embedded outpost routes solely on the original `Host` vs each proxy provider's `external_host`.