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:
@@ -218,6 +218,19 @@ def _write_wav(path, seconds=2, framerate=8000):
|
|||||||
wf.writeframes(b'\x00\x00' * (seconds * framerate))
|
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):
|
def test_non_16_bit_wav_is_rejected_rather_than_misread(tmp_path):
|
||||||
# RMS windowing decodes as int16 regardless of the declared sample width,
|
# RMS windowing decodes as int16 regardless of the declared sample width,
|
||||||
# so an 8-bit file used to produce meaningless sections with no warning.
|
# so an 8-bit file used to produce meaningless sections with no warning.
|
||||||
|
|||||||
@@ -566,8 +566,8 @@ def list_files(recordings_dir: str):
|
|||||||
|
|
||||||
# The recording start is encoded in the filename and is the true clock
|
# 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.
|
# anchor; mtime is only a fallback for files not in FILENAME_FORMAT.
|
||||||
started = _recording_start(path.stem)
|
started = _recording_start(path.stem) or datetime.fromtimestamp(stat.st_mtime)
|
||||||
date = (started or datetime.fromtimestamp(stat.st_mtime)).strftime('%Y-%m-%d %H:%M:%S')
|
date = started.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# Skip reading partial headers for in-progress files — the WAV nframes
|
# Skip reading partial headers for in-progress files — the WAV nframes
|
||||||
# field and FLAC total_samples are both unfinalized while recording,
|
# field and FLAC total_samples are both unfinalized while recording,
|
||||||
@@ -584,7 +584,13 @@ def list_files(recordings_dir: str):
|
|||||||
'recording': is_active,
|
'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
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user