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 <noreply@anthropic.com>