Domain + repo rename: nasflix.s8n.ru → arrflix.s8n.ru, NASFLIX → ARRFLIX (Forgejo repo, Pi-hole DNS, Traefik file+label routes, compose env+labels, onyx /etc/hosts, branding LoginDisclaimer, all repo refs, logo asset). Theme: ElegantFin → Cineplex v1.0.6 (MRunkehl, pinned). Picked by research agent over JellyFlix (halted), DarkFlix (10.8.x only), Theme Park (no Netflix preset). Real #E50914 + Netflix Sans webfont + transform:scale hover + gradient login backdrop. Doc 04 updated with full candidate matrix, theme-history subsection, rollback-to-ElegantFin snippet. Logo asset saved at assets/logo.png (235x85 RGBA). Live: https://arrflix.s8n.ru → 302. tv.s8n.ru + nasflix.s8n.ru retired (404).
22 KiB
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.
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/<hash-prefix>/<item-id>/poster.jpg
/config/metadata/library/<hash-prefix>/<item-id>/backdrop.jpg
/config/metadata/library/<hash-prefix>/<item-id>/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:
{
"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:
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.
# 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/providerlines in the log at all. ProviderIdsmay showTmdb: 615if you manually identified the show via the UI — that goes throughItemLookupController, 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:
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:
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:
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:
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 | FullRefreshimageRefreshModesame setreplaceAllMetadata=truewipes Overview/Genres/etc and re-pulls.replaceAllImages=truewipes attached images and re-pulls. Use this every time you've changed fetcher order or installed a new image plugin.regenerateTrickplay=trueforces 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):
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:
# 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
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)
# 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:
- Wait until we upgrade Jellyfin to 10.11+ and then install.
- Run an old-ABI build by manually dropping a
10.9.0.0-targeted Fanart DLL into/config/plugins/Fanart_<version>/— works in practice, but requires a server restart and the user has asked us not to restart.
If/when you do install:
# 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
- TV Shows library — flipped
EnableInternetProviderstotrue, replaced"The Movie Database"and"TheTVDB"with"TheMovieDb"inMetadataFetchers,MetadataFetcherOrder,ImageFetchers,ImageFetcherOrderfor Series/Season/Episode TypeOptions. Added aSeasonTypeOptions block (was missing — would have inherited "no fetchers" silently for season images). - Movies library — same:
EnableInternetProviders=true, fetcher string"TheMovieDb". - PreferredMetadataLanguage kept as
pl,MetadataCountryCode=PL. Polish overview and genres landed; English fallback worked for poster art. - 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 flipEnableInternetProvidersappeared 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.) - Triggered refresh on series ID
156e57437f795e5c8cd80fc98bafaee0withmetadataRefreshMode=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=3628Genres: Animacja, Komedia, Sci-Fi & FantasyOverview: 182 chars (Polish)PremiereDate: 1999-03-28,EndDate: 2025-09-15,Status: ContinuingOfficialRating: PL-12,CommunityRating: 8.37ImageTags: Primary, Logo, Thumb attachedBackdropImageTags: 1 backdrop attached- On-disk files:
poster.jpg,backdrop.jpg,landscape.jpg,logo.pngin/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)
# 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=<token> works for everything
above. The ?api_key=<token> query parameter form also works for GETs.