Poll MoveMovie command to completion instead of trusting DB fields

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.
This commit is contained in:
Gmer4Lfe
2026-07-17 23:00:08 -04:00
parent 4f0cd1083a
commit 7bf47be106
2 changed files with 52 additions and 5 deletions
+50 -5
View File
@@ -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.