arr_full_rescan.sh, arr_cache_prefill.sh, and arr_rescan_monitor.sh were missing the standard SAFEGUARDS header section other Arrs_Stack/Tools scripts have. Also: arr_cache_prefill.sh now checks for an active rescan before fetching, instead of doing a live fetch that arr_cache_write() would just refuse to persist anyway -- avoids wasted API calls every 30min during a long rescan. arr_rescan_monitor.sh was also missing an actual root check despite writing cache files; added it to match convention rather than just document a safeguard that wasn't there.
169 lines
8.1 KiB
Bash
Executable File
169 lines
8.1 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.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Root required — required for consistency across Arrs_Stack/, no direct fs writes here
|
|
# acquire_lock "wait" — waits for a prior run to finish rather than skipping or colliding;
|
|
# a full-library rescan across three arrs can run long, worth queuing
|
|
# behind rather than silently no-op'ing
|
|
# Reachability check — check_api before touching an arr; unreachable → skip that arr only
|
|
# Active-rescan check — skips triggering a NEW rescan if one's already active on that arr
|
|
# (manual trigger, another script) — never stacks a duplicate scan,
|
|
# see Tools/arr_rescan_monitor.sh for catching up that arr's cache
|
|
# once the pre-existing scan finishes instead of waiting for next week
|
|
# Sequential only — never runs two arrs' rescans in parallel; each is a heavy full-disk
|
|
# walk and concurrent walks would just contend for the same disk I/O
|
|
# Per-arr isolation — one arr failing, timing out, or being skipped never blocks the others
|
|
# --dry-run mode — reports which arrs would be rescanned, triggers nothing
|
|
#
|
|
# ==============================================================================================
|
|
# 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 (run Tools/arr_rescan_monitor.sh ${arr} to refresh its cache as soon as this one finishes instead of waiting)"
|
|
(( 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
|