From 16237c69ad33d8e7445138a3f333a6dd2b8f0735 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:50:20 -0600 Subject: [PATCH] ensure emergency cleanup also sets has_clip on overlapping events improves https://github.com/blakeblackshear/frigate/discussions/20945 --- frigate/storage.py | 55 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/frigate/storage.py b/frigate/storage.py index 611412e1e..3e88a06aa 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -113,6 +113,7 @@ class StorageMaintainer(threading.Thread): recordings: Recordings = ( Recordings.select( Recordings.id, + Recordings.camera, Recordings.start_time, Recordings.end_time, Recordings.segment_size, @@ -137,7 +138,7 @@ class StorageMaintainer(threading.Thread): ) event_start = 0 - deleted_recordings = set() + deleted_recordings = [] for recording in recordings: # check if 1 hour of storage has been reclaimed if deleted_segments_size > hourly_bandwidth: @@ -172,7 +173,7 @@ class StorageMaintainer(threading.Thread): if not keep: try: clear_and_unlink(Path(recording.path), missing_ok=False) - deleted_recordings.add(recording.id) + deleted_recordings.append(recording) deleted_segments_size += recording.segment_size except FileNotFoundError: # this file was not found so we must assume no space was cleaned up @@ -186,6 +187,9 @@ class StorageMaintainer(threading.Thread): recordings = ( Recordings.select( Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, Recordings.path, Recordings.segment_size, ) @@ -201,7 +205,7 @@ class StorageMaintainer(threading.Thread): try: clear_and_unlink(Path(recording.path), missing_ok=False) deleted_segments_size += recording.segment_size - deleted_recordings.add(recording.id) + deleted_recordings.append(recording) except FileNotFoundError: # this file was not found so we must assume no space was cleaned up pass @@ -211,7 +215,50 @@ class StorageMaintainer(threading.Thread): logger.debug(f"Expiring {len(deleted_recordings)} recordings") # delete up to 100,000 at a time max_deletes = 100000 - deleted_recordings_list = list(deleted_recordings) + + # Update has_clip for events that overlap with deleted recordings + if deleted_recordings: + # Group deleted recordings by camera + camera_recordings = {} + for recording in deleted_recordings: + if recording.camera not in camera_recordings: + camera_recordings[recording.camera] = { + "min_start": recording.start_time, + "max_end": recording.end_time, + } + else: + camera_recordings[recording.camera]["min_start"] = min( + camera_recordings[recording.camera]["min_start"], + recording.start_time, + ) + camera_recordings[recording.camera]["max_end"] = max( + camera_recordings[recording.camera]["max_end"], + recording.end_time, + ) + + # Find all events that overlap with deleted recordings time range per camera + events_to_update = [] + for camera, time_range in camera_recordings.items(): + overlapping_events = Event.select(Event.id).where( + Event.camera == camera, + Event.has_clip == True, + Event.start_time < time_range["max_end"], + Event.end_time > time_range["min_start"], + ) + + for event in overlapping_events: + events_to_update.append(event.id) + + # Update has_clip to False for overlapping events + if events_to_update: + for i in range(0, len(events_to_update), max_deletes): + batch = events_to_update[i : i + max_deletes] + Event.update(has_clip=False).where(Event.id << batch).execute() + logger.info( + f"Updated has_clip to False for {len(events_to_update)} events" + ) + + deleted_recordings_list = [r.id for r in deleted_recordings] for i in range(0, len(deleted_recordings_list), max_deletes): Recordings.delete().where( Recordings.id << deleted_recordings_list[i : i + max_deletes]