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