Files
Varaverk/Arrs_Stack/arr_full_rescan.sh
T
Gmer4Lfe fb13958881 Add weekly full-library rescan job for Lidarr/Sonarr/Radarr
Organic scans only touch files actually involved in an import — an
artist/series/movie that already has files sitting untouched on disk
never gets its tracked-file stats refreshed on its own. Confirmed
2026-07-16: Lidarr reported ~23% of its true trackFileCount with no
scan running, for artists whose files were verified present and
readable the whole time. Downstream scripts trust these stats as
source of truth for the share, so drift needs to be caught before
someone notices a suspiciously low number.
2026-07-16 15:57:12 -04:00

151 lines
6.6 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ======================= Arr Full Library Rescan =============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Forces a genuine full disk↔database reconciliation for Lidarr/Sonarr/Radarr. Organic
# scans (triggered by new imports, RSS sync, etc.) only touch the files actually involved —
# an artist/series/movie that already has files sitting untouched on disk never gets its
# file-tracking stats refreshed on its own. Confirmed 2026-07-16: Lidarr reported only ~23%
# of its true trackFileCount with no active scan running, for 1,004 of 1,357 artists — files
# verified present and readable on disk the whole time. Every downstream script (cleanup,
# duplicate-artist detection, discovery) trusts these arr stats as source of truth for what's
# on the share, so silent drift like this is exactly what check_tracked_count_floor() exists
# to catch reactively. This job exists to catch it proactively instead of waiting for someone
# to notice a suspiciously low number.
#
# Runs sequentially across all three arrs, never parallel — each is a heavy full-disk walk,
# and running them concurrently would just contend for the same disk I/O for no benefit.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
# ARR_FULL_RESCAN_TIMEOUT — seconds to wait per arr (default 3600). A whole-library
# RescanFolders/RescanSeries/RescanMovie is far heavier than the 600s pre-flight scan
# timeout used elsewhere — that shorter timeout is sized for a single release, not a
# full-library walk.
#
# host*.conf
# HOST1_LIDARR_URL / _API_KEY, HOST1_SONARR_URL / _API_KEY, HOST1_RADARR_URL / _API_KEY
# — aliased by detect_hosts()
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# arr_full_rescan.sh — normal run
# arr_full_rescan.sh --dry-run — preview which arrs would be rescanned, trigger nothing
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock "wait"
detect_hosts
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no rescans will be triggered"
declare -A ARR_FULL_RESCAN_COMMAND=(
[lidarr]="RescanFolders"
[sonarr]="RescanSeries"
[radarr]="RescanMovie"
)
RESCANNED=0
SKIPPED=0
for arr in lidarr sonarr radarr; do
url_var="${arr^^}_URL"; key_var="${arr^^}_API_KEY"
url="${!url_var:-}"; key="${!key_var:-}"
ver="v3"; [[ "$arr" == "lidarr" ]] && ver="v1"
if [[ -z "$url" || -z "$key" ]]; then
info "${arr^} not configured on $MY_ID — skipping"
continue
fi
check_api "$url" "${arr^}" 10 || {
warn "${arr^} unreachable — skipping full rescan this run"
(( SKIPPED++ ))
continue
}
active=$(arr_active_rescan_command "$arr" "$url" "$key" "$ver")
if [[ -n "$active" ]]; then
warn "${arr^} already mid-rescan ($active) — skipping, will catch it next scheduled run"
(( SKIPPED++ ))
continue
fi
endpoint="${ARR_LIBRARY_ENDPOINT[$arr]}"
expr="${ARR_TRACKED_COUNT_EXPR[$arr]}"
before_json=$(curl -sf --max-time 60 -H "X-Api-Key: $key" "${url}/api/${ver}/${endpoint}" 2>/dev/null)
before_count=$(echo "$before_json" | jq "$expr" 2>/dev/null)
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would trigger full ${ARR_FULL_RESCAN_COMMAND[$arr]} for ${arr} (currently: ${before_count:-unknown} tracked)"
continue
fi
log "$ICON_GEAR Triggering full ${ARR_FULL_RESCAN_COMMAND[$arr]} for ${arr} (before: ${before_count:-unknown} tracked)"
payload=$(jq -c -n --arg name "${ARR_FULL_RESCAN_COMMAND[$arr]}" '{name:$name}')
trigger_and_await_command "$url" "$key" "$ver" "$payload" "${ARR_FULL_RESCAN_TIMEOUT:-3600}" "$arr"
after_json=$(curl -sf --max-time 60 -H "X-Api-Key: $key" "${url}/api/${ver}/${endpoint}" 2>/dev/null)
after_count=$(echo "$after_json" | jq "$expr" 2>/dev/null)
if [[ -n "$after_json" && -n "$after_count" && "$after_count" != "null" ]]; then
arr_cache_write "$arr" "$after_json"
log "$ICON_DONE ${arr^} rescan complete — tracked: ${before_count:-?}${after_count}"
(( RESCANNED++ ))
# A completed full rescan is ground truth — if it's STILL far below the running
# baseline, that's a real problem (missing disk, permissions, actual data loss),
# not a stale-cache or mid-scan artifact. Worth a direct heads-up either way.
count_file_var="${arr^^}_TRACKED_COUNT_FILE"
min_pct_var="${arr^^}_MIN_TRACKED_PCT"
count_file="${!count_file_var:-}"
min_pct="${!min_pct_var:-80}"
if [[ -n "$count_file" && -f "$count_file" ]]; then
baseline=$(cat "$count_file" 2>/dev/null || echo 0)
if [[ "$baseline" -gt 0 ]]; then
pct=$(awk "BEGIN {printf \"%d\", ($after_count / $baseline) * 100}")
if [[ "$pct" -lt "$min_pct" ]]; then
notify "${arr^} full rescan complete but tracked count still ${pct}% of baseline ($after_count vs $baseline) on $(hostname) — real drop, not a scan artifact, needs a look" \
"Arr Full Rescan" "warning"
else
echo "$after_count" > "$count_file"
fi
else
echo "$after_count" > "$count_file"
fi
fi
else
warn "${arr^} rescan finished but re-fetch failed — cache not updated"
fi
done
echo ""
echo "━━━━━ $ICON_SUMMARY ARR FULL RESCAN SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Rescanned: $RESCANNED"
echo "$ICON_SKIP Skipped: $SKIPPED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0