Make Snapshots path configurable

This commit is contained in:
Judah Rand 2024-03-25 18:03:15 +00:00
parent 258cd5b6d7
commit 43edbac4d0
6 changed files with 34 additions and 23 deletions

View File

@ -18,9 +18,8 @@ from flask import (
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
from frigate.const import ( from frigate.config import SnapshotsConfig
CLIPS_DIR, from frigate.const import CLIPS_DIR
)
from frigate.models import Event, Timeline from frigate.models import Event, Timeline
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
from frigate.util.builtin import get_tz_modifiers from frigate.util.builtin import get_tz_modifiers
@ -350,8 +349,9 @@ def send_to_plus(id):
# load clean.png # load clean.png
try: try:
snapshot_config: SnapshotsConfig = current_app.frigate_config.cameras[event.camera]
filename = f"{event.camera}-{event.id}-clean.png" filename = f"{event.camera}-{event.id}-clean.png"
image = cv2.imread(os.path.join(CLIPS_DIR, filename)) image = cv2.imread(os.path.join(snapshot_config.path, filename))
except Exception: except Exception:
logger.error(f"Unable to load clean png for event: {event.id}") logger.error(f"Unable to load clean png for event: {event.id}")
return make_response( return make_response(
@ -601,9 +601,10 @@ def delete_event(id):
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
if event.has_snapshot: if event.has_snapshot:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") snapshot_config: SnapshotsConfig = current_app.frigate_config.cameras[event.camera].snapshots
media = Path(f"{os.path.join(snapshot_config.path, media_name)}.jpg")
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png") media = Path(f"{os.path.join(snapshot_config.path, media_name)}-clean.png")
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
if event.has_clip: if event.has_clip:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4") media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")

View File

@ -25,6 +25,7 @@ from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name from tzlocal import get_localzone_name
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from frigate.config import SnapshotsConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
@ -978,8 +979,9 @@ def event_snapshot_clean(id):
) )
if png_bytes is None: if png_bytes is None:
try: try:
snapshot_config: SnapshotsConfig = current_app.frigate_config.cameras[event.camera].snapshots
clean_snapshot_path = os.path.join( clean_snapshot_path = os.path.join(
CLIPS_DIR, f"{event.camera}-{event.id}-clean.png" snapshot_config.path, f"{event.camera}-{event.id}-clean.png"
) )
if not os.path.exists(clean_snapshot_path): if not os.path.exists(clean_snapshot_path):
return make_response( return make_response(
@ -989,7 +991,7 @@ def event_snapshot_clean(id):
404, 404,
) )
with open( with open(
os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}-clean.png"), "rb" os.path.join(snapshot_config.path, f"{event.camera}-{event.id}-clean.png"), "rb"
) as image_file: ) as image_file:
png_bytes = image_file.read() png_bytes = image_file.read()
except Exception: except Exception:
@ -1023,8 +1025,9 @@ def event_snapshot(id):
jsonify({"success": False, "message": "Snapshot not available"}), 404 jsonify({"success": False, "message": "Snapshot not available"}), 404
) )
# read snapshot from disk # read snapshot from disk
snapshot_config: SnapshotsConfig = current_app.frigate_config.cameras[event.camera].snapshots
with open( with open(
os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg"), "rb" os.path.join(snapshot_config.path, f"{event.camera}-{event.id}.jpg"), "rb"
) as image_file: ) as image_file:
jpg_bytes = image_file.read() jpg_bytes = image_file.read()
except DoesNotExist: except DoesNotExist:

View File

