diff --git a/README.md b/README.md index 4fc2fd4..e73ee7c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Die Konfigurationsdatei liegt unter `config/stream-recorder.conf`: | `LOG_DIR` | `/var/log/stream-recorder` | Log-Verzeichnis | | `LOG_LEVEL` | `INFO` | Log-Level: DEBUG, INFO, WARN, ERROR | | `PID_DIR` | `/tmp/stream-recorder` | PID-Dateien für laufende Aufnahmen | +| `STATE_DIR` | `/var/lib/stream-recorder` | Status für bereits gestartete Zeitpläne | | `MAX_RETRIES` | `0` | Wiederholungsversuche (0 = unbegrenzt)| | `RETRY_DELAY` | `5` | Wartezeit zwischen Versuchen (Sek.) | | `NTFY_URL` | (leer) | NTFY Server-URL inkl. Topic | @@ -122,7 +123,7 @@ nano jobs/mein-stream.job | `STREAM_URL` | Ja | URL des Streams | | `STREAM_TYPE` | Nein | `auto`, `rtmp`, `hls`, `mp3`, `aac`, `ogg`, `flac` | | `OUTPUT_FORMAT` | Nein | Dateiendung (z.B. `mp4`, `mp3`) - sonst automatisch | -| `MAX_DURATION` | Nein | Maximale Dauer in Sekunden (leer = unbegrenzt) | +| `MAX_DURATION` | Nein | Maximale Dauer, z.B. `300`, `5m`, `1h30m` (leer = unbegrenzt) | | `EXTRA_FFMPEG_ARGS`| Nein | Zusätzliche ffmpeg-Parameter | | `PLIK_ENABLED` | Nein | Plik-Upload aktivieren (`true`/`false`) | @@ -131,7 +132,9 @@ nano jobs/mein-stream.job | Parameter | Standard | Beschreibung | |--------------------|----------|-------------------------------------------------| | `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_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). +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 ```bash @@ -163,12 +168,26 @@ SCHEDULE_STOP="22:00" ```bash STREAM_NAME="Mein-Radiosender" STREAM_URL="https://example.com/stream.mp3" -MAX_DURATION="3600" +MAX_DURATION="1h" SCHEDULE_ENABLED=true +SCHEDULE_ONCE=false SCHEDULE_DAYS="Mo-Fr" 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 ### Befehle @@ -177,6 +196,9 @@ SCHEDULE_START="18:00" # Einzelnen Stream aufzeichnen ./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 ./stream-recorder.sh record-all @@ -186,6 +208,9 @@ SCHEDULE_START="18:00" # Interaktive Schnellaufnahme (URL direkt eingeben) ./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 ./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 - **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 - **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 - 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`) +- Plik-Uploads verwenden den direkten Multipart-Upload-Endpunkt des Servers. ### NTFY + Plik diff --git a/config/stream-recorder.conf b/config/stream-recorder.conf index 140491e..d1f41e3 100644 --- a/config/stream-recorder.conf +++ b/config/stream-recorder.conf @@ -15,6 +15,9 @@ LOG_LEVEL="INFO" # PID-Verzeichnis für laufende Aufnahmen PID_DIR="/tmp/stream-recorder" +# Persistentes Status-Verzeichnis für geplante Läufe +STATE_DIR="/var/lib/stream-recorder" + # Anzahl Wiederholungsversuche bei Verbindungsabbruch # 0 = unbegrenzt (empfohlen für Langzeitaufnahmen) MAX_RETRIES=5 diff --git a/jobs/euer-radio_de-main-30min.job b/jobs/euer-radio_de-main-30min.job new file mode 100644 index 0000000..805b894 --- /dev/null +++ b/jobs/euer-radio_de-main-30min.job @@ -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 diff --git a/jobs/templates/example-hls.job.dist b/jobs/templates/example-hls.job.dist index 7a03192..9e32b48 100644 --- a/jobs/templates/example-hls.job.dist +++ b/jobs/templates/example-hls.job.dist @@ -16,7 +16,7 @@ STREAM_TYPE="hls" # Ausgabeformat (mp4 oder mkv empfohlen) OUTPUT_FORMAT="mp4" -# Maximale Aufnahmedauer in Sekunden +# Maximale Aufnahmedauer (leer = unbegrenzt, z.B. 5m oder 1h) MAX_DURATION="" # Zusätzliche ffmpeg-Argumente (optional) @@ -27,6 +27,8 @@ EXTRA_FFMPEG_ARGS="" ############################################### SCHEDULE_ENABLED=false +SCHEDULE_ONCE=false +SCHEDULE_DATE="" SCHEDULE_DAYS="*" SCHEDULE_START="20:00" SCHEDULE_STOP="22:00" diff --git a/jobs/templates/example-mp3.job.dist b/jobs/templates/example-mp3.job.dist index 0f5df56..7f070e4 100644 --- a/jobs/templates/example-mp3.job.dist +++ b/jobs/templates/example-mp3.job.dist @@ -16,9 +16,9 @@ STREAM_TYPE="auto" # Ausgabeformat OUTPUT_FORMAT="" -# Maximale Aufnahmedauer in Sekunden (leer = unbegrenzt) -# Für Radio-Mitschnitte empfohlen, z.B. 3600 für 1 Stunde -MAX_DURATION="3600" +# Maximale Aufnahmedauer (leer = unbegrenzt) +# Für Radio-Mitschnitte empfohlen, z.B. 1h für 1 Stunde +MAX_DURATION="1h" # Zusätzliche ffmpeg-Argumente (optional) EXTRA_FFMPEG_ARGS="" @@ -28,6 +28,8 @@ EXTRA_FFMPEG_ARGS="" ############################################### SCHEDULE_ENABLED=false +SCHEDULE_ONCE=false +SCHEDULE_DATE="" SCHEDULE_DAYS="Mo-Fr" SCHEDULE_START="18:00" SCHEDULE_STOP="" diff --git a/jobs/templates/example-rtmp.job.dist b/jobs/templates/example-rtmp.job.dist index bbad15d..7d9e618 100644 --- a/jobs/templates/example-rtmp.job.dist +++ b/jobs/templates/example-rtmp.job.dist @@ -18,8 +18,8 @@ STREAM_TYPE="auto" # Leer lassen für automatische Erkennung basierend auf Stream-Typ OUTPUT_FORMAT="" -# Maximale Aufnahmedauer in Sekunden (leer = unbegrenzt) -# Beispiel: 3600 = 1 Stunde, 7200 = 2 Stunden +# Maximale Aufnahmedauer (leer = unbegrenzt) +# Beispiele: 300 = 5 Minuten, 5m = 5 Minuten, 1h30m = 90 Minuten MAX_DURATION="" # Zusätzliche ffmpeg-Argumente (optional) @@ -34,7 +34,14 @@ EXTRA_FFMPEG_ARGS="" # Zeitplan aktivieren (true/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 # Mo-Fr = Montag bis Freitag # Sa,So = Samstag und Sonntag diff --git a/stream-recorder.sh b/stream-recorder.sh index 13dbd39..ef1fa45 100644 --- a/stream-recorder.sh +++ b/stream-recorder.sh @@ -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 Vorhandene Datei nach Plik hochladen stop Aufnahme stoppen stop-all Alle Aufnahmen stoppen list Jobs und deren Zeitpläne anzeigen @@ -997,6 +1142,7 @@ Befehle: Optionen: -c, --config 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