movetdarr v4: flock-Serialisierung gegen n8n-staticData-Race bei parallelen SAB-Auftraegen

This commit is contained in:
sascha 2026-09-05 18:48:42 +02:00
parent 71247af6b6
commit e8a36502e9

View file

@ -1,7 +1,9 @@
#!/bin/bash #!/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, # Categories: serien4k->Sonarr UHD, serien->Sonarr FHD,
# video4k->Radarr UHD, video->Radarr 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" DIR="$1"
NZB_NAME="$2" NZB_NAME="$2"
@ -15,6 +17,7 @@ SOURCE_ROOT="${SOURCE_ROOT:-/usenet/complete}"
DEST_ROOT="${DEST_ROOT:-/tdarr/complete}" DEST_ROOT="${DEST_ROOT:-/tdarr/complete}"
POLL_SECONDS="${POLL_SECONDS:-60}" POLL_SECONDS="${POLL_SECONDS:-60}"
MAX_POLLS="${MAX_POLLS:-1440}" # 24 hours 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"; } log() { echo "$(date '+%d.%m.%Y %H:%M:%S'): $1" >> "$LOGFILE"; }
@ -39,7 +42,7 @@ notify_failure() {
handoff "$payload" >/dev/null 2>&1 || true 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 if [ "$STATUS" != "0" ]; then
log "Download fehlgeschlagen, ueberspringe" log "Download fehlgeschlagen, ueberspringe"
@ -74,69 +77,87 @@ if [ -z "$EXPECTED_FILES" ] || [ "$EXPECTED_FILES" -lt 1 ] || [ "$EXPECTED_FILES
exit 1 exit 1
fi fi
# Lease registrieren, solange der SAB-Job und sein Quellpfad noch sichtbar sind. # Lease registrieren und Move+Bestaetigung unter exclusivem Lock, damit parallele
START_PAYLOAD="$(json_payload action start category "$CATEGORY" directory "$DIR" release "$NZB_NAME" cleanName "$CLEAN_NAME" expectedFiles "$EXPECTED_FILES")" # SAB-Auftraege sich nicht gegenseitig den n8n-staticData-Job zerstoeren (v4).
START_RESPONSE="$(handoff "$START_PAYLOAD" 2>>"$LOGFILE")" || { register_lease() {
log "FEHLER: n8n-Handoff konnte nicht registriert werden - kein Move" START_PAYLOAD="$(json_payload action start category "$CATEGORY" directory "$DIR" release "$NZB_NAME" cleanName "$CLEAN_NAME" expectedFiles "$EXPECTED_FILES")"
exit 1 START_RESPONSE="$(handoff "$START_PAYLOAD" 2>>"$LOGFILE")" || {
} log "FEHLER: n8n-Handoff konnte nicht registriert werden - kein Move"
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"
exit 1 exit 1
} }
fi JOB_ID="$(printf '%s' "$START_RESPONSE" | json_field jobId 2>>"$LOGFILE")"
START_OK="$(printf '%s' "$START_RESPONSE" | json_field ok 2>>"$LOGFILE")"
if ! mv -- "$DIR" "$DEST/"; then if [ "$START_OK" != "True" ] && [ "$START_OK" != "true" ] || [ -z "$JOB_ID" ]; then
log "FEHLER beim Verschieben nach $TARGET (Quelle bleibt: $DIR)" log "FEHLER: ungueltige Handoff-Antwort - kein Move"
notify_failure "move_failed" exit 1
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 fi
log "Handoff-Move-Bestaetigung Versuch $attempt/5 fehlgeschlagen" log "Handoff registriert: Job=$JOB_ID"
sleep 15 export JOB_ID
done
if [ "$MOVED_OK" != "true" ]; then DEST="${DEST_ROOT:?DEST_ROOT muss gesetzt sein}/${CATEGORY}"
log "FEHLER: Move konnte n8n nicht bestaetigt werden; Datei bleibt fuer Recovery in $TARGET" BASENAME="$(basename "$DIR")"
notify_failure "move_confirmation_failed" TARGET="$DEST/$BASENAME"
exit 1 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 fi
# Ab hier: Status-Polling ohne Lock (lang laufend, stoert keine andere Registrierung).
STATUS_PAYLOAD="$(json_payload action status jobId "$JOB_ID")" STATUS_PAYLOAD="$(json_payload action status jobId "$JOB_ID")"
for poll in $(seq 1 "$MAX_POLLS"); do for poll in $(seq 1 "$MAX_POLLS"); do
if RESPONSE="$(handoff "$STATUS_PAYLOAD" 2>>"$LOGFILE")"; then if RESPONSE="$(handoff "$STATUS_PAYLOAD" 2>>"$LOGFILE")"; then