From 808d34a244faffcbcaa28be320707dd42c4eae8c Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Sun, 6 Sep 2026 13:46:50 +0200 Subject: [PATCH] Release: v2.3.0 --- README.md | 355 ++++++++- config/stream-recorder.conf | 59 ++ install/install.sh | 93 +++ install/uninstall.sh | 61 ++ jobs/templates/example-hls.job.dist | 40 + jobs/templates/example-mp3.job.dist | 41 + jobs/templates/example-rtmp.job.dist | 57 ++ stream-recorder.sh | 1068 ++++++++++++++++++++++++++ 8 files changed, 1766 insertions(+), 8 deletions(-) create mode 100644 config/stream-recorder.conf create mode 100644 install/install.sh create mode 100644 install/uninstall.sh create mode 100644 jobs/templates/example-hls.job.dist create mode 100644 jobs/templates/example-mp3.job.dist create mode 100644 jobs/templates/example-rtmp.job.dist create mode 100644 stream-recorder.sh diff --git a/README.md b/README.md index 7bae320..4fc2fd4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,351 @@ -# RTMP Stream Recorder für die Bash +

+ + Techniverse Community + +

-Dies Skript ermöglicht die lokale Aufzeichnung von RTMP-Streams und speichert diese als *.mp4-Dateien ab. Das Skript überprüft zunächst, ob das Programm "ffmpeg" installiert ist und installiert es gegebenenfalls. Es erstellt dann ein Downloadverzeichnis und bietet dem Benutzer die Möglichkeit, zwischen verschiedenen Streams auszuwählen. Bei Auswahl eines Streams wird die Aufzeichnung gestartet und im Downloadverzeichnis gespeichert. Das Skript enthält auch eine Funktion zum Abbrechen der Aufzeichnung und bietet die Option, die zuletzt erstellte Datei zu löschen. +

stream-recorder

-Das Skript bietet die folgenden Funktionen: +

+ Generischer Stream-Recorder für RTMP, HLS, HTTP-Audio und weitere Formate +

-- Auswahl Streams (oder Eingabe einer benutzerdefinierten Stream-URL und Stream-Namen) -- Aufzeichnung des ausgewählten Streams und Speicherung im angegebenen Downloadverzeichnis -- Überprüfung und Installation von "ffmpeg", wenn es nicht vorhanden ist -- Abbruch der Aufzeichnung per Interrupt (INT-Signal) mit der Möglichkeit, die zuletzt erstellte Datei zu löschen +
+ 🏰 Website + · + 📰 Community + · + 🐘 Mastodon + · + 💬 Support +
+

