Die Kategorie-Whitelist kannte nur serien4k|serien|video4k|video. Die
englischen Arr-Instanzen sonarrEN (Port 8991, Root /data/FHD/serienen) und
radarrEN (Port 7880, Root /data/FHD/videoen) nutzen aber serienen/videoen.
Folge: Downloads dieser Kategorien wurden mit "nicht unterstuetzte Kategorie"
abgewiesen und blieben in /usenet/complete liegen, ohne je in den
FileFlows/Arr-Flow zu gelangen. Gefunden: Mutiny.2026 x2 (08./09.09.2026,
zusammen ~21 GB), beide Filme in radarrEN weiter monitored/hasFile=false.
/tdarr/complete/{serienen,videoen} existieren bereits; die FileFlows-Library
scannt /tdarr/complete rekursiv, es braucht also nur die Whitelist.
2 neue Testfaelle (serienen + videoen), test_movetdarr.py und
test_movetdarr_parallel.py gruen.
206 lines
7.2 KiB
Bash
206 lines
7.2 KiB
Bash
#!/bin/bash
|
|
# movetdarr.sh v5 — SABnzbd -> FileFlows with durable n8n handoff lease
|
|
# Categories: serien4k->Sonarr UHD, serien->Sonarr FHD,
|
|
# video4k->Radarr UHD, video->Radarr FHD.
|
|
# v5: BusyBox-kompatibler FD-Lock auf lokalem /config-Dateisystem.
|
|
# Der alte Lock unter /usenet lag auf NFS und konnte dauerhaft fehlschlagen.
|
|
|
|
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:-/config/.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 v5 - Kategorie=$CATEGORY Status=$STATUS Dir=$DIR"
|
|
|
|
if [ "$STATUS" != "0" ]; then
|
|
log "Download fehlgeschlagen, ueberspringe"
|
|
exit 1
|
|
fi
|
|
|
|
case "$CATEGORY" in
|
|
serien4k|serien|serienen|video4k|video|videoen) ;;
|
|
*)
|
|
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: FD öffnen und -n mit Retry-Loop verwenden.
|
|
# Der FD bleibt bis nach start -> mv -> moved offen und wird vor dem
|
|
# lang laufenden Status-Polling ausdrücklich entsperrt.
|
|
exec 9>"$LEASE_LOCK" || {
|
|
log "FEHLER: Lease-Lock-Datei konnte nicht geoeffnet werden - kein Move"
|
|
exit 1
|
|
}
|
|
LEASE_ACQUIRED=false
|
|
for i in $(seq 1 900); do
|
|
if flock -n 9 2>/dev/null; then
|
|
LEASE_ACQUIRED=true
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ "$LEASE_ACQUIRED" != "true" ]; then
|
|
log "FEHLER: Lease-Lock nach 15 Minuten nicht erhalten - kein Move"
|
|
exec 9>&-
|
|
exit 1
|
|
fi
|
|
register_lease
|
|
flock -u 9
|
|
exec 9>&-
|
|
else
|
|
log "FEHLER: flock fehlt - kein sicherer Handoff und kein Move"
|
|
exit 1
|
|
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
|