ARRFLIX/bin/force-english-all-users.sh
s8n 14f63e8027 doc 15: force English UI for all users (plan + script)
Owner saw "Abspielen" on the Play button — caused by every user having
Configuration.UICulture absent, so the web SPA falls back to browser
Accept-Language. No server-side flag exists to override this.

Adds docs/15-force-english.md with the per-user forcing mechanism,
limits (pre-auth splash bundle still uses navigator.language), and a
ready-to-execute bash script bin/force-english-all-users.sh that pins
UICulture=en-US on every user via POST /Users/{id}/Configuration.

Plan-only commit — no live config changed. Owner triggers when ready.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 04:22:04 +01:00

87 lines
3.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# force-english-all-users.sh — pin Configuration.UICulture=en-US on every Jellyfin user.
#
# Why this exists: see docs/15-force-english.md.
# TL;DR — when a user has UICulture unset, the Jellyfin web SPA falls back to
# browser Accept-Language. Owner saw "Abspielen" (German "Play") on a Play
# button because someone's browser sends de-*. Pinning UICulture per user
# overrides Accept-Language and gives every account English UI regardless
# of where they log in from.
#
# Read-modify-write on /Users/{id}/Configuration. Idempotent — running it
# twice produces the same end state. Prints before/after UICulture per user.
#
# Usage:
# JELLYFIN_TOKEN=<admin-token> ./force-english-all-users.sh
#
# Optional env:
# JELLYFIN_URL default https://arrflix.s8n.ru
# TARGET_LOCALE default en-US (e.g. en-GB also works)
# DRY_RUN default unset; set DRY_RUN=1 to print payloads without POSTing
set -euo pipefail
JELLYFIN_URL="${JELLYFIN_URL:-https://arrflix.s8n.ru}"
JELLYFIN_TOKEN="${JELLYFIN_TOKEN:?set JELLYFIN_TOKEN=<admin-token>}"
TARGET_LOCALE="${TARGET_LOCALE:-en-US}"
DRY_RUN="${DRY_RUN:-}"
AUTH="MediaBrowser Token=$JELLYFIN_TOKEN"
echo "[*] Listing users..."
USERS_JSON=$(curl -ks "$JELLYFIN_URL/Users" -H "Authorization: $AUTH")
COUNT=$(echo "$USERS_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
echo " $COUNT users found."
echo
# Iterate. Pipe-to-while loses set -e on subshell exit, so use process-substitution.
while IFS=$'\t' read -r USER_ID USER_NAME OLD_CULTURE; do
echo "[*] $USER_NAME ($USER_ID)"
echo " before: UICulture=${OLD_CULTURE:-<absent>}"
if [[ "$OLD_CULTURE" == "$TARGET_LOCALE" ]]; then
echo " skip: already $TARGET_LOCALE"
echo
continue
fi
TMP_IN=$(mktemp)
TMP_OUT=$(mktemp)
curl -ks "$JELLYFIN_URL/Users/$USER_ID" -H "Authorization: $AUTH" > "$TMP_IN"
python3 - <<PYEOF > "$TMP_OUT"
import json
with open("$TMP_IN") as f: u = json.load(f)
c = u["Configuration"]
c["UICulture"] = "$TARGET_LOCALE"
print(json.dumps(c))
PYEOF
if [[ -n "$DRY_RUN" ]]; then
echo " DRY_RUN: would POST $(wc -c < "$TMP_OUT") bytes to /Users/$USER_ID/Configuration"
else
HTTP=$(curl -ks -X POST "$JELLYFIN_URL/Users/$USER_ID/Configuration" \
-H "Authorization: $AUTH" \
-H "Content-Type: application/json" \
--data-binary @"$TMP_OUT" -w "%{http_code}" -o /dev/null)
if [[ "$HTTP" != "204" ]]; then
echo " ERROR: POST returned HTTP $HTTP"
rm -f "$TMP_IN" "$TMP_OUT"
exit 1
fi
# Verify
NEW_CULTURE=$(curl -ks "$JELLYFIN_URL/Users/$USER_ID" -H "Authorization: $AUTH" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['Configuration'].get('UICulture','<absent>'))")
echo " after: UICulture=$NEW_CULTURE"
fi
rm -f "$TMP_IN" "$TMP_OUT"
echo
done < <(echo "$USERS_JSON" | python3 -c "
import json, sys
for u in json.load(sys.stdin):
cur = u.get('Configuration', {}).get('UICulture', '')
print(f\"{u['Id']}\t{u['Name']}\t{cur}\")
")
echo "[*] Done. Tell users to hard-refresh (Ctrl-Shift-R) so the SPA reloads"
echo " the locale bundle. Verify on a movie detail page — Play button"
echo " should read 'Play', not 'Abspielen'."