Files
Varaverk/Arrs_Stack/radarr_tmdb_removed.sh
Gmer4Lfe bac1ef1c17 Update headers on today's arr-caching changes
Comment-only. Headers on the scripts touched during today's caching work
(cache-first fetches, write-through per-item cache, single-walk
consolidation, movieFile-embedded fix) still described pre-change
behavior. Also brought common.sh's top-level cache doc block current --
it was written for the single-consumer 2026-07-16 state and didn't
mention the tmpfs move, the write guard, or the 15+ consumers that now
go through it.
2026-07-17 01:08:46 -04:00

276 lines
11 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ============================= Radarr — TMDb Removed ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Remove movies from Radarr that TMDb has dropped. Radarr marks these with
# status="deleted" — they generate health errors and can never be monitored
# or downloaded. Most are announced-but-never-released films delisted before
# release.
#
# Cache-first movie list (2026-07-17) — comes from the shared tracked-data cache via
# arr_get_tracked_data(), fresh (kept warm every 30min by arr_cache_prefill.sh), live fetch
# as fallback.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Queries Radarr API for movies with status="deleted" (TMDb removal marker)
# Reports each 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 — prevents re-add)
#
# Files are NOT deleted by default — use --delete-files to also remove from disk.
# Per-deletion output is always visible — deletions are never silently swallowed.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Health Error Hygiene
# status="deleted" entries can never be monitored or downloaded — they only
# generate persistent health errors. Removing them is maintenance, not
# data loss: the content never existed on disk for most of these entries.
#
# Conservative File Handling
# Files are not deleted by default because most TMDb-removed entries are
# announced-but-never-released films with no files. The --delete-files flag
# is an explicit opt-in, not the default path.
#
# Exclusion List Prevents Re-add
# Removed entries are added to Radarr's import exclusion list by default.
# Without this, the same deleted entry can be re-added by lists or searches
# and immediately generate the same health error again.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# detect_hosts() — exits cleanly if RADARR_URL empty (Radarr not on this host)
# API pre-flight — verifies Radarr reachable before querying
# Dry-run mode — full preview without removing anything
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# RADARR_DROPPED_ADD_EXCLUSION — add removed movies to import exclusion (default: true)
#
# host*.conf
#
# HOST1_RADARR_URL / HOST1_RADARR_API_KEY
# Aliased by detect_hosts() — script uses RADARR_URL / RADARR_API_KEY
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# 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 ━━━
# ==============================================================================================
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
echo "Radarr not configured for $MY_ID — skipping"
exit 0
fi
ADD_EXCLUSION="${RADARR_DROPPED_ADD_EXCLUSION:-true}"
log "$ICON_GEAR Config: url=${RADARR_URL} add-exclusion=${ADD_EXCLUSION} delete-files=${DELETE_FILES:-false}"
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
# Cache-first — arr_get_tracked_data() serves the shared cache when it's fresh (kept current
# every 30min by arr_cache_prefill.sh in CRITICAL_MAINTENANCE_SCRIPTS), falls back to a live
# fetch when it's stale, and waits out an active rescan before either.
MOVIES=$(arr_get_tracked_data "radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3")
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
echo "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}")
log "$ICON_WARN $title ($year) [tmdbid $tmdb_id] — HAS FILE: $SIZE_HUMAN"
else
log "$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
echo " 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
echo "$ICON_DONE Status: done ✅"
else
warn "Status: ${#FAILED[@]} removal(s) failed"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
exit 0