# Jellyfin Metadata & Episode Titles — Operator Guide

Server: `https://arrflix.s8n.ru` (Jellyfin 10.10.3, container on nullstone)
Fixture for this doc: TV Shows library, series **Futurama** (1999), 44 episodes split across S01–S03.

---

## 1. The bug we hit

User report: "the titles suck."

Symptoms (verified via `/Items` REST query):

| Field            | Before                     |
|------------------|----------------------------|
| `Name`           | `Futurama.s01e01.pl` (raw filename) |
| `ProviderIds`    | `{}` (empty) |
| `Overview`       | `""` (empty) |
| `PremiereDate`   | 2021-04-12 (file mtime, not airdate) |
| `ProductionYear` | 2021 (file mtime year, not 1999) |
| Series ProviderIds | `{}` — never matched on TMDB |

Root cause: the **Futurama series object had no provider IDs**, so episode-level metadata fetchers had nothing to query against. The library scan had ingested files purely from the path/filename parser; nothing had been pinned to a TMDB / TVDB show.

Why it happened: the library option `EnableInternetProviders` was reported as `false` by the API at the moment Futurama was first scanned, so the auto-identify step on first scan never ran. After enabling it via API + restart, the series still didn't auto-match because by then the per-item record existed without provider hints.

The fix is a two-step: **(a)** lock the series to TMDB id 615 via `RemoteSearch/Apply`, **(b)** trigger a recursive `Refresh` with `ReplaceAllMetadata=true` so all episodes pull from TMDB/TVDB using the locked series id.

---

## 2. How Jellyfin parses TV filenames

Jellyfin's TV file matcher is documented at https://jellyfin.org/docs/general/server/media/shows/ . It uses a regex chain in `Emby.Naming.TV.EpisodeResolver`. The supported episode-number patterns include (case-insensitive, dots and dashes equivalent to spaces):

| Pattern               | Example                          | Matches |
|-----------------------|----------------------------------|---------|
| `S##E##`              | `Futurama.S01E01.mkv`            | season 1, ep 1 |
| `s##e##`              | `Futurama.s01e01.pl.mkv`         | season 1, ep 1 (our case) |
| `Season ## Episode ##`| `Futurama Season 1 Episode 1.mkv`| season 1, ep 1 |
| `##x##`               | `Futurama 1x01.mkv`              | season 1, ep 1 |
| `S##E##-E##`          | `Futurama.S01E01-E02.mkv`        | one file, episodes 1+2 (multi) |
| `S##E## - S##E##`     | `S01E01 - S01E02`                | multi-episode range |

Series name comes from the **parent folder** when present, or the filename prefix before the season/episode token. With our layout:

```
/media/tv/Futurama/Season 01/Futurama.s01e01.pl.mkv
```

- `Futurama` (top-level folder) → series name (used for TMDB search if no NFO/IDs)
- `Season 01` → season number (also accepts `Season 1`, `S01`, `Season.01`, …)
- `Futurama.s01e01.pl.mkv` → episode 1 of season 1; the trailing `.pl` is treated as a tag (language hint), not part of the title

Anything before the `s##e##` token is ignored; anything after is treated as an optional episode title segment, but Jellyfin will overwrite it from TMDB once identified.

---

## 3. Official naming conventions Jellyfin recommends

From the Jellyfin docs:

```
TV Shows
├── Show Name (Year)              ← year disambiguates remakes (Futurama 1999 vs 2023)
│   ├── Season 00                 ← specials
│   │   └── S00E01.mkv
│   ├── Season 01
│   │   ├── Show Name S01E01.mkv
│   │   ├── Show Name S01E02.mkv
│   │   └── Show Name S01E03-E04.mkv ← multi-episode file
│   └── Season 02
│       └── ...
```

Recommended for our case (to avoid future ambiguity with the 2023 reboot):
`/media/tv/Futurama (1999)/Season 01/Futurama (1999) S01E01.mkv`

We did **not** rename on disk — instead we identified the series via API. Renaming would require either touching the source under `/home/admin/Downloads` (forbidden) or the bind-mount on nullstone (allowed but not necessary now that the API match is locked).

---

## 4. The Identify flow (REST API)

Jellyfin's Identify wizard has three calls. Auth header: `X-Emby-Token: <api-key>`.

