Release: v2.4.0

This commit is contained in:
Patrick Asmus
2026-09-06 16:58:37 +02:00
parent 808d34a244
commit 2732427721
7 changed files with 301 additions and 66 deletions
+32 -4
View File
@@ -81,6 +81,7 @@ Die Konfigurationsdatei liegt unter `config/stream-recorder.conf`:
| `LOG_DIR` | `/var/log/stream-recorder` | Log-Verzeichnis | | `LOG_DIR` | `/var/log/stream-recorder` | Log-Verzeichnis |
| `LOG_LEVEL` | `INFO` | Log-Level: DEBUG, INFO, WARN, ERROR | | `LOG_LEVEL` | `INFO` | Log-Level: DEBUG, INFO, WARN, ERROR |
| `PID_DIR` | `/tmp/stream-recorder` | PID-Dateien für laufende Aufnahmen | | `PID_DIR` | `/tmp/stream-recorder` | PID-Dateien für laufende Aufnahmen |
| `STATE_DIR` | `/var/lib/stream-recorder` | Status für bereits gestartete Zeitpläne |
| `MAX_RETRIES` | `0` | Wiederholungsversuche (0 = unbegrenzt)| | `MAX_RETRIES` | `0` | Wiederholungsversuche (0 = unbegrenzt)|
| `RETRY_DELAY` | `5` | Wartezeit zwischen Versuchen (Sek.) | | `RETRY_DELAY` | `5` | Wartezeit zwischen Versuchen (Sek.) |
| `NTFY_URL` | (leer) | NTFY Server-URL inkl. Topic | | `NTFY_URL` | (leer) | NTFY Server-URL inkl. Topic |
@@ -122,7 +123,7 @@ nano jobs/mein-stream.job
| `STREAM_URL` | Ja | URL des Streams | | `STREAM_URL` | Ja | URL des Streams |
| `STREAM_TYPE` | Nein | `auto`, `rtmp`, `hls`, `mp3`, `aac`, `ogg`, `flac` | | `STREAM_TYPE` | Nein | `auto`, `rtmp`, `hls`, `mp3`, `aac`, `ogg`, `flac` |
| `OUTPUT_FORMAT` | Nein | Dateiendung (z.B. `mp4`, `mp3`) - sonst automatisch | | `OUTPUT_FORMAT` | Nein | Dateiendung (z.B. `mp4`, `mp3`) - sonst automatisch |
| `MAX_DURATION` | Nein | Maximale Dauer in Sekunden (leer = unbegrenzt) | | `MAX_DURATION` | Nein | Maximale Dauer, z.B. `300`, `5m`, `1h30m` (leer = unbegrenzt) |
| `EXTRA_FFMPEG_ARGS`| Nein | Zusätzliche ffmpeg-Parameter | | `EXTRA_FFMPEG_ARGS`| Nein | Zusätzliche ffmpeg-Parameter |
| `PLIK_ENABLED` | Nein | Plik-Upload aktivieren (`true`/`false`) | | `PLIK_ENABLED` | Nein | Plik-Upload aktivieren (`true`/`false`) |
@@ -131,7 +132,9 @@ nano jobs/mein-stream.job
| Parameter | Standard | Beschreibung | | Parameter | Standard | Beschreibung |
|--------------------|----------|-------------------------------------------------| |--------------------|----------|-------------------------------------------------|
| `SCHEDULE_ENABLED` | `false` | Zeitplan aktivieren (`true`/`false`) | | `SCHEDULE_ENABLED` | `false` | Zeitplan aktivieren (`true`/`false`) |
| `SCHEDULE_DAYS` | `*` | Aufnahmetage (siehe Formate unten) | | `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_START` | (leer) | Startzeit im Format `HH:MM` |
| `SCHEDULE_STOP` | (leer) | Endzeit im Format `HH:MM` (leer = kein Auto-Stop)| | `SCHEDULE_STOP` | (leer) | Endzeit im Format `HH:MM` (leer = kein Auto-Stop)|
@@ -147,6 +150,8 @@ nano jobs/mein-stream.job
Die Tagesangaben funktionieren auf Deutsch (Mo, Di, Mi, Do, Fr, Sa, So) und Englisch (Mon, Tue, Wed, Thu, Fri, Sat, Sun). Die Tagesangaben funktionieren auf Deutsch (Mo, Di, Mi, Do, Fr, Sa, So) und Englisch (Mon, Tue, Wed, Thu, Fri, Sat, Sun).
Ohne `SCHEDULE_ONCE=true` sind Tagesangaben wöchentlich/wiederkehrend. Ein Job mit `SCHEDULE_DAYS="Mi"` und `SCHEDULE_START="21:00"` läuft also jeden Mittwoch.
### Beispiel: Tägliche RTMP-Aufnahme von 20:00 bis 22:00 ### Beispiel: Tägliche RTMP-Aufnahme von 20:00 bis 22:00
```bash ```bash
@@ -163,12 +168,26 @@ SCHEDULE_STOP="22:00"
```bash ```bash
STREAM_NAME="Mein-Radiosender" STREAM_NAME="Mein-Radiosender"
STREAM_URL="https://example.com/stream.mp3" STREAM_URL="https://example.com/stream.mp3"
MAX_DURATION="3600" MAX_DURATION="1h"
SCHEDULE_ENABLED=true SCHEDULE_ENABLED=true
SCHEDULE_ONCE=false
SCHEDULE_DAYS="Mo-Fr" SCHEDULE_DAYS="Mo-Fr"
SCHEDULE_START="18:00" SCHEDULE_START="18:00"
``` ```
### Beispiel: Einmalige Aufnahme am 09.09.2026 um 21:00, 30 Minuten
```bash
STREAM_NAME="Einmaliger-Mitschnitt"
STREAM_URL="https://example.com/stream.mp3"
MAX_DURATION="30m"
SCHEDULE_ENABLED=true
SCHEDULE_ONCE=true
SCHEDULE_DATE="2026-09-09"
SCHEDULE_START="21:00"
SCHEDULE_STOP=""
```
## Verwendung ## Verwendung
### Befehle ### Befehle
@@ -177,6 +196,9 @@ SCHEDULE_START="18:00"
# Einzelnen Stream aufzeichnen # Einzelnen Stream aufzeichnen
./stream-recorder.sh record mein-stream ./stream-recorder.sh record mein-stream
# Im Vordergrund aufzeichnen, z.B. für Tests
./stream-recorder.sh --foreground record mein-stream
# Alle Jobs gleichzeitig aufzeichnen # Alle Jobs gleichzeitig aufzeichnen
./stream-recorder.sh record-all ./stream-recorder.sh record-all
@@ -186,6 +208,9 @@ SCHEDULE_START="18:00"
# Interaktive Schnellaufnahme (URL direkt eingeben) # Interaktive Schnellaufnahme (URL direkt eingeben)
./stream-recorder.sh quick ./stream-recorder.sh quick
# Vorhandene Aufnahme erneut nach Plik hochladen
./stream-recorder.sh upload /pfad/zur/aufnahme.mp4 "Stream-Name"
# Aufnahme mit ausführlicher Ausgabe # Aufnahme mit ausführlicher Ausgabe
./stream-recorder.sh -V record mein-stream ./stream-recorder.sh -V record mein-stream
@@ -239,6 +264,7 @@ Die Zeitplanung wird direkt in der Job-Datei konfiguriert - kein Cron nötig.
- **Start + Stop gesetzt:** Aufnahme läuft im angegebenen Zeitfenster - **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 - **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 - **Manuell gestartete Aufnahmen** werden vom Scheduler nicht beeinflusst
- **Nach Neustart** des Servers erkennt der Scheduler automatisch, welche Aufnahmen laufen sollten - **Nach Neustart** des Servers erkennt der Scheduler automatisch, welche Aufnahmen laufen sollten
@@ -293,8 +319,10 @@ PLIK_ENABLED=true
- Der Upload erfolgt automatisch nach jeder regulär abgeschlossenen Aufnahme - Der Upload erfolgt automatisch nach jeder regulär abgeschlossenen Aufnahme
- Bei manuellem Stop oder Verbindungsabbruch wird kein Upload ausgeführt - Bei manuellem Stop oder Verbindungsabbruch wird kein Upload ausgeführt
- Der Download-Link wird im Log ausgegeben - Der Browser-Link zur Plik-Upload-Seite wird im Log und per NTFY ausgegeben
- Die direkte Datei-URL wird zusätzlich ins Log geschrieben
- Optional wird der Link per NTFY-Benachrichtigung gesendet (Event: `upload`) - Optional wird der Link per NTFY-Benachrichtigung gesendet (Event: `upload`)
- Plik-Uploads verwenden den direkten Multipart-Upload-Endpunkt des Servers.
### NTFY + Plik ### NTFY + Plik
+3
View File
@@ -15,6 +15,9 @@ LOG_LEVEL="INFO"
# PID-Verzeichnis für laufende Aufnahmen # PID-Verzeichnis für laufende Aufnahmen
PID_DIR="/tmp/stream-recorder" PID_DIR="/tmp/stream-recorder"
# Persistentes Status-Verzeichnis für geplante Läufe
STATE_DIR="/var/lib/stream-recorder"
# Anzahl Wiederholungsversuche bei Verbindungsabbruch # Anzahl Wiederholungsversuche bei Verbindungsabbruch
# 0 = unbegrenzt (empfohlen für Langzeitaufnahmen) # 0 = unbegrenzt (empfohlen für Langzeitaufnahmen)
MAX_RETRIES=5 MAX_RETRIES=5
+27
View File
@@ -0,0 +1,27 @@
###############################################
# stream-recorder - Job: Euer-Radio.DE-Main 30min
###############################################
STREAM_NAME="Euer-Radio.DE-Main-30min"
STREAM_URL="rtmp://rtmp23.euer.tv/live/1080p"
STREAM_TYPE="rtmp"
OUTPUT_FORMAT=""
MAX_DURATION="30m"
EXTRA_FFMPEG_ARGS=""
###############################################
# Zeitplanung (optional)
###############################################
SCHEDULE_ENABLED=false
SCHEDULE_ONCE=false
SCHEDULE_DATE=""
SCHEDULE_DAYS="*"
SCHEDULE_START=""
SCHEDULE_STOP=""
###############################################
# Plik-Upload (optional)
###############################################
PLIK_ENABLED=true
+3 -1
View File
@@ -16,7 +16,7 @@ STREAM_TYPE="hls"
# Ausgabeformat (mp4 oder mkv empfohlen) # Ausgabeformat (mp4 oder mkv empfohlen)
OUTPUT_FORMAT="mp4" OUTPUT_FORMAT="mp4"
# Maximale Aufnahmedauer in Sekunden # Maximale Aufnahmedauer (leer = unbegrenzt, z.B. 5m oder 1h)
MAX_DURATION="" MAX_DURATION=""
# Zusätzliche ffmpeg-Argumente (optional) # Zusätzliche ffmpeg-Argumente (optional)
@@ -27,6 +27,8 @@ EXTRA_FFMPEG_ARGS=""
############################################### ###############################################
SCHEDULE_ENABLED=false SCHEDULE_ENABLED=false
SCHEDULE_ONCE=false
SCHEDULE_DATE=""
SCHEDULE_DAYS="*" SCHEDULE_DAYS="*"
SCHEDULE_START="20:00" SCHEDULE_START="20:00"
SCHEDULE_STOP="22:00" SCHEDULE_STOP="22:00"
+5 -3
View File
@@ -16,9 +16,9 @@ STREAM_TYPE="auto"
# Ausgabeformat # Ausgabeformat
OUTPUT_FORMAT="" OUTPUT_FORMAT=""
# Maximale Aufnahmedauer in Sekunden (leer = unbegrenzt) # Maximale Aufnahmedauer (leer = unbegrenzt)
# Für Radio-Mitschnitte empfohlen, z.B. 3600 für 1 Stunde # Für Radio-Mitschnitte empfohlen, z.B. 1h für 1 Stunde
MAX_DURATION="3600" MAX_DURATION="1h"
# Zusätzliche ffmpeg-Argumente (optional) # Zusätzliche ffmpeg-Argumente (optional)
EXTRA_FFMPEG_ARGS="" EXTRA_FFMPEG_ARGS=""
@@ -28,6 +28,8 @@ EXTRA_FFMPEG_ARGS=""
############################################### ###############################################
SCHEDULE_ENABLED=false SCHEDULE_ENABLED=false
SCHEDULE_ONCE=false
SCHEDULE_DATE=""
SCHEDULE_DAYS="Mo-Fr" SCHEDULE_DAYS="Mo-Fr"
SCHEDULE_START="18:00" SCHEDULE_START="18:00"
SCHEDULE_STOP="" SCHEDULE_STOP=""
+10 -3
View File
@@ -18,8 +18,8 @@ STREAM_TYPE="auto"
# Leer lassen für automatische Erkennung basierend auf Stream-Typ # Leer lassen für automatische Erkennung basierend auf Stream-Typ
OUTPUT_FORMAT="" OUTPUT_FORMAT=""
# Maximale Aufnahmedauer in Sekunden (leer = unbegrenzt) # Maximale Aufnahmedauer (leer = unbegrenzt)
# Beispiel: 3600 = 1 Stunde, 7200 = 2 Stunden # Beispiele: 300 = 5 Minuten, 5m = 5 Minuten, 1h30m = 90 Minuten
MAX_DURATION="" MAX_DURATION=""
# Zusätzliche ffmpeg-Argumente (optional) # Zusätzliche ffmpeg-Argumente (optional)
@@ -34,7 +34,14 @@ EXTRA_FFMPEG_ARGS=""
# Zeitplan aktivieren (true/false) # Zeitplan aktivieren (true/false)
SCHEDULE_ENABLED=false SCHEDULE_ENABLED=false
# Aufnahmetage # 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 # * = täglich
# Mo-Fr = Montag bis Freitag # Mo-Fr = Montag bis Freitag
# Sa,So = Samstag und Sonntag # Sa,So = Samstag und Sonntag
+215 -49
View File
@@ -8,15 +8,15 @@
# Web: https://www.cleveradmin.de # Web: https://www.cleveradmin.de
# Repository: https://git.techniverse.net/scriptos/stream-recorder.git # Repository: https://git.techniverse.net/scriptos/stream-recorder.git
# License: MIT # License: MIT
# Version: v2.3.0 # Version: v2.4.0
# Date: 06.09.2026 # Date: 06.09.2026
# Modifications: Grundlegende Überarbeitung + viele neue Funktionen implementiert # Modifications: Scheduler-Watchdog stabilisiert, Plik-Upload repariert, einmalige Zeitplanung ergänzt
############################################### ###############################################
set -uo pipefail set -uo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="2.3.0" readonly VERSION="2.4.0"
# --- Standardwerte (überschreibbar via Konfiguration) --- # --- Standardwerte (überschreibbar via Konfiguration) ---
CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf" CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf"
@@ -25,6 +25,7 @@ DOWNLOAD_PATH="/home/downloads/recordings"
LOG_DIR="/var/log/stream-recorder" LOG_DIR="/var/log/stream-recorder"
LOG_LEVEL="INFO" LOG_LEVEL="INFO"
PID_DIR="/tmp/stream-recorder" PID_DIR="/tmp/stream-recorder"
STATE_DIR="/var/lib/stream-recorder"
MAX_RETRIES=0 MAX_RETRIES=0
RETRY_DELAY=5 RETRY_DELAY=5
@@ -40,6 +41,7 @@ PLIK_TTL="30d"
# --- Laufzeit-Flags --- # --- Laufzeit-Flags ---
VERBOSE=false VERBOSE=false
FOREGROUND=false
# ============================================= # =============================================
# Hilfsfunktionen # Hilfsfunktionen
@@ -103,13 +105,36 @@ notify() {
disown 2>/dev/null || true 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() { upload_to_plik() {
local file="$1" local file="$1"
local stream_name="$2" local stream_name="$2"
[[ "${PLIK_ENABLED:-false}" != "true" ]] && return [[ "${PLIK_ENABLED:-false}" != "true" ]] && return
[[ -z "${PLIK_URL:-}" ]] && return [[ -z "${PLIK_URL:-}" ]] && return
[[ ! -f "$file" ]] && 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; } command -v curl &>/dev/null || { log "WARN" "curl nicht installiert - Plik-Upload übersprungen"; return; }
local filename local filename
@@ -117,8 +142,7 @@ upload_to_plik() {
local plik_base="${PLIK_URL%/}" local plik_base="${PLIK_URL%/}"
local -a curl_args=(-s --max-time 300) local -a curl_args=(-sS --max-time 7200)
curl_args+=(-X POST)
if [[ -n "${PLIK_API_KEY:-}" ]]; then if [[ -n "${PLIK_API_KEY:-}" ]]; then
curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}") curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}")
@@ -136,51 +160,44 @@ upload_to_plik() {
fi fi
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}" log "INFO" "Plik-Upload: ${filename} -> ${plik_base}"
local create_json="{\"files\":[{\"fileName\":\"${filename}\"}]" local upload_tmp upload_response http_code curl_status=0
if [[ -n "$ttl_seconds" ]]; then upload_tmp=$(mktemp)
create_json+=",\"ttl\":${ttl_seconds}" http_code=$(curl "${curl_args[@]}" \
fi -w "%{http_code}" \
create_json+="}" -o "$upload_tmp" \
"${plik_base}" 2>/dev/null) || curl_status=$?
upload_response=$(cat "$upload_tmp" 2>/dev/null || true)
rm -f "$upload_tmp"
local create_response if [[ $curl_status -ne 0 || ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
create_response=$(curl "${curl_args[@]}" \ log "ERROR" "Plik-Upload fehlgeschlagen: Datei konnte nicht hochgeladen werden (HTTP ${http_code:-000}, curl ${curl_status})"
-H "Content-Type: application/json" \ [[ -n "$upload_response" ]] && log "DEBUG" "Plik-Antwort: ${upload_response}"
-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 return 1
fi fi
local upload_id file_id local download_url
upload_id=$(echo "$create_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) download_url=$(printf '%s\n' "$upload_response" | grep -Eo 'https?://[^[:space:]]+' | tail -1)
file_id=$(echo "$create_response" | grep -o '"id":"[^"]*"' | tail -1 | cut -d'"' -f4) if [[ -z "$download_url" ]]; then
log "ERROR" "Plik-Upload fehlgeschlagen: Keine Download-URL in der Antwort"
if [[ -z "$upload_id" || -z "$file_id" ]]; then log "DEBUG" "Plik-Antwort: ${upload_response}"
log "ERROR" "Plik-Upload fehlgeschlagen: Upload konnte nicht erstellt werden"
log "DEBUG" "Plik-Antwort: ${create_response}"
return 1 return 1
fi fi
local -a upload_curl_args=(-s --max-time 600) local browser_url
if [[ -n "${PLIK_API_KEY:-}" ]]; then browser_url=$(plik_browser_url_from_download_url "$download_url")
upload_curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}")
log "INFO" "Plik-Upload abgeschlossen: ${browser_url}"
if [[ "$browser_url" != "$download_url" ]]; then
log "INFO" "Plik-Datei-URL: ${download_url}"
fi fi
notify "upload" "Plik-Upload" "${stream_name}: ${browser_url}"
local upload_response echo "$browser_url"
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() { parse_duration() {
@@ -277,7 +294,7 @@ load_config() {
else else
log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte" log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte"
fi fi
mkdir -p "$DOWNLOAD_PATH" "$LOG_DIR" "$PID_DIR" 2>/dev/null || true mkdir -p "$DOWNLOAD_PATH" "$LOG_DIR" "$PID_DIR" "$STATE_DIR" 2>/dev/null || true
} }
check_dependencies() { check_dependencies() {
@@ -330,6 +347,8 @@ load_job() {
MAX_DURATION="" MAX_DURATION=""
EXTRA_FFMPEG_ARGS="" EXTRA_FFMPEG_ARGS=""
SCHEDULE_ENABLED="false" SCHEDULE_ENABLED="false"
SCHEDULE_ONCE="false"
SCHEDULE_DATE=""
SCHEDULE_DAYS="*" SCHEDULE_DAYS="*"
SCHEDULE_START="" SCHEDULE_START=""
SCHEDULE_STOP="" SCHEDULE_STOP=""
@@ -351,6 +370,27 @@ load_job() {
log "ERROR" "STREAM_URL fehlt in: ${job_file}" log "ERROR" "STREAM_URL fehlt in: ${job_file}"
return 1 return 1
fi 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() { resolve_job_path() {
@@ -377,6 +417,44 @@ is_recording() {
return 1 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
}
# ============================================= # =============================================
# Aufnahme # Aufnahme
# ============================================= # =============================================
@@ -461,9 +539,15 @@ record_stream() {
_SR_FFMPEG_PID="" _SR_FFMPEG_PID=""
if [[ $ret -eq 0 ]]; then if [[ $ret -eq 0 ]]; then
if [[ -s "$output_file" ]]; then
log "INFO" "Aufnahme abgeschlossen: ${output_file}" log "INFO" "Aufnahme abgeschlossen: ${output_file}"
notify "stop" "Aufnahme abgeschlossen" "${STREAM_NAME}: ${output_file}" notify "stop" "Aufnahme abgeschlossen" "${STREAM_NAME}: ${output_file}"
upload_to_plik "$output_file" "$STREAM_NAME" 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 break
fi fi
@@ -519,7 +603,7 @@ record_all() {
[[ -f "$job_file" ]] || continue [[ -f "$job_file" ]] || continue
found=true found=true
log "INFO" "Starte Job im Hintergrund: $(basename "$job_file")" log "INFO" "Starte Job im Hintergrund: $(basename "$job_file")"
"$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record "$job_file" & "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" &
done done
if ! $found; then if ! $found; then
@@ -532,16 +616,43 @@ record_all() {
log "INFO" "Alle Aufnahmen beendet" 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) # Scheduler (wird per systemd-Timer aufgerufen)
# ============================================= # =============================================
run_scheduler() { run_scheduler() {
local today_num now_time local today_date today_num now_time
today_date=$(date '+%F')
today_num=$(date '+%u') today_num=$(date '+%u')
now_time=$(date '+%H:%M') now_time=$(date '+%H:%M')
log "DEBUG" "Scheduler: Tag=${today_num} Zeit=${now_time}" log "DEBUG" "Scheduler: Datum=${today_date} Tag=${today_num} Zeit=${now_time}"
for job_file in "${JOBS_DIR}"/*.job; do for job_file in "${JOBS_DIR}"/*.job; do
[[ -f "$job_file" ]] || continue [[ -f "$job_file" ]] || continue
@@ -554,7 +665,11 @@ run_scheduler() {
[[ -z "${SCHEDULE_START:-}" ]] && continue [[ -z "${SCHEDULE_START:-}" ]] && continue
local is_today=false 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 _matches_schedule_day "$today_num" "${SCHEDULE_DAYS:-*}" && is_today=true
fi
local in_window=false local in_window=false
if $is_today; then if $is_today; then
@@ -569,11 +684,20 @@ run_scheduler() {
fi fi
if $in_window; then 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 if ! is_recording "$job_id"; then
log "INFO" "Scheduler: Starte '${STREAM_NAME}' [${job_id}]" log "INFO" "Scheduler: Starte '${STREAM_NAME}' [${job_id}]"
notify "start" "Geplante Aufnahme" "${STREAM_NAME} - gestartet durch Scheduler" notify "start" "Geplante Aufnahme" "${STREAM_NAME} - gestartet durch Scheduler"
touch "${PID_DIR}/${job_id}.scheduled" mark_schedule_started "$job_id" "$occurrence_key" || continue
nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record "$job_file" \ 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 & >> "${LOG_DIR}/nohup_${job_id}.log" 2>&1 &
disown 2>/dev/null || true disown 2>/dev/null || true
fi fi
@@ -707,15 +831,25 @@ create_job() {
echo "--- Zeitplanung (optional) ---" echo "--- Zeitplanung (optional) ---"
read -rp "Zeitplan aktivieren? [j/N] " sched_input read -rp "Zeitplan aktivieren? [j/N] " sched_input
local sched_enabled="false" local sched_enabled="false"
local sched_days="*" sched_start="" sched_stop="" local sched_once="false" sched_date="" sched_days="*" sched_start="" sched_stop=""
if [[ "$sched_input" =~ ^[JjYy]$ ]]; then if [[ "$sched_input" =~ ^[JjYy]$ ]]; then
sched_enabled="true" sched_enabled="true"
read -rp "Einmaliger Termin statt wöchentlich? [j/N] " sched_once_input
if [[ "$sched_once_input" =~ ^[JjYy]$ ]]; then
sched_once="true"
read -rp "Datum (YYYY-MM-DD) [$(date +%F)]: " sched_date_input
sched_date="${sched_date_input:-$(date +%F)}"
else
read -rp "Tage [*] (z.B. Mo-Fr, Sa,So, *): " sched_days_input read -rp "Tage [*] (z.B. Mo-Fr, Sa,So, *): " sched_days_input
sched_days="${sched_days_input:-*}" sched_days="${sched_days_input:-*}"
fi
read -rp "Startzeit (HH:MM): " sched_start read -rp "Startzeit (HH:MM): " sched_start
if [[ -z "$sched_start" ]]; then if [[ -z "$sched_start" ]]; then
echo "Keine Startzeit angegeben - Zeitplan deaktiviert." echo "Keine Startzeit angegeben - Zeitplan deaktiviert."
sched_enabled="false" sched_enabled="false"
elif [[ "$sched_once" == "true" && -z "$sched_date" ]]; then
echo "Kein Datum angegeben - Zeitplan deaktiviert."
sched_enabled="false"
else else
read -rp "Endzeit (HH:MM, leer=keine): " sched_stop read -rp "Endzeit (HH:MM, leer=keine): " sched_stop
fi fi
@@ -753,7 +887,11 @@ create_job() {
echo " Format: ${ext:-auto}" echo " Format: ${ext:-auto}"
echo " Dauer: ${duration:-unbegrenzt}" echo " Dauer: ${duration:-unbegrenzt}"
if [[ "$sched_enabled" == "true" ]]; then if [[ "$sched_enabled" == "true" ]]; then
echo " Zeitplan: ${sched_days} ${sched_start}${sched_stop:+ - ${sched_stop}}" if [[ "$sched_once" == "true" ]]; then
echo " Zeitplan: einmalig ${sched_date} ${sched_start}${sched_stop:+ - ${sched_stop}}"
else
echo " Zeitplan: wöchentlich ${sched_days} ${sched_start}${sched_stop:+ - ${sched_stop}}"
fi
else else
echo " Zeitplan: deaktiviert" echo " Zeitplan: deaktiviert"
fi fi
@@ -782,6 +920,8 @@ EXTRA_FFMPEG_ARGS=""
############################################### ###############################################
SCHEDULE_ENABLED=${sched_enabled} SCHEDULE_ENABLED=${sched_enabled}
SCHEDULE_ONCE=${sched_once}
SCHEDULE_DATE="${sched_date}"
SCHEDULE_DAYS="${sched_days}" SCHEDULE_DAYS="${sched_days}"
SCHEDULE_START="${sched_start}" SCHEDULE_START="${sched_start}"
SCHEDULE_STOP="${sched_stop}" SCHEDULE_STOP="${sched_stop}"
@@ -832,7 +972,11 @@ list_jobs() {
local extra_info="" local extra_info=""
if [[ "${SCHEDULE_ENABLED:-false}" == "true" && -n "${SCHEDULE_START:-}" ]]; then if [[ "${SCHEDULE_ENABLED:-false}" == "true" && -n "${SCHEDULE_START:-}" ]]; then
extra_info="Zeitplan: ${SCHEDULE_DAYS:-*} ${SCHEDULE_START}" 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 if [[ -n "${SCHEDULE_STOP:-}" ]]; then
extra_info+=" - ${SCHEDULE_STOP}" extra_info+=" - ${SCHEDULE_STOP}"
elif [[ -n "${MAX_DURATION:-}" ]]; then elif [[ -n "${MAX_DURATION:-}" ]]; then
@@ -987,6 +1131,7 @@ Befehle:
record-all Alle Jobs aufzeichnen record-all Alle Jobs aufzeichnen
create Neuen Job interaktiv erstellen create Neuen Job interaktiv erstellen
quick Interaktive Schnellaufnahme (URL eingeben) quick Interaktive Schnellaufnahme (URL eingeben)
upload <datei> Vorhandene Datei nach Plik hochladen
stop <name> Aufnahme stoppen stop <name> Aufnahme stoppen
stop-all Alle Aufnahmen stoppen stop-all Alle Aufnahmen stoppen
list Jobs und deren Zeitpläne anzeigen list Jobs und deren Zeitpläne anzeigen
@@ -997,6 +1142,7 @@ Befehle:
Optionen: Optionen:
-c, --config <datei> Alternative Konfigurationsdatei -c, --config <datei> Alternative Konfigurationsdatei
-V, --verbose Ausführliche Ausgabe -V, --verbose Ausführliche Ausgabe
--foreground Aufnahme im Vordergrund ausführen
-h, --help Diese Hilfe -h, --help Diese Hilfe
-v, --version Version -v, --version Version
@@ -1021,6 +1167,7 @@ main() {
case "$1" in case "$1" in
-c|--config) CONFIG_FILE="$2"; shift 2 ;; -c|--config) CONFIG_FILE="$2"; shift 2 ;;
-V|--verbose) VERBOSE=true; shift ;; -V|--verbose) VERBOSE=true; shift ;;
--foreground) FOREGROUND=true; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
-v|--version) echo "stream-recorder v${VERSION}"; exit 0 ;; -v|--version) echo "stream-recorder v${VERSION}"; exit 0 ;;
*) break ;; *) break ;;
@@ -1035,6 +1182,19 @@ main() {
case "$command" in case "$command" in
record) 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")
if [[ "$FOREGROUND" == "true" || "$VERBOSE" == "true" ]]; then
record_stream "$job_file"
else
start_detached_recording "$job_file"
fi
;;
record-run)
if [[ -z "${1:-}" ]]; then if [[ -z "${1:-}" ]]; then
log "ERROR" "Job-Datei oder Job-Name erforderlich" log "ERROR" "Job-Datei oder Job-Name erforderlich"
echo ""; usage; exit 1 echo ""; usage; exit 1
@@ -1046,6 +1206,12 @@ main() {
record-all) record_all ;; record-all) record_all ;;
create) create_job ;; create) create_job ;;
quick) interactive_record ;; 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 ;; scheduler) run_scheduler ;;
stop) stop)
if [[ -z "${1:-}" ]]; then if [[ -z "${1:-}" ]]; then