# 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":""}' \ 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 <","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 `@` 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).