diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index d60d7ccce..25c9d2cff 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -97,6 +97,7 @@ class RecordingMaintainer(threading.Thread): self.object_recordings_info: dict[str, list] = defaultdict(list) self.audio_recordings_info: dict[str, list] = defaultdict(list) self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} + self.unexpected_cache_files_logged: bool = False async def move_files(self) -> None: cache_files = [ @@ -112,7 +113,14 @@ class RecordingMaintainer(threading.Thread): for cache in cache_files: cache_path = os.path.join(CACHE_DIR, cache) basename = os.path.splitext(cache)[0] - camera, date = basename.rsplit("@", maxsplit=1) + try: + camera, date = basename.rsplit("@", maxsplit=1) + except ValueError: + if not self.unexpected_cache_files_logged: + logger.warning("Skipping unexpected files in cache") + self.unexpected_cache_files_logged = True + continue + start_time = datetime.datetime.strptime( date, CACHE_SEGMENT_FORMAT ).astimezone(datetime.timezone.utc) @@ -164,7 +172,13 @@ class RecordingMaintainer(threading.Thread): cache_path = os.path.join(CACHE_DIR, cache) basename = os.path.splitext(cache)[0] - camera, date = basename.rsplit("@", maxsplit=1) + try: + camera, date = basename.rsplit("@", maxsplit=1) + except ValueError: + if not self.unexpected_cache_files_logged: + logger.warning("Skipping unexpected files in cache") + self.unexpected_cache_files_logged = True + continue # important that start_time is utc because recordings are stored and compared in utc start_time = datetime.datetime.strptime( diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py new file mode 100644 index 000000000..d978cfd9f --- /dev/null +++ b/frigate/test/test_maintainer.py @@ -0,0 +1,66 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock complex imports before importing maintainer +sys.modules["frigate.comms.inter_process"] = MagicMock() +sys.modules["frigate.comms.detections_updater"] = MagicMock() +sys.modules["frigate.comms.recordings_updater"] = MagicMock() +sys.modules["frigate.config.camera.updater"] = MagicMock() + +# Now import the class under test +from frigate.config import FrigateConfig # noqa: E402 +from frigate.record.maintainer import RecordingMaintainer # noqa: E402 + + +class TestMaintainer(unittest.IsolatedAsyncioTestCase): + async def test_move_files_survives_bad_filename(self): + config = MagicMock(spec=FrigateConfig) + config.cameras = {} + stop_event = MagicMock() + + maintainer = RecordingMaintainer(config, stop_event) + + # We need to mock end_time_cache to avoid key errors if logic proceeds + maintainer.end_time_cache = {} + + # Mock filesystem + # One bad file, one good file + files = ["bad_filename.mp4", "camera@20210101000000+0000.mp4"] + + with patch("os.listdir", return_value=files): + with patch("os.path.isfile", return_value=True): + with patch( + "frigate.record.maintainer.psutil.process_iter", return_value=[] + ): + with patch("frigate.record.maintainer.logger.warning") as warn: + # Mock validate_and_move_segment to avoid further logic + maintainer.validate_and_move_segment = MagicMock() + + try: + await maintainer.move_files() + except ValueError as e: + if "not enough values to unpack" in str(e): + self.fail("move_files() crashed on bad filename!") + raise e + except Exception: + # Ignore other errors (like DB connection) as we only care about the unpack crash + pass + + # The bad filename is encountered in multiple loops, but should only warn once. + matching = [ + c + for c in warn.call_args_list + if c.args + and isinstance(c.args[0], str) + and "Skipping unexpected files in cache" in c.args[0] + ] + self.assertEqual( + 1, + len(matching), + f"Expected a single warning for unexpected files, got {len(matching)}", + ) + + +if __name__ == "__main__": + unittest.main()