250 lines
9.9 KiB
Bash
Executable File
250 lines
9.9 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.
|
|
#
|
|
# ==============================================================================================
|
|
# 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.
|
|
#
|
|
# ==============================================================================================
|
|
# 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
|
|
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}")
|
|
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
|
|
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
|
|
echo "$ICON_DONE Status: done ✅"
|
|
else
|
|
warn "Status: ${#FAILED[@]} removal(s) failed"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
|
|
exit 0
|