From 794156a0880bbd368ba4ea61144eb1bdf6e2fe32 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Tue, 15 Sep 2026 18:10:34 +0200 Subject: [PATCH] erster docker release --- config/stream-recorder.conf.dist | 62 - docs/befehle.md | 105 -- docs/beispiele.md | 157 --- docs/benachrichtigungen.md | 41 - docs/installation.md | 65 -- docs/jobs.md | 80 -- docs/konfiguration.md | 66 -- docs/plik-upload.md | 62 - docs/segment-splitting.md | 76 -- docs/zeitplanung.md | 80 -- install/install.sh | 104 -- install/uninstall.sh | 61 - jobs/templates/example-hls.job.dist | 49 - jobs/templates/example-mp3.job.dist | 50 - jobs/templates/example-rtmp.job.dist | 74 -- stream-recorder.sh | 1621 -------------------------- 16 files changed, 2753 deletions(-) delete mode 100644 config/stream-recorder.conf.dist delete mode 100644 docs/befehle.md delete mode 100644 docs/beispiele.md delete mode 100644 docs/benachrichtigungen.md delete mode 100644 docs/installation.md delete mode 100644 docs/jobs.md delete mode 100644 docs/konfiguration.md delete mode 100644 docs/plik-upload.md delete mode 100644 docs/segment-splitting.md delete mode 100644 docs/zeitplanung.md delete mode 100644 install/install.sh delete mode 100644 install/uninstall.sh delete mode 100644 jobs/templates/example-hls.job.dist delete mode 100644 jobs/templates/example-mp3.job.dist delete mode 100644 jobs/templates/example-rtmp.job.dist delete mode 100644 stream-recorder.sh diff --git a/config/stream-recorder.conf.dist b/config/stream-recorder.conf.dist deleted file mode 100644 index d1f41e3..0000000 --- a/config/stream-recorder.conf.dist +++ /dev/null @@ -1,62 +0,0 @@ -############################################### -# stream-recorder - Konfigurationsdatei -############################################### - -# Basis-Verzeichnis für Aufnahmen -# Pro Stream wird ein Unterordner angelegt -DOWNLOAD_PATH="/home/downloads/recordings" - -# Log-Verzeichnis -LOG_DIR="/var/log/stream-recorder" - -# Log-Level: DEBUG, INFO, WARN, ERROR -LOG_LEVEL="INFO" - -# PID-Verzeichnis für laufende Aufnahmen -PID_DIR="/tmp/stream-recorder" - -# Persistentes Status-Verzeichnis für geplante Läufe -STATE_DIR="/var/lib/stream-recorder" - -# Anzahl Wiederholungsversuche bei Verbindungsabbruch -# 0 = unbegrenzt (empfohlen für Langzeitaufnahmen) -MAX_RETRIES=5 - -# Wartezeit zwischen Wiederholungsversuchen (Sekunden) -RETRY_DELAY=5 - -############################################### -# NTFY-Benachrichtigungen (optional) -# https://ntfy.sh oder selbst gehostet -############################################### - -# NTFY Server-URL inkl. Topic -# Beispiel: https://ntfy.sh/mein-stream-topic -# Beispiel: https://ntfy.example.com/recordings -# Leer = deaktiviert -NTFY_URL="" - -# NTFY Access-Token (optional, nur bei geschützten Topics) -NTFY_TOKEN="" - -# Ereignisse, bei denen benachrichtigt wird (kommasepariert) -# Möglich: start, stop, error, upload -# Beispiel: "start,stop,error,upload" für alle Ereignisse -NTFY_EVENTS="error" - -############################################### -# Plik-Upload (optional) -# https://github.com/root-gg/plik -############################################### - -# Plik Server-URL -# Beispiel: https://plik.example.com -# Leer = deaktiviert (kann pro Job aktiviert werden) -PLIK_URL="" - -# Plik API-Key (optional, nur bei geschützten Servern) -PLIK_API_KEY="" - -# Gültigkeitsdauer des Uploads -# Beispiel: 30d (30 Tage), 24h (24 Stunden), 0 (unbegrenzt) -PLIK_TTL="30d" diff --git a/docs/befehle.md b/docs/befehle.md deleted file mode 100644 index 5fb3118..0000000 --- a/docs/befehle.md +++ /dev/null @@ -1,105 +0,0 @@ -# Befehle - -## Übersicht - -| Befehl | Beschreibung | -|----------------------|-------------------------------------------------| -| `record [job]` | Einzelnen Stream aufzeichnen (ohne Angabe: Auswahl) | -| `record-all` | Alle Jobs gleichzeitig aufzeichnen | -| `create` | Neuen Job interaktiv erstellen | -| `quick` | Schnellaufnahme (URL direkt eingeben) | -| `upload ` | Vorhandene Datei nach Plik hochladen | -| `stop [name]` | Aufnahme stoppen (ohne Angabe: Auswahl) | -| `stop-all` | Alle Aufnahmen stoppen | -| `list` | Jobs und deren Zeitpläne anzeigen | -| `status` | Laufende Aufnahmen mit Details anzeigen | -| `scheduler` | Zeitplanung prüfen und Jobs starten/stoppen | -| `logs` | Log-Ausgabe live verfolgen | - -## Optionen - -| Option | Beschreibung | -|--------------------------|---------------------------------------| -| `-c, --config ` | Alternative Konfigurationsdatei | -| `-V, --verbose` | Ausführliche Ausgabe | -| `--foreground` | Aufnahme im Vordergrund ausführen | -| `-h, --help` | Hilfe anzeigen | -| `-v, --version` | Version anzeigen | - -## Details - -### record - -Startet eine Aufnahme im Hintergrund (Standard) oder Vordergrund (`--foreground`). Ohne Job-Name wird eine interaktive Auswahl aller verfügbaren Jobs angezeigt. - -```bash -./stream-recorder.sh record # Job aus Liste wählen -./stream-recorder.sh record mein-stream # Direkt starten -./stream-recorder.sh --foreground record mein-stream -./stream-recorder.sh -V record mein-stream -``` - -### create - -Erstellt interaktiv eine neue Job-Datei. Fragt nach URL, Name, Typ, Segment-Splitting, Zeitplan und Plik-Upload. - -```bash -./stream-recorder.sh create -``` - -### quick - -Schnellaufnahme ohne Job-Datei. Fragt nach URL, Name, Typ und Dauer. - -```bash -./stream-recorder.sh quick -``` - -### stop / stop-all - -Stoppt eine laufende Aufnahme. Ohne Job-Name wird eine Auswahl angezeigt. Wenn Plik aktiviert ist, wird die Teilaufnahme hochgeladen. Bei Segment-Splitting werden alle verbleibenden Segmente hochgeladen. - -```bash -./stream-recorder.sh stop # Job aus Liste wählen -./stream-recorder.sh stop mein-stream # Direkt stoppen -./stream-recorder.sh stop-all # Alle stoppen -``` - -### status - -Zeigt detaillierte Informationen zu laufenden Aufnahmen: -- Stream-Name und Typ -- PID und Startmodus (manuell/geplant) -- Startzeit und bisherige Laufzeit -- Dateigröße bzw. Anzahl der Segmente und Gesamtgröße -- Scheduler-Status (systemd-Timer) - -```bash -./stream-recorder.sh status -``` - -### list - -Zeigt alle konfigurierten Jobs mit Status, Zeitplan, Segment-Einstellungen und Plik-Konfiguration. - -```bash -./stream-recorder.sh list -``` - -### logs - -Verfolgt die Log-Ausgabe in Echtzeit (`tail -f`). - -```bash -./stream-recorder.sh logs -``` - -## Logging - -Logs werden nach `LOG_DIR` geschrieben (Standard: `/var/log/stream-recorder/`): - -| Datei | Inhalt | -|----------------------------|---------------------------------| -| `stream-recorder.log` | Alle Operationen des Scripts | -| `ffmpeg_.log` | ffmpeg-Ausgabe pro Job | -| `nohup_.log` | Ausgabe im Hintergrund-Modus | diff --git a/docs/beispiele.md b/docs/beispiele.md deleted file mode 100644 index 1bd56a6..0000000 --- a/docs/beispiele.md +++ /dev/null @@ -1,157 +0,0 @@ -# Praxisbeispiele - -## RTMP-Livestream täglich aufzeichnen - -Tägliche Aufnahme eines Twitch-/YouTube-/RTMP-Streams von 20:00 bis 22:00 Uhr mit Plik-Upload: - -```bash -# jobs/abend-livestream.job -STREAM_NAME="Abend-Livestream" -STREAM_URL="rtmp://example.com/live/stream-key" -STREAM_TYPE="auto" -OUTPUT_FORMAT="" -MAX_DURATION="" -EXTRA_FFMPEG_ARGS="" - -SEGMENT_DURATION="" - -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="*" -SCHEDULE_START="20:00" -SCHEDULE_STOP="22:00" - -PLIK_ENABLED=true -DELETE_AFTER_UPLOAD=false -``` - -## Radio-Mitschnitt werktags - -Werktags um 18:00 Uhr eine Stunde Radio mitschneiden: - -```bash -# jobs/radio-abends.job -STREAM_NAME="Radio-Abendshow" -STREAM_URL="https://example.com/stream.mp3" -STREAM_TYPE="auto" -OUTPUT_FORMAT="" -MAX_DURATION="1h" -EXTRA_FFMPEG_ARGS="" - -SEGMENT_DURATION="" - -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="Mo-Fr" -SCHEDULE_START="18:00" -SCHEDULE_STOP="" - -PLIK_ENABLED=false -``` - -## 24/7-Stream mit stündlichem Upload - -Ein Überwachungsstream, der dauerhaft läuft und stündlich Segmente zu Plik hochlädt. Lokale Dateien werden nach dem Upload gelöscht: - -```bash -# jobs/ueberwachung.job -STREAM_NAME="Kamera-Front" -STREAM_URL="rtmp://192.168.1.100/live/cam1" -STREAM_TYPE="auto" -OUTPUT_FORMAT="mp4" -MAX_DURATION="" -EXTRA_FFMPEG_ARGS="" - -SEGMENT_DURATION="1h" - -SCHEDULE_ENABLED=false - -PLIK_ENABLED=true -DELETE_AFTER_UPLOAD=true -``` - -Starten mit: - -```bash -./stream-recorder.sh record ueberwachung -``` - -## Podcast-/HLS-Stream in 2-Stunden-Blöcken - -Ein langer HLS-Livestream (z.B. Konferenz) in 2-Stunden-Segmenten aufzeichnen, jedes Segment wird sofort hochgeladen: - -```bash -# jobs/konferenz.job -STREAM_NAME="Dev-Konferenz-2026" -STREAM_URL="https://example.com/live/playlist.m3u8" -STREAM_TYPE="hls" -OUTPUT_FORMAT="mp4" -MAX_DURATION="10h" -EXTRA_FFMPEG_ARGS="" - -SEGMENT_DURATION="2h" - -SCHEDULE_ENABLED=false - -PLIK_ENABLED=true -DELETE_AFTER_UPLOAD=false -``` - -## Einmalige Aufnahme zu einem bestimmten Termin - -Ein Event am 15.09.2026 um 21:00 Uhr aufnehmen, 2 Stunden: - -```bash -# jobs/einmaliges-event.job -STREAM_NAME="Einmaliges-Event" -STREAM_URL="https://example.com/live/playlist.m3u8" -STREAM_TYPE="hls" -OUTPUT_FORMAT="mp4" -MAX_DURATION="2h" -EXTRA_FFMPEG_ARGS="" - -SEGMENT_DURATION="" - -SCHEDULE_ENABLED=true -SCHEDULE_ONCE=true -SCHEDULE_DATE="2026-09-15" -SCHEDULE_START="21:00" -SCHEDULE_STOP="" - -PLIK_ENABLED=true -DELETE_AFTER_UPLOAD=false -``` - -## Schnellaufnahme ohne Job-Datei - -Für spontane Aufnahmen ohne Konfiguration: - -```bash -./stream-recorder.sh quick -``` - -Das Script fragt interaktiv nach URL, Name, Typ und Dauer. - -## Typischer Workflow - -```bash -# 1. Konfiguration erstellen (einmalig) -cp config/stream-recorder.conf.dist config/stream-recorder.conf -nano config/stream-recorder.conf - -# 2. Scheduler installieren (einmalig) -sudo install/install.sh - -# 3. Job erstellen -./stream-recorder.sh create - -# 4. Jobs anzeigen -./stream-recorder.sh list - -# 5. Status prüfen -./stream-recorder.sh status - -# 6. Logs verfolgen -./stream-recorder.sh logs - -# 7. Aufnahme stoppen (falls nötig) -./stream-recorder.sh stop mein-stream -``` diff --git a/docs/benachrichtigungen.md b/docs/benachrichtigungen.md deleted file mode 100644 index 83e7597..0000000 --- a/docs/benachrichtigungen.md +++ /dev/null @@ -1,41 +0,0 @@ -# NTFY-Benachrichtigungen - -Push-Nachrichten via [ntfy](https://ntfy.sh) (selbst gehostet oder öffentlich) bei wichtigen Ereignissen. - -## Einrichtung - -In `config/stream-recorder.conf`: - -```bash -NTFY_URL="https://ntfy.sh/mein-stream-topic" -NTFY_TOKEN="" # Optional, nur bei geschützten Topics -NTFY_EVENTS="start,stop,error" # Kommasepariert -``` - -## Events - -| Event | Auslöser | Priorität | -|----------|-----------------------------------------------------|-----------| -| `start` | Aufnahme wurde gestartet | Normal | -| `stop` | Aufnahme beendet (regulär oder manuell) | Normal | -| `error` | Verbindung verloren oder max. Versuche erreicht | Hoch | -| `upload` | Datei auf Plik hochgeladen (mit Download-Link) | Normal | - -## Alle Events aktivieren - -```bash -NTFY_EVENTS="start,stop,error,upload" -``` - -## Nur Fehler (Standard) - -```bash -NTFY_EVENTS="error" -``` - -## Hinweise - -- Benachrichtigungen werden asynchron im Hintergrund gesendet -- Ein fehlgeschlagener Versand blockiert die Aufnahme nicht -- curl muss installiert sein (`sudo apt install curl`) -- Ohne gesetzte `NTFY_URL` sind Benachrichtigungen komplett deaktiviert diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index 2052f98..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,65 +0,0 @@ -# Installation - -## Voraussetzungen - -| Komponente | Anforderung | -|------------------|------------------------------------------| -| Betriebssystem | Linux (getestet unter Ubuntu/Debian) | -| Bash | Version 4.0 oder neuer | -| ffmpeg | Pflicht (`sudo apt install ffmpeg`) | -| curl | Optional, für NTFY und Plik-Upload | - -## Installation - -```bash -# Repository klonen -git clone https://git.techniverse.net/scriptos/stream-recorder.git /opt/stream-recorder -cd /opt/stream-recorder - -# Konfiguration aus Vorlage erstellen und anpassen -cp config/stream-recorder.conf.dist config/stream-recorder.conf -nano config/stream-recorder.conf - -# Scheduler installieren (systemd-Timer) -sudo install/install.sh -``` - -Das Install-Script: -- Macht das Hauptscript ausführbar -- Installiert fehlende Abhängigkeiten (ffmpeg) -- Erstellt die Konfigurationsdatei aus der Vorlage (falls nicht vorhanden) -- Richtet einen systemd-Timer ein, der jede Minute die Zeitpläne prüft - -## Updates - -Da die Konfiguration als Kopie (`stream-recorder.conf`) vorliegt und die Vorlage (`stream-recorder.conf.dist`) getrennt gepflegt wird, gehen eigene Einstellungen bei einem `git pull` nicht verloren. - -```bash -cd /opt/stream-recorder -git pull -``` - -Nach einem Update sollte man prüfen, ob in `stream-recorder.conf.dist` neue Parameter hinzugekommen sind, und diese ggf. in die eigene `stream-recorder.conf` übernehmen. - -## Deinstallation - -```bash -sudo install/uninstall.sh -``` - -Entfernt den systemd-Timer und stoppt optional alle laufenden Aufnahmen. Job-Dateien, Konfiguration und Aufnahmen bleiben erhalten. - -## Ohne systemd - -Das Script funktioniert auch ohne Scheduler. In dem Fall muss man Aufnahmen manuell starten: - -```bash -./stream-recorder.sh record mein-stream -``` - -Oder den Scheduler manuell per Cron aufrufen: - -```bash -# crontab -e -* * * * * /opt/stream-recorder/stream-recorder.sh scheduler -``` diff --git a/docs/jobs.md b/docs/jobs.md deleted file mode 100644 index a62c59f..0000000 --- a/docs/jobs.md +++ /dev/null @@ -1,80 +0,0 @@ -# Job-Dateien - -Jeder Stream wird durch eine `.job`-Datei im `jobs/`-Verzeichnis definiert. Vorlagen befinden sich unter `jobs/templates/`. - -## Job erstellen - -### Interaktiv (empfohlen) - -```bash -./stream-recorder.sh create -``` - -Das Script fragt Schritt für Schritt nach URL, Name, Typ, Segment-Splitting, Zeitplan und Plik-Upload und erstellt die Job-Datei automatisch. - -### Aus Vorlage - -```bash -# Vorlage kopieren -cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job - -# Anpassen -nano jobs/mein-stream.job -``` - -Verfügbare Vorlagen: - -| Vorlage | Beschreibung | -|----------------------------|-------------------------| -| `example-rtmp.job.dist` | RTMP/RTMPS-Stream | -| `example-hls.job.dist` | HLS/m3u8-Stream | -| `example-mp3.job.dist` | HTTP-Audio (MP3/Radio) | - -## Stream-Parameter - -| Parameter | Pflicht | Beschreibung | -|----------------------|---------|-----------------------------------------------------| -| `STREAM_NAME` | Ja | Name des Streams (wird für Dateinamen verwendet) | -| `STREAM_URL` | Ja | URL des Streams | -| `STREAM_TYPE` | Nein | `auto`, `rtmp`, `hls`, `mp3`, `aac`, `ogg`, `flac` | -| `OUTPUT_FORMAT` | Nein | Dateiendung (z.B. `mp4`, `mp3`) — sonst automatisch | -| `MAX_DURATION` | Nein | Aufnahmedauer (z.B. `1h`, `30m`, `1d12h`, `3600`) | -| `EXTRA_FFMPEG_ARGS` | Nein | Zusätzliche ffmpeg-Parameter | -| `SEGMENT_DURATION` | Nein | Segment-Splitting (z.B. `1h`, `2h`) — siehe [Segment-Splitting](segment-splitting.md) | -| `PLIK_ENABLED` | Nein | Plik-Upload aktivieren (`true`/`false`) | -| `DELETE_AFTER_UPLOAD` | Nein | Lokale Datei nach Upload löschen (`true`/`false`) | - -### Dauerformate - -Die Dauer kann in verschiedenen Formaten angegeben werden: - -| Format | Bedeutung | -|-----------|------------------| -| `3600` | 3600 Sekunden | -| `30m` | 30 Minuten | -| `1h` | 1 Stunde | -| `1h30m` | 90 Minuten | -| `48h` | 48 Stunden | -| `48h30m` | 48,5 Stunden | -| `2d` | 2 Tage | -| `1d12h` | 36 Stunden | - -Formate lassen sich frei kombinieren: `d` (Tage), `h` (Stunden), `m` (Minuten), `s` (Sekunden). - -## Job-Angabe beim Starten - -Der `record`-Befehl akzeptiert verschiedene Formate: - -```bash -./stream-recorder.sh record jobs/mein-stream.job # Vollständiger Pfad -./stream-recorder.sh record mein-stream.job # Nur Dateiname -./stream-recorder.sh record mein-stream # Nur Job-Name (ohne .job) -``` - -## Jobs anzeigen - -```bash -./stream-recorder.sh list -``` - -Zeigt alle Jobs mit Stream-Typ, URL, Status, Zeitplan, Segment-Einstellungen und Plik-Konfiguration. diff --git a/docs/konfiguration.md b/docs/konfiguration.md deleted file mode 100644 index dd3c1ec..0000000 --- a/docs/konfiguration.md +++ /dev/null @@ -1,66 +0,0 @@ -# Konfiguration - -Die Konfigurationsdatei liegt unter `config/stream-recorder.conf`. Sie wird aus der Vorlage `config/stream-recorder.conf.dist` erstellt: - -```bash -cp config/stream-recorder.conf.dist config/stream-recorder.conf -nano config/stream-recorder.conf -``` - -Die `.conf`-Datei wird von Git ignoriert und bleibt bei Updates erhalten. - -## Parameter - -### Basis-Einstellungen - -| Parameter | Standard | Beschreibung | -|-----------------|------------------------------|-------------------------------------------| -| `DOWNLOAD_PATH` | `/home/downloads/recordings` | Basis-Verzeichnis für Aufnahmen | -| `LOG_DIR` | `/var/log/stream-recorder` | Log-Verzeichnis | -| `LOG_LEVEL` | `INFO` | Log-Level: `DEBUG`, `INFO`, `WARN`, `ERROR` | -| `PID_DIR` | `/tmp/stream-recorder` | PID-Dateien für laufende Aufnahmen | -| `STATE_DIR` | `/var/lib/stream-recorder` | Status für bereits gestartete Zeitpläne | - -### Reconnect - -| Parameter | Standard | Beschreibung | -|---------------|----------|-------------------------------------------| -| `MAX_RETRIES` | `0` | Wiederholungsversuche (0 = unbegrenzt) | -| `RETRY_DELAY` | `5` | Wartezeit zwischen Versuchen (Sekunden) | - -Bei Verbindungsabbrüchen versucht das Script automatisch, die Verbindung wiederherzustellen. Bei jedem Reconnect wird eine neue Datei mit aktuellem Zeitstempel erzeugt. - -- `MAX_RETRIES=0` — Unbegrenzte Versuche (empfohlen für Langzeitaufnahmen) -- `MAX_RETRIES=5` — Maximal 5 Versuche, danach Abbruch mit Benachrichtigung - -### NTFY-Benachrichtigungen - -Push-Nachrichten via [ntfy](https://ntfy.sh) (selbst gehostet oder öffentlich). - -| Parameter | Standard | Beschreibung | -|---------------|----------|-------------------------------------------| -| `NTFY_URL` | (leer) | NTFY Server-URL inkl. Topic | -| `NTFY_TOKEN` | (leer) | NTFY Access-Token (optional) | -| `NTFY_EVENTS` | `error` | Benachrichtigungs-Events (kommasepariert) | - -Siehe [Benachrichtigungen](benachrichtigungen.md) für Details zu den Events. - -### Plik-Upload - -Fertige Aufnahmen auf einen [Plik](https://github.com/root-gg/plik)-Server hochladen. - -| Parameter | Standard | Beschreibung | -|----------------|----------|-------------------------------------------| -| `PLIK_URL` | (leer) | Plik Server-URL | -| `PLIK_API_KEY` | (leer) | Plik API-Key (optional) | -| `PLIK_TTL` | `30d` | Gültigkeitsdauer: `30d`, `24h`, Sekunden | - -Siehe [Plik-Upload](plik-upload.md) für die Einrichtung pro Job. - -## Alternative Konfigurationsdatei - -Eine andere Konfigurationsdatei kann per `-c` übergeben werden: - -```bash -./stream-recorder.sh -c /pfad/zur/config.conf record mein-stream -``` diff --git a/docs/plik-upload.md b/docs/plik-upload.md deleted file mode 100644 index 11db03e..0000000 --- a/docs/plik-upload.md +++ /dev/null @@ -1,62 +0,0 @@ -# Plik-Upload - -Fertige Aufnahmen können automatisch auf einen [Plik](https://github.com/root-gg/plik)-Server hochgeladen werden. Die Browser-URL zum Download wird im Log und optional per NTFY-Benachrichtigung ausgegeben. - -## Einrichtung - -### 1. Plik-Server konfigurieren - -In `config/stream-recorder.conf`: - -```bash -PLIK_URL="https://plik.example.com" -PLIK_API_KEY="" # Optional, nur bei geschützten Servern -PLIK_TTL="30d" # Gültigkeitsdauer: 30d, 24h, oder Sekunden -``` - -### 2. Pro Job aktivieren - -In der Job-Datei: - -```bash -PLIK_ENABLED=true -``` - -### 3. Optional: Datei nach Upload löschen - -```bash -DELETE_AFTER_UPLOAD=true -``` - -## Verhalten - -- Upload nach jeder regulär abgeschlossenen Aufnahme -- Upload nach manuellem Stop (`stop`-Befehl) — die Teilaufnahme wird hochgeladen -- Upload nach Verbindungsabbruch, wenn eine Teildatei vorhanden ist -- Browser-Link und Datei-URL werden ins Log geschrieben -- Optional wird der Link per NTFY gesendet (Event: `upload`) - -## Mit Segment-Splitting - -Bei aktiviertem [Segment-Splitting](segment-splitting.md) wird jedes Segment einzeln hochgeladen: - -- Fertige Segmente werden während der Aufnahme hochgeladen (alle 60s geprüft) -- Das letzte Segment wird nach Ende der Aufnahme hochgeladen -- `DELETE_AFTER_UPLOAD=true` löscht jedes Segment nach dem Upload - -## Manueller Upload - -Eine vorhandene Datei kann auch manuell hochgeladen werden: - -```bash -./stream-recorder.sh upload /pfad/zur/aufnahme.mp4 "Stream-Name" -``` - -## Download-Link per Push - -Um den Plik-Link automatisch als Push-Nachricht zu erhalten: - -```bash -# In config/stream-recorder.conf -NTFY_EVENTS="start,stop,error,upload" -``` diff --git a/docs/segment-splitting.md b/docs/segment-splitting.md deleted file mode 100644 index e8202b7..0000000 --- a/docs/segment-splitting.md +++ /dev/null @@ -1,76 +0,0 @@ -# Segment-Splitting - -Lange Aufnahmen (z.B. 8-Stunden-Streams) können automatisch in kleinere Dateien geteilt werden. Jedes Segment wird einzeln nach Plik hochgeladen, sobald es fertig ist. - -## Einrichtung - -In der Job-Datei `SEGMENT_DURATION` setzen: - -```bash -# Aufnahme alle 2 Stunden teilen -SEGMENT_DURATION="2h" - -# Plik-Upload aktivieren (empfohlen) -PLIK_ENABLED=true -``` - -## Dauerformate - -| Format | Bedeutung | -|----------|------------------| -| `30m` | Alle 30 Minuten | -| `1h` | Stündlich | -| `2h` | Alle 2 Stunden | -| `3600` | 3600 Sekunden | - -## So funktioniert's - -1. ffmpeg teilt die Aufnahme automatisch in Segmente der angegebenen Länge -2. Ein Hintergrund-Prozess prüft alle 60 Sekunden auf fertige Segmente -3. Fertige Segmente werden sofort nach Plik hochgeladen (falls aktiviert) -4. Beim Beenden (regulär oder per `stop`) wird das letzte Segment noch hochgeladen - -### Dateinamen - -Segmente erhalten fortlaufende Nummern: - -``` -Mein-Stream_20260907_200000_seg000.mp4 -Mein-Stream_20260907_200000_seg001.mp4 -Mein-Stream_20260907_200000_seg002.mp4 -``` - -## Kombination mit Plik - -Segment-Splitting ist besonders nützlich in Kombination mit Plik-Upload: - -```bash -SEGMENT_DURATION="1h" -PLIK_ENABLED=true -DELETE_AFTER_UPLOAD=true # Lokale Segmente nach Upload löschen -``` - -So wird jede Stunde ein Segment hochgeladen und lokal gelöscht. Das spart Speicherplatz bei langen Aufnahmen. - -## Kombination mit Zeitplanung - -Segment-Splitting und Zeitplanung lassen sich kombinieren: - -```bash -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="*" -SCHEDULE_START="08:00" -SCHEDULE_STOP="20:00" -SEGMENT_DURATION="2h" -PLIK_ENABLED=true -``` - -Die 12-Stunden-Aufnahme wird in 6 Segmente zu je 2 Stunden geteilt, jedes wird nach Fertigstellung hochgeladen. - -## Verhalten bei Reconnect - -Wenn die Verbindung abbricht und wiederhergestellt wird, werden die bisherigen Segmente hochgeladen und eine neue Segment-Serie mit neuem Zeitstempel begonnen. - -## Status - -Mit `./stream-recorder.sh status` werden bei segmentierten Aufnahmen die Anzahl der Segmente und die Gesamtgröße angezeigt. diff --git a/docs/zeitplanung.md b/docs/zeitplanung.md deleted file mode 100644 index 048f904..0000000 --- a/docs/zeitplanung.md +++ /dev/null @@ -1,80 +0,0 @@ -# Zeitplanung - -Die Zeitplanung wird direkt in der Job-Datei konfiguriert — kein Cron-Wissen nötig. Ein systemd-Timer prüft jede Minute alle Jobs und startet/stoppt Aufnahmen automatisch. - -## Einrichtung - -1. **Job-Datei erstellen** und `SCHEDULE_*`-Parameter setzen -2. **Scheduler installieren** mit `sudo install/install.sh` (einmalig) -3. Fertig — der Timer prüft jede Minute alle Jobs - -## Zeitplan-Parameter - -| Parameter | Standard | Beschreibung | -|--------------------|----------|-------------------------------------------------| -| `SCHEDULE_ENABLED` | `false` | Zeitplan aktivieren (`true`/`false`) | -| `SCHEDULE_ONCE` | `false` | Einmaliger Termin statt wöchentlichem Zeitplan | -| `SCHEDULE_DATE` | (leer) | Datum für einmalige Termine (`YYYY-MM-DD`) | -| `SCHEDULE_DAYS` | `*` | Aufnahmetage für wöchentliche Zeitpläne | -| `SCHEDULE_START` | (leer) | Startzeit im Format `HH:MM` | -| `SCHEDULE_STOP` | (leer) | Endzeit im Format `HH:MM` (leer = kein Auto-Stop)| - -## Tagesformate - -| Format | Bedeutung | -|--------------|----------------------------| -| `*` | Täglich | -| `Mo-Fr` | Montag bis Freitag | -| `Sa,So` | Samstag und Sonntag | -| `Mo,Mi,Fr` | Einzelne Tage | -| `Mo-Fr,So` | Kombiniert | - -Die Tagesangaben funktionieren auf Deutsch (Mo, Di, Mi, Do, Fr, Sa, So) und Englisch (Mon, Tue, Wed, Thu, Fri, Sat, Sun). - -## Verhalten - -- **Start + Stop gesetzt:** Aufnahme läuft im angegebenen Zeitfenster -- **Nur Start gesetzt:** Aufnahme startet zur angegebenen Zeit, läuft bis `MAX_DURATION` oder manueller Stop -- **Einmalig:** Mit `SCHEDULE_ONCE=true` und `SCHEDULE_DATE="YYYY-MM-DD"` startet der Job nur an diesem Datum -- **Manuell gestartete Aufnahmen** werden vom Scheduler nicht beeinflusst -- **Nach Neustart** des Servers erkennt der Scheduler automatisch, welche Aufnahmen laufen sollten -- **Nachtübergreifend:** Start- und Stoppzeiten können über Mitternacht gehen (z.B. `23:00` bis `02:00`) - -## Beispiele - -### Tägliche Aufnahme von 20:00 bis 22:00 - -```bash -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="*" -SCHEDULE_START="20:00" -SCHEDULE_STOP="22:00" -``` - -### Werktags um 18:00, 1 Stunde - -```bash -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="Mo-Fr" -SCHEDULE_START="18:00" -MAX_DURATION="1h" -``` - -### Nur am Wochenende - -```bash -SCHEDULE_ENABLED=true -SCHEDULE_DAYS="Sa,So" -SCHEDULE_START="14:00" -SCHEDULE_STOP="18:00" -``` - -### Einmalige Aufnahme am 15.09.2026 - -```bash -SCHEDULE_ENABLED=true -SCHEDULE_ONCE=true -SCHEDULE_DATE="2026-09-15" -SCHEDULE_START="21:00" -MAX_DURATION="30m" -``` diff --git a/install/install.sh b/install/install.sh deleted file mode 100644 index 99f4838..0000000 --- a/install/install.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash -############################################### -# stream-recorder - Installations-Script -# Richtet den systemd-Timer für die -# automatische Zeitplanung ein. -############################################### - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SERVICE_NAME="stream-recorder" - -echo "" -echo "stream-recorder - Installation" -echo "==============================" -echo "" - -# Root-Prüfung -if [[ $EUID -ne 0 ]]; then - echo "FEHLER: Dieses Script muss als root ausgeführt werden." - echo " sudo $0" - exit 1 -fi - -# Prüfe ob systemd vorhanden -if ! command -v systemctl &>/dev/null; then - echo "FEHLER: systemd ist nicht verfügbar." - echo " Dieses Script benötigt systemd für die Zeitplanung." - exit 1 -fi - -# Prüfe Abhängigkeiten -if ! command -v ffmpeg &>/dev/null; then - echo "ffmpeg nicht gefunden. Installiere ffmpeg..." - apt-get update -qq && apt-get install -y -qq ffmpeg - echo "ffmpeg installiert." -fi - -# Hauptscript ausführbar machen -chmod +x "${SCRIPT_DIR}/stream-recorder.sh" - -# Konfiguration aus Vorlage erstellen, falls nicht vorhanden -if [[ ! -f "${SCRIPT_DIR}/config/stream-recorder.conf" ]]; then - if [[ -f "${SCRIPT_DIR}/config/stream-recorder.conf.dist" ]]; then - cp "${SCRIPT_DIR}/config/stream-recorder.conf.dist" "${SCRIPT_DIR}/config/stream-recorder.conf" - echo "Konfiguration erstellt: config/stream-recorder.conf" - echo " Bitte anpassen: nano ${SCRIPT_DIR}/config/stream-recorder.conf" - echo "" - fi -fi - -echo "Installationspfad: ${SCRIPT_DIR}" -echo "" - -# systemd Service erstellen -cat > "/etc/systemd/system/${SERVICE_NAME}.service" < "/etc/systemd/system/${SERVICE_NAME}.timer" </dev/null || true -echo "" -echo "Der Scheduler prüft jetzt jede Minute die Job-Dateien" -echo "und startet/stoppt Aufnahmen automatisch." -echo "" -echo "Nächste Schritte:" -echo " 1. Konfiguration anpassen: nano ${SCRIPT_DIR}/config/stream-recorder.conf" -echo " 2. Job-Datei erstellen: cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job" -echo " 3. Job-Datei bearbeiten: nano jobs/mein-stream.job" -echo " 4. SCHEDULE_ENABLED=true setzen und Zeiten konfigurieren" -echo " 5. Jobs prüfen: ./stream-recorder.sh list" -echo "" -echo "Deinstallation: sudo install/uninstall.sh" diff --git a/install/uninstall.sh b/install/uninstall.sh deleted file mode 100644 index 3a4d156..0000000 --- a/install/uninstall.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -############################################### -# stream-recorder - Deinstallations-Script -# Entfernt den systemd-Timer und stoppt -# alle laufenden Aufnahmen. -############################################### - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SERVICE_NAME="stream-recorder" - -echo "" -echo "stream-recorder - Deinstallation" -echo "=================================" -echo "" - -# Root-Prüfung -if [[ $EUID -ne 0 ]]; then - echo "FEHLER: Dieses Script muss als root ausgeführt werden." - echo " sudo $0" - exit 1 -fi - -# Timer stoppen und deaktivieren -if systemctl is-active --quiet "${SERVICE_NAME}.timer" 2>/dev/null; then - echo "Stoppe Timer..." - systemctl stop "${SERVICE_NAME}.timer" -fi - -if systemctl is-enabled --quiet "${SERVICE_NAME}.timer" 2>/dev/null; then - echo "Deaktiviere Timer..." - systemctl disable "${SERVICE_NAME}.timer" -fi - -# Service-Dateien entfernen -if [[ -f "/etc/systemd/system/${SERVICE_NAME}.timer" ]]; then - rm -f "/etc/systemd/system/${SERVICE_NAME}.timer" - echo "Timer-Datei entfernt." -fi - -if [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then - rm -f "/etc/systemd/system/${SERVICE_NAME}.service" - echo "Service-Datei entfernt." -fi - -systemctl daemon-reload - -# Laufende Aufnahmen stoppen -echo "" -read -rp "Alle laufenden Aufnahmen stoppen? [j/N] " stop_all -if [[ "$stop_all" =~ ^[Jj]$ ]]; then - "${SCRIPT_DIR}/stream-recorder.sh" stop-all 2>/dev/null || true - echo "Aufnahmen gestoppt." -fi - -echo "" -echo "Deinstallation abgeschlossen." -echo "" -echo "Hinweis: Die Job-Dateien, Konfiguration und Aufnahmen" -echo " wurden NICHT entfernt." diff --git a/jobs/templates/example-hls.job.dist b/jobs/templates/example-hls.job.dist deleted file mode 100644 index 4fe205f..0000000 --- a/jobs/templates/example-hls.job.dist +++ /dev/null @@ -1,49 +0,0 @@ -############################################### -# stream-recorder - Job-Definition (HLS/m3u8) -############################################### -# Kopieren und anpassen: -# cp jobs/templates/example-hls.job.dist jobs/mein-hls-stream.job - -# Name des Streams -STREAM_NAME="Mein-HLS-Stream" - -# Stream-URL (m3u8-Playlist) -STREAM_URL="https://example.com/live/playlist.m3u8" - -# Stream-Typ -STREAM_TYPE="hls" - -# Ausgabeformat (mp4 oder mkv empfohlen) -OUTPUT_FORMAT="mp4" - -# Maximale Aufnahmedauer (leer = unbegrenzt, z.B. 5m oder 1h) -MAX_DURATION="" - -# Zusätzliche ffmpeg-Argumente (optional) -EXTRA_FFMPEG_ARGS="" - -############################################### -# Segment-Splitting (optional) -# z.B. 1h = stündlich, 2h = alle 2 Stunden -############################################### - -SEGMENT_DURATION="" - -############################################### -# Zeitplanung (optional) -############################################### - -SCHEDULE_ENABLED=false -SCHEDULE_ONCE=false -SCHEDULE_DATE="" -SCHEDULE_DAYS="*" -SCHEDULE_START="20:00" -SCHEDULE_STOP="22:00" - -############################################### -# Plik-Upload (optional) -# Fertige Aufnahme automatisch auf Plik hochladen -# PLIK_URL muss in der Konfiguration gesetzt sein -############################################### - -PLIK_ENABLED=false diff --git a/jobs/templates/example-mp3.job.dist b/jobs/templates/example-mp3.job.dist deleted file mode 100644 index 2f9b11c..0000000 --- a/jobs/templates/example-mp3.job.dist +++ /dev/null @@ -1,50 +0,0 @@ -############################################### -# stream-recorder - Job-Definition (MP3/Audio) -############################################### -# Kopieren und anpassen: -# cp jobs/templates/example-mp3.job.dist jobs/mein-radio.job - -# Name des Streams -STREAM_NAME="Mein-Radiosender" - -# Stream-URL (HTTP/HTTPS Audio-Stream) -STREAM_URL="https://example.com/stream.mp3" - -# Stream-Typ: auto, rtmp, hls, mp3, aac, ogg, flac, http -STREAM_TYPE="auto" - -# Ausgabeformat -OUTPUT_FORMAT="" - -# Maximale Aufnahmedauer (leer = unbegrenzt) -# Für Radio-Mitschnitte empfohlen, z.B. 1h für 1 Stunde -MAX_DURATION="1h" - -# Zusätzliche ffmpeg-Argumente (optional) -EXTRA_FFMPEG_ARGS="" - -############################################### -# Segment-Splitting (optional) -# z.B. 1h = stündlich, 2h = alle 2 Stunden -############################################### - -SEGMENT_DURATION="" - -############################################### -# Zeitplanung (optional) -############################################### - -SCHEDULE_ENABLED=false -SCHEDULE_ONCE=false -SCHEDULE_DATE="" -SCHEDULE_DAYS="Mo-Fr" -SCHEDULE_START="18:00" -SCHEDULE_STOP="" - -############################################### -# Plik-Upload (optional) -# Fertige Aufnahme automatisch auf Plik hochladen -# PLIK_URL muss in der Konfiguration gesetzt sein -############################################### - -PLIK_ENABLED=false diff --git a/jobs/templates/example-rtmp.job.dist b/jobs/templates/example-rtmp.job.dist deleted file mode 100644 index aad41c6..0000000 --- a/jobs/templates/example-rtmp.job.dist +++ /dev/null @@ -1,74 +0,0 @@ -############################################### -# stream-recorder - Job-Definition (RTMP) -############################################### -# Kopieren und anpassen: -# cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job - -# Name des Streams (wird für Dateinamen und Verzeichnisse verwendet) -STREAM_NAME="Mein-RTMP-Stream" - -# Stream-URL -STREAM_URL="rtmp://example.com/live/stream-key" - -# Stream-Typ: auto, rtmp, hls, mp3, aac, ogg, flac, http -# Bei "auto" wird der Typ anhand der URL erkannt -STREAM_TYPE="auto" - -# Ausgabeformat (Dateiendung): mp4, mkv, mp3, aac, ogg, flac -# Leer lassen für automatische Erkennung basierend auf Stream-Typ -OUTPUT_FORMAT="" - -# Maximale Aufnahmedauer (leer = unbegrenzt) -# Beispiele: 300 = 5 Minuten, 5m = 5 Minuten, 1h30m = 90 Minuten -MAX_DURATION="" - -# Zusätzliche ffmpeg-Argumente (optional) -EXTRA_FFMPEG_ARGS="" - -############################################### -# Segment-Splitting (optional) -# Aufnahme in Teile zerlegen -# Jedes Segment wird einzeln nach Plik hochgeladen -# Beispiele: 1h = stündlich, 2h = alle 2 Stunden, 30m = alle 30 Minuten -# Leer = deaktiviert (eine durchgehende Datei) -############################################### - -SEGMENT_DURATION="" - -############################################### -# Zeitplanung (optional) -# Damit der Scheduler funktioniert, muss er -# installiert sein: sudo install/install.sh -############################################### - -# Zeitplan aktivieren (true/false) -SCHEDULE_ENABLED=false - -# Einmalige Aufnahme statt wöchentlichem Zeitplan (true/false) -SCHEDULE_ONCE=false - -# Datum für einmalige Aufnahme (YYYY-MM-DD) -# Nur relevant, wenn SCHEDULE_ONCE=true ist -SCHEDULE_DATE="" - -# Aufnahmetage für wöchentliche Zeitpläne -# * = täglich -# Mo-Fr = Montag bis Freitag -# Sa,So = Samstag und Sonntag -# Mo,Mi,Fr = einzelne Tage -SCHEDULE_DAYS="*" - -# Startzeit (HH:MM) -SCHEDULE_START="20:00" - -# Endzeit (HH:MM) -# Leer = Aufnahme läuft bis MAX_DURATION oder manueller Stop -SCHEDULE_STOP="22:00" - -############################################### -# Plik-Upload (optional) -# Fertige Aufnahme automatisch auf Plik hochladen -# PLIK_URL muss in der Konfiguration gesetzt sein -############################################### - -PLIK_ENABLED=false diff --git a/stream-recorder.sh b/stream-recorder.sh deleted file mode 100644 index 7f6a998..0000000 --- a/stream-recorder.sh +++ /dev/null @@ -1,1621 +0,0 @@ -#!/bin/bash -############################################### -# Script: stream-recorder -# Description: Generischer Stream-Recorder für RTMP, HLS, -# HTTP-Audio (MP3, AAC, OGG) und weitere Formate -# Platforms: Debian/Ubuntu -# Author: Patrick Asmus -# Web: https://www.cleveradmin.de -# Repository: https://git.techniverse.net/scriptos/stream-recorder.git -# License: MIT -# Version: v2.8.0 -# Date: 07.09.2026 -# Modifications: Segment-Splitting, erweiterter Status, interaktive Job-Auswahl, Konfig updatesicher -############################################### - -set -uo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly VERSION="2.8.0" - -# --- Standardwerte (überschreibbar via Konfiguration) --- -CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf" -JOBS_DIR="${SCRIPT_DIR}/jobs" -DOWNLOAD_PATH="/home/downloads/recordings" -LOG_DIR="/var/log/stream-recorder" -LOG_LEVEL="INFO" -PID_DIR="/tmp/stream-recorder" -STATE_DIR="/var/lib/stream-recorder" -MAX_RETRIES=0 -RETRY_DELAY=5 - -# --- NTFY-Benachrichtigungen (optional) --- -NTFY_URL="" -NTFY_TOKEN="" -NTFY_EVENTS="error" - -# --- Plik-Upload (optional) --- -PLIK_URL="" -PLIK_API_KEY="" -PLIK_TTL="30d" - -# --- Laufzeit-Flags --- -VERBOSE=false -FOREGROUND=false - -# ============================================= -# Hilfsfunktionen -# ============================================= - -_log_level_num() { - case "$1" in - DEBUG) echo 0 ;; - INFO) echo 1 ;; - WARN) echo 2 ;; - ERROR) echo 3 ;; - *) echo 1 ;; - esac -} - -log() { - local level="$1" - shift - local message="$*" - - local msg_num cur_num - msg_num=$(_log_level_num "$level") - cur_num=$(_log_level_num "$LOG_LEVEL") - [[ $msg_num -lt $cur_num ]] && return - - local timestamp - timestamp=$(date '+%Y-%m-%d %H:%M:%S') - local entry="[${timestamp}] [${level}] ${message}" - - mkdir -p "$LOG_DIR" 2>/dev/null || true - echo "$entry" >> "${LOG_DIR}/stream-recorder.log" 2>/dev/null || true - - if [[ -t 1 ]] || [[ "$VERBOSE" == "true" ]]; then - echo "$entry" - fi -} - -notify() { - local event="$1" - local title="$2" - local message="$3" - local priority="${4:-default}" - - [[ -z "${NTFY_URL:-}" ]] && return - command -v curl &>/dev/null || return - [[ ! ",${NTFY_EVENTS}," == *",${event},"* ]] && return - - local -a curl_args=(-s -o /dev/null --max-time 10) - curl_args+=(-H "Title: ${title}") - curl_args+=(-H "Priority: ${priority}") - curl_args+=(-H "Tags: stream-recorder,${event}") - - if [[ -n "${NTFY_TOKEN:-}" ]]; then - curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}") - fi - - curl_args+=(-d "$message") - curl_args+=("$NTFY_URL") - - curl "${curl_args[@]}" 2>/dev/null & - disown 2>/dev/null || true -} - -plik_browser_url_from_download_url() { - local download_url="$1" - local plik_base="${PLIK_URL%/}" - local file_path="${download_url#"$plik_base"/file/}" - - if [[ "$file_path" != "$download_url" ]]; then - local upload_id="${file_path%%/*}" - if [[ -n "$upload_id" && "$upload_id" != "$file_path" ]]; then - echo "${plik_base}/#/?id=${upload_id}" - return - fi - fi - - echo "$download_url" -} - -upload_to_plik() { - local file="$1" - local stream_name="$2" - - [[ "${PLIK_ENABLED:-false}" != "true" ]] && return - [[ -z "${PLIK_URL:-}" ]] && return - if [[ ! -f "$file" ]]; then - log "WARN" "Plik-Upload übersprungen: Datei nicht gefunden: ${file}" - return 1 - fi - if [[ ! -s "$file" ]]; then - log "WARN" "Plik-Upload übersprungen: Datei ist leer: ${file}" - return 1 - fi - command -v curl &>/dev/null || { log "WARN" "curl nicht installiert - Plik-Upload übersprungen"; return; } - - local filename - filename=$(basename "$file") - - local plik_base="${PLIK_URL%/}" - - local -a curl_args=(-sS --max-time 7200) - - if [[ -n "${PLIK_API_KEY:-}" ]]; then - curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}") - fi - - local ttl_seconds="" - if [[ -n "${PLIK_TTL:-}" ]]; then - local ttl_val="${PLIK_TTL}" - if [[ "$ttl_val" =~ ^([0-9]+)d$ ]]; then - ttl_seconds=$(( ${BASH_REMATCH[1]} * 86400 )) - elif [[ "$ttl_val" =~ ^([0-9]+)h$ ]]; then - ttl_seconds=$(( ${BASH_REMATCH[1]} * 3600 )) - elif [[ "$ttl_val" =~ ^[0-9]+$ ]]; then - ttl_seconds="$ttl_val" - fi - fi - - curl_args+=(-F "file=@${file};filename=${filename}") - [[ -n "$ttl_seconds" ]] && curl_args+=(-F "ttl=${ttl_seconds}") - - log "INFO" "Plik-Upload: ${filename} -> ${plik_base}" - - local upload_tmp upload_response http_code curl_status=0 - upload_tmp=$(mktemp) - http_code=$(curl "${curl_args[@]}" \ - -w "%{http_code}" \ - -o "$upload_tmp" \ - "${plik_base}" 2>/dev/null) || curl_status=$? - upload_response=$(cat "$upload_tmp" 2>/dev/null || true) - rm -f "$upload_tmp" - - if [[ $curl_status -ne 0 || ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then - log "ERROR" "Plik-Upload fehlgeschlagen: Datei konnte nicht hochgeladen werden (HTTP ${http_code:-000}, curl ${curl_status})" - [[ -n "$upload_response" ]] && log "DEBUG" "Plik-Antwort: ${upload_response}" - return 1 - fi - - local download_url - download_url=$(printf '%s\n' "$upload_response" | grep -Eo 'https?://[^[:space:]]+' | tail -1) - if [[ -z "$download_url" ]]; then - log "ERROR" "Plik-Upload fehlgeschlagen: Keine Download-URL in der Antwort" - log "DEBUG" "Plik-Antwort: ${upload_response}" - return 1 - fi - - local browser_url - browser_url=$(plik_browser_url_from_download_url "$download_url") - - log "INFO" "Plik-Upload abgeschlossen: ${browser_url}" - if [[ "$browser_url" != "$download_url" ]]; then - log "INFO" "Plik-Datei-URL: ${download_url}" - fi - notify "upload" "Plik-Upload" "${stream_name}: ${browser_url}" - - if [[ "${DELETE_AFTER_UPLOAD:-false}" == "true" ]]; then - rm -f "$file" - log "INFO" "Lokale Datei gelöscht: ${file}" - fi - - echo "$browser_url" -} - -parse_duration() { - local input="$1" - [[ -z "$input" ]] && { echo ""; return; } - [[ "$input" =~ ^[0-9]+$ ]] && { echo "$input"; return; } - - local total=0 - [[ "$input" =~ ([0-9]+)d ]] && total=$((total + ${BASH_REMATCH[1]} * 86400)) - [[ "$input" =~ ([0-9]+)h ]] && total=$((total + ${BASH_REMATCH[1]} * 3600)) - [[ "$input" =~ ([0-9]+)m ]] && total=$((total + ${BASH_REMATCH[1]} * 60)) - [[ "$input" =~ ([0-9]+)s ]] && total=$((total + ${BASH_REMATCH[1]})) - - if [[ $total -eq 0 ]]; then - log "WARN" "Ungültige Dauer: ${input}" - echo "" - return - fi - echo "$total" -} - -format_elapsed() { - local seconds=$1 - local d=$((seconds / 86400)) - local h=$(( (seconds % 86400) / 3600 )) - local m=$(( (seconds % 3600) / 60 )) - local s=$((seconds % 60)) - - if [[ $d -gt 0 ]]; then - printf "%dd %dh %dm" "$d" "$h" "$m" - elif [[ $h -gt 0 ]]; then - printf "%dh %dm %ds" "$h" "$m" "$s" - else - printf "%dm %ds" "$m" "$s" - fi -} - -format_size() { - local bytes=$1 - if [[ $bytes -ge 1073741824 ]]; then - awk "BEGIN {printf \"%.1f GB\", $bytes/1073741824}" - elif [[ $bytes -ge 1048576 ]]; then - awk "BEGIN {printf \"%.1f MB\", $bytes/1048576}" - elif [[ $bytes -ge 1024 ]]; then - awk "BEGIN {printf \"%.1f KB\", $bytes/1024}" - else - printf "%d B" "$bytes" - fi -} - -write_job_info() { - local job_id="$1" stream_name="$2" output_dir="$3" output_file="$4" - local stream_type="$5" segment_mode="$6" job_file="$7" - local info_file="${PID_DIR}/${job_id}.info" - - cat > "$info_file" </dev/null || true -} - -# --- Tages-/Zeitplanung --- - -_day_name_to_num() { - case "${1,,}" in - mo|mon|montag|1) echo 1 ;; - di|tue|dienstag|2) echo 2 ;; - mi|wed|mittwoch|3) echo 3 ;; - do|thu|donnerstag|4) echo 4 ;; - fr|fri|freitag|5) echo 5 ;; - sa|sat|samstag|6) echo 6 ;; - so|sun|sonntag|7) echo 7 ;; - *) echo 0 ;; - esac -} - -_matches_schedule_day() { - local today_num="$1" - local schedule="$2" - - [[ "$schedule" == "*" ]] && return 0 - - local IFS=',' - for entry in $schedule; do - entry=$(echo "$entry" | tr -d ' ') - - if [[ "$entry" == *-* ]]; then - local start_num end_num - start_num=$(_day_name_to_num "${entry%%-*}") - end_num=$(_day_name_to_num "${entry##*-}") - - if [[ $start_num -le $end_num ]]; then - [[ $today_num -ge $start_num && $today_num -le $end_num ]] && return 0 - else - [[ $today_num -ge $start_num || $today_num -le $end_num ]] && return 0 - fi - else - local day_num - day_num=$(_day_name_to_num "$entry") - [[ $today_num -eq $day_num ]] && return 0 - fi - done - return 1 -} - -_time_to_minutes() { - local time="$1" - local hours="${time%%:*}" - local minutes="${time##*:}" - echo $(( 10#$hours * 60 + 10#$minutes )) -} - -_in_time_window() { - local now="$1" start="$2" stop="$3" - local now_min start_min stop_min - now_min=$(_time_to_minutes "$now") - start_min=$(_time_to_minutes "$start") - stop_min=$(_time_to_minutes "$stop") - - if [[ $start_min -le $stop_min ]]; then - [[ $now_min -ge $start_min && $now_min -lt $stop_min ]] - else - [[ $now_min -ge $start_min || $now_min -lt $stop_min ]] - fi -} - -# ============================================= -# Konfiguration & Abhängigkeiten -# ============================================= - -load_config() { - if [[ -f "$CONFIG_FILE" ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" - else - local dist_file="${CONFIG_FILE}.dist" - if [[ -f "$dist_file" ]]; then - log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE}" - log "INFO" "Vorlage kopieren: cp ${dist_file} ${CONFIG_FILE}" - else - log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte" - fi - fi - mkdir -p "$DOWNLOAD_PATH" "$LOG_DIR" "$PID_DIR" "$STATE_DIR" 2>/dev/null || true -} - -check_dependencies() { - if ! command -v ffmpeg &>/dev/null; then - log "ERROR" "ffmpeg ist nicht installiert" - log "INFO" "Installation: sudo apt install ffmpeg" - exit 1 - fi - if [[ -n "${NTFY_URL:-}" ]] && ! command -v curl &>/dev/null; then - log "WARN" "curl ist nicht installiert - NTFY-Benachrichtigungen deaktiviert" - fi -} - -# ============================================= -# Stream-Erkennung & Job-Verwaltung -# ============================================= - -detect_stream_type() { - local url="$1" - case "$url" in - rtmp://*|rtmps://*) echo "rtmp" ;; - *.[Mm]3[Uu]8|*.[Mm]3[Uu]8\?*) echo "hls" ;; - *.[Mm][Pp]3|*.[Mm][Pp]3\?*) echo "mp3" ;; - *.[Aa][Aa][Cc]|*.[Aa][Aa][Cc]\?*) echo "aac" ;; - *.[Oo][Gg][Gg]|*.[Oo][Gg][Gg]\?*) echo "ogg" ;; - *.[Ff][Ll][Aa][Cc]|*.[Ff][Ll][Aa][Cc]\?*) echo "flac" ;; - http://*|https://*) echo "http" ;; - *) echo "http" ;; - esac -} - -default_extension() { - case "$1" in - rtmp|hls|http) echo "mp4" ;; - mp3) echo "mp3" ;; - aac) echo "aac" ;; - ogg) echo "ogg" ;; - flac) echo "flac" ;; - *) echo "mp4" ;; - esac -} - -load_job() { - local job_file="$1" - - STREAM_NAME="" - STREAM_URL="" - STREAM_TYPE="auto" - OUTPUT_FORMAT="" - MAX_DURATION="" - EXTRA_FFMPEG_ARGS="" - SCHEDULE_ENABLED="false" - SCHEDULE_ONCE="false" - SCHEDULE_DATE="" - SCHEDULE_DAYS="*" - SCHEDULE_START="" - SCHEDULE_STOP="" - PLIK_ENABLED="false" - DELETE_AFTER_UPLOAD="false" - SEGMENT_DURATION="" - - if [[ ! -f "$job_file" ]]; then - log "ERROR" "Job-Datei nicht gefunden: ${job_file}" - return 1 - fi - - # shellcheck source=/dev/null - source "$job_file" - - if [[ -z "$STREAM_NAME" ]]; then - log "ERROR" "STREAM_NAME fehlt in: ${job_file}" - return 1 - fi - if [[ -z "$STREAM_URL" ]]; then - log "ERROR" "STREAM_URL fehlt in: ${job_file}" - return 1 - fi - - if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then - if [[ -z "${SCHEDULE_DATE:-}" ]]; then - log "ERROR" "SCHEDULE_DATE fehlt in einmaligem Zeitplan: ${job_file}" - return 1 - fi - if [[ ! "${SCHEDULE_DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then - log "ERROR" "Ungültiges SCHEDULE_DATE in ${job_file}: ${SCHEDULE_DATE} (erwartet: YYYY-MM-DD)" - return 1 - fi - fi - - if [[ -n "$MAX_DURATION" && "$MAX_DURATION" != *:* ]]; then - local normalized_duration - normalized_duration=$(parse_duration "$MAX_DURATION") - if [[ -z "$normalized_duration" ]]; then - log "ERROR" "Ungültige MAX_DURATION in ${job_file}: ${MAX_DURATION}" - return 1 - fi - MAX_DURATION="$normalized_duration" - fi -} - -resolve_job_path() { - local input="$1" - [[ -f "$input" ]] && { echo "$input"; return; } - [[ -f "${JOBS_DIR}/${input}" ]] && { echo "${JOBS_DIR}/${input}"; return; } - [[ -f "${JOBS_DIR}/${input}.job" ]] && { echo "${JOBS_DIR}/${input}.job"; return; } - echo "$input" -} - -is_recording() { - local job_id="$1" - local pid_file="${PID_DIR}/${job_id}.pid" - - if [[ -f "$pid_file" ]]; then - local pid - pid=$(cat "$pid_file") - if kill -0 "$pid" 2>/dev/null; then - return 0 - else - rm -f "$pid_file" - fi - fi - return 1 -} - -schedule_state_file() { - local job_id="$1" - echo "${STATE_DIR}/${job_id}.last-scheduled" -} - -schedule_occurrence_key() { - local schedule_date - if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then - schedule_date="${SCHEDULE_DATE:-$(date '+%F')}" - else - schedule_date=$(date '+%F') - fi - echo "${SCHEDULE_ONCE:-false}|${schedule_date}|${SCHEDULE_DAYS:-*}|${SCHEDULE_START:-}|${SCHEDULE_STOP:-}|${MAX_DURATION:-}" -} - -schedule_already_started() { - local job_id="$1" - local occurrence_key="$2" - local state_file - state_file=$(schedule_state_file "$job_id") - - [[ -f "$state_file" ]] || return 1 - [[ "$(cat "$state_file" 2>/dev/null || true)" == "$occurrence_key" ]] -} - -mark_schedule_started() { - local job_id="$1" - local occurrence_key="$2" - local state_file - state_file=$(schedule_state_file "$job_id") - - mkdir -p "$(dirname "$state_file")" 2>/dev/null || true - if ! printf '%s\n' "$occurrence_key" > "$state_file"; then - log "ERROR" "Scheduler-Status konnte nicht geschrieben werden: ${state_file}" - return 1 - fi -} - -# ============================================= -# Segment-Upload -# ============================================= - -_segment_upload_loop() { - local output_dir="$1" segment_glob="$2" stream_name="$3" - local uploaded_list="$4" stop_flag="$5" - - while [[ ! -f "$stop_flag" ]]; do - sleep 60 - [[ -f "$stop_flag" ]] && break - - local -a all_segments=() - for f in "$output_dir"/$segment_glob; do - [[ -f "$f" ]] && all_segments+=("$f") - done - - IFS=$'\n' all_segments=($(sort <<<"${all_segments[*]}")); unset IFS - - [[ ${#all_segments[@]} -lt 2 ]] && continue - - local i - for (( i=0; i < ${#all_segments[@]} - 1; i++ )); do - local seg="${all_segments[$i]}" - if ! grep -qxF "$seg" "$uploaded_list" 2>/dev/null; then - if [[ -s "$seg" ]]; then - log "INFO" "Segment-Upload: $(basename "$seg")" - upload_to_plik "$seg" "$stream_name" && echo "$seg" >> "$uploaded_list" - fi - fi - done - done -} - -_upload_remaining_segments() { - local output_dir="$1" segment_glob="$2" stream_name="$3" uploaded_list="$4" - - local count=0 - for seg in "$output_dir"/$segment_glob; do - [[ -f "$seg" ]] || continue - [[ -s "$seg" ]] || continue - if ! grep -qxF "$seg" "$uploaded_list" 2>/dev/null; then - log "INFO" "Segment-Upload (verbleibend): $(basename "$seg")" - upload_to_plik "$seg" "$stream_name" && echo "$seg" >> "$uploaded_list" - count=$((count + 1)) - fi - done - - [[ $count -gt 0 ]] && log "INFO" "${count} verbleibende(s) Segment(e) hochgeladen" - rm -f "$uploaded_list" 2>/dev/null || true -} - -# ============================================= -# Aufnahme -# ============================================= - -record_stream() { - local job_file="$1" - - load_job "$job_file" || return 1 - - local job_id - job_id=$(basename "$job_file" .job) - - local stream_type="${STREAM_TYPE}" - [[ "$stream_type" == "auto" ]] && stream_type=$(detect_stream_type "$STREAM_URL") - - local ext="${OUTPUT_FORMAT}" - [[ -z "$ext" ]] && ext=$(default_extension "$stream_type") - - local segment_seconds="" - local segment_mode=false - if [[ -n "${SEGMENT_DURATION:-}" ]]; then - segment_seconds=$(parse_duration "$SEGMENT_DURATION") - if [[ -n "$segment_seconds" && "$segment_seconds" -gt 0 ]]; then - segment_mode=true - fi - fi - - local safe_name - safe_name=$(echo "$STREAM_NAME" | tr -cs 'A-Za-z0-9_-' '_' | sed 's/_$//') - local timestamp - timestamp=$(date +'%Y%m%d_%H%M%S') - local output_dir="${DOWNLOAD_PATH}/${safe_name}" - local output_file="${output_dir}/${safe_name}_${timestamp}.${ext}" - local output_pattern="" - if $segment_mode; then - output_file="${output_dir}/${safe_name}_${timestamp}_seg%03d.${ext}" - output_pattern="${safe_name}_${timestamp}_seg*.${ext}" - fi - local pid_file="${PID_DIR}/${job_id}.pid" - local ffmpeg_log="${LOG_DIR}/ffmpeg_${job_id}.log" - - mkdir -p "$output_dir" - - if is_recording "$job_id"; then - log "WARN" "Aufnahme von '${STREAM_NAME}' läuft bereits (Job: ${job_id})" - return 1 - fi - - if $segment_mode; then - log "INFO" "Starte Aufnahme: ${STREAM_NAME} [${stream_type}] -> ${output_dir} (Segment: alle $(format_elapsed "$segment_seconds"))" - else - log "INFO" "Starte Aufnahme: ${STREAM_NAME} [${stream_type}] -> ${output_file}" - fi - notify "start" "Aufnahme gestartet" "${STREAM_NAME} [${stream_type}]" - write_job_info "$job_id" "$STREAM_NAME" "$output_dir" "$output_file" "$stream_type" "$segment_mode" "$job_file" - - local ffmpeg_loglevel="warning" - [[ "$VERBOSE" == "true" ]] && ffmpeg_loglevel="info" - - local -a ffmpeg_args=(-nostdin -y -hide_banner -loglevel "$ffmpeg_loglevel") - - case "$stream_type" in - hls) ffmpeg_args+=(-i "$STREAM_URL" -c copy -bsf:a aac_adtstoasc) ;; - *) ffmpeg_args+=(-i "$STREAM_URL" -c copy) ;; - esac - - if $segment_mode; then - ffmpeg_args+=(-f segment -segment_time "$segment_seconds" -reset_timestamps 1) - fi - - [[ -n "$MAX_DURATION" ]] && ffmpeg_args+=(-t "$MAX_DURATION") - - if [[ -n "$EXTRA_FFMPEG_ARGS" ]]; then - local -a extra - read -ra extra <<< "$EXTRA_FFMPEG_ARGS" - ffmpeg_args+=("${extra[@]}") - fi - - ffmpeg_args+=("$output_file") - - _SR_FFMPEG_PID="" - _SR_PID_FILE="$pid_file" - _SR_JOB_ID="$job_id" - _SR_SEGMENT_STOP_FLAG="" - _SR_SEGMENT_UPLOADER_PID="" - _cleanup() { - if [[ -n "${_SR_SEGMENT_STOP_FLAG:-}" ]]; then - touch "$_SR_SEGMENT_STOP_FLAG" 2>/dev/null || true - fi - if [[ -n "${_SR_SEGMENT_UPLOADER_PID:-}" ]] && kill -0 "$_SR_SEGMENT_UPLOADER_PID" 2>/dev/null; then - wait "$_SR_SEGMENT_UPLOADER_PID" 2>/dev/null || true - fi - if [[ -n "${_SR_FFMPEG_PID:-}" ]] && kill -0 "$_SR_FFMPEG_PID" 2>/dev/null; then - kill -INT "$_SR_FFMPEG_PID" 2>/dev/null || true - wait "$_SR_FFMPEG_PID" 2>/dev/null || true - fi - rm -f "${_SR_PID_FILE:-}" - rm -f "${_SR_PID_FILE%.pid}.scheduled" 2>/dev/null || true - remove_job_info "${_SR_JOB_ID:-}" - rm -f "${_SR_SEGMENT_STOP_FLAG:-}" 2>/dev/null || true - } - trap _cleanup EXIT TERM INT - - local _sr_uploaded_list="" - if $segment_mode; then - _sr_uploaded_list=$(mktemp) - _SR_SEGMENT_STOP_FLAG=$(mktemp -u) - fi - - local retry=0 - while true; do - if [[ "$VERBOSE" == "true" ]]; then - ffmpeg "${ffmpeg_args[@]}" > >(tee -a "$ffmpeg_log") 2>&1 & - else - ffmpeg "${ffmpeg_args[@]}" >> "$ffmpeg_log" 2>&1 & - fi - _SR_FFMPEG_PID=$! - echo "$_SR_FFMPEG_PID" > "$pid_file" - - if $segment_mode; then - rm -f "$_SR_SEGMENT_STOP_FLAG" 2>/dev/null || true - _segment_upload_loop "$output_dir" "$output_pattern" "$STREAM_NAME" "$_sr_uploaded_list" "$_SR_SEGMENT_STOP_FLAG" & - _SR_SEGMENT_UPLOADER_PID=$! - fi - - local ret=0 - wait "$_SR_FFMPEG_PID" || ret=$? - _SR_FFMPEG_PID="" - - if [[ -n "${_SR_SEGMENT_UPLOADER_PID:-}" ]] && kill -0 "$_SR_SEGMENT_UPLOADER_PID" 2>/dev/null; then - touch "$_SR_SEGMENT_STOP_FLAG" 2>/dev/null || true - wait "$_SR_SEGMENT_UPLOADER_PID" 2>/dev/null || true - _SR_SEGMENT_UPLOADER_PID="" - fi - - if [[ $ret -eq 0 ]]; then - if $segment_mode; then - _upload_remaining_segments "$output_dir" "$output_pattern" "$STREAM_NAME" "$_sr_uploaded_list" - log "INFO" "Aufnahme abgeschlossen: ${STREAM_NAME} (segmentiert)" - notify "stop" "Aufnahme abgeschlossen" "${STREAM_NAME} (segmentiert)" - elif [[ -s "$output_file" ]]; then - log "INFO" "Aufnahme abgeschlossen: ${output_file}" - notify "stop" "Aufnahme abgeschlossen" "${STREAM_NAME}: ${output_file}" - upload_to_plik "$output_file" "$STREAM_NAME" - else - [[ -f "$output_file" ]] && rm -f "$output_file" - log "ERROR" "Aufnahme fehlgeschlagen: Ausgabedatei fehlt oder ist leer (${output_file})" - notify "error" "Aufnahme fehlgeschlagen" "${STREAM_NAME}: Ausgabedatei fehlt oder ist leer" "high" - fi - break - fi - - if [[ $ret -eq 143 || $ret -eq 130 ]]; then - log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}" - notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt" - if $segment_mode; then - _upload_remaining_segments "$output_dir" "$output_pattern" "$STREAM_NAME" "$_sr_uploaded_list" - elif [[ -s "$output_file" ]]; then - upload_to_plik "$output_file" "$STREAM_NAME" - fi - break - fi - - if [[ ! -f "$pid_file" ]]; then - log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}" - notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt" - if $segment_mode; then - _upload_remaining_segments "$output_dir" "$output_pattern" "$STREAM_NAME" "$_sr_uploaded_list" - elif [[ -s "$output_file" ]]; then - upload_to_plik "$output_file" "$STREAM_NAME" - fi - break - fi - - retry=$((retry + 1)) - if [[ $MAX_RETRIES -gt 0 && $retry -ge $MAX_RETRIES ]]; then - log "ERROR" "Maximale Versuche (${MAX_RETRIES}) für '${STREAM_NAME}' erreicht - Aufnahme abgebrochen" - notify "error" "Aufnahme fehlgeschlagen" "${STREAM_NAME}: Max. Versuche (${MAX_RETRIES}) erreicht (Code: ${ret})" "high" - break - fi - - local retry_info="" - if [[ $MAX_RETRIES -gt 0 ]]; then - retry_info=" (Versuch: ${retry}/${MAX_RETRIES})" - else - retry_info=" (Versuch: ${retry})" - fi - - log "WARN" "Verbindung zu '${STREAM_NAME}' verloren (Code: ${ret})${retry_info}. Reconnect in ${RETRY_DELAY}s..." - notify "error" "Stream abgerissen" "${STREAM_NAME}: Verbindung verloren (Code: ${ret}). Reconnect in ${RETRY_DELAY}s..." "default" - - if $segment_mode; then - _upload_remaining_segments "$output_dir" "$output_pattern" "$STREAM_NAME" "$_sr_uploaded_list" - fi - - sleep "$RETRY_DELAY" - - timestamp=$(date +'%Y%m%d_%H%M%S') - if $segment_mode; then - output_file="${output_dir}/${safe_name}_${timestamp}_seg%03d.${ext}" - output_pattern="${safe_name}_${timestamp}_seg*.${ext}" - else - output_file="${output_dir}/${safe_name}_${timestamp}.${ext}" - fi - ffmpeg_args[-1]="$output_file" - done - - trap - EXIT TERM INT - rm -f "$pid_file" - rm -f "${pid_file%.pid}.scheduled" 2>/dev/null || true - remove_job_info "$job_id" - rm -f "${_SR_SEGMENT_STOP_FLAG:-}" 2>/dev/null || true - - if $segment_mode; then - for seg in "$output_dir"/$output_pattern; do - [[ -f "$seg" ]] || continue - if [[ ! -s "$seg" ]]; then - rm -f "$seg" - log "WARN" "Leeres Segment entfernt: ${seg}" - fi - done - elif [[ -f "$output_file" && ! -s "$output_file" ]]; then - rm -f "$output_file" - log "WARN" "Leere Ausgabedatei entfernt: ${output_file}" - fi -} - -record_all() { - local found=false - - for job_file in "${JOBS_DIR}"/*.job; do - [[ -f "$job_file" ]] || continue - found=true - log "INFO" "Starte Job im Hintergrund: $(basename "$job_file")" - "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" & - done - - if ! $found; then - log "WARN" "Keine .job-Dateien gefunden in: ${JOBS_DIR}" - log "INFO" "Erstelle Job-Dateien aus den Vorlagen: cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job" - return 1 - fi - - wait - log "INFO" "Alle Aufnahmen beendet" -} - -start_detached_recording() { - local job_file="$1" - - load_job "$job_file" || return 1 - - local job_id - job_id=$(basename "$job_file" .job) - - if is_recording "$job_id"; then - log "WARN" "Aufnahme von '${STREAM_NAME}' läuft bereits (Job: ${job_id})" - return 1 - fi - - local nohup_log="${LOG_DIR}/nohup_${job_id}.log" - log "INFO" "Starte Aufnahme im Hintergrund: ${STREAM_NAME} [${job_id}]" - - nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" \ - >> "$nohup_log" 2>&1 < /dev/null & - local detached_pid=$! - disown 2>/dev/null || true - - echo "Aufnahme im Hintergrund gestartet: ${STREAM_NAME} [${job_id}]" - echo "Starter-PID: ${detached_pid}" - echo "Logs: ${LOG_DIR}/stream-recorder.log" -} - -# ============================================= -# Scheduler (wird per systemd-Timer aufgerufen) -# ============================================= - -run_scheduler() { - local today_date today_num now_time - today_date=$(date '+%F') - today_num=$(date '+%u') - now_time=$(date '+%H:%M') - - log "DEBUG" "Scheduler: Datum=${today_date} Tag=${today_num} Zeit=${now_time}" - - for job_file in "${JOBS_DIR}"/*.job; do - [[ -f "$job_file" ]] || continue - load_job "$job_file" 2>/dev/null || continue - - local job_id - job_id=$(basename "$job_file" .job) - - [[ "${SCHEDULE_ENABLED:-false}" != "true" ]] && continue - [[ -z "${SCHEDULE_START:-}" ]] && continue - - local is_today=false - if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then - [[ "${SCHEDULE_DATE:-}" == "$today_date" ]] && is_today=true - else - _matches_schedule_day "$today_num" "${SCHEDULE_DAYS:-*}" && is_today=true - fi - - local in_window=false - if $is_today; then - if [[ -n "${SCHEDULE_STOP:-}" ]]; then - _in_time_window "$now_time" "$SCHEDULE_START" "$SCHEDULE_STOP" && in_window=true - else - local now_min start_min - now_min=$(_time_to_minutes "$now_time") - start_min=$(_time_to_minutes "$SCHEDULE_START") - [[ $now_min -ge $start_min ]] && in_window=true - fi - fi - - if $in_window; then - local occurrence_key - occurrence_key=$(schedule_occurrence_key) - - if schedule_already_started "$job_id" "$occurrence_key"; then - log "DEBUG" "Scheduler: '${STREAM_NAME}' [${job_id}] wurde für dieses Zeitfenster bereits gestartet" - continue - fi - - if ! is_recording "$job_id"; then - log "INFO" "Scheduler: Starte '${STREAM_NAME}' [${job_id}]" - mark_schedule_started "$job_id" "$occurrence_key" || continue - printf '%s\n' "$occurrence_key" > "${PID_DIR}/${job_id}.scheduled" - nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" \ - >> "${LOG_DIR}/nohup_${job_id}.log" 2>&1 & - disown 2>/dev/null || true - fi - else - if is_recording "$job_id" && [[ -f "${PID_DIR}/${job_id}.scheduled" ]]; then - if [[ -n "${SCHEDULE_STOP:-}" ]]; then - log "INFO" "Scheduler: Stoppe '${STREAM_NAME}' [${job_id}]" - stop_recording "$job_id" - rm -f "${PID_DIR}/${job_id}.scheduled" - elif ! $is_today && [[ "${SCHEDULE_ONCE:-false}" != "true" ]]; then - log "INFO" "Scheduler: Stoppe '${STREAM_NAME}' [${job_id}]" - stop_recording "$job_id" - rm -f "${PID_DIR}/${job_id}.scheduled" - fi - fi - fi - done -} - -# ============================================= -# Interaktive Schnellaufnahme -# ============================================= - -interactive_record() { - echo "" - echo "stream-recorder v${VERSION} - Schnellaufnahme" - echo "$(printf '=%.0s' {1..50})" - echo "" - - read -rp "Stream-URL: " url - if [[ -z "$url" ]]; then - echo "Keine URL angegeben. Abbruch." - return 1 - fi - - local detected_type - detected_type=$(detect_stream_type "$url") - - local default_name - default_name=$(echo "$url" | sed 's|.*://||; s|/|_|g; s|\?.*||' | tr -cs 'A-Za-z0-9_-' '_' | sed 's/_$//' | cut -c1-40) - [[ -z "$default_name" ]] && default_name="Schnellaufnahme" - - read -rp "Name [${default_name}]: " name - name="${name:-$default_name}" - - read -rp "Typ [${detected_type}]: " custom_type - local stream_type="${custom_type:-$detected_type}" - - read -rp "Dauer (z.B. 30m, 1h30m, 3600 oder leer=unbegrenzt): " duration_input - local duration="" - if [[ -n "$duration_input" ]]; then - duration=$(parse_duration "$duration_input") - if [[ -n "$duration" ]]; then - local h=$((duration / 3600)) - local m=$(( (duration % 3600) / 60 )) - local s=$((duration % 60)) - echo "Dauer: ${h}h ${m}m ${s}s (${duration} Sekunden)" - fi - fi - - echo "" - echo "Zusammenfassung:" - echo " Name: ${name}" - echo " URL: ${url}" - echo " Typ: ${stream_type}" - echo " Dauer: ${duration:-unbegrenzt}" - echo "" - read -rp "Aufnahme starten? [J/n] " confirm - if [[ "$confirm" =~ ^[Nn]$ ]]; then - echo "Abgebrochen." - return 1 - fi - - local safe_id - safe_id=$(echo "$name" | tr -cs 'A-Za-z0-9_-' '_' | sed 's/_$//') - local tmp_job="${PID_DIR}/_quick_${safe_id}.job" - - cat > "$tmp_job" < "$job_file" </dev/null || continue - - local job_id - job_id=$(basename "$job_file" .job) - local status="bereit" - - if is_recording "$job_id"; then - local pid - pid=$(cat "${PID_DIR}/${job_id}.pid") - status="läuft (PID: ${pid})" - fi - - printf " %-20s %-8s %-40s [%s]\n" "$job_id" "${STREAM_TYPE:-auto}" "$STREAM_URL" "$status" - - local extra_info="" - if [[ "${SCHEDULE_ENABLED:-false}" == "true" && -n "${SCHEDULE_START:-}" ]]; then - if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then - extra_info="Zeitplan: einmalig ${SCHEDULE_DATE:-?} ${SCHEDULE_START}" - else - extra_info="Zeitplan: wöchentlich ${SCHEDULE_DAYS:-*} ${SCHEDULE_START}" - fi - if [[ -n "${SCHEDULE_STOP:-}" ]]; then - extra_info+=" - ${SCHEDULE_STOP}" - elif [[ -n "${MAX_DURATION:-}" ]]; then - extra_info+=" (Dauer: ${MAX_DURATION}s)" - fi - fi - if [[ -n "${SEGMENT_DURATION:-}" ]]; then - local seg_secs - seg_secs=$(parse_duration "$SEGMENT_DURATION") - if [[ -n "$seg_secs" && "$seg_secs" -gt 0 ]]; then - extra_info+="${extra_info:+ | }Segmente: alle $(format_elapsed "$seg_secs")" - fi - fi - if [[ "${PLIK_ENABLED:-false}" == "true" ]]; then - extra_info+="${extra_info:+ | }Plik-Upload: an" - fi - if [[ -n "$extra_info" ]]; then - printf " %-20s %s\n" "" "$extra_info" - fi - done - - if ! $found; then - echo " Keine .job-Dateien gefunden." - echo "" - echo " Erstelle einen Job aus einer Vorlage:" - echo " cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job" - fi - - echo "" - - local dist_count=0 - for dist_file in "${JOBS_DIR}"/templates/*.job.dist; do - [[ -f "$dist_file" ]] || continue - dist_count=$((dist_count + 1)) - done - - if [[ $dist_count -gt 0 ]]; then - echo "Vorlagen: ${dist_count} Stück (*.job.dist)" - fi -} - -show_status() { - echo "Laufende Aufnahmen:" - echo "$(printf '=%.0s' {1..60})" - echo "" - - local found=false - for pid_file in "${PID_DIR}"/*.pid; do - [[ -f "$pid_file" ]] || continue - - local job_id - job_id=$(basename "$pid_file" .pid) - local pid - pid=$(cat "$pid_file") - - if kill -0 "$pid" 2>/dev/null; then - found=true - local mode="manuell" - [[ -f "${PID_DIR}/${job_id}.scheduled" ]] && mode="geplant" - - local info_file="${PID_DIR}/${job_id}.info" - if [[ -f "$info_file" ]]; then - local STREAM_NAME="" STREAM_TYPE="" OUTPUT_DIR="" OUTPUT_FILE="" - local START_TIME="" START_EPOCH="" SEGMENT_MODE="" - # shellcheck source=/dev/null - source "$info_file" - - printf " %-20s %s\n" "$job_id" "$STREAM_NAME" - printf " %-20s PID: %s | Modus: %s | Typ: %s\n" "" "$pid" "$mode" "$STREAM_TYPE" - printf " %-20s Gestartet: %s" "" "$START_TIME" - - if [[ -n "$START_EPOCH" ]]; then - local now_epoch elapsed - now_epoch=$(date +%s) - elapsed=$((now_epoch - START_EPOCH)) - printf " | Laufzeit: %s" "$(format_elapsed "$elapsed")" - fi - echo "" - - local total_size=0 - if [[ "$SEGMENT_MODE" == "true" && -n "$OUTPUT_DIR" ]]; then - local seg_count=0 - for seg in "$OUTPUT_DIR"/*_seg*; do - [[ -f "$seg" ]] || continue - local s - s=$(stat -c%s "$seg" 2>/dev/null || echo 0) - total_size=$((total_size + s)) - seg_count=$((seg_count + 1)) - done - printf " %-20s Verzeichnis: %s | Segmente: %d | Größe: %s\n" "" "$OUTPUT_DIR" "$seg_count" "$(format_size "$total_size")" - elif [[ -n "$OUTPUT_FILE" && -f "$OUTPUT_FILE" ]]; then - total_size=$(stat -c%s "$OUTPUT_FILE" 2>/dev/null || echo 0) - printf " %-20s Datei: %s | Größe: %s\n" "" "$OUTPUT_FILE" "$(format_size "$total_size")" - fi - else - printf " %-20s PID: %-10s [%s]\n" "$job_id" "$pid" "$mode" - fi - echo "" - else - rm -f "$pid_file" - rm -f "${PID_DIR}/${job_id}.info" 2>/dev/null || true - fi - done - - if ! $found; then - echo " Keine aktiven Aufnahmen." - fi - - echo "" - - if systemctl is-active --quiet stream-recorder.timer 2>/dev/null; then - echo "Scheduler: aktiv (systemd-Timer)" - else - echo "Scheduler: inaktiv" - echo " Aktivieren: sudo install/install.sh" - fi -} - -stop_recording() { - local name="$1" - - local job_id="${name%.job}" - local pid_file="${PID_DIR}/${job_id}.pid" - - if [[ ! -f "$pid_file" ]]; then - log "WARN" "Keine aktive Aufnahme gefunden: ${job_id}" - return 1 - fi - - local pid - pid=$(cat "$pid_file") - - rm -f "$pid_file" - rm -f "${PID_DIR}/${job_id}.scheduled" 2>/dev/null || true - - if kill -0 "$pid" 2>/dev/null; then - kill -INT "$pid" - log "INFO" "Stop-Signal gesendet: ${job_id} (PID: ${pid})" - - local i=0 - while [[ $i -lt 10 ]] && kill -0 "$pid" 2>/dev/null; do - sleep 1 - i=$((i + 1)) - done - - if kill -0 "$pid" 2>/dev/null; then - kill -KILL "$pid" 2>/dev/null || true - log "WARN" "Prozess musste erzwungen beendet werden: ${job_id} (PID: ${pid})" - fi - else - log "WARN" "Prozess nicht mehr aktiv: ${job_id} (PID: ${pid})" - fi -} - -stop_all() { - local found=false - - for pid_file in "${PID_DIR}"/*.pid; do - [[ -f "$pid_file" ]] || continue - found=true - local job_id - job_id=$(basename "$pid_file" .pid) - stop_recording "$job_id" - done - - if ! $found; then - echo "Keine aktiven Aufnahmen zum Stoppen." - fi -} - -select_job() { - local -a job_files=() - local -a job_names=() - local -a job_urls=() - local -a job_stati=() - - for job_file in "${JOBS_DIR}"/*.job; do - [[ -f "$job_file" ]] || continue - load_job "$job_file" 2>/dev/null || continue - - local job_id - job_id=$(basename "$job_file" .job) - local status="bereit" - if is_recording "$job_id"; then - status="läuft" - fi - - job_files+=("$job_file") - job_names+=("$STREAM_NAME") - job_urls+=("$STREAM_URL") - job_stati+=("$status") - done - - if [[ ${#job_files[@]} -eq 0 ]]; then - echo "Keine .job-Dateien gefunden in: ${JOBS_DIR}" - echo "Erstelle einen Job: $(basename "$0") create" - return 1 - fi - - echo "" - echo "Verfügbare Jobs:" - echo "$(printf '=%.0s' {1..50})" - echo "" - - local i - for (( i=0; i < ${#job_files[@]}; i++ )); do - local num=$((i + 1)) - local marker=" " - [[ "${job_stati[$i]}" == "läuft" ]] && marker="* " - printf " %s%d) %-25s [%s]\n" "$marker" "$num" "${job_names[$i]}" "${job_stati[$i]}" - done - - echo "" - read -rp "Job auswählen (1-${#job_files[@]}): " choice - - if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 || "$choice" -gt ${#job_files[@]} ]]; then - echo "Ungültige Auswahl." - return 1 - fi - - local idx=$((choice - 1)) - echo "${job_files[$idx]}" -} - -watch_logs() { - local log_file="${LOG_DIR}/stream-recorder.log" - - if [[ ! -f "$log_file" ]]; then - echo "Noch keine Log-Datei vorhanden: ${log_file}" - echo "Starte eine Aufnahme, um Logs zu erzeugen." - return 1 - fi - - echo "Log-Datei: ${log_file}" - echo "Beenden mit Strg+C" - echo "$(printf '=%.0s' {1..50})" - tail -f "$log_file" -} - -# ============================================= -# Hilfe & Hauptprogramm -# ============================================= - -usage() { - cat < [Argumente] - -Befehle: - record [job] Einzelnen Stream aufzeichnen (ohne Angabe: Auswahl) - record-all Alle Jobs aufzeichnen - create Neuen Job interaktiv erstellen - quick Interaktive Schnellaufnahme (URL eingeben) - upload Vorhandene Datei nach Plik hochladen - stop [name] Aufnahme stoppen (ohne Angabe: Auswahl) - stop-all Alle Aufnahmen stoppen - list Jobs und deren Zeitpläne anzeigen - status Laufende Aufnahmen mit Details anzeigen - scheduler Zeitplanung prüfen und Jobs starten/stoppen - logs Log-Ausgabe live verfolgen - -Optionen: - -c, --config Alternative Konfigurationsdatei - -V, --verbose Ausführliche Ausgabe - --foreground Aufnahme im Vordergrund ausführen - -h, --help Diese Hilfe - -v, --version Version - -Segment-Splitting: - Aufnahmen können in Segmente (z.B. 1h, 2h) geteilt werden. - Jedes Segment wird einzeln nach Plik hochgeladen. - Konfiguration: SEGMENT_DURATION in der Job-Datei. - -Zeitplanung: - Aufnahmen werden über die Job-Dateien geplant (SCHEDULE_*). - Der Scheduler wird automatisch per systemd-Timer ausgeführt. - Einrichtung: sudo install/install.sh - -Beispiele: - $(basename "$0") record mein-stream - $(basename "$0") quick - $(basename "$0") -V record mein-stream - $(basename "$0") status - $(basename "$0") stop mein-stream - $(basename "$0") list - $(basename "$0") logs - -Dokumentation: Siehe README.md -EOF -} - -main() { - while [[ $# -gt 0 ]]; do - case "$1" in - -c|--config) CONFIG_FILE="$2"; shift 2 ;; - -V|--verbose) VERBOSE=true; shift ;; - --foreground) FOREGROUND=true; shift ;; - -h|--help) usage; exit 0 ;; - -v|--version) echo "stream-recorder v${VERSION}"; exit 0 ;; - *) break ;; - esac - done - - load_config - check_dependencies - - local command="${1:-}" - shift 2>/dev/null || true - - case "$command" in - record) - local job_file - if [[ -z "${1:-}" ]]; then - job_file=$(select_job) || exit 1 - else - job_file=$(resolve_job_path "$1") - fi - if [[ "$FOREGROUND" == "true" || "$VERBOSE" == "true" ]]; then - record_stream "$job_file" - else - start_detached_recording "$job_file" - fi - ;; - record-run) - if [[ -z "${1:-}" ]]; then - log "ERROR" "Job-Datei oder Job-Name erforderlich" - echo ""; usage; exit 1 - fi - local job_file - job_file=$(resolve_job_path "$1") - record_stream "$job_file" - ;; - record-all) record_all ;; - create) create_job ;; - quick) interactive_record ;; - upload) - if [[ -z "${1:-}" ]]; then - log "ERROR" "Dateipfad erforderlich"; exit 1 - fi - PLIK_ENABLED=true upload_to_plik "$1" "${2:-$(basename "$1")}" - ;; - scheduler) run_scheduler ;; - stop) - if [[ -z "${1:-}" ]]; then - local job_file - job_file=$(select_job) || exit 1 - stop_recording "$(basename "$job_file" .job)" - else - stop_recording "$1" - fi - ;; - stop-all) stop_all ;; - list) list_jobs ;; - status) show_status ;; - logs) watch_logs ;; - "") usage; exit 1 ;; - *) - log "ERROR" "Unbekannter Befehl: ${command}" - echo ""; usage; exit 1 - ;; - esac -} - -main "$@"