Bring script headers onto the template and close safeguard gaps

Headers claimed protections the code never had, and several destructive paths had no
guard against a collapsed config value.
This commit is contained in:
Gmer4Lfe
2026-08-01 20:37:59 -04:00
parent cdce877601
commit e8b114094a
78 changed files with 3301 additions and 277 deletions
+72 -11
View File
@@ -18,7 +18,7 @@
# Audio → MusicBrainz Track ID
#
# ==============================================================================================
# SYNC LOGIC
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each matched item across ≥2 servers:
@@ -55,21 +55,71 @@
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# PLAY_SYNC_ENABLED gate — exits cleanly when disabled; no partial runs
# PARTNERSHIP gate — skips remote sync when partnership is inactive
# Per-server reachability — unreachable servers are skipped individually;
# one offline server does not abort the entire sync
# User match required — a user missing from a server is skipped for that
# server; no cross-account state pollution
# acquire_lock — prevents concurrent runs from racing on the same
# items during the 30-minute critical window
# Root Enforcement
# The probe fingerprint is written under STATE_DIR, which is not user-writable.
# Without root the fingerprint silently fails to persist and the change probe
# never suppresses anything.
#
# jq Dependency Check
# Exits if jq is missing. All API response parsing and the epoch comparisons
# depend on it — without jq every comparison would silently evaluate empty.
#
# PLAY_SYNC_ENABLED Gate
# Exits cleanly when disabled; no partial runs.
#
# PLAY_SYNC_REMOTE Gate
# When false, only this host's own servers are synced. Remote hosts are skipped
# before any network call is attempted.
#
# Partnership Gate
# Remote hosts are skipped when PARTNERSHIP_ENABLED=false. Local Emby↔Jellyfin
# sync still runs — a dormant partnership does not disable local work.
#
# Tailscale Resolution Guard
# A remote host whose Tailscale IP cannot be resolved is skipped rather than
# contacted at its literal localhost URL, which would otherwise point the sync
# at this host's own server and cross-contaminate state.
#
# Placeholder Credential Guard
# Servers whose API key is empty or still a placeholder are dropped from the
# list before any request is made.
#
# Per-Server Reachability
# Unreachable servers are skipped individually; one offline server does not
# abort the entire sync.
#
# User Match Required
# A user missing from a server is skipped for that server. State is never
# written to an unrelated account that happens to exist there.
#
# Forward-Only Writes
# The sync only pushes state forward — it never clears a Played flag or resets
# a resume position. The worst outcome of a bad comparison is a no-op, not
# erased watch history.
#
# Lock Acquisition
# acquire_lock prevents concurrent runs racing on the same items during the
# 30 minute critical window. --wait switches from skip to wait for manual runs.
#
# Probe Staleness Ceiling
# PLAY_SYNC_PROBE_MAX_AGE_HOURS forces a full comparison regardless of the
# hash. Fetches happen every run either way, so the probe can only skip
# per-item processing — it can never cause a change to be missed outright.
#
# Dry Run Support
# --dry-run performs all comparisons and writes no state.
#
# ==============================================================================================
# CONFIGURATION (host*.conf, aliased by detect_hosts)
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
# HOST*_TRANSCODE_SERVERS "Name|URL|APIKey|type" entries per host (emby/jellyfin)
# All hosts are discovered automatically — no extra config needed.
# Read directly for every HOST[0-9]+ defined — this script
# deliberately does NOT call detect_hosts(), because it needs
# every host's servers, not just this one's. Self is identified
# by comparing HOST* values against hostname -s.
# Remote host URLs have localhost rewritten to their Tailscale IP.
#
# master.conf
@@ -103,6 +153,10 @@
# play_state_sync.sh --full
# Bypass the change probe — always run the full comparison.
#
# play_state_sync.sh --wait
# Wait for an in-progress run to finish instead of exiting. For manual runs
# that would otherwise be skipped by the every-30-minute scheduled pass.
#
# play_state_sync.sh --log
# Verbose output — show each item comparison.
#
@@ -129,6 +183,13 @@ parse_args "${_FILTERED[@]}"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
# The probe fingerprint lives under STATE_DIR — without root it silently fails to persist
# and the change probe can never suppress a run.
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
[[ "${PLAY_SYNC_ENABLED:-true}" != "true" ]] && echo "Play state sync disabled" && exit 0
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode}"