legacy-arrflix/docs/30-stock-jellyfin-tv-build.md
s8n 93b9c9d533
Some checks are pending
secret-scan / gitleaks (HEAD + history) (push) Waiting to run
secret-scan / detect-secrets (entropy + cross-tool) (push) Waiting to run
secret-scan / summary (push) Blocked by required conditions
docs(30): stock Jellyfin 10.11.8 rebuild on tv.s8n.ru
Brand-new container, brand-new volumes, ZERO ARRFLIX customisation.
Sister instance to prod (10.10.3, untouched) and dev (10.11.8 + scyfin).

P1+P2 from the rebuild plan: Movies + TV Shows libraries added via API,
library scan complete (4 movies / 12 series / 230 eps). Auto-scrape
matched 10/12 series + 4/4 movies to canonical TMDB IDs without manual
intervention; 3 unmatched are TMDB-absent indie content.

No theme, no shim, no CustomCss, no plugins, no user import, no home-
section seed — owner explicitly asked for a stock baseline.
2026-05-11 04:15:18 +01:00

6.4 KiB

30 — Stock Jellyfin rebuild on tv.s8n.ru (ground-up)

Date: 2026-05-11 Scope: brand new container, brand new volumes, zero ARRFLIX customisation. Sister docs: 29 (the failed in-place dev upgrade that led here).


1. Decision

After running the dev migration (10.10.3 → 10.11.8 + scyfin) on the existing jellyfin-dev container, the result still carried index.html shim, Cineplex remnants, and accumulated configuration drift. Owner asked for a true clean build instead.

Approach: new container, new domain, no shim, no CustomCss. Stock Jellyfin. We layer ARRFLIX brand on top once the bare server is happy.


2. Deploy

# /opt/docker/jellyfin-stock/docker-compose.yml
services:
  jellyfin-stock:
    image: jellyfin/jellyfin:10.11.8
    container_name: jellyfin-stock
    restart: unless-stopped
    user: "1000:1000"
    userns_mode: "host"
    environment:
      - TZ=Europe/London
      - JELLYFIN_PublishedServerUrl=https://tv.s8n.ru
    volumes:
      - /home/docker/jellyfin-stock/config:/config
      - /home/docker/jellyfin-stock/cache:/cache
      - /home/user/media:/media:ro
    networks: [proxy]
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.jellyfin-stock.rule=Host(`tv.s8n.ru`)
      - traefik.http.routers.jellyfin-stock.entrypoints=websecure
      - traefik.http.routers.jellyfin-stock.tls=true
      - traefik.http.routers.jellyfin-stock.tls.certresolver=letsencrypt
      - traefik.http.services.jellyfin-stock.loadbalancer.server.port=8096

Volumes initialised empty. No bind-mount of index.html — the stock web UI serves from the image as-is.

DNS

Pi-hole local DNS:       <nullstone-LAN-IP>  tv.s8n.ru
onyx /etc/hosts:         <nullstone-LAN-IP>  tv.s8n.ru  (appended to existing pin block)
Public DNS (Gandi):      none — LAN-only by design

(LAN IP is the standard nullstone bind, see SYSTEM.md.)

/opt/docker/pihole/etc-pihole/custom.list is owned by root; we wrote via privileged Alpine container + --userns=host to bypass the userns-remap. Same trick used for the /home/docker/jellyfin-stock/ dirs.

ServerId: adbc441eb46e475c9610c3bd5258dc6e (fresh, not migrated from prod).


3. Library scope (P1+P2)

User chose P1+P2 from tv.s8n.ru plan: libraries + canonical-ID lock only. No user import, no watched-state transfer, no plugins, no theme.

Libraries added via API

TOKEN=<admin token from Devices table after wizard>

curl -X POST -H "X-Emby-Token: $TOKEN" \
  "https://tv.s8n.ru/Library/VirtualFolders?name=Movies&collectionType=movies&paths=%2Fmedia%2Fmovies&refreshLibrary=false" \
  -H "Content-Type: application/json" \
  -d '{"LibraryOptions":{"EnableInternetProviders":true,"PreferredMetadataLanguage":"en","MetadataCountryCode":"US","SubtitleDownloadLanguages":["eng"],"SaveSubtitlesWithMedia":true,"RequirePerfectSubtitleMatch":false,"EnabledMetadataFetchers":["TheMovieDb","The Open Movie Database"],"MetadataFetcherOrder":["TheMovieDb","The Open Movie Database"]}}'