-Aufruf: ./bash erde-stream-recorder.sh +## Beschreibung + +**stream-recorder** ist ein Bash-basiertes Tool zur automatisierten Aufzeichnung von Streams. Es unterstützt verschiedene Stream-Typen und kann sowohl interaktiv als auch vollautomatisch genutzt werden. + +Jeder Stream wird als eigene Job-Datei definiert. Aufnahmezeiten werden direkt in der Job-Datei festgelegt - kein Cron-Wissen nötig. Ein systemd-Timer übernimmt die Zeitsteuerung automatisch. + +## Funktionen + +- **Mehrere Stream-Typen:** RTMP, RTMPS, HLS (m3u8), MP3, AAC, OGG, FLAC und generische HTTP-Streams +- **Job-basiert:** Ein Stream = eine Job-Datei, inklusive Zeitplanung +- **Automatische Zeitplanung:** Aufnahmezeiten direkt in der Job-Datei konfigurieren +- **Schnellaufnahme:** Interaktiver Modus für spontane Aufnahmen (`quick`) +- **Automatische Erkennung:** Stream-Typ und Dateiformat werden anhand der URL erkannt +- **Auto-Reconnect:** Automatische Wiederverbindung bei Verbindungsabbrüchen +- **NTFY-Benachrichtigungen:** Optionale Push-Nachrichten bei Start, Stop und Fehlern +- **Logging:** Strukturiertes Logging mit konfigurierbarem Log-Level und Live-Ansicht +- **Prozessverwaltung:** Start, Stop und Status einzelner oder aller Aufnahmen +- **Einfache Installation:** Install/Uninstall-Scripts für den systemd-Service + +## Voraussetzungen + +- **Betriebssystem:** Linux (getestet unter Ubuntu/Debian) +- **Bash:** Version 4.0 oder neuer +- **ffmpeg:** Muss installiert sein (`sudo apt install ffmpeg`) +- **curl:** Optional, nur für NTFY-Benachrichtigungen (`sudo apt install curl`) + +## Installation + +```bash +# Repository klonen +git clone https://git.techniverse.net/scriptos/stream-recorder.git /opt/stream-recorder +cd /opt/stream-recorder + +# Konfiguration anpassen +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) und richtet den systemd-Timer ein, der jede Minute die Zeitpläne prüft. + +### Deinstallation + +```bash +sudo install/uninstall.sh +``` + +Entfernt den systemd-Timer. Job-Dateien, Konfiguration und Aufnahmen bleiben erhalten. + +## Konfiguration + +Die Konfigurationsdatei liegt unter `config/stream-recorder.conf`: + +| 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 | +| `MAX_RETRIES` | `0` | Wiederholungsversuche (0 = unbegrenzt)| +| `RETRY_DELAY` | `5` | Wartezeit zwischen Versuchen (Sek.) | +| `NTFY_URL` | (leer) | NTFY Server-URL inkl. Topic | +| `NTFY_TOKEN` | (leer) | NTFY Access-Token (optional) | +| `NTFY_EVENTS` | `error` | Benachrichtigungs-Events (s. unten) | +| `PLIK_URL` | (leer) | Plik Server-URL | +| `PLIK_API_KEY` | (leer) | Plik API-Key (optional) | +| `PLIK_TTL` | `30d` | Gültigkeitsdauer der Uploads | + +## 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, Dauer, Zeitplan und Plik-Upload und erstellt die Job-Datei automatisch. + +**Manuell (aus Vorlage):** + +```bash +# Vorlage kopieren +cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job + +# Anpassen +nano jobs/mein-stream.job +``` + +### 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 | Maximale Dauer in Sekunden (leer = unbegrenzt) | +| `EXTRA_FFMPEG_ARGS`| Nein | Zusätzliche ffmpeg-Parameter | +| `PLIK_ENABLED` | Nein | Plik-Upload aktivieren (`true`/`false`) | + +### Zeitplan-Parameter + +| Parameter | Standard | Beschreibung | +|--------------------|----------|-------------------------------------------------| +| `SCHEDULE_ENABLED` | `false` | Zeitplan aktivieren (`true`/`false`) | +| `SCHEDULE_DAYS` | `*` | Aufnahmetage (siehe Formate unten) | +| `SCHEDULE_START` | (leer) | Startzeit im Format `HH:MM` | +| `SCHEDULE_STOP` | (leer) | Endzeit im Format `HH:MM` (leer = kein Auto-Stop)| + +**Tage-Formate:** + +| 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). + +### Beispiel: Tägliche RTMP-Aufnahme von 20:00 bis 22:00 + +```bash +STREAM_NAME="Abend-Livestream" +STREAM_URL="rtmp://example.com/live/stream-key" +SCHEDULE_ENABLED=true +SCHEDULE_DAYS="*" +SCHEDULE_START="20:00" +SCHEDULE_STOP="22:00" +``` + +### Beispiel: Radio-Mitschnitt werktags um 18:00, 1 Stunde + +```bash +STREAM_NAME="Mein-Radiosender" +STREAM_URL="https://example.com/stream.mp3" +MAX_DURATION="3600" +SCHEDULE_ENABLED=true +SCHEDULE_DAYS="Mo-Fr" +SCHEDULE_START="18:00" +``` + +## Verwendung + +### Befehle + +```bash +# Einzelnen Stream aufzeichnen +./stream-recorder.sh record mein-stream + +# Alle Jobs gleichzeitig aufzeichnen +./stream-recorder.sh record-all + +# Neuen Job interaktiv erstellen +./stream-recorder.sh create + +# Interaktive Schnellaufnahme (URL direkt eingeben) +./stream-recorder.sh quick + +# Aufnahme mit ausführlicher Ausgabe +./stream-recorder.sh -V record mein-stream + +# Verfügbare Jobs und Zeitpläne anzeigen +./stream-recorder.sh list + +# Laufende Aufnahmen und Scheduler-Status anzeigen +./stream-recorder.sh status + +# Log-Ausgabe live verfolgen +./stream-recorder.sh logs + +# Einzelne Aufnahme stoppen +./stream-recorder.sh stop mein-stream + +# Alle Aufnahmen stoppen +./stream-recorder.sh stop-all +``` + +### Schnellaufnahme + +Mit dem `quick`-Befehl kann ein beliebiger Stream ohne Job-Datei aufgezeichnet werden: + +```bash +./stream-recorder.sh quick +``` + +Das Script fragt interaktiv nach URL, Name, Typ und Dauer. Die Dauer kann in verschiedenen Formaten angegeben werden: `3600` (Sekunden), `1h`, `30m`, `1h30m`. + +### Job-Angabe + +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 +``` + +## Zeitplanung + +Die Zeitplanung wird direkt in der Job-Datei konfiguriert - kein Cron nötig. + +### So funktioniert's + +1. **Job-Datei erstellen** und `SCHEDULE_*`-Parameter setzen +2. **Scheduler installieren** mit `sudo install/install.sh` (einmalig) +3. Der systemd-Timer prüft **jede Minute** alle Jobs und startet/stoppt Aufnahmen automatisch + +### 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 +- **Manuell gestartete Aufnahmen** werden vom Scheduler nicht beeinflusst +- **Nach Neustart** des Servers erkennt der Scheduler automatisch, welche Aufnahmen laufen sollten + +### NTFY-Benachrichtigungen + +Push-Nachrichten via [ntfy](https://ntfy.sh) (selbst gehostet oder öffentlich). In der Konfigurationsdatei: + +```bash +NTFY_URL="https://ntfy.sh/mein-stream-topic" +NTFY_TOKEN="" # Optional, nur bei geschützten Topics +NTFY_EVENTS="start,stop,error" # Kommasepariert: start, stop, error +``` + +| Event | Auslöser | Priorität | +|---------|-----------------------------------------------------|-----------| +| `start` | Aufnahme wurde gestartet | Normal | +| `stop` | Aufnahme wurde beendet (regulär oder manuell) | Normal | +| `error` | Verbindung verloren oder max. Versuche erreicht | Hoch | +| `upload` | Datei wurde auf Plik hochgeladen (mit Download-Link)| Normal | + +## Auto-Reconnect + +Bei Verbindungsabbrüchen versucht das Script automatisch, die Verbindung wiederherzustellen: + +- `MAX_RETRIES=0` - Unbegrenzte Wiederverbindungsversuche (Standard) +- `MAX_RETRIES=5` - Maximal 5 Versuche, danach Abbruch mit NTFY-Benachrichtigung +- `RETRY_DELAY=5` - 5 Sekunden Wartezeit zwischen den Versuchen + +Bei jedem Reconnect wird eine neue Datei mit aktuellem Zeitstempel erzeugt. + +## Plik-Upload + +Fertige Aufnahmen können automatisch auf einen [Plik](https://github.com/root-gg/plik)-Server hochgeladen werden. + +### 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 +``` + +### Verhalten + +- Der Upload erfolgt automatisch nach jeder regulär abgeschlossenen Aufnahme +- Bei manuellem Stop oder Verbindungsabbruch wird kein Upload ausgeführt +- Der Download-Link wird im Log ausgegeben +- Optional wird der Link per NTFY-Benachrichtigung gesendet (Event: `upload`) + +### NTFY + Plik + +Um den Download-Link per Push-Nachricht zu erhalten: + +```bash +NTFY_EVENTS="start,stop,error,upload" +``` + +## Logging + +Logs werden nach `LOG_DIR` geschrieben (Standard: `/var/log/stream-recorder/`): + +- `stream-recorder.log` - Alle Operationen des Scripts +- `ffmpeg_.log` - ffmpeg-Ausgabe pro Job + +```bash +# Logs in Echtzeit verfolgen +./stream-recorder.sh logs + +# Aufnahme mit ausführlicher Ausgabe starten +./stream-recorder.sh -V record mein-stream +``` + +## Verzeichnisstruktur + +``` +stream-recorder/ +├── stream-recorder.sh # Hauptscript +├── config/ +│ └── stream-recorder.conf # Konfiguration +├── jobs/ # Job-Definitionen +│ └── templates/ # Vorlagen +│ ├── example-rtmp.job.dist +│ ├── example-mp3.job.dist +│ └── example-hls.job.dist +├── install/ +│ ├── install.sh # Scheduler installieren +│ └── uninstall.sh # Scheduler deinstallieren +├── README.md +└── LICENSE +``` + + +

