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:
co-authored by
Claude Sonnet 4.6
parent
6674dd4c17
commit
f277ce92d5
+133
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user