14 KiB
15 — The media library: media apps on every drive
What this is: how the media apps (download → library → player) share the household's drives — the folder layout, the links between the apps, what happens when a drive comes or goes, and what backup, removal and export must learn. No document covered this before 2026-10-14 — itself a finding: the four templates were written one by one and never tested together (R-1014). Rulings:
09§3 decisions 237–241. Measured on Tester 1:audits/media-all-drives-2026-10-14/. Marks as in01: [DESIGN] a decision or proposal; [FACT] read in a named file or measured. Status: DRAFT — measured end to end on Tester 1; the operator's layout decision (§2, STATUS D9) is open.
1. The problem
[FACT] Live catalog (main 8ee687e): qBittorrent saves to ${USERDATA_PATH}/media/torrents, Transmission to
media/transmission; Sonarr and Radarr look in ${USERDATA_PATH}/downloads. A download never reaches a library.
[FACT] Even with matching folders, Sonarr/Radarr bind media/tv (or movies) and downloads as TWO bind mounts:
link(2) answers EXDEV across two mount points on one filesystem, so every import is a full copy (S1:
ln: … Cross-device link; two inodes). [FACT] Each app binds ONE drive (HDD_PATH, locked after deploy,
stacks/deploy.go withPathVars); only FileBrowser follows every drive (web/handlers.go buildFileBrowserPaths).
2. The layout [DESIGN, measured on Tester 1]
Each media app binds, per enrolled drive, that drive's userdata/media at the same path inside the container as
in the guest:
/mnt/felhom-drives/<drive>/userdata/media → /mnt/felhom-drives/<drive>/userdata/media (rw; :ro for players)
media/tv/<Show>/… Sonarr library media/torrents/{tv,movies}/… qBittorrent
media/movies/<Film>/… Radarr library media/transmission/{tv,movies}/… Transmission
The four reasons:
- One bind per drive keeps download and library on one mount point — the import is a hardlink: one file, two names, no extra space. [FACT] S2: Transmission and qBittorrent, drive B → link count 2, same inode; S3: drive A the same.
- The same path everywhere — no app translates another app's path (no „remote path mapping"), and moving an installed app from one drive to many later ADDS binds without rewriting the paths Sonarr and Radarr have stored.
media/only — neverfelhom-data, never the wholeuserdata(documents, roms and the app's own data stay out).- Today's host folders (
media/torrents,media/transmission) — an installed download app keeps its files.
[FACT, and a correction of the brief] media/ is not photo-free: the skeleton puts photos, books,
audiobooks, music, comics, podcasts there too (appbackup/userdata.go + the catalog-derived set), and Immich,
calibre-web, audiobookshelf and navidrome use them. With reason 1, a download app needs ONE mount that holds both its
download folder and the libraries — so binding media/tv and media/torrents separately brings EXDEV back. Open
question for the operator (two options):
- A — bind
media/whole, rw for the download apps. Simple; measured working. Cost: qBittorrent, Transmission, Sonarr and Radarr can write into the family's photos and music folders. - B — move the video library into its own folder (e.g.
userdata/video/{tv,movies,torrents,transmission}) and bind only that. Download apps never see photos. Cost: a one-time move on boxes that already have files there, and every template and skeleton entry changes. - CC recommends B for the build: the download apps are the internet-facing ones, and „the box cannot touch your photos" is a promise worth one move. A is fine for the Tester 1 rig.
3. Which apps are „media apps" [DESIGN, proposal]
Sonarr, Radarr, qBittorrent, Transmission (download/library, rw); Plex, Jellyfin, Emby (players, :ro). Others
(Lidarr, Readarr, SABnzbd, NZBGet, navidrome, audiobookshelf…) later, each with its own rule. A media app carries a
marker in .felhom.yml (e.g. media_library: rw|ro); the controller builds its per-drive binds — templates never
spell drive names (the drill used plain host paths only to stay out of the classifier, §7).
4. The drive-absent gate and the restart notice
[FACT] FileBrowser's pattern is the one to extend: bind every drive's userdata/, skip a drive that is not a live
mountpoint, recreate the container when the set changes. [FACT] The app gate (web/intermediary.go
planDriveGates/appsOnStoragePath) stops and restarts only apps whose HDD_PATH equals the absent drive; at boot it
waits for THAT drive's bind only (seen after the 17:25 power cut: the four media apps were recreated onto
media_a with no check of media_b). A media app depends on EVERY drive it binds.
[FACT] What the dashboard says when a drive is added (A5): the drive card „Aktív", „Nincs alkalmazás ezen a
tárolón" — nothing about apps that need a restart. [FACT] Docker binds are resolved at container START: a running
container does not see a drive added later (guest propagation is shared,slave, the container's bind is rprivate).
[FACT] Today's „restart" restarts nothing (R-1026): RestartStack = compose up -d, a no-op when the compose did
not change; it logged success in 0.1 s while the containers kept their start time. Stop + start works. The notice of
decisions 238–239 needs a real restart (up -d --force-recreate once the per-drive binds are in the compose).
[DESIGN] The restart notice (decision 238) fires on: a drive added, a drive removed, and — pending S6 — a drive that came back. Each drive card carries „apps restarted since this drive was added: yes/no" with the button (decision 239).
5. A drive is pulled / comes back [FACT, measured on Tester 1 2026-10-14, audits/media-all-drives-2026-10-14/S5-S6/]
S5 — Media B's disk detached at 20:10:23 while six media apps ran (Sonarr, Radarr, qBittorrent, Transmission,
Plex, Jellyfin; all with HDD_PATH on Media A):
- The host saw the disk go in 2 s and unmounted its own mount, but the guest kept a dead ext4 mount of
/mnt/felhom-drives/media_b:mountpoint= yes, every read = Input/output error (not an empty folder — so nothing can be written onto the guest's own disk there, which the brief feared). - The controller marked the drive disconnected at 20:10:47, stopped 0 apps (none keys on Media B), pushed
storage_disconnected(error); the hub got it the same second and mailed the operator;health_degradedfollowed at 20:13. It also tried to rebuild the skeleton ON the dead mount („chmod … input/output error") — the gate asks „is it a mountpoint?", and a dead mount says yes (R-1038). - The dashboard: the card reads „Media B — Leválasztva 2026. 10. 14. 20:10" with „Csatlakoztatás"; it does not say that six apps use the drive. No banner on the start page.
- The apps did not crash; all read I/O errors on B. Sonarr/Radarr mark root folder B
accessible=falsebut their own health page stays EMPTY. qBittorrent's B torrents sat queued and did not notice. - Plex and Jellyfin removed nothing in 30 minutes. Plex scans only when asked (R-1037); Jellyfin's real-time monitor got errors, not deletions. A scan during an outage is the risk; with Plex's „empty trash after every scan" on by default such a scan would drop B's items — not measured, deliberately.
S6 — the same volume re-attached to the same slot at 20:41:00: the agent re-mounted it (sde) and bound it under
the parent within 24 s, ON TOP of the dead mount (two stacked mounts); the controller logged „drive RETURNED …
restarted gate-stopped apps" (there were none) and storage_reconnected. Every running container still saw the dead
mount, FileBrowser included („ensured running without recreate"). The dashboard's app „restart" changed nothing
(same start time, still I/O error — R-1026). Stop + start fixed all six apps at once; files and all 13 hardlink pairs
intact. FileBrowser is protected — the household cannot stop or start it (403), so it stays broken (R-1038).
What the restart notice (decisions 238–239) must therefore cover [DESIGN]: a drive added, a drive removed, and a
drive that came back — the last is the one where the household is told nothing today and the apps are silently half
blind. The restart is stop + start (or up -d --force-recreate) of every container that binds the drive, FileBrowser
included, and the box — not Sonarr — says that a root folder is unreachable.
6. The links [FACT, measured calls in audits/media-all-drives-2026-10-14/tools/wire.py]
Per drive: a qBittorrent category tv-<d>/movies-<d> (save path on that drive, incomplete path on that drive); a
Sonarr/Radarr root folder, a tag, and two client entries („qBittorrent " by category, „Transmission " by
directory) with that tag. Facts the link code needs:
- The API key: read from
/config/config.xml— or SET at install withSONARR__AUTH__APIKEY/RADARR__AUTH__APIKEY(measured: 200 with that key, 401 with another). Setting it is the cleaner design. - qBittorrent must run in automatic torrent management (
auto_tmm_enabled), or a category's save path is ignored and every torrent lands on the default drive. - A Transmission entry must clear the default category — Sonarr/Radarr refuse „Cannot use Category and Directory".
- Transmission has ONE incomplete folder for all torrents; it must be off, or every drive-B download is a cross-drive copy at completion.
- Transmission answers by its container name (
transmission:9091) despite its Host allow-list (measured). - A show with no tag gets no download app when every entry carries a tag. The UI must make the drive choice set both the root folder AND the tag; S3: a mismatch (root B, tag A) works but makes a full copy on B while A seeds.
- The OnlyOffice revoke-on-stop rule must NOT apply (
integrations/lifecycle.go:10-13): it would delete the download entries whenever a download app restarts. And the OnlyOffice links themselves have no live proof since Proxmox (R-1018). - Prowlarr holds the indexers ONCE and pushes them (Settings → Apps, „Full Sync”) to Sonarr and Radarr by container
name (
http://sonarr:8989,http://radarr:7878,prowlarrUrl http://prowlarr:9696); both entries pass Prowlarr's own connection test; the sync command completes (measured with zero indexers — the push of a real one waits for the household's own). It needs no drive and no media folder. FlareSolverr only for an indexer that asks for it. - Call the apps with
Host: <container name>: once the household sets „Allowed hosts”, Sonarr/Radarr answer a bare IP with 400 „Invalid Hostname” (measured). - A setting made through Transmission's RPC is lost on a power cut (it writes
settings.jsononly on a clean exit):incomplete-dir-enabled=falsewas back totrueafter the 17:25 cut. The link code writes it intosettings.jsonat start (as the template's entrypoint does for port forwarding), not by RPC alone. - Sign-in (R-1030): with „Disabled for Local Addresses” an outside visitor IS asked (the tunnel keeps the public
address in
X-Forwarded-For), but the home network and every other container get in with no sign-in, the API key included. The box seeds „Enabled”. - Plex and Jellyfin are NOT linked: they watch folders. Indexers stay the household's choice.
- Plex behind the tunnel needs
ADVERTISE_IP=https://<sub>.<domain>:443/so plex.tv hands apps a reachable address (measured:customConnectionsset; public/identity200; anonymous library calls 401).
7. Backup classes for a per-drive media bind
[FACT] An unlisted writable ${…_PATH} bind is MANDATORY (every tier, off-site included); a plain host path is in
no capture set (stacks/classify_binds.go classifyRoot). Measured over the live templates: Transmission's unlisted
watch bind goes off-site; the download folders are optional, so a second drive gets a nightly copy of every
download (R-1019). [DESIGN] The per-drive media bind is declared excluded for every media app — the family's
media is no single app's data; whether the library gets its own household-level backup is a separate decision.
8. Removal and export assume one drive per app
[FACT] Removal with „delete data" deletes every bind under the app's HDD_PATH, literal and :ro ones included —
removing a reader Plex deleted drive A's whole userdata/media (R-1027). [DESIGN] A media app's removal never
deletes the shared library; it deletes only its own config.
Call sites that assume ONE HDD_PATH/USERDATA_PATH per app (controller main acbbf4b):
stacks/deploy.go withPathVars; stacks/manager.go:1566; stacks/delete.go appHDDPath, hddPathForRemoval,
ExportDataMounts, ParseComposeUserdataMounts; stacks/kept.go:226; stacks/migrate.go MigrateApp (rewrites
HDD_PATH); web/intermediary.go appsOnStoragePath; web/handlers.go appsUsingPath; backup/file_legs.go,
backup/backup.go:431, backup/offbox_capture.go, backup/offbox_placement.go, backup/offbox_restore.go;
report/dr_recipe.go:110; appexport/provider.go.
9. From one drive to many
An installed media app keeps its HDD_PATH (its config's unit lives there) and gains binds for the other drives at its
next recreate. Because the paths are the same everywhere (§2 reason 2), nothing Sonarr or Radarr stored changes. A
household's existing files under media/torrents stay where they are.