@ -26,6 +26,7 @@ from frigate.const import (
AUDIO_MIN_CONFIDENCE, AUDIO_MIN_CONFIDENCE,
CACHE_DIR, CACHE_DIR,
CACHE_SEGMENT_FORMAT, CACHE_SEGMENT_FORMAT,
CLIPS_DIR,
DEFAULT_DB_PATH, DEFAULT_DB_PATH,
MAX_PRE_CAPTURE, MAX_PRE_CAPTURE,
REGEX_CAMERA_NAME, REGEX_CAMERA_NAME,
@ -722,6 +723,7 @@ class CameraFfmpegConfig(FfmpegConfig):
class SnapshotsConfig(FrigateBaseModel): class SnapshotsConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Snapshots enabled.") enabled: bool = Field(default=False, title="Snapshots enabled.")
path: str = Field(default=CLIPS_DIR, title="Snapshots path.")
clean_copy: bool = Field( clean_copy: bool = Field(
default=True, title="Create a clean copy of the snapshot image." default=True, title="Create a clean copy of the snapshot image."
) )

View File

@ -8,7 +8,7 @@ from enum import Enum
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path from pathlib import Path
from frigate.config import FrigateConfig from frigate.config import FrigateConfig, SnapshotsConfig
from frigate.const import CLIPS_DIR from frigate.const import CLIPS_DIR
from frigate.models import Event, Timeline from frigate.models import Event, Timeline
@ -64,10 +64,12 @@ class EventCleanup(threading.Thread):
def expire(self, media_type: EventCleanupType) -> list[str]: def expire(self, media_type: EventCleanupType) -> list[str]:
## Expire events from unlisted cameras based on the global config ## Expire events from unlisted cameras based on the global config
if media_type == EventCleanupType.clips: if media_type == EventCleanupType.clips:
base_dir = CLIPS_DIR
retain_config = self.config.record.events.retain retain_config = self.config.record.events.retain
file_extension = None # mp4 clips are no longer stored in /clips file_extension = None # mp4 clips are no longer stored in /clips
update_params = {"has_clip": False} update_params = {"has_clip": False}
else: else:
base_dir = self.config.snapshots.path
retain_config = self.config.snapshots.retain retain_config = self.config.snapshots.retain
file_extension = "jpg" file_extension = "jpg"
update_params = {"has_snapshot": False} update_params = {"has_snapshot": False}
@ -101,12 +103,12 @@ class EventCleanup(threading.Thread):
for expired in expired_events: for expired in expired_events:
media_name = f"{expired.camera}-{expired.id}" media_name = f"{expired.camera}-{expired.id}"
media_path = Path( media_path = Path(
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}" f"{os.path.join(base_dir, media_name)}.{file_extension}"
) )
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
if file_extension == "jpg": if file_extension == "jpg":
media_path = Path( media_path = Path(
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" f"{os.path.join(base_dir, media_name)}-clean.png"
) )
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
@ -124,8 +126,10 @@ class EventCleanup(threading.Thread):
## Expire events from cameras based on the camera config ## Expire events from cameras based on the camera config
for name, camera in self.config.cameras.items(): for name, camera in self.config.cameras.items():
if media_type == EventCleanupType.clips: if media_type == EventCleanupType.clips:
base_dir = CLIPS_DIR
retain_config = camera.record.events.retain retain_config = camera.record.events.retain
else: else:
base_dir = camera.snapshots.path
retain_config = camera.snapshots.retain retain_config = camera.snapshots.retain
# get distinct objects in database for this camera # get distinct objects in database for this camera
@ -165,11 +169,11 @@ class EventCleanup(threading.Thread):
if media_type == EventCleanupType.snapshots: if media_type == EventCleanupType.snapshots:
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
media_path = Path( media_path = Path(
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}" f"{os.path.join(base_dir, media_name)}.{file_extension}"
) )
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
media_path = Path( media_path = Path(
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" f"{os.path.join(base_dir, media_name)}-clean.png"
) )
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
@ -198,10 +202,11 @@ class EventCleanup(threading.Thread):
duplicate_events = Event.raw(duplicate_query) duplicate_events = Event.raw(duplicate_query)
for event in duplicate_events: for event in duplicate_events:
logger.debug(f"Removing duplicate: {event.id}") logger.debug(f"Removing duplicate: {event.id}")
snapshot_config: SnapshotsConfig = self.config.cameras[event.camera].snapshots
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") media_path = Path(f"{os.path.join(snapshot_config.path, media_name)}.jpg")
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png") media_path = Path(f"{os.path.join(snapshot_config.path, media_name)}-clean.png")
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
( (

View File

@ -11,8 +11,7 @@ from typing import Optional
import cv2 import cv2
from frigate.comms.events_updater import EventUpdatePublisher from frigate.comms.events_updater import EventUpdatePublisher
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, FrigateConfig, SnapshotsConfig
from frigate.const import CLIPS_DIR
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.util.image import draw_box_with_label from frigate.util.image import draw_box_with_label
@ -94,6 +93,8 @@ class ExternalEventProcessor:
draw: dict[str, any], draw: dict[str, any],
img_frame: any, img_frame: any,
) -> str: ) -> str:
snapshot_config: SnapshotsConfig = camera_config.snapshots
# write clean snapshot if enabled # write clean snapshot if enabled
if camera_config.snapshots.clean_copy: if camera_config.snapshots.clean_copy:
ret, png = cv2.imencode(".png", img_frame) ret, png = cv2.imencode(".png", img_frame)
@ -101,7 +102,7 @@ class ExternalEventProcessor:
if ret: if ret:
with open( with open(
os.path.join( os.path.join(
CLIPS_DIR, snapshot_config.path,
f"{camera_config.name}-{event_id}-clean.png", f"{camera_config.name}-{event_id}-clean.png",
), ),
"wb", "wb",
@ -130,7 +131,7 @@ class ExternalEventProcessor:
ret, jpg = cv2.imencode(".jpg", img_frame) ret, jpg = cv2.imencode(".jpg", img_frame)
with open( with open(
os.path.join(CLIPS_DIR, f"{camera_config.name}-{event_id}.jpg"), os.path.join(snapshot_config.path, f"{camera_config.name}-{event_id}.jpg"),
"wb", "wb",
) as j: ) as j:
j.write(jpg.tobytes()) j.write(jpg.tobytes())

View File

@ -23,7 +23,6 @@ from frigate.config import (
SnapshotsConfig, SnapshotsConfig,
ZoomingModeEnum, ZoomingModeEnum,
) )
from frigate.const import CLIPS_DIR
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.util.image import ( from frigate.util.image import (
@ -897,7 +896,7 @@ class TrackedObjectProcessor(threading.Thread):
logger.warning(f"Unable to save snapshot for {obj.obj_data['id']}.") logger.warning(f"Unable to save snapshot for {obj.obj_data['id']}.")
else: else:
with open( with open(
os.path.join(CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"), os.path.join(snapshot_config.path, f"{camera}-{obj.obj_data['id']}.jpg"),
"wb", "wb",
) as j: ) as j:
j.write(jpg_bytes) j.write(jpg_bytes)
@ -912,7 +911,7 @@ class TrackedObjectProcessor(threading.Thread):
else: else:
with open( with open(
os.path.join( os.path.join(
CLIPS_DIR, snapshot_config.path,
f"{camera}-{obj.obj_data['id']}-clean.png", f"{camera}-{obj.obj_data['id']}-clean.png",
), ),
"wb", "wb",