1301 lines
40 KiB
Bash
1301 lines
40 KiB
Bash
#!/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.6.1
|
|
# Date: 06.09.2026
|
|
# Modifications: Doppelte Benachrichtigung entfernt
|
|
###############################################
|
|
|
|
set -uo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly VERSION="2.6.0"
|
|
|
|
# --- Standardwerte (überschreibbar via Konfiguration) ---
|
|
CONFIG_FILE="${SCRIPT_DIR}/config/stream-recorder.conf"
|
|
JOBS_DIR="${SCRIPT_DIR}/jobs"
|
|
DOWNLOAD_PATH="/home/downloads/recordings"
|
|
LOG_DIR="/var/log/stream-recorder"
|
|
LOG_LEVEL="INFO"
|
|
PID_DIR="/tmp/stream-recorder"
|
|
STATE_DIR="/var/lib/stream-recorder"
|
|
MAX_RETRIES=0
|
|
RETRY_DELAY=5
|
|
|
|
# --- NTFY-Benachrichtigungen (optional) ---
|
|
NTFY_URL=""
|
|
NTFY_TOKEN=""
|
|
NTFY_EVENTS="error"
|
|
|
|
# --- Plik-Upload (optional) ---
|
|
PLIK_URL=""
|
|
PLIK_API_KEY=""
|
|
PLIK_TTL="30d"
|
|
|
|
# --- Laufzeit-Flags ---
|
|
VERBOSE=false
|
|
FOREGROUND=false
|
|
|
|
# =============================================
|
|
# Hilfsfunktionen
|
|
# =============================================
|
|
|
|
_log_level_num() {
|
|
case "$1" in
|
|
DEBUG) echo 0 ;;
|
|
INFO) echo 1 ;;
|
|
WARN) echo 2 ;;
|
|
ERROR) echo 3 ;;
|
|
*) echo 1 ;;
|
|
esac
|
|
}
|
|
|
|
log() {
|
|
local level="$1"
|
|
shift
|
|
local message="$*"
|
|
|
|
local msg_num cur_num
|
|
msg_num=$(_log_level_num "$level")
|
|
cur_num=$(_log_level_num "$LOG_LEVEL")
|
|
[[ $msg_num -lt $cur_num ]] && return
|
|
|
|
local timestamp
|
|
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local entry="[${timestamp}] [${level}] ${message}"
|
|
|
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
echo "$entry" >> "${LOG_DIR}/stream-recorder.log" 2>/dev/null || true
|
|
|
|
if [[ -t 1 ]] || [[ "$VERBOSE" == "true" ]]; then
|
|
echo "$entry"
|
|
fi
|
|
}
|
|
|
|
notify() {
|
|
local event="$1"
|
|
local title="$2"
|
|
local message="$3"
|
|
local priority="${4:-default}"
|
|
|
|
[[ -z "${NTFY_URL:-}" ]] && return
|
|
command -v curl &>/dev/null || return
|
|
[[ ! ",${NTFY_EVENTS}," == *",${event},"* ]] && return
|
|
|
|
local -a curl_args=(-s -o /dev/null --max-time 10)
|
|
curl_args+=(-H "Title: ${title}")
|
|
curl_args+=(-H "Priority: ${priority}")
|
|
curl_args+=(-H "Tags: stream-recorder,${event}")
|
|
|
|
if [[ -n "${NTFY_TOKEN:-}" ]]; then
|
|
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
|
fi
|
|
|
|
curl_args+=(-d "$message")
|
|
curl_args+=("$NTFY_URL")
|
|
|
|
curl "${curl_args[@]}" 2>/dev/null &
|
|
disown 2>/dev/null || true
|
|
}
|
|
|
|
plik_browser_url_from_download_url() {
|
|
local download_url="$1"
|
|
local plik_base="${PLIK_URL%/}"
|
|
local file_path="${download_url#"$plik_base"/file/}"
|
|
|
|
if [[ "$file_path" != "$download_url" ]]; then
|
|
local upload_id="${file_path%%/*}"
|
|
if [[ -n "$upload_id" && "$upload_id" != "$file_path" ]]; then
|
|
echo "${plik_base}/#/?id=${upload_id}"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
echo "$download_url"
|
|
}
|
|
|
|
upload_to_plik() {
|
|
local file="$1"
|
|
local stream_name="$2"
|
|
|
|
[[ "${PLIK_ENABLED:-false}" != "true" ]] && return
|
|
[[ -z "${PLIK_URL:-}" ]] && return
|
|
if [[ ! -f "$file" ]]; then
|
|
log "WARN" "Plik-Upload übersprungen: Datei nicht gefunden: ${file}"
|
|
return 1
|
|
fi
|
|
if [[ ! -s "$file" ]]; then
|
|
log "WARN" "Plik-Upload übersprungen: Datei ist leer: ${file}"
|
|
return 1
|
|
fi
|
|
command -v curl &>/dev/null || { log "WARN" "curl nicht installiert - Plik-Upload übersprungen"; return; }
|
|
|
|
local filename
|
|
filename=$(basename "$file")
|
|
|
|
local plik_base="${PLIK_URL%/}"
|
|
|
|
local -a curl_args=(-sS --max-time 7200)
|
|
|
|
if [[ -n "${PLIK_API_KEY:-}" ]]; then
|
|
curl_args+=(-H "X-PlikToken: ${PLIK_API_KEY}")
|
|
fi
|
|
|
|
local ttl_seconds=""
|
|
if [[ -n "${PLIK_TTL:-}" ]]; then
|
|
local ttl_val="${PLIK_TTL}"
|
|
if [[ "$ttl_val" =~ ^([0-9]+)d$ ]]; then
|
|
ttl_seconds=$(( ${BASH_REMATCH[1]} * 86400 ))
|
|
elif [[ "$ttl_val" =~ ^([0-9]+)h$ ]]; then
|
|
ttl_seconds=$(( ${BASH_REMATCH[1]} * 3600 ))
|
|
elif [[ "$ttl_val" =~ ^[0-9]+$ ]]; then
|
|
ttl_seconds="$ttl_val"
|
|
fi
|
|
fi
|
|
|
|
curl_args+=(-F "file=@${file};filename=${filename}")
|
|
[[ -n "$ttl_seconds" ]] && curl_args+=(-F "ttl=${ttl_seconds}")
|
|
|
|
log "INFO" "Plik-Upload: ${filename} -> ${plik_base}"
|
|
|
|
local upload_tmp upload_response http_code curl_status=0
|
|
upload_tmp=$(mktemp)
|
|
http_code=$(curl "${curl_args[@]}" \
|
|
-w "%{http_code}" \
|
|
-o "$upload_tmp" \
|
|
"${plik_base}" 2>/dev/null) || curl_status=$?
|
|
upload_response=$(cat "$upload_tmp" 2>/dev/null || true)
|
|
rm -f "$upload_tmp"
|
|
|
|
if [[ $curl_status -ne 0 || ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
|
log "ERROR" "Plik-Upload fehlgeschlagen: Datei konnte nicht hochgeladen werden (HTTP ${http_code:-000}, curl ${curl_status})"
|
|
[[ -n "$upload_response" ]] && log "DEBUG" "Plik-Antwort: ${upload_response}"
|
|
return 1
|
|
fi
|
|
|
|
local download_url
|
|
download_url=$(printf '%s\n' "$upload_response" | grep -Eo 'https?://[^[:space:]]+' | tail -1)
|
|
if [[ -z "$download_url" ]]; then
|
|
log "ERROR" "Plik-Upload fehlgeschlagen: Keine Download-URL in der Antwort"
|
|
log "DEBUG" "Plik-Antwort: ${upload_response}"
|
|
return 1
|
|
fi
|
|
|
|
local browser_url
|
|
browser_url=$(plik_browser_url_from_download_url "$download_url")
|
|
|
|
log "INFO" "Plik-Upload abgeschlossen: ${browser_url}"
|
|
if [[ "$browser_url" != "$download_url" ]]; then
|
|
log "INFO" "Plik-Datei-URL: ${download_url}"
|
|
fi
|
|
notify "upload" "Plik-Upload" "${stream_name}: ${browser_url}"
|
|
|
|
if [[ "${DELETE_AFTER_UPLOAD:-false}" == "true" ]]; then
|
|
rm -f "$file"
|
|
log "INFO" "Lokale Datei gelöscht: ${file}"
|
|
fi
|
|
|
|
echo "$browser_url"
|
|
}
|
|
|
|
parse_duration() {
|
|
local input="$1"
|
|
[[ -z "$input" ]] && { echo ""; return; }
|
|
[[ "$input" =~ ^[0-9]+$ ]] && { echo "$input"; return; }
|
|
|
|
local total=0
|
|
[[ "$input" =~ ([0-9]+)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" "$STATE_DIR" 2>/dev/null || true
|
|
}
|
|
|
|
check_dependencies() {
|
|
if ! command -v ffmpeg &>/dev/null; then
|
|
log "ERROR" "ffmpeg ist nicht installiert"
|
|
log "INFO" "Installation: sudo apt install ffmpeg"
|
|
exit 1
|
|
fi
|
|
if [[ -n "${NTFY_URL:-}" ]] && ! command -v curl &>/dev/null; then
|
|
log "WARN" "curl ist nicht installiert - NTFY-Benachrichtigungen deaktiviert"
|
|
fi
|
|
}
|
|
|
|
# =============================================
|
|
# Stream-Erkennung & Job-Verwaltung
|
|
# =============================================
|
|
|
|
detect_stream_type() {
|
|
local url="$1"
|
|
case "$url" in
|
|
rtmp://*|rtmps://*) echo "rtmp" ;;
|
|
*.[Mm]3[Uu]8|*.[Mm]3[Uu]8\?*) echo "hls" ;;
|
|
*.[Mm][Pp]3|*.[Mm][Pp]3\?*) echo "mp3" ;;
|
|
*.[Aa][Aa][Cc]|*.[Aa][Aa][Cc]\?*) echo "aac" ;;
|
|
*.[Oo][Gg][Gg]|*.[Oo][Gg][Gg]\?*) echo "ogg" ;;
|
|
*.[Ff][Ll][Aa][Cc]|*.[Ff][Ll][Aa][Cc]\?*) echo "flac" ;;
|
|
http://*|https://*) echo "http" ;;
|
|
*) echo "http" ;;
|
|
esac
|
|
}
|
|
|
|
default_extension() {
|
|
case "$1" in
|
|
rtmp|hls|http) echo "mp4" ;;
|
|
mp3) echo "mp3" ;;
|
|
aac) echo "aac" ;;
|
|
ogg) echo "ogg" ;;
|
|
flac) echo "flac" ;;
|
|
*) echo "mp4" ;;
|
|
esac
|
|
}
|
|
|
|
load_job() {
|
|
local job_file="$1"
|
|
|
|
STREAM_NAME=""
|
|
STREAM_URL=""
|
|
STREAM_TYPE="auto"
|
|
OUTPUT_FORMAT=""
|
|
MAX_DURATION=""
|
|
EXTRA_FFMPEG_ARGS=""
|
|
SCHEDULE_ENABLED="false"
|
|
SCHEDULE_ONCE="false"
|
|
SCHEDULE_DATE=""
|
|
SCHEDULE_DAYS="*"
|
|
SCHEDULE_START=""
|
|
SCHEDULE_STOP=""
|
|
PLIK_ENABLED="false"
|
|
DELETE_AFTER_UPLOAD="false"
|
|
|
|
if [[ ! -f "$job_file" ]]; then
|
|
log "ERROR" "Job-Datei nicht gefunden: ${job_file}"
|
|
return 1
|
|
fi
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$job_file"
|
|
|
|
if [[ -z "$STREAM_NAME" ]]; then
|
|
log "ERROR" "STREAM_NAME fehlt in: ${job_file}"
|
|
return 1
|
|
fi
|
|
if [[ -z "$STREAM_URL" ]]; then
|
|
log "ERROR" "STREAM_URL fehlt in: ${job_file}"
|
|
return 1
|
|
fi
|
|
|
|
if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then
|
|
if [[ -z "${SCHEDULE_DATE:-}" ]]; then
|
|
log "ERROR" "SCHEDULE_DATE fehlt in einmaligem Zeitplan: ${job_file}"
|
|
return 1
|
|
fi
|
|
if [[ ! "${SCHEDULE_DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
|
log "ERROR" "Ungültiges SCHEDULE_DATE in ${job_file}: ${SCHEDULE_DATE} (erwartet: YYYY-MM-DD)"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$MAX_DURATION" && "$MAX_DURATION" != *:* ]]; then
|
|
local normalized_duration
|
|
normalized_duration=$(parse_duration "$MAX_DURATION")
|
|
if [[ -z "$normalized_duration" ]]; then
|
|
log "ERROR" "Ungültige MAX_DURATION in ${job_file}: ${MAX_DURATION}"
|
|
return 1
|
|
fi
|
|
MAX_DURATION="$normalized_duration"
|
|
fi
|
|
}
|
|
|
|
resolve_job_path() {
|
|
local input="$1"
|
|
[[ -f "$input" ]] && { echo "$input"; return; }
|
|
[[ -f "${JOBS_DIR}/${input}" ]] && { echo "${JOBS_DIR}/${input}"; return; }
|
|
[[ -f "${JOBS_DIR}/${input}.job" ]] && { echo "${JOBS_DIR}/${input}.job"; return; }
|
|
echo "$input"
|
|
}
|
|
|
|
is_recording() {
|
|
local job_id="$1"
|
|
local pid_file="${PID_DIR}/${job_id}.pid"
|
|
|
|
if [[ -f "$pid_file" ]]; then
|
|
local pid
|
|
pid=$(cat "$pid_file")
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
return 0
|
|
else
|
|
rm -f "$pid_file"
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
schedule_state_file() {
|
|
local job_id="$1"
|
|
echo "${STATE_DIR}/${job_id}.last-scheduled"
|
|
}
|
|
|
|
schedule_occurrence_key() {
|
|
local schedule_date
|
|
if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then
|
|
schedule_date="${SCHEDULE_DATE:-$(date '+%F')}"
|
|
else
|
|
schedule_date=$(date '+%F')
|
|
fi
|
|
echo "${SCHEDULE_ONCE:-false}|${schedule_date}|${SCHEDULE_DAYS:-*}|${SCHEDULE_START:-}|${SCHEDULE_STOP:-}|${MAX_DURATION:-}"
|
|
}
|
|
|
|
schedule_already_started() {
|
|
local job_id="$1"
|
|
local occurrence_key="$2"
|
|
local state_file
|
|
state_file=$(schedule_state_file "$job_id")
|
|
|
|
[[ -f "$state_file" ]] || return 1
|
|
[[ "$(cat "$state_file" 2>/dev/null || true)" == "$occurrence_key" ]]
|
|
}
|
|
|
|
mark_schedule_started() {
|
|
local job_id="$1"
|
|
local occurrence_key="$2"
|
|
local state_file
|
|
state_file=$(schedule_state_file "$job_id")
|
|
|
|
mkdir -p "$(dirname "$state_file")" 2>/dev/null || true
|
|
if ! printf '%s\n' "$occurrence_key" > "$state_file"; then
|
|
log "ERROR" "Scheduler-Status konnte nicht geschrieben werden: ${state_file}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# =============================================
|
|
# 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
|
|
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
|
|
|
|
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
|
|
upload_to_plik "$output_file" "$STREAM_NAME"
|
|
fi
|
|
break
|
|
fi
|
|
|
|
if [[ ! -f "$pid_file" ]]; then
|
|
log "INFO" "Aufnahme gestoppt: ${STREAM_NAME}"
|
|
notify "stop" "Aufnahme gestoppt" "${STREAM_NAME} wurde gestoppt"
|
|
if [[ -s "$output_file" ]]; then
|
|
upload_to_plik "$output_file" "$STREAM_NAME"
|
|
fi
|
|
break
|
|
fi
|
|
|
|
retry=$((retry + 1))
|
|
if [[ $MAX_RETRIES -gt 0 && $retry -ge $MAX_RETRIES ]]; then
|
|
log "ERROR" "Maximale Versuche (${MAX_RETRIES}) für '${STREAM_NAME}' erreicht - Aufnahme abgebrochen"
|
|
notify "error" "Aufnahme fehlgeschlagen" "${STREAM_NAME}: Max. Versuche (${MAX_RETRIES}) erreicht (Code: ${ret})" "high"
|
|
break
|
|
fi
|
|
|
|
local retry_info=""
|
|
if [[ $MAX_RETRIES -gt 0 ]]; then
|
|
retry_info=" (Versuch: ${retry}/${MAX_RETRIES})"
|
|
else
|
|
retry_info=" (Versuch: ${retry})"
|
|
fi
|
|
|
|
log "WARN" "Verbindung zu '${STREAM_NAME}' verloren (Code: ${ret})${retry_info}. Reconnect in ${RETRY_DELAY}s..."
|
|
notify "error" "Stream abgerissen" "${STREAM_NAME}: Verbindung verloren (Code: ${ret}). Reconnect in ${RETRY_DELAY}s..." "default"
|
|
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-run "$job_file" &
|
|
done
|
|
|
|
if ! $found; then
|
|
log "WARN" "Keine .job-Dateien gefunden in: ${JOBS_DIR}"
|
|
log "INFO" "Erstelle Job-Dateien aus den Vorlagen: cp jobs/templates/example-rtmp.job.dist jobs/mein-stream.job"
|
|
return 1
|
|
fi
|
|
|
|
wait
|
|
log "INFO" "Alle Aufnahmen beendet"
|
|
}
|
|
|
|
start_detached_recording() {
|
|
local job_file="$1"
|
|
|
|
load_job "$job_file" || return 1
|
|
|
|
local job_id
|
|
job_id=$(basename "$job_file" .job)
|
|
|
|
if is_recording "$job_id"; then
|
|
log "WARN" "Aufnahme von '${STREAM_NAME}' läuft bereits (Job: ${job_id})"
|
|
return 1
|
|
fi
|
|
|
|
local nohup_log="${LOG_DIR}/nohup_${job_id}.log"
|
|
log "INFO" "Starte Aufnahme im Hintergrund: ${STREAM_NAME} [${job_id}]"
|
|
|
|
nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" \
|
|
>> "$nohup_log" 2>&1 < /dev/null &
|
|
local detached_pid=$!
|
|
disown 2>/dev/null || true
|
|
|
|
echo "Aufnahme im Hintergrund gestartet: ${STREAM_NAME} [${job_id}]"
|
|
echo "Starter-PID: ${detached_pid}"
|
|
echo "Logs: ${LOG_DIR}/stream-recorder.log"
|
|
}
|
|
|
|
# =============================================
|
|
# Scheduler (wird per systemd-Timer aufgerufen)
|
|
# =============================================
|
|
|
|
run_scheduler() {
|
|
local today_date today_num now_time
|
|
today_date=$(date '+%F')
|
|
today_num=$(date '+%u')
|
|
now_time=$(date '+%H:%M')
|
|
|
|
log "DEBUG" "Scheduler: Datum=${today_date} Tag=${today_num} Zeit=${now_time}"
|
|
|
|
for job_file in "${JOBS_DIR}"/*.job; do
|
|
[[ -f "$job_file" ]] || continue
|
|
load_job "$job_file" 2>/dev/null || continue
|
|
|
|
local job_id
|
|
job_id=$(basename "$job_file" .job)
|
|
|
|
[[ "${SCHEDULE_ENABLED:-false}" != "true" ]] && continue
|
|
[[ -z "${SCHEDULE_START:-}" ]] && continue
|
|
|
|
local is_today=false
|
|
if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then
|
|
[[ "${SCHEDULE_DATE:-}" == "$today_date" ]] && is_today=true
|
|
else
|
|
_matches_schedule_day "$today_num" "${SCHEDULE_DAYS:-*}" && is_today=true
|
|
fi
|
|
|
|
local in_window=false
|
|
if $is_today; then
|
|
if [[ -n "${SCHEDULE_STOP:-}" ]]; then
|
|
_in_time_window "$now_time" "$SCHEDULE_START" "$SCHEDULE_STOP" && in_window=true
|
|
else
|
|
local now_min start_min
|
|
now_min=$(_time_to_minutes "$now_time")
|
|
start_min=$(_time_to_minutes "$SCHEDULE_START")
|
|
[[ $now_min -ge $start_min ]] && in_window=true
|
|
fi
|
|
fi
|
|
|
|
if $in_window; then
|
|
local occurrence_key
|
|
occurrence_key=$(schedule_occurrence_key)
|
|
|
|
if schedule_already_started "$job_id" "$occurrence_key"; then
|
|
log "DEBUG" "Scheduler: '${STREAM_NAME}' [${job_id}] wurde für dieses Zeitfenster bereits gestartet"
|
|
continue
|
|
fi
|
|
|
|
if ! is_recording "$job_id"; then
|
|
log "INFO" "Scheduler: Starte '${STREAM_NAME}' [${job_id}]"
|
|
mark_schedule_started "$job_id" "$occurrence_key" || continue
|
|
printf '%s\n' "$occurrence_key" > "${PID_DIR}/${job_id}.scheduled"
|
|
nohup setsid "$0" ${CONFIG_FILE:+-c "$CONFIG_FILE"} record-run "$job_file" \
|
|
>> "${LOG_DIR}/nohup_${job_id}.log" 2>&1 &
|
|
disown 2>/dev/null || true
|
|
fi
|
|
else
|
|
if is_recording "$job_id" && [[ -f "${PID_DIR}/${job_id}.scheduled" ]]; then
|
|
if [[ -n "${SCHEDULE_STOP:-}" ]] || ! $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" <<TMPJOB
|
|
STREAM_NAME="${name}"
|
|
STREAM_URL="${url}"
|
|
STREAM_TYPE="${stream_type}"
|
|
OUTPUT_FORMAT=""
|
|
MAX_DURATION="${duration}"
|
|
EXTRA_FFMPEG_ARGS=""
|
|
TMPJOB
|
|
|
|
echo ""
|
|
record_stream "$tmp_job"
|
|
local result=$?
|
|
rm -f "$tmp_job"
|
|
return $result
|
|
}
|
|
|
|
# =============================================
|
|
# Interaktive Job-Erstellung
|
|
# =============================================
|
|
|
|
create_job() {
|
|
echo ""
|
|
echo "stream-recorder v${VERSION} - Neuen Job erstellen"
|
|
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="Mein-Stream"
|
|
|
|
read -rp "Stream-Name [${default_name}]: " name
|
|
name="${name:-$default_name}"
|
|
|
|
read -rp "Stream-Typ [${detected_type}]: " custom_type
|
|
local stream_type="${custom_type:-$detected_type}"
|
|
|
|
local default_ext
|
|
default_ext=$(default_extension "$stream_type")
|
|
read -rp "Ausgabeformat [${default_ext}]: " custom_ext
|
|
local ext="${custom_ext:-}"
|
|
|
|
local duration=""
|
|
|
|
echo ""
|
|
echo "--- Zeitplanung (optional) ---"
|
|
read -rp "Zeitplan aktivieren? [j/N] " sched_input
|
|
local sched_enabled="false"
|
|
local sched_once="false" sched_date="" sched_days="*" sched_start="" sched_stop=""
|
|
if [[ "$sched_input" =~ ^[JjYy]$ ]]; then
|
|
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
|
|
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
|
|
echo ""
|
|
echo "Wie soll die Aufnahme begrenzt werden?"
|
|
echo " 1) Endzeit angeben (z.B. 18:30)"
|
|
echo " 2) Laufzeit angeben (z.B. 1h, 30m)"
|
|
echo " 3) Unbegrenzt (läuft bis manuell gestoppt)"
|
|
read -rp "Auswahl [1/2/3]: " limit_choice
|
|
case "$limit_choice" in
|
|
1)
|
|
read -rp "Endzeit (HH:MM): " sched_stop
|
|
if [[ -n "$sched_stop" ]]; then
|
|
local start_min stop_min diff_min
|
|
start_min=$(_time_to_minutes "$sched_start")
|
|
stop_min=$(_time_to_minutes "$sched_stop")
|
|
diff_min=$(( stop_min - start_min ))
|
|
[[ $diff_min -le 0 ]] && diff_min=$(( diff_min + 1440 ))
|
|
duration=$(( diff_min * 60 ))
|
|
echo " -> Laufzeit automatisch berechnet: ${diff_min} Minuten"
|
|
fi
|
|
;;
|
|
2)
|
|
read -rp "Laufzeit (z.B. 1h, 30m, 1h30m, 3600): " duration_input
|
|
if [[ -n "$duration_input" ]]; then
|
|
duration=$(parse_duration "$duration_input")
|
|
if [[ -n "$duration" ]]; then
|
|
local start_min end_min end_h end_m
|
|
start_min=$(_time_to_minutes "$sched_start")
|
|
end_min=$(( start_min + duration / 60 ))
|
|
[[ $end_min -ge 1440 ]] && end_min=$(( end_min - 1440 ))
|
|
end_h=$(( end_min / 60 ))
|
|
end_m=$(( end_min % 60 ))
|
|
sched_stop=$(printf "%02d:%02d" "$end_h" "$end_m")
|
|
echo " -> Endzeit automatisch berechnet: ${sched_stop}"
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
if [[ "$sched_enabled" != "true" ]]; then
|
|
read -rp "Laufzeit (z.B. 1h, 30m, 3600 oder leer=unbegrenzt): " duration_input
|
|
if [[ -n "$duration_input" ]]; then
|
|
duration=$(parse_duration "$duration_input")
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "--- Plik-Upload (optional) ---"
|
|
local plik_enabled="false"
|
|
local delete_after_upload="false"
|
|
if [[ -n "${PLIK_URL:-}" ]]; then
|
|
read -rp "Plik-Upload aktivieren? [j/N] " plik_input
|
|
if [[ "$plik_input" =~ ^[JjYy]$ ]]; then
|
|
plik_enabled="true"
|
|
read -rp "Lokale Datei nach Upload löschen? [j/N] " delete_input
|
|
[[ "$delete_input" =~ ^[JjYy]$ ]] && delete_after_upload="true"
|
|
fi
|
|
else
|
|
echo " (Übersprungen - PLIK_URL nicht konfiguriert)"
|
|
fi
|
|
|
|
local safe_id
|
|
safe_id=$(echo "$name" | tr -cs 'A-Za-z0-9_-' '_' | sed 's/_$//' | tr '[:upper:]' '[:lower:]')
|
|
local job_file="${JOBS_DIR}/${safe_id}.job"
|
|
|
|
if [[ -f "$job_file" ]]; then
|
|
echo ""
|
|
read -rp "Job '${safe_id}' existiert bereits. Überschreiben? [j/N] " overwrite
|
|
if [[ ! "$overwrite" =~ ^[JjYy]$ ]]; then
|
|
echo "Abgebrochen."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "Zusammenfassung:"
|
|
echo " Datei: ${job_file}"
|
|
echo " Name: ${name}"
|
|
echo " URL: ${url}"
|
|
echo " Typ: ${stream_type}"
|
|
echo " Format: ${ext:-auto}"
|
|
local duration_display="unbegrenzt"
|
|
if [[ -n "$duration" ]]; then
|
|
local d_h=$((duration / 3600)) d_m=$(( (duration % 3600) / 60 ))
|
|
duration_display="${d_h}h ${d_m}m (${duration}s)"
|
|
fi
|
|
echo " Dauer: ${duration_display}"
|
|
if [[ "$sched_enabled" == "true" ]]; then
|
|
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
|
|
if [[ "$plik_enabled" == "true" && "$delete_after_upload" == "true" ]]; then
|
|
echo " Plik: ${plik_enabled} (nach Upload löschen)"
|
|
else
|
|
echo " Plik: ${plik_enabled}"
|
|
fi
|
|
echo ""
|
|
read -rp "Job erstellen? [J/n] " confirm
|
|
if [[ "$confirm" =~ ^[Nn]$ ]]; then
|
|
echo "Abgebrochen."
|
|
return 1
|
|
fi
|
|
|
|
cat > "$job_file" <<JOBEOF
|
|
###############################################
|
|
# stream-recorder - Job: ${name}
|
|
###############################################
|
|
|
|
STREAM_NAME="${name}"
|
|
STREAM_URL="${url}"
|
|
STREAM_TYPE="${stream_type}"
|
|
OUTPUT_FORMAT="${ext}"
|
|
MAX_DURATION="${duration}"
|
|
EXTRA_FFMPEG_ARGS=""
|
|
|
|
###############################################
|
|
# Zeitplanung (optional)
|
|
###############################################
|
|
|
|
SCHEDULE_ENABLED=${sched_enabled}
|
|
SCHEDULE_ONCE=${sched_once}
|
|
SCHEDULE_DATE="${sched_date}"
|
|
SCHEDULE_DAYS="${sched_days}"
|
|
SCHEDULE_START="${sched_start}"
|
|
SCHEDULE_STOP="${sched_stop}"
|
|
|
|
###############################################
|
|
# Plik-Upload (optional)
|
|
###############################################
|
|
|
|
PLIK_ENABLED=${plik_enabled}
|
|
DELETE_AFTER_UPLOAD=${delete_after_upload}
|
|
JOBEOF
|
|
|
|
echo ""
|
|
echo "Job erstellt: ${job_file}"
|
|
echo ""
|
|
echo "Nächste Schritte:"
|
|
echo " Bearbeiten: nano ${job_file}"
|
|
echo " Starten: $(basename "$0") record ${safe_id}"
|
|
echo " Alle zeigen: $(basename "$0") list"
|
|
}
|
|
|
|
# =============================================
|
|
# Verwaltung & Status
|
|
# =============================================
|
|
|
|
list_jobs() {
|
|
echo "Verfügbare Jobs in: ${JOBS_DIR}"
|
|
echo "$(printf '=%.0s' {1..50})"
|
|
echo ""
|
|
|
|
local found=false
|
|
for job_file in "${JOBS_DIR}"/*.job; do
|
|
[[ -f "$job_file" ]] || continue
|
|
found=true
|
|
|
|
load_job "$job_file" 2>/dev/null || continue
|
|
|
|
local job_id
|
|
job_id=$(basename "$job_file" .job)
|
|
local status="bereit"
|
|
|
|
if is_recording "$job_id"; then
|
|
local pid
|
|
pid=$(cat "${PID_DIR}/${job_id}.pid")
|
|
status="läuft (PID: ${pid})"
|
|
fi
|
|
|
|
printf " %-20s %-8s %-40s [%s]\n" "$job_id" "${STREAM_TYPE:-auto}" "$STREAM_URL" "$status"
|
|
|
|
local extra_info=""
|
|
if [[ "${SCHEDULE_ENABLED:-false}" == "true" && -n "${SCHEDULE_START:-}" ]]; then
|
|
if [[ "${SCHEDULE_ONCE:-false}" == "true" ]]; then
|
|
extra_info="Zeitplan: einmalig ${SCHEDULE_DATE:-?} ${SCHEDULE_START}"
|
|
else
|
|
extra_info="Zeitplan: wöchentlich ${SCHEDULE_DAYS:-*} ${SCHEDULE_START}"
|
|
fi
|
|
if [[ -n "${SCHEDULE_STOP:-}" ]]; then
|
|
extra_info+=" - ${SCHEDULE_STOP}"
|
|
elif [[ -n "${MAX_DURATION:-}" ]]; then
|
|
extra_info+=" (Dauer: ${MAX_DURATION}s)"
|
|
fi
|
|
fi
|
|
if [[ "${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 <<EOF
|
|
stream-recorder v${VERSION} - Generischer Stream-Recorder
|
|
|
|
Verwendung: $(basename "$0") [Optionen] <Befehl> [Argumente]
|
|
|
|
Befehle:
|
|
record <job> Einzelnen Stream aufzeichnen
|
|
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
|
|
status Laufende Aufnahmen und Scheduler-Status
|
|
scheduler Zeitplanung prüfen und Jobs starten/stoppen
|
|
logs Log-Ausgabe live verfolgen
|
|
|
|
Optionen:
|
|
-c, --config <datei> Alternative Konfigurationsdatei
|
|
-V, --verbose Ausführliche Ausgabe
|
|
--foreground Aufnahme im Vordergrund ausführen
|
|
-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 ;;
|
|
--foreground) FOREGROUND=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
-v|--version) echo "stream-recorder v${VERSION}"; exit 0 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
|
|
load_config
|
|
check_dependencies
|
|
|
|
local command="${1:-}"
|
|
shift 2>/dev/null || true
|
|
|
|
case "$command" in
|
|
record)
|
|
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
|
|
fi
|
|
local job_file
|
|
job_file=$(resolve_job_path "$1")
|
|
record_stream "$job_file"
|
|
;;
|
|
record-all) record_all ;;
|
|
create) create_job ;;
|
|
quick) interactive_record ;;
|
|
upload)
|
|
if [[ -z "${1:-}" ]]; then
|
|
log "ERROR" "Dateipfad erforderlich"; exit 1
|
|
fi
|
|
PLIK_ENABLED=true upload_to_plik "$1" "${2:-$(basename "$1")}"
|
|
;;
|
|
scheduler) run_scheduler ;;
|
|
stop)
|
|
if [[ -z "${1:-}" ]]; then
|
|
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 "$@"
|