fix: order the file listing by the filename clock, not mtime

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 20:31:27 +02:00
co-authored by Claude Opus 5
parent b661e166ed
commit a836948882
2 changed files with 22 additions and 3 deletions
+13
View File
@@ -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.
+9 -3
View File
@@ -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