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>
87 lines
3.2 KiB
Bash
Executable file
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'."
|