diff --git a/Arrs_Stack/radarr_classification_scan.sh b/Arrs_Stack/radarr_classification_scan.sh index b9739ae..f7855bd 100755 --- a/Arrs_Stack/radarr_classification_scan.sh +++ b/Arrs_Stack/radarr_classification_scan.sh @@ -100,6 +100,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" +# --move is a script-local flag, not one parse_args recognizes — check the raw args before +# they get filtered into PARSED_ARGS. +MOVE_MODE=false +for _arg in "$@"; do + [[ "$_arg" == "--move" ]] && MOVE_MODE=true +done +unset _arg + parse_args "$@" # ============================================================================================== @@ -136,6 +144,7 @@ if [[ "$SHOW_STATUS" == true ]]; then echo "$ICON_GEAR Anime studios: ${#RADARR_ANIME_STUDIOS[@]} curated" echo "$ICON_GEAR Kids studios: ${#RADARR_KIDS_STUDIOS[@]} curated" echo "$ICON_GEAR Junk min votes: ${RADARR_JUNK_MIN_VOTES:-15}" + echo "$ICON_GEAR Move mode: $MOVE_MODE" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi @@ -237,4 +246,112 @@ echo "$ICON_PROTECTED Bad metadata (junk): $JUNK_COUNT (hasFile=false, no imdb echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [[ "$ENABLE_LOGGING" != true ]] && echo " (run with --log for the per-title list)" +# ============================================================================================== +# ━━━ Move Mode ━━━ +# ============================================================================================== +# Acts only on FORWARD misplacements (clear-cut: classified anime/kids, sitting in the wrong +# root) and never on REVERSE leaks (those are judgment calls — deliberate style placements +# like Castlevania/Legend of Korra live there, so they're report-only) or JUNK (those need +# removal from Radarr, not a file move — nothing to move for a hasFile=false entry). +# +# 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. +if [[ "$MOVE_MODE" == true ]]; then + echo "" + echo "━━━ $ICON_SYNC Move Mode — Forward Misplacements ━━━" + + acquire_lock "wait" + trap "_release_all_locks" EXIT + + build_arr_path_map "RADARR" + + MOVE_TARGETS=$(echo "$RESULTS" | jq -c '[.[] | select((.forward_anime_miss or .forward_kids_miss) and (.is_junk | not))]') + MOVE_COUNT=$(echo "$MOVE_TARGETS" | jq 'length') + + if [[ "$MOVE_COUNT" -eq 0 ]]; then + info "Nothing to move" + exit 0 + fi + + warn "About to move $MOVE_COUNT movies — one at a time, verifying after each" + + MOVED=0 + FAILED=0 + + while IFS= read -r item; do + id=$(echo "$item" | jq -r '.id') + title=$(echo "$item" | jq -r '.title') + is_anime_flag=$(echo "$item" | jq -r '.is_anime') + if [[ "$is_anime_flag" == "true" ]]; then + target_root="$RADARR_ANIME_ROOT" + else + target_root="$RADARR_KIDS_ROOT" + fi + + # RESULTS only carries the reduced report fields — Radarr's PUT expects the complete + # resource representation, so fetch a fresh full movie record to modify and send back. + full_movie=$(arr_api "$RADARR_URL" "$RADARR_API_KEY" "v3" "movie/$id" "Radarr") + if [[ -z "$full_movie" ]]; then + error " ✗ $title — could not fetch full movie record, skipping" + (( FAILED++ )) + continue + fi + + old_path=$(echo "$full_movie" | jq -r '.path') + folder_name="${old_path##*/}" + + # A literal "/" in the folder name would build a broken nested directory instead of + # moving to one clean folder — bit us once already doing this by hand for Sonarr. + if [[ "$folder_name" == *"/"* ]]; then + error " ✗ $title — folder name contains '/', skipping (needs manual handling)" + (( FAILED++ )) + continue + fi + + new_path="${target_root}/${folder_name}" + info " → $title: $old_path → $new_path" + + updated_movie=$(echo "$full_movie" | jq --arg root "$target_root" --arg path "$new_path" \ + '.rootFolderPath = $root | .path = $path') + + http_code=$(curl -sf -o /dev/null -w "%{http_code}" -X PUT \ + --max-time 30 \ + -H "X-Api-Key: $RADARR_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$updated_movie" \ + "${RADARR_URL}/api/v3/movie/${id}?moveFiles=true" 2>/dev/null) + + if [[ "$http_code" != "200" && "$http_code" != "202" ]]; then + error " ✗ $title — API returned HTTP $http_code — stopping (review before re-running)" + (( FAILED++ )) + break + fi + + sleep 3 + + # Never trust the PUT response alone — re-fetch and confirm the change actually landed. + verify_movie=$(arr_api "$RADARR_URL" "$RADARR_API_KEY" "v3" "movie/$id" "Radarr") + verify_root=$(echo "$verify_movie" | jq -r '.rootFolderPath') + verify_hasfile=$(echo "$verify_movie" | jq -r '.hasFile') + + if [[ "$verify_root" == "$target_root" ]] && [[ "$verify_hasfile" == "true" ]]; then + echo " $ICON_SUCCESS $title — moved and verified" + (( MOVED++ )) + else + error " ✗ $title — verification failed (root: $verify_root, hasFile: $verify_hasfile) — stopping" + (( FAILED++ )) + break + fi + done < <(echo "$MOVE_TARGETS" | jq -c '.[]') + + echo "" + echo "━━━━━ $ICON_SUMMARY MOVE SUMMARY ━━━━━" + echo "$ICON_SUCCESS Moved: $MOVED" + echo "$ICON_ERROR Failed: $FAILED" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +fi + exit 0