Suchfunktion eingebaut
This commit is contained in:
+126
-34
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import requests
|
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")
|
NORMALIZE_LRA = os.environ.get("NORMALIZE_LRA", "11")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Application state
|
# Application state
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -90,6 +91,9 @@ last_status_ok = False
|
|||||||
last_status_check = None
|
last_status_check = None
|
||||||
last_status_error = None
|
last_status_error = None
|
||||||
|
|
||||||
|
SSE_CLIENTS = set()
|
||||||
|
SSE_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
@@ -294,6 +298,43 @@ def query_icecast():
|
|||||||
return None
|
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
|
# Recording
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -333,6 +374,8 @@ def start_recording():
|
|||||||
recording_filename = filename
|
recording_filename = filename
|
||||||
recording_started = time.time()
|
recording_started = time.time()
|
||||||
|
|
||||||
|
broadcast_event("recording_started", filename)
|
||||||
|
|
||||||
|
|
||||||
def normalize_recording(filename):
|
def normalize_recording(filename):
|
||||||
"""Normalize a completed MP3 in place in RECORDINGS_DIR."""
|
"""Normalize a completed MP3 in place in RECORDINGS_DIR."""
|
||||||
@@ -435,6 +478,8 @@ def stop_recording():
|
|||||||
finally:
|
finally:
|
||||||
normalizing_filename = None
|
normalizing_filename = None
|
||||||
|
|
||||||
|
broadcast_event("recording_stopped", filename)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Monitoring loop
|
# Monitoring loop
|
||||||
@@ -732,6 +777,7 @@ a.button:hover {
|
|||||||
|
|
||||||
<div id="dynamic-content">
|
<div id="dynamic-content">
|
||||||
|
|
||||||
|
<div id="live-section">
|
||||||
{% if normalizing_filename %}
|
{% if normalizing_filename %}
|
||||||
<div class="status recording">
|
<div class="status recording">
|
||||||
● AUFNAHME WIRD NORMALISIERT
|
● AUFNAHME WIRD NORMALISIERT
|
||||||
@@ -778,7 +824,7 @@ a.button:hover {
|
|||||||
● keine laufende Aufnahme
|
● keine laufende Aufnahme
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Fertige Aufnahmen</h2>
|
<h2>Fertige Aufnahmen</h2>
|
||||||
|
|
||||||
@@ -844,46 +890,25 @@ a.button:hover {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Aktualisierung automatisch alle 10 Sekunden
|
Live-Status wird bei Start und Ende einer Aufnahme automatisch aktualisiert.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function updatePage() {
|
async function updateLiveSection() {
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/status", {
|
const response = await fetch("/api/live", { cache: "no-store" });
|
||||||
cache: "no-store"
|
if (!response.ok) return;
|
||||||
});
|
const html = await response.text();
|
||||||
|
const current = document.getElementById("live-section");
|
||||||
if (!response.ok) {
|
if (current) current.innerHTML = html;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} 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>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
@@ -962,6 +987,73 @@ def index():
|
|||||||
return render_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")
|
@app.route("/api/status")
|
||||||
def api_status():
|
def api_status():
|
||||||
return render_index()
|
return render_index()
|
||||||
|
|||||||
Reference in New Issue
Block a user