Files
Varaverk/Media/radarr_tmdb_removed.sh
T
Gmer4LfeandClaude Sonnet 4.6 b65f572367 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>
2026-05-09 09:53:40 -04:00

231 lines
9.9 KiB
Bash

#!/bin/bash
# ==============================================================================================
# ============================= Radarr — TMDb Removed ==========================================
# ==============================================================================================
# Removes movies from Radarr that have been dropped from TMDb.
# Radarr marks these with status="deleted" — they generate system health errors and
# can never be monitored or downloaded. 99% are future/announced movies that were
# delisted before release.
#
# ── WHAT IT DOES ──────────────────────────────────────────────────────────────────────────────
# Queries Radarr API for movies with status="deleted" (TMDb removal marker)
# Reports each found entry with file status and size
# Removes the movie record from Radarr
# Optionally deletes associated files (disabled by default — most have none)
# Optionally adds to Radarr's import exclusion list (default: true)
#
# ── SAFE DEFAULTS ─────────────────────────────────────────────────────────────────────────────
# Files are NOT deleted by default — use --delete-files to also remove from disk
# Import exclusion added by default — prevents Radarr re-adding dropped movies
# Per-deletion output always visible — deletions are never silently swallowed
#
# ── HOST AWARENESS ────────────────────────────────────────────────────────────────────────────
# Radarr runs on HOST1 only. detect_hosts() sets RADARR_URL — if empty (HOST2)
# the script exits cleanly.
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# RADARR_DROPPED_ADD_EXCLUSION — add removed movies to import exclusion (default: true)
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# radarr_tmdb_removed.sh — remove records, keep files, add exclusion
# radarr_tmdb_removed.sh --delete-files — also delete files from disk
# radarr_tmdb_removed.sh --dry-run — preview without removing anything
# radarr_tmdb_removed.sh --log — verbose output
# radarr_tmdb_removed.sh --status — show config and exit
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
# ── Parse --delete-files before standard parse_args ───────────────────────────────────────────
DELETE_FILES=false
FILTERED_ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--delete-files" ]]; then
DELETE_FILES=true
else
FILTERED_ARGS+=("$arg")
fi
done
parse_args "${FILTERED_ARGS[@]}"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Setup ━━━"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
error "curl not found"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
error "jq not found"
exit 1
fi
acquire_lock
detect_hosts
if [[ -z "$RADARR_URL" ]]; then
log "Radarr not configured for $MY_ID — nothing to do"
exit 0
fi
ADD_EXCLUSION="${RADARR_DROPPED_ADD_EXCLUSION:-true}"
echo " $MY_ID ($LOCAL_SERVER_NAME) — $RADARR_URL"
[[ "$DELETE_FILES" == true ]] && warn "DELETE FILES MODE — files will be removed from disk"
[[ "$DELETE_FILES" == false ]] && echo " Files: records only (use --delete-files to also remove from disk)"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Radarr URL: $RADARR_URL"
echo "$ICON_GEAR Add exclusion: $ADD_EXCLUSION"
echo "$ICON_GEAR Delete files: $DELETE_FILES"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ Query Radarr ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Querying Radarr ━━━"
if ! curl -sf --connect-timeout 5 --max-time 10 \
"$RADARR_URL/api/v3/system/status?apikey=$RADARR_API_KEY" | jq -e '.version' >/dev/null 2>&1; then
error "Radarr API unreachable at $RADARR_URL"
exit 1
fi
MOVIES=$(curl -sf --connect-timeout 5 --max-time 30 \
"$RADARR_URL/api/v3/movie?apikey=$RADARR_API_KEY" 2>/dev/null)
if [[ -z "$MOVIES" || "$MOVIES" == "null" ]]; then
error "Radarr movie API returned empty"
exit 1
fi
TOTAL=$(echo "$MOVIES" | jq '. | length')
DROPPED=$(echo "$MOVIES" | jq '[.[] | select(.status == "deleted")] | length')
echo " $TOTAL movies total — $DROPPED dropped from TMDb"
if [[ "$DROPPED" -eq 0 ]]; then
log "No TMDb-removed movies found — nothing to do"
echo ""
echo "━━━━━ $ICON_SUMMARY RADARR TMDB REMOVED SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_DONE Status: nothing to remove"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ Remove Dropped Movies ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_TRASH Remove TMDb-Dropped Movies ━━━"
START=$(date +%s)
REMOVED=()
FAILED=()
FILES_DELETED=0
FILES_SKIPPED=0
while IFS=$'\t' read -r id title year tmdb_id has_file file_size; do
[[ -z "$id" ]] && continue
SIZE_HUMAN=""
if [[ "$has_file" == "true" && "$file_size" -gt 0 ]]; then
SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fGB\", $file_size / 1073741824}")
echo "$ICON_WARN $title ($year) [tmdbid $tmdb_id] — HAS FILE: $SIZE_HUMAN"
else
echo "$ICON_TRASH $title ($year) [tmdbid $tmdb_id] — no file"
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove: $title"
[[ "$DELETE_FILES" == true && "$has_file" == "true" ]] && \
warn "DRY RUN — would delete file: $SIZE_HUMAN"
REMOVED+=("$title")
continue
fi
DELETE_PARAM="false"
if [[ "$DELETE_FILES" == true && "$has_file" == "true" ]]; then
DELETE_PARAM="true"
fi
RESP=$(curl -sf -X DELETE --connect-timeout 5 --max-time 15 \
"$RADARR_URL/api/v3/movie/${id}?deleteFiles=${DELETE_PARAM}&addImportExclusion=${ADD_EXCLUSION}&apikey=$RADARR_API_KEY" \
2>/dev/null)
CURL_EXIT=$?
if [[ "$CURL_EXIT" -eq 0 ]]; then
log " Removed from Radarr ✅"
REMOVED+=("$title")
if [[ "$DELETE_PARAM" == "true" ]]; then
(( FILES_DELETED++ ))
elif [[ "$has_file" == "true" ]]; then
(( FILES_SKIPPED++ ))
fi
else
warn " Failed to remove $title (curl exit $CURL_EXIT)"
FAILED+=("$title")
fi
done < <(echo "$MOVIES" | jq -r '
.[] | select(.status == "deleted") |
[
(.id | tostring),
.title,
(.year | tostring),
(.tmdbId | tostring),
(if .hasFile then "true" else "false" end),
(if .movieFile.size? then (.movieFile.size | tostring) else "0" end)
] | @tsv
')
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY RADARR TMDB REMOVED SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo "$ICON_TRASH Removed: ${#REMOVED[@]} of $DROPPED"
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
[[ "$FILES_DELETED" -gt 0 ]] && echo "$ICON_TRASH Files deleted: $FILES_DELETED"
[[ "$FILES_SKIPPED" -gt 0 ]] && echo "$ICON_WARN Files kept: $FILES_SKIPPED (had files — use --delete-files to remove)"
[[ "$ADD_EXCLUSION" == "true" ]] && echo "$ICON_GEAR Import exclusion added for removed entries"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no changes made"
elif [[ ${#FAILED[@]} -eq 0 ]]; then
log "$ICON_DONE Status: done ✅"
else
warn "Status: ${#FAILED[@]} removal(s) failed"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
exit 0