# 01 — Artwork & Images on Jellyfin How artwork actually flows through Jellyfin 10.10.x on this server, what went wrong with the Futurama library on first scan, and the exact API calls used to fix it. Written for an operator who's never used Jellyfin before but is comfortable with curl and Docker. Server: `https://arrflix.s8n.ru` (Jellyfin `10.10.3`, container `jellyfin` on nullstone). Auth header below uses the long-lived API token — replace with your own `X-Emby-Token` if needed. ```bash TOKEN="*redacted*" H="-H \"Authorization: MediaBrowser Token=${TOKEN}\"" BASE="https://arrflix.s8n.ru" ``` --- ## 1. How Jellyfin's image system works Every "item" in Jellyfin (Movie, Series, Season, Episode, Person, BoxSet, MusicAlbum, etc.) can hold one or more images, keyed by **ImageType**. Per-item-type, the relevant types are: | ImageType | Movie | Series | Season | Episode | Notes | |-------------|:----:|:------:|:------:|:-------:|-------| | Primary | yes | yes | yes | yes | "Poster". The thumbnail you see in the grid. | | Backdrop | yes | yes | yes | no | Fanart / hero background. Multiple allowed (`Backdrop/0`, `Backdrop/1`, ...). | | Logo | yes | yes | no | no | Transparent text-logo, used in the detail-page header. | | Banner | no | yes | yes | no | Wide 758×140-ish strip. Mostly TheTVDB heritage. | | Thumb | yes | yes | yes | yes | 16:9 still. Episodes use this as their poster. | | Disc | yes | no | no | no | DVD/Blu-ray hub icon. Rare. | | Art | yes | yes | no | no | Clearart — transparent character cutout. | | BoxSet/Menu | (CC) | (CC) | - | - | ClearArt-style, used by some skins. | For an **Episode**, the Primary IS the still — there is no separate "Thumb" in practice; clients fall back to whichever is present. ### Where images live on disk Inside the container: ``` /config/metadata/library///poster.jpg /config/metadata/library///backdrop.jpg /config/metadata/library///backdrop1.jpg ... ``` On the host that's `/home/docker/jellyfin/config/metadata/library/...`. They are stamped from URLs returned by the configured **Image Fetchers** — which are plugins, not built into core. ### What the API returns `GET /Items?...&Fields=ImageTags,BackdropImageTags` returns: ```json { "ImageTags": { "Primary": "abc...", "Logo": "def...", "Thumb": "..." }, "BackdropImageTags": ["hash1", "hash2"] } ``` An empty `ImageTags: {}` and `BackdropImageTags: []` means **no images attached at all**. That was the Futurama symptom. `GET /Items/{id}/Images` returns a per-image listing with size and path — returns `[]` (empty array) when nothing is attached. --- ## 2. Which scrapers fetch which images Jellyfin core ships with these image-related providers: | Plugin | Movies | Series/Episode | Music | Images? | API key? | Notes | |-----------------------|:------:|:--------------:|:-----:|:-------:|:--------:|-------| | **TMDb** (built-in) | yes | yes | - | yes | bundled | The default. Provides Primary/Backdrop/Logo/Still. | | **TheTVDB** (plugin) | - | yes | - | yes | needs v4 key | Better for Banners and obscure shows. Requires installing the plugin and adding a TVDB v4 API key. | | **Fanart.tv** (plugin)| yes | yes | yes | yes | community key | Best clearart/clearlogo/disc art. Requires plugin install. | | **OMDb** (built-in) | yes | yes | - | no | optional | Metadata only. No images. | | **Studio Images** (built-in) | -| - | - | yes | none | Studio logos, scraped from a hard-coded GitHub list. Frequently 404s — harmless. | | **AudioDB / MusicBrainz** | - | - | yes | yes | none | Music only. | ### Provider order matters In **Library options → TV Shows → Series → Image Fetchers** you order them. Jellyfin walks the list, takes the **first** result for each ImageType. So if TheTVDB is first and returns a Primary, TMDb is not consulted for that Primary — even if its image is better. Reorder, then refresh with `replaceAllImages=true` to force re-evaluation. ### What happens if a fetcher is listed but the plugin is missing The library config carries the *names* "The Movie Database" and "TheTVDB" as strings. If the TheTVDB plugin isn't installed, that entry is silently ignored on each refresh — no error. Symptom: refresh "succeeds" but TVDB images never appear. Verify with `GET /Plugins`. --- ## 3. Common failure modes (and fixes) ### A. `EnableInternetProviders: false` on the library ⟵ THIS WAS US (1/2) This is the first big gotcha. The setting is per-library, off-by-default when a library is created via the API or via certain "I'll configure it later" paths in the wizard. With it off, Jellyfin builds items from filenames only — no metadata, no images, no provider IDs. Diagnostic: ```bash curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Library/VirtualFolders" | jq '.[] | {Name, EnableIP: .LibraryOptions.EnableInternetProviders}' ``` Fix: POST a full LibraryOptions payload back with `EnableInternetProviders: true`. **Note**: saving library options causes Jellyfin 10.10.3 to reload itself (~10–15s of HTTP 503). If you're scripting against the API, sleep before the next call. ```bash # Pull current options, mutate, push back curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Library/VirtualFolders" \ | jq '.[] | select(.Name=="TV Shows") | {Id: .ItemId, LibraryOptions: (.LibraryOptions | .EnableInternetProviders=true)}' \ > /tmp/tv-options.json curl -s -X POST \ -H "Authorization: MediaBrowser Token=$TOKEN" \ -H "Content-Type: application/json" \ --data-binary @/tmp/tv-options.json \ "$BASE/Library/VirtualFolders/LibraryOptions" # Expect: HTTP 204 ``` ### A2. Wrong fetcher names in TypeOptions ⟵ THIS WAS US (2/2, the real killer) In **Jellyfin 10.10.x** the TMDb plugin advertises itself with the name **`TheMovieDb`** (no spaces). Older Jellyfin versions called it `The Movie Database`. If your library was upgraded from an old config OR if you copy-pasted from a 2022 forum post, your `TypeOptions[].MetadataFetchers` and `ImageFetchers` may say `"The Movie Database"` — and the matcher is **case+space-strict**. There is no fallback, no warning logged, no error. The fetcher list silently resolves to *zero matching providers* and refresh runs to completion in milliseconds without ever calling TMDB. Symptoms: - Refresh returns 204, then nothing happens. - No `tmdb`/`fetch`/`provider` lines in the log at all. - `ProviderIds` may show `Tmdb: 615` if you manually identified the show via the UI — that goes through `ItemLookupController`, a different code path that *does* match by name. But subsequent automatic refresh still does nothing because the *library* fetcher list doesn't match. Diagnostic — list what the server actually advertises: ```bash curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Libraries/AvailableOptions?LibraryContentType=tvshows" \ | jq '.TypeOptions[] | {Type, MetadataFetchers: [.MetadataFetchers[].Name], ImageFetchers: [.ImageFetchers[].Name]}' ``` Cross-check those exact strings against your library's TypeOptions. Fix: rewrite the library's TypeOptions to use the names from `AvailableOptions` verbatim. Same payload as in fix A, but with `"TheMovieDb"` everywhere. Then trigger a FullRefresh. The first refresh fixed the missing series-level data (Overview, Genres, Primary, Logo, Thumb, Backdrop) and the second one — running without you doing anything because Jellyfin queues a children-refresh after a parent-refresh — populated all seasons and all 44 episodes with their stills and ProviderIds. ### B. Outbound DNS / network blocked from container Symptom: TMDB plugin loaded, IDs not getting set, no obvious log error. Diagnostic: ```bash ssh user@192.168.0.100 \ 'docker exec jellyfin curl -sI https://api.themoviedb.org/3/configuration --max-time 5' ``` A bare `HTTP/2 401` from `api.themoviedb.org` is **fine** — that's TMDB rejecting an unauthenticated probe. What matters is that the TLS handshake and HTTP response happened at all. If you see `Could not resolve host`, fix DNS (check `/etc/resolv.conf` inside the container, the docker network's DNS, or Pi-hole upstream). ### C. TMDB rate limit (40 req / 10 s per IP) Symptom: refresh stalls partway, episode stills missing for some episodes, others fine. Look for `429` in the logs: ```bash ssh user@192.168.0.100 'docker logs jellyfin --since 24h 2>&1 | grep -E "429|TooMany"' ``` Fix: nothing — just re-run the refresh. The bundled TMDB key is shared across all Jellyfin installs but rate-limited per source IP. Big libraries can take minutes to fully populate on the first scan. ### D. Language fallback with `PreferredMetadataLanguage` We run `pl` (Polish) with `MetadataCountryCode=PL`. TMDB has very sparse Polish posters/backdrops for older Western shows. If `pl` returns nothing for a given ImageType, Jellyfin core asks for language `null` (international, no text) and then `en` as a final fallback. This works **as long as** TMDB plugin's "Include image fetcher language" setting includes English — set in *Dashboard → Plugins → TMDb → Image language*. Usually default-includes `en`. If you see the metadata in Polish but only English-language posters, that's working as designed. ### E. NSFW / "Adult" filter blanking images TMDB has an `include_adult` flag. Jellyfin defaults to *off*. For SFW content this never matters; for kids-anime imports that TMDB has flagged adult, posters go missing. Fix: `Dashboard → Plugins → TMDb → Include adult content = on`. Not relevant to Futurama. ### F. TheTVDB plugin without v4 API key Old configs from Jellyfin 10.7 listed "TheTVDB" as the default series scraper. Since 10.9 the bundled key is gone — TVDB went paid. Symptoms: "TheTVDB" in the Image Fetcher list but no images sourced from there. Either install the **TheTVDB** plugin from `repo.jellyfin.org` and paste a v4 subscriber API key, or remove it from the fetcher order and let TMDb cover everything. We did the latter. ### G. Studios image errors in the log (red herring) ``` StudiosImageProvider failed in GetImageInfos for type Studio at /config/metadata/Studio/Adult Swim ``` The bundled Studio Images plugin scrapes a hard-coded GitHub repo (`MediaBrowser/jellyfin-plugin-studioimages-data`) for studio logos. That repo is occasionally unreachable or doesn't have the studio you need. **It does not affect series/episode/movie artwork at all.** Ignore unless you specifically want studio logos in the home-screen "Studios" row. --- ## 4. Triggering a metadata + image refresh Per-item refresh — full replace: ```bash ITEM_ID="156e57437f795e5c8cd80fc98bafaee0" # Futurama series curl -s -X POST \ -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items/$ITEM_ID/Refresh?metadataRefreshMode=FullRefresh\ &imageRefreshMode=FullRefresh\ &replaceAllMetadata=true\ &replaceAllImages=true\ ®enerateTrickplay=false" # Expect: HTTP 204 ``` Modes: - `metadataRefreshMode` ∈ `None | ValidationOnly | Default | FullRefresh` - `imageRefreshMode` same set - `replaceAllMetadata=true` wipes Overview/Genres/etc and re-pulls. - `replaceAllImages=true` wipes attached images and re-pulls. Use this every time you've changed fetcher order or installed a new image plugin. - `regenerateTrickplay=true` forces re-extraction of trickplay sprites — heavy, leave off for metadata-only refreshes. Refresh recurses into children: Series → Seasons → Episodes. For a single episode refresh, pass that episode's item ID instead. Library-wide refresh (rare — usually you want per-item): ```bash curl -s -X POST -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Library/Refresh" ``` ### Watching progress The Refresh task is asynchronous — `204` just means "queued". Monitor via: ```bash # Container log (best signal) ssh user@192.168.0.100 'docker logs jellyfin --tail 200 -f' \ | grep -iE "futurama|tmdb|provider|fetch" # API: poll the item itself curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items?Recursive=true&IncludeItemTypes=Series&userId=$USER_ID&\ Fields=ProviderIds,ImageTags,BackdropImageTags" \ | jq '.Items[] | {Name, ProviderIds, ImgKeys: (.ImageTags|keys), Backdrops: (.BackdropImageTags|length)}' ``` `ProviderIds` populates first (within seconds), `ImageTags.Primary` and backdrops follow after the image fetcher has iterated through TMDB's image list and cached them locally. ### Inspecting a single item's images ```bash curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items/$ITEM_ID/Images" | jq ``` Returns an array of `{ImageType, ImageIndex, Path, BlurHash, Width, Height, Size}`. Empty array = nothing attached. ### Manually identifying a series (when auto-match misses) ```bash # Search TMDB for a candidate curl -s -H "Authorization: MediaBrowser Token=$TOKEN" -X POST \ -H "Content-Type: application/json" \ --data '{"SearchInfo":{"Name":"Futurama","Year":1999},"IncludeDisabledProviders":false}' \ "$BASE/Items/RemoteSearch/Series" | jq '.[0]' # Apply the picked TMDB ID curl -s -H "Authorization: MediaBrowser Token=$TOKEN" -X POST \ -H "Content-Type: application/json" \ --data '{"ProviderIds":{"Tmdb":"615"}, "Name":"Futurama"}' \ "$BASE/Items/RemoteSearch/Apply/$ITEM_ID?replaceAllImages=true" ``` --- ## 5. Installing the Fanart.tv plugin (optional) Adds clearart / clearlogo / disc-art and richer backdrops. Not needed for the Futurama fix, but worth knowing for music libraries especially. The current `https://repo.jellyfin.org/files/plugin/manifest.json` ships **Fanart 14.0.0.0** with `targetAbi 10.11.5.0` — that requires Jellyfin 10.11+. This server is 10.10.3, so the latest Fanart build is **not compatible**. Older builds in the manifest target `10.9.0.0` ABI which is too old for 10.10. Options: 1. Wait until we upgrade Jellyfin to 10.11+ and then install. 2. Run an old-ABI build by manually dropping a `10.9.0.0`-targeted Fanart DLL into `/config/plugins/Fanart_/` — works in practice, but requires a server restart and the user has asked us not to restart. If/when you do install: ```bash # Find the plugin GUID GUID="170a157f-ac6c-437a-abdd-ca9c25cebd39" # Fanart VERSION="14.0.0.0" curl -s -X POST -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Packages/Installed/Fanart?AssemblyGuid=$GUID&Version=$VERSION" # Then a SERVER RESTART is required. The plugin won't appear in /Plugins # until restart. ``` After install, edit each library's image fetcher list to include `"Fanart"`, then trigger `replaceAllImages=true` refresh. --- ## 6. Futurama — what we found and what we did ### Findings | What | Value | |-------------------------------------------------------|--------------------------------------| | Series item ID | `156e57437f795e5c8cd80fc98bafaee0` | | Path | `/media/tv/Futurama` | | Seasons / episodes on disk | 3 / 44 | | `LibraryOptions.EnableInternetProviders` (TV) | **`false`** ← root cause #1 | | `LibraryOptions.EnableInternetProviders` (Movies) | **`false`** (same bug) | | Fetcher names in TypeOptions | `"The Movie Database"` ← root cause #2; should be `"TheMovieDb"` in 10.10.x | | `"TheTVDB"` listed as fetcher | yes — but plugin not installed → silent no-op | | `ProviderIds` before fix | `{}` — nothing | | `ImageTags` / `BackdropImageTags` before fix | `{}` / `[]` | | Plugins installed | TMDb, OMDb, MusicBrainz, AudioDB, Studio Images, Open Subtitles | | TheTVDB plugin | **NOT installed** | | Fanart plugin | **NOT installed** (manifest only ships ABI 10.11/10.9 — not compatible with 10.10.3) | | Container outbound to `api.themoviedb.org` | OK — TLS + HTTP working, returned full image set for tv/615 | | Logs before fix | Only `StudiosImageProvider` errors (harmless GitHub 404s) — no TMDB activity at all | ### Changes applied 1. **TV Shows library** — flipped `EnableInternetProviders` to `true`, replaced `"The Movie Database"` and `"TheTVDB"` with `"TheMovieDb"` in `MetadataFetchers`, `MetadataFetcherOrder`, `ImageFetchers`, `ImageFetcherOrder` for Series/Season/Episode TypeOptions. Added a `Season` TypeOptions block (was missing — would have inherited "no fetchers" silently for season images). 2. **Movies library** — same: `EnableInternetProviders=true`, fetcher string `"TheMovieDb"`. 3. **PreferredMetadataLanguage** kept as `pl`, `MetadataCountryCode=PL`. Polish overview and genres landed; English fallback worked for poster art. 4. **Server reloads** — three of them, each triggered automatically by `POST /Library/VirtualFolders/LibraryOptions`. No Docker restart, no compose change. (10–15s of HTTP 503 each. The reload IS the reason the first attempts to flip `EnableInternetProviders` appeared to "stick" then "revert" — what was actually happening: the API saw the new in-memory value, but the BACKGROUND save to disk omitted any property whose value matched the .NET serializer default; on reload the in-memory state came back out of sync with what we'd posted. Solution was to push the **full** options blob with EVERY explicit value set, including the corrected fetcher names — once both fixes were in the same payload, the in-memory state was usable for the next refresh and all metadata flowed.) 5. **Triggered refresh** on series ID `156e57437f795e5c8cd80fc98bafaee0` with `metadataRefreshMode=FullRefresh&imageRefreshMode=FullRefresh&replaceAllMetadata=true&replaceAllImages=true`. First refresh populated Series-level data; the recursive children refresh that fires automatically afterwards filled all Seasons and Episodes. ### Final state (verified 2026-05-08 ~01:11Z) Series **Futurama** (`156e57437f795e5c8cd80fc98bafaee0`): - `ProviderIds`: `Tmdb=615`, `Imdb=tt0149460`, `Tvdb=73871`, `TvRage=3628` - `Genres`: Animacja, Komedia, Sci-Fi & Fantasy - `Overview`: 182 chars (Polish) - `PremiereDate`: 1999-03-28, `EndDate`: 2025-09-15, `Status`: Continuing - `OfficialRating`: PL-12, `CommunityRating`: 8.37 - `ImageTags`: Primary, Logo, Thumb attached - `BackdropImageTags`: 1 backdrop attached - On-disk files: `poster.jpg`, `backdrop.jpg`, `landscape.jpg`, `logo.png` in `/home/docker/jellyfin/config/metadata/library/15/156e57437f795e5c8cd80fc98bafaee0/` Series-level images detail: | ImageType | Resolution | Size (bytes) | |-----------|-------------|--------------| | Primary | 2000×3000 | 347,957 | | Logo | 1999×625 | 246,060 | | Thumb | 3840×2160 | 572,338 | | Backdrop | 3840×2160 | 995,768 | Seasons (3/3 with Primary image): - Season 1 (Tvdb=6588) — Primary attached - Season 2 (Tvdb=6589) — Primary attached - Season 3 (Tvdb=6590) — Primary attached Episodes: - 44 / 44 with `ProviderIds` (Tvdb + Imdb + TvRage on every one) - 44 / 44 with `ImageTags.Primary` (per-episode still) - All episode names in Polish (e.g. "Rok 3000", "Lądowanie", "Współlokator") ### Verification commands (run them yourself) ```bash # Series curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items?Ids=156e57437f795e5c8cd80fc98bafaee0&Fields=ProviderIds,ImageTags,BackdropImageTags&userId=2be0f0d3fe3a45dc9298138a15a01925" \ | jq '.Items[0] | {Name, ProviderIds, ImageTags, Backdrops: (.BackdropImageTags|length)}' # Seasons curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items?ParentId=156e57437f795e5c8cd80fc98bafaee0&IncludeItemTypes=Season&userId=2be0f0d3fe3a45dc9298138a15a01925&Fields=ImageTags,ProviderIds" \ | jq '.Items[] | {Name, ProviderIds, ImageTags}' # Episodes — count those with a Primary curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \ "$BASE/Items?Recursive=true&ParentId=156e57437f795e5c8cd80fc98bafaee0&IncludeItemTypes=Episode&userId=2be0f0d3fe3a45dc9298138a15a01925&Fields=ImageTags" \ | jq '[.Items[] | select(.ImageTags.Primary)] | length' ``` --- ## 7. Useful endpoints reference (Jellyfin 10.10 API) | Endpoint | Purpose | |---------------------------------------------------|-------------------------------| | `GET /Library/VirtualFolders` | List libraries + options. | | `POST /Library/VirtualFolders/LibraryOptions` | Update one library's options. | | `POST /Library/Refresh` | Refresh whole library set. | | `POST /Items/{id}/Refresh` | Refresh one item + children. | | `GET /Items?Recursive=true&IncludeItemTypes=...` | Query items. | | `GET /Items/{id}/Images` | List attached images. | | `POST /Items/{id}/Images/{type}` (multipart) | Upload image manually. | | `DELETE /Items/{id}/Images/{type}/{index}` | Delete one image. | | `POST /Items/RemoteSearch/Series` (or `/Movie`) | Manual provider search. | | `POST /Items/RemoteSearch/Apply/{id}` | Apply chosen ProviderId set. | | `GET /Plugins` | List installed plugins. | | `GET /Repositories` | List plugin repositories. | | `POST /Packages/Installed/{name}?AssemblyGuid=&Version=` | Install plugin (needs restart). | | `GET /System/ActivityLog/Entries?Limit=N` | Recent server events. | | `GET /ScheduledTasks` | Task states + progress. | Auth: header `Authorization: MediaBrowser Token=` works for everything above. The `?api_key=` query parameter form also works for GETs.