Add support for storing the relationship between recordings and events in the RecordingsToEvents table

This commit is contained in:
Sergey Krashevich 2023-08-11 00:54:18 +03:00
parent 3921a7faa2
commit 7adea8f3c9
No known key found for this signature in database
GPG Key ID: 625171324E7D3856
6 changed files with 165 additions and 3 deletions

View File

@ -5,6 +5,7 @@ import json
import logging import logging
import os import os
import subprocess as sp import subprocess as sp
import tempfile
import time import time
import traceback import traceback
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -23,6 +24,7 @@ from flask import (
jsonify, jsonify,
make_response, make_response,
request, request,
send_from_directory,
) )
from peewee import DoesNotExist, fn, operator from peewee import DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
@ -38,7 +40,7 @@ from frigate.const import (
RECORD_DIR, RECORD_DIR,
) )
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Recordings, Timeline from frigate.models import Event, Recordings, RecordingsToEvents, Timeline
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
@ -699,6 +701,66 @@ def label_snapshot(camera_name, label):
return response return response
@bp.route("/events/<id>/record.mp3")
def event_audio(id):
download = request.args.get("download", type=bool)
try:
event: Event = Event.get(Event.id == id)
except DoesNotExist:
return "Event not found.", 404
recordings = (
Recordings.select(Recordings.path)
.join(RecordingsToEvents, on=(Recordings.id == RecordingsToEvents.recording_id))
.where(RecordingsToEvents.event_id == event.id)
)
# Extract file paths from the query
file_paths = [rec.path for rec in recordings]
# Generate a temporary output file name for the combined MP3
output_file = tempfile.NamedTemporaryFile(
prefix=id, suffix=".mp3", delete=False
).name
os.unlink(output_file) # fucking python
# Create a list of inputs for FFmpeg
ffmpeg_inputs = sum([["-i", path] for path in file_paths], [])
# Use FFmpeg to extract audio from each mp4 and combine into a single MP3
cmd = [
"ffmpeg",
"-y", # fucking python #2
*ffmpeg_inputs,
"-filter_complex",
"concat=n={}:v=0:a=1[aout]".format(len(file_paths)),
"-map",
"[aout]",
"-vn",
output_file,
]
logger.debug(f"ffmpeg command for {id}/record.mp3: {cmd}")
sp.run(cmd)
if not os.path.exists(output_file):
return "Error processing audio files.", 500
# Trigger download if requested
if download:
return send_from_directory(
os.path.dirname(output_file),
os.path.basename(output_file),
as_attachment=True,
attachment_filename=f"event-{id}.mp3",
)
# Otherwise, just return the combined file's path or content
# Depending on your needs, you can directly stream the audio or just provide the path
return send_from_directory(
os.path.dirname(output_file), os.path.basename(output_file)
)
@bp.route("/events/<id>/clip.mp4") @bp.route("/events/<id>/clip.mp4")
def event_clip(id): def event_clip(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
@ -1167,6 +1229,8 @@ def latest_frame(camera_name):
"motion_boxes": request.args.get("motion", type=int), "motion_boxes": request.args.get("motion", type=int),
"regions": request.args.get("regions", type=int), "regions": request.args.get("regions", type=int),
} }
# TODO: debug print draw_options
logger.debug(f"Drawing options for {camera_name}: {draw_options}")
resize_quality = request.args.get("quality", default=70, type=int) resize_quality = request.args.get("quality", default=70, type=int)
if camera_name in current_app.frigate_config.cameras: if camera_name in current_app.frigate_config.cameras:

View File

@ -1,6 +1,7 @@
from peewee import ( from peewee import (
BooleanField, BooleanField,
CharField, CharField,
CompositeKey,
DateTimeField, DateTimeField,
FloatField, FloatField,
IntegerField, IntegerField,
@ -70,6 +71,15 @@ class Recordings(Model): # type: ignore[misc]
segment_size = FloatField(default=0) # this should be stored as MB segment_size = FloatField(default=0) # this should be stored as MB
class RecordingsToEvents(Model): # type: ignore[misc]
event_id = CharField(null=False, index=True, max_length=30)
recording_id = CharField(null=False, index=True, max_length=30)
class Meta:
db_table = "recordingstoevents"
primary_key = CompositeKey("event", "recording")
# Used for temporary table in record/cleanup.py # Used for temporary table in record/cleanup.py
class RecordingsToDelete(Model): # type: ignore[misc] class RecordingsToDelete(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=False, max_length=30) id = CharField(null=False, primary_key=False, max_length=30)

View File

@ -66,6 +66,14 @@ class RecordingCleanup(threading.Thread):
Recordings.delete().where( Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes] Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute() ).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
logger.debug("End deleted cameras.") logger.debug("End deleted cameras.")
logger.debug("Start all cameras.") logger.debug("Start all cameras.")
@ -154,6 +162,14 @@ class RecordingCleanup(threading.Thread):
Recordings.delete().where( Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes] Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute() ).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
logger.debug(f"End camera: {camera}.") logger.debug(f"End camera: {camera}.")