### 4.1 Search providers
```bash
curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"SearchInfo":{"Name":"Futurama"}}' \
  https://arrflix.s8n.ru/Items/RemoteSearch/Series
```
Returns an array of candidates from each registered provider (TheMovieDb, TheTVDB, OMDb). Inspect `ProviderIds`, `ProductionYear`, `Overview`, `ImageUrl` to pick the right one.

For Episodes use `RemoteSearch/Episode` and pass `SeriesProviderIds` + `IndexNumber` + `ParentIndexNumber`. For Movies, `RemoteSearch/Movie`.

### 4.2 Apply the chosen result
```bash
curl -s --max-time 60 -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"Name":"Futurama","ProviderIds":{"Tmdb":"615"},"ProductionYear":1999,"SearchProviderName":"TheMovieDb"}' \
  "https://arrflix.s8n.ru/Items/RemoteSearch/Apply/$SERIES_ID?ReplaceAllImages=true"
```
Response: `204 No Content` on success. **Note**: this endpoint runs synchronously for ~30–60 s while Jellyfin pulls images. The Traefik proxy in front of Jellyfin will return `502 Bad Gateway` if you don't bump `--max-time` above the proxy's idle threshold. Use `--max-time 60` from the client; the operation continues server-side regardless of the client timeout.

### 4.3 Trigger a metadata refresh
```bash
curl -s --max-time 60 -X POST -H "X-Emby-Token: $TOKEN" \
  "https://arrflix.s8n.ru/Items/$SERIES_ID/Refresh?Recursive=true&MetadataRefreshMode=FullRefresh&ImageRefreshMode=FullRefresh&ReplaceAllMetadata=true&ReplaceAllImages=false"
```
Parameters:
- `Recursive=true` — descend into seasons + episodes (otherwise only the series item refreshes)
- `MetadataRefreshMode=FullRefresh` — bypass cache, hit network
- `ReplaceAllMetadata=true` — overwrite local fields including title, overview, premiere date (this is what kills the `Futurama.s01e01.pl` placeholder names)
- `ReplaceAllImages=false` — keep existing images (set `true` if you also want to repull artwork)

The refresh is a fire-and-forget job. Poll for completion by re-querying episodes:
```bash
curl -s -H "X-Emby-Token: $TOKEN" \
  "https://arrflix.s8n.ru/Items?Recursive=true&IncludeItemTypes=Episode&Fields=ProviderIds&ParentId=$SERIES_ID&Limit=44" \
  | jq '[.Items[] | select(.ProviderIds | length > 0)] | length'
```

---

## 5. Manually overriding metadata

### 5.1 Single episode via API
```bash
EP_ID=2b73bc176fbf8a02bb9bea9015ec13c6
curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "Id":"'$EP_ID'",
    "Name":"Space Pilot 3000",
    "Overview":"Pizza-delivery boy Philip J. Fry is cryogenically frozen in 1999 and wakes up in the year 3000.",
    "PremiereDate":"1999-03-28",
    "LockedFields":["Name","Overview"]
  }' \
  https://arrflix.s8n.ru/Items/$EP_ID
```
`LockedFields` tells subsequent refreshes to leave those fields alone — important if you want to keep your override across future scans.

### 5.2 Single episode via UI
Three-dot menu on the episode → **Identify** → enter known title or provider id → pick result → **OK**. Equivalent to the API flow above.

### 5.3 Locking entire series to a provider (as we did for Futurama)
Three-dot menu on the series → **Identify** → search by name → pick the right candidate (year matters!) → **OK**. Then three-dot → **Refresh metadata** → "Replace all metadata".

---

## 6. Language preferences and how they cascade

Three layers, most-specific wins:

1. **Server default** — `Dashboard > Display > Metadata language`. Affects libraries that don't set their own.
2. **Library** — `Dashboard > Libraries > <Library> > Manage > Preferred metadata language`. Stored in `/config/root/default/<Library>/options.xml` as `<PreferredMetadataLanguage>` and `<MetadataCountryCode>`.
3. **Item** — items can have `LockedFields` on individual fields; when a field is locked it ignores the library/server defaults for that field.

Our state (intentional, per user preference):

| Library  | PreferredMetadataLanguage | MetadataCountryCode |
|----------|---------------------------|---------------------|
| Movies   | `pl`                      | `PL`                |
| TV Shows | `pl`                      | `PL`                |

