From e8a36502e93d8dbf0e8c7c229ce24120d6c30373 Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 5 Sep 2026 18:48:42 +0200 Subject: [PATCH] movetdarr v4: flock-Serialisierung gegen n8n-staticData-Race bei parallelen SAB-Auftraegen --- usenet-scripts/movetdarr.sh | 141 +++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 60 deletions(-) diff --git a/usenet-scripts/movetdarr.sh b/usenet-scripts/movetdarr.sh index 8d209ae..8671483 100644 --- a/usenet-scripts/movetdarr.sh +++ b/usenet-scripts/movetdarr.sh @@ -1,7 +1,9 @@ #!/bin/bash -# movetdarr.sh v3 — SABnzbd -> FileFlows with durable n8n handoff lease +# movetdarr.sh v4 — SABnzbd -> FileFlows with durable n8n handoff lease # Categories: serien4k->Sonarr UHD, serien->Sonarr FHD, # video4k->Radarr UHD, video->Radarr FHD. +# v4: flock-Serialisierung des Lease-Blocks gegen n8n-staticData-Race +# bei parallelen SAB-Aufträgen (Last-Writer-Wins verschluckt Jobs). DIR="$1" NZB_NAME="$2" @@ -15,6 +17,7 @@ SOURCE_ROOT="${SOURCE_ROOT:-/usenet/complete}" DEST_ROOT="${DEST_ROOT:-/tdarr/complete}" POLL_SECONDS="${POLL_SECONDS:-60}" MAX_POLLS="${MAX_POLLS:-1440}" # 24 hours +LEASE_LOCK="${LEASE_LOCK:-/usenet/scripts/.movetdarr.lease.lock}" log() { echo "$(date '+%d.%m.%Y %H:%M:%S'): $1" >> "$LOGFILE"; } @@ -39,7 +42,7 @@ notify_failure() { handoff "$payload" >/dev/null 2>&1 || true } -log "Start v3 - Kategorie=$CATEGORY Status=$STATUS Dir=$DIR" +log "Start v4 - Kategorie=$CATEGORY Status=$STATUS Dir=$DIR" if [ "$STATUS" != "0" ]; then log "Download fehlgeschlagen, ueberspringe" @@ -74,69 +77,87 @@ if [ -z "$EXPECTED_FILES" ] || [ "$EXPECTED_FILES" -lt 1 ] || [ "$EXPECTED_FILES exit 1 fi -# Lease registrieren, solange der SAB-Job und sein Quellpfad noch sichtbar sind. -START_PAYLOAD="$(json_payload action start category "$CATEGORY" directory "$DIR" release "$NZB_NAME" cleanName "$CLEAN_NAME" expectedFiles "$EXPECTED_FILES")" -START_RESPONSE="$(handoff "$START_PAYLOAD" 2>>"$LOGFILE")" || { - log "FEHLER: n8n-Handoff konnte nicht registriert werden - kein Move" - exit 1 -} -JOB_ID="$(printf '%s' "$START_RESPONSE" | json_field jobId 2>>"$LOGFILE")" -START_OK="$(printf '%s' "$START_RESPONSE" | json_field ok 2>>"$LOGFILE")" -if [ "$START_OK" != "True" ] && [ "$START_OK" != "true" ] || [ -z "$JOB_ID" ]; then - log "FEHLER: ungueltige Handoff-Antwort - kein Move" - exit 1 -fi -log "Handoff registriert: Job=$JOB_ID" - -DEST="${DEST_ROOT}/${CATEGORY}" -BASENAME="$(basename "$DIR")" -TARGET="$DEST/$BASENAME" -mkdir -p "$DEST" || { - log "FEHLER: Zielbasis konnte nicht erstellt werden: $DEST" - notify_failure "mkdir_failed" - exit 1 -} - -if [ -d "$TARGET" ]; then - if find "$TARGET" -maxdepth 1 -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) -print -quit | grep -q .; then - log "WARNUNG: Ziel $TARGET enthaelt bereits Video-Dateien - kein Ueberschreiben" - notify_failure "target_contains_video" - exit 2 - fi - log "Entferne FileFlows-Restverzeichnis ohne Video: $TARGET" - rm -rf -- "$TARGET" || { - log "FEHLER: Restverzeichnis konnte nicht entfernt werden" - notify_failure "stale_target_remove_failed" +# Lease registrieren und Move+Bestaetigung unter exclusivem Lock, damit parallele +# SAB-Auftraege sich nicht gegenseitig den n8n-staticData-Job zerstoeren (v4). +register_lease() { + START_PAYLOAD="$(json_payload action start category "$CATEGORY" directory "$DIR" release "$NZB_NAME" cleanName "$CLEAN_NAME" expectedFiles "$EXPECTED_FILES")" + START_RESPONSE="$(handoff "$START_PAYLOAD" 2>>"$LOGFILE")" || { + log "FEHLER: n8n-Handoff konnte nicht registriert werden - kein Move" exit 1 } -fi - -if ! mv -- "$DIR" "$DEST/"; then - log "FEHLER beim Verschieben nach $TARGET (Quelle bleibt: $DIR)" - notify_failure "move_failed" - exit 1 -fi -log "Erfolgreich verschoben nach $TARGET; SAB-Job bleibt bis Arr-Import aktiv" - -MOVED_PAYLOAD="$(json_payload action moved jobId "$JOB_ID")" -MOVED_OK=false -for attempt in 1 2 3 4 5; do - if MOVED_RESPONSE="$(handoff "$MOVED_PAYLOAD" 2>>"$LOGFILE")"; then - MOVED_STATE="$(printf '%s' "$MOVED_RESPONSE" | json_field state 2>>"$LOGFILE")" - if [ "$MOVED_STATE" = "processing" ] || [ "$MOVED_STATE" = "success" ]; then - MOVED_OK=true - break - fi + JOB_ID="$(printf '%s' "$START_RESPONSE" | json_field jobId 2>>"$LOGFILE")" + START_OK="$(printf '%s' "$START_RESPONSE" | json_field ok 2>>"$LOGFILE")" + if [ "$START_OK" != "True" ] && [ "$START_OK" != "true" ] || [ -z "$JOB_ID" ]; then + log "FEHLER: ungueltige Handoff-Antwort - kein Move" + exit 1 fi - log "Handoff-Move-Bestaetigung Versuch $attempt/5 fehlgeschlagen" - sleep 15 -done -if [ "$MOVED_OK" != "true" ]; then - log "FEHLER: Move konnte n8n nicht bestaetigt werden; Datei bleibt fuer Recovery in $TARGET" - notify_failure "move_confirmation_failed" - exit 1 + log "Handoff registriert: Job=$JOB_ID" + export JOB_ID + + DEST="${DEST_ROOT:?DEST_ROOT muss gesetzt sein}/${CATEGORY}" + BASENAME="$(basename "$DIR")" + TARGET="$DEST/$BASENAME" + mkdir -p "$DEST" || { + log "FEHLER: Zielbasis konnte nicht erstellt werden: $DEST" + notify_failure "mkdir_failed" + exit 1 + } + + if [ -d "$TARGET" ]; then + if find "$TARGET" -maxdepth 1 -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) -print -quit | grep -q .; then + log "WARNUNG: Ziel $TARGET enthaelt bereits Video-Dateien - kein Ueberschreiben" + notify_failure "target_contains_video" + exit 2 + fi + log "Entferne FileFlows-Restverzeichnis ohne Video: $TARGET" + rm -rf -- "$TARGET" || { + log "FEHLER: Restverzeichnis konnte nicht entfernt werden" + notify_failure "stale_target_remove_failed" + exit 1 + } + fi + + if ! mv -- "$DIR" "$DEST/"; then + log "FEHLER beim Verschieben nach $TARGET (Quelle bleibt: $DIR)" + notify_failure "move_failed" + exit 1 + fi + log "Erfolgreich verschoben nach $TARGET; SAB-Job bleibt bis Arr-Import aktiv" + + MOVED_PAYLOAD="$(json_payload action moved jobId "$JOB_ID")" + MOVED_OK=false + for attempt in 1 2 3 4 5; do + if MOVED_RESPONSE="$(handoff "$MOVED_PAYLOAD" 2>>"$LOGFILE")"; then + MOVED_STATE="$(printf '%s' "$MOVED_RESPONSE" | json_field state 2>>"$LOGFILE")" + if [ "$MOVED_STATE" = "processing" ] || [ "$MOVED_STATE" = "success" ]; then + MOVED_OK=true + break + fi + fi + log "Handoff-Move-Bestaetigung Versuch $attempt/5 fehlgeschlagen" + sleep 15 + done + if [ "$MOVED_OK" != "true" ]; then + log "FEHLER: Move konnte n8n nicht bestaetigt werden; Datei bleibt fuer Recovery in $TARGET" + notify_failure "move_confirmation_failed" + exit 1 + fi +} + +if command -v flock >/dev/null 2>&1; then + flock -w 900 "$LEASE_LOCK" -c 'set -e' || true # Lock-Datei anlegen + exec 9>"$LEASE_LOCK" + if ! flock -w 900 9; then + log "WARNUNG: Lease-Lock nach 15 Minuten nicht erhalten; fahre ohne Lock fort" + register_lease + else + register_lease + fi +else + register_lease fi +# Ab hier: Status-Polling ohne Lock (lang laufend, stoert keine andere Registrierung). STATUS_PAYLOAD="$(json_payload action status jobId "$JOB_ID")" for poll in $(seq 1 "$MAX_POLLS"); do if RESPONSE="$(handoff "$STATUS_PAYLOAD" 2>>"$LOGFILE")"; then