feat: arr-native distributed media sync + config architecture fixes

Arr sync (new):
- Media/arr_sync.sh — full mesh bidirectional sync across all HOST* nodes
  - Lidarr (MusicBrainz), Sonarr (TVDB), Radarr (TMDB) all handled in one script
  - Remote API keys read live from config.xml via SSH — never stored in conf files
  - Shared blocklist (DATA_DIR/arr_sync_blocklist.tsv) merged from all nodes at runtime
  - Graceful skip if arr not configured locally or not reachable on a remote node
  - --blocklist-add / --blocklist-remove / --blocklist-list management flags
- daily_sync_maintenance.sh — arr sync runs as explicit phase before rsync
- partnership_onboard.sh — Step 3 bootstraps merged library on both sides at onboard
- master.conf — ARR_SYNC_* config block, DOCKER_APPDATA_BASE

Rsync / cleanup:
- DEFAULT_RSYNC_OPTS — removed --delete; arr_cleanup.sh owns orphan enforcement
- lidarr_cleanup.sh — removed HOST1-only guard; runs on any node with Lidarr configured

Config architecture:
- HOST1/HOST2 hostnames moved from master_host*.conf → master.conf (not credentials)
- Sparse checkout now works correctly: each server only needs its own host conf
- detect_hosts() still resolves MY_ID + REMOTE_ID via master.conf hostname values

Bug fix:
- common.sh line 493 — watchdog toggle eval had broken quoting; all SYS_WATCHDOG_CHECK_*
  globals were silently set to empty instead of their configured values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gmer4Lfe
