From ebb5dde4fbef681eb4b63c57c3f5c3aa606bede3 Mon Sep 17 00:00:00 2001 From: bernd Date: Tue, 29 Sep 2026 17:06:19 +0200 Subject: [PATCH] Suchfunktion eingebaut --- recorder.py | 160 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 34 deletions(-) diff --git a/recorder.py b/recorder.py index af4d867..96fc29c 100644 --- a/recorder.py +++ b/recorder.py @@ -9,7 +9,7 @@ from pathlib import Path from zoneinfo import ZoneInfo import requests -from flask import Flask, render_template_string, send_from_directory, abort, Response, request +from flask import Flask, render_template_string, send_from_directory, abort, Response, request, stream_with_context # --------------------------------------------------------------------------- @@ -70,6 +70,7 @@ NORMALIZE_TRUE_PEAK = os.environ.get("NORMALIZE_TRUE_PEAK", "-1.5") NORMALIZE_LRA = os.environ.get("NORMALIZE_LRA", "11") + # --------------------------------------------------------------------------- # Application state # --------------------------------------------------------------------------- @@ -90,6 +91,9 @@ last_status_ok = False last_status_check = None last_status_error = None +SSE_CLIENTS = set() +SSE_LOCK = threading.Lock() + # --------------------------------------------------------------------------- # Helpers @@ -294,6 +298,43 @@ def query_icecast(): return None + +def broadcast_event(event, filename=None): + payload = json.dumps({ + "event": event, + "filename": filename, + }, ensure_ascii=False) + dead = [] + with SSE_LOCK: + for client_queue in list(SSE_CLIENTS): + try: + client_queue.put_nowait(payload) + except Exception: + dead.append(client_queue) + for client_queue in dead: + SSE_CLIENTS.discard(client_queue) + + +def sse_stream(): + import queue + + client_queue = queue.Queue() + with SSE_LOCK: + SSE_CLIENTS.add(client_queue) + + try: + yield ": connected\n\n" + while True: + try: + payload = client_queue.get(timeout=20) + yield f"data: {payload}\n\n" + except queue.Empty: + yield ": keepalive\n\n" + finally: + with SSE_LOCK: + SSE_CLIENTS.discard(client_queue) + + # --------------------------------------------------------------------------- # Recording # --------------------------------------------------------------------------- @@ -333,6 +374,8 @@ def start_recording(): recording_filename = filename recording_started = time.time() + broadcast_event("recording_started", filename) + def normalize_recording(filename): """Normalize a completed MP3 in place in RECORDINGS_DIR.""" @@ -435,6 +478,8 @@ def stop_recording(): finally: normalizing_filename = None + broadcast_event("recording_stopped", filename) + # --------------------------------------------------------------------------- # Monitoring loop @@ -732,6 +777,7 @@ a.button:hover {
+
{% if normalizing_filename %}
● AUFNAHME WIRD NORMALISIERT @@ -778,7 +824,7 @@ a.button:hover { ● keine laufende Aufnahme
{% endif %} - +

Fertige Aufnahmen

@@ -844,46 +890,25 @@ a.button:hover {
@@ -962,6 +987,73 @@ def index(): return render_index() +@app.route("/api/live") +def api_live(): + with state_lock: + process = ffmpeg_process + current_filename = recording_filename + current_started = recording_started + status_error = not last_status_ok + + recording = None + if process is not None and process.poll() is None and current_filename: + current_size = "–" + try: + current_size = format_size((RECORDINGS_DIR / current_filename).stat().st_size) + except FileNotFoundError: + pass + recording = { + "filename": current_filename, + "started": format_datetime(current_started), + "size": current_size, + } + + return render_template_string( + """{% if normalizing_filename %} +
● AUFNAHME WIRD NORMALISIERT
+
+
{{ normalizing_filename }}
+
Lautstärke wird automatisch normalisiert …
+
+{% elif recording %} +
● AUFNAHME LÄUFT
+
+
+
🔴 Live-Stream
+
Direkt vom Icecast-Stream
+ +
+
+
▶ Aktuelle Aufnahme
+
Zeitversetzt · gestartet {{ recording.started }} · aktuell {{ recording.size }}
+ +
+
+{% elif status_error %} +
⚠ Icecast-Status momentan nicht erreichbar
+{% else %} +
● keine laufende Aufnahme
+{% endif %}""", + recording=recording, + normalizing_filename=normalizing_filename, + status_error=status_error, + icecast_public_url=ICECAST_PUBLIC_URL, + ) + + +@app.route("/api/events") +def api_events(): + return Response( + stream_with_context(sse_stream()), + mimetype="text/event-stream", + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", + }, + ) + + @app.route("/api/status") def api_status(): return render_index()