TMDB falls back to English automatically when a Polish translation doesn't exist, so this is generally safe. To override per-library, edit the library and change the dropdown — or via API:

```bash
# Fetch current options
curl -s -H "X-Emby-Token: $TOKEN" https://arrflix.s8n.ru/Library/VirtualFolders \
  | jq '.[] | select(.Name=="TV Shows") | .LibraryOptions' > opts.json

# Modify in place (e.g. flip language to en)
jq '.PreferredMetadataLanguage="en" | .MetadataCountryCode="US"' opts.json > opts2.json

# POST it back
LIB_ID=767bffe4f11c93ef34b805451a696a4e   # TV Shows
jq -n --arg id "$LIB_ID" --slurpfile o opts2.json '{Id:$id, LibraryOptions:$o[0]}' \
  | curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
      -d @- "https://arrflix.s8n.ru/Library/VirtualFolders/LibraryOptions"
```
Returns 204. **Side effect**: changing library options can trigger a container restart cycle on this Jellyfin install — saw it in our logs at the moment of update. Schedule this when no playback is active.

To override per-item to force English titles on a single show despite a Polish library, set `LockedFields` after manually editing the names — refreshes will then respect the override.

---

## 7. Multi-episode files (`S01E01-E02`)

Jellyfin handles a single file containing two consecutive episodes by:

1. Filename matches the multi-episode regex: `S01E01-E02`, `S01E01-E03`, etc.
2. Library DB creates **one** physical media item but exposes it as **N** virtual `Episode` rows (one per episode number in range), each pointing to the same `Path`.
3. Each virtual episode pulls its own metadata (title, overview, airdate) from TMDB.
4. Playback: clicking any of the N virtual episodes plays the same file from the start; users have to scrub manually.

If your file actually contains two episodes back-to-back, this is what you want. If it's a single mistitled file, rename it to a non-multi pattern.

For our Futurama set, all files are single-episode (`s01e01.pl.mkv`), so this didn't apply. But note: the original-broadcast Season 1 of Futurama is 13 episodes; our collection groups it as 9 / 20 / 15 = 44, which is the **production-order** split (Fox broadcast some S1 episodes during the S2 run). The user's filenames already reflect production order, and TMDB id 615 is keyed to production order, so episode names line up.

---

## 8. The exact fix applied to Futurama on arrflix.s8n.ru (2026-05-08)

Step-by-step, with the exact commands run:

```bash
TOKEN=<JELLYFIN_API_TOKEN>
SERIES_ID=156e57437f795e5c8cd80fc98bafaee0   # Futurama
LIB_ID=767bffe4f11c93ef34b805451a696a4e      # TV Shows library

# 1. Pull current TV Shows options, flip EnableInternetProviders=true, post back.
curl -s -H "X-Emby-Token: $TOKEN" https://arrflix.s8n.ru/Library/VirtualFolders \
  | jq '.[] | select(.Name=="TV Shows") | .LibraryOptions' > /tmp/opts.json
jq '.EnableInternetProviders=true' /tmp/opts.json > /tmp/opts_new.json
jq -n --arg id "$LIB_ID" --slurpfile o /tmp/opts_new.json \
  '{Id:$id, LibraryOptions:$o[0]}' \
  | curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
      -d @- https://arrflix.s8n.ru/Library/VirtualFolders/LibraryOptions
# -> 204

# 2. Search TMDB for Futurama (without provider hint to see all candidates).
curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"SearchInfo":{"Name":"Futurama"}}' \
  https://arrflix.s8n.ru/Items/RemoteSearch/Series | jq '.[0]'
# -> first hit: TheMovieDb, Tmdb=615, PremiereDate=1999-03-28 (correct: original 1999 series)

# 3. Apply that match to the series.
curl -s --max-time 60 -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"Name":"Futurama","ProviderIds":{"Tmdb":"615"},"ProductionYear":1999,"SearchProviderName":"TheMovieDb"}' \
  "https://arrflix.s8n.ru/Items/RemoteSearch/Apply/$SERIES_ID?ReplaceAllImages=true"
# -> 204 (after first attempt 502'd at proxy because client timeout was too low)

# 4. Trigger recursive full refresh with replace-all metadata.
curl -s --max-time 60 -X POST -H "X-Emby-Token: $TOKEN" \
  "https://arrflix.s8n.ru/Items/$SERIES_ID/Refresh?Recursive=true&MetadataRefreshMode=FullRefresh&ImageRefreshMode=FullRefresh&ReplaceAllMetadata=true&ReplaceAllImages=false"
# -> 204

# 5. Wait ~3 minutes, then verify episodes have providerids and Polish titles.
curl -s -H "X-Emby-Token: $TOKEN" \
  "https://arrflix.s8n.ru/Items?Recursive=true&IncludeItemTypes=Episode&Fields=ProviderIds&ParentId=$SERIES_ID" \
  | jq '[.Items[] | select(.ProviderIds | length > 0)] | length'
# -> 44/44
```

