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.
This commit is contained in:
parent
a061949ee1
commit
f7c872d687
5 changed files with 1506 additions and 2 deletions
21
README.md
21
README.md
|
|
@ -49,8 +49,25 @@ deploy:
|
|||
2. Create the admin user (Jellyfin onboarding wizard).
|
||||
3. Add libraries pointing at `/media/movies` and `/media/tv` inside the
|
||||
container (these map to `/home/user/media/{movies,tv}`).
|
||||
4. (Optional) Apply Netflix-style theme by pasting a community theme into
|
||||
Dashboard → General → Custom CSS.
|
||||
4. (Optional) Apply Netflix-style theme — see `docs/04-theming-and-users.md`.
|
||||
|
||||
## Operations docs
|
||||
|
||||
Detailed playbooks (research-grade, with API curls, failure modes, recovery):
|
||||
|
||||
| File | Topic |
|
||||
|------|-------|
|
||||
| [`docs/01-artwork-and-images.md`](docs/01-artwork-and-images.md) | Posters, backdrops, scrapers (TMDB/TVDB/Fanart), refresh API, language fallback |
|
||||
| [`docs/02-metadata-and-titles.md`](docs/02-metadata-and-titles.md) | Filename parsing, Identify flow, locking the right show, language cascade, multi-episode files |
|
||||
| [`docs/03-subtitles.md`](docs/03-subtitles.md) | OpenSubtitles plugin (.com), sidecar naming, ffmpeg/mkvextract extraction, per-user prefs |
|
||||
| [`docs/04-theming-and-users.md`](docs/04-theming-and-users.md) | ElegantFin theme, branding API, multi-user policies, SyncPlay, friend account playbook |
|
||||
|
||||
## State as of 2026-05-08
|
||||
|
||||
- **Library**: Futurama 1999 series (TMDB 615), S01–S03, 44 episodes, fully scraped (Polish metadata + posters + backdrops + episode stills)
|
||||
- **Theme**: ElegantFin v25.12.31 applied via `/System/Configuration/branding`
|
||||
- **Subtitles**: OpenSubtitles plugin v20 installed; user must add opensubtitles.com creds (free tier = 20 dl/day)
|
||||
- **Users**: 1 admin (`s8n`); friend account creation playbook in doc 04
|
||||
|
||||
## Deploy
|
||||
|
||||
|
|
|
|||
479
docs/01-artwork-and-images.md
Normal file
479
docs/01-artwork-and-images.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
# 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://tv.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://tv.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\
|
||||
®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_<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.
|
||||
281
docs/02-metadata-and-titles.md
Normal file
281
docs/02-metadata-and-titles.md
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# 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 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://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
|
||||
```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://tv.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://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:
|
||||
```bash
|
||||
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
|
||||
```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://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 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://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:
|
||||
|
||||
```bash
|
||||
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
|
||||
323
docs/03-subtitles.md
Normal file
323
docs/03-subtitles.md
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
# 03 — Subtitles on Jellyfin (tv.s8n.ru)
|
||||
|
||||
Last updated: 2026-05-08
|
||||
Server: Jellyfin 10.10.3 (X64) on nullstone, container `jellyfin`
|
||||
URL: <https://tv.s8n.ru>
|
||||
Use-case: Futurama (44 episodes), Polish audio, no embedded subs, automatic English subtitles required.
|
||||
|
||||
---
|
||||
|
||||
## 1. How Jellyfin resolves subtitles (priority order)
|
||||
|
||||
When a client requests playback, Jellyfin presents subtitle streams from three sources, in this order:
|
||||
|
||||
1. **Embedded** — subtitle tracks muxed inside the container (`mkv`, `mp4`, etc.). Codecs: `srt`, `ass`, `ssa`, `pgs`, `vobsub`, `dvd_subtitle`, `mov_text`. Always available.
|
||||
2. **Sidecar** (external) — `.srt`, `.vtt`, `.ass`, `.ssa`, `.sub` files placed next to the video on disk. Discovered automatically on library scan.
|
||||
3. **Downloaded** — fetched at scan time (or via "Find subtitles" UI) by a subtitle provider plugin (OpenSubtitles, etc.). Saved as a sidecar when `SaveSubtitlesWithMedia=true`.
|
||||
|
||||
A user's `SubtitleMode` setting (per-user) decides what is auto-selected at playback:
|
||||
|
||||
- `None` — never load subtitles by default.
|
||||
- `Default` — load only if marked `IsDefault` in the file.
|
||||
- `Always` — always load if any track in `SubtitleLanguagePreference` exists.
|
||||
- `Smart` — load only when audio language differs from the user's audio preference.
|
||||
- `OnlyForced` — load only forced tracks.
|
||||
|
||||
The s8n user is set to **`Always` + preference `eng`** (see § 6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Current Futurama state (verified 2026-05-08)
|
||||
|
||||
```
|
||||
Library: TV Shows (Id 767bffe4f11c93ef34b805451a696a4e, path /media/tv)
|
||||
Episodes: 44 (Futurama.s01e01.pl.mkv … s01e44? — actually s01–s07 PL dub split)
|
||||
Container: mkv
|
||||
Audio: ac3 stereo 192k, language=pol
|
||||
Video: h264 1080p (the eng tag on the video stream is a mux artefact, not English audio)
|
||||
Subs: 0 embedded, 0 sidecar — verified on episodes 1–5; assume same for all 44
|
||||
```
|
||||
|
||||
Result: every episode needs subs from OpenSubtitles **or** sidecars dropped on disk. There is nothing to extract — `mkvextract`/`ffmpeg` would yield nothing.
|
||||
|
||||
---
|
||||
|
||||
## 3. OpenSubtitles plugin — what is installed, what is pending
|
||||
|
||||
### 3.1 Installed (done by automation 2026-05-08)
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Plugin | **Open Subtitles** v20.0.0.0 |
|
||||
| GUID | `4b9ed42f-5185-48b5-9803-6ff2989014c4` |
|
||||
| Status | `Active` (after `docker restart jellyfin`, ~5 s downtime) |
|
||||
| Repo | Official `https://repo.jellyfin.org/files/plugin/manifest.json` (already configured) |
|
||||
| API endpoint | `https://api.opensubtitles.com/api/v1` (REST, NOT the legacy XML-RPC at .org) |
|
||||
| API key | **Embedded in the plugin binary** — user does NOT supply one |
|
||||
|
||||
### 3.2 Why v20 and not v24
|
||||
|
||||
| Plugin version | targetAbi | Compatible with 10.10.3? |
|
||||
|---|---|---|
|
||||
| v24, v23, v22, v21 | 10.11.x | **NO** — ABI too new, will not load |
|
||||
| **v20** | **10.9.0.0** | **YES** — installed |
|
||||
| v19 and older | 10.8.x | yes but lacks recent fixes |
|
||||
|
||||
When the server is upgraded to 10.11.x, switch to v24 via:
|
||||
|
||||
```bash
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" \
|
||||
"https://tv.s8n.ru/Packages/Installed/Open%20Subtitles?AssemblyGuid=4b9ed42f-5185-48b5-9803-6ff2989014c4&Version=24.0.0.0&RepositoryUrl=https%3A%2F%2Frepo.jellyfin.org%2Ffiles%2Fplugin%2Fmanifest.json"
|
||||
docker restart jellyfin
|
||||
```
|
||||
|
||||
### 3.3 The `.com` vs `.org` distinction (READ THIS)
|
||||
|
||||
OpenSubtitles split into two services in 2021:
|
||||
|
||||
- **opensubtitles.org** — legacy site, legacy XML-RPC API. Old Jellyfin plugin (v16 and below) used this. **DEAD** for new code.
|
||||
- **opensubtitles.com** — new site, new REST API. Jellyfin plugin v17+ (including the v20 we have) uses this exclusively.
|
||||
|
||||
User accounts: an opensubtitles.org account does NOT automatically work on .com. Existing .org users must:
|
||||
1. Visit <https://www.opensubtitles.com/en/users/sign_up>.
|
||||
2. Sign up with the same email — the system will offer to import the .org account.
|
||||
3. Reset password (mandatory; the .org password hash is incompatible).
|
||||
|
||||
User does NOT need to obtain an API key. The plugin embeds its own key (verified by reading `OpenSubtitlesPlugin.ApiKey` in `RequestHandler.cs` of v20). Free accounts get **20 downloads/day**; VIP accounts get more.
|
||||
|
||||
### 3.4 Pending — user supplies credentials
|
||||
|
||||
After signup at opensubtitles.com, save creds via API:
|
||||
|
||||
```bash
|
||||
TOKEN=*redacted*
|
||||
USER='your-opensubtitles-com-username'
|
||||
PASS='your-opensubtitles-com-password'
|
||||
|
||||
# 1. Validate (returns 200 on success, 401 on bad creds)
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"Username\":\"$USER\",\"Password\":\"$PASS\"}" \
|
||||
"https://tv.s8n.ru/Jellyfin.Plugin.OpenSubtitles/ValidateLoginInfo" -w "\nHTTP %{http_code}\n"
|
||||
|
||||
# 2. Persist into plugin config
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"Username\":\"$USER\",\"Password\":\"$PASS\",\"CredentialsInvalid\":false}" \
|
||||
"https://tv.s8n.ru/Plugins/4b9ed42f-5185-48b5-9803-6ff2989014c4/Configuration" -w "\nHTTP %{http_code}\n"
|
||||
```
|
||||
|
||||
Or via UI: `Dashboard → Plugins → Open Subtitles → Settings`.
|
||||
|
||||
A failed validate currently returns `{"Message":"Error, invalid username/password failed:5 remaining:5"}` HTTP 401 — the embedded API key is fine, only the user creds are missing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Library + user configuration (already applied)
|
||||
|
||||
### 4.1 Library options — `TV Shows` (and `Movies`)
|
||||
|
||||
`POST /Library/VirtualFolders/LibraryOptions` was issued with:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "767bffe4f11c93ef34b805451a696a4e",
|
||||
"LibraryOptions": {
|
||||
"SubtitleDownloadLanguages": ["eng"],
|
||||
"RequirePerfectSubtitleMatch": false,
|
||||
"SkipSubtitlesIfAudioTrackMatches": true,
|
||||
"SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
|
||||
"SaveSubtitlesWithMedia": true,
|
||||
"AllowEmbeddedSubtitles": "AllowAll"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effect:
|
||||
|
||||
- **`SubtitleDownloadLanguages: ["eng"]`** — primary trigger. Any new scan or "Refresh metadata" will fetch English subs for items lacking them.
|
||||
- **`RequirePerfectSubtitleMatch: false`** — accept good-match subs even when filename hashes don't line up (Polish-dub `Futurama.s01e01.pl.mkv` will not byte-match an English .srt anywhere on the planet).
|
||||
- **`SkipSubtitlesIfAudioTrackMatches: true`** — never fetch a language already present as an audio track (no English audio here, so no effect; safe to leave on).
|
||||
- **`SaveSubtitlesWithMedia: true`** — write `.eng.srt` next to the `.mkv` instead of caching in the metadata folder. Survives library wipes and is portable.
|
||||
|
||||
### 4.2 Per-user playback prefs — user `s8n`
|
||||
|
||||
`POST /Users/2be0f0d3fe3a45dc9298138a15a01925/Configuration`:
|
||||
|
||||
```json
|
||||
{
|
||||
"AudioLanguagePreference": "pol",
|
||||
"SubtitleLanguagePreference": "eng",
|
||||
"SubtitleMode": "Always"
|
||||
}
|
||||
```
|
||||
|
||||
Result: every Futurama episode auto-plays Polish audio + auto-loads English subs.
|
||||
|
||||
Other users (if any) need the same change individually — there is no global "default new user" subtitle prefs in 10.10.x; set on each user.
|
||||
|
||||
---
|
||||
|
||||
## 5. Triggering download for the existing 44 episodes
|
||||
|
||||
Adding `SubtitleDownloadLanguages` does NOT retroactively fetch subs for items already scanned. After credentials are saved, force a search:
|
||||
|
||||
### 5.1 Single episode (test path — episode `Futurama.s01e01.pl`)
|
||||
|
||||
```bash
|
||||
EP=2b73bc176fbf8a02bb9bea9015ec13c6
|
||||
|
||||
# Query providers
|
||||
curl -s -H "X-Emby-Token: $TOKEN" \
|
||||
"https://tv.s8n.ru/Items/$EP/RemoteSearch/Subtitles/eng" | jq .
|
||||
# Returns array of SubtitleInfo objects: Id, ProviderName, Format, Comment, IsHashMatch, ...
|
||||
|
||||
# Pick one, e.g. SUBID = first result's Id
|
||||
SUBID="opensubtitles_..."
|
||||
|
||||
# Download + save next to media
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" \
|
||||
"https://tv.s8n.ru/Items/$EP/RemoteSearch/Subtitles/$SUBID" -w "HTTP %{http_code}\n"
|
||||
# 204 = saved. File appears as Futurama.s01e01.pl.eng.srt next to the mkv.
|
||||
```
|
||||
|
||||
While unauthenticated, `RemoteSearch` returned `[]` — confirms credentials are the only blocker.
|
||||
|
||||
### 5.2 All 44 — refresh metadata for the whole series
|
||||
|
||||
Easiest is a series-level refresh once creds are entered:
|
||||
|
||||
```bash
|
||||
SERIES=156e57437f795e5c8cd80fc98bafaee0 # Futurama
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" \
|
||||
"https://tv.s8n.ru/Items/$SERIES/Refresh?MetadataRefreshMode=FullRefresh&ImageRefreshMode=Default&ReplaceAllMetadata=false&ReplaceAllImages=false&Recursive=true" \
|
||||
-w "HTTP %{http_code}\n"
|
||||
```
|
||||
|
||||
`MetadataRefreshMode=FullRefresh` triggers subtitle download for items missing matching language. This will take a few minutes and is rate-limited by the OpenSubtitles 20-downloads/day cap on free accounts. **44 episodes exceeds that** — plan for 3 days, or upgrade to VIP, or use sidecars (§ 7).
|
||||
|
||||
---
|
||||
|
||||
## 6. Per-user vs library default — who wins
|
||||
|
||||
| Setting | Lives in | Effect |
|
||||
|---|---|---|
|
||||
| `SubtitleDownloadLanguages` (library) | Library options | What languages are *fetched* and *saved to disk* |
|
||||
| `SubtitleLanguagePreference` (user) | User config | Which existing track is *auto-selected at playback* |
|
||||
| `SubtitleMode` (user) | User config | When to auto-display (`Always` / `Default` / `OnlyForced` / `Smart` / `None`) |
|
||||
| `AudioLanguagePreference` (user) | User config | Which audio track is auto-selected |
|
||||
|
||||
A user's preferences are *display-time* only — they cannot trigger a download. A library's `SubtitleDownloadLanguages` is *scan-time* only — it does not affect what plays. You need both, and they should agree.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sidecar subtitles — naming convention
|
||||
|
||||
If/when subs come from OpenSubtitles, Jellyfin saves them as sidecars (because `SaveSubtitlesWithMedia=true`). You can also drop your own. Filename pattern:
|
||||
|
||||
```
|
||||
<videobasename>.<lang>[.flag].<ext>
|
||||
```
|
||||
|
||||
Examples for `Futurama.s01e01.pl.mkv`:
|
||||
|
||||
| Filename | Meaning |
|
||||
|---|---|
|
||||
| `Futurama.s01e01.pl.en.srt` | English, regular |
|
||||
| `Futurama.s01e01.pl.eng.srt` | English (ISO-639-2 code, also accepted) |
|
||||
| `Futurama.s01e01.pl.en.forced.srt` | English forced (only foreign-language scenes) |
|
||||
| `Futurama.s01e01.pl.en.sdh.srt` | English SDH (deaf/hard-of-hearing — includes [music], [door slams]) |
|
||||
| `Futurama.s01e01.pl.en.cc.srt` | English closed captions (alias of SDH in Jellyfin) |
|
||||
| `Futurama.s01e01.pl.en.default.srt` | Marked default — auto-selects regardless of `SubtitleMode` |
|
||||
| `Futurama.s01e01.pl.en.forced.default.srt` | Forced AND default (flags compose, any order) |
|
||||
| `Futurama.s01e01.pl.en.ass` | Advanced SubStation Alpha — supports styling/positions |
|
||||
| `Futurama.s01e01.pl.en.ssa` | SubStation Alpha — older, also supported |
|
||||
| `Futurama.s01e01.pl.en.vtt` | WebVTT — supported |
|
||||
| `Futurama.s01e01.pl.en.sub` + `.idx` | VobSub (DVD bitmap subs) |
|
||||
|
||||
**Language codes:** Jellyfin accepts both ISO-639-1 (`en`, `pl`, `de`) and ISO-639-2/T (`eng`, `pol`, `deu`). Either works for filename matching — the parser canonicalises to 639-2 internally.
|
||||
|
||||
**Region tags** like `en-US`, `pt-BR` are accepted and shown to the user but treated as the bare language for matching.
|
||||
|
||||
After dropping sidecars, trigger a library scan:
|
||||
|
||||
```bash
|
||||
curl -s -X POST -H "X-Emby-Token: $TOKEN" "https://tv.s8n.ru/Library/Refresh"
|
||||
```
|
||||
|
||||
Or per-item: `POST /Items/{id}/Refresh`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Extracting embedded subs to sidecars (irrelevant for Futurama, but documented)
|
||||
|
||||
If a future series has embedded but slow-loading PGS/SSA subs, extract once with `ffmpeg`:
|
||||
|
||||
```bash
|
||||
# List streams
|
||||
ffmpeg -i input.mkv 2>&1 | grep Subtitle
|
||||
|
||||
# Extract stream index 2 (first sub) to .srt — works for srt/ass/mov_text
|
||||
ffmpeg -i input.mkv -map 0:s:0 -c:s copy input.en.srt
|
||||
|
||||
# For PGS or VobSub bitmap subs, copy keeps them as-is (rename .sup):
|
||||
ffmpeg -i input.mkv -map 0:s:0 -c:s copy input.en.sup
|
||||
# Then OCR with subtitleedit-cli or pgs2srt to convert to text srt.
|
||||
```
|
||||
|
||||
Or with `mkvextract` (mkvtoolnix-cli):
|
||||
|
||||
```bash
|
||||
mkvmerge -i input.mkv # lists tracks with IDs
|
||||
mkvextract tracks input.mkv 2:input.en.srt
|
||||
```
|
||||
|
||||
Jellyfin also has a built-in plugin **Subtitle Extract** (manifest GUID `cd893c24-b59e-4060-87b2-184070e1bf68`, latest v7.0.0.0 needs ABI 10.11.2.0 — wait until 10.11 upgrade). It extracts on the fly and caches in `metadata/Subtitles/`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Auto-download settings cheatsheet
|
||||
|
||||
| Field | Where set | Recommended value | Why |
|
||||
|---|---|---|---|
|
||||
| `SubtitleDownloadLanguages` | Library options | `["eng"]` | Trigger downloads in English |
|
||||
| `RequirePerfectSubtitleMatch` | Library options | `false` | Don't insist on hash-match for foreign-dub releases |
|
||||
| `SkipSubtitlesIfEmbeddedSubtitlesPresent` | Library options | `false` | Allow override fetch even if embedded subs exist (e.g. if embedded are forced-only) |
|
||||
| `SkipSubtitlesIfAudioTrackMatches` | Library options | `true` | Don't fetch English subs for English audio (no waste) |
|
||||
| `SaveSubtitlesWithMedia` | Library options | `true` | Subs saved as sidecars on disk; portable; survives metadata wipe |
|
||||
| `AllowEmbeddedSubtitles` | Library options | `AllowAll` | Default; `AllowText` excludes PGS bitmap |
|
||||
| `MaxResults` | (n/a in v20 plugin config) | — | The plugin returns the provider's full list; client picks |
|
||||
| `IsAutomated` | (n/a, removed) | — | Older plugins had a flag; v20 always auto-fetches when a library scan finds an item missing the configured language |
|
||||
|
||||
---
|
||||
|
||||
## 10. Troubleshooting
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
|---|---|
|
||||
| `RemoteSearch` returns `[]` for every episode | Creds missing / wrong (HTTP 401 in plugin logs). Re-validate via `ValidateLoginInfo`. |
|
||||
| `429 Too Many Requests` in logs | Hit the 20/day quota on free account. Wait 24 h, upgrade to VIP, or fall back to sidecars. |
|
||||
| Subs found but in wrong language | OpenSubtitles can mislabel — set `RequirePerfectSubtitleMatch: true` to filter, or pick manually via UI. |
|
||||
| `.srt` on disk but Jellyfin doesn't show it | Filename language token doesn't match. Use `.en.srt` not `.english.srt`. Trigger library scan. |
|
||||
| Subs show but client doesn't auto-display | User-side. Set `SubtitleMode: Always` and `SubtitleLanguagePreference: eng`. |
|
||||
| Embedded subs preferred over downloaded | Expected — embedded come first in the priority order. Use the player's track switcher, or remux without subs. |
|
||||
| `CredentialsInvalid: true` keeps reappearing | Plugin auto-flips this on a 401. Re-enter creds (likely changed on opensubtitles.com) and reset to `false`. |
|
||||
| Plugin v24 install but stuck on `Restart` forever after upgrade | Server still on 10.10.x — v24 needs ABI 10.11. Reinstall v20. |
|
||||
|
||||
Plugin logs: `docker logs jellyfin 2>&1 | grep -i opensubtitles`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Summary — Futurama after this work
|
||||
|
||||
| Step | State |
|
||||
|---|---|
|
||||
| Plugin **Open Subtitles v20.0.0.0** | Installed + Active |
|
||||
| TV library `SubtitleDownloadLanguages` | `["eng"]` |
|
||||
| TV library `RequirePerfectSubtitleMatch` | `false` |
|
||||
| TV library `SaveSubtitlesWithMedia` | `true` |
|
||||
| User `s8n` `SubtitleMode` | `Always` |
|
||||
| User `s8n` `SubtitleLanguagePreference` | `eng` |
|
||||
| User `s8n` `AudioLanguagePreference` | `pol` |
|
||||
| OpenSubtitles **credentials** | **PENDING — user signs up at <https://www.opensubtitles.com>** |
|
||||
| Series refresh to fetch all 44 | **PENDING — after creds entered** |
|
||||
|
||||
When the user enters creds and runs the series refresh in § 5.2, expect ~20 episodes downloaded the first day (free quota), the rest over the next two days unless upgraded. Sidecar filenames will be `Futurama.s01eXX.pl.eng.srt` next to each `.mkv`.
|
||||
404
docs/04-theming-and-users.md
Normal file
404
docs/04-theming-and-users.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# 04 — Theming and Users
|
||||
|
||||
Status: applied 2026-05-08 against `https://tv.s8n.ru` (Jellyfin 10.10.3 on nullstone).
|
||||
Scope: visual theme, server-side branding, multi-user UX prep, SyncPlay,
|
||||
maintenance/revert. LAN-only constraints preserved (no public-facing changes).
|
||||
|
||||
---
|
||||
|
||||
## 1. Theme decision: ElegantFin
|
||||
|
||||
### Candidates surveyed
|
||||
|
||||
| Theme | Type | Maintenance (May 2026) | Notes |
|
||||
|---|---|---|---|
|
||||
| Jellyfin built-in CSS variables | first-party | n/a | Minimal. Recolour only, no layout polish. |
|
||||
| **ElegantFin** (lscambo13) | community CSS | **active — v25.12.31 (Dec 2025)**, tested 10.11.5 | Jellyseerr-inspired. Single-import, jsDelivr-hosted. |
|
||||
| Ultrachromic (CTalvio) | community CSS | "selectively maintained, project is old" | Three presets (mono / kaleido / nova). Risk of breaking on newer JF. |
|
||||
| JellyFlix (prayag17) | community CSS | **development halted** per repo notice | Most Netflix-look-alike but stale. |
|
||||
| DarkFlix (DevilsDesigns) | community CSS, fork of JellyFlix | sporadic | Inherits JellyFlix risk. |
|
||||
| Theme Park (Jellyfin pack) | multi-app CSS | active | dracula/nord/hotline/plex variants. Less Netflix, more "skin pack". |
|
||||
| Jellyfin-Vue | full alt web client | active | Replaces the entire web UI. Out of scope: violates "theme via CSS only" constraint and forces an image swap. |
|
||||
| Finetwo / JellyfinNetflixWeb | fork-style replacement | varies | Same constraint violation. |
|
||||
|
||||
### Why ElegantFin
|
||||
|
||||
1. **Most recent activity by far** — v25.12.31 released 31 Dec 2025; tested
|
||||
on Jellyfin 10.11.5 which means it'll keep working as we upgrade past
|
||||
our current 10.10.3.
|
||||
2. **Single `@import` line** — zero ops overhead. CDN-hosted on jsDelivr
|
||||
with `cache-control: public, max-age=604800`. No assets to host
|
||||
ourselves. Revert = clear one field.
|
||||
3. **Jellyseerr-inspired** — modern dark UI with rounded cards, hero
|
||||
backdrops, smooth hover, condensed sidebar. Closer to "premium
|
||||
streaming" feel than Netflix's red-and-black, and ages better.
|
||||
4. **Doesn't touch the upstream image** — we stay on `jellyfin/jellyfin:10.10.3`.
|
||||
5. **Compatible with multi-user setup** — applies server-wide via
|
||||
`Branding.CustomCss`, every user inherits.
|
||||
6. **Not the JellyWatch 2026 darling** (that was JellyFlix) but JellyFlix
|
||||
is explicitly halted. ElegantFin is the safer, longer-lived pick.
|
||||
|
||||
### What it looks like
|
||||
|
||||
- Inter typeface throughout (loaded from Google Fonts inside the CSS).
|
||||
- Dark-only colour scheme (`color-scheme: dark`), primary background
|
||||
`#111827`, secondary `#1d2635` (Tailwind slate territory).
|
||||
- Backdrop hero on item pages with darker bottom-gradient overlay.
|
||||
- Rounded cards (~10px radius), subtle shadow, hover lift.
|
||||
- "ElegantFin v25.12.31" tag in the footer (visible to users — fine for us).
|
||||
- Login screen restyled into a centred card on a blurred backdrop.
|
||||
|
||||
---
|
||||
|
||||
## 2. How it was applied
|
||||
|
||||
### Branding API (already done 2026-05-08)
|
||||
|
||||
```bash
|
||||
TOKEN=*redacted*
|
||||
|
||||
cat > /tmp/branding.json <<'EOF'
|
||||
{
|
||||
"LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.",
|
||||
"CustomCss": "/* ElegantFin v25.12.31 — Jellyseerr-inspired Netflix-y theme */\n@import url(\"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css\");\n",
|
||||
"SplashscreenEnabled": true
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST \
|
||||
-H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/branding.json \
|
||||
https://tv.s8n.ru/System/Configuration/branding
|
||||
# expect: HTTP 204
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
curl -sS -H "X-Emby-Token: $TOKEN" \
|
||||
https://tv.s8n.ru/System/Configuration/branding | python3 -m json.tool
|
||||
# Should include the @import line and the disclaimer text.
|
||||
|
||||
# Public branding endpoint the SPA reads at runtime — confirms anonymous
|
||||
# clients (i.e. the browser before login) will see the theme:
|
||||
curl -sS https://tv.s8n.ru/Branding/Configuration | python3 -m json.tool
|
||||
```
|
||||
|
||||
`/` returns Jellyfin's SPA shell; the theme CSS is fetched **at runtime**
|
||||
by the JS bundle from `/Branding/Configuration`, not inlined into
|
||||
`index.html`. So `curl /` won't grep-match. The valid JSON at
|
||||
`/Branding/Configuration` is the API-level confirmation. Final check is a
|
||||
hard browser reload (cache-bust) on `https://tv.s8n.ru` from the LAN.
|
||||
|
||||
### Cache clear
|
||||
|
||||
Jellyfin web caches aggressively in browsers. After applying:
|
||||
|
||||
- Users: hard reload (Ctrl-Shift-R / Cmd-Shift-R) once.
|
||||
- Server: no restart needed; CustomCss change is live on next page load.
|
||||
|
||||
---
|
||||
|
||||
## 3. Server-side branding state (as of 2026-05-08)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `LoginDisclaimer` | "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind." |
|
||||
| `CustomCss` | `@import` of ElegantFin v25.12.31 from jsDelivr (autoupdating off `@main`) |
|
||||
| `SplashscreenEnabled` | `true` |
|
||||
|
||||
`SplashscreenEnabled: true` makes Jellyfin auto-pick a backdrop from the
|
||||
library and serve it at `/Branding/Splashscreen`. The web client doesn't
|
||||
itself surface this; mobile/TV clients do. Harmless to leave on.
|
||||
|
||||
---
|
||||
|
||||
## 4. Multi-user UX prep
|
||||
|
||||
### 4a. Library inventory
|
||||
|
||||
| Library | Type | ItemId |
|
||||
|---|---|---|
|
||||
| Movies | movies | `f137a2dd21bbc1b99aa5c0f6bf02a805` |
|
||||
| TV Shows | tvshows | `767bffe4f11c93ef34b805451a696a4e` |
|
||||
| Playlists | playlists | `1071671e7bffa0532e930debee501d2e` |
|
||||
|
||||
### 4b. Existing users
|
||||
|
||||
| Name | UserId | Admin? |
|
||||
|---|---|---|
|
||||
| s8n | `2be0f0d3fe3a45dc9298138a15a01925` | yes |
|
||||
|
||||
### 4c. Creating a new user (UI)
|
||||
|
||||
1. Dashboard → Users → "+ Add User".
|
||||
2. Username + initial password. Tick "User can manage server" OFF.
|
||||
3. After creation, click the user → tabs:
|
||||
- **Profile**: language, audio default, subtitle default. Set per
|
||||
user; doesn't have to match server defaults.
|
||||
- **Library Access**: untick "Enable access to all libraries", tick
|
||||
only the libraries this user should see.
|
||||
- **Parental Control**: max rating, blocked tags, access schedule.
|
||||
- **Password**: set / reset.
|
||||
|
||||
### 4d. Creating a new user (API) — playbook
|
||||
|
||||
> **Do not run this without explicit user request.** Documented for the
|
||||
> friend account that will exist later.
|
||||
|
||||
```bash
|
||||
TOKEN=*redacted*
|
||||
TVSHOWS_ID=767bffe4f11c93ef34b805451a696a4e
|
||||
|
||||
# 1. Create the user (auth header REQUIRED — admin token).
|
||||
NEW_USER=$(curl -sS -X POST \
|
||||
-H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"Name":"friend","Password":"<initial-password>"}' \
|
||||
https://tv.s8n.ru/Users/New)
|
||||
echo "$NEW_USER" | python3 -m json.tool
|
||||
NEW_ID=$(echo "$NEW_USER" | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")
|
||||
echo "NEW_ID=$NEW_ID"
|
||||
|
||||
# 2. Tighten the policy: TV-only, non-admin, can change own prefs,
|
||||
# no content deletion, SyncPlay enabled (so we can co-watch).
|
||||
cat > /tmp/policy.json <<EOF
|
||||
{
|
||||
"IsAdministrator": false,
|
||||
"IsHidden": false,
|
||||
"IsDisabled": false,
|
||||
"EnableContentDeletion": false,
|
||||
"EnableUserPreferenceAccess": true,
|
||||
"EnableRemoteAccess": true,
|
||||
"EnableSharedDeviceControl": false,
|
||||
"EnableLiveTvAccess": false,
|
||||
"EnableLiveTvManagement": false,
|
||||
"EnableMediaPlayback": true,
|
||||
"EnableAudioPlaybackTranscoding": true,
|
||||
"EnableVideoPlaybackTranscoding": true,
|
||||
"EnablePlaybackRemuxing": true,
|
||||
"EnableAllFolders": false,
|
||||
"EnabledFolders": ["$TVSHOWS_ID"],
|
||||
"EnableAllChannels": false,
|
||||
"EnabledChannels": [],
|
||||
"EnableAllDevices": true,
|
||||
"BlockedTags": [],
|
||||
"BlockedMediaFolders": [],
|
||||
"MaxParentalRating": null,
|
||||
"AccessSchedules": [],
|
||||
"SyncPlayAccess": "CreateAndJoinGroups",
|
||||
"InvalidLoginAttemptCount": 0,
|
||||
"LoginAttemptsBeforeLockout": 5,
|
||||
"MaxActiveSessions": 0
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST \
|
||||
-H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/policy.json \
|
||||
"https://tv.s8n.ru/Users/$NEW_ID/Policy"
|
||||
# expect: HTTP 204
|
||||
|
||||
# 3. Verify
|
||||
curl -sS -H "X-Emby-Token: $TOKEN" \
|
||||
"https://tv.s8n.ru/Users/$NEW_ID" | python3 -m json.tool
|
||||
```
|
||||
|
||||
### 4e. Policy field cheat-sheet
|
||||
|
||||
| Field | What it does | Recommended for `friend` |
|
||||
|---|---|---|
|
||||
| `IsAdministrator` | Full server admin | **false** |
|
||||
| `EnableContentDeletion` | Can delete media items via UI | **false** |
|
||||
| `EnableUserPreferenceAccess` | Change own profile/audio/sub prefs | **true** |
|
||||
| `EnableAllFolders` | Master switch for library ACL | **false** |
|
||||
| `EnabledFolders` | Whitelist of CollectionFolder Ids | `[TVShows]` (only) |
|
||||
| `BlockedTags` | Skip items tagged with these | optional; e.g. `["adult","unrated"]` |
|
||||
| `MaxParentalRating` | Hide above this rating | `null` for friend (adult). Set `15` for a kid. |
|
||||
| `AccessSchedules` | Day-of-week + time windows | `[]` (no restriction) |
|
||||
| `SyncPlayAccess` | `CreateAndJoinGroups` / `JoinGroups` / `None` | `CreateAndJoinGroups` |
|
||||
| `MaxActiveSessions` | Concurrent sessions cap; 0 = unlimited | `2` if you want to throttle |
|
||||
| `LoginAttemptsBeforeLockout` | Brute-force protection | `5` |
|
||||
| `EnableLiveTvAccess` / `Management` | Live TV / DVR | **false** (we don't run it) |
|
||||
|
||||
### 4f. Password reset flow
|
||||
|
||||
The friend forgot their password. Two routes:
|
||||
|
||||
- **Self-serve (only if SMTP is configured — we don't currently)**: login
|
||||
page → "Forgot Password". Jellyfin emits a PIN file at
|
||||
`/config/data/passwordreset-*.json` valid 30 minutes. Without SMTP, the
|
||||
admin reads the PIN out of the container and gives it to the friend.
|
||||
- **Admin reset (what we'll do)**:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"NewPw":"<new-password>","ResetPassword":false}' \
|
||||
"https://tv.s8n.ru/Users/$USER_ID/Password"
|
||||
```
|
||||
|
||||
To clear the password entirely (forces friend to set one on next login):
|
||||
same call with `"ResetPassword": true` and no `NewPw`.
|
||||
|
||||
### 4g. Easy Pin / quick login
|
||||
|
||||
Jellyfin's built-in equivalent is **Quick Connect**:
|
||||
|
||||
- Dashboard → General → "Allow Quick Connect" (server-wide toggle).
|
||||
- Friend opens a Jellyfin client (TV app, mobile), taps "Quick Connect" →
|
||||
6-digit code.
|
||||
- They enter the code in any already-logged-in browser session under
|
||||
Settings → Quick Connect → Authorize.
|
||||
|
||||
Casual-friendly and avoids them typing passwords on a TV remote. **We
|
||||
have not enabled it yet** — flip on when friend account is created.
|
||||
|
||||
### 4h. Per-user defaults (profile UI)
|
||||
|
||||
Set on each user's profile page (or via `/Users/{id}/Configuration` API):
|
||||
|
||||
- `AudioLanguagePreference`: `eng`
|
||||
- `SubtitleLanguagePreference`: `eng`
|
||||
- `SubtitleMode`: `Smart` (only show when audio differs) or `Always`.
|
||||
- `PlayDefaultAudioTrack`: true.
|
||||
- Display language: pick on first login.
|
||||
|
||||
---
|
||||
|
||||
## 5. Watching together / continue-watching
|
||||
|
||||
### 5a. Resume / Next Up / Up Next — how Jellyfin builds them
|
||||
|
||||
- **Continue Watching** ("Resume" row): items where `UserData.PlaybackPositionTicks > 0`
|
||||
and not yet `Played: true`. Threshold for "watched" is server-side
|
||||
~90% by default. Per-user.
|
||||
- **Next Up**: for series the user has started, Jellyfin walks the
|
||||
next unwatched episode in season/episode order. Configurable in
|
||||
Dashboard → Display → Next Up (max age, rewatching toggle).
|
||||
- **Up Next** (the in-player auto-advance card): client-side feature in
|
||||
the web/mobile players, fed by the same Next Up logic.
|
||||
|
||||
No action needed — these light up automatically once a user has played
|
||||
something. Futurama is loaded, so as soon as anyone plays an episode,
|
||||
the homepage gets populated.
|
||||
|
||||
### 5b. SyncPlay (synchronised group playback)
|
||||
|
||||
Server-side: nothing to enable, ships on. Per-user permission lives in
|
||||
`Policy.SyncPlayAccess`:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `CreateAndJoinGroups` | Can start a SyncPlay group + invite | (s8n + recommended for friend) |
|
||||
| `JoinGroups` | Can only join existing groups | useful for kid accounts |
|
||||
| `None` | Disabled | — |
|
||||
|
||||
Verified current state: `s8n.SyncPlayAccess = CreateAndJoinGroups` ✓.
|
||||
|
||||
**How to use**:
|
||||
|
||||
1. s8n opens a series episode and starts playing.
|
||||
2. Player overlay → top-right people-icon ("SyncPlay") → "Create group".
|
||||
3. Friend logs in (any device — same `tv.s8n.ru`), opens the same item
|
||||
or the SyncPlay menu → "Join {s8n}'s group".
|
||||
4. Anyone in the group's play/pause/seek is mirrored within ~1 second.
|
||||
5. Voice chat is up to you — Jellyfin doesn't bundle one (Matrix room
|
||||
on `txt.s8n.ru` works fine; or just a phone call).
|
||||
|
||||
Caveat: SyncPlay uses WebSockets. Our reverse proxy (`traefik`) handles
|
||||
WS by default, no changes needed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Maintenance
|
||||
|
||||
### 6a. Updating the theme
|
||||
|
||||
ElegantFin's `@import` URL pins to `@main` on jsDelivr — meaning new
|
||||
upstream commits propagate after jsDelivr's cache TTL (12h s-maxage,
|
||||
7d max-age). To pull immediately:
|
||||
|
||||
```bash
|
||||
# Force refresh by pinning to a specific tag, then back to main:
|
||||
curl -sS -X POST -H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"CustomCss": "@import url(\"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@v25.12.31/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css\");", "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \
|
||||
https://tv.s8n.ru/System/Configuration/branding
|
||||
```
|
||||
|
||||
Or just ask each user to hard-reload — their browser cache is the
|
||||
common bottleneck, not jsDelivr.
|
||||
|
||||
When upgrading Jellyfin (e.g. 10.10.3 → 10.11.x), check
|
||||
[the ElegantFin release notes](https://github.com/lscambo13/ElegantFin/releases)
|
||||
first. The current theme is tagged tested-against 10.11.5, so we're
|
||||
forward-compatible through that.
|
||||
|
||||
### 6b. Reverting
|
||||
|
||||
Empty out the CustomCss field via API:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST -H "X-Emby-Token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"CustomCss": "", "LoginDisclaimer": "", "SplashscreenEnabled": false}' \
|
||||
https://tv.s8n.ru/System/Configuration/branding
|
||||
```
|
||||
|
||||
Or in the UI: Dashboard → General → clear "Custom CSS code" → Save.
|
||||
Hard-reload browsers. Vanilla Jellyfin returns instantly.
|
||||
|
||||
### 6c. Pinning a known-good revision
|
||||
|
||||
If `@main` ships a regression, switch the URL to a specific release tag
|
||||
(e.g. `@v25.12.31`). Tags are in the GitHub releases page. jsDelivr
|
||||
serves `@<tag>` immutably and forever.
|
||||
|
||||
---
|
||||
|
||||
## 7. First-30-minutes UX checklist (new user)
|
||||
|
||||
When the friend gets their account, walk them through this **once**:
|
||||
|
||||
1. **Login** → see the LAN-only disclaimer; that's the right server.
|
||||
2. **Profile picture** → set one (just helps SyncPlay group UX).
|
||||
3. **Display preferences** (top-right user icon → Display):
|
||||
- Theme: keep "Dark" (ElegantFin is dark-only, light theme will look
|
||||
half-applied). Don't switch.
|
||||
- Landing screen: Home.
|
||||
4. **Playback preferences**:
|
||||
- Default audio language: `English`.
|
||||
- Default subtitle language: `English`.
|
||||
- Subtitle mode: `Smart` (auto-show on foreign audio).
|
||||
- "Play next episode automatically": on (this is what enables Up Next).
|
||||
5. **Quality** — first-time playback test on Futurama:
|
||||
- Pick S01E01, play. Click the gear → quality. If it stutters on
|
||||
1080p, drop to 720p; transcoder is CPU-only on nullstone today
|
||||
(GTX 1660 Ti driver still broken — see `README.md`).
|
||||
- Once confirmed playing, that quality is remembered per device.
|
||||
6. **SyncPlay test**: friend in one tab, s8n in another, friend joins
|
||||
s8n's group, confirm play/pause syncs. (Drops the "do you have it
|
||||
running" question forever.)
|
||||
7. **Mobile/TV**: install Jellyfin app, server URL `https://tv.s8n.ru`
|
||||
(must be on LAN or Tailscale), Quick Connect or password.
|
||||
8. **Bookmarks/RSS**: there isn't one — Jellyfin's "Latest" row is the
|
||||
substitute. Friend can favourite shows (heart icon) to pin.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open items / future work
|
||||
|
||||
- [ ] Enable Quick Connect when friend account is created (Dashboard →
|
||||
General → Quick Connect).
|
||||
- [ ] Configure SMTP for self-serve password reset (currently admin-only).
|
||||
- [ ] Replace `@main` pin with `@v25.12.31` if we hit upstream churn.
|
||||
- [ ] Add a 2nd library (movies are mounted but the server may have an
|
||||
empty Movies folder — confirm with friend's first ask).
|
||||
- [ ] After GPU driver fix on nullstone, NVENC transcode → 1080p HEVC
|
||||
will stop being CPU-bound; revisit per-user quality defaults.
|
||||
- [ ] Optional: tweak `--elegantFinFooterText` CSS var to drop the
|
||||
ElegantFin version label from the footer (cosmetic).
|
||||
Loading…
Reference in a new issue