From a836948882f6d15d4f0c015f4f86f9a96efda2bd Mon Sep 17 00:00:00 2001 From: jonathan Date: Wed, 29 Jul 2026 20:31:27 +0200 Subject: [PATCH] fix: order the file listing by the filename clock, not mtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list_files sorted on mtime while `date` — and so the DATE column, the day grouping and both date filters — came from _recording_start(path.stem). The two disagree for any file whose mtime drifted from its name: a copy, a restore, an ffmpeg round-trip. Such a file landed in the right day group but at the wrong position inside it, and could drag its whole day group to the wrong place on the page. 'date' is fixed-width zero-padded, so sorting the string is already chronological — no extra field needed. Co-Authored-By: Claude Opus 5 --- tests/test_web.py | 13 +++++++++++++ web.py | 12 +++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index aab04b5..9c57238 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -218,6 +218,19 @@ def _write_wav(path, seconds=2, framerate=8000): wf.writeframes(b'\x00\x00' * (seconds * framerate)) +def test_listing_is_ordered_by_the_filename_clock_not_mtime(tmp_path): + # mtime is the last write (≈ the end of the recording) and drifts from the + # name on any copy or round-trip. Here the older recording is touched last. + import os + early, late = tmp_path / '20260523_080000.wav', tmp_path / '20260523_090000.wav' + _write_wav(early) + _write_wav(late) + os.utime(late, (1000, 1000)) # newest recording, oldest mtime + + names = [f['name'] for f in web.list_files(str(tmp_path))] + assert names == ['20260523_090000.wav', '20260523_080000.wav'] + + def test_non_16_bit_wav_is_rejected_rather_than_misread(tmp_path): # RMS windowing decodes as int16 regardless of the declared sample width, # so an 8-bit file used to produce meaningless sections with no warning. diff --git a/web.py b/web.py index d0b2fc1..2da2be9 100644 --- a/web.py +++ b/web.py @@ -566,8 +566,8 @@ def list_files(recordings_dir: str): # The recording start is encoded in the filename and is the true clock # anchor; mtime is only a fallback for files not in FILENAME_FORMAT. - started = _recording_start(path.stem) - date = (started or datetime.fromtimestamp(stat.st_mtime)).strftime('%Y-%m-%d %H:%M:%S') + started = _recording_start(path.stem) or datetime.fromtimestamp(stat.st_mtime) + date = started.strftime('%Y-%m-%d %H:%M:%S') # Skip reading partial headers for in-progress files — the WAV nframes # field and FLAC total_samples are both unfinalized while recording, @@ -584,7 +584,13 @@ def list_files(recordings_dir: str): 'recording': is_active, }) - files.sort(key=lambda f: f['mtime'], reverse=True) + # Sort by the recording start, not mtime: the filename is the clock, and + # everything the user sees — the DATE column, the day grouping, the date + # filters — is derived from it. mtime is the last write (≈ the end), so a + # file whose mtime drifted from its name (a copy, a restore, an ffmpeg + # round-trip) used to land at the wrong position within its own day. + # 'date' is fixed-width zero-padded, so string order is time order. + files.sort(key=lambda f: f['date'], reverse=True) return files