ARRFLIX/docs/04-theming-and-users.md

405 lines
16 KiB
Markdown
Raw Normal View History

# 04 — Theming and Users
Status: applied 2026-05-08 against `https://nasflix.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 nasflix.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://nasflix.s8n.ru/System/Configuration/branding
# expect: HTTP 204
```
### Verification
```bash
curl -sS -H "X-Emby-Token: $TOKEN" \
https://nasflix.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://nasflix.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://nasflix.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 nasflix.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://nasflix.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://nasflix.s8n.ru/Users/$NEW_ID/Policy"
# expect: HTTP 204
# 3. Verify
curl -sS -H "X-Emby-Token: $TOKEN" \
"https://nasflix.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://nasflix.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 `nasflix.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 nasflix.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \
https://nasflix.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://nasflix.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://nasflix.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).