Implement tiered recording

This commit is contained in:
Nicolas Mowen 2025-05-30 13:28:56 -06:00
parent 20e0addae1
commit 55c598fc5a
2 changed files with 50 additions and 21 deletions

View File

@ -22,27 +22,31 @@ __all__ = [
DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
class RecordRetainConfig(FrigateBaseModel):
days: float = Field(default=0, ge=0, title="Default retention period.")
class RetainModeEnum(str, Enum): class RetainModeEnum(str, Enum):
all = "all" all = "all"
motion = "motion" motion = "motion"
active_objects = "active_objects" active_objects = "active_objects"
class RecordRetainConfig(FrigateBaseModel):
days: float = Field(default=0, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
class ReviewRetainConfig(FrigateBaseModel): class ReviewRetainConfig(FrigateBaseModel):
days: float = Field(default=10, title="Default retention period.") days: float = Field(default=10, ge=10, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.") mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
class EventsConfig(FrigateBaseModel): class EventsConfig(FrigateBaseModel):
pre_capture: int = Field( pre_capture: int = Field(
default=5, title="Seconds to retain before event starts.", le=MAX_PRE_CAPTURE default=5,
title="Seconds to retain before event starts.",
le=MAX_PRE_CAPTURE,
ge=0,
)
post_capture: int = Field(
default=5, ge=0, title="Seconds to retain after event ends."
) )
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
retain: ReviewRetainConfig = Field( retain: ReviewRetainConfig = Field(
default_factory=ReviewRetainConfig, title="Event retention settings." default_factory=ReviewRetainConfig, title="Event retention settings."
) )
@ -77,8 +81,12 @@ class RecordConfig(FrigateBaseModel):
default=60, default=60,
title="Number of minutes to wait between cleanup runs.", title="Number of minutes to wait between cleanup runs.",
) )
retain: RecordRetainConfig = Field( continuous: RecordRetainConfig = Field(
default_factory=RecordRetainConfig, title="Record retention settings." default_factory=RecordRetainConfig,
title="Continuous recording retention settings.",
)
motion: RecordRetainConfig = Field(
default_factory=RecordRetainConfig, title="Motion recording retention settings."
) )
detections: EventsConfig = Field( detections: EventsConfig = Field(
default_factory=EventsConfig, title="Detection specific retention settings." default_factory=EventsConfig, title="Detection specific retention settings."

View File

@ -100,7 +100,11 @@ class RecordingCleanup(threading.Thread):
).execute() ).execute()
def expire_existing_camera_recordings( def expire_existing_camera_recordings(
self, expire_date: float, config: CameraConfig, reviews: ReviewSegment self,
continuous_expire_date: float,
motion_expire_date: float,
config: CameraConfig,
reviews: ReviewSegment,
) -> None: ) -> None:
"""Delete recordings for existing camera based on retention config.""" """Delete recordings for existing camera based on retention config."""
# Get the timestamp for cutoff of retained days # Get the timestamp for cutoff of retained days
@ -116,8 +120,14 @@ class RecordingCleanup(threading.Thread):
Recordings.motion, Recordings.motion,
) )
.where( .where(
Recordings.camera == config.name, (Recordings.camera == config.name)
Recordings.end_time < expire_date, & (
(
(Recordings.end_time < continuous_expire_date)
& (Recordings.motion == 0)
)
| (Recordings.end_time < motion_expire_date)
)
) )
.order_by(Recordings.start_time) .order_by(Recordings.start_time)
.namedtuples() .namedtuples()
@ -188,7 +198,7 @@ class RecordingCleanup(threading.Thread):
Recordings.id << deleted_recordings_list[i : i + max_deletes] Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute() ).execute()
previews: Previews = ( previews: list[Previews] = (
Previews.select( Previews.select(
Previews.id, Previews.id,
Previews.start_time, Previews.start_time,
@ -196,8 +206,14 @@ class RecordingCleanup(threading.Thread):
Previews.path, Previews.path,
) )
.where( .where(
Previews.camera == config.name, (Recordings.camera == config.name)
Previews.end_time < expire_date, & (
(
(Recordings.end_time < continuous_expire_date)
& (Recordings.motion == 0)
)
| (Recordings.end_time < motion_expire_date)
)
) )
.order_by(Previews.start_time) .order_by(Previews.start_time)
.namedtuples() .namedtuples()
@ -291,9 +307,12 @@ class RecordingCleanup(threading.Thread):
now = datetime.datetime.now() now = datetime.datetime.now()
self.expire_review_segments(config, now) self.expire_review_segments(config, now)
continuous_expire_date = (
expire_days = config.record.retain.days now - datetime.timedelta(days=config.record.continuous.days)
expire_date = (now - datetime.timedelta(days=expire_days)).timestamp() ).timestamp()
motion_expire_date = (
now - datetime.timedelta(days=config.record.motion.days)
).timestamp()
# Get all the reviews to check against # Get all the reviews to check against
reviews: ReviewSegment = ( reviews: ReviewSegment = (
@ -306,13 +325,15 @@ class RecordingCleanup(threading.Thread):
ReviewSegment.camera == camera, ReviewSegment.camera == camera,
# need to ensure segments for all reviews starting # need to ensure segments for all reviews starting
# before the expire date are included # before the expire date are included
ReviewSegment.start_time < expire_date, ReviewSegment.start_time < motion_expire_date,
) )
.order_by(ReviewSegment.start_time) .order_by(ReviewSegment.start_time)
.namedtuples() .namedtuples()
) )
self.expire_existing_camera_recordings(expire_date, config, reviews) self.expire_existing_camera_recordings(
continuous_expire_date, motion_expire_date, config, reviews
)
logger.debug(f"End camera: {camera}.") logger.debug(f"End camera: {camera}.")
logger.debug("End all cameras.") logger.debug("End all cameras.")