diff --git a/Kapitel 13/Tutorial.md b/Kapitel 13/Tutorial.md index 52f5222..96ed086 100644 --- a/Kapitel 13/Tutorial.md +++ b/Kapitel 13/Tutorial.md @@ -123,6 +123,7 @@ CLIPPER_IN=/srv/clipper/watch CLIPPER_OUT=/srv/clipper/out CLIPPER_TMP=/srv/clipper/temp CLIPPER_LOG=/srv/clipper/logs/clipper.log +CLIPPER_NC_REMOTE="nc:/" ``` Dateirechte setzen, damit `clipper` sie lesen darf: ```bash @@ -436,32 +437,79 @@ In diesem Schritt erstellen wir den eigentlichen Workflow in **n8n**. Er sorgt d ```bash #!/usr/bin/env bash set -euo pipefail + . /etc/clipper/clipper.env + ID="${1:?need VOD id}" URL="${2:-https://www.twitch.tv/videos/${ID}}" - TMP="/temp" - LOGDIR="/logs" + + TMP="${CLIPPER_TMP}" + OUT_BASE="${CLIPPER_OUT}/${ID}" + LOGDIR="${CLIPPER_LOG}/${ID}" CONF="/home/clipper/.config/rclone/rclone.conf" - DST="nc:/VODs/${ID}/" + + DST="${CLIPPER_NC_REMOTE}/VODs/${ID}/" + OUT="$TMP/${ID}.%(ext)s" FILE="$TMP/${ID}.mp4" TEMP="$TMP/${ID}.temp.mp4" PART="$TMP/${ID}.mp4.part" LOCK="$TMP/${ID}.lock" - LOG="$LOGDIR/${ID}.log" + mkdir -p "$TMP" "$LOGDIR" - log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; } + LOG="$LOGDIR/download.log" + log(){ echo "[$(date '+%F %T')] $*"; } + exec > >(tee -a "$LOG") 2>&1 + log "=== Start VOD $ID ===" log "URL: $URL" - if rclone lsf "$DST" --config "$CONF" >/dev/null 2>&1; then log "SKIP: $ID bereits in Nextcloud"; exit 0; fi - if [[ -e "$LOCK" ]]; then log "LOCK: $ID wird bereits verarbeitet"; exit 0; fi - trap 'rm -f "$LOCK"' EXIT; : > "$LOCK" - if [[ -s "$TEMP" && ! -s "$FILE" ]]; then mv -f "$TEMP" "$FILE"; fi - if [[ -s "$FILE" ]]; then rclone move "$FILE" "$DST" --config "$CONF" --create-empty-src-dirs -v; rm -f "$PART" "$TEMP" || true; exit 0; fi - yt-dlp -q --no-progress --retries 20 --fragment-retries 50 --retry-sleep 5 --socket-timeout 30 --hls-prefer-ffmpeg --remux-video mp4 -o "$OUT" "$URL" - [[ -s "$FILE" ]] || { [[ -s "$TEMP" ]] && mv -f "$TEMP" "$FILE"; } - if [[ ! -s "$FILE" ]]; then log "ERROR: Download fehlgeschlagen ($FILE fehlt/leer)"; exit 2; fi + log "DST: $DST" + + if rclone lsf "$DST" --config "$CONF" >/dev/null 2>&1; then + log "SKIP: $ID bereits in Nextcloud" + exit 0 + fi + + if [[ -e "$LOCK" ]]; then + log "LOCK: $ID wird bereits verarbeitet" + exit 0 + fi + trap 'rm -f "$LOCK"' EXIT + : > "$LOCK" + + # Resume + if [[ -s "$TEMP" && ! -s "$FILE" ]]; then + log "RESUME: $TEMP -> $FILE" + mv -f "$TEMP" "$FILE" + fi + + if [[ -s "$FILE" ]]; then + log "MOVE (resume): $FILE → $DST" rclone move "$FILE" "$DST" --config "$CONF" --create-empty-src-dirs -v rm -f "$PART" "$TEMP" || true + log "CLEANUP: $TMP" + rm -rf "${TMP:?}/"* + log "=== Done VOD $ID (resume path) ===" + exit 0 + fi + + # Download + Remux + yt-dlp -q --no-progress --retries 20 --fragment-retries 50 --retry-sleep 5 \ + --socket-timeout 30 --hls-prefer-ffmpeg --remux-video mp4 -o "$OUT" "$URL" + + [[ -s "$FILE" ]] || { [[ -s "$TEMP" ]] && mv -f "$TEMP" "$FILE"; } + + if [[ ! -s "$FILE" ]]; then + log "ERROR: Download fehlgeschlagen ($FILE fehlt/leer)" + exit 2 + fi + + log "MOVE: $FILE → $DST" + rclone move "$FILE" "$DST" --config "$CONF" --create-empty-src-dirs -v + rm -f "$PART" "$TEMP" || true + + log "CLEANUP: $TMP" + rm -rf "${TMP:?}/"* + log "=== Done VOD $ID ===" ``` Rechte setzen: @@ -479,83 +527,83 @@ Mit diesem Aufbau ist der **False-Pfad** fertig: Wenn die State-Datei fehlt oder --- - ### Schritt 8: True-Pfad – Check und ggf. Download +### Schritt 8: True-Pfad – Check und ggf. Download -> [!NOTE] -> Der **False-Pfad** wurde bereits im vorherigen Abschnitt vollständig erklärt. Viele Nodes überschneiden sich mit dem True-Pfad. Damit du nicht alles doppelt anlegen musst, verweisen wir hier auf die bereits erstellten und konfigurierten Nodes. + > [!NOTE] + > Der **False-Pfad** wurde bereits im vorherigen Abschnitt vollständig erklärt. Viele Nodes überschneiden sich mit dem True-Pfad. Damit du nicht alles doppelt anlegen musst, verweisen wir hier auf die bereits erstellten und konfigurierten Nodes. ---- + --- -**Verkabelung (Kurzüberblick):** -1) HTTP Request → -2) Split Out → -3) Set – vods in Array → -4) Split Out Array → -5) Merge → -6) Split In Batches → -7) SSH – Write State → -8) SSH – Download VOD + **Verkabelung (Kurzüberblick):** + 1) HTTP Request → + 2) Split Out → + 3) Set – vods in Array → + 4) Split Out Array → + 5) Merge → + 6) Split In Batches → + 7) SSH – Write State → + 8) SSH – Download VOD ---- + --- -### Node-Einstellungen (1:1 in n8n eintragen) + ### Node-Einstellungen (1:1 in n8n eintragen) -> [!IMPORTANT] -> Die folgenden Nodes haben wir im vorherigen Schritt (False-Pfad) bereits erstellt und konfiguriert. Wenn du dem Tutorial bis hierhin gefolgt bist, musst du an diesen Nodes nichts mehr verändern: -> - **HTTP Request – Get Videos** -> - **Split Out – data** -> - **Split In Batches** -> - **SSH – Write State** -> - **SSH – Download VOD** + > [!IMPORTANT] + > Die folgenden Nodes haben wir im vorherigen Schritt (False-Pfad) bereits erstellt und konfiguriert. Wenn du dem Tutorial bis hierhin gefolgt bist, musst du an diesen Nodes nichts mehr verändern: + > - **HTTP Request – Get Videos** + > - **Split Out – data** + > - **Split In Batches** + > - **SSH – Write State** + > - **SSH – Download VOD** ---- + --- -**Neu im True-Pfad:** + **Neu im True-Pfad:** -**3) Set – vods in Array** (Node-Name: `Set – vods in Array`) -- Node-Typ: Set -- Field: vods -- Expression: -```js -{{ (typeof $json.vods === 'string' ? $json.vods : String($json.vods)) -.split(',') -.map(s => s.trim()) -.filter(Boolean) }} -``` + **3) Set – vods in Array** (Node-Name: `Set – vods in Array`) + - Node-Typ: Set + - Field: vods + - Expression: + ```js + {{ (typeof $json.vods === 'string' ? $json.vods : String($json.vods)) + .split(',') + .map(s => s.trim()) + .filter(Boolean) }} + ``` -**4) Split Out – vods** (Node-Name: `Split Out – vods`) -- Node-Typ: Split Out -- Field to Split Out: vods + **4) Split Out – vods** (Node-Name: `Split Out – vods`) + - Node-Typ: Split Out + - Field to Split Out: vods -**5) Merge – Combine** (Node-Name: `Merge – Combine`) -- Node-Typ: Merge -- Mode: Combine -- Combine Mode: Matching Fields -- Fields To Match Have Different Names: ON -- Field 1: vods -- Field 2: data.id -- Output Type: Keep Non-Matches -- Output Data From: Input 2 -- Eingang 1: Split Out Array VODs -- Eingang 2: Split Out Twitch VODs + **5) Merge – Combine** (Node-Name: `Merge – Combine`) + - Node-Typ: Merge + - Mode: Combine + - Combine Mode: Matching Fields + - Fields To Match Have Different Names: ON + - Field 1: vods + - Field 2: data.id + - Output Type: Keep Non-Matches + - Output Data From: Input 2 + - Eingang 1: Split Out Array VODs + - Eingang 2: Split Out Twitch VODs ---- + --- -### Ergebnis -- Es werden nur **neue VODs** heruntergeladen und hochgeladen. -- Die State-Datei wird erweitert, ohne bestehende IDs zu überschreiben. -- Logs bleiben konsistent, Doppel-Downloads werden vermieden. -- Jeder Node ist eindeutig benannt, was die Übersicht verbessert. \ No newline at end of file + ### Ergebnis + - Es werden nur **neue VODs** heruntergeladen und hochgeladen. + - Die State-Datei wird erweitert, ohne bestehende IDs zu überschreiben. + - Logs bleiben konsistent, Doppel-Downloads werden vermieden. + - Jeder Node ist eindeutig benannt, was die Übersicht verbessert. \ No newline at end of file