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
+221 -55
View File
@@ -8,15 +8,15 @@
# Web: https://www.cleveradmin.de
# Repository: https://git.techniverse.net/scriptos/stream-recorder.git
# License: MIT
# Version: v2.3.0
# Version: v2.4.0
# 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
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="2.3.0"
readonly VERSION="2.4.0"
# --- Standardwerte (überschreibbar via Konfiguration) ---
CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf"
@@ -25,6 +25,7 @@ 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
@@ -40,6 +41,7 @@ PLIK_TTL="30d"
# --- Laufzeit-Flags ---
VERBOSE=false
FOREGROUND=false
# =============================================
# Hilfsfunktionen
@@ -103,13 +105,36 @@ notify() {
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
[[ ! -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; }
local filename
@@ -117,8 +142,7 @@ upload_to_plik() {
local plik_base="${PLIK_URL%/}"
local -a curl_args=(-s --max-time 300)
curl_args+=(-X POST)
local -a curl_args=(-sS --max-time 7200)
if [[ -n "${PLIK_API_KEY:-}" ]]; then
curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}")
@@ -136,51 +160,44 @@ upload_to_plik() {
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 create_json="{\"files\":[{\"fileName\":\"${filename}\"}]"
if [[ -n "$ttl_seconds" ]]; then
create_json+=",\"ttl\":${ttl_seconds}"
fi
create_json+="}"
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"
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"
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 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}"
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 -a upload_curl_args=(-s --max-time 600)
if [[ -n "${PLIK_API_KEY:-}" ]]; then
upload_curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}")
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}"
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"
echo "$browser_url"
}
parse_duration() {
@@ -277,7 +294,7 @@ load_config() {
else
log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte"
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() {
@@ -330,6 +347,8 @@ load_job() {
MAX_DURATION=""
EXTRA_FFMPEG_ARGS=""
SCHEDULE_ENABLED="false"
SCHEDULE_ONCE="false"
SCHEDULE_DATE=""
SCHEDULE_DAYS="*"
SCHEDULE_START=""
SCHEDULE_STOP=""
@@ -351,6 +370,27 @@ load_job() {
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() {
@@ -377,6 +417,44 @@ is_recording() {
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
# =============================================
@@ -461,9 +539,15 @@ record_stream() {
_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"
if [[ -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
@@ -519,7 +603,7 @@ record_all() {
[[ -f "$job_file" ]] || continue
found=true
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
if ! $found; then
@@ -532,16 +616,43 @@ record_all() {
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_num now_time
local today_date today_num now_time
today_date=$(date '+%F')
today_num=$(date '+%u')
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
[[ -f "$job_file" ]] || continue
@@ -554,7 +665,11 @@ run_scheduler() {
[[ -z "${SCHEDULE_START:-}" ]] && continue
local is_today=false
_matches_schedule_day "$today_num" "${SCHEDULE_DAYS:-*}" && is_today=true
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
@@ -569,11 +684,20 @@ run_scheduler() {
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}]"
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" \
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
@@ -707,15 +831,25 @@ create_job() {
echo "--- Zeitplanung (optional) ---"
read -rp "Zeitplan aktivieren? [j/N] " sched_input
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
sched_enabled="true"
read -rp "Tage [*] (z.B. Mo-Fr, Sa,So, *): " sched_days_input
sched_days="${sched_days_input:-*}"
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
sched_days="${sched_days_input:-*}"
fi
read -rp "Startzeit (HH:MM): " sched_start
if [[ -z "$sched_start" ]]; then
echo "Keine Startzeit angegeben - Zeitplan deaktiviert."
sched_enabled="false"
elif [[ "$sched_once" == "true" && -z "$sched_date" ]]; then
echo "Kein Datum angegeben - Zeitplan deaktiviert."
sched_enabled="false"
else
read -rp "Endzeit (HH:MM, leer=keine): " sched_stop
fi
@@ -753,7 +887,11 @@ create_job() {
echo " Format: ${ext:-auto}"
echo " Dauer: ${duration:-unbegrenzt}"
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
echo " Zeitplan: deaktiviert"
fi
@@ -782,6 +920,8 @@ EXTRA_FFMPEG_ARGS=""
###############################################
SCHEDULE_ENABLED=${sched_enabled}
SCHEDULE_ONCE=${sched_once}
SCHEDULE_DATE="${sched_date}"
SCHEDULE_DAYS="${sched_days}"
SCHEDULE_START="${sched_start}"
SCHEDULE_STOP="${sched_stop}"
@@ -832,7 +972,11 @@ list_jobs() {
local extra_info=""
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
extra_info+=" - ${SCHEDULE_STOP}"
elif [[ -n "${MAX_DURATION:-}" ]]; then
@@ -987,6 +1131,7 @@ Befehle:
record-all Alle Jobs aufzeichnen
create Neuen Job interaktiv erstellen
quick Interaktive Schnellaufnahme (URL eingeben)
upload <datei> Vorhandene Datei nach Plik hochladen
stop <name> Aufnahme stoppen
stop-all Alle Aufnahmen stoppen
list Jobs und deren Zeitpläne anzeigen
@@ -997,6 +1142,7 @@ Befehle:
Optionen:
-c, --config <datei> Alternative Konfigurationsdatei
-V, --verbose Ausführliche Ausgabe
--foreground Aufnahme im Vordergrund ausführen
-h, --help Diese Hilfe
-v, --version Version
@@ -1021,6 +1167,7 @@ main() {
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 ;;
@@ -1035,6 +1182,19 @@ main() {
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")
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
@@ -1046,6 +1206,12 @@ main() {
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