View File

@ -24,7 +24,7 @@ from frigate.const import (
MAX_SEGMENT_DURATION, MAX_SEGMENT_DURATION,
RECORD_DIR, RECORD_DIR,
) )
from frigate.models import Event, Recordings from frigate.models import Event, Recordings, RecordingsToEvents
from frigate.types import FeatureMetricsTypes from frigate.types import FeatureMetricsTypes
from frigate.util.image import area from frigate.util.image import area
from frigate.util.services import get_video_properties from frigate.util.services import get_video_properties
@ -173,6 +173,13 @@ class RecordingMaintainer(threading.Thread):
(INSERT_MANY_RECORDINGS, [r for r in recordings_to_insert if r is not None]) (INSERT_MANY_RECORDINGS, [r for r in recordings_to_insert if r is not None])
) )
def store_recording_to_event_relation(
self, recording_id: str, event_id: str
) -> None:
"""Store the relationship between a recording and an event in the RecordingsToEvents table."""
relation = RecordingsToEvents(recording=recording_id, event=event_id)
relation.save()
async def validate_and_move_segment( async def validate_and_move_segment(
self, camera: str, events: Event, recording: dict[str, any] self, camera: str, events: Event, recording: dict[str, any]
) -> None: ) -> None:
@ -221,6 +228,7 @@ class RecordingMaintainer(threading.Thread):
): ):
# if the cached segment overlaps with the events: # if the cached segment overlaps with the events:
overlaps = False overlaps = False
overlapping_event_id = None
for event in events: for event in events:
# if the event starts in the future, stop checking events # if the event starts in the future, stop checking events
# and remove this segment # and remove this segment
@ -234,12 +242,13 @@ class RecordingMaintainer(threading.Thread):
# and stop looking at events # and stop looking at events
if event.end_time is None or event.end_time >= start_time.timestamp(): if event.end_time is None or event.end_time >= start_time.timestamp():
overlaps = True overlaps = True
overlapping_event_id = event.id
break break
if overlaps: if overlaps:
record_mode = self.config.cameras[camera].record.events.retain.mode record_mode = self.config.cameras[camera].record.events.retain.mode
# move from cache to recordings immediately # move from cache to recordings immediately
return await self.move_segment( recording_result = await self.move_segment(
camera, camera,
start_time, start_time,
end_time, end_time,
@ -247,6 +256,24 @@ class RecordingMaintainer(threading.Thread):
cache_path, cache_path,
record_mode, record_mode,
) )
if recording_result:
try:
# Store the relation in the RecordingsToEvents table
self.store_recording_to_event_relation(
recording_result.id, overlapping_event_id
)
logging.debug(
f"Successfully stored relation for recording_id: {recording_result}, event_id: {overlapping_event_id} in RecordingsToEvents table"
)
except Exception as e:
logging.error(
f"Failed to store relation r:{recording_result},e:{overlapping_event_id} in RecordingsToEvents table: {str(e)}"
)
else:
logging.debug(
f"No recording result available for overlapping event_id {overlapping_event_id}"
)
return recording_result
# if it doesn't overlap with an event, go ahead and drop the segment # if it doesn't overlap with an event, go ahead and drop the segment
# if it ends more than the configured pre_capture for the camera # if it ends more than the configured pre_capture for the camera
else: else:

View File

@ -173,6 +173,14 @@ class StorageMaintainer(threading.Thread):
Recordings.delete().where( Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes] Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute() ).execute()
"""
TODO: right way
RecordingsToEvents.update(is_deleted=True).where(
RecordingsToEvents.recording_id
<< deleted_recordings_list[i : i + max_deletes]
).execute()
"""
def run(self): def run(self):
"""Check every 5 minutes if storage needs to be cleaned up.""" """Check every 5 minutes if storage needs to be cleaned up."""

View File

@ -0,0 +1,37 @@
# migrations/019_recordings_to_events.py
from peewee import CharField, CompositeKey, Model
from frigate.models import Recordings, Event
def migrate(migrator, database, fake=False, **kwargs):
"""Write your migrations here."""
@migrator.create_model
class RecordingsToEvents(Model): # type: ignore[misc]
event_id = CharField(null=False, index=True, max_length=30)
recording_id = CharField(null=False, index=True, max_length=30)
class Meta:
db_table = "recordingstoevents"
primary_key = CompositeKey("event_id", "recording_id")
sql = """
INSERT INTO recordingstoevents (recording_id, event_id)
SELECT
r.id AS recording,
e.id AS event
FROM
event e
JOIN
recordings r ON e.camera = r.camera
WHERE
r.start_time <= e.end_time
AND r.end_time >= e.start_time;
"""
migrator.sql(sql)
def rollback(migrator, database, fake=False, **kwargs):
"""This function is used to undo the migration, i.e., to drop the RecordingToEvent table."""
migrator.drop_table("recordingstoevents")