# 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="<JELLYFIN_API_TOKEN>"
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:

```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\
&regenerateTrickplay=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_<version>/` — 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=<token>` works for everything
above. The `?api_key=<token>` query parameter form also works for GETs.
