#!/bin/bash # 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" CLEAN_NAME="$3" CATEGORY="$5" STATUS="$7" LOGFILE="${LOGFILE:-/usenet/scripts/postprocess.log}" HANDOFF_URL="${HANDOFF_URL:-http://10.5.85.2:8888/media/handoff}" 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"; } json_payload() { python3 -c 'import json,sys; print(json.dumps(dict(zip(sys.argv[1::2],sys.argv[2::2]))))' "$@" } handoff() { curl --silent --show-error --fail-with-body \ --connect-timeout 5 --max-time 20 \ -H 'Content-Type: application/json' \ --data "$1" "$HANDOFF_URL" } json_field() { python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1], ""))' "$1" } notify_failure() { [ -n "${JOB_ID:-}" ] || return 0 payload="$(json_payload action fail jobId "$JOB_ID" reason "$1")" handoff "$payload" >/dev/null 2>&1 || true } log "Start v4 - Kategorie=$CATEGORY Status=$STATUS Dir=$DIR" if [ "$STATUS" != "0" ]; then log "Download fehlgeschlagen, ueberspringe" exit 1 fi case "$CATEGORY" in serien4k|serien|video4k|video) ;; *) log "FEHLER: nicht unterstuetzte Kategorie '$CATEGORY' - Quelle bleibt unangetastet" exit 1 ;; esac EXPECTED_PREFIX="${SOURCE_ROOT}/${CATEGORY}/" case "$DIR/" in "$EXPECTED_PREFIX"*) ;; *) log "FEHLER: Quellpfad passt nicht zur Kategorie - Quelle bleibt unangetastet" exit 1 ;; esac if [ ! -d "$DIR" ]; then log "Quellverzeichnis existiert nicht: $DIR" exit 1 fi EXPECTED_FILES="$(find "$DIR" -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) | wc -l | tr -d ' ')" if [ -z "$EXPECTED_FILES" ] || [ "$EXPECTED_FILES" -lt 1 ] || [ "$EXPECTED_FILES" -gt 100 ]; then log "FEHLER: ungueltige Anzahl Videodateien: ${EXPECTED_FILES:-0} - kein Move" exit 1 fi # 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 } 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" 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 # BusyBox flock hat kein -w (timeout) → -n (non-blocking) mit Retry-Loop LEASE_ACQUIRED=false for i in $(seq 1 900); do if flock -n "$LEASE_LOCK" -c true 2>/dev/null; then LEASE_ACQUIRED=true break fi sleep 1 done if [ "$LEASE_ACQUIRED" != "true" ]; then log "WARNUNG: Lease-Lock nach 15 Minuten nicht erhalten; fahre ohne Lock fort" fi register_lease 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 STATE="$(printf '%s' "$RESPONSE" | json_field state 2>>"$LOGFILE")" case "$STATE" in success) log "Arr-Import verifiziert: Job=$JOB_ID - SAB darf jetzt abschliessen/aufraeumen" exit 0 ;; failed|timeout) REASON="$(printf '%s' "$RESPONSE" | json_field reason 2>>"$LOGFILE")" log "FEHLER: Handoff Job=$JOB_ID State=$STATE Reason=$REASON" exit 1 ;; registered|processing) if [ $((poll % 10)) -eq 0 ]; then log "Warte auf FileFlows/Arr-Import: Job=$JOB_ID State=$STATE Poll=$poll/$MAX_POLLS" fi ;; *) log "WARNUNG: unbekannter Handoff-State '$STATE' fuer Job=$JOB_ID" ;; esac else log "WARNUNG: Handoff-Status temporaer nicht erreichbar; Job=$JOB_ID Poll=$poll/$MAX_POLLS" fi sleep "$POLL_SECONDS" done log "FEHLER: lokaler 24h-Timeout fuer Job=$JOB_ID" notify_failure "local_poll_timeout" exit 1