Media refresh drivers: the two scan doors become commands #140

Merged
dank merged 5 commits from feat/media-refresh-drivers into main 2026-11-04 12:15:38 -08:00
5 Commits
Author SHA1 Message Date
dank e519f6b554 docs(vault): the promo-clip hold is purged; merge main's purge record 2026-11-04 11:59:00 -08:00
Frank 1e0247f29e Merge main into feat/media-refresh-drivers (main moved: the purge record) 2026-11-04 11:58:36 -08:00
Frank 24d793eb7d docs(vault): the promo-clip hold is 31 clips and one sidecar 2026-11-04 11:30:07 -08:00
Frank 95375683d5 docs(vault): the scan pages name their drivers, and a class no screen sees
Plex.md and Jellyfin.md keep every law and point at the driver that now carries
them, so the procedure has one owner and the pages keep the failure signatures.
Jellyfin.md also records where `jellyfin.db` actually lives on this host (the
quadlet's config bind, not the stock path) and demotes `immutable=1` on the
`ApiKeys` read to the `-shm` fallback, because the database is WAL and
`immutable` skips the log. Plex.md gains the live section keys and the
`scanned_at == scannedAt` fact.

Media Relayout.md's hand-off after an apply names both drivers and states the
ordering law: Jellyfin first and alone, since its one door walks every library
while Plex forks a scanner per section, and the array is what both share.

Media Duplicate Prune.md gains the guard the 2026-11-04 hold taught: 32 files
carrying real film titles were 8-20-second h264 clips (49 MiB, 25 of them in a
`_release-group watermarks/` folder), invisible to every duplicate screen
because nothing shares their size and the feature-length guard hides them. The
census that finds the next batch is a duration floor, not a hash.
2026-11-04 11:29:03 -08:00
Frank e7d7c6eea2 feat(scripts): the two media scan doors become commands
Both media servers' scan procedure lived only as a hand-written curl block in
the vault, and that block has bitten this fleet twice: the escaped-quote form
that reads exactly like a dead API key, and a config `url` line meeting a
command-line URL so curl quietly performs a second transfer and dumps the whole
task table on your stdout. Jellyfin's door is now
`scripts/media-refresh-jellyfin.py` (trigger / status / wait) and Plex's is
`scripts/media-refresh-plex.sh` (trigger / status);
`tests/media_refresh.bats` pins the contracts an operator observes, against a
stub and with no live server.

The Jellyfin `wait` encodes the settle law instead of a poll count: the Scan
Media Library task must be seen Running — or finish while watching, or already
pass the `--after` it is given — before any quiet read counts toward settled.
A watcher that saw no Running otherwise reports the pre-scan library as the
fresh one, which is how a rename looks applied while nothing has rescanned.
Plex's `status` needs no token at all: `section_locations.scanned_at` carries
the same epoch the API prints as `scannedAt` (measured on section 1 today).

Neither driver lets a secret reach argv or a URL: the Jellyfin key rides a
mode-600 curl config as the quoted MediaBrowser form, and the Plex token is
read inside the container, where the HTTPD actually answers, into a config file
that is shredded there.
2026-11-04 11:29:03 -08:00