feat: intermediate orchestrator + arr_sync blocklist end-to-end + mesh sync

Add intermediate_sync_maintenance.sh (0 */4 * * *) — arr library sync as
fixed first step, optional mid-day rsync (INTERMEDIATE_SYNC_SHARES, empty
by default), then INTERMEDIATE_MAINTENANCE_SCRIPTS. Wired with full rsync
infrastructure (temperature abort, check_rsync_enabled, INTERMEDIATE_RSYNC_ENABLED
toggle) matching daily/weekly pattern. lidarr_missing_art.sh scheduled here.

arr_sync.sh: blocklist --add now end-to-end — looks up real display name
from local arr API, writes TSV, deletes from local arr API (deleteFiles=false),
SSHes each remote node and deletes from their arr API. Files become orphans
for arr_cleanup safety pass.

master.conf: DEFAULT_RSYNC_OPTS updated (--partial, --timeout=60,
--numeric-ids; --no-whole-file removed). Media share comments updated to
mesh model.

master_host1.conf + master_host2.conf: both hosts now push all media shares
bidirectionally (true mesh — no ownership per share, arr_cleanup enforces truth).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gmer4Lfe
2026-05-09 15:24:35 -04:00
co-authored by Claude Sonnet 4.6
parent 6674dd4c17
commit f277ce92d5
5 changed files with 500 additions and 18 deletions
+133 -6
View File
@@ -25,10 +25,16 @@
#
# ── BLOCKLIST ─────────────────────────────────────────────────────────────────────────────────
# ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added anywhere.
# Read from ALL nodes via SSH at start of each run — immediate effect before rsync.
# Propagates to all nodes via rsync on the next cycle.
# Read from ALL nodes via SSH at start of each run — immediate effect across all nodes.
# (Each node reads every other node's blocklist file via SSH — no rsync delay.)
# Manage via --blocklist-add / --blocklist-remove / --blocklist-list.
#
# --blocklist-add does three things atomically:
# 1. Writes the TSV tombstone entry (prevents future re-adds by arr_sync)
# 2. Deletes the item from the local arr API (deleteFiles=false)
# 3. SSHes each remote node and deletes from their arr API (deleteFiles=false)
# Files become orphans on all nodes — arr_cleanup.sh removes them on next run.
#
# ── GRACEFUL SKIP ─────────────────────────────────────────────────────────────────────────────
# Arr not configured locally → skip cleanly, no error.
# Arr not reachable on a remote → skip that node for that arr type, continue with others.
@@ -47,8 +53,10 @@
# arr_sync.sh --dry-run — preview only, no changes
# arr_sync.sh --log — verbose output
# arr_sync.sh --status — show config and exit
# arr_sync.sh --blocklist-add lidarr <id> "reason" tombstone an ID on all nodes
# arr_sync.sh --blocklist-remove lidarr <id> — un-tombstone an ID
# arr_sync.sh --blocklist-add lidarr <mbid> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-add sonarr <tvdbId> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-add radarr <tmdbId> "reason" — remove from all arrs + tombstone
# arr_sync.sh --blocklist-remove lidarr <id> — un-tombstone (does NOT re-add)
# arr_sync.sh --blocklist-list — show all blocklisted IDs
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
@@ -238,6 +246,68 @@ _blocklist_remove() {
fi
}
# Look up item in local arr by stable_id — returns "internal_id\tdisplay_name" or empty
_lookup_local_item() {
local url="$1" api_key="$2" api_ver="$3" endpoint="$4"
local id_field="$5" id_type="$6" name_field="$7" stable_id="$8"
local raw select_expr
raw=$(curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
-H "X-Api-Key: $api_key" \
"${url}/api/${api_ver}/${endpoint}" 2>/dev/null)
[[ -z "$raw" ]] && return 1
if [[ "$id_type" == "string" ]]; then
select_expr=".[] | select(.${id_field} == \"${stable_id}\")"
else
select_expr=".[] | select(.${id_field} == ${stable_id})"
fi
echo "$raw" | jq -r "${select_expr} | [(.id | tostring), .${name_field}] | @tsv" 2>/dev/null | head -1
}
# Delete item from local arr by internal integer id — returns HTTP status code
_delete_local_item() {
local url="$1" api_key="$2" api_ver="$3" endpoint="$4" internal_id="$5"
curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
-H "X-Api-Key: $api_key" \
"${url}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
}
# Delete item from remote arr by stable_id via SSH.
# Outputs: HTTP code on success | "not_found" if item absent | empty on SSH/API failure.
# deleteFiles=false — files become orphans for arr_cleanup to handle with its safety checks.
_delete_remote_item() {
local node_id="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5"
local id_field="$6" id_type="$7" stable_id="$8"
local node_name="${!node_id}"
local ts_name="${node_name,,}"
local node_ip
node_ip=$(tailscale ip -4 "$ts_name" 2>/dev/null)
[[ -z "$node_ip" ]] && node_ip=$(tailscale status 2>/dev/null | \
awk -v n="$ts_name" '$2 ~ "^" n { print $1; exit }')
[[ -z "$node_ip" ]] && return 1
local select_expr
if [[ "$id_type" == "string" ]]; then
select_expr=".[] | select(.${id_field} == \"${stable_id}\") | .id"
else
select_expr=".[] | select(.${id_field} == ${stable_id}) | .id"
fi
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
root@"$node_ip" bash <<REMOTE 2>/dev/null
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
[[ -z "\$KEY" ]] && exit 1
LIBRARY=\$(curl -sf --max-time ${ARR_SYNC_API_TIMEOUT} \
-H "X-Api-Key: \$KEY" \
"http://localhost:${port}/api/${api_ver}/${endpoint}" 2>/dev/null)
[[ -z "\$LIBRARY" ]] && exit 1
INTERNAL_ID=\$(echo "\$LIBRARY" | jq -r '${select_expr}' 2>/dev/null | head -1)
[[ -z "\$INTERNAL_ID" ]] && echo "not_found" && exit 0
curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
-H "X-Api-Key: \$KEY" \
"http://localhost:${port}/api/${api_ver}/${endpoint}/\${INTERNAL_ID}?deleteFiles=false"
REMOTE
}
# ── Blocklist management mode ──────────────────────────────────────────────────────────────────
if [[ -n "$BLOCKLIST_ACTION" ]]; then
case "$BLOCKLIST_ACTION" in
@@ -264,8 +334,65 @@ if [[ -n "$BLOCKLIST_ACTION" ]]; then
error " arr_type: lidarr | sonarr | radarr"
exit 1
fi
_blocklist_add "$BLOCKLIST_ARR" "$BLOCKLIST_ID" "${BLOCKLIST_ID}" "${BLOCKLIST_REASON:-manually excluded}"
echo " Blocklisted [$BLOCKLIST_ARR] $BLOCKLIST_ID — will propagate via rsync on next cycle"
if [[ -z "${_PORT[$BLOCKLIST_ARR]:-}" ]]; then
error "Unknown arr type: $BLOCKLIST_ARR — use lidarr, sonarr, or radarr"
exit 1
fi
_bl_port="${_PORT[$BLOCKLIST_ARR]}"
_bl_ver="${_VER[$BLOCKLIST_ARR]}"
_bl_ep="${_EP[$BLOCKLIST_ARR]}"
_bl_id_field="${_ID[$BLOCKLIST_ARR]}"
_bl_id_type="${_ID_TYPE[$BLOCKLIST_ARR]}"
_bl_name_field="${_NAME[$BLOCKLIST_ARR]}"
_bl_config_xml="${DOCKER_APPDATA_BASE}/${BLOCKLIST_ARR^}/config.xml"
_bl_url="" _bl_key=""
case "$BLOCKLIST_ARR" in
lidarr) _bl_url="${LIDARR_URL:-}"; _bl_key="${LIDARR_API_KEY:-}" ;;
sonarr) _bl_url="${SONARR_URL:-}"; _bl_key="${SONARR_API_KEY:-}" ;;
radarr) _bl_url="${RADARR_URL:-}"; _bl_key="${RADARR_API_KEY:-}" ;;
esac
# Look up display name and internal id from local arr
_bl_display_name="$BLOCKLIST_ID"
_bl_internal_id=""
if [[ -n "$_bl_url" ]] && [[ -n "$_bl_key" ]]; then
_bl_lookup=$(_lookup_local_item "$_bl_url" "$_bl_key" "$_bl_ver" "$_bl_ep" \
"$_bl_id_field" "$_bl_id_type" "$_bl_name_field" "$BLOCKLIST_ID")
if [[ -n "$_bl_lookup" ]]; then
IFS=$'\t' read -r _bl_internal_id _bl_display_name <<< "$_bl_lookup"
fi
fi
_blocklist_add "$BLOCKLIST_ARR" "$BLOCKLIST_ID" "$_bl_display_name" "${BLOCKLIST_REASON:-manually excluded}"
echo " Blocklisted [$BLOCKLIST_ARR] $_bl_display_name ($BLOCKLIST_ID)"
# Remove from local arr (deleteFiles=false — arr_cleanup handles file removal)
if [[ -n "$_bl_internal_id" ]]; then
_bl_http=$(_delete_local_item "$_bl_url" "$_bl_key" "$_bl_ver" "$_bl_ep" "$_bl_internal_id")
if [[ "$_bl_http" == "200" ]]; then
log "Removed from local ${BLOCKLIST_ARR^}: $_bl_display_name"
else
warn "Failed to remove from local ${BLOCKLIST_ARR^} (HTTP ${_bl_http:-no response}) — remove manually via UI"
fi
else
log "Not found in local ${BLOCKLIST_ARR^} — already removed or not tracked locally"
fi
# Remove from all remote arrs
for _bl_node_id in "${REMOTE_NODES[@]}"; do
_bl_node_name="${!_bl_node_id}"
_bl_result=$(_delete_remote_item "$_bl_node_id" "$_bl_port" "$_bl_ver" "$_bl_ep" \
"$_bl_config_xml" "$_bl_id_field" "$_bl_id_type" "$BLOCKLIST_ID")
case "$_bl_result" in
200) log "Removed from $_bl_node_name ${BLOCKLIST_ARR^}: $_bl_display_name" ;;
not_found) log "Not found on $_bl_node_name ${BLOCKLIST_ARR^} — already removed or not tracked" ;;
*) warn "Failed to remove from $_bl_node_name ${BLOCKLIST_ARR^} (${_bl_result:-SSH error}) — remove manually via UI" ;;
esac
done
echo ""
echo " Files are now orphans on all nodes — arr_cleanup.sh will remove them on next run"
exit 0
;;
remove)