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
+66 -27
View File
@@ -24,6 +24,10 @@
# Lidarr runs on HOST1 only. detect_hosts() sets LIDARR_URL — if empty (HOST2) the
# script exits cleanly with no action rather than failing.
#
# ── OUTPUT ────────────────────────────────────────────────────────────────────────────────────
# Minimal by default — section headers + per-section summary always visible.
# --log shows per-item detail (each album, each artist, each file fetched).
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock — prevents concurrent runs during large library scans
# curl + jq check — fail fast if tools missing
@@ -50,7 +54,7 @@
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# lidarr_missing_art.sh — fetch all missing artwork
# lidarr_missing_art.sh --dry-run — preview without downloading
# lidarr_missing_art.sh --log — verbose output
# lidarr_missing_art.sh --log — verbose per-item output
# lidarr_missing_art.sh --status — show config and exit
# ==============================================================================================
@@ -70,7 +74,6 @@ if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
success "Running as root"
if ! command -v curl >/dev/null 2>&1; then
error "curl not found — required for API calls"
@@ -80,7 +83,6 @@ if ! command -v jq >/dev/null 2>&1; then
error "jq not found — required for JSON parsing"
exit 1
fi
success "curl and jq found"
acquire_lock
@@ -91,7 +93,8 @@ if [[ -z "$LIDARR_URL" ]]; then
log "Lidarr not configured for $MY_ID — nothing to do"
exit 0
fi
success "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo " $MY_ID ($LOCAL_SERVER_NAME) — tools OK"
# ==============================================================================================
# ━━━ Status ━━━
@@ -115,6 +118,10 @@ fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be written"
# ── Temp dir for subshell fetch/fail counters ─────────────────────────────────────────────────
LIDARR_TMP=$(mktemp -d)
trap 'rm -rf "$LIDARR_TMP"' EXIT
# ==============================================================================================
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
@@ -143,7 +150,7 @@ download_if_valid() {
[[ -f "$dest" ]] && return 0
if [[ "$DRY_RUN" == true ]]; then
info "DRY RUN — would fetch: $(basename "$dest")"
log "DRY RUN — would fetch: $(basename "$dest")"
return 0
fi
@@ -155,14 +162,14 @@ download_if_valid() {
size=$(stat -c%s "$tmp" 2>/dev/null || echo 0)
if (( size > LIDARR_ART_MIN_SIZE )); then
mv "$tmp" "$dest"
log "Fetched: $dest"
log " Fetched: $(basename "$dest")"
return 0
fi
rm -f "$tmp"
sleep 1
done
warn "Failed to fetch valid image: $(basename "$dest")"
warn "Failed to fetch: $(basename "$dest")"
return 1
}
@@ -193,19 +200,15 @@ if ! curl_json "$LIDARR_URL/api/v1/system/status?apikey=$LIDARR_API_KEY" | jq -e
notify "lidarr_missing_art failed — Lidarr API unreachable on $(hostname)" "Lidarr Missing Art" "warning"
exit 1
fi
success "Lidarr API reachable"
echo " Reachable — $LIDARR_URL"
START=$(date +%s)
ALBUMS_CHECKED=0
ALBUMS_COMPLETE=0
ALBUM_FETCHES=0
ALBUM_FAILS=0
ARTISTS_CHECKED=0
ARTISTS_COMPLETE=0
ARTIST_FETCHES=0
ARTIST_FAILS=0
# ==============================================================================================
# ━━━ Albums ━━━
@@ -222,7 +225,7 @@ if [[ -z "$albums" || "$albums" == "null" ]]; then
fi
total_albums=$(echo "$albums" | jq '. | length')
info "$total_albums albums to process"
echo " Processing $total_albums albums..."
while IFS=$'\t' read -r local_path mbid artist_name album_name; do
(( ALBUMS_CHECKED++ ))
@@ -242,6 +245,8 @@ while IFS=$'\t' read -r local_path mbid artist_name album_name; do
wait_for_slot
(
_fetches=0 _fails=0
JSON=""
if [[ -n "$mbid" && "$mbid" != "null" ]]; then
JSON=$(curl_json "http://webservice.fanart.tv/v3/music/albums/$mbid?api_key=$FANART_API_KEY")
@@ -250,29 +255,43 @@ while IFS=$'\t' read -r local_path mbid artist_name album_name; do
if [[ ! -f "$local_path/cover.jpg" ]]; then
IMG=$(echo "$JSON" | jq -r '.[].albumcover[0].url // empty')
if ! download_if_valid "$IMG" "$local_path/cover.jpg"; then
if download_if_valid "$IMG" "$local_path/cover.jpg"; then
(( _fetches++ ))
else
query=$(printf "%s %s" "$artist_name" "$album_name" | sed 's/ /+/g')
itunes=$(curl_json "https://itunes.apple.com/search?term=$query&entity=album&limit=1" |
jq -r '.results[0].artworkUrl100 // empty' | sed 's/100x100/600x600/')
download_if_valid "$itunes" "$local_path/cover.jpg"
if download_if_valid "$itunes" "$local_path/cover.jpg"; then
(( _fetches++ ))
else
(( _fails++ ))
fi
fi
fi
if [[ ! -f "$local_path/cdart.png" ]]; then
IMG=$(echo "$JSON" | jq -r '.[].cdart[0].url // empty')
download_if_valid "$IMG" "$local_path/cdart.png"
if download_if_valid "$IMG" "$local_path/cdart.png"; then (( _fetches++ )); else (( _fails++ )); fi
fi
if [[ ! -f "$local_path/back.jpg" ]]; then
IMG=$(echo "$JSON" | jq -r '.[].albumback[0].url // empty')
download_if_valid "$IMG" "$local_path/back.jpg"
if download_if_valid "$IMG" "$local_path/back.jpg"; then (( _fetches++ )); else (( _fails++ )); fi
fi
(( _fetches > 0 )) && printf '1\n' >> "$LIDARR_TMP/album_fetches"
(( _fails > 0 )) && printf '1\n' >> "$LIDARR_TMP/album_fails"
) &
done < <(echo "$albums" | jq -r '.[] | [.path, .foreignAlbumId, .artist.artistName, .title] | @tsv')
wait
ALBUM_FETCHED=$(wc -l < "$LIDARR_TMP/album_fetches" 2>/dev/null || echo 0)
ALBUM_FAILED=$(wc -l < "$LIDARR_TMP/album_fails" 2>/dev/null || echo 0)
ALBUM_MISSING=$(( ALBUMS_CHECKED - ALBUMS_COMPLETE ))
echo " Checked: $ALBUMS_CHECKED | Complete: $ALBUMS_COMPLETE | Needed art: $ALBUM_MISSING | Fetched: $ALBUM_FETCHED | Failed: $ALBUM_FAILED"
# ==============================================================================================
# ━━━ Artists ━━━
# ==============================================================================================
@@ -288,7 +307,7 @@ if [[ -z "$artists" || "$artists" == "null" ]]; then
fi
total_artists=$(echo "$artists" | jq '. | length')
info "$total_artists artists to process"
echo " Processing $total_artists artists..."
while IFS=$'\t' read -r local_path mbid name; do
(( ARTISTS_CHECKED++ ))
@@ -310,43 +329,63 @@ while IFS=$'\t' read -r local_path mbid name; do
wait_for_slot
(
_fetches=0 _fails=0
JSON=$(curl_json "http://webservice.fanart.tv/v3/music/$mbid?api_key=$FANART_API_KEY")
sleep "$LIDARR_ART_SLEEP_BETWEEN"
if [[ ! -f "$local_path/folder.jpg" ]]; then
IMG=$(echo "$JSON" | jq -r '.artistthumb[0].url // empty')
if ! download_if_valid "$IMG" "$local_path/folder.jpg"; then
if download_if_valid "$IMG" "$local_path/folder.jpg"; then
(( _fetches++ ))
else
IMG=$(deezer_artist_image "$name")
if ! download_if_valid "$IMG" "$local_path/folder.jpg"; then
if download_if_valid "$IMG" "$local_path/folder.jpg"; then
(( _fetches++ ))
else
IMG=$(lastfm_artist_image "$name")
download_if_valid "$IMG" "$local_path/folder.jpg"
if download_if_valid "$IMG" "$local_path/folder.jpg"; then
(( _fetches++ ))
else
(( _fails++ ))
fi
fi
fi
fi
if [[ ! -f "$local_path/fanart.jpg" ]]; then
IMG=$(echo "$JSON" | jq -r '.artistbackground[0].url // empty')
if ! download_if_valid "$IMG" "$local_path/fanart.jpg"; then
if download_if_valid "$IMG" "$local_path/fanart.jpg"; then
(( _fetches++ ))
else
IMG=$(deezer_artist_image "$name")
download_if_valid "$IMG" "$local_path/fanart.jpg"
if download_if_valid "$IMG" "$local_path/fanart.jpg"; then (( _fetches++ )); else (( _fails++ )); fi
fi
fi
if [[ ! -f "$local_path/logo.png" ]]; then
IMG=$(echo "$JSON" | jq -r '.hdmusiclogo[0].url // empty')
download_if_valid "$IMG" "$local_path/logo.png"
if download_if_valid "$IMG" "$local_path/logo.png"; then (( _fetches++ )); else (( _fails++ )); fi
fi
if [[ ! -f "$local_path/banner.jpg" ]]; then
IMG=$(echo "$JSON" | jq -r '.musicbanner[0].url // empty')
download_if_valid "$IMG" "$local_path/banner.jpg"
if download_if_valid "$IMG" "$local_path/banner.jpg"; then (( _fetches++ )); else (( _fails++ )); fi
fi
(( _fetches > 0 )) && printf '1\n' >> "$LIDARR_TMP/artist_fetches"
(( _fails > 0 )) && printf '1\n' >> "$LIDARR_TMP/artist_fails"
) &
done < <(echo "$artists" | jq -r '.[] | [.path, .foreignArtistId, .artistName] | @tsv')
wait
ARTIST_FETCHED=$(wc -l < "$LIDARR_TMP/artist_fetches" 2>/dev/null || echo 0)
ARTIST_FAILED=$(wc -l < "$LIDARR_TMP/artist_fails" 2>/dev/null || echo 0)
ARTIST_MISSING=$(( ARTISTS_CHECKED - ARTISTS_COMPLETE ))
echo " Checked: $ARTISTS_CHECKED | Complete: $ARTISTS_COMPLETE | Needed art: $ARTIST_MISSING | Fetched: $ARTIST_FETCHED | Failed: $ARTIST_FAILED"
END=$(date +%s)
# ==============================================================================================
@@ -356,8 +395,8 @@ echo ""
echo "━━━━━ $ICON_SUMMARY LIDARR MISSING ART SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
echo "$ICON_EMBY Albums: $ALBUMS_CHECKED checked, $ALBUMS_COMPLETE already complete"
echo "$ICON_EMBY Artists: $ARTISTS_CHECKED checked, $ARTISTS_COMPLETE already complete"
echo "$ICON_EMBY Albums: $ALBUMS_CHECKED checked | $ALBUMS_COMPLETE complete | $ALBUM_FETCHED fetched | $ALBUM_FAILED failed"
echo "$ICON_EMBY Artists: $ARTISTS_CHECKED checked | $ARTISTS_COMPLETE complete | $ARTIST_FETCHED fetched | $ARTIST_FAILED failed"
if [[ "$DRY_RUN" == true ]]; then
echo "$ICON_WARN Status: DRY RUN — no files written"