fix: Docker volume path, graceful shutdown deadline, inline stream headers
docker-compose.yml: mount ./recordings at /app/recordings (matches output_directory = recordings in config.ini); previously the recorder wrote to /app/recordings while the web container read from /recordings, causing all files to appear missing — explaining the 9-byte Not-found download from /stream/ and the 0-byte recordings in the UI. Add stop_grace_period: 30s so Docker waits long enough for files to close. isr.py: replace per-thread join(timeout=5) with a shared 25 s deadline; with N recorders the old code could exceed Docker's SIGKILL window and leave WAV/FLAC files unclosed (corrupt headers). web.py: add Content-Disposition: inline to /stream/ responses so browsers never treat the audio response as a file download. CLAUDE.md: document web.py endpoints, status.json lifecycle, corrected Docker volume layout, and web.py CLI flags.
This commit is contained in:
@@ -15,8 +15,10 @@ python isr.py myconfig.ini # custom config file
|
||||
python isr.py --list-devices # list available ALSA devices
|
||||
|
||||
# Run the web UI
|
||||
python web.py # http://localhost:8080
|
||||
python web.py --dir recordings # custom recordings directory
|
||||
python web.py # http://localhost:8080
|
||||
python web.py --dir recordings # custom recordings directory
|
||||
python web.py --port 8888 # custom port
|
||||
python web.py --threshold 0.03 # loudness threshold (0-1, default 0.05)
|
||||
|
||||
# Stop: Ctrl+C (or docker compose down)
|
||||
|
||||
@@ -53,6 +55,16 @@ docker compose down
|
||||
- OGG/Opus/FLAC headers captured from first ~16 KB of stream and prepended to each split file
|
||||
- File splits aligned to time period boundaries (`get_next_split_time()`)
|
||||
- SIGTERM handled in `main()` so Docker `docker compose down` shuts down cleanly
|
||||
- `RecorderManager._write_status()` atomically writes `recordings/status.json` every 2 s while running; deleted on clean shutdown so the web UI shows no stale active-recording badges
|
||||
|
||||
### Web UI (web.py)
|
||||
- **`GET /`** — Single-page archive table; lists all recordings sorted newest first
|
||||
- **`GET /api/files`** — JSON list of file metadata (name, size, date, duration, ext, recording flag)
|
||||
- **`GET /api/analyze?file=<path>`** — RMS loudness analysis for WAV and FLAC files; returns waveform data, loud sections, and duration. Requires `numpy` and `soundfile` for FLAC.
|
||||
- **`GET /api/status`** — Returns `{"active": [...]}` from `status.json`; used by the UI to animate the REC badge on in-progress files (polled every 5 s)
|
||||
- **`GET /stream/<path>`** — Serves audio for inline `<audio>` playback with full HTTP Range support (seekable). Responds 206 Partial Content for range requests. Files are served with `Content-Disposition: inline`.
|
||||
- **`GET /download/<path>`** — Serves audio as a file download (`Content-Disposition: attachment`)
|
||||
- All paths are validated against the recordings directory to prevent path traversal.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -60,10 +72,10 @@ Copy `config.example.ini` to `config.ini`. Each section defines a recording sour
|
||||
- `type = stream` — HTTP/Icecast stream recording
|
||||
- `type = soundcard` — ALSA device recording
|
||||
|
||||
In Docker, set `output_directory = /recordings` and `log_file = /recordings/recorder.log`.
|
||||
The `output_directory` value is used as-is: a relative path like `recordings` resolves to `recordings/` next to `isr.py`. No Docker-specific config change is needed — the docker-compose.yml mounts `./recordings` at `/app/recordings` to match this default.
|
||||
|
||||
## Docker
|
||||
|
||||
Two services share a `./recordings` bind mount:
|
||||
- `recorder` — runs `isr.py`, maps `/dev/snd` for ALSA access
|
||||
- `web` — runs `web.py`, read-only access to recordings, exposes port 8080
|
||||
- `recorder` — runs `isr.py`; volume mounted at `/app/recordings` (matches `output_directory = recordings`); maps `/dev/snd` for ALSA access; `stop_grace_period: 30s` so open files are closed before SIGKILL
|
||||
- `web` — runs `web.py`; same `./recordings` bind mounted read-only at `/recordings`; exposes port 8080
|
||||
|
||||
+2
-1
@@ -3,11 +3,12 @@ services:
|
||||
build: .
|
||||
volumes:
|
||||
- ./config.ini:/app/config.ini:ro
|
||||
- ./recordings:/recordings
|
||||
- ./recordings:/app/recordings # matches output_directory = recordings in config.ini
|
||||
# Soundcard (ALSA) access — comment out if you only record streams
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s # allow time for open files to be closed cleanly
|
||||
|
||||
web:
|
||||
build: .
|
||||
|
||||
@@ -946,8 +946,12 @@ class RecorderManager:
|
||||
for recorder in self.recorders:
|
||||
recorder.stop()
|
||||
|
||||
# Use a shared deadline so N recorders don't each burn 5 s sequentially,
|
||||
# which would exceed Docker's stop_grace_period for more than 2 recorders.
|
||||
deadline = time.time() + 25
|
||||
for thread in self.threads:
|
||||
thread.join(timeout=5)
|
||||
remaining = max(0.1, deadline - time.time())
|
||||
thread.join(timeout=remaining)
|
||||
|
||||
# Clear status file so the web UI shows no active recordings
|
||||
try:
|
||||
|
||||
@@ -337,6 +337,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
|
||||
self.send_response(206)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header('Content-Disposition', f'inline; filename="{path.name}"')
|
||||
self.send_header('Content-Range', f'bytes {start}-{end}/{size}')
|
||||
self.send_header('Content-Length', str(length))
|
||||
self.send_header('Accept-Ranges', 'bytes')
|
||||
@@ -354,6 +355,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header('Content-Disposition', f'inline; filename="{path.name}"')
|
||||
self.send_header('Content-Length', str(size))
|
||||
self.send_header('Accept-Ranges', 'bytes')
|
||||
self.end_headers()
|
||||
|
||||
Reference in New Issue
Block a user