feat: FLAC analysis, inline player, WCAG, live-recording status

web.py:
- Extend loudness analysis to FLAC files via soundfile (numpy required)
- Add /stream/ endpoint with HTTP Range support for seekable inline playback
- Add collapsible ▶ Play button per row (hidden by default); src loaded lazily
- Add /api/status endpoint returning active filenames from status.json
- Animated ● REC badge on in-progress files, polled every 5 s
- Full WCAG: skip link, aria-expanded/controls, aria-label, role=img on
  waveform SVG, role=list on loud-section chips, focus-visible outlines,
  aria-live on subtitle, focus moved to <audio> when player opens

isr.py:
- Write recordings/status.json atomically every 2 s while recording
- Delete status.json on clean shutdown so web UI shows no stale state
This commit is contained in:
2026-04-26 12:53:01 +02:00
parent 8254ccde86
commit 624f1f2664
2 changed files with 444 additions and 147 deletions
+44 -2
View File
@@ -5,6 +5,7 @@ Records from multiple sources: Icecast streams and soundcards.
Supports time-based file splitting and concurrent recording.
"""
import json
import os
import sys
import time
@@ -787,6 +788,8 @@ class RecorderManager:
self.threads: List[threading.Thread] = []
self.logger = None
self.audio_system = None
self.output_dir = 'recordings'
self._status_running = False
self._load_config()
@@ -812,6 +815,8 @@ class RecorderManager:
'log_file': config.get('general', 'log_file', fallback='recorder.log'),
}
self.output_dir = general['output_directory']
# Setup logging
self._setup_logging(general['log_level'], general['log_file'])
@@ -877,6 +882,32 @@ class RecorderManager:
)
self.logger = logging.getLogger(__name__)
def _write_status(self):
"""Write active filenames to status.json for the web UI to read."""
active = []
for r in self.recorders:
fn = r.current_filename
if fn:
try:
rel = os.path.relpath(fn, self.output_dir).replace('\\', '/')
active.append(rel)
except ValueError:
pass
try:
out = Path(self.output_dir)
out.mkdir(parents=True, exist_ok=True)
tmp = str(out / 'status.json.tmp')
with open(tmp, 'w') as fh:
json.dump({'active': active}, fh)
os.replace(tmp, str(out / 'status.json'))
except Exception:
pass
def _status_writer_loop(self):
while self._status_running:
self._write_status()
time.sleep(2)
def start(self):
"""Start all recorders in separate threads."""
if not self.recorders:
@@ -891,12 +922,15 @@ class RecorderManager:
self.threads.append(thread)
thread.start()
self._status_running = True
st = threading.Thread(target=self._status_writer_loop, name='StatusWriter', daemon=True)
st.start()
self.logger.info("All recorders started")
# Wait for interrupt
try:
while True:
# Check if all threads are still alive
alive = [t for t in self.threads if t.is_alive()]
if not alive:
self.logger.info("All recorders have stopped")
@@ -908,13 +942,21 @@ class RecorderManager:
def stop(self):
"""Stop all recorders gracefully."""
self._status_running = False
for recorder in self.recorders:
recorder.stop()
# Wait for threads to finish
for thread in self.threads:
thread.join(timeout=5)
# Clear status file so the web UI shows no active recordings
try:
status_path = Path(self.output_dir) / 'status.json'
if status_path.exists():
status_path.unlink()
except Exception:
pass
self.logger.info("All recorders stopped")