mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-06 11:15:21 +03:00
Add support for storing the relationship between recordings and events in the RecordingsToEvents table
This commit is contained in:
parent
3921a7faa2
commit
7adea8f3c9
@ -5,6 +5,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess as sp
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@ -23,6 +24,7 @@ from flask import (
|
||||
jsonify,
|
||||
make_response,
|
||||
request,
|
||||
send_from_directory,
|
||||
)
|
||||
from peewee import DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
@ -38,7 +40,7 @@ from frigate.const import (
|
||||
RECORD_DIR,
|
||||
)
|
||||
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.plus import PlusApi
|
||||
from frigate.ptz.onvif import OnvifController
|
||||
@ -699,6 +701,66 @@ def label_snapshot(camera_name, label):
|
||||
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")
|
||||
def event_clip(id):
|
||||
download = request.args.get("download", type=bool)
|
||||
@ -1167,6 +1229,8 @@ def latest_frame(camera_name):
|
||||
"motion_boxes": request.args.get("motion", 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)
|
||||
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from peewee import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
CompositeKey,
|
||||
DateTimeField,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
@ -70,6 +71,15 @@ class Recordings(Model): # type: ignore[misc]
|
||||
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
|
||||
class RecordingsToDelete(Model): # type: ignore[misc]
|
||||
id = CharField(null=False, primary_key=False, max_length=30)
|
||||
|
||||
@ -66,6 +66,14 @@ class RecordingCleanup(threading.Thread):
|
||||
Recordings.delete().where(
|
||||
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||
).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("Start all cameras.")
|
||||
@ -154,6 +162,14 @@ class RecordingCleanup(threading.Thread):
|
||||
Recordings.delete().where(
|
||||
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||
).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}.")
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ from frigate.const import (
|
||||
MAX_SEGMENT_DURATION,
|
||||
RECORD_DIR,
|
||||
)
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.models import Event, Recordings, RecordingsToEvents
|
||||
from frigate.types import FeatureMetricsTypes
|
||||
from frigate.util.image import area
|
||||
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])
|
||||
)
|
||||
|
||||
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(
|
||||
self, camera: str, events: Event, recording: dict[str, any]
|
||||
) -> None:
|
||||
@ -221,6 +228,7 @@ class RecordingMaintainer(threading.Thread):
|
||||
):
|
||||
# if the cached segment overlaps with the events:
|
||||
overlaps = False
|
||||
overlapping_event_id = None
|
||||
for event in events:
|
||||
# if the event starts in the future, stop checking events
|
||||
# and remove this segment
|
||||
@ -234,12 +242,13 @@ class RecordingMaintainer(threading.Thread):
|
||||
# and stop looking at events
|
||||
if event.end_time is None or event.end_time >= start_time.timestamp():
|
||||
overlaps = True
|
||||
overlapping_event_id = event.id
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
record_mode = self.config.cameras[camera].record.events.retain.mode
|
||||
# move from cache to recordings immediately
|
||||
return await self.move_segment(
|
||||
recording_result = await self.move_segment(
|
||||
camera,
|
||||
start_time,
|
||||
end_time,
|
||||
@ -247,6 +256,24 @@ class RecordingMaintainer(threading.Thread):
|
||||
cache_path,
|
||||
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 ends more than the configured pre_capture for the camera
|
||||
else:
|
||||
|
||||
@ -173,6 +173,14 @@ class StorageMaintainer(threading.Thread):
|
||||
Recordings.delete().where(
|
||||
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||
).execute()
|
||||
"""
|
||||
TODO: right way
|
||||
|
||||
RecordingsToEvents.update(is_deleted=True).where(
|
||||
RecordingsToEvents.recording_id
|
||||
<< deleted_recordings_list[i : i + max_deletes]
|
||||
).execute()
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
"""Check every 5 minutes if storage needs to be cleaned up."""
|
||||
|
||||
37
migrations/019_recordings_to_events.py
Normal file
37
migrations/019_recordings_to_events.py
Normal 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")
|
||||
Loading…
Reference in New Issue
Block a user