From 138f6db0a59393ca9fef5b0df5464406b70db998 Mon Sep 17 00:00:00 2001 From: jonathan Date: Wed, 29 Jul 2026 21:29:57 +0200 Subject: [PATCH] refactor: one Range-capable file sender; HEAD support; assorted hygiene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit web.py * _download and _stream each had their own copy loop; _download had no Range support at all, so an interrupted download of a multi-hundred-MB recording had to restart. Both now go through _serve_bytes(), which keeps _stream's live-header `prefix` trick (the patched bytes are the same length as the header they replace, so Range math is unaffected). * do_HEAD answers / , /stream/ and /download/ as a body-less do_GET. /api/clip and /api/cut return 405 instead — generating a clip or shelling out to ffmpeg only to discard the result is worse than not answering. * _api_delete drops the deleted file's _DURATION_CACHE entry, which was never evicted. * WAV durations are rounded to 2dp like FLAC's already were. isr.py * _open_output_file flushed the buffer and then called close_current_file(), which is overridden to flush again. * log_level/log_file were merged into the dict copied into every source's config; no recorder reads them. Read them where they are used. webui.html * scoreOrder() is memoised on the section array's identity — U/I otherwise re-sorted a whole day (thousands of sections) per keypress. Verified with a byte-level harness: full/partial/open-ended/unsatisfiable ranges byte-identical to disk, download Range, HEAD on all three supported paths returning headers with an empty body, 405 on /api/clip, and live-header patching still correct both whole-file and for a range straddling the patched/disk boundary. Not done from the audit: BaseRecorder.generate_filename's mkdir per file open. It runs once per split (once an hour by default), so "once at startup suffices" was not worth a behaviour change. `nul` stays in .gitignore — it guards against a Windows shell artifact on a Windows box. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- README.md | 4 +- isr.py | 11 ++-- tests/test_web.py | 1 + web.py | 132 +++++++++++++++++++++++++--------------------- webui.html | 9 +++- 6 files changed, 91 insertions(+), 68 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b981d68..8ee77aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC - Clips: `_api_clip()` validates params, `_clip_wav()` / `_clip_flac()` stream the decoded slice, `_wav_header()` builds the 44-byte PCM header. - Filenames as a clock: `_recording_start()` parses the start time out of a filename stem; `_cut_filename()` turns a (stem, ext, start, end) into a wall-clock-named cut. Both the listing `date` field and `_api_cut()` use them. - Live headers: `_live_wav_header()`, `_live_flac_header()` (+ `_flac_frame_samples()`, CRC-8 verified). -- Serving: `_stream()` (Range support), `_copy_to_response()`, `_safe_path()` (path traversal guard). +- Serving: `_serve_bytes()` is the single Range-capable file sender — `_stream()` (live header patch via its `prefix` argument, `no_store`) and `_download()` (`Content-Disposition`) both go through it, and it is what makes `do_HEAD` a body-less `do_GET`. Plus `_copy_to_response()`, `_safe_path()` (path traversal guard). `webui.html` (one `