Release: v2.7.0 + Dokumentation ausgelagert

This commit is contained in:
Patrick Asmus
2026-09-07 12:54:53 +02:00
parent 7ddfb0e0d4
commit f3a8e20d7f
16 changed files with 1103 additions and 330 deletions
+283 -15
View File
@@ -8,15 +8,15 @@
# Web: https://www.cleveradmin.de
# Repository: https://git.techniverse.net/scriptos/stream-recorder.git
# License: MIT
# Version: v2.6.2
# Date: 06.09.2026
# Modifications: Mehrtägige Aufnahmen nun möglich
# Version: v2.7.0
# Date: 07.09.2026
# Modifications: Segment-Splitting, erweiterter Status, verbessertes Stoppen
###############################################
set -uo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="2.6.0"
readonly VERSION="2.7.0"
# --- Standardwerte (überschreibbar via Konfiguration) ---
CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf"
@@ -224,6 +224,57 @@ parse_duration() {
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" <<INFOEOF
STREAM_NAME="${stream_name}"
STREAM_TYPE="${stream_type}"
OUTPUT_DIR="${output_dir}"
OUTPUT_FILE="${output_file}"
START_TIME="$(date '+%Y-%m-%d %H:%M:%S')"
START_EPOCH=$(date +%s)
SEGMENT_MODE="${segment_mode}"
JOB_FILE="${job_file}"
INFOEOF
}
remove_job_info() {
local job_id="$1"
rm -f "${PID_DIR}/${job_id}.info" 2>/dev/null || true
}
# --- Tages-/Zeitplanung ---
_day_name_to_num() {
@@ -298,7 +349,13 @@ load_config() {
# shellcheck source=/dev/null
source "$CONFIG_FILE"
else
log "WARN" "Konfigurationsdatei nicht gefunden: ${CONFIG_FILE} - verwende Standardwerte"
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
}
@@ -360,6 +417,7 @@ load_job() {
SCHEDULE_STOP=""
PLIK_ENABLED="false"
DELETE_AFTER_UPLOAD="false"
SEGMENT_DURATION=""
if [[ ! -f "$job_file" ]]; then
log "ERROR" "Job-Datei nicht gefunden: ${job_file}"
@@ -462,6 +520,58 @@ mark_schedule_started() {
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
# =============================================
@@ -480,12 +590,26 @@ record_stream() {
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"
@@ -496,8 +620,13 @@ record_stream() {
return 1
fi
log "INFO" "Starte Aufnahme: ${STREAM_NAME} [${stream_type}] -> ${output_file}"
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"
@@ -509,6 +638,10 @@ record_stream() {
*) 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
@@ -521,16 +654,33 @@ record_stream() {
_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
@@ -541,12 +691,28 @@ record_stream() {
_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 [[ -s "$output_file" ]]; 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"
@@ -561,7 +727,9 @@ record_stream() {
if [[ $ret -eq 143 || $ret -eq 130 ]]; then
log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}"
notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt"
if [[ -s "$output_file" ]]; then
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
@@ -570,7 +738,9 @@ record_stream() {
if [[ ! -f "$pid_file" ]]; then
log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}"
notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt"
if [[ -s "$output_file" ]]; then
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
@@ -592,18 +762,38 @@ record_stream() {
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')
output_file="${output_dir}/${safe_name}_${timestamp}.${ext}"
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 [[ -f "$output_file" && ! -s "$output_file" ]]; then
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
@@ -887,6 +1077,19 @@ create_job() {
fi
fi
echo ""
echo "--- Segment-Splitting (optional) ---"
local segment_duration=""
read -rp "Aufnahme in Segmente teilen? (z.B. 1h, 2h oder leer=nein): " segment_input
if [[ -n "$segment_input" ]]; then
segment_duration=$(parse_duration "$segment_input")
if [[ -n "$segment_duration" && "$segment_duration" -gt 0 ]]; then
echo " Segmente alle $(format_elapsed "$segment_duration")"
else
segment_duration=""
fi
fi
echo ""
echo "--- Plik-Upload (optional) ---"
local plik_enabled="false"
@@ -930,6 +1133,11 @@ create_job() {
duration_display+="${d_h}h ${d_m}m"
fi
echo " Dauer: ${duration_display}"
if [[ -n "$segment_duration" ]]; then
echo " Segmente: alle $(format_elapsed "$segment_duration")"
else
echo " Segmente: deaktiviert"
fi
if [[ "$sched_enabled" == "true" ]]; then
if [[ "$sched_once" == "true" ]]; then
echo " Zeitplan: einmalig ${sched_date} ${sched_start}${sched_stop:+ - ${sched_stop}}"
@@ -963,6 +1171,13 @@ OUTPUT_FORMAT="${ext}"
MAX_DURATION="${duration}"
EXTRA_FFMPEG_ARGS=""
###############################################
# Segment-Splitting (optional)
# Aufnahme in Teile zerlegen, z.B. 1h, 2h, 30m
###############################################
SEGMENT_DURATION="${segment_duration}"
###############################################
# Zeitplanung (optional)
###############################################
@@ -1032,6 +1247,13 @@ list_jobs() {
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
@@ -1062,7 +1284,7 @@ list_jobs() {
show_status() {
echo "Laufende Aufnahmen:"
echo "$(printf '=%.0s' {1..50})"
echo "$(printf '=%.0s' {1..60})"
echo ""
local found=false
@@ -1078,9 +1300,48 @@ show_status() {
found=true
local mode="manuell"
[[ -f "${PID_DIR}/${job_id}.scheduled" ]] && mode="geplant"
printf " %-20s PID: %-10s [%s]\n" "$job_id" "$pid" "$mode"
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
@@ -1181,10 +1442,10 @@ Befehle:
create Neuen Job interaktiv erstellen
quick Interaktive Schnellaufnahme (URL eingeben)
upload <datei> Vorhandene Datei nach Plik hochladen
stop <name> Aufnahme stoppen
stop <name> Aufnahme stoppen (Teilaufnahme wird hochgeladen)
stop-all Alle Aufnahmen stoppen
list Jobs und deren Zeitpläne anzeigen
status Laufende Aufnahmen und Scheduler-Status
status Laufende Aufnahmen mit Details anzeigen
scheduler Zeitplanung prüfen und Jobs starten/stoppen
logs Log-Ausgabe live verfolgen
@@ -1195,6 +1456,11 @@ Optionen:
-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.
@@ -1204,6 +1470,8 @@ 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