From 7bf47be10625c3d0bf1c9cfc676cacaf835a614e Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Fri, 17 Jul 2026 23:00:08 -0400 Subject: [PATCH] Poll MoveMovie command to completion instead of trusting DB fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Sonarr fix — same MoveMovieService one-at-a-time drain architecture, never confirmed live on the Radarr side but the DB-instant/move-async split is identical, so the same batch-verification race applies. --- Arrs_Stack/radarr_classification_scan.sh | 55 +++++++++++++++++++++--- Deployment/master.conf.template | 2 + 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Arrs_Stack/radarr_classification_scan.sh b/Arrs_Stack/radarr_classification_scan.sh index c2ee486..35d8626 100755 --- a/Arrs_Stack/radarr_classification_scan.sh +++ b/Arrs_Stack/radarr_classification_scan.sh @@ -343,11 +343,14 @@ fi # like Castlevania/Legend of Korra legitimately live in the anime root without matching the # anime signal — or on JUNK (those need removal from Radarr, not a file move). # -# One movie at a time, verified after each, matching the lesson learned doing this by hand -# for Sonarr earlier: a rapid-fire batch of moves raced Radarr's own file-move worker and -# left two series' files at an intermediate path while the API still reported success. A -# short sleep plus a real re-fetch-and-check after every single move catches that here -# before it can compound across dozens of movies. +# One movie at a time, verified after each. moveFiles=true flips the DB (rootFolderPath/ +# hasFile) instantly, but the physical move is a separate async MoveMovie command Radarr's +# own MoveMovieService drains one at a time internally — same architecture that raced on the +# Sonarr side (episodeFileCount reported at the new path via API while the real files were +# still sitting at the old one, MoveSeries command queued behind ~20 others). Never confirmed +# live on the Radarr side, but the same DB-write-is-instant/move-is-async split applies, so +# each move here polls its own MoveMovie command to "completed" before the DB-field check +# runs, mirroring the Sonarr fix. if [[ "$MOVE_MODE" == true ]]; then echo "" echo "━━━ $ICON_SYNC Move Mode — Forward Misplacements ━━━" @@ -451,6 +454,48 @@ if [[ "$MOVE_MODE" == true ]]; then break fi + # moveFiles=true flips rootFolderPath/hasFile in the DB instantly, but the actual + # physical move is a separate async MoveMovie command that Radarr's MoveMovieService + # drains one at a time internally — mirrors the confirmed Sonarr race (see header + # comment above Move Mode). Poll the actual command to completion before trusting the + # DB-field check below. + 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: $RADARR_API_KEY" \ + "${RADARR_URL}/api/v3/command" 2>/dev/null | \ + jq -r --argjson mid "$id" \ + '[.[] | select(.name == "MoveMovie" and .body.movieId == $mid)] | 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 MoveMovie command — stopping (review before re-running)" + (( FAILED++ )) + break + fi + + info " → $title: MoveMovie command $move_cmd_id queued, waiting for completion..." + move_status="" move_polled=0 + while [[ "$move_polled" -lt "$RADARR_MOVE_POLL_TIMEOUT" ]]; do + move_status=$(curl -sf --max-time 10 -H "X-Api-Key: $RADARR_API_KEY" \ + "${RADARR_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 — MoveMovie command $move_cmd_id ended as '${move_status:-timed out after ${RADARR_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 b24efd7..8ec8e7f 100644 --- a/Deployment/master.conf.template +++ b/Deployment/master.conf.template @@ -1209,6 +1209,8 @@ # protects against API returning partial data on a bad day RADARR_TRACKED_COUNT_FILE="$DATA_DIR/radarr_tracked.count" RADARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan + RADARR_MOVE_POLL_TIMEOUT=3600 # seconds to wait for a single async MoveMovie command to + # reach "completed" — mirrors SONARR_MOVE_POLL_TIMEOUT RADARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "wmv" "mov") RADARR_PROTECTED_PATTERNS=( # Subtitles