---

## 9. Final state — sample table (5 rows)

| Path                                       | Before (Name)         | After (Name)              | Polish title meaning      | TMDB / TVDB / IMDB                       | Airdate    |
|--------------------------------------------|-----------------------|---------------------------|---------------------------|------------------------------------------|------------|
| Season 01/Futurama.s01e01.pl.mkv           | `Futurama.s01e01.pl`  | **Rok 3000**              | "Year 3000" (Space Pilot 3000) | tvdb=131174, imdb=tt0584449       | 1999-03-28 |
| Season 01/Futurama.s01e02.pl.mkv           | `Futurama.s01e02.pl`  | **Lądowanie**             | "The Landing"             | tvdb=131175, imdb=tt0756891              | 1999-04-04 |
| Season 01/Futurama.s01e03.pl.mkv           | `Futurama.s01e03.pl`  | **Współlokator**          | "The Roommate"            | tvdb=131176, imdb=tt0756882              | 1999-04-06 |
| Season 02/Futurama.s02e01.pl.mkv           | `Futurama.s02e01.pl`  | **Pamiętny lot**          | "Memorable Flight"        | (S2 episodes also TVDB+IMDB-tagged)      | 1999-09-26 |
| Season 03/Futurama.s03e01.pl.mkv           | `Futurama.s03e01.pl`  | **Trąbienie**             | "Honking"                 | tvdb=131203, imdb=tt0768399              | 2000-11-05 |

Series-level final state:

```
Name:        Futurama
ProductionYear (returned by API): null   (TMDB has it; API surface bug — confirmed via PremiereDate)
PremiereDate: 1999-03-28
ProviderIds: { Tmdb: "615", Tvdb: "73871", Imdb: "tt0149460", TvRage: "3628" }
Overview:    "Philip J. Fry przez przypadek hibernuje się i budzi w odległej przyszłości,
              gdzie poznaje krewnego, profesora Hubert J. Farnswortha, który postanawia..."
```

Confirmed correct show: 1999 original Futurama (NOT the 2023 Hulu reboot, which is TMDB id 211410).

---

## 10. Operational notes and gotchas

- **Empty `EnableInternetProviders` in `options.xml`**: when the field is absent from XML, Jellyfin defaults to `true`. The `false` we saw in the API response on the initial `GET /Library/VirtualFolders` may have been a stale DTO state from an older config; the XML never had the override. Future-proof by saving options through the UI once.
- **`/Items/RemoteSearch/Apply/{id}` is slow.** It downloads images synchronously. Always use `--max-time 60` (or longer) and accept that a `502` from the proxy doesn't mean it failed — verify with a follow-up GET on the item.
- **No TMDB API key set** in `Jellyfin.Plugin.Tmdb.xml` — that's fine, the plugin ships with a built-in non-anon key. If you ever hit rate limits, set `<TmdbApiKey>` in `/config/plugins/configurations/Jellyfin.Plugin.Tmdb.xml`.
- **Restart sensitivity**: changing library options can trigger ffprobe re-scan of the whole library, observed in our logs. Schedule library option changes during low-use windows.
- **Open Subtitles plugin is throwing errors on every episode refresh** (`MediaBrowser.Providers.Subtitles.SubtitleManager: Error downloading subtitles from Open Subtitles`). Not blocking, but worth fixing in a future doc — likely needs an Open Subtitles account configured under the plugin settings.
- **Polish-as-default for media language is intentional.** TMDB's Polish coverage on classic American shows is good. Override per-library only if you find titles falling back to filenames (which means TMDB had no Polish entry — extremely rare for US prime-time shows).

---
Last updated: 2026-05-08
