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:
s8n 2026-05-08 01:13:42 +01:00
parent a061949ee1
commit f7c872d687
5 changed files with 1506 additions and 2 deletions

View file

@ -49,8 +49,25 @@ deploy:
2. Create the admin user (Jellyfin onboarding wizard). 2. Create the admin user (Jellyfin onboarding wizard).
3. Add libraries pointing at `/media/movies` and `/media/tv` inside the 3. Add libraries pointing at `/media/movies` and `/media/tv` inside the
container (these map to `/home/user/media/{movies,tv}`). container (these map to `/home/user/media/{movies,tv}`).
4. (Optional) Apply Netflix-style theme by pasting a community theme into 4. (Optional) Apply Netflix-style theme — see `docs/04-theming-and-users.md`.
Dashboard → General → Custom CSS.
## 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), S01S03, 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 ## Deploy

View 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 (~1015s of HTTP 503). If you're scripting against the API, sleep before
the next call.
```bash
# Pull current options, mutate, push back
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Library/VirtualFolders" \
| jq '.[] | select(.Name=="TV Shows") | {Id: .ItemId, LibraryOptions: (.LibraryOptions | .EnableInternetProviders=true)}' \
> /tmp/tv-options.json
curl -s -X POST \
-H "Authorization: MediaBrowser Token=$TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/tmp/tv-options.json \
"$BASE/Library/VirtualFolders/LibraryOptions"
# Expect: HTTP 204
```
### A2. Wrong fetcher names in TypeOptions ⟵ THIS WAS US (2/2, the real killer)
In **Jellyfin 10.10.x** the TMDb plugin advertises itself with the name
**`TheMovieDb`** (no spaces). Older Jellyfin versions called it
`The Movie Database`. If your library was upgraded from an old config OR if
you copy-pasted from a 2022 forum post, your `TypeOptions[].MetadataFetchers`
and `ImageFetchers` may say `"The Movie Database"` — and the matcher is
**case+space-strict**. There is no fallback, no warning logged, no error.
The fetcher list silently resolves to *zero matching providers* and refresh
runs to completion in milliseconds without ever calling TMDB.
Symptoms:
- Refresh returns 204, then nothing happens.
- No `tmdb`/`fetch`/`provider` lines in the log at all.
- `ProviderIds` may show `Tmdb: 615` if you manually identified the show via
the UI — that goes through `ItemLookupController`, a different code path
that *does* match by name. But subsequent automatic refresh still does
nothing because the *library* fetcher list doesn't match.
Diagnostic — list what the server actually advertises:
```bash
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Libraries/AvailableOptions?LibraryContentType=tvshows" \
| jq '.TypeOptions[] | {Type, MetadataFetchers: [.MetadataFetchers[].Name], ImageFetchers: [.ImageFetchers[].Name]}'
```
Cross-check those exact strings against your library's TypeOptions.
Fix: rewrite the library's TypeOptions to use the names from
`AvailableOptions` verbatim. Same payload as in fix A, but with
`"TheMovieDb"` everywhere. Then trigger a FullRefresh. The first refresh
fixed the missing series-level data (Overview, Genres, Primary, Logo, Thumb,
Backdrop) and the second one — running without you doing anything because
Jellyfin queues a children-refresh after a parent-refresh — populated all
seasons and all 44 episodes with their stills and ProviderIds.
### B. Outbound DNS / network blocked from container
Symptom: TMDB plugin loaded, IDs not getting set, no obvious log error.
Diagnostic:
```bash
ssh user@192.168.0.100 \
'docker exec jellyfin curl -sI https://api.themoviedb.org/3/configuration --max-time 5'
```
A bare `HTTP/2 401` from `api.themoviedb.org` is **fine** — that's TMDB
rejecting an unauthenticated probe. What matters is that the TLS handshake and
HTTP response happened at all. If you see `Could not resolve host`, fix DNS
(check `/etc/resolv.conf` inside the container, the docker network's DNS, or
Pi-hole upstream).
### C. TMDB rate limit (40 req / 10 s per IP)
Symptom: refresh stalls partway, episode stills missing for some episodes,
others fine. Look for `429` in the logs:
```bash
ssh user@192.168.0.100 'docker logs jellyfin --since 24h 2>&1 | grep -E "429|TooMany"'
```
Fix: nothing — just re-run the refresh. The bundled TMDB key is shared across
all Jellyfin installs but rate-limited per source IP. Big libraries can take
minutes to fully populate on the first scan.
### D. Language fallback with `PreferredMetadataLanguage`
We run `pl` (Polish) with `MetadataCountryCode=PL`. TMDB has very sparse Polish
posters/backdrops for older Western shows. If `pl` returns nothing for a given
ImageType, Jellyfin core asks for language `null` (international, no text) and
then `en` as a final fallback. This works **as long as** TMDB plugin's
"Include image fetcher language" setting includes English — set in
*Dashboard → Plugins → TMDb → Image language*. Usually default-includes `en`.
If you see the metadata in Polish but only English-language posters, that's
working as designed.
### E. NSFW / "Adult" filter blanking images
TMDB has an `include_adult` flag. Jellyfin defaults to *off*. For SFW content
this never matters; for kids-anime imports that TMDB has flagged adult, posters
go missing. Fix: `Dashboard → Plugins → TMDb → Include adult content = on`.
Not relevant to Futurama.
### F. TheTVDB plugin without v4 API key
Old configs from Jellyfin 10.7 listed "TheTVDB" as the default series scraper.
Since 10.9 the bundled key is gone — TVDB went paid. Symptoms: "TheTVDB" in the
Image Fetcher list but no images sourced from there. Either install the
**TheTVDB** plugin from `repo.jellyfin.org` and paste a v4 subscriber API key,
or remove it from the fetcher order and let TMDb cover everything. We did the
latter.
### G. Studios image errors in the log (red herring)
```
StudiosImageProvider failed in GetImageInfos for type Studio at /config/metadata/Studio/Adult Swim
```
The bundled Studio Images plugin scrapes a hard-coded GitHub repo
(`MediaBrowser/jellyfin-plugin-studioimages-data`) for studio logos. That repo
is occasionally unreachable or doesn't have the studio you need. **It does not
affect series/episode/movie artwork at all.** Ignore unless you specifically
want studio logos in the home-screen "Studios" row.
---
## 4. Triggering a metadata + image refresh
Per-item refresh — full replace:
```bash
ITEM_ID="156e57437f795e5c8cd80fc98bafaee0" # Futurama series
curl -s -X POST \
-H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Items/$ITEM_ID/Refresh?metadataRefreshMode=FullRefresh\
&imageRefreshMode=FullRefresh\
&replaceAllMetadata=true\
&replaceAllImages=true\
&regenerateTrickplay=false"
# Expect: HTTP 204
```
Modes:
- `metadataRefreshMode``None | ValidationOnly | Default | FullRefresh`
- `imageRefreshMode` same set
- `replaceAllMetadata=true` wipes Overview/Genres/etc and re-pulls.
- `replaceAllImages=true` wipes attached images and re-pulls. Use this every
time you've changed fetcher order or installed a new image plugin.
- `regenerateTrickplay=true` forces re-extraction of trickplay sprites — heavy,
leave off for metadata-only refreshes.
Refresh recurses into children: Series → Seasons → Episodes. For a single
episode refresh, pass that episode's item ID instead.
Library-wide refresh (rare — usually you want per-item):
```bash
curl -s -X POST -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Library/Refresh"
```
### Watching progress
The Refresh task is asynchronous — `204` just means "queued". Monitor via:
```bash
# Container log (best signal)
ssh user@192.168.0.100 'docker logs jellyfin --tail 200 -f' \
| grep -iE "futurama|tmdb|provider|fetch"
# API: poll the item itself
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Items?Recursive=true&IncludeItemTypes=Series&userId=$USER_ID&\
Fields=ProviderIds,ImageTags,BackdropImageTags" \
| jq '.Items[] | {Name, ProviderIds, ImgKeys: (.ImageTags|keys), Backdrops: (.BackdropImageTags|length)}'
```
`ProviderIds` populates first (within seconds), `ImageTags.Primary` and
backdrops follow after the image fetcher has iterated through TMDB's image
list and cached them locally.
### Inspecting a single item's images
```bash
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Items/$ITEM_ID/Images" | jq
```
Returns an array of `{ImageType, ImageIndex, Path, BlurHash, Width, Height,
Size}`. Empty array = nothing attached.
### Manually identifying a series (when auto-match misses)
```bash
# Search TMDB for a candidate
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" -X POST \
-H "Content-Type: application/json" \
--data '{"SearchInfo":{"Name":"Futurama","Year":1999},"IncludeDisabledProviders":false}' \
"$BASE/Items/RemoteSearch/Series" | jq '.[0]'
# Apply the picked TMDB ID
curl -s -H "Authorization: MediaBrowser Token=$TOKEN" -X POST \
-H "Content-Type: application/json" \
--data '{"ProviderIds":{"Tmdb":"615"}, "Name":"Futurama"}' \
"$BASE/Items/RemoteSearch/Apply/$ITEM_ID?replaceAllImages=true"
```
---
## 5. Installing the Fanart.tv plugin (optional)
Adds clearart / clearlogo / disc-art and richer backdrops. Not needed for the
Futurama fix, but worth knowing for music libraries especially.
The current `https://repo.jellyfin.org/files/plugin/manifest.json` ships
**Fanart 14.0.0.0** with `targetAbi 10.11.5.0` — that requires Jellyfin 10.11+.
This server is 10.10.3, so the latest Fanart build is **not compatible**.
Older builds in the manifest target `10.9.0.0` ABI which is too old for 10.10.
Options:
1. Wait until we upgrade Jellyfin to 10.11+ and then install.
2. Run an old-ABI build by manually dropping a `10.9.0.0`-targeted Fanart DLL
into `/config/plugins/Fanart_<version>/` — works in practice, but requires
a server restart and the user has asked us not to restart.
If/when you do install:
```bash
# Find the plugin GUID
GUID="170a157f-ac6c-437a-abdd-ca9c25cebd39" # Fanart
VERSION="14.0.0.0"
curl -s -X POST -H "Authorization: MediaBrowser Token=$TOKEN" \
"$BASE/Packages/Installed/Fanart?AssemblyGuid=$GUID&Version=$VERSION"
# Then a SERVER RESTART is required. The plugin won't appear in /Plugins
# until restart.
```
After install, edit each library's image fetcher list to include `"Fanart"`,
then trigger `replaceAllImages=true` refresh.
---
## 6. Futurama — what we found and what we did
### Findings
| What | Value |
|-------------------------------------------------------|--------------------------------------|
| Series item ID | `156e57437f795e5c8cd80fc98bafaee0` |
| Path | `/media/tv/Futurama` |
| Seasons / episodes on disk | 3 / 44 |
| `LibraryOptions.EnableInternetProviders` (TV) | **`false`** ← root cause #1 |
| `LibraryOptions.EnableInternetProviders` (Movies) | **`false`** (same bug) |
| Fetcher names in TypeOptions | `"The Movie Database"` ← root cause #2; should be `"TheMovieDb"` in 10.10.x |
| `"TheTVDB"` listed as fetcher | yes — but plugin not installed → silent no-op |
| `ProviderIds` before fix | `{}` — nothing |
| `ImageTags` / `BackdropImageTags` before fix | `{}` / `[]` |
| Plugins installed | TMDb, OMDb, MusicBrainz, AudioDB, Studio Images, Open Subtitles |
| TheTVDB plugin | **NOT installed** |
| Fanart plugin | **NOT installed** (manifest only ships ABI 10.11/10.9 — not compatible with 10.10.3) |
| Container outbound to `api.themoviedb.org` | OK — TLS + HTTP working, returned full image set for tv/615 |
| Logs before fix | Only `StudiosImageProvider` errors (harmless GitHub 404s) — no TMDB activity at all |
### Changes applied
1. **TV Shows library** — flipped `EnableInternetProviders` to `true`,
replaced `"The Movie Database"` and `"TheTVDB"` with `"TheMovieDb"` in
`MetadataFetchers`, `MetadataFetcherOrder`, `ImageFetchers`,
`ImageFetcherOrder` for Series/Season/Episode TypeOptions. Added a
`Season` TypeOptions block (was missing — would have inherited "no
fetchers" silently for season images).
2. **Movies library** — same: `EnableInternetProviders=true`, fetcher
string `"TheMovieDb"`.
3. **PreferredMetadataLanguage** kept as `pl`, `MetadataCountryCode=PL`.
Polish overview and genres landed; English fallback worked for poster art.
4. **Server reloads** — three of them, each triggered automatically by
`POST /Library/VirtualFolders/LibraryOptions`. No Docker restart, no
compose change. (1015s 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.

View 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 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
```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 ~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
```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
View 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 s01s07 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 15; 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`.

View 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).