Merge remote-tracking branch 'origin/main' into feat/jellyfin-provider-keys
# Conflicts: # serverAppConfigs-docs/Dankserver/Audiobookshelf.md # serverAppConfigs-docs/Dankserver/Jellyfin.md
This commit is contained in:
commit
d1493de31a
10 files changed
+506
-209
No files matched your search
@@ -3,7 +3,7 @@ type: reference
|
||||
title: Jellyfin
|
||||
description: "Jellyfin on Dankserver: quadlet unit, overlay metadata view, provider keys, ports, access, transcode status."
|
||||
tags: [jellyfin, dankserver, media-server]
|
||||
timestamp: 2026-10-30T00:00:00Z
|
||||
timestamp: 2026-11-02T11:00:00Z
|
||||
---
|
||||
|
||||
# Jellyfin
|
||||
@@ -82,24 +82,26 @@ fully populated — order is not enablement — and the populated `<MetadataFetc
|
||||
built next to it makes the library look healthy.
|
||||
|
||||
**Live posture, measured 2026-11-02** from `GET /Library/VirtualFolders`, cross-checked
|
||||
against each library's `config/root/default/<Library>/options.xml`. The per-library switches
|
||||
are NOT top-level DTO keys: the folder carries `Name`, `CollectionType`, `Locations`,
|
||||
`ItemId`, `PrimaryImageItemId`, `RefreshStatus` and `LibraryOptions`, and the switches live
|
||||
inside `LibraryOptions`. Reading `EnableInternetProviders` off the folder itself returns
|
||||
`None` for every library, which looks exactly like an unconfigured library — that is the
|
||||
10.x camelCase trap one level deeper.
|
||||
against each library's `config/root/default/<Library>/options.xml`. The switches are NOT
|
||||
top-level DTO keys: the folder carries `Name`, `CollectionType`, `Locations`, `ItemId`,
|
||||
`PrimaryImageItemId`, `RefreshStatus` and `LibraryOptions`, and every per-library option
|
||||
sits inside `LibraryOptions`. Reading `EnableInternetProviders` off the folder itself
|
||||
answers `None` for all four — the camelCase trap one level deeper, and it looks exactly
|
||||
like an unconfigured library.
|
||||
|
||||
| Library | `CollectionType` | `EnableInternetProviders` | `EnableRealtimeMonitor` |
|
||||
|---|---|---|---|
|
||||
| `Movies` | movies | true (fixed 2026-11-02) | **false** |
|
||||
| `Shows` | tvshows | true (flipped 2026-11-02, before the TV arm ran) | true |
|
||||
| `Shows` | tvshows | true (fixed 2026-11-02) | true |
|
||||
| `Music` | music | **false** | true |
|
||||
| `Collections` | boxsets | no element — false by default | false |
|
||||
| `Collections` | boxsets | **false** | false |
|
||||
|
||||
`Music` is still a silent file index in the sense above: a refresh answers `204`, stores
|
||||
nothing, logs nothing, and any metadata or artwork work on it starts by flipping the switch,
|
||||
never by firing a scan. `Shows` was flipped on 2026-11-02 and the flip is at rest in the
|
||||
server's own `options.xml`: scan #7 resolved provider ids for 731 of 733 series.
|
||||
`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. `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
|
||||
@@ -134,17 +136,30 @@ 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.
|
||||
|
||||
**Never pin `Providers=` on a TV art loop.** The pinned form reports `none` for exactly the
|
||||
items the unpinned `RemoteImages` query answers for: the TVShows arm's 16 fillable seasons
|
||||
all offered `TheTVDB` posters, and a `Providers=TheMovieDb` pass — correct for movies, wrong
|
||||
here — logged `none` for every one of them and called the gap closed. The unpinned answer is
|
||||
the provider census; the pinned one only proves one provider is empty.
|
||||
**A `Download` pass reports zero only when it is pinned to one provider.** A TMDb-pinned
|
||||
campaign over `Shows` classified every item in 20 s, downloaded nothing, and concluded that
|
||||
identification was the only lever left. The same door queried with no `Providers=` filter
|
||||
answered for 16 seasons that same day — every offer from `TheTVDB`, which the pinned form
|
||||
never asks — and all 16 are dressed now. A pinned answer proves one provider is empty; the
|
||||
unpinned answer is the provider census, and a TV art loop that pins is simply the wrong tool.
|
||||
Post-fill the artless remainder measures 86 items (80 seasons, 6 series): 61 carry no provider
|
||||
id at all — the unnamed long tail (`3a BBW - Series (2010-11)`, `History Of Africa With Zeinab
|
||||
Badawi`, `(Warhammer TV)`-shaped directories), where the lever is folder names or an identify
|
||||
pass — and 25 hold an id and draw no offer from any provider. No image retry moves either
|
||||
group, which is the half of the old conclusion that survives.
|
||||
|
||||
**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/{id}` needs `UserId` when authenticated by API key — without
|
||||
it the answer is `400 Error processing request`, which reads exactly like an authentication
|
||||
failure, and it is easy to hit as a *verification* read right after a write that succeeded.
|
||||
The `/Items` list route, `?Ids=<id>` included, does not need it, so verify a write through
|
||||
the list route. `MetadataRefreshMode`
|
||||
failure, and it bites hardest as a *verification* read directly after a write that succeeded.
|
||||
The `/Items` list route, `?Ids=<id>` included, needs no `UserId`, so verify writes there.
|
||||
`MetadataRefreshMode`
|
||||
accepts `Default` (and `Realtime`) only; `Full` and `ValidationFailure` are
|
||||
rejected outright, and what forces the providers to re-run is
|
||||
`ReplaceAllMetadata=true`.
|
||||
@@ -255,9 +270,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 `<PluginConfiguration>`, not `<Configuration>`. TVDB's file had
|
||||
no `<ApiKey>` element at all, which is why `install` refused with
|
||||
`no place to write <ApiKey>` 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.
|
||||
@@ -307,15 +330,32 @@ 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 settle read is `GET /ScheduledTasks`: the row whose `Name` is exactly
|
||||
`Scan Media Library`, and its `State` (`Idle`|`Running`).** `/Library/ScanStatus`
|
||||
is a 404 on 12.4, so a watcher built on the old endpoint reports nothing and a
|
||||
scanning server looks an idle one. Call it settled only after a `Running`
|
||||
sighting followed by two consecutive `Idle`s: sub-tasks leave gaps that a single
|
||||
poll reads as finished, and a census taken in one reports the pre-scan library.
|
||||
|
||||
**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
|
||||
|
||||
Reference in new issue
Block a user