136 lines
9.8 KiB
Markdown
136 lines
9.8 KiB
Markdown
# 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 in `01`: **[DESIGN]** a decision or proposal; **[FACT]** read in a
|
||
> named file or measured. **Status: DRAFT** — §5 waits on S5/S6 (after the operator's test).
|
||
|
||
## 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:
|
||
|
||
1. **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.
|
||
2. **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.
|
||
3. **`media/` only** — never `felhom-data`, never the whole `userdata` (documents, roms and the app's own data stay
|
||
out).
|
||
4. **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 — PENDING (S5/S6, after the operator's test)
|
||
|
||
Predictions written: a pulled drive B stops nothing (no media app has `HDD_PATH` on B); the apps keep a dead view of B;
|
||
Plex/Jellyfin may mark items unavailable; on return, running containers do not see B until recreated.
|
||
|
||
## 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 <d>" by category, „Transmission <d>" by
|
||
directory) with that tag. Facts the link code needs:
|
||
|
||
- **The API key**: read from `/config/config.xml` — or SET at install with `SONARR__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).
|
||
- 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: `customConnections` set; public `/identity` 200; 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.
|