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.
This commit is contained in:
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/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
|
||||
@@ -425,6 +425,7 @@
|
||||
"Docker_Essentials/docker_weekly_restart.sh" # weekly container restarts after sync
|
||||
"Docker_Essentials/docker_update_remaining.sh" # pull updates for all other containers
|
||||
"unRAID_Essentials/clear_logs.sh" # purge aged logs — Sunday only, low priority
|
||||
"Arrs_Stack/arr_full_rescan.sh" # full disk↔DB reconciliation for Lidarr/Sonarr/Radarr — keeps tracked stats honest, runs before discovery so it works off fresh data
|
||||
"Arrs_Stack/playback_aware_lidarr_discovery.sh" # behavior-driven music discovery using weekly Emby playback history
|
||||
"Arrs_Stack/playback_aware_radarr_discovery.sh" # behavior-driven movie discovery using TMDB recommendations
|
||||
"Arrs_Stack/playback_aware_sonarr_discovery.sh" # behavior-driven TV discovery using TMDB recommendations
|
||||
@@ -1240,6 +1241,11 @@
|
||||
# candidate is this language; an existing file in a
|
||||
# different language is always considered upgradeable
|
||||
|
||||
# ━━━ Arr Full Library Rescan ━━━
|
||||
# arr_full_rescan.sh — weekly disk↔DB reconciliation for Lidarr/Sonarr/Radarr. See script
|
||||
# header for why this exists (2026-07-16 stale-stats incident).
|
||||
ARR_FULL_RESCAN_TIMEOUT=3600 # seconds per arr — a full-library walk, not a single release
|
||||
|
||||
# ==============================================================================================
|
||||
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
Reference in New Issue
Block a user