diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index 88b6a9eda5..1369ab463d 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -365,6 +365,7 @@ class EventCleanup(threading.Thread): chunk = ids_to_delete[i : i + CHUNK_SIZE] logger.debug(f"Deleting {len(chunk)} events from the database") Event.delete().where(Event.id << chunk).execute() + Timeline.delete().where(Timeline.source_id << chunk).execute() # embeddings are always cleaned up, even when semantic search # is disabled, so that they don't outlive their events diff --git a/frigate/test/test_timeline.py b/frigate/test/test_timeline.py new file mode 100644 index 0000000000..a438528ab1 --- /dev/null +++ b/frigate/test/test_timeline.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import MagicMock, patch + +from frigate.events.types import EventStateEnum +from frigate.models import Timeline +from frigate.timeline import TimelineProcessor + + +def make_event(has_clip: bool, has_snapshot: bool) -> dict: + return { + "id": "event-1", + "frame_time": 1000.0, + "box": [0, 0, 10, 10], + "region": [0, 0, 100, 100], + "label": "car", + "sub_label": None, + "score": 0.8, + "has_clip": has_clip, + "has_snapshot": has_snapshot, + "current_zones": [], + "stationary": False, + "attributes": {}, + "current_attributes": [], + } + + +class TestTimelineProcessor(unittest.TestCase): + def setUp(self): + camera_config = MagicMock() + camera_config.detect.width = 1280 + camera_config.detect.height = 720 + config = MagicMock() + config.cameras.get.return_value = camera_config + self.processor = TimelineProcessor(config, MagicMock(), MagicMock()) + + @patch.object(Timeline, "insert") + def test_unsaved_event_writes_no_timeline_rows(self, insert): + event = make_event(has_clip=False, has_snapshot=False) + self.processor.handle_object_detection( + "front", EventStateEnum.start, None, event + ) + self.processor.handle_object_detection( + "front", EventStateEnum.end, event, event + ) + + insert.assert_not_called() + self.assertEqual(self.processor.pre_event_cache, {}) + + @patch.object(Timeline, "insert") + def test_cached_entries_flush_when_event_is_saved(self, insert): + start = make_event(has_clip=False, has_snapshot=False) + self.processor.handle_object_detection( + "front", EventStateEnum.start, None, start + ) + insert.assert_not_called() + + end = make_event(has_clip=True, has_snapshot=False) + self.processor.handle_object_detection("front", EventStateEnum.end, start, end) + + class_types = [c.args[0][Timeline.class_type] for c in insert.call_args_list] + self.assertEqual(class_types, ["visible", "gone"]) + self.assertEqual(self.processor.pre_event_cache, {}) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/timeline.py b/frigate/timeline.py index d82f17cb7d..cc864c7bc3 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -191,6 +191,9 @@ class TimelineProcessor(threading.Thread): timeline_entry[Timeline.class_type] = "gone" self.insert_or_save(timeline_entry, prev_event_data, event_data) + # drop entries for events that ended without being saved + self.pre_event_cache.pop(event_id, None) + def handle_api_entry( self, camera: str,