Suchfunktion eingebaut

This commit is contained in:
2026-09-29 17:06:19 +02:00
parent 09e7f79952
commit ebb5dde4fb
+126 -34
View File
@@ -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 {
<div id="dynamic-content">
<div id="live-section">
{% if normalizing_filename %}
<div class="status recording">
● AUFNAHME WIRD NORMALISIERT
@@ -778,7 +824,7 @@ a.button:hover {
● keine laufende Aufnahme
</div>
{% endif %}
</div>
<h2>Fertige Aufnahmen</h2>
@@ -844,46 +890,25 @@ a.button:hover {
</div>
<div class="footer">
Aktualisierung automatisch alle 10 Sekunden
Live-Status wird bei Start und Ende einer Aufnahme automatisch aktualisiert.
</div>
<script>
async function updatePage() {
// Never rebuild the DOM while an audio player is playing or paused at
// a non-zero position. Replacing <audio> elements would reset playback.
const players = Array.from(document.querySelectorAll("audio"));
const audioActive = players.some(player =>
!player.paused || player.currentTime > 0
);
if (audioActive) {
return;
}
async function updateLiveSection() {
try {
const response = await fetch("/api/status", {
cache: "no-store"
});
if (!response.ok) {
return;
}
const data = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(data, "text/html");
const newContent = doc.querySelector("#dynamic-content");
const currentContent = document.querySelector("#dynamic-content");
if (newContent && currentContent) {
currentContent.innerHTML = newContent.innerHTML;
}
const response = await fetch("/api/live", { cache: "no-store" });
if (!response.ok) return;
const html = await response.text();
const current = document.getElementById("live-section");
if (current) current.innerHTML = html;
} catch (error) {
console.warn("Status update failed:", error);
console.warn("Live-Status update failed:", error);
}
}
setInterval(updatePage, 10000);
const events = new EventSource("/api/events");
events.onmessage = () => updateLiveSection();
events.onerror = () => { /* EventSource reconnects automatically. */ };
</script>
</body>
@@ -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 %}
<div class="status recording">● AUFNAHME WIRD NORMALISIERT</div>
<div class="recording">
<div class="recording-title">{{ normalizing_filename }}</div>
<div class="recording-info">Lautstärke wird automatisch normalisiert …</div>
</div>
{% elif recording %}
<div class="status recording">● AUFNAHME LÄUFT</div>
<div class="live-panel">
<div class="live-item">
<div class="live-title">🔴 Live-Stream</div>
<div class="live-info">Direkt vom Icecast-Stream</div>
<audio controls preload="none"><source src="{{ icecast_public_url }}" type="audio/mpeg"></audio>
</div>
<div class="live-item">
<div class="live-title">▶ Aktuelle Aufnahme</div>
<div class="live-info">Zeitversetzt · gestartet {{ recording.started }} · aktuell {{ recording.size }}</div>
<audio controls><source src="/recordings/{{ recording.filename }}?live=1" type="audio/mpeg"></audio>
</div>
</div>
{% elif status_error %}
<div class="status error">⚠ Icecast-Status momentan nicht erreichbar</div>
{% else %}
<div class="status idle">● keine laufende Aufnahme</div>
{% 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()