ARRFLIX/bin/english-lockdown-runner.sh

203 lines
7.2 KiB
Bash
Raw Normal View History

#!/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