The 2026-11-04 promo-clip hold left both media servers indexing paths that no
longer exist, so the reconcile step had to run. The only door for it was a
hand-written curl -K block copied out of the vault — the same block that has
bitten this fleet twice (the \" vs \\" quoting 401, and curl silently
performing a second transfer when a config url meets a command-line URL). Both
servers' scan procedures are now commands, and the vault pages point at them.
What
scripts/media-refresh-jellyfin.py — trigger / status / wait.
The API key is read from ~/.config/secrets/jellyfin into a mode-600 curl
config and reaches the wire only as Authorization: MediaBrowser Token="…";
it never enters argv (/proc/<pid>/cmdline is world-readable) and never the
URL.
trigger accepts only 204, retries the identical request otherwise, and
refuses to fire a second refresh while one is running: a second accepted
refresh moves Scan Media Library to Cancelling, after which every 204 is
accepted and discarded. --force overrides for the documented restart case.
wait calls the library settled only on the page's rule: the Scan Media Library task seen Running (or a run that finished while it watched, or an --after the task already passes) FIRST, then --require-quiet consecutive
reads with no scan and no database writes. Without a start sighting it refuses
by name rather than reporting the pre-scan library as fresh — and ScheduledTasks is the only honest progress read: RefreshStatus on the
virtual-folder record stayed Idle through a scan verifiably at 75 %, and GET /Library/ScanStatus is 404 on 12.4.
status --after <epoch> (a journal mtime) answers "has the scan reached my
moves" from the task's EndTimeUtc and exits non-zero while it is owed; recentMedia counts BaseItems.DateModified rows in jellyfin.db, opened mode=ro (the DB is WAL; immutable=1 would skip the log). The database path
is discovered, because on this host it is the quadlet's config bind at /home/dank/Containers/jellyfin/config/data/, not the stock location.
PMS's HTTPD answers nothing to a request from another netns (host and 10.89.x.x both read as "empty reply"), so the runner is podman cp-shipped
into the container and rides plain http://127.0.0.1:32400 from inside. No
quoting chain survives ssh → podman exec → sh -c.
PlexOnlineToken is read from the container's Preferences.xmlinside the
container into a mode-600 curl config there — the token never crosses the
host, never argv, never the URL. 000 (server warming) retries with a
parameterised sleep instead of restarting anything.
status needs no token at all: section_locations.scanned_at in the library
database carries the same epoch the API prints as scannedAt — verified live
(section 1: 2026-11-04T18:31:33Z in the DB, 1793817093 from the API).
tests/media_refresh.bats — 17 tests over the contracts an operator
observes, no live server. Jellyfin: the driver authenticates with the MediaBrowser form and puts no key in argv; a non-204 retry loop names the code
it saw; a missing credential is refused before any request; wait returns only
after the library reads quiet repeatedly, and refuses outright when it never saw
the scan start; status --after refuses to certify a scan that predates the
moves. Plex: the container token reaches X-Plex-Token and never argv or the
URL, and a section with no location, an unreadable database and an un-answered 000 all fail by name.
Docs
Dankserver/Plex.md — the driver is the door; adds the live section keys and
the scanned_at == scannedAt fact; keeps every law (one section, the netns
behaviour, Preferences.xml vs PlexHTTPToken, the literal
backslash-newline in a quoted printf).
Dankserver/Jellyfin.md — the driver is the door; records where jellyfin.db
actually lives on this host, and corrects the ApiKeys read to plain mode=ro with immutable=1 demoted to the -shm fallback.
Dankserver/Media Relayout.md — the hand-off after an apply now names both
drivers and states the ordering law: Jellyfin first and alone, because its one
door walks every library and Plex forks a scanner per section.
Dankserver/Media Duplicate Prune.md — a new guard: 32 promo clips in Movies
(25 in _release-group watermarks/) were 8–20-second h264 files carrying real
film titles; no duplicate screen can see them, so the census that finds them is
a duration floor.
Verification
./tests/run.sh — full suite green, including the new seam (auto-discovered by tests/run.sh's bats *.bats).
Live on Dankserver: trigger printed refresh accepted (204), and status
then read {"scan": true, "taskState": "Running", "lastScan": null, "recentMedia": 0, "scannedAfter": false} with exit 1 and "a refresh is still owed" — that is
the never-completed EndTimeUtc signature the page describes, now surfaced
instead of guessed at. Before the trigger, Plex status --library 1 --after <journal mtime> had returned the same verdict from the database, and media-verify.py replayed promo-hold.jsonl against both databases, finding
the same stale rows in each — the rule that says a rescan is owed.
The end-to-end chain (Jellyfin to quiet, then Plex section 1, then the journal
replay against both DBs) is running on the host; its log will be posted to this
PR as a comment when it finishes.
Not deployed by this PR: the drivers are new files; nothing in the deploy plan
touches a running service.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed and tested in the build worktree /tmp/wt-deploy)
## Why
The 2026-11-04 promo-clip hold left both media servers indexing paths that no
longer exist, so the reconcile step had to run. The only door for it was a
hand-written `curl -K` block copied out of the vault — the same block that has
bitten this fleet twice (the `\"` vs `\\"` quoting 401, and curl silently
performing a second transfer when a config `url` meets a command-line URL). Both
servers' scan procedures are now commands, and the vault pages point at them.
## What
**`scripts/media-refresh-jellyfin.py`** — `trigger` / `status` / `wait`.
- The API key is read from `~/.config/secrets/jellyfin` into a mode-600 curl
config and reaches the wire only as `Authorization: MediaBrowser Token="…"`;
it never enters argv (`/proc/<pid>/cmdline` is world-readable) and never the
URL.
- `trigger` accepts only `204`, retries the identical request otherwise, and
refuses to fire a second refresh while one is running: a second accepted
refresh moves `Scan Media Library` to `Cancelling`, after which every `204` is
accepted and discarded. `--force` overrides for the documented restart case.
- `wait` calls the library settled only on the page's rule: the `Scan Media
Library` task seen `Running` (or a run that finished while it watched, or an
`--after` the task already passes) FIRST, then `--require-quiet` consecutive
reads with no scan and no database writes. Without a start sighting it refuses
by name rather than reporting the pre-scan library as fresh — and
`ScheduledTasks` is the only honest progress read: `RefreshStatus` on the
virtual-folder record stayed `Idle` through a scan verifiably at 75 %, and
`GET /Library/ScanStatus` is 404 on 12.4.
- `status --after <epoch>` (a journal mtime) answers "has the scan reached my
moves" from the task's `EndTimeUtc` and exits non-zero while it is owed;
`recentMedia` counts `BaseItems.DateModified` rows in `jellyfin.db`, opened
`mode=ro` (the DB is WAL; `immutable=1` would skip the log). The database path
is discovered, because on this host it is the quadlet's config bind at
`/home/dank/Containers/jellyfin/config/data/`, not the stock location.
**`scripts/media-refresh-plex.sh`** — `trigger --library <key>` / `status --library <key> --after <epoch>`.
- PMS's HTTPD answers nothing to a request from another netns (host and
`10.89.x.x` both read as "empty reply"), so the runner is `podman cp`-shipped
into the container and rides plain `http://127.0.0.1:32400` from inside. No
quoting chain survives `ssh → podman exec → sh -c`.
- `PlexOnlineToken` is read from the container's `Preferences.xml` *inside* the
container into a mode-600 curl config there — the token never crosses the
host, never argv, never the URL. `000` (server warming) retries with a
parameterised sleep instead of restarting anything.
- `status` needs no token at all: `section_locations.scanned_at` in the library
database carries the same epoch the API prints as `scannedAt` — verified live
(section 1: `2026-11-04T18:31:33Z` in the DB, `1793817093` from the API).
**`tests/media_refresh.bats`** — 17 tests over the contracts an operator
observes, no live server. Jellyfin: the driver authenticates with the
`MediaBrowser` form and puts no key in argv; a non-204 retry loop names the code
it saw; a missing credential is refused before any request; `wait` returns only
after the library reads quiet repeatedly, and refuses outright when it never saw
the scan start; `status --after` refuses to certify a scan that predates the
moves. Plex: the container token reaches `X-Plex-Token` and never argv or the
URL, and a section with no location, an unreadable database and an un-answered
`000` all fail by name.
## Docs
- `Dankserver/Plex.md` — the driver is the door; adds the live section keys and
the `scanned_at == scannedAt` fact; keeps every law (one section, the netns
behaviour, `Preferences.xml` vs `PlexHTTPToken`, the literal
backslash-newline in a quoted `printf`).
- `Dankserver/Jellyfin.md` — the driver is the door; records where `jellyfin.db`
actually lives on this host, and corrects the `ApiKeys` read to plain
`mode=ro` with `immutable=1` demoted to the `-shm` fallback.
- `Dankserver/Media Relayout.md` — the hand-off after an apply now names both
drivers and states the ordering law: Jellyfin first and alone, because its one
door walks every library and Plex forks a scanner per section.
- `Dankserver/Media Duplicate Prune.md` — a new guard: 32 promo clips in `Movies`
(25 in `_release-group watermarks/`) were 8–20-second h264 files carrying real
film titles; no duplicate screen can see them, so the census that finds them is
a duration floor.
## Verification
- `./tests/run.sh` — full suite green, including the new seam (auto-discovered by
`tests/run.sh`'s `bats *.bats`).
- Live on Dankserver: `trigger` printed `refresh accepted (204)`, and `status`
then read
`{"scan": true, "taskState": "Running", "lastScan": null, "recentMedia": 0,
"scannedAfter": false}` with exit 1 and *"a refresh is still owed"* — that is
the never-completed `EndTimeUtc` signature the page describes, now surfaced
instead of guessed at. Before the trigger, Plex `status --library 1 --after
<journal mtime>` had returned the same verdict from the database, and
`media-verify.py` replayed `promo-hold.jsonl` against both databases, finding
the same stale rows in each — the rule that says a rescan is owed.
- The end-to-end chain (Jellyfin to quiet, then Plex section 1, then the journal
replay against both DBs) is running on the host; its log will be posted to this
PR as a comment when it finishes.
Not deployed by this PR: the drivers are new files; nothing in the deploy plan
touches a running service.
---
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed and tested in the build worktree `/tmp/wt-deploy`)
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.
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.
Reconcile chain ran on the live fleet, 2026-11-04 19:17:51Z → 19:46:59Z, after the promo-clip hold (32 renames) landed in the SSD-tier view. Durable log: /mnt/entropy/.media-campaign/rescan-20261104.log.
scripts/media-refresh-jellyfin.py trigger → 204 accepted; … wait --interval 20 --require-quiet 3 --timeout 5400 polled ~28 min of scan (scan: true, recentMedia: 0 per poll), then scan: false plus three quiet reads → exit 0.
scripts/media-refresh-plex.sh trigger --library 1 → 200 accepted; … status --library 1 --after <journal mtime> exited 0 in the same second, so that wait was a no-op.
scripts/media-verify.py against both databases: still at src: 0 and REAL: 0. That is the purge proof for the promo-clip hold, and it holds on Jellyfin and Plex.
Two traps this chain exposed. Both of my earlier drafts of this comment were wrong, so the corrections are in the record:
Driver names. The merged drivers are scripts/media-refresh-jellyfin.py and scripts/media-refresh-plex.sh. The first draft named jellyfin-refresh.py / plex-refresh.py, which do not exist, and claimed the Plex door never polls.
Plex's status --after is not a waiter, and no epoch makes it one. It compares section_locations.scanned_at — the last scan PMS recorded — with --after. Measured: trigger accepted 19:46:58Z → settled 19:46:58Z, and a direct read at 20:17Z still answered settled off that single stamp while the walk ran. A journal mtime fails, and so does the trigger's own epoch: the request satisfies the very door it was asked to wait for. The purge conclusion is untouched — media-verify.py asks whether any row still points at a path with no file, and a scan can clear rows but cannot invent one. Jellyfin's door is honest: its EndTimeUtc advances only at completion, and wait refuses by name an epoch an earlier task already passes.
Reconcile chain ran on the live fleet, 2026-11-04 19:17:51Z → 19:46:59Z, after the promo-clip hold (32 renames) landed in the SSD-tier view. Durable log: `/mnt/entropy/.media-campaign/rescan-20261104.log`.
* `scripts/media-refresh-jellyfin.py trigger` → 204 accepted; `… wait --interval 20 --require-quiet 3 --timeout 5400` polled ~28 min of scan (`scan: true`, `recentMedia: 0` per poll), then `scan: false` plus three quiet reads → exit 0.
* `scripts/media-refresh-plex.sh trigger --library 1` → 200 accepted; `… status --library 1 --after <journal mtime>` exited 0 **in the same second**, so that wait was a no-op.
* `scripts/media-verify.py` against both databases: `still at src: 0` and `REAL: 0`. That is the purge proof for the promo-clip hold, and it holds on Jellyfin *and* Plex.
Two traps this chain exposed. Both of my earlier drafts of this comment were wrong, so the corrections are in the record:
1. **Driver names.** The merged drivers are `scripts/media-refresh-jellyfin.py` and `scripts/media-refresh-plex.sh`. The first draft named `jellyfin-refresh.py` / `plex-refresh.py`, which do not exist, and claimed the Plex door never polls.
2. **Plex's `status --after` is not a waiter, and no epoch makes it one.** It compares `section_locations.scanned_at` — the last scan PMS *recorded* — with `--after`. Measured: trigger accepted 19:46:58Z → `settled` 19:46:58Z, and a direct read at 20:17Z still answered `settled` off that single stamp while the walk ran. A journal mtime fails, and so does the trigger's own epoch: the request satisfies the very door it was asked to wait for. The purge conclusion is untouched — `media-verify.py` asks whether any row still points at a path with no file, and a scan can clear rows but cannot invent one. Jellyfin's door is honest: its `EndTimeUtc` advances only at completion, and `wait` refuses by name an epoch an earlier task already passes.
Laws recorded on [[Plex]] and [[Jellyfin]] (`main` 146c28d).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
The 2026-11-04 promo-clip hold left both media servers indexing paths that no
longer exist, so the reconcile step had to run. The only door for it was a
hand-written
curl -Kblock copied out of the vault — the same block that hasbitten this fleet twice (the
\"vs\\"quoting 401, and curl silentlyperforming a second transfer when a config
urlmeets a command-line URL). Bothservers' scan procedures are now commands, and the vault pages point at them.
What
scripts/media-refresh-jellyfin.py—trigger/status/wait.~/.config/secrets/jellyfininto a mode-600 curlconfig and reaches the wire only as
Authorization: MediaBrowser Token="…";it never enters argv (
/proc/<pid>/cmdlineis world-readable) and never theURL.
triggeraccepts only204, retries the identical request otherwise, andrefuses to fire a second refresh while one is running: a second accepted
refresh moves
Scan Media LibrarytoCancelling, after which every204isaccepted and discarded.
--forceoverrides for the documented restart case.waitcalls the library settled only on the page's rule: theScan Media Librarytask seenRunning(or a run that finished while it watched, or an--afterthe task already passes) FIRST, then--require-quietconsecutivereads with no scan and no database writes. Without a start sighting it refuses
by name rather than reporting the pre-scan library as fresh — and
ScheduledTasksis the only honest progress read:RefreshStatuson thevirtual-folder record stayed
Idlethrough a scan verifiably at 75 %, andGET /Library/ScanStatusis 404 on 12.4.status --after <epoch>(a journal mtime) answers "has the scan reached mymoves" from the task's
EndTimeUtcand exits non-zero while it is owed;recentMediacountsBaseItems.DateModifiedrows injellyfin.db, openedmode=ro(the DB is WAL;immutable=1would skip the log). The database pathis discovered, because on this host it is the quadlet's config bind at
/home/dank/Containers/jellyfin/config/data/, not the stock location.scripts/media-refresh-plex.sh—trigger --library <key>/status --library <key> --after <epoch>.10.89.x.xboth read as "empty reply"), so the runner ispodman cp-shippedinto the container and rides plain
http://127.0.0.1:32400from inside. Noquoting chain survives
ssh → podman exec → sh -c.PlexOnlineTokenis read from the container'sPreferences.xmlinside thecontainer into a mode-600 curl config there — the token never crosses the
host, never argv, never the URL.
000(server warming) retries with aparameterised sleep instead of restarting anything.
statusneeds no token at all:section_locations.scanned_atin the librarydatabase carries the same epoch the API prints as
scannedAt— verified live(section 1:
2026-11-04T18:31:33Zin the DB,1793817093from the API).tests/media_refresh.bats— 17 tests over the contracts an operatorobserves, no live server. Jellyfin: the driver authenticates with the
MediaBrowserform and puts no key in argv; a non-204 retry loop names the codeit saw; a missing credential is refused before any request;
waitreturns onlyafter the library reads quiet repeatedly, and refuses outright when it never saw
the scan start;
status --afterrefuses to certify a scan that predates themoves. Plex: the container token reaches
X-Plex-Tokenand never argv or theURL, and a section with no location, an unreadable database and an un-answered
000all fail by name.Docs
Dankserver/Plex.md— the driver is the door; adds the live section keys andthe
scanned_at == scannedAtfact; keeps every law (one section, the netnsbehaviour,
Preferences.xmlvsPlexHTTPToken, the literalbackslash-newline in a quoted
printf).Dankserver/Jellyfin.md— the driver is the door; records wherejellyfin.dbactually lives on this host, and corrects the
ApiKeysread to plainmode=rowithimmutable=1demoted to the-shmfallback.Dankserver/Media Relayout.md— the hand-off after an apply now names bothdrivers and states the ordering law: Jellyfin first and alone, because its one
door walks every library and Plex forks a scanner per section.
Dankserver/Media Duplicate Prune.md— a new guard: 32 promo clips inMovies(25 in
_release-group watermarks/) were 8–20-second h264 files carrying realfilm titles; no duplicate screen can see them, so the census that finds them is
a duration floor.
Verification
./tests/run.sh— full suite green, including the new seam (auto-discovered bytests/run.sh'sbats *.bats).triggerprintedrefresh accepted (204), andstatusthen read
{"scan": true, "taskState": "Running", "lastScan": null, "recentMedia": 0, "scannedAfter": false}with exit 1 and "a refresh is still owed" — that isthe never-completed
EndTimeUtcsignature the page describes, now surfacedinstead of guessed at. Before the trigger, Plex
status --library 1 --after <journal mtime>had returned the same verdict from the database, andmedia-verify.pyreplayedpromo-hold.jsonlagainst both databases, findingthe same stale rows in each — the rule that says a rescan is owed.
replay against both DBs) is running on the host; its log will be posted to this
PR as a comment when it finishes.
Not deployed by this PR: the drivers are new files; nothing in the deploy plan
touches a running service.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
(committed and tested in the build worktree
/tmp/wt-deploy)Reconcile chain ran on the live fleet, 2026-11-04 19:17:51Z → 19:46:59Z, after the promo-clip hold (32 renames) landed in the SSD-tier view. Durable log:
/mnt/entropy/.media-campaign/rescan-20261104.log.scripts/media-refresh-jellyfin.py trigger→ 204 accepted;… wait --interval 20 --require-quiet 3 --timeout 5400polled ~28 min of scan (scan: true,recentMedia: 0per poll), thenscan: falseplus three quiet reads → exit 0.scripts/media-refresh-plex.sh trigger --library 1→ 200 accepted;… status --library 1 --after <journal mtime>exited 0 in the same second, so that wait was a no-op.scripts/media-verify.pyagainst both databases:still at src: 0andREAL: 0. That is the purge proof for the promo-clip hold, and it holds on Jellyfin and Plex.Two traps this chain exposed. Both of my earlier drafts of this comment were wrong, so the corrections are in the record:
scripts/media-refresh-jellyfin.pyandscripts/media-refresh-plex.sh. The first draft namedjellyfin-refresh.py/plex-refresh.py, which do not exist, and claimed the Plex door never polls.status --afteris not a waiter, and no epoch makes it one. It comparessection_locations.scanned_at— the last scan PMS recorded — with--after. Measured: trigger accepted 19:46:58Z →settled19:46:58Z, and a direct read at 20:17Z still answeredsettledoff that single stamp while the walk ran. A journal mtime fails, and so does the trigger's own epoch: the request satisfies the very door it was asked to wait for. The purge conclusion is untouched —media-verify.pyasks whether any row still points at a path with no file, and a scan can clear rows but cannot invent one. Jellyfin's door is honest: itsEndTimeUtcadvances only at completion, andwaitrefuses by name an epoch an earlier task already passes.Laws recorded on Plex and Jellyfin (
main146c28d).