From a018245f4083767e06c571bca4877b2afeb02d50 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Fri, 17 Jul 2026 00:37:31 -0400 Subject: [PATCH] Consolidate cleanup scripts' double filesystem walk into one lidarr_cleanup.sh/sonarr_cleanup.sh/radarr_cleanup.sh each walked their full media root twice per run: once to classify files and total the deletion size for the safety-threshold check, then again to actually delete. The size check needs to know the total before deleting anything, not before knowing what to delete -- the classification pass now records orphan/junk paths as it finds them, and the deletion pass just reads that list instead of re-walking and re-classifying the whole tree again. Only affects real (non-dry-run) executions, where the second walk used to happen. Also merges two separate stat calls per file into one. --- Arrs_Stack/lidarr_cleanup.sh | 31 +++++++++++++++---------------- Arrs_Stack/radarr_cleanup.sh | 33 ++++++++++++++------------------- Arrs_Stack/sonarr_cleanup.sh | 33 ++++++++++++++------------------- 3 files changed, 43 insertions(+), 54 deletions(-) diff --git a/Arrs_Stack/lidarr_cleanup.sh b/Arrs_Stack/lidarr_cleanup.sh index 4ec824e..09ef18f 100755 --- a/Arrs_Stack/lidarr_cleanup.sh +++ b/Arrs_Stack/lidarr_cleanup.sh @@ -423,6 +423,14 @@ JUNK_BYTES=0 AGE_SECONDS=$(( LIDARR_ORPHAN_AGE * 86400 )) NOW=$(date +%s) +# Files classified ORPHAN/JUNK below get their path recorded here, so the deletion pass can +# just delete them directly instead of re-walking and re-classifying the whole tree a second +# time (2026-07-17) — the size-threshold check below needs to know the total before deleting +# anything, not before knowing what to delete, so there's no need to redo the classification +# itself once it's already been decided. +TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt" +> "$TO_DELETE_FILE" + while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue @@ -439,10 +447,10 @@ while IFS= read -r filepath; do continue fi - FILE_SIZE=$(stat -c%s "$filepath" 2>/dev/null || echo 0) + # Single stat call for both fields instead of two separate subprocess invocations. + read -r FILE_SIZE FILE_MTIME < <(stat -c '%s %Y' "$filepath" 2>/dev/null || echo "0 0") if has_extension "$filepath" "${LIDARR_EXTENSIONS[@]}"; then - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) FILE_AGE=$(( NOW - FILE_MTIME )) if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then @@ -454,10 +462,12 @@ while IFS= read -r filepath; do warn "$ICON_TRASH ORPHAN: $filepath" (( ORPHAN_COUNT++ )) ORPHAN_BYTES=$(( ORPHAN_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" else log "JUNK: $filepath" (( JUNK_COUNT++ )) JUNK_BYTES=$(( JUNK_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" fi done < <(find "$LIDARR_MUSIC_ROOT" -type f 2>/dev/null) @@ -471,24 +481,13 @@ TOTAL_REMOVED=$(( ORPHAN_COUNT + JUNK_COUNT )) check_delete_size_threshold "$TOTAL_DELETE_BYTES" "$LIDARR_MAX_DELETE_GB" "Lidarr Cleanup" # ── Execute Deletions ───────────────────────────────────────────────────────────────────────── -# All safety layers passed — delete orphans and junk +# All safety layers passed — delete orphans and junk. Reuses TO_DELETE_FILE from the +# classification pass above instead of re-walking and re-classifying the whole tree again. if [[ "$DRY_RUN" == false ]]; then while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue - [[ -n "${TRACKED_MAP[$filepath]:-}" ]] && continue - matches_pattern_list "$filepath" "${LIDARR_PROTECTED_PATTERNS[@]}" && continue - - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) - FILE_AGE=$(( NOW - FILE_MTIME )) - - if has_extension "$filepath" "${LIDARR_EXTENSIONS[@]}"; then - [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && \ - [[ "$SKIP_AGE_CHECK" != true ]] && continue - fi - rm -f "$filepath" 2>/dev/null || error "Failed to delete: $filepath" - - done < <(find "$LIDARR_MUSIC_ROOT" -type f 2>/dev/null) + done < "$TO_DELETE_FILE" info "Cleaning up empty folders..." find "$LIDARR_MUSIC_ROOT" -mindepth 1 -type d -empty -delete 2>/dev/null diff --git a/Arrs_Stack/radarr_cleanup.sh b/Arrs_Stack/radarr_cleanup.sh index 23b59a8..24e18bc 100755 --- a/Arrs_Stack/radarr_cleanup.sh +++ b/Arrs_Stack/radarr_cleanup.sh @@ -406,6 +406,13 @@ JUNK_BYTES=0 AGE_SECONDS=$(( RADARR_ORPHAN_AGE * 86400 )) NOW=$(date +%s) +# Files classified ORPHAN/JUNK below get their path recorded here, so the deletion pass can +# just delete them directly instead of re-walking and re-classifying every SCAN_ROOTS entry a +# second time (2026-07-17) — the size-threshold check below needs to know the total before +# deleting anything, not before knowing what to delete. +TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt" +> "$TO_DELETE_FILE" + while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue @@ -420,10 +427,10 @@ while IFS= read -r filepath; do continue fi - FILE_SIZE=$(stat -c%s "$filepath" 2>/dev/null || echo 0) + # Single stat call for both fields instead of two separate subprocess invocations. + read -r FILE_SIZE FILE_MTIME < <(stat -c '%s %Y' "$filepath" 2>/dev/null || echo "0 0") if has_extension "$filepath" "${RADARR_EXTENSIONS[@]}"; then - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) FILE_AGE=$(( NOW - FILE_MTIME )) if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then @@ -435,10 +442,12 @@ while IFS= read -r filepath; do warn "$ICON_TRASH ORPHAN: $filepath" (( ORPHAN_COUNT++ )) ORPHAN_BYTES=$(( ORPHAN_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" else log "JUNK: $filepath" (( JUNK_COUNT++ )) JUNK_BYTES=$(( JUNK_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" fi done < <( @@ -456,27 +465,13 @@ TOTAL_REMOVED=$(( ORPHAN_COUNT + JUNK_COUNT )) check_delete_size_threshold "$TOTAL_DELETE_BYTES" "$RADARR_MAX_DELETE_GB" "Radarr Cleanup" # ── Execute Deletions ───────────────────────────────────────────────────────────────────────── +# Reuses TO_DELETE_FILE from the classification pass above instead of re-walking and +# re-classifying every SCAN_ROOTS entry again. if [[ "$DRY_RUN" == false ]]; then while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue - [[ -n "${TRACKED_MAP[$filepath]:-}" ]] && continue - matches_pattern_list "$filepath" "${RADARR_PROTECTED_PATTERNS[@]}" && continue - - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) - FILE_AGE=$(( NOW - FILE_MTIME )) - - if has_extension "$filepath" "${RADARR_EXTENSIONS[@]}"; then - [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && \ - [[ "$SKIP_AGE_CHECK" != true ]] && continue - fi - rm -f "$filepath" 2>/dev/null || error "Failed to delete: $filepath" - - done < <( - for host_path in "${SCAN_ROOTS[@]}"; do - [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null - done | sort -u - ) + done < "$TO_DELETE_FILE" info "Cleaning up empty folders..." for host_path in "${SCAN_ROOTS[@]}"; do diff --git a/Arrs_Stack/sonarr_cleanup.sh b/Arrs_Stack/sonarr_cleanup.sh index b0322a7..0e67f91 100755 --- a/Arrs_Stack/sonarr_cleanup.sh +++ b/Arrs_Stack/sonarr_cleanup.sh @@ -405,6 +405,13 @@ JUNK_BYTES=0 AGE_SECONDS=$(( SONARR_ORPHAN_AGE * 86400 )) NOW=$(date +%s) +# Files classified ORPHAN/JUNK below get their path recorded here, so the deletion pass can +# just delete them directly instead of re-walking and re-classifying every SCAN_ROOTS entry a +# second time (2026-07-17) — the size-threshold check below needs to know the total before +# deleting anything, not before knowing what to delete. +TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt" +> "$TO_DELETE_FILE" + while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue @@ -419,10 +426,10 @@ while IFS= read -r filepath; do continue fi - FILE_SIZE=$(stat -c%s "$filepath" 2>/dev/null || echo 0) + # Single stat call for both fields instead of two separate subprocess invocations. + read -r FILE_SIZE FILE_MTIME < <(stat -c '%s %Y' "$filepath" 2>/dev/null || echo "0 0") if has_extension "$filepath" "${SONARR_EXTENSIONS[@]}"; then - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) FILE_AGE=$(( NOW - FILE_MTIME )) if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then @@ -434,10 +441,12 @@ while IFS= read -r filepath; do warn "$ICON_TRASH ORPHAN: $filepath" (( ORPHAN_COUNT++ )) ORPHAN_BYTES=$(( ORPHAN_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" else log "JUNK: $filepath" (( JUNK_COUNT++ )) JUNK_BYTES=$(( JUNK_BYTES + FILE_SIZE )) + echo "$filepath" >> "$TO_DELETE_FILE" fi done < <( @@ -455,27 +464,13 @@ TOTAL_REMOVED=$(( ORPHAN_COUNT + JUNK_COUNT )) check_delete_size_threshold "$TOTAL_DELETE_BYTES" "$SONARR_MAX_DELETE_GB" "Sonarr Cleanup" # ── Execute Deletions ───────────────────────────────────────────────────────────────────────── +# Reuses TO_DELETE_FILE from the classification pass above instead of re-walking and +# re-classifying every SCAN_ROOTS entry again. if [[ "$DRY_RUN" == false ]]; then while IFS= read -r filepath; do [[ -z "$filepath" ]] && continue - [[ -n "${TRACKED_MAP[$filepath]:-}" ]] && continue - matches_pattern_list "$filepath" "${SONARR_PROTECTED_PATTERNS[@]}" && continue - - FILE_MTIME=$(stat -c %Y "$filepath" 2>/dev/null || echo 0) - FILE_AGE=$(( NOW - FILE_MTIME )) - - if has_extension "$filepath" "${SONARR_EXTENSIONS[@]}"; then - [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && \ - [[ "$SKIP_AGE_CHECK" != true ]] && continue - fi - rm -f "$filepath" 2>/dev/null || error "Failed to delete: $filepath" - - done < <( - for host_path in "${SCAN_ROOTS[@]}"; do - [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null - done | sort -u - ) + done < "$TO_DELETE_FILE" info "Cleaning up empty folders..." for host_path in "${SCAN_ROOTS[@]}"; do