Generalize tracked-data cache from Lidarr-only to all three arrs
Shared cache/rescan-duration logic in common.sh now takes an arr_type param instead of being Lidarr-specific, so Sonarr and Radarr cleanup scripts get the same cache-first fetch + rescan-aware retry Lidarr had. Avoids redundant full-library API calls across scripts run back to back, and stops false failures when a fetch lands mid-rescan.
This commit is contained in:
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Arr Cache Prefill ===========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Populates the shared tracked-data cache (see arr_get_tracked_data() in common.sh) for
|
||||
# Lidarr, Sonarr, and Radarr once at array start, before any other script needs it. Without
|
||||
# this, each arr's cache stays cold from boot until whichever script happens to touch that
|
||||
# arr first writes through — which could be hours, depending on the daily schedule. One-shot:
|
||||
# runs and exits. Originally Lidarr-only (lidarr_cache_prefill.sh, 2026-07-16), generalized
|
||||
# the same day to cover all three arrs once the cache mechanism itself was generalized.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Each arr's container may still be starting when this fires (array just started) — retries
|
||||
# reaching that arr's API for up to ARR_PREFILL_WAIT_MINUTES before giving up on it and moving
|
||||
# to the next. Not fatal if one never comes up in time; that arr's cache just stays cold until
|
||||
# the next script writes through naturally, exactly as it would without this script existing.
|
||||
# An arr not configured on this host (e.g. Lidarr is HOST1-only) is skipped cleanly.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY
|
||||
# HOST*_SONARR_URL / HOST*_SONARR_API_KEY
|
||||
# HOST*_RADARR_URL / HOST*_RADARR_API_KEY
|
||||
# All aliased by detect_hosts()
|
||||
#
|
||||
# master.conf
|
||||
# ARR_PREFILL_WAIT_MINUTES — how long to retry reaching each arr before giving up on it (default 10)
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
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
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
warn "jq not found — skipping arr cache prefill"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
acquire_lock
|
||||
|
||||
detect_hosts
|
||||
|
||||
WAIT_MINUTES="${ARR_PREFILL_WAIT_MINUTES:-10}"
|
||||
|
||||
# Args: arr_type, url, api_key, api_version
|
||||
_prefill_one() {
|
||||
local arr_type="$1" url="$2" api_key="$3" api_version="$4"
|
||||
|
||||
if [[ -z "$url" ]] || [[ -z "$api_key" ]]; then
|
||||
info "${arr_type^} not configured on $MY_ID ($LOCAL_SERVER_NAME) — nothing to prefill"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local waited=0
|
||||
until check_api "$url" "${arr_type^}" 5 >/dev/null 2>&1; do
|
||||
if [[ "$waited" -ge $(( WAIT_MINUTES * 60 )) ]]; then
|
||||
warn "${arr_type^} not reachable after ${WAIT_MINUTES}m — leaving cache cold, next script will write through"
|
||||
return 0
|
||||
fi
|
||||
sleep 15
|
||||
(( waited += 15 ))
|
||||
done
|
||||
|
||||
local endpoint="${ARR_LIBRARY_ENDPOINT[$arr_type]:-}"
|
||||
if [[ -z "$endpoint" ]]; then
|
||||
warn "No library endpoint known for ${arr_type} — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local items
|
||||
items=$(arr_api "$url" "$api_key" "$api_version" "$endpoint" "${arr_type^}") || {
|
||||
warn "Could not fetch ${arr_type} library for cache prefill — leaving cache cold"
|
||||
return 0
|
||||
}
|
||||
|
||||
if arr_cache_write "$arr_type" "$items"; then
|
||||
log "$ICON_DONE ${arr_type^} cache prefilled ($(echo "$items" | jq 'length') items)"
|
||||
else
|
||||
warn "Failed to write ${arr_type} cache prefill"
|
||||
fi
|
||||
}
|
||||
|
||||
_prefill_one "lidarr" "${LIDARR_URL:-}" "${LIDARR_API_KEY:-}" "v1"
|
||||
_prefill_one "sonarr" "${SONARR_URL:-}" "${SONARR_API_KEY:-}" "v3"
|
||||
_prefill_one "radarr" "${RADARR_URL:-}" "${RADARR_API_KEY:-}" "v3"
|
||||
|
||||
exit 0
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Lidarr Cache Prefill ========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Populates the shared Lidarr tracked-data cache (see lidarr_get_tracked_data() in common.sh)
|
||||
# once at array start, before any other script needs it. Without this, the cache stays cold
|
||||
# from boot until whichever Lidarr-touching script happens to run first and write through —
|
||||
# which could be hours, depending on the daily schedule. One-shot: runs and exits.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Lidarr's container may still be starting when this fires (array just started) — retries
|
||||
# reaching the API for up to LIDARR_PREFILL_WAIT_MINUTES before giving up. Not fatal if it
|
||||
# never comes up in time; the cache just stays cold until the next script writes through
|
||||
# naturally, exactly as it would without this script existing at all.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
# HOST1_LIDARR_URL / HOST1_LIDARR_API_KEY — aliased by detect_hosts()
|
||||
#
|
||||
# master.conf
|
||||
# LIDARR_PREFILL_WAIT_MINUTES — how long to retry reaching Lidarr before giving up (default 10)
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
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
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
warn "jq not found — skipping Lidarr cache prefill"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
acquire_lock
|
||||
|
||||
detect_hosts
|
||||
|
||||
if [[ -z "${LIDARR_URL:-}" ]] || [[ -z "${LIDARR_API_KEY:-}" ]]; then
|
||||
info "Lidarr not configured on $MY_ID ($LOCAL_SERVER_NAME) — nothing to prefill"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WAIT_MINUTES="${LIDARR_PREFILL_WAIT_MINUTES:-10}"
|
||||
WAITED=0
|
||||
until check_api "$LIDARR_URL" "Lidarr" 5 >/dev/null 2>&1; do
|
||||
if [[ "$WAITED" -ge $(( WAIT_MINUTES * 60 )) ]]; then
|
||||
warn "Lidarr not reachable after ${WAIT_MINUTES}m — leaving cache cold, next script will write through"
|
||||
exit 0
|
||||
fi
|
||||
sleep 15
|
||||
(( WAITED += 15 ))
|
||||
done
|
||||
|
||||
ARTISTS=$(arr_api "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "artist" "Lidarr") || {
|
||||
warn "Could not fetch artists for cache prefill — leaving cache cold"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if lidarr_cache_write "$ARTISTS"; then
|
||||
log "$ICON_DONE Lidarr cache prefilled ($(echo "$ARTISTS" | jq 'length') artists)"
|
||||
else
|
||||
warn "Failed to write Lidarr cache prefill"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -15,7 +15,7 @@
|
||||
# 22% of normal during an active RescanFolders), which used to trigger this script's own
|
||||
# hard abort every time it overlapped with a real rescan. Now it checks for an active
|
||||
# rescan-type command first — if one's running, it waits (calibrated to that command's
|
||||
# own historical duration via lidarr_get_rescan_duration(), up to 3 strikes) and re-fetches
|
||||
# own historical duration via arr_get_rescan_duration(), up to 3 strikes) and re-fetches
|
||||
# rather than either stacking a duplicate scan or crying wolf on a normal, if slow, state.
|
||||
# Only escalates to the scary abort-and-notify when the count is genuinely low AND nothing
|
||||
# is actively rescanning.
|
||||
@@ -260,14 +260,14 @@ unset _cp
|
||||
# repeated runs each firing their own DownloadedAlbumsScan piled up in Lidarr's command
|
||||
# queue behind each other rather than replacing/coalescing, contributing to a multi-hour
|
||||
# backlog. If something's already scanning, just wait for that one instead.
|
||||
_already_active=$(lidarr_active_rescan_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
_already_active=$(arr_active_rescan_command "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
|
||||
if [[ -n "$_already_active" ]]; then
|
||||
info "$_already_active already in progress — waiting for it instead of starting a new scan"
|
||||
_wait=$(( $(lidarr_get_rescan_duration "$_already_active" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}") ))
|
||||
_wait=$(( $(arr_get_rescan_duration "lidarr" "$_already_active" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}") ))
|
||||
_polled=0
|
||||
while [[ "$_polled" -lt "$_wait" ]]; do
|
||||
[[ -z "$(lidarr_active_rescan_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1")" ]] && break
|
||||
[[ -z "$(arr_active_rescan_command "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1")" ]] && break
|
||||
sleep 15
|
||||
(( _polled += 15 ))
|
||||
[[ $(( _polled % 60 )) -eq 0 ]] && log " Still waiting on $_already_active... (${_polled}s elapsed)"
|
||||
@@ -275,11 +275,11 @@ if [[ -n "$_already_active" ]]; then
|
||||
elif [[ -n "$LIDARR_CONTAINER_ROOT" ]]; then
|
||||
info "Triggering DownloadedAlbumsScan on: $LIDARR_CONTAINER_ROOT"
|
||||
SCAN_PAYLOAD="{\"name\": \"DownloadedAlbumsScan\", \"path\": \"$LIDARR_CONTAINER_ROOT\"}"
|
||||
trigger_and_await_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$SCAN_PAYLOAD" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}"
|
||||
trigger_and_await_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$SCAN_PAYLOAD" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}" "lidarr"
|
||||
else
|
||||
info "No path map match — triggering DownloadedAlbumsScan (all root folders)"
|
||||
SCAN_PAYLOAD='{"name": "DownloadedAlbumsScan"}'
|
||||
trigger_and_await_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$SCAN_PAYLOAD" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}"
|
||||
trigger_and_await_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "$SCAN_PAYLOAD" "${LIDARR_IMPORT_SCAN_TIMEOUT:-600}" "lidarr"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
@@ -306,6 +306,8 @@ ARTIST_RESPONSE=$(arr_api "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "artist" "Lidarr"
|
||||
"Lidarr Cleanup" "warning"
|
||||
exit 1
|
||||
}
|
||||
# Write-through — free cache refresh from a fetch this script already needed.
|
||||
arr_cache_write "lidarr" "$ARTIST_RESPONSE"
|
||||
|
||||
ARTIST_IDS=$(echo "$ARTIST_RESPONSE" | jq -r '.[].id' 2>/dev/null)
|
||||
ARTIST_COUNT=$(echo "$ARTIST_IDS" | grep -c "[0-9]" 2>/dev/null || echo 0)
|
||||
@@ -379,10 +381,10 @@ if [[ "$_last_known" -gt 0 ]]; then
|
||||
_pct=$(awk "BEGIN {printf \"%d\", ($TRACKED_COUNT / $_last_known) * 100}")
|
||||
[[ "$_pct" -ge "${LIDARR_MIN_TRACKED_PCT:-50}" ]] && break
|
||||
|
||||
_active_cmd=$(lidarr_active_rescan_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
_active_cmd=$(arr_active_rescan_command "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
[[ -z "$_active_cmd" ]] && break # low count, nothing rescanning — genuine, don't retry
|
||||
|
||||
_wait=$(( $(lidarr_get_rescan_duration "$_active_cmd" 300) / 2 ))
|
||||
_wait=$(( $(arr_get_rescan_duration "lidarr" "$_active_cmd" 300) / 2 ))
|
||||
[[ "$_wait" -lt 30 ]] && _wait=30
|
||||
warn "Tracked count ${_pct}% of last run, but $_active_cmd active — waiting ${_wait}s (strike ${_strike}/3)"
|
||||
sleep "$_wait"
|
||||
@@ -391,7 +393,7 @@ if [[ "$_last_known" -gt 0 ]]; then
|
||||
done
|
||||
|
||||
if [[ "$_strike" -gt 3 ]]; then
|
||||
_active_cmd=$(lidarr_active_rescan_command "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
_active_cmd=$(arr_active_rescan_command "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
if [[ -n "$_active_cmd" ]]; then
|
||||
warn "Lidarr still busy ($_active_cmd) after 3 strikes — deferring to next scheduled run"
|
||||
exit 0
|
||||
|
||||
@@ -134,7 +134,7 @@ trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
# ━━━ Fetch Artists (cache-aware — waits out an active rescan rather than trusting a
|
||||
# mid-scan number, falls back to cache if Lidarr's still busy after the strike limit) ━━━
|
||||
# ==============================================================================================
|
||||
ARTISTS=$(lidarr_get_tracked_data "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
ARTISTS=$(arr_get_tracked_data "lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1")
|
||||
if [[ -z "$ARTISTS" ]]; then
|
||||
warn "Lidarr busy and no usable cache — deferring to next scheduled run"
|
||||
exit 0
|
||||
|
||||
@@ -256,8 +256,8 @@ echo "━━━ $ICON_SYNC Building Album Directory Map ━━━"
|
||||
declare -A ALBUM_DIR_MAP
|
||||
_artist_list=$(curl_json "$LIDARR_URL/api/v1/artist?apikey=$LIDARR_API_KEY")
|
||||
# Write-through — keeps the shared tracked-data cache fresh as a side effect of a fetch
|
||||
# this script already needed for its own purposes. See lidarr_get_tracked_data() in common.sh.
|
||||
lidarr_cache_write "$_artist_list"
|
||||
# this script already needed for its own purposes. See arr_get_tracked_data() in common.sh.
|
||||
arr_cache_write "lidarr" "$_artist_list"
|
||||
_map_artist_count=$(echo "$_artist_list" | jq '. | length')
|
||||
info "Fetching track files for $_map_artist_count artists..."
|
||||
|
||||
|
||||
@@ -307,8 +307,8 @@ ALL_ARTISTS=$(lidarr_api "artist") || {
|
||||
exit 1
|
||||
}
|
||||
# Write-through — keeps the shared tracked-data cache fresh as a side effect of a fetch
|
||||
# this script already needed for its own purposes. See lidarr_get_tracked_data() in common.sh.
|
||||
lidarr_cache_write "$ALL_ARTISTS"
|
||||
# this script already needed for its own purposes. See arr_get_tracked_data() in common.sh.
|
||||
arr_cache_write "lidarr" "$ALL_ARTISTS"
|
||||
|
||||
while IFS= read -r artist; do
|
||||
aid=$(echo "$artist" | jq -r '.id')
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
# untracked that is old enough to be past the import window. Triggers an Emby
|
||||
# library clean after each deletion run so ghost entries disappear immediately.
|
||||
#
|
||||
# The tracked-count floor check (Safety Layer 6) is rescan-aware: if Radarr's own
|
||||
# RescanMovie/DownloadedMoviesScan is active (independently of this script's own
|
||||
# lighter ProcessMonitoredDownloads pre-flight), a genuinely low mid-scan count gets
|
||||
# waited out (calibrated to that command's historical duration via
|
||||
# arr_get_rescan_duration(), up to 3 strikes) and re-fetched rather than triggering a
|
||||
# false-alarm abort. Mirrors the same fix built for lidarr_cleanup.sh 2026-07-16 after
|
||||
# a whole-library rescan there made trackFileCount read 22% of normal mid-scan.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
@@ -251,7 +259,7 @@ info "Scan targets (${#SCAN_ROOTS[@]}): ${SCAN_ROOTS[*]}"
|
||||
info "Triggering ProcessMonitoredDownloads pre-flight"
|
||||
SCAN_PAYLOAD='{"name": "ProcessMonitoredDownloads"}'
|
||||
|
||||
trigger_and_await_command "$RADARR_URL" "$RADARR_API_KEY" "v3" "$SCAN_PAYLOAD" "${RADARR_IMPORT_SCAN_TIMEOUT:-600}"
|
||||
trigger_and_await_command "$RADARR_URL" "$RADARR_API_KEY" "v3" "$SCAN_PAYLOAD" "${RADARR_IMPORT_SCAN_TIMEOUT:-600}" "radarr"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Fetch Radarr Tracked Files ━━━
|
||||
@@ -277,6 +285,8 @@ MOVIES_RESPONSE=$(arr_api "$RADARR_URL" "$RADARR_API_KEY" "v3" "movie" "Radarr")
|
||||
"Radarr Cleanup" "warning"
|
||||
exit 1
|
||||
}
|
||||
# Write-through — free cache refresh from a fetch this script already needed.
|
||||
arr_cache_write "radarr" "$MOVIES_RESPONSE"
|
||||
|
||||
MOVIE_IDS=$(echo "$MOVIES_RESPONSE" | jq -r '.[].id' 2>/dev/null)
|
||||
MOVIE_COUNT=$(echo "$MOVIE_IDS" | grep -c "." 2>/dev/null || echo 0)
|
||||
@@ -294,32 +304,41 @@ info "Found $MOVIE_COUNT movies — fetching movie files..."
|
||||
TRACKED_FILE="$TMP_DIR/tracked_paths.txt"
|
||||
> "$TRACKED_FILE"
|
||||
|
||||
MOVIE_INDEX=0
|
||||
while IFS= read -r movie_id; do
|
||||
[[ -z "$movie_id" ]] && continue
|
||||
(( MOVIE_INDEX++ ))
|
||||
[[ $(( MOVIE_INDEX % 100 )) -eq 0 ]] && \
|
||||
log "Fetching files: $MOVIE_INDEX/$MOVIE_COUNT movies..."
|
||||
MOVIE_FILES=$(arr_api "$RADARR_URL" "$RADARR_API_KEY" "v3" "moviefile?movieId=${movie_id}" "Radarr" 2>/dev/null)
|
||||
if [[ -n "$MOVIE_FILES" ]]; then
|
||||
while IFS= read -r api_path; do
|
||||
[[ -z "$api_path" ]] && continue
|
||||
translate_path "$api_path" >> "$TRACKED_FILE"
|
||||
done < <(echo "$MOVIE_FILES" | jq -r '.[].path // .path' 2>/dev/null)
|
||||
fi
|
||||
done <<< "$MOVIE_IDS"
|
||||
# Fetches every movie's file path fresh into TRACKED_FILE/TRACKED_MAP/TRACKED_COUNT. Pulled
|
||||
# into a function so the rescan-aware retry below can re-fetch after waiting without
|
||||
# duplicating this whole loop inline.
|
||||
_fetch_tracked_files() {
|
||||
> "$TRACKED_FILE"
|
||||
local _movie_index=0
|
||||
while IFS= read -r movie_id; do
|
||||
[[ -z "$movie_id" ]] && continue
|
||||
(( _movie_index++ ))
|
||||
[[ $(( _movie_index % 100 )) -eq 0 ]] && \
|
||||
log "Fetching files: $_movie_index/$MOVIE_COUNT movies..."
|
||||
MOVIE_FILES=$(arr_api "$RADARR_URL" "$RADARR_API_KEY" "v3" "moviefile?movieId=${movie_id}" "Radarr" 2>/dev/null)
|
||||
if [[ -n "$MOVIE_FILES" ]]; then
|
||||
while IFS= read -r api_path; do
|
||||
[[ -z "$api_path" ]] && continue
|
||||
translate_path "$api_path" >> "$TRACKED_FILE"
|
||||
done < <(echo "$MOVIE_FILES" | jq -r '.[].path // .path' 2>/dev/null)
|
||||
fi
|
||||
done <<< "$MOVIE_IDS"
|
||||
|
||||
sort -u "$TRACKED_FILE" -o "$TRACKED_FILE"
|
||||
sort -u "$TRACKED_FILE" -o "$TRACKED_FILE"
|
||||
|
||||
# Build in-memory lookup map — O(1) per lookup vs O(n) grep per file
|
||||
# Eliminates the main performance bottleneck for large libraries
|
||||
declare -A TRACKED_MAP
|
||||
while IFS= read -r _tracked_path; do
|
||||
[[ -n "$_tracked_path" ]] && TRACKED_MAP["$_tracked_path"]=1
|
||||
done < "$TRACKED_FILE"
|
||||
unset _tracked_path
|
||||
# Build in-memory lookup map — O(1) per lookup vs O(n) grep per file
|
||||
# Eliminates the main performance bottleneck for large libraries
|
||||
unset TRACKED_MAP
|
||||
declare -gA TRACKED_MAP
|
||||
while IFS= read -r _tracked_path; do
|
||||
[[ -n "$_tracked_path" ]] && TRACKED_MAP["$_tracked_path"]=1
|
||||
done < "$TRACKED_FILE"
|
||||
unset _tracked_path
|
||||
TRACKED_COUNT=$(wc -l < "$TRACKED_FILE")
|
||||
}
|
||||
|
||||
_fetch_tracked_files
|
||||
info "Built in-memory lookup map: ${#TRACKED_MAP[@]} tracked paths"
|
||||
TRACKED_COUNT=$(wc -l < "$TRACKED_FILE")
|
||||
|
||||
# Safety Layer 5 — tracked count > 0
|
||||
if [[ "$TRACKED_COUNT" -eq 0 ]]; then
|
||||
@@ -331,7 +350,39 @@ fi
|
||||
|
||||
info "$MOVIE_COUNT movies | $TRACKED_COUNT tracked movie files"
|
||||
|
||||
# Safety Layer 6 — percentage drop vs last known count
|
||||
# Safety Layer 6 — percentage drop vs last known count, with rescan-aware retry.
|
||||
# ProcessMonitoredDownloads (this script's own pre-flight) is a different, lighter operation
|
||||
# than a full library rescan — but Radarr's own RescanMovie/DownloadedMoviesScan can be
|
||||
# triggered independently and would cause the exact same mid-scan count dip confirmed on
|
||||
# Lidarr 2026-07-16. Wait it out (calibrated to that command's own historical duration)
|
||||
# before treating a drop as genuine.
|
||||
_last_known=$(cat "$RADARR_TRACKED_COUNT_FILE" 2>/dev/null || echo 0)
|
||||
if [[ "$_last_known" -gt 0 ]]; then
|
||||
_strike=1
|
||||
while [[ "$_strike" -le 3 ]]; do
|
||||
_pct=$(awk "BEGIN {printf \"%d\", ($TRACKED_COUNT / $_last_known) * 100}")
|
||||
[[ "$_pct" -ge "${RADARR_MIN_TRACKED_PCT:-50}" ]] && break
|
||||
|
||||
_active_cmd=$(arr_active_rescan_command "radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3")
|
||||
[[ -z "$_active_cmd" ]] && break # low count, nothing rescanning — genuine, don't retry
|
||||
|
||||
_wait=$(( $(arr_get_rescan_duration "radarr" "$_active_cmd" 300) / 2 ))
|
||||
[[ "$_wait" -lt 30 ]] && _wait=30
|
||||
warn "Tracked count ${_pct}% of last run, but $_active_cmd active — waiting ${_wait}s (strike ${_strike}/3)"
|
||||
sleep "$_wait"
|
||||
_fetch_tracked_files
|
||||
(( _strike++ ))
|
||||
done
|
||||
|
||||
if [[ "$_strike" -gt 3 ]]; then
|
||||
_active_cmd=$(arr_active_rescan_command "radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3")
|
||||
if [[ -n "$_active_cmd" ]]; then
|
||||
warn "Radarr still busy ($_active_cmd) after 3 strikes — deferring to next scheduled run"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
check_tracked_count_floor "$TRACKED_COUNT" "$RADARR_TRACKED_COUNT_FILE" "$RADARR_MIN_TRACKED_PCT" "Radarr Cleanup"
|
||||
|
||||
# ==============================================================================================
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
# untracked that is old enough to be past the import window. Triggers an Emby
|
||||
# library clean after each deletion run so ghost entries disappear immediately.
|
||||
#
|
||||
# The tracked-count floor check (Safety Layer 6) is rescan-aware: if Sonarr's own
|
||||
# RescanSeries/DownloadedEpisodesScan is active (independently of this script's own
|
||||
# lighter ProcessMonitoredDownloads pre-flight — e.g. during a large missing-episode
|
||||
# search campaign), a genuinely low mid-scan count gets waited out (calibrated to that
|
||||
# command's historical duration via arr_get_rescan_duration(), up to 3 strikes) and
|
||||
# re-fetched rather than triggering a false-alarm abort. Mirrors the same fix built for
|
||||
# lidarr_cleanup.sh 2026-07-16 after a whole-library rescan there made trackFileCount
|
||||
# read 22% of normal mid-scan.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
@@ -251,7 +260,7 @@ info "Scan targets (${#SCAN_ROOTS[@]}): ${SCAN_ROOTS[*]}"
|
||||
info "Triggering ProcessMonitoredDownloads pre-flight"
|
||||
SCAN_PAYLOAD='{"name": "ProcessMonitoredDownloads"}'
|
||||
|
||||
trigger_and_await_command "$SONARR_URL" "$SONARR_API_KEY" "v3" "$SCAN_PAYLOAD" "${SONARR_IMPORT_SCAN_TIMEOUT:-600}"
|
||||
trigger_and_await_command "$SONARR_URL" "$SONARR_API_KEY" "v3" "$SCAN_PAYLOAD" "${SONARR_IMPORT_SCAN_TIMEOUT:-600}" "sonarr"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Fetch Sonarr Tracked Files ━━━
|
||||
@@ -277,6 +286,8 @@ SERIES_RESPONSE=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "series" "Sonarr"
|
||||
"Sonarr Cleanup" "warning"
|
||||
exit 1
|
||||
}
|
||||
# Write-through — free cache refresh from a fetch this script already needed.
|
||||
arr_cache_write "sonarr" "$SERIES_RESPONSE"
|
||||
|
||||
SERIES_IDS=$(echo "$SERIES_RESPONSE" | jq -r '.[].id' 2>/dev/null)
|
||||
SERIES_COUNT=$(echo "$SERIES_IDS" | grep -c "." 2>/dev/null || echo 0)
|
||||
@@ -294,31 +305,40 @@ info "Found $SERIES_COUNT series — fetching episode files..."
|
||||
TRACKED_FILE="$TMP_DIR/tracked_paths.txt"
|
||||
> "$TRACKED_FILE"
|
||||
|
||||
SERIES_INDEX=0
|
||||
while IFS= read -r series_id; do
|
||||
[[ -z "$series_id" ]] && continue
|
||||
(( SERIES_INDEX++ ))
|
||||
[[ $(( SERIES_INDEX % 50 )) -eq 0 ]] && \
|
||||
log "Fetching files: $SERIES_INDEX/$SERIES_COUNT series..."
|
||||
SERIES_FILES=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "episodefile?seriesId=${series_id}" "Sonarr" 2>/dev/null)
|
||||
if [[ -n "$SERIES_FILES" ]]; then
|
||||
while IFS= read -r api_path; do
|
||||
[[ -z "$api_path" ]] && continue
|
||||
translate_path "$api_path" >> "$TRACKED_FILE"
|
||||
done < <(echo "$SERIES_FILES" | jq -r '.[].path' 2>/dev/null)
|
||||
fi
|
||||
done <<< "$SERIES_IDS"
|
||||
# Fetches every series' episode-file paths fresh into TRACKED_FILE/TRACKED_MAP/TRACKED_COUNT.
|
||||
# Pulled into a function so the rescan-aware retry below can re-fetch after waiting without
|
||||
# duplicating this whole loop inline.
|
||||
_fetch_tracked_files() {
|
||||
> "$TRACKED_FILE"
|
||||
local _series_index=0
|
||||
while IFS= read -r series_id; do
|
||||
[[ -z "$series_id" ]] && continue
|
||||
(( _series_index++ ))
|
||||
[[ $(( _series_index % 50 )) -eq 0 ]] && \
|
||||
log "Fetching files: $_series_index/$SERIES_COUNT series..."
|
||||
SERIES_FILES=$(arr_api "$SONARR_URL" "$SONARR_API_KEY" "v3" "episodefile?seriesId=${series_id}" "Sonarr" 2>/dev/null)
|
||||
if [[ -n "$SERIES_FILES" ]]; then
|
||||
while IFS= read -r api_path; do
|
||||
[[ -z "$api_path" ]] && continue
|
||||
translate_path "$api_path" >> "$TRACKED_FILE"
|
||||
done < <(echo "$SERIES_FILES" | jq -r '.[].path' 2>/dev/null)
|
||||
fi
|
||||
done <<< "$SERIES_IDS"
|
||||
|
||||
sort -u "$TRACKED_FILE" -o "$TRACKED_FILE"
|
||||
sort -u "$TRACKED_FILE" -o "$TRACKED_FILE"
|
||||
|
||||
# Build in-memory lookup map — O(1) per lookup vs O(n) grep per file
|
||||
declare -A TRACKED_MAP
|
||||
while IFS= read -r _tracked_path; do
|
||||
[[ -n "$_tracked_path" ]] && TRACKED_MAP["$_tracked_path"]=1
|
||||
done < "$TRACKED_FILE"
|
||||
unset _tracked_path
|
||||
# Build in-memory lookup map — O(1) per lookup vs O(n) grep per file
|
||||
unset TRACKED_MAP
|
||||
declare -gA TRACKED_MAP
|
||||
while IFS= read -r _tracked_path; do
|
||||
[[ -n "$_tracked_path" ]] && TRACKED_MAP["$_tracked_path"]=1
|
||||
done < "$TRACKED_FILE"
|
||||
unset _tracked_path
|
||||
TRACKED_COUNT=$(wc -l < "$TRACKED_FILE")
|
||||
}
|
||||
|
||||
_fetch_tracked_files
|
||||
info "Built in-memory lookup map: ${#TRACKED_MAP[@]} tracked paths"
|
||||
TRACKED_COUNT=$(wc -l < "$TRACKED_FILE")
|
||||
|
||||
# Safety Layer 5 — tracked count > 0
|
||||
if [[ "$TRACKED_COUNT" -eq 0 ]]; then
|
||||
@@ -330,7 +350,39 @@ fi
|
||||
|
||||
info "$SERIES_COUNT series | $TRACKED_COUNT tracked episode files"
|
||||
|
||||
# Safety Layer 6 — percentage drop vs last known count
|
||||
# Safety Layer 6 — percentage drop vs last known count, with rescan-aware retry.
|
||||
# ProcessMonitoredDownloads (this script's own pre-flight) is a different, lighter operation
|
||||
# than a full library rescan — but Sonarr's own RescanSeries/DownloadedEpisodesScan can be
|
||||
# triggered independently (e.g. during a large missing-episode search campaign) and would
|
||||
# cause the exact same mid-scan count dip confirmed on Lidarr 2026-07-16. Wait it out
|
||||
# (calibrated to that command's own historical duration) before treating a drop as genuine.
|
||||
_last_known=$(cat "$SONARR_TRACKED_COUNT_FILE" 2>/dev/null || echo 0)
|
||||
if [[ "$_last_known" -gt 0 ]]; then
|
||||
_strike=1
|
||||
while [[ "$_strike" -le 3 ]]; do
|
||||
_pct=$(awk "BEGIN {printf \"%d\", ($TRACKED_COUNT / $_last_known) * 100}")
|
||||
[[ "$_pct" -ge "${SONARR_MIN_TRACKED_PCT:-50}" ]] && break
|
||||
|
||||
_active_cmd=$(arr_active_rescan_command "sonarr" "$SONARR_URL" "$SONARR_API_KEY" "v3")
|
||||
[[ -z "$_active_cmd" ]] && break # low count, nothing rescanning — genuine, don't retry
|
||||
|
||||
_wait=$(( $(arr_get_rescan_duration "sonarr" "$_active_cmd" 300) / 2 ))
|
||||
[[ "$_wait" -lt 30 ]] && _wait=30
|
||||
warn "Tracked count ${_pct}% of last run, but $_active_cmd active — waiting ${_wait}s (strike ${_strike}/3)"
|
||||
sleep "$_wait"
|
||||
_fetch_tracked_files
|
||||
(( _strike++ ))
|
||||
done
|
||||
|
||||
if [[ "$_strike" -gt 3 ]]; then
|
||||
_active_cmd=$(arr_active_rescan_command "sonarr" "$SONARR_URL" "$SONARR_API_KEY" "v3")
|
||||
if [[ -n "$_active_cmd" ]]; then
|
||||
warn "Sonarr still busy ($_active_cmd) after 3 strikes — deferring to next scheduled run"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
check_tracked_count_floor "$TRACKED_COUNT" "$SONARR_TRACKED_COUNT_FILE" "$SONARR_MIN_TRACKED_PCT" "Sonarr Cleanup"
|
||||
|
||||
# ==============================================================================================
|
||||
|
||||
Reference in New Issue
Block a user