From c21c48e61eef35807484e7b4eec26586eb860f1a Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 02:56:03 -0800 Subject: [PATCH 1/4] docs(jellyfin): the door that scans is /Library/Refresh, and the art door names its field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page sent the next agent to POST /Items/{id}/Refresh?Recursive=true per library to make a new folder visible. Measured against a Shows library that had never been identified: that route answers 204 and queues nothing — item census byte-identical, ScheduledTasks idle, journal silent, and still a silent 204 after a service restart. The door that works is POST /Library/Refresh, and the progress read is ScheduledTasks -> Scan Media Library -> CurrentProgressPercentage. RefreshStatus on the VirtualFolders record stays Idle through a real scan, so it is not a progress read at all; a 204 cannot distinguish accepted-and-discarded from accepted-and-queued, so the acceptance read is the journal naming folders. Also recorded the art door's wire shape, because guessing it reads as a server bug: the list response carries the address in Url (there is no ImageUrl), the Download POST takes camelCase type/imageUrl and answers 204, and PascalCase or an empty imageUrl answers 500. RemoteImages is empty for items with no ProviderIds, so art coverage is capped by identification, not by the image door. --- serverAppConfigs-docs/Dankserver/Jellyfin.md | 35 ++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/serverAppConfigs-docs/Dankserver/Jellyfin.md b/serverAppConfigs-docs/Dankserver/Jellyfin.md index 9d11766..2c4298c 100644 --- a/serverAppConfigs-docs/Dankserver/Jellyfin.md +++ b/serverAppConfigs-docs/Dankserver/Jellyfin.md @@ -119,6 +119,17 @@ store on a build where the automatic image pass will not run at all — it is ho the Movies library got its art: 2,515 of 2,564 films carry a primary now, and the rest have none in TMDb (shorts like `Astartes`, files whose names match nothing). +**The Download door's exact wire shape:** the list response carries the address in +`Url` — there is no `ImageUrl` field — and the POST takes camelCase query params, +`?type=Primary&imageUrl=`, answering `204`; the stored tag then appears in +`ImageTags`. PascalCase params, or an `imageUrl` left empty, answer `500`, which +reads as a server fault and is a field-name fault. + +**`RemoteImages` answers empty for an item with no `ProviderIds`** — no provider id, +nothing to look up. Art coverage is capped by identification, not by the image door: +the items a campaign returns nothing for are the unidentified ones, and no image +retry will change that. + **API shape on 12.x:** `/Items` and `/Items/{id}` need `UserId` when authenticated by API key — without it the answer is `400 Error processing request`, which reads exactly like an authentication failure. `MetadataRefreshMode` @@ -284,12 +295,24 @@ anything. The fix is `systemctl --user restart jellyfin.service`, one POST after health answers, and then believe `ScheduledTasks` State at 130 % CPU, not the response code. -**A scan that must see a new folder has to be recursive.** `POST /Library/Refresh` -is the incremental pass, and inotify does not cross a rootless podman bind, so -folders created on the host stay invisible to it. The move is -`POST /Items/{id}/Refresh?Recursive=true` (204) per library, the id from -`GET /Library/VirtualFolders` — its field is `ItemId`, not `Id` — and that same -record's `RefreshStatus` is the progress read. +**The door that scans is `POST /Library/Refresh` (204), and `ScheduledTasks` is the +progress read.** Measured 2026-11-02 against a `Shows` library that had never been +identified: the per-library `POST /Items/{id}/Refresh?Recursive=true` answers `204` +and queues **nothing** — item census identical afterwards, `Scan Media Library` +idle, journal silent — and it is still an empty `204` after a service restart, so +that status code carries no information at all. `/Library/Refresh` walks every +library; within seconds the journal names folders (`…/TVShows/…`, +`EpisodeMetadataService`) and `GET /ScheduledTasks` reports +`Scan Media Library | Running | 75 %` while the process sits near 200 % CPU. + +**`RefreshStatus` on the `GET /Library/VirtualFolders` record is not a progress +read.** It stayed `Idle` through a scan that was verifiably running at 75 %. The +record's useful field is `ItemId`, and the live library set is still the entries +whose `Locations` are non-empty. + +`inotify` does not cross a rootless podman bind, so folders created on the host +stay invisible until a pass like this runs; every pass that writes into a library +ends by firing one. **`Movies` has no watcher at all.** `EnableRealtimeMonitor` is per library (table in *Data layout*) and it is false for `Movies` and `Collections`. Nothing notices a -- 2.54.0 From 8889a4ceff5826c952fc0313c61a8fb09550b06a Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 03:00:42 -0800 Subject: [PATCH 2/4] docs(jellyfin): episode stills are out of scope, and the cost that says so A per-episode RemoteImages query is never cached, so the same campaign that dressed 620 series in an afternoon costs ~11 s per episode for one hit in five. Across 42k episodes that is days of provider traffic to dress a fifth of them, and clients show series and season art regardless. Recorded so nobody runs the pass on a hunch; the door and the resumable log are the same if Frank orders it. --- serverAppConfigs-docs/Dankserver/Jellyfin.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/serverAppConfigs-docs/Dankserver/Jellyfin.md b/serverAppConfigs-docs/Dankserver/Jellyfin.md index 2c4298c..6f827ae 100644 --- a/serverAppConfigs-docs/Dankserver/Jellyfin.md +++ b/serverAppConfigs-docs/Dankserver/Jellyfin.md @@ -130,6 +130,13 @@ nothing to look up. Art coverage is capped by identification, not by the image d the items a campaign returns nothing for are the unidentified ones, and no image retry will change that. +**Series and season art is fetched; episode stills are not, deliberately.** Every +episode query is a fresh provider round-trip — nothing caches per episode — measured +at ~11 s per item beside ~2 s per series on the same host, with one hit in five in a +sample. Across 42k episodes that is days of provider traffic to dress a fifth of them, +and clients display the series and season art anyway. If episode stills are ever +ordered, the loop is the same door and its log makes the pass resumable. + **API shape on 12.x:** `/Items` and `/Items/{id}` need `UserId` when authenticated by API key — without it the answer is `400 Error processing request`, which reads exactly like an authentication failure. `MetadataRefreshMode` -- 2.54.0 From 69effb542def089c8553f6a1d4bb7faa8ea35191 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 04:47:16 -0800 Subject: [PATCH 3/4] docs(jellyfin): Shows is on providers, and the per-library door stays dead anyway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Shows` flip landed today (one field, proven in the server's own `LibraryOptions` answer), so the posture table no longer calls it a silent file index and `Music`/`Collections` are named as the remaining ones. Two corrections the same day's measurements force: - The per-library `POST /Items/{id}/Refresh` answered `204` and queued nothing both before the flip and after it — zero journal lines for ten minutes, and `Scan Media Library` kept `LastTaskResult=None, EndTimeUtc=None`, the never-run signature. The provider switch was never the reason that door is dead; only `POST /Library/Refresh` runs (204, then `Running` inside 15 s at 33 % CPU), and it walks every library, so a library-scoped pass has no door on this build. That paragraph also records the null-pair read that separates "never ran" from "just finished", and that query+body on the same call answers `400` with an empty body whatever the values. - The key law: a plugin owns its own config file under its own `` root and omits null settings, so TVDB's `` element never existed and `install` refused — which is what `MISSING key` had been reporting since October. PR #121 fixes the writer. The `Shows` art census replaces speculation with the split that matters: 2,908 of 3,558 series+season carry a primary, 624 have no provider id, 26 have an id and no poster. A `Download` campaign over them took 20 s and downloaded nothing, which is the measurement that says identification is the only lever left. --- serverAppConfigs-docs/Dankserver/Jellyfin.md | 60 +++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/serverAppConfigs-docs/Dankserver/Jellyfin.md b/serverAppConfigs-docs/Dankserver/Jellyfin.md index 6f827ae..0a02a97 100644 --- a/serverAppConfigs-docs/Dankserver/Jellyfin.md +++ b/serverAppConfigs-docs/Dankserver/Jellyfin.md @@ -89,13 +89,16 @@ looks exactly like an unconfigured library. | Library | `CollectionType` | `EnableInternetProviders` | `EnableRealtimeMonitor` | |---|---|---|---| | `Movies` | movies | true (fixed 2026-11-02) | **false** | -| `Shows` | tvshows | **false** | true | +| `Shows` | tvshows | true (fixed 2026-11-02) | true | | `Music` | music | **false** | true | | `Collections` | boxsets | **false** | false | -`Shows` and `Music` are silent file indexes in the sense above: a refresh answers +`Music` and `Collections` are silent file indexes in the sense above: a refresh answers `204`, stores nothing, logs nothing. Any metadata or artwork work on either one -starts by flipping the switch, never by firing a scan. +starts by flipping the switch, never by firing a scan. `Shows` sat in that set +until 2026-11-02; the door is `POST /Library/VirtualFolders/LibraryOptions` with +the library's own `ItemId` (below), and `scripts/jellyfin-providers.sh verify` is +the one command that says which libraries are still in it. **The per-library option door on 12.x is `POST /Library/VirtualFolders/LibraryOptions`** — `GET` there answers `405`, which is how the route announces itself. The 10.x doors @@ -130,6 +133,15 @@ nothing to look up. Art coverage is capped by identification, not by the image d the items a campaign returns nothing for are the unidentified ones, and no image retry will change that. +**A `Download` campaign over a library that is already dressed does nothing in +seconds, and that is the measurement proving identification is the only lever +left.** Run over all 753 series and 2,805 seasons of `Shows` on 2026-11-02 it +classified 3,558 items in 20 s: 2,908 already carried a primary, 624 have no +provider id at all, 26 have an id and no poster in TMDb — zero downloads. The +artless remainder is not an image-door problem: it is the unnamed long tail (clip +collections, `(Warhammer TV)`-shaped directories), and the lever is folder names +or an identify pass, never another retry loop. + **Series and season art is fetched; episode stills are not, deliberately.** Every episode query is a fresh provider round-trip — nothing caches per episode — measured at ~11 s per item beside ~2 s per series on the same host, with one hit in five in a @@ -250,9 +262,17 @@ scripts/jellyfin-providers.sh verify # plugin + key LENGTHS, never key bytes - **The homelab TVDB key is a licensed key** (its JWT claims say `community_supported: false`), so it needs **no PIN** — the optional `TVDB_PIN=` line exists only for a subscriber-shaped key. -- **TVDB is installed and keyed but is not a metadata agent**: TMDb still - identifies everything. Enabling TVDB as a provider would re-fetch show - metadata across the library, so it is a deliberate, separate change. +- **TVDB is installed but is not a metadata agent**: TMDb still identifies + everything. Enabling TVDB as a provider would re-fetch show metadata across + the library, so it is a deliberate, separate change. +- **A plugin owns its own config file, under its own root, and omits null + settings** — so the element an installer wants to fill may simply not exist, + and its root is ``, not ``. TVDB's file had + no `` element at all, which is why `install` refused with + `no place to write ` from October until the anchor fix: the secret sat + valid in `provider-keys` the whole time, and `verify`'s `MISSING key` was this + fault, not a missing key. The writer now anchors on the file's own root and + still refuses by name when a file has none. - **The deployer places the keys before it restarts jellyfin** (`ensure_provider_keys` in `scripts/deploy-remote.sh`) and warns instead of failing when `provider-keys` is absent, so a host without it still deploys. @@ -302,15 +322,25 @@ anything. The fix is `systemctl --user restart jellyfin.service`, one POST after health answers, and then believe `ScheduledTasks` State at 130 % CPU, not the response code. -**The door that scans is `POST /Library/Refresh` (204), and `ScheduledTasks` is the -progress read.** Measured 2026-11-02 against a `Shows` library that had never been -identified: the per-library `POST /Items/{id}/Refresh?Recursive=true` answers `204` -and queues **nothing** — item census identical afterwards, `Scan Media Library` -idle, journal silent — and it is still an empty `204` after a service restart, so -that status code carries no information at all. `/Library/Refresh` walks every -library; within seconds the journal names folders (`…/TVShows/…`, -`EpisodeMetadataService`) and `GET /ScheduledTasks` reports -`Scan Media Library | Running | 75 %` while the process sits near 200 % CPU. +**The door that scans is `POST /Library/Refresh` (204), and `ScheduledTasks` is +the progress read.** A task that has never completed answers +`LastTaskResult=None, EndTimeUtc=None`, and that pair is what separates "never +ran" from "finished a moment ago" — the difference between a door that is dead +and one that merely answered `204`. + +**The per-library `POST /Items/{id}/Refresh` queues nothing, and the provider +switch is not the reason.** Measured twice on 2026-11-02 against `Shows`: before +the flip and again after it, the call answered `204`, the journal wrote zero lines +over the following ten minutes, and `Scan Media Library` kept its never-run +signature; a service restart changes nothing. `POST /Library/Refresh` does run — +`204`, then `Running` inside 15 s at 33 % CPU — but it walks every library, so on +this build a library-scoped metadata pass has no working door and the global one +is the only lever. + +**That endpoint takes its parameters from one source.** The documented query form +(empty body) and the JSON body form (no query) each parse; supplying both at once +answers `400` with an empty body whatever the values, and `ValidationFailure` is +rejected in the body while the query form accepts it. **`RefreshStatus` on the `GET /Library/VirtualFolders` record is not a progress read.** It stayed `Idle` through a scan that was verifiably running at 75 %. The -- 2.54.0 From 0951d909c9c3bf037f7b2b84c257c2b4e6b4844c Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 04:47:16 -0800 Subject: [PATCH 4/4] docs(agents): the bracket trick for a pkill pattern that must be named MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing rule says don't `pkill -f` a pattern your own command line carries, and to prefer `pkill -x` — but a remote one-shot often has to name the pattern, and hitting it again today cost a silent `ssh` exit 255 with zero output, which reads like a network fault. Record the escape (`"serve[-]script.sh"`), the zsh glob-expansion trap that makes an unquoted `[s]` a no-op, and the exit-code signature. --- AGENTS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ffc5ae1..fad25dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -735,7 +735,12 @@ linked runbooks. **Ops** - Never `pkill -f` a pattern that appears in your own command line — it kills your ssh session. - Use `pkill -x `. + Use `pkill -x `, or the bracket trick — `pkill -f "serve[-]script.sh"` — + when the pattern has to be named: the bracket makes the pattern stop matching the + command line that carries it. Quote it: a zsh remote glob-expands an unquoted `[s]` + and the kill silently never runs. Re-hit 2026-11-02 — + `ssh host 'pkill -f jfshowswatch.py; …'` killed the remote shell mid-command, and + ssh exits **255 with zero output**, which reads exactly like a network fault. - Both hosts allow passwordless sudo for the deploy user. Use plain `sudo` in committed scripts (no `echo pw | sudo -S`); `FRANK_SUDO_PW` covers hosts without it. - **ComfyUI on Dankclaw runs with GPU access** (`/dev/kfd` + `/dev/dri` via `AddDevice`, -- 2.54.0