From 3084546b322fd5e4214aedc00b79c7aed7641aa8 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 27 Jul 2026 17:38:49 -0400 Subject: [PATCH] =?UTF-8?q?Delete=20download=20orphans=20the=20arr=20canno?= =?UTF-8?q?t=20match=20instead=20of=20holding=20them=20forever=20=E2=80=94?= =?UTF-8?q?=20past=20the=20age=20gate=20an=20unnameable=20entry=20will=20n?= =?UTF-8?q?ever=20import,=20and=20clearing=20it=20lets=20a=20monitored=20t?= =?UTF-8?q?itle=20search=20for=20a=20copy=20the=20arr=20can=20parse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Arrs_Stack/arr_download_orphan_cleaner.sh | 50 +++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/Arrs_Stack/arr_download_orphan_cleaner.sh b/Arrs_Stack/arr_download_orphan_cleaner.sh index 8b58339..3b8c911 100755 --- a/Arrs_Stack/arr_download_orphan_cleaner.sh +++ b/Arrs_Stack/arr_download_orphan_cleaner.sh @@ -33,10 +33,15 @@ # movie file → delete (the arr already refused it as not-an-upgrade) # IMPORTABLE — parse API matches it but the library is missing episodes / the movie # → trigger DownloadedEpisodesScan/DownloadedMoviesScan on the folder and -# leave it; whatever imports gets swept as REDUNDANT next week, whatever +# leave it; whatever imports gets swept as REDUNDANT next run, whatever # the arr rejects (XEM-blocked, season-span files) stays HELD for a human -# HELD — parse API can't match it (series/movie not in the arr) → report only, -# deciding whether to add the series or bin the files is a human call +# UNMATCHED — parse API can't match it (series/movie not in the arr) → delete. Past the +# age gate an entry the arr cannot even name is not going to import: if the +# title is in the library and monitored, clearing it lets the arr search a +# copy it can actually parse; if it is not in the library, nothing is +# tracking it and it is dead weight either way. Guarded — see safeguard 6 +# HELD — IMPORTABLE entries the arr keeps refusing (XEM-blocked, season-span +# files), and everything skipped by a guard → report only, human call # # The queue fetch is a hard gate: if it fails, the whole arr is skipped — with no queue # there is no way to tell tracked from orphaned, and guessing means deleting active imports. @@ -49,8 +54,14 @@ # 2. Download dir must exist and live under /mnt/ — refuses to walk anything else # 3. Queue fetch must succeed (see above) # 4. Age gate — nothing under DOWNLOAD_ORPHAN_AGE days is touched -# 5. Deletion only for JUNK and parse-verified REDUNDANT — never for HELD/IMPORTABLE -# 6. Run total over DOWNLOAD_ORPHAN_MAX_DELETE_GB aborts the delete pass and notifies — +# 5. Deletion only for JUNK, parse-verified REDUNDANT, and UNMATCHED — never for +# IMPORTABLE or anything a guard has held +# 6. UNMATCHED deletes require the arr to report a non-empty library. An empty or +# restoring database answers every parse with "no match", which would condemn the +# entire download dir; the library is queried directly rather than inferred from the +# run's own matches, since a small batch that is legitimately all-unmatched is normal +# once daily runs have caught up and would otherwise read as a broken database +# 7. Run total over DOWNLOAD_ORPHAN_MAX_DELETE_GB aborts the delete pass and notifies — # a queue fetch that returned partial data would classify live downloads as orphans, # and an abnormally large delete total is the visible symptom of exactly that # (--i-know-what-im-doing overrides, e.g. for a first run against a known backlog) @@ -157,10 +168,12 @@ for arr in sonarr radarr; do queue_endpoint="queue?pageSize=1000&includeUnknownSeriesItems=true" exts_var="SONARR_EXTENSIONS" scan_command="DownloadedEpisodesScan" + library_endpoint="series" else queue_endpoint="queue?pageSize=1000&includeUnknownMovieItems=true" exts_var="RADARR_EXTENSIONS" scan_command="DownloadedMoviesScan" + library_endpoint="movie" fi QUEUE_JSON=$(arr_api "$arr_url" "$arr_key" "v3" "$queue_endpoint" "${arr^}") || { @@ -179,6 +192,8 @@ for arr in sonarr radarr; do DELETE_SIZES=() DELETE_LABELS=() SCAN_PATHS=() + UNMATCHED_PATHS=() + UNMATCHED_SIZES=() arr_tracked=0; arr_recent=0; arr_held=0; arr_delete_mb=0 while IFS= read -r entry; do @@ -231,8 +246,8 @@ for arr in sonarr radarr; do fi if [[ "$matched" != true ]]; then - echo " $ICON_WARN HELD (no match in ${arr^}): $base (${size_mb}M)" - arr_held=$((arr_held + 1)) + UNMATCHED_PATHS+=("$entry") + UNMATCHED_SIZES+=("$size_mb") elif (( missing == 0 )); then DELETE_PATHS+=("$entry") DELETE_SIZES+=("$size_mb") @@ -244,6 +259,27 @@ for arr in sonarr radarr; do fi done < <(find "$dl_dir" -mindepth 1 -maxdepth 1 2>/dev/null) + # An arr with an empty or still-restoring database answers every parse with "no match", + # which would turn the whole download dir into UNMATCHED and delete it. Confirm the + # library actually holds titles before trusting a no-match to mean what it says. This + # has to be asked of the arr directly — inferring health from the run's own matches + # fails on a small batch that is legitimately all-unmatched, which is the normal case + # once daily runs have caught up. + if (( ${#UNMATCHED_PATHS[@]} > 0 )); then + library_count=$(arr_api "$arr_url" "$arr_key" "v3" "$library_endpoint" "${arr^}" | jq 'length' 2>/dev/null) + if [[ ! "$library_count" =~ ^[0-9]+$ ]] || (( library_count == 0 )); then + warn " ${arr^}: library reports ${library_count:-no} titles — cannot trust 'no match', holding ${#UNMATCHED_PATHS[@]} unmatched" + arr_held=$((arr_held + ${#UNMATCHED_PATHS[@]})) + else + for i in "${!UNMATCHED_PATHS[@]}"; do + DELETE_PATHS+=("${UNMATCHED_PATHS[$i]}") + DELETE_SIZES+=("${UNMATCHED_SIZES[$i]}") + DELETE_LABELS+=("UNMATCHED") + arr_delete_mb=$((arr_delete_mb + UNMATCHED_SIZES[i])) + done + fi + fi + if (( arr_delete_mb / 1024 > DOWNLOAD_ORPHAN_MAX_DELETE_GB )) && [[ "$I_KNOW" != true ]]; then error "${arr^}: delete total $((arr_delete_mb / 1024))G exceeds cap of ${DOWNLOAD_ORPHAN_MAX_DELETE_GB}G — aborting delete pass" notify "${arr^} download orphan delete total $((arr_delete_mb / 1024))G exceeds ${DOWNLOAD_ORPHAN_MAX_DELETE_GB}G cap on $(hostname) — possible partial queue data, nothing deleted. Re-run with --i-know-what-im-doing if legitimate." \