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

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

16 KiB

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)

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

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.

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):
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
JoinGroups Can only join existing groups
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:

# 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 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:

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