#!/usr/bin/env bash # add-jellyfin-user.sh — create a Jellyfin user with the canonical s8n layout. # # Defaults applied: # - Home sections 0..9: resume, resumeaudio, nextup, latestmedia, none x6 # (no "My Media" tile row — sidebar already exposes libraries) # - SubtitleMode=Default, SubtitleLanguagePreference=eng, AudioLanguagePreference=eng # - Non-admin policy with EnableContentDeletion=false # - LoginAttemptsBeforeLockout=5 (brute-force lockout after 5 failed attempts; # required since arrflix.s8n.ru exposed to public internet on 2026-05-08) # - EnableUserPreferenceAccess=false (hides Display/Home/Playback/Subtitles # pref pages — owner controls all UX defaults centrally; non-admins can't # change their own home layout, subtitle lang, etc.) # # Why this exists: Jellyfin has no native global default for new-user # DisplayPreferences. The home-layout defaults are baked into the web bundle. # This wrapper layers our preferences on top after user creation, so the same # layout shows up for every user without anyone having to click through # Settings → Display → Home Screen. # # Usage: # JELLYFIN_TOKEN=xxx ./add-jellyfin-user.sh # # Or interactive (will prompt for missing creds). set -euo pipefail JELLYFIN_URL="${JELLYFIN_URL:-https://arrflix.s8n.ru}" JELLYFIN_TOKEN="${JELLYFIN_TOKEN:?set JELLYFIN_TOKEN=}" USERNAME="${1:?usage: $0 }" PASSWORD="${2:?usage: $0 }" AUTH="MediaBrowser Token=$JELLYFIN_TOKEN" echo "[1/4] Creating user '$USERNAME'..." RESP=$(curl -ks -X POST "$JELLYFIN_URL/Users/New" \ -H "Authorization: $AUTH" \ -H "Content-Type: application/json" \ -d "{\"Name\":\"$USERNAME\",\"Password\":\"$PASSWORD\"}") USER_ID=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['Id'])" 2>/dev/null || true) if [[ -z "$USER_ID" ]]; then echo "User creation failed:" echo "$RESP" exit 1 fi echo " Created. UserId=$USER_ID" echo "[2/4] Applying canonical home layout..." curl -ks "$JELLYFIN_URL/DisplayPreferences/usersettings?userId=$USER_ID&client=emby" \ -H "Authorization: $AUTH" > /tmp/dp-cur.$$.json python3 - < /tmp/dp-fix.$$.json import json with open('/tmp/dp-cur.$$.json') as f: dp = json.load(f) cp = dp['CustomPrefs'] for k in list(cp.keys()): if k.lower().startswith('homesection'): del cp[k] cp['homesection0'] = 'resume' cp['homesection1'] = 'resumeaudio' cp['homesection2'] = 'nextup' cp['homesection3'] = 'latestmedia' for i in range(4, 10): cp[f'homesection{i}'] = 'none' print(json.dumps(dp)) EOF HTTP=$(curl -ks -X POST "$JELLYFIN_URL/DisplayPreferences/usersettings?userId=$USER_ID&client=emby" \ -H "Authorization: $AUTH" \ -H "Content-Type: application/json" \ --data-binary @/tmp/dp-fix.$$.json -w "%{http_code}" -o /dev/null) rm -f /tmp/dp-cur.$$.json /tmp/dp-fix.$$.json [[ "$HTTP" == "204" ]] || { echo " DisplayPreferences POST failed: $HTTP"; exit 1; } echo " Home layout applied." echo "[3/4] Setting language prefs (force English everywhere, no fallback)..." curl -ks "$JELLYFIN_URL/Users/$USER_ID" -H "Authorization: $AUTH" > /tmp/u.$$.json python3 - < /tmp/u-fix.$$.json import json with open('/tmp/u.$$.json') as f: u = json.load(f) c = u['Configuration'] c['SubtitleMode'] = 'Default' c['SubtitleLanguagePreference'] = 'eng' c['AudioLanguagePreference'] = 'eng' c['PlayDefaultAudioTrack'] = True c['UICulture'] = 'en-US' c['DisplayMissingEpisodes'] = False print(json.dumps(c)) EOF HTTP=$(curl -ks -X POST "$JELLYFIN_URL/Users/$USER_ID/Configuration" \ -H "Authorization: $AUTH" \ -H "Content-Type: application/json" \ --data-binary @/tmp/u-fix.$$.json -w "%{http_code}" -o /dev/null) rm -f /tmp/u.$$.json /tmp/u-fix.$$.json [[ "$HTTP" == "204" ]] || { echo " User config POST failed: $HTTP"; exit 1; } echo " User prefs applied." echo "[4/4] Applying restricted policy (lockout=5, no user-pref access)..." curl -ks "$JELLYFIN_URL/Users/$USER_ID" -H "Authorization: $AUTH" > /tmp/p.$$.json python3 - < /tmp/p-fix.$$.json import json with open('/tmp/p.$$.json') as f: u = json.load(f) p = u['Policy'] p['LoginAttemptsBeforeLockout'] = 5 p['EnableUserPreferenceAccess'] = False print(json.dumps(p)) EOF HTTP=$(curl -ks -X POST "$JELLYFIN_URL/Users/$USER_ID/Policy" \ -H "Authorization: $AUTH" \ -H "Content-Type: application/json" \ --data-binary @/tmp/p-fix.$$.json -w "%{http_code}" -o /dev/null) rm -f /tmp/p.$$.json /tmp/p-fix.$$.json [[ "$HTTP" == "204" ]] || { echo " Policy POST failed: $HTTP"; exit 1; } echo " Lockout policy applied." echo echo "Done. User '$USERNAME' ready at $JELLYFIN_URL" echo " UserId: $USER_ID"