Headers claimed protections the code never had, and several destructive paths had no guard against a collapsed config value.
173 lines
8.1 KiB
Bash
Executable File
173 lines
8.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ========================= Arr Rescan Monitor ==================================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Waits for an already-active rescan-type command (RescanFolders/RescanSeries/RescanMovie,
|
|
# DownloadedXScan, RefreshX — see ARR_RESCAN_COMMANDS in common.sh) on the given arr to finish,
|
|
# then refreshes the shared tracked-data cache (arr_cache_write) with the real post-scan
|
|
# numbers. Triggers nothing itself — for watching a rescan that was already started some
|
|
# other way (manual API call, arr's own UI, another script).
|
|
#
|
|
# Exists because arr_cache_write() now refuses to write while a rescan is active for that arr
|
|
# (2026-07-17 — a direct cache write mid-scan read as real data loss to every consumer of the
|
|
# cache, e.g. check_tracked_count_floor, instead of what it was: an in-flight scan). That
|
|
# guard protects the cache, but it also means nothing writes the real number through once the
|
|
# scan finishes unless something is watching for completion — arr_full_rescan.sh already does
|
|
# this for scans it triggers itself; this tool covers everything else.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# 1. Validate the arr type and resolve its URL / API key / API version
|
|
# 2. Poll /command for any active rescan-type command (ARR_RESCAN_COMMANDS)
|
|
# none active → nothing to watch, exit cleanly
|
|
# 3. Wait for it to leave the active state, bounded by the timeout argument
|
|
# (default 7200s — a full-library scan is slow by nature)
|
|
# 4. Once finished, fetch the library and arr_cache_write() the real numbers
|
|
#
|
|
# Triggers nothing. It only watches a scan someone or something else started.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Watch, Never Trigger
|
|
# This tool deliberately starts nothing. Its whole job is to be the thing that notices a
|
|
# scan finished, so a rescan begun by hand, by the arr's own UI, or by another script
|
|
# still gets its result written through to the shared cache.
|
|
#
|
|
# Complement to the Write Guard
|
|
# arr_cache_write() refuses to write while a rescan is active, because a mid-scan snapshot
|
|
# reads as real data loss to consumers like check_tracked_count_floor. That guard protects
|
|
# the cache but leaves nobody to write the true number afterwards. arr_full_rescan.sh
|
|
# covers the scans it starts itself; this covers every other origin.
|
|
#
|
|
# Bounded Wait
|
|
# The wait is capped by the timeout argument, so a scan that stalls or never reports
|
|
# completion cannot leave this running indefinitely holding its lock.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# host*.conf (aliased by detect_hosts())
|
|
#
|
|
# SONARR_URL / SONARR_API_KEY
|
|
# RADARR_URL / RADARR_API_KEY
|
|
# LIDARR_URL / LIDARR_API_KEY
|
|
# Connection details for whichever arr is passed as the first argument.
|
|
#
|
|
# common.sh
|
|
#
|
|
# ARR_RESCAN_COMMANDS
|
|
# Command names treated as a rescan for this purpose — shared with
|
|
# arr_cache_write()'s own active-scan guard so both agree on what counts.
|
|
#
|
|
# ARR_API_VERSION
|
|
# Per-arr API version map (v3 for Sonarr/Radarr, v1 for Lidarr).
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Root required — matches convention across Arrs_Stack/Tools/, writes cache files
|
|
# Arg validation — arr_type must be lidarr/sonarr/radarr, usage error otherwise
|
|
# Timeout, never waits forever — default 2h ceiling, override via second positional arg;
|
|
# on timeout, cache is left untouched rather than acted on stale
|
|
# --status mode — read-only, shows active rescan + cache age, waits/writes nothing
|
|
# --dry-run mode — shows what would be waited on, waits/writes nothing
|
|
# Triggers nothing — never starts a rescan itself, only watches one already running;
|
|
# use arr_full_rescan.sh or the arr's own UI/API to start one
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# arr_rescan_monitor.sh lidarr
|
|
# Wait for lidarr's active rescan (if any) to finish, then refresh its cache.
|
|
# Exits immediately if no rescan is currently active — nothing to wait for.
|
|
#
|
|
# arr_rescan_monitor.sh lidarr 3600
|
|
# Override the default 2-hour wait ceiling (second positional arg, seconds).
|
|
#
|
|
# arr_rescan_monitor.sh lidarr --status
|
|
# Show whether a rescan is currently active and the cache's current age, no waiting.
|
|
#
|
|
# arr_rescan_monitor.sh lidarr --dry-run
|
|
# Show what would be waited on without actually waiting or writing the cache.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
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
|
|
|
|
# This writes the shared tracked-data cache on completion. Two monitors watching the same
|
|
# arr would both write it, and the loser's stale snapshot could land last.
|
|
acquire_lock
|
|
|
|
detect_hosts
|
|
|
|
ARR_TYPE="${PARSED_ARGS[0]:-}"
|
|
TIMEOUT="${PARSED_ARGS[1]:-7200}"
|
|
|
|
case "$ARR_TYPE" in
|
|
lidarr|sonarr|radarr) ;;
|
|
*) error "Usage: arr_rescan_monitor.sh lidarr|sonarr|radarr [timeout_seconds] [--status] [--dry-run]"; exit 1 ;;
|
|
esac
|
|
|
|
url_var="${ARR_TYPE^^}_URL"; key_var="${ARR_TYPE^^}_API_KEY"
|
|
URL="${!url_var:-}"; KEY="${!key_var:-}"
|
|
VER="${ARR_API_VERSION[$ARR_TYPE]}"
|
|
|
|
require_var "$url_var"
|
|
require_var "$key_var"
|
|
|
|
ACTIVE=$(arr_active_rescan_command "$ARR_TYPE" "$URL" "$KEY" "$VER")
|
|
CACHE_AGE=$(arr_cache_age_seconds "$ARR_TYPE")
|
|
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
info "${ARR_TYPE^} active rescan: ${ACTIVE:-none}"
|
|
info "${ARR_TYPE^} cache age: $(( CACHE_AGE / 60 )) minutes"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ -z "$ACTIVE" ]]; then
|
|
info "No active rescan on ${ARR_TYPE^} — nothing to wait for."
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would wait for ${ARR_TYPE^}'s active $ACTIVE (timeout ${TIMEOUT}s), then refresh its cache"
|
|
exit 0
|
|
fi
|
|
|
|
info "Waiting for ${ARR_TYPE^}'s active $ACTIVE to finish (timeout ${TIMEOUT}s)..."
|
|
if ! arr_wait_for_active_rescan "$ARR_TYPE" "$URL" "$KEY" "$VER" "$TIMEOUT"; then
|
|
error "Timed out after ${TIMEOUT}s waiting for ${ARR_TYPE^}'s rescan — cache not refreshed. Re-run once it finishes, or it'll pick up naturally next time something calls arr_get_tracked_data()."
|
|
exit 1
|
|
fi
|
|
|
|
ENDPOINT="${ARR_LIBRARY_ENDPOINT[$ARR_TYPE]}"
|
|
EXPR="${ARR_TRACKED_COUNT_EXPR[$ARR_TYPE]}"
|
|
ITEMS=$(curl -sf --max-time 60 -H "X-Api-Key: $KEY" "${URL}/api/${VER}/${ENDPOINT}" 2>/dev/null)
|
|
TOTAL=$(echo "$ITEMS" | jq "$EXPR" 2>/dev/null)
|
|
|
|
if [[ -z "$ITEMS" || -z "$TOTAL" || "$TOTAL" == "null" ]]; then
|
|
error "${ARR_TYPE^} rescan finished but couldn't fetch fresh library data — cache not refreshed"
|
|
exit 1
|
|
fi
|
|
|
|
arr_cache_write "$ARR_TYPE" "$ITEMS"
|
|
success "${ARR_TYPE^} rescan finished — cache refreshed, tracked: ${TOTAL}"
|
|
notify "${ARR_TYPE^} rescan finished on $(hostname) — cache refreshed, tracked: ${TOTAL}" "Arr Rescan Monitor" "normal"
|