2026-05-09 09:53:40 -04:00
co-authored by Claude Sonnet 4.6
parent 009820e981
commit b65f572367
21 changed files with 1548 additions and 272 deletions
+39 -9
View File
@@ -47,8 +47,8 @@
# REMOTE HEALTH CHECKS Rootfs threshold for pre-flight abort
# RSYNC PROFILE SYSTEM Per-profile overrides for appdata syncs
#
# ── FAILOVER ───────────────────────────────────────────────────────────────────────────────
# FAILOVER Mutual container failover shared settings
# ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
# FALLBACK Mutual container failover shared settings
# FALLBACK TEST Simulated outage settings for fallback_test.sh
#
# ── DOCKER ESSENTIALS ──────────────────────────────────────────────────────────────────────
@@ -92,10 +92,20 @@
#
# ==============================================================================================
# ==============================================================================================
# ── HOST IDENTITIES ───────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Hostnames for every server in the ecosystem — not credentials, safe for all machines.
# Must match the exact unRAID hostname AND Tailscale device name (case sensitive).
# detect_hosts() in common.sh matches the local hostname against these to set MY_ID / REMOTE_ID.
# Tailscale IP resolution uses these names — no hardcoded IPs needed.
# To add a new server: add HOST3="unRAID-NewServer" here + create master_host3.conf.
HOST1="unRAID-Gmer4Lfe"
HOST2="unRAID-Jayred365"
# ==============================================================================================
# ── SHARED HOST CONFIGURATION ─────────────────────────────────────────────────────────────────
# ==============================================================================================
# Hostnames defined in master_host*.conf — detect_hosts() reads them at runtime.
# DATA_DIR is the same path on all servers — persistent script state and statistics.
# Array share — survives reboots, no flash drive wear.
# Created automatically if it doesn't exist.
@@ -109,6 +119,19 @@
# "abort" — refuse to continue (strict — ensures both sides always match)
UNRAID_VERSION_MISMATCH_ACTION="warn"
# ── Arr Sync ──
# arr_sync.sh syncs Lidarr/Sonarr/Radarr libraries across all nodes bidirectionally.
# Runs before rsync — all nodes agree on tracked library before files are transferred.
# Remote API keys are read live from each node's config.xml via SSH — never stored here.
ARR_SYNC_ENABLED=true
ARR_SYNC_BLOCKLIST="${DATA_DIR}/arr_sync_blocklist.tsv"
ARR_SYNC_CONNECT_TIMEOUT=10 # seconds — SSH connect timeout per node
ARR_SYNC_API_TIMEOUT=60 # seconds — curl timeout for library fetches
DOCKER_APPDATA_BASE="/mnt/user/appdata"
ARR_SYNC_LIDARR_PORT=8686
ARR_SYNC_SONARR_PORT=8989
ARR_SYNC_RADARR_PORT=7878
# ── Remote Docker Daemon ──
# Strike system for remote Docker daemon health checks.
# Scripts that issue remote container commands check the remote daemon first.
@@ -269,6 +292,8 @@
#"Media/lidarr_cleanup.sh" # remove orphaned music files — enable when ready
#"Media/sonarr_cleanup.sh" # remove orphaned TV files — enable when ready
#"Media/radarr_cleanup.sh" # remove orphaned movie files — enable when ready
"Media/radarr_tmdb_removed.sh" # remove movies dropped from TMDb
"Media/sonarr_tvdb_removed.sh" # remove series dropped from TVDB
"Docker_Essentials/docker_update.sh" # pull container image updates before restart
"Docker_Essentials/docker_daily_restart.sh" # daily container restarts — runs last
)
@@ -373,10 +398,11 @@
CONTAINER_DELAY=5 # seconds before starting delayed containers
EXCLUDE_DIRS=() # directories excluded from transfer — profiles override
# --delete removes files on remote not on source (mirror behaviour)
# --inplace writes directly to destination — better for large files
# --no-whole-file forces delta transfer — sends only changed blocks
DEFAULT_RSYNC_OPTS=(-av --info=progress2 --human-readable --bwlimit="$BW_LIMIT" --delete --inplace --no-whole-file)
# --delete intentionally omitted — arr_cleanup.sh enforces media truth post-rsync.
# Media shares use this default; rsync spreads files only, never removes them.
DEFAULT_RSYNC_OPTS=(-av --info=progress2 --human-readable --bwlimit="$BW_LIMIT" --inplace --no-whole-file)
# ━━━ Remote Health Checks ━━━
# Pre-flight — aborts if remote rootfs (/) usage is at or above this percentage.
@@ -502,13 +528,13 @@
# check_remote_disks() reads fsType per disk and handles XFS, ZFS, and cache pools automatically.
# ==============================================================================================
# ── FAILOVER ──────────────────────────────────────────────────────────────────────────────────
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Mutual container failover between two unRAID servers.
# Each server runs Fallback/fallback.sh independently via array_start.sh.
# All decisions based on two pings: remote reachable + internet reachable.
#
# States: NORMAL | FAILOVER | NO_INTERNET | DARK
# States: NORMAL | FALLBACK | NO_INTERNET | DARK
#
# DDNS rules — absolute:
# Internet loss → stop own DDNS immediately
@@ -519,8 +545,8 @@
# Shared settings (intervals, state file, thresholds) live here.
EXTERNAL_IP="8.8.8.8"
FALLBACK_CHECK_INTERVAL=120 # seconds between fallback state checks
FALLBACK_HANDBACK_STRIKES=2 # consecutive healthy checks before initiating handback
FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks
FALLBACK_HANDBACK_STRIKES=3 # consecutive healthy checks before initiating handback (3×30s = 90s)
FALLBACK_STATE_FILE="/boot/config/fallback_state.db"
FALLBACK_ENABLED=false # HOST2 being rebuilt — set true when back online and tested
# false = suppresses "not running" warnings in status scripts
@@ -910,6 +936,10 @@
"theme.mp3" "theme.flac" "theme.wav" "theme.m4a" "theme.mka"
)
# Radarr/Sonarr TMDb/TVDB removed entry cleanup
RADARR_DROPPED_ADD_EXCLUSION=true # add removed movies to import exclusion list
SONARR_DROPPED_ADD_EXCLUSION=true # add removed series to import exclusion list
# ━━━ Arr Failed/Stalled Recovery ━━━
# Auto blocklist + re-search failed imports and stalled downloads.
# Runs every 6 hours — schedule: 0 */6 * * *