feat: name cut clips by wall-clock time; fix recording filename format

Cut downloads were named by byte offsets (`..._cut_740s-750s.flac`). They are
now named by the actual recording time the slice covers, e.g.
`20260523_22-31-30_22-32-30.flac` for a 22:31:30->22:32:30 cut of a recording
started at 22:00:00.

To make this reliable, the recording filename is now a fixed
`%Y%m%d_%H%M%S` start-time format (`FILENAME_FORMAT`) shared by isr.py and
web.py, replacing the user-configurable `filename_pattern` (web.py never reads
config.ini, so a custom pattern could not be parsed back). web.py parses the
start time out of the filename via `_recording_start()` and builds cut names
with `_cut_filename()`. The DATE column now also comes from the filename
(falling back to mtime only for non-standard names), since mtime is the last
write, not the start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:30:30 +02:00
parent 2caf23f17d
commit 5e7620627b
7 changed files with 97 additions and 55 deletions
+5 -23
View File
@@ -142,7 +142,6 @@ class TestGetNextSplitTime:
r._clock = fixed_clock(now)
r.split_duration = split_minutes
r.output_dir = cfg["output_directory"]
r.filename_pattern = "%Y%m%d_%H%M%S"
r.max_retries = 3
r.retry_delay = 1
r.file_format = "auto"
@@ -184,7 +183,7 @@ class TestGetNextSplitTime:
class TestGenerateFilename:
"""Tests for BaseRecorder.generate_filename()."""
def _recorder(self, pattern: str, now: datetime, output_dir: str) -> isr.BaseRecorder:
def _recorder(self, now: datetime, output_dir: str) -> isr.BaseRecorder:
class _Rec(isr.BaseRecorder):
def record(self): pass
@@ -198,28 +197,21 @@ class TestGenerateFilename:
r._clock = fixed_clock(now)
r.split_duration = 60
r.output_dir = output_dir
r.filename_pattern = pattern
r.max_retries = 3
r.retry_delay = 1
r.file_format = "auto"
return r
def test_basic_pattern(self, tmp_path):
def test_fixed_format(self, tmp_path):
# Filenames are always <YYYYMMDD>_<HHMMSS>.<ext> — the recording start.
now = datetime(2024, 12, 25, 14, 30, 0)
r = self._recorder("%Y%m%d_%H%M%S", now, str(tmp_path))
r = self._recorder(now, str(tmp_path))
name = r.generate_filename("mp3")
assert name.endswith("20241225_143000.mp3")
def test_subdirectory_created(self, tmp_path):
now = datetime(2024, 12, 25, 14, 30, 0)
r = self._recorder("%Y/%m/%d/rec_%H%M%S", now, str(tmp_path))
name = r.generate_filename("ogg")
parent = Path(name).parent
assert parent.exists()
def test_output_dir_prefix(self, tmp_path):
now = datetime(2024, 1, 1, 0, 0, 0)
r = self._recorder("%Y%m%d", now, str(tmp_path))
r = self._recorder(now, str(tmp_path))
name = r.generate_filename("wav")
assert name.startswith(str(tmp_path))
@@ -236,7 +228,6 @@ class TestDetectFormat:
"url": "http://example.com/stream",
"output_directory": "/tmp",
"split_minutes": 60,
"filename_pattern": "%Y%m%d_%H%M%S",
"max_retries": 1,
"retry_delay_seconds": 0,
"format": "auto",
@@ -252,7 +243,6 @@ class TestDetectFormat:
r._clock = datetime.now
r.split_duration = 60
r.output_dir = "/tmp"
r.filename_pattern = "%Y%m%d_%H%M%S"
r.max_retries = 1
r.retry_delay = 0
r.file_format = "auto"
@@ -446,7 +436,6 @@ class TestRecorderManagerLoadConfig:
[general]
output_directory = {str(tmp_path / "recordings")}
split_minutes = 30
filename_pattern = test_%Y%m%d
max_retries = 3
retry_delay_seconds = 2
log_level = WARNING
@@ -491,7 +480,6 @@ format = ogg
[general]
output_directory = {str(tmp_path / "recordings")}
split_minutes = 60
filename_pattern = %Y%m%d_%H%M%S
max_retries = 10
retry_delay_seconds = 5
log_level = WARNING
@@ -501,7 +489,6 @@ log_file = {log_file}
type = stream
url = http://example.com/stream
split_minutes = 15
filename_pattern = custom_%Y%m%d
"""
cfg_path = tmp_path / "config.ini"
cfg_path.write_text(config_text)
@@ -512,7 +499,6 @@ filename_pattern = custom_%Y%m%d
rec = mgr.recorders[0]
assert rec.split_duration == 15
assert rec.filename_pattern == "custom_%Y%m%d"
def test_unknown_type_is_skipped(self, tmp_path):
log_file = str(tmp_path / "test.log")
@@ -558,7 +544,6 @@ class TestStreamRecorderRecord:
"url": "http://example.com/stream",
"output_directory": "", # overridden per-test with tmp_path
"split_minutes": 60,
"filename_pattern": "%Y%m%d_%H%M%S",
"max_retries": 2,
"retry_delay_seconds": 0,
"format": fmt,
@@ -610,7 +595,6 @@ class TestSoundcardRecorder:
"format": "wav",
"output_directory": str(tmp_path),
"split_minutes": 60,
"filename_pattern": "%Y%m%d_%H%M%S",
"max_retries": 1,
"retry_delay_seconds": 0,
}
@@ -639,7 +623,6 @@ class TestSoundcardRecorder:
"format": "flac",
"output_directory": str(tmp_path),
"split_minutes": 60,
"filename_pattern": "%Y%m%d_%H%M%S",
"max_retries": 1,
"retry_delay_seconds": 0,
}
@@ -662,7 +645,6 @@ class TestSoundcardRecorder:
"format": "flac",
"output_directory": str(tmp_path),
"split_minutes": 60,
"filename_pattern": "%Y%m%d_%H%M%S",
"max_retries": 1,
"retry_delay_seconds": 0,
}