+

+ +

+ +

+ + © Patrick Asmus · Techniverse Network · Lizenz + +

diff --git a/config/stream-recorder.conf b/config/stream-recorder.conf new file mode 100644 index 0000000..140491e --- /dev/null +++ b/config/stream-recorder.conf @@ -0,0 +1,59 @@ +############################################### +# 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" + +# 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/install/install.sh b/install/install.sh new file mode 100644 index 0000000..21a74c2 --- /dev/null +++ b/install/install.sh @@ -0,0 +1,93 @@ +#!/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" + +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. Job-Datei erstellen: cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job" +echo " 2. Job-Datei bearbeiten: nano jobs/mein-stream.job" +echo " 3. SCHEDULE_ENABLED=true setzen und Zeiten konfigurieren" +echo " 4. Jobs prüfen: ./stream-recorder.sh list" +echo "" +echo "Deinstallation: sudo install/uninstall.sh" diff --git a/install/uninstall.sh b/install/uninstall.sh new file mode 100644 index 0000000..3a4d156 --- /dev/null +++ b/install/uninstall.sh @@ -0,0 +1,61 @@ +#!/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 new file mode 100644 index 0000000..7a03192 --- /dev/null +++ b/jobs/templates/example-hls.job.dist @@ -0,0 +1,40 @@ +############################################### +# 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 in Sekunden +MAX_DURATION="" + +# Zusätzliche ffmpeg-Argumente (optional) +EXTRA_FFMPEG_ARGS="" + +############################################### +# Zeitplanung (optional) +############################################### + +SCHEDULE_ENABLED=false +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 new file mode 100644 index 0000000..0f5df56 --- /dev/null +++ b/jobs/templates/example-mp3.job.dist @@ -0,0 +1,41 @@ +############################################### +# 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 in Sekunden (leer = unbegrenzt) +# Für Radio-Mitschnitte empfohlen, z.B. 3600 für 1 Stunde +MAX_DURATION="3600" + +# Zusätzliche ffmpeg-Argumente (optional) +EXTRA_FFMPEG_ARGS="" + +############################################### +# Zeitplanung (optional) +############################################### + +SCHEDULE_ENABLED=false +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 new file mode 100644 index 0000000..bbad15d --- /dev/null +++ b/jobs/templates/example-rtmp.job.dist @@ -0,0 +1,57 @@ +############################################### +# 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 in Sekunden (leer = unbegrenzt) +# Beispiel: 3600 = 1 Stunde, 7200 = 2 Stunden +MAX_DURATION="" + +# Zusätzliche ffmpeg-Argumente (optional) +EXTRA_FFMPEG_ARGS="" + +############################################### +# Zeitplanung (optional) +# Damit der Scheduler funktioniert, muss er +# installiert sein: sudo install/install.sh +############################################### + +# Zeitplan aktivieren (true/false) +SCHEDULE_ENABLED=false + +# Aufnahmetage +# * = 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 new file mode 100644 index 0000000..13dbd39 --- /dev/null +++ b/stream-recorder.sh @@ -0,0 +1,1068 @@ +#!/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.3.0 +# Date: 06.09.2026 +# Modifications: Grundlegende Überarbeitung + viele neue Funktionen implementiert +############################################### + +set -uo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly VERSION="2.3.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" +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 + +# ============================================= +# 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 +} + +upload_to_plik() { + local file="$1" + local stream_name="$2" + + [[ "${PLIK_ENABLED:-false}" != "true" ]] && return + [[ -z "${PLIK_URL:-}" ]] && return + [[ ! -f "$file" ]] && return + 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=(-s --max-time 300) + curl_args+=(-X POST) + + 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 + + log "INFO" "Plik-Upload: ${filename} -> ${plik_base}" + + local create_json="{\"files\":[{\"fileName\":\"${filename}\"}]" + if [[ -n "$ttl_seconds" ]]; then + create_json+=",\"ttl\":${ttl_seconds}" + fi + create_json+="}" + + local create_response + create_response=$(curl "${curl_args[@]}" \ + -H "Content-Type: application/json" \ + -d "$create_json" \ + "${plik_base}/upload" 2>/dev/null) + + if [[ -z "$create_response" ]]; then + log "ERROR" "Plik-Upload fehlgeschlagen: Keine Antwort vom Server" + return 1 + fi + + local upload_id file_id + upload_id=$(echo "$create_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + file_id=$(echo "$create_response" | grep -o '"id":"[^"]*"' | tail -1 | cut -d'"' -f4) + + if [[ -z "$upload_id" || -z "$file_id" ]]; then + log "ERROR" "Plik-Upload fehlgeschlagen: Upload konnte nicht erstellt werden" + log "DEBUG" "Plik-Antwort: ${create_response}" + return 1 + fi + + local -a upload_curl_args=(-s --max-time 600) + if [[ -n "${PLIK_API_KEY:-}" ]]; then + upload_curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}") + fi + + local upload_response + upload_response=$(curl "${upload_curl_args[@]}" \ + -X POST \ + -T "$file" \ + "${plik_base}/file/${upload_id}/${file_id}/${filename}" 2>/dev/null) + + local download_url="${plik_base}/file/${upload_id}/${file_id}/${filename}" + log "INFO" "Plik-Upload abgeschlossen: ${download_url}" + notify "upload" "Plik-Upload" "${stream_name}: ${download_url}" + + echo "$download_url" +} + +parse_duration() { + local input="$1" + [[ -z "$input" ]] && { echo ""; return; } + [[ "$input" =~ ^[0-9]+$ ]] && { echo "$input"; return; } + + local total=0 + [[ "$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" +} + +# --- 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 + log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte" + fi + mkdir -p "$DOWNLOAD_PATH" "$LOG_DIR" "$PID_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_DAYS="*" + SCHEDULE_START="" + SCHEDULE_STOP="" + PLIK_ENABLED="false" + + 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 +} + +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 +} + +# ============================================= +# 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 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 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 + + log "INFO" "Starte Aufnahme: ${STREAM_NAME} [${stream_type}] -> ${output_file}" + notify "start" "Aufnahme gestartet" "${STREAM_NAME} [${stream_type}]" + + 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 + + [[ -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" + _cleanup() { + 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 + } + trap _cleanup EXIT TERM INT + + 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" + + local ret=0 + wait "$_SR_FFMPEG_PID" || ret=$? + _SR_FFMPEG_PID="" + + if [[ $ret -eq 0 ]]; then + log "INFO" "Aufnahme abgeschlossen: ${output_file}" + notify "stop" "Aufnahme abgeschlossen" "${STREAM_NAME}: ${output_file}" + upload_to_plik "$output_file" "$STREAM_NAME" + break + fi + + if [[ $ret -eq 143 || $ret -eq 130 ]]; then + log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}" + notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt" + break + fi + + if [[ ! -f "$pid_file" ]]; then + log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}" + notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt" + 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" + sleep "$RETRY_DELAY" + + timestamp=$(date +'%Y%m%d_%H%M%S') + output_file="${output_dir}/${safe_name}_${timestamp}.${ext}" + ffmpeg_args[-1]="$output_file" + done + + trap - EXIT TERM INT + rm -f "$pid_file" + rm -f "${pid_file%.pid}.scheduled" 2>/dev/null || true + + if [[ -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 "$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" +} + +# ============================================= +# Scheduler (wird per systemd-Timer aufgerufen) +# ============================================= + +run_scheduler() { + local today_num now_time + today_num=$(date '+%u') + now_time=$(date '+%H:%M') + + log "DEBUG" "Scheduler: 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 + _matches_schedule_day "$today_num" "${SCHEDULE_DAYS:-*}" && is_today=true + + 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 + if ! is_recording "$job_id"; then + log "INFO" "Scheduler: Starte '${STREAM_NAME}' [${job_id}]" + notify "start" "Geplante Aufnahme" "${STREAM_NAME} - gestartet durch Scheduler" + touch "${PID_DIR}/${job_id}.scheduled" + nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record "$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:-}" ]] || ! $is_today; 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 + extra_info="Zeitplan: ${SCHEDULE_DAYS:-*} ${SCHEDULE_START}" + if [[ -n "${SCHEDULE_STOP:-}" ]]; then + extra_info+=" - ${SCHEDULE_STOP}" + elif [[ -n "${MAX_DURATION:-}" ]]; then + extra_info+=" (Dauer: ${MAX_DURATION}s)" + 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..50})" + 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" + printf " %-20s PID: %-10s [%s]\n" "$job_id" "$pid" "$mode" + else + rm -f "$pid_file" + 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 +} + +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 Einzelnen Stream aufzeichnen + record-all Alle Jobs aufzeichnen + create Neuen Job interaktiv erstellen + quick Interaktive Schnellaufnahme (URL eingeben) + stop Aufnahme stoppen + stop-all Alle Aufnahmen stoppen + list Jobs und deren Zeitpläne anzeigen + status Laufende Aufnahmen und Scheduler-Status + scheduler Zeitplanung prüfen und Jobs starten/stoppen + logs Log-Ausgabe live verfolgen + +Optionen: + -c, --config Alternative Konfigurationsdatei + -V, --verbose Ausführliche Ausgabe + -h, --help Diese Hilfe + -v, --version Version + +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") 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 ;; + -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) + 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 ;; + scheduler) run_scheduler ;; + stop) + if [[ -z "${1:-}" ]]; then + log "ERROR" "Job-Name erforderlich"; exit 1 + fi + stop_recording "$1" + ;; + 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 "$@"