# 04 — Theming and Users Status: re-themed 2026-05-08 against `https://arrflix.s8n.ru` (Jellyfin 10.10.3 on nullstone). Active theme: **Cineplex v1.0.6** (Netflix-faithful). Replaced ElegantFin v25.12.31 the same day after a Netflix-fidelity-driven survey. Scope: visual theme, server-side branding, multi-user UX prep, SyncPlay, maintenance/revert. LAN-only constraints preserved (no public-facing changes). > Hostname note: this site is being renamed `tv.s8n.ru` → `arrflix.s8n.ru` > in the same session. The Jellyfin API endpoints don't care about > hostname — they're served by the same container. All `curl` examples > below are reachable as either `https://tv.s8n.ru/...` (legacy) or > `https://arrflix.s8n.ru/...` (new), as long as Traefik has a SNI cert > for the name. Internal pin: both names should resolve to `192.168.0.100` > (see CLAUDE.md memory `feedback_s8n_hosts_override.md`). If a > hostname's DNS or cert isn't up yet, use > `--resolve tv.s8n.ru:443:192.168.0.100` on curl — that's how this > re-theming was applied while `arrflix.s8n.ru` was still missing a cert. --- ## 1. Theme decision: Cineplex v1.0.6 (Netflix-faithful) ### Candidates surveyed (2026-05-08) | Theme | Type | Last commit | License | Netflix-fidelity | JF 10.10.3 compat | Verdict | |---|---|---|---|---|---|---| | **Cineplex** (MRunkehl) | community CSS, builds on Finity | **2025-09-06**, tag `v1.0.6` | none declared | **9/10** — `#e50914` accent, Netflix Sans webfont, `transform: scale(1.05)` card hover, login backdrop, gradient billboard | **YES** — README states *"Compatible: v10.10.7 and higher"*; renders on 10.10.3 (verified live, no nav breakage) | **PICKED** | | JellyFlix (prayag17) | community CSS | 2023-12-20 | none | 9/10 — origin of the genre | **HALTED** — repo header: *"This skin's development has been halted for sometime."* Confirmed broken post-10.11. Risky on 10.10.3 too. | rejected | | DarkFlix v5.1 (DevilsDesigns) | fork of JellyFlix | 2024-06 | GPL-3.0 | 8/10 | only states 10.8.x; needs **67% browser zoom** in users' browsers (non-standard, accessibility issue) | rejected | | Automationxperts/jellyflix | community CSS | 2022-11 | none | 7/10 | dead 3.5y, untested on 10.10 | rejected | | **ElegantFin v25.12.31** (lscambo13, previous) | community CSS | 2026-04-30 | GPL-2.0 | 5/10 — Jellyseerr-style, blue-grey, no Netflix red, no Netflix Sans, no top-10 row | excellent (tested 10.11.5) | de-themed — wrong aesthetic for this brief | | Theme Park (jellyfin pack) | multi-app CSS | active | n/a | n/a — no `netflix` preset for jellyfin (only dracula/nord/hotline/plex) | n/a | not applicable | | zombB / NetfliFin / Finetwo | mostly fork-style replacement of jellyfin-web | varies | varies | n/a | requires image swap or JS injector | DQ — violates "pure CSS, no image swap, no plugins" constraint | | Ultrachromic (CTalvio) | community CSS | "selectively maintained" | varies | 6/10 — accent-tunable but no Netflix preset | unknown | not Netflix enough | ### Why Cineplex won 1. **It is actually Netflix.** The CSS literally embeds Netflix Sans (`https://assets.nflxext.com/ffe/siteui/fonts/netflix-sans/v3/...`) and uses Netflix's exact `#E50914` for `--accent` / `--focus-color`. Card hover applies `transform: scale(1.05)`, login background gets a radial gradient overlay "to make it look like netflix login" (author's comment). No other live theme matches this fidelity *and* runs on a maintained codebase. 2. **It targets our Jellyfin series.** Header line of `cineplex.css` reads `Compatible: v10.10.7 and higher`. We're on 10.10.3 — same minor series, ABI-compatible for selectors. Verified live: page loads, navigation works, no broken layouts. 3. **Single `@import` line.** Zero ops overhead. The CSS imports two transitive deps internally (`finity-complete.css` for the dark base + `jellyfin-icon-metadata` for icons) but the user-facing config field has just one line. 4. **Pinned to immutable tag** `@v1.0.6`. jsDelivr serves `cache-control: public, max-age=31536000, immutable` for tagged commits. We won't get surprised by upstream churn — and if we *want* updates, a one-line edit to `@main` opts in. 5. **Companion `cineplex.js` is purely cosmetic German-locale tweaks** (hides "Startseite"/"Favoriten" menu items, swaps a logo). Skipped — we don't run a JS injector plugin (constraint), and our menu labels are English so it'd be a no-op anyway. Theme works fine without it. 6. **Cast/crew hide rule still appended** at the bottom of `CustomCss`, exactly as before. ### Tradeoffs (honest list) - **License: none.** Cineplex doesn't declare one. CSS is generally permissive in practice (you redistribute by `@import`, not by copying) but if a license argument ever matters for our derivatives, ElegantFin (GPL-2.0) is cleaner. - **Bus factor: 1.** Single author (Maverick Runkehl), 0 stars, last commit Sep 2025. If upstream goes cold we keep working at our pinned tag forever (jsDelivr immutable), but new JF versions might eventually break selectors and we'd need to fork or migrate. - **Netflix Sans license note.** The font files are loaded from Netflix's own CDN, not bundled. If Netflix changes that path or rate- limits non-netflix.com referers, we'd fall back to system-ui (also declared in the stack). Acceptable. - **Theme footer.** Cineplex doesn't add a brand stamp, so users see no "Cineplex" tag — cleaner than ElegantFin's footer label was. ### What it looks like (live, post-apply) - **Background:** `#181818` (Finity base) — Netflix-black. - **Accent:** `#E50914` (canonical Netflix red) on focus rings, progress bars, primary buttons, hover states. - **Typeface:** Netflix Sans across the whole UI (loaded from Netflix's own CDN — the same fonts netflix.com itself serves). Subtitles also use Netflix Sans Medium. - **Cards:** rounded ~6px, hover scales to 1.05× with subtle shadow lift. - **Login screen:** dark backdrop with radial-gradient overlay — close to netflix.com's sign-in page. - **No theme-brand footer label** any more. ### Theme history | Date | Theme | Version | Why changed | |---|---|---|---| | 2026-05-08 (earlier today) | ElegantFin | v25.12.31 | Initial Jellyfin theming pass. Picked for activity + safety (most actively maintained CSS in the ecosystem). | | 2026-05-08 (this entry) | **Cineplex** | **v1.0.6** | Owner asked for the most Netflix-faithful theme available. ElegantFin's Jellyseerr aesthetic (blue-grey, no red) is too far from Netflix; Cineplex is purpose-built for this look and explicitly targets the 10.10 series we're on. JellyFlix (the genre's elder) is halted. | If we ever roll back to ElegantFin, the previous `@import` was `https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@v25.12.31/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css`. The previous incarnation of this doc lives in git history. --- ## 2. How it was applied ### Branding API (Cineplex, applied 2026-05-08) ```bash TOKEN=*redacted* cat > /tmp/branding.json <<'EOF' { "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.", "CustomCss": "/* Cineplex v1.0.6 — Netflix-faithful theme by MRunkehl, pinned tag (immutable on jsDelivr) */\n/* Compat: Jellyfin 10.10.7+ ; we run 10.10.3 — verified rendering 2026-05-08 */\n@import url(\"https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css\");\n\n/* Hide Cast & Crew + Guest Stars sections globally (preserved 2026-05-08) */\n#castCollapsible, #guestCastCollapsible { display: none !important; }\n", "SplashscreenEnabled": true } EOF # Note: arrflix.s8n.ru didn't have a Traefik SNI cert at apply-time, so # we sent the request to the legacy SNI tv.s8n.ru and pinned its address # with --resolve. Either form is fine once both names have certs. curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ -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 (got HTTP 204 — applied) ``` ### Verification (executed 2026-05-08) ```bash # 1. Admin endpoint — confirms the new CustomCss is stored. curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ -H "X-Emby-Token: $TOKEN" \ https://tv.s8n.ru/System/Configuration/branding | python3 -m json.tool # Result: HTTP 200, contains the Cineplex @import + cast/crew hide rule. # 2. Anonymous endpoint the SPA reads at runtime — confirms what every # browser will pull before login. curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ https://tv.s8n.ru/Branding/Configuration | python3 -m json.tool # Result: HTTP 200, identical CustomCss to admin endpoint. ✓ # 3. The CSS asset itself on jsDelivr (sanity-check the network path). curl -sSI "https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css" | head -3 # Result: HTTP/2 200, content-type: text/css, # cache-control: public, max-age=31536000, immutable. ✓ # 4. SPA shell still routes (nav not broken). curl -sSI --resolve tv.s8n.ru:443:192.168.0.100 https://tv.s8n.ru/ | head -1 # Result: HTTP/2 302 → /web/. ✓ ``` `/` 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 visual check is a hard browser reload (Ctrl-Shift-R) on `https://tv.s8n.ru` (or `https://arrflix.s8n.ru` once its cert is up) from the LAN — owner will do this. ### 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, post-Cineplex) | Field | Value | |---|---| | `LoginDisclaimer` | "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind." (will swap to arrflix.s8n.ru when the new cert lands) | | `CustomCss` | `@import` of **Cineplex v1.0.6** from jsDelivr — pinned tag `@v1.0.6` (immutable). Plus appended cast/crew hide rule. | | `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://arrflix.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://arrflix.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 `arrflix.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 We currently pin Cineplex to `@v1.0.6` (immutable) — no auto-updates, no surprise breakage. To opt into upstream changes: ```bash # Move from immutable tag to floating @main (pulls future commits; # jsDelivr cache TTL is up to 7d for floating refs). curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ -X POST -H "X-Emby-Token: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"CustomCss": "@import url(\"https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@main/cineplex.css\");\n#castCollapsible, #guestCastCollapsible { display: none !important; }", "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.10.7+ → 10.11.x), check the [Cineplex commits](https://github.com/MRunkehl/cineplex/commits/main) and the README compatibility line. Cineplex's stated floor is 10.10.7, so going forward in the 10.10 series is safe; jumping to 10.11 needs a re-test (selectors changed in some 10.11 release notes). If something regresses, pin back to `@v1.0.6`. ### 6b. Reverting to ElegantFin (or vanilla) Replace the `@import` line: ```bash # Back to ElegantFin (Jellyseerr-style): curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ -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\");\n#castCollapsible, #guestCastCollapsible { display: none !important; }", "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \ https://tv.s8n.ru/System/Configuration/branding # To vanilla Jellyfin (clear everything): curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \ -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 → edit / clear "Custom CSS code" → Save. Hard-reload browsers afterward. ### 6c. Pinning a known-good revision Cineplex is already pinned to `@v1.0.6`. If a future tag (e.g. `v1.0.7`) ships and is good, bump the URL. jsDelivr serves `@` immutably and forever. Tag list: . --- ## 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" (Cineplex is dark-only — Netflix-black `#181818` base; 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://arrflix.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). - [ ] Get Traefik to issue a SNI cert for `arrflix.s8n.ru` so the curl examples don't need `--resolve tv.s8n.ru:443:192.168.0.100`. Until then, both names point to the same backend on `192.168.0.100` but only `tv.s8n.ru` has a valid cert. - [ ] Watch [Cineplex commits](https://github.com/MRunkehl/cineplex/commits/main) monthly; if a `v1.0.7` lands and looks safe, bump the pin. - [ ] 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. - [ ] Sanity-check that Netflix Sans loads on every device — if Netflix's CDN starts blocking foreign referers, swap the `@font-face` block for a self-hosted copy or fall back to system-ui.