diff --git a/Arrs_Stack/sonarr_classification_scan.sh b/Arrs_Stack/sonarr_classification_scan.sh index 25f2cd5..3863bb0 100755 --- a/Arrs_Stack/sonarr_classification_scan.sh +++ b/Arrs_Stack/sonarr_classification_scan.sh @@ -76,10 +76,13 @@ # rootFolderPath/path corrected and an immediate SeriesSearch triggered rather than a file # move (mirrors radarr_classification_scan.sh's handling of hasFile=false movies). # -# One series at a time, verified after each — a rapid-fire batch of Sonarr moves is exactly -# what raced Sonarr's own file-move worker earlier this session doing this by hand (2 series -# reported "success" while their files sat at an intermediate path). A short sleep plus a -# real re-fetch-and-check after every single move catches that here before it can compound. +# One series at a time, verified after each. moveFiles=true flips the DB (rootFolderPath/ +# episodeFileCount) instantly, but the physical move is a separate async MoveSeries command +# Sonarr drains one at a time internally — DB fields alone can report "moved" while the real +# files are still sitting at the old path behind other queued moves (confirmed live: "Full +# House" reported episodeFileCount:192 at the new path via API while the actual 75GB/192 +# files hadn't moved yet). Each move polls its own MoveSeries command to "completed" before +# the DB-field check runs, so a batch can't compound the race the way a bare sleep-and-check did. # # ============================================================================================== # DESIGN PRINCIPLES @@ -355,6 +358,50 @@ if [[ "$MOVE_MODE" == true ]]; then break fi + # moveFiles=true flips rootFolderPath/episodeFileCount in the DB instantly, but the actual + # physical move is a separate async MoveSeries command that Sonarr drains one at a time + # internally — confirmed live: "Full House" showed episodeFileCount:192 at the new path via + # API while the real 75GB/192 files were still sitting at the old path, MoveSeries queued + # behind ~20 others. The DB-field check below cannot see that: poll the actual command to + # completion first, or a batch run can report every series "moved" while most are still + # mid-drain. + if [[ -n "$move_qs" ]]; then + move_cmd_id="" + for _ in 1 2 3 4 5; do + move_cmd_id=$(curl -sf --max-time 10 -H "X-Api-Key: $SONARR_API_KEY" \ + "${SONARR_URL}/api/v3/command" 2>/dev/null | \ + jq -r --argjson sid "$id" \ + '[.[] | select(.name == "MoveSeries" and .body.seriesId == $sid)] | sort_by(.id) | last | .id // empty' \ + 2>/dev/null) + [[ -n "$move_cmd_id" ]] && break + sleep 1 + done + + if [[ -z "$move_cmd_id" ]]; then + error " ✗ $title — could not locate the MoveSeries command — stopping (review before re-running)" + (( FAILED++ )) + break + fi + + info " → $title: MoveSeries command $move_cmd_id queued, waiting for completion..." + move_status="" move_polled=0 + while [[ "$move_polled" -lt "$SONARR_MOVE_POLL_TIMEOUT" ]]; do + move_status=$(curl -sf --max-time 10 -H "X-Api-Key: $SONARR_API_KEY" \ + "${SONARR_URL}/api/v3/command/${move_cmd_id}" 2>/dev/null | \ + jq -r '.status // empty' 2>/dev/null) + [[ "$move_status" == "completed" || "$move_status" == "failed" ]] && break + sleep 10 + (( move_polled += 10 )) + [[ $(( move_polled % 60 )) -eq 0 ]] && log " still moving $title... (${move_polled}s elapsed)" + done + + if [[ "$move_status" != "completed" ]]; then + error " ✗ $title — MoveSeries command $move_cmd_id ended as '${move_status:-timed out after ${SONARR_MOVE_POLL_TIMEOUT}s}' — stopping" + (( FAILED++ )) + break + fi + fi + sleep 3 # Never trust the PUT response alone — re-fetch and confirm the change actually landed. diff --git a/Deployment/master.conf.template b/Deployment/master.conf.template index 721c3ae..b24efd7 100644 --- a/Deployment/master.conf.template +++ b/Deployment/master.conf.template @@ -1180,6 +1180,10 @@ # protects against API returning partial data on a bad day SONARR_TRACKED_COUNT_FILE="$DATA_DIR/sonarr_tracked.count" SONARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan + SONARR_MOVE_POLL_TIMEOUT=3600 # seconds to wait for a single async MoveSeries command to + # reach "completed" — generous because a large series can sit + # queued behind other moves already in progress, not just its + # own copy time SONARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "ts" "wmv" "mov") SONARR_PROTECTED_PATTERNS=( # Subtitles