ARRFLIX/docs/02-metadata-and-titles.md
s8n f7c872d687 Add operations playbooks: artwork, metadata, subtitles, theming/users
Four research-grade docs covering everything four parallel agents found while
fixing the live Futurama deploy:

- 01-artwork-and-images: root-cause of missing posters (EnableInternetProviders=false
  + wrong fetcher names 'The Movie Database' vs 'TheMovieDb' in 10.10.x). Fixed
  in-place. Doc covers image-type matrix, scraper-to-imagetype map, refresh API.
- 02-metadata-and-titles: Futurama series had empty ProviderIds; locked to
  TMDB 615 (1999 original, not 2023 reboot). All 44 eps now have proper titles +
  Polish overviews. Doc covers filename regex, Identify flow, language cascade.
- 03-subtitles: installed OpenSubtitles plugin v20.0.0.0 (v21+ needs JF 10.11
  ABI). User must add opensubtitles.com creds. Doc covers sidecar naming,
  embedded extraction, per-user prefs.
- 04-theming-and-users: applied ElegantFin v25.12.31 via Branding API. Doc covers
  theme rationale, multi-user policy template, SyncPlay, friend playbook.
2026-05-08 01:13:42 +01:00

16 KiB
Raw Blame History

Jellyfin Metadata & Episode Titles — Operator Guide

Server: https://tv.s8n.ru (Jellyfin 10.10.3, container on nullstone) Fixture for this doc: TV Shows library, series Futurama (1999), 44 episodes split across S01S03.


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

curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"SearchInfo":{"Name":"Futurama"}}' \
  https://tv.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

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://tv.s8n.ru/Items/RemoteSearch/Apply/$SERIES_ID?ReplaceAllImages=true"

Response: 204 No Content on success. Note: this endpoint runs synchronously for ~3060 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

curl -s --max-time 60 -X POST -H "X-Emby-Token: $TOKEN" \
  "https://tv.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:

curl -s -H "X-Emby-Token: $TOKEN" \
  "https://tv.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

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://tv.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 defaultDashboard > Display > Metadata language. Affects libraries that don't set their own.
  2. LibraryDashboard > 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:

# Fetch current options
curl -s -H "X-Emby-Token: $TOKEN" https://tv.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://tv.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 tv.s8n.ru (2026-05-08)

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

TOKEN=*redacted*
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://tv.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://tv.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://tv.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://tv.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://tv.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://tv.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