curl -X POST -H "X-Emby-Token: $TOKEN" \
  "https://tv.s8n.ru/Library/VirtualFolders?name=TV%20Shows&collectionType=tvshows&paths=%2Fmedia%2Ftv&refreshLibrary=false" \
  -H "Content-Type: application/json" \
  -d '{"LibraryOptions":{...same shape, fetchers=[TheMovieDb,TheTVDB]}}'

curl -X POST -H "X-Emby-Token: $TOKEN" "https://tv.s8n.ru/Library/Refresh"

Scan result

MovieCount   4
SeriesCount  12
EpisodeCount 230

Auto-scrape outcome

10 / 12 series + 4 / 4 movies matched canonical IDs without intervention. Three unmatched, all expected:

The Big Lez Saga (2022)                  TMDB --- (TMDB has no entry; Australian indie)
The Donny & Clarence Show                TMDB --- (IMDb tt32043762 only)
Star Wars: Maul - Shadow Lord [Before Upscale]   no IDs (intentional dupe folder)

Matched IDs (sanity-checked against prod docs):

American Dad!         TMDB 1433
Archer                TMDB 10283
Futurama              TMDB 615   TVDB 73871   IMDb tt0149460
The Mandalorian       TMDB 82856
The Mike Nolan Show   TMDB 67160
Obi-Wan Kenobi        TMDB 92830
Rick and Morty        TMDB 60625
Sassy the Sasquatch   TMDB 321760
Star Wars: Maul       TMDB 289219
Movies
  The Dark Knight     TMDB 155
  Idiocracy           TMDB 7512
  The Incredible Hulk TMDB 1724
  Lilo & Stitch       TMDB 11544

No POST /Items/{id} lock calls needed — the auto-scrape was clean.


4. Passwords

Admin s8n + user guest created via first-run wizard with throwaway passwords. Owner asked to use the same passwords as prod. Approach for that (deferred — pending owner decision):

-- prod jellyfin.db Users.Password is $PBKDF2-SHA512$iterations=2100$<salt>$<hash>
-- Copy hash from prod to stock:
ATTACH '/path/to/prod-jellyfin.db' AS prod;
UPDATE Users
   SET Password = (SELECT Password FROM prod.Users WHERE Username = Users.Username)
 WHERE Username IN ('s8n', 'guest');

Run with container stopped. Verified the PBKDF2 hash includes the salt inline so copying the column is enough — no separate salt column.


5. Explicitly NOT done

  • No theme (no scyfin, no Cineplex, no ElegantFin).
  • No web-overrides/index.html shim — stock Jellyfin chrome visible.
  • No CustomCss in branding.xml (file is the 225-byte default).
  • No plugins installed (no OpenSubtitles, no anything).
  • No 13-user import — only s8n admin + guest.
  • No home-section seed — stock defaults apply (smalllibrarytiles, resume, resumeaudio, nextup, latestmedia). Owner will iterate from here.
  • No backdrop pinning, no scrollbar themeing, no per-user prefs scripts.

6. State table

Instance Domain Image Theme Brand Status
jellyfin (prod) arrflix.s8n.ru 10.10.3 Cineplex v1.0.6 + INC1-7 patches ARRFLIX Untouched, real users on it
jellyfin-dev dev.arrflix.s8n.ru 10.11.8 scyfin OLED (broken brand-vs-shim mismatch) ARRFLIX Experimental — can be wiped
jellyfin-stock tv.s8n.ru 10.11.8 stock Jellyfin Fresh, ready to configure

7. Open follow-ups (none owed before owner sign-off)

  • Decide fate of jellyfin-dev (keep / wipe / repurpose).
  • Owner explores stock UX → identifies what to brand vs leave alone.
  • Eventually layer ARRFLIX skin (logo, accent, dark scrollbar) on top of stock — incrementally, documenting each step.
  • If migration to 10.11.8 on prod is later approved: docs/29 staged 10.10.3 → 10.10.7 → 10.11.8 path with snapshots is the playbook.