From 52600b28ab9f6eb82c622c9c0c9793478be60d6b Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 1 Nov 2026 10:31:42 -0800 Subject: [PATCH] fix(jellyfin): refuse a library that indexes files but fetches nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Movies library had indexed 2,571 films and had never asked a provider for a single one: no TMDb id, no plot, no rating, no poster. Nothing was broken in the provider chain — the plugin is core in 12.4 and active, its key is set, image.tmdb.org answers the container — and nothing was broken in the write path: the overlay view has been read-write since the 2026-10-31 cutover, and movie.nfo lands on the NVMe upper layer today. The state was two settings in the per-library option file, config/root/default//options.xml (Jellyfin 12 renamed it from library.xml; the database stores none of these options, so a census that greps for the old name and then queries the DB concludes nothing is configured and goes hunting elsewhere): false (empty, beside a populated and a fully populated ) With the internet switch off every refresh answers 204, stores nothing and logs nothing, so the item count reads like a healthy scan — this passed the deploy health gate, the Playwright suite and three agent audits. Order is not enablement: the fetcher ORDER stays populated while the ENABLED set sits empty, and the populated metadata list beside it makes the library look well. verify now audits every library's options.xml and fails with `SILENT INDEX : ` on providers-off or on a type whose image fetchers are empty while its metadata fetchers are named; an options.xml that never carried the switch (BoxSets, written before the property existed) is the vendor default and stays accepted. Vault: the page claimed artwork is a sidecar like movie.nfo. It is not — no image saver is bound for any library, so art goes to the internal store and Plex never reads it. The claim is corrected, and the RemoteImages diagnostic that separates a provider fault from a library fault is written down with the 12.x API shape traps (Items needs UserId; MetadataRefreshMode has no Full/ValidationFailure, ReplaceAllMetadata=true is what re-runs providers). --- scripts/jellyfin-providers.sh | 62 ++++++++++++- serverAppConfigs-docs/Dankserver/Jellyfin.md | 38 +++++++- tests/jellyfin_providers.bats | 92 ++++++++++++++++++++ 3 files changed, 187 insertions(+), 5 deletions(-) diff --git a/scripts/jellyfin-providers.sh b/scripts/jellyfin-providers.sh index cc95ec2..c982b65 100755 --- a/scripts/jellyfin-providers.sh +++ b/scripts/jellyfin-providers.sh @@ -35,7 +35,8 @@ # Subcommands # plan print the resolved shape and every pending change, touch nothing # install fetch+verify the plugin, then write the keys (backing up first) -# verify prove the plugin and both keys are actually in place +# verify prove the plugin and both keys are in place, and that no library +# is a silent file index (providers off / image fetchers empty) # # Nothing here restarts Jellyfin: the deployer owns that # (`scripts/deploy.sh dankserver --service jellyfin`), and the plugin is read at @@ -340,6 +341,64 @@ cmd_install() { fi } + +# A library whose options.xml carries EnableInternetProviders=false, or an empty +# ImageFetchers list beside a populated MetadataFetchers list, is a SILENT FILE +# INDEX: it indexes every file, answers 204 to every refresh, stores no provider +# id, plot, rating or artwork, and logs nothing — so the item count reads like a +# healthy scan. Jellyfin 12 keeps these options in root/default//options.xml +# (it was library.xml) and stores NONE of them in the database, so this audit is +# the only place the state can be read without a browser session. +verify_library_options() { + local root="$JF_ROOT/root/default" f lib problems + if [ ! -d "$root" ]; then + printf '= no library options under %s (nothing audited)\n' "$root" + return 0 + fi + local rc=0 + for f in "$root"/*/options.xml; do + [ -f "$f" ] || continue + lib="$(basename "$(dirname "$f")")" + problems="$(python3 -s - "$f" <<'PY' +import sys, xml.dom.minidom as X +root = X.parse(sys.argv[1]).documentElement +def child(el, name): + for n in el.childNodes: + if n.nodeType == n.ELEMENT_NODE and n.tagName == name: + return n +def flag(el, name): + n = child(el, name) + return None if n is None else (n.firstChild.data.strip() if n.firstChild else "") +def names(el, name): + n = child(el, name) + return None if n is None else [s.firstChild.data.strip() for s in n.childNodes + if s.nodeType == s.ELEMENT_NODE and s.firstChild] +bad = [] +ip = flag(root, "EnableInternetProviders") +if ip is not None and ip.lower() != "true": + bad.append("EnableInternetProviders=%s" % (ip or "empty")) +types = child(root, "TypeOptions") +if types is not None: + for t in types.childNodes: + if t.nodeType != t.ELEMENT_NODE or t.tagName != "TypeOptions": + continue + meta = names(t, "MetadataFetchers") or [] + if meta and not (names(t, "ImageFetchers") or []): + bad.append("%s: ImageFetchers empty while MetadataFetchers names %d" + % (flag(t, "Type") or "?", len(meta))) +print("; ".join(bad)) +PY +)" + if [ -n "$problems" ]; then + printf 'SILENT INDEX %s: %s\n' "$lib" "$problems" >&2 + rc=1 + else + printf '= %s: library options fetch from providers\n' "$lib" + fi + done + return "$rc" +} + cmd_verify() { check_root local bad=0 @@ -378,6 +437,7 @@ cmd_verify() { printf '%s %s chars\n' "$elem" "${#n}" fi done + verify_library_options || bad=1 [ "$bad" -eq 0 ] || return 1 printf 'providers configured\n' } diff --git a/serverAppConfigs-docs/Dankserver/Jellyfin.md b/serverAppConfigs-docs/Dankserver/Jellyfin.md index 395545a..ccdc333 100644 --- a/serverAppConfigs-docs/Dankserver/Jellyfin.md +++ b/serverAppConfigs-docs/Dankserver/Jellyfin.md @@ -46,10 +46,14 @@ sees is the [[Media Metadata View]] overlay, live since the 2026-10-31 cutover: `/var/lib/media-meta/view:/data`, read-write. `Local metadata` — save metadata to media folders — is enabled, and it is the -reason the view exists: `movie.nfo`, `tvshow.nfo`, `episode.nfo` and artwork -written once on the NVMe upper layer are files [[Plex]] can read too — and since -the 2026-10-31 cutover that is what happens: Jellyfin is the fleet's only -metadata writer, and every sidecar it saves is an SSD byte the array never sees. +reason the view exists: `movie.nfo`, `tvshow.nfo` and `episode.nfo` written once +on the NVMe upper layer are files [[Plex]] can read too — and since the +2026-10-31 cutover that is what happens: Jellyfin is the fleet's only metadata +writer, and every sidecar it saves is an SSD byte the array never sees. +**Artwork is not one of those sidecars.** No image saver is bound for any +library, so posters and fanart go to the internal store +(`config/metadata/library///poster.jpg`), which [[Plex]] never +reads — movie art has never existed beside the media on either layer. **Library coverage is the first thing to check when a title is missing.** The four libraries are `movies → /data/Movies`, `tvshows → /data/TVShows`, @@ -66,6 +70,32 @@ instead of the view — always a fault on this host since the 2026-10-31 cutover `scripts/media-view.sh status` says which: the quadlet drifted back to the raw array, or the mount died. +**`EnableInternetProviders` false turns a library into a silent file index.** +Every refresh answers `204`, stores nothing and logs nothing — no provider ids, +no plot, no rating, no art — while the item count reads like a healthy scan. The +switch lives in `config/root/default//options.xml`, Jellyfin 12's name +for the per-library option file: grepping for the old `library.xml` concludes "no +per-library options exist", and the database holds none, so the trail dies there. +Two more traps in the same file: `TypeOptions/ImageFetchers` is the *enabled* set, +so an empty `` switches art off while `ImageFetcherOrder` stays +fully populated — order is not enablement — and the populated `` +built next to it makes the library look healthy. + +**`RemoteImages` is the call that separates a provider fault from a library +fault.** `GET /Items/{id}/RemoteImages?Type=Primary` returns the provider's real +poster list even with every automatic pass switched off — plugin, key and CDN all +proven by that one response — so a refresh that stores nothing beside it is a +library-switch problem, never a provider problem. It is also the delivery door: +`POST /Items/{id}/RemoteImages/Download` saves the chosen image to the internal +store on a build where the automatic image pass will not run at all. + +**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` +accepts `Default` (and `Realtime`) only; `Full` and `ValidationFailure` are +rejected outright, and what forces the providers to re-run is +`ReplaceAllMetadata=true`. + ## Ports & access | Port | Proto | Purpose | diff --git a/tests/jellyfin_providers.bats b/tests/jellyfin_providers.bats index 50aab63..c9e71d0 100755 --- a/tests/jellyfin_providers.bats +++ b/tests/jellyfin_providers.bats @@ -24,6 +24,9 @@ # 9. no plugin configuration XML is tracked in git, the deployer places the # keys BEFORE it restarts Jellyfin, and the planner routes a change to this # installer to jellyfin. +# 10. verify refuses a library that is a SILENT FILE INDEX — internet providers +# switched off, or a type whose ImageFetchers is empty while its +# MetadataFetchers list is populated (the 2026-11-01 missing-posters fault). load helpers @@ -59,6 +62,32 @@ setup() { TRASH="$BATS_TEST_TMPDIR/trash" SEC="$BATS_TEST_TMPDIR/secrets" mkdir -p "$JF/plugins/configurations" "$TRASH" "$SEC" + # The per-library option file Jellyfin 12 owns (it was library.xml). Conforming + # by default so the pre-existing verify tests keep testing what they tested. + mkdir -p "$JF/root/default/Movies" + cat >"$JF/root/default/Movies/options.xml" < + + true + + Nfo + + + + Movie + + TheMovieDb + + + TheMovieDb + + + TheMovieDb + + + + +XML cat >"$JF/plugins/configurations/Jellyfin.Plugin.Tmdb.xml" < @@ -270,6 +299,69 @@ XML printf '%s\n' "${lines[@]}" | grep -qE 'TmdbApiKey .*[0-9]+ chars' } +@test "verify refuses a library whose internet providers are switched off" { + # The whole library becomes a file index: refreshes answer 204 and store + # nothing, and the item count still looks like a healthy scan. + prov install >/dev/null + sed -i 's#true#false#' \ + "$JF/root/default/Movies/options.xml" + run prov verify + [ "$status" -ne 0 ] + printf '%s\n' "${lines[@]}" | grep -qiF 'SILENT INDEX Movies' + printf '%s\n' "${lines[@]}" | grep -qiF 'EnableInternetProviders' +} + +@test "verify refuses a type with no image fetchers while metadata fetchers are named" { + prov install >/dev/null + cat >"$JF/root/default/Movies/options.xml" < + + true + + + Movie + + TheMovieDb + + + + TheMovieDb + + + + +XML + run prov verify + [ "$status" -ne 0 ] + printf '%s\n' "${lines[@]}" | grep -qiF 'ImageFetchers empty' +} + +@test "an older options.xml that never carried the switch is not refused" { + # BoxSets on this host were written before the property existed; absence is + # the vendor default, not a switched-off library, so it must not fail. + prov install >/dev/null + mkdir -p "$JF/root/default/Collections" + cat >"$JF/root/default/Collections/options.xml" < + + + + BoxSet + + TheMovieDb + + + TheMovieDb + + + + +XML + run prov verify + [ "$status" -eq 0 ] + printf '%s\n' "${lines[@]}" | grep -qiF '= Collections' +} + @test "no plugin configuration XML is tracked in this repo" { # These files hold provider keys and are owned by the running container; # tracking them would put credentials in git and guarantee drift.