203 lines
7.2 KiB
Bash
203 lines
7.2 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# english-lockdown-runner.sh — idempotent re-apply of the ARRFLIX English-only lockdown.
|
||
|
|
#
|
||
|
|
# See docs/20-english-only-lockdown.md for the full design, layer breakdown,
|
||
|
|
# and drift-check procedure. This script handles two of the three layers:
|
||
|
|
#
|
||
|
|
# 1. Server-wide: UICulture / PreferredMetadataLanguage / MetadataCountryCode
|
||
|
|
# via POST /System/Configuration.
|
||
|
|
# 2. Per-user: UICulture / AudioLanguagePreference / SubtitleLanguagePreference /
|
||
|
|
# PlayDefaultAudioTrack via POST /Users/{id}/Configuration for every account.
|
||
|
|
#
|
||
|
|
# The third layer (web SPA shim — navigator.language override + language-switcher
|
||
|
|
# CSS hide) is served via the bind-mounted web-overrides/ tree; nothing for
|
||
|
|
# this script to push.
|
||
|
|
#
|
||
|
|
# Idempotent — running it twice produces the same end state. Each layer is
|
||
|
|
# read, merged with English defaults, and POSTed back. Skips writes when the
|
||
|
|
# server already matches.
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# JELLYFIN_API_TOKEN=<admin-token> ./english-lockdown-runner.sh
|
||
|
|
#
|
||
|
|
# Optional env:
|
||
|
|
# JELLYFIN_URL default https://arrflix.s8n.ru
|
||
|
|
# DRY_RUN default unset; set DRY_RUN=1 to print payloads without POSTing
|
||
|
|
#
|
||
|
|
# Exit codes:
|
||
|
|
# 0 every layer landed (or already correct)
|
||
|
|
# 1 at least one POST failed; check stderr/stdout for which surface
|
||
|
|
# 2 bad invocation (missing required env)
|
||
|
|
#
|
||
|
|
# Token rotation note: the API token has full admin scope. Use a dedicated
|
||
|
|
# token, not a personal-account session token, and rotate after offboarding
|
||
|
|
# any operator with shell access to the host running this script.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
JELLYFIN_URL="${JELLYFIN_URL:-https://arrflix.s8n.ru}"
|
||
|
|
JELLYFIN_API_TOKEN="${JELLYFIN_API_TOKEN:?set JELLYFIN_API_TOKEN=<admin-token>; aborting (see docs/20-english-only-lockdown.md)}"
|
||
|
|
DRY_RUN="${DRY_RUN:-}"
|
||
|
|
|
||
|
|
AUTH="MediaBrowser Token=$JELLYFIN_API_TOKEN"
|
||
|
|
|
||
|
|
# Server-wide targets
|
||
|
|
SERVER_UI_CULTURE="en-US"
|
||
|
|
SERVER_METADATA_LANG="en"
|
||
|
|
SERVER_METADATA_COUNTRY="US"
|
||
|
|
|
||
|
|
# Per-user targets
|
||
|
|
USER_UI_CULTURE="en-US"
|
||
|
|
USER_AUDIO_LANG="eng"
|
||
|
|
USER_SUBTITLE_LANG="eng"
|
||
|
|
USER_PLAY_DEFAULT_AUDIO="true"
|
||
|
|
|
||
|
|
FAIL_COUNT=0
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Layer 1: server-wide config
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
echo "[*] Layer 1: server-wide /System/Configuration"
|
||
|
|
SERVER_TMP_IN=$(mktemp)
|
||
|
|
SERVER_TMP_OUT=$(mktemp)
|
||
|
|
trap 'rm -f "$SERVER_TMP_IN" "$SERVER_TMP_OUT"' EXIT
|
||
|
|
|
||
|
|
curl -ks "$JELLYFIN_URL/System/Configuration" -H "Authorization: $AUTH" > "$SERVER_TMP_IN"
|
||
|
|
|
||
|
|
CURRENT_SERVER=$(python3 -c "
|
||
|
|
import json
|
||
|
|
with open('$SERVER_TMP_IN') as f: c = json.load(f)
|
||
|
|
print(f\"UICulture={c.get('UICulture','<absent>')} PreferredMetadataLanguage={c.get('PreferredMetadataLanguage','<absent>')} MetadataCountryCode={c.get('MetadataCountryCode','<absent>')}\")
|
||
|
|
")
|
||
|
|
echo " before: $CURRENT_SERVER"
|
||
|
|
|
||
|
|
NEEDS_SERVER_WRITE=$(python3 -c "
|
||
|
|
import json
|
||
|
|
with open('$SERVER_TMP_IN') as f: c = json.load(f)
|
||
|
|
ok = (
|
||
|
|
c.get('UICulture') == '$SERVER_UI_CULTURE'
|
||
|
|
and c.get('PreferredMetadataLanguage') == '$SERVER_METADATA_LANG'
|
||
|
|
and c.get('MetadataCountryCode') == '$SERVER_METADATA_COUNTRY'
|
||
|
|
)
|
||
|
|
print('0' if ok else '1')
|
||
|
|
")
|
||
|
|
|
||
|
|
if [[ "$NEEDS_SERVER_WRITE" == "0" ]]; then
|
||
|
|
echo " ok: server already pinned, skipping write"
|
||
|
|
else
|
||
|
|
python3 - <<PYEOF > "$SERVER_TMP_OUT"
|
||
|
|
import json
|
||
|
|
with open("$SERVER_TMP_IN") as f: c = json.load(f)
|
||
|
|
c["UICulture"] = "$SERVER_UI_CULTURE"
|
||
|
|
c["PreferredMetadataLanguage"] = "$SERVER_METADATA_LANG"
|
||
|
|
c["MetadataCountryCode"] = "$SERVER_METADATA_COUNTRY"
|
||
|
|
print(json.dumps(c))
|
||
|
|
PYEOF
|
||
|
|
|
||
|
|
if [[ -n "$DRY_RUN" ]]; then
|
||
|
|
echo " DRY_RUN: would POST $(wc -c < "$SERVER_TMP_OUT") bytes to /System/Configuration"
|
||
|
|
else
|
||
|
|
HTTP=$(curl -ks -X POST "$JELLYFIN_URL/System/Configuration" \
|
||
|
|
-H "Authorization: $AUTH" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
--data-binary @"$SERVER_TMP_OUT" -w "%{http_code}" -o /dev/null)
|
||
|
|
if [[ "$HTTP" == "204" || "$HTTP" == "200" ]]; then
|
||
|
|
echo " after: UICulture=$SERVER_UI_CULTURE PreferredMetadataLanguage=$SERVER_METADATA_LANG MetadataCountryCode=$SERVER_METADATA_COUNTRY (HTTP $HTTP)"
|
||
|
|
else
|
||
|
|
echo " ERROR: POST /System/Configuration returned HTTP $HTTP" >&2
|
||
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Layer 2: per-user config
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
echo "[*] Layer 2: per-user /Users/{id}/Configuration"
|
||
|
|
USERS_JSON=$(curl -ks "$JELLYFIN_URL/Users" -H "Authorization: $AUTH")
|
||
|
|
USER_COUNT=$(echo "$USERS_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
|
||
|
|
echo " $USER_COUNT users found."
|
||
|
|
echo
|
||
|
|
|
||
|
|
# Process-substitution to keep `set -e` semantics in the loop body.
|
||
|
|
while IFS=$'\t' read -r USER_ID USER_NAME OLD_UI OLD_AUDIO OLD_SUB OLD_PLAY; do
|
||
|
|
TMP_IN=$(mktemp)
|
||
|
|
TMP_OUT=$(mktemp)
|
||
|
|
curl -ks "$JELLYFIN_URL/Users/$USER_ID" -H "Authorization: $AUTH" > "$TMP_IN"
|
||
|
|
|
||
|
|
NEEDS_USER_WRITE=$(python3 -c "
|
||
|
|
import json
|
||
|
|
with open('$TMP_IN') as f: u = json.load(f)
|
||
|
|
c = u.get('Configuration', {})
|
||
|
|
ok = (
|
||
|
|
c.get('UICulture') == '$USER_UI_CULTURE'
|
||
|
|
and c.get('AudioLanguagePreference') == '$USER_AUDIO_LANG'
|
||
|
|
and c.get('SubtitleLanguagePreference') == '$USER_SUBTITLE_LANG'
|
||
|
|
and c.get('PlayDefaultAudioTrack') is True
|
||
|
|
)
|
||
|
|
print('0' if ok else '1')
|
||
|
|
")
|
||
|
|
|
||
|
|
if [[ "$NEEDS_USER_WRITE" == "0" ]]; then
|
||
|
|
echo " [ok] $USER_NAME ($USER_ID) — already pinned"
|
||
|
|
rm -f "$TMP_IN" "$TMP_OUT"
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
|
||
|
|
python3 - <<PYEOF > "$TMP_OUT"
|
||
|
|
import json
|
||
|
|
with open("$TMP_IN") as f: u = json.load(f)
|
||
|
|
c = u["Configuration"]
|
||
|
|
c["UICulture"] = "$USER_UI_CULTURE"
|
||
|
|
c["AudioLanguagePreference"] = "$USER_AUDIO_LANG"
|
||
|
|
c["SubtitleLanguagePreference"] = "$USER_SUBTITLE_LANG"
|
||
|
|
c["PlayDefaultAudioTrack"] = True
|
||
|
|
print(json.dumps(c))
|
||
|
|
PYEOF
|
||
|
|
|
||
|
|
if [[ -n "$DRY_RUN" ]]; then
|
||
|
|
echo " [dry] $USER_NAME ($USER_ID) — would POST $(wc -c < "$TMP_OUT") bytes"
|
||
|
|
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" || "$HTTP" == "200" ]]; then
|
||
|
|
echo " [pin] $USER_NAME ($USER_ID) — UICulture=$USER_UI_CULTURE Audio=$USER_AUDIO_LANG Sub=$USER_SUBTITLE_LANG PlayDefault=true (HTTP $HTTP)"
|
||
|
|
else
|
||
|
|
echo " [FAIL] $USER_NAME ($USER_ID) — HTTP $HTTP" >&2
|
||
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
rm -f "$TMP_IN" "$TMP_OUT"
|
||
|
|
done < <(echo "$USERS_JSON" | python3 -c "
|
||
|
|
import json, sys
|
||
|
|
for u in json.load(sys.stdin):
|
||
|
|
c = u.get('Configuration', {})
|
||
|
|
print('\t'.join([
|
||
|
|
u['Id'],
|
||
|
|
u['Name'],
|
||
|
|
str(c.get('UICulture', '')),
|
||
|
|
str(c.get('AudioLanguagePreference', '')),
|
||
|
|
str(c.get('SubtitleLanguagePreference', '')),
|
||
|
|
str(c.get('PlayDefaultAudioTrack', '')),
|
||
|
|
]))
|
||
|
|
")
|
||
|
|
|
||
|
|
echo
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Summary + exit
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
if [[ $FAIL_COUNT -eq 0 ]]; then
|
||
|
|
echo "[*] Done. All layers pinned (or already correct). Drift-check commands"
|
||
|
|
echo " in docs/20-english-only-lockdown.md."
|
||
|
|
exit 0
|
||
|
|
else
|
||
|
|
echo "[!] Done with $FAIL_COUNT failure(s). Re-run after investigating;"
|
||
|
|
echo " drift-check commands in docs/20-english-only-lockdown.md." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|