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:
@@ -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