mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-22 20:18:30 +03:00
implement case management for export apis (#21295)
This commit is contained in:
parent
b962c95725
commit
5cced22f65
@ -23,3 +23,13 @@ class ExportCaseUpdateBody(BaseModel):
|
|||||||
description: Optional[str] = Field(
|
description: Optional[str] = Field(
|
||||||
default=None, description="Updated description of the export case"
|
default=None, description="Updated description of the export case"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportCaseAssignBody(BaseModel):
|
||||||
|
"""Request body for assigning or unassigning an export to a case."""
|
||||||
|
|
||||||
|
export_case_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
max_length=30,
|
||||||
|
description="Case ID to assign to the export, or null to unassign",
|
||||||
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic.json_schema import SkipJsonSchema
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
@ -18,3 +18,9 @@ class ExportRecordingsBody(BaseModel):
|
|||||||
)
|
)
|
||||||
name: str = Field(title="Friendly name", default=None, max_length=256)
|
name: str = Field(title="Friendly name", default=None, max_length=256)
|
||||||
image_path: Union[str, SkipJsonSchema[None]] = None
|
image_path: Union[str, SkipJsonSchema[None]] = None
|
||||||
|
export_case_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
title="Export case ID",
|
||||||
|
max_length=30,
|
||||||
|
description="ID of the export case to assign this export to",
|
||||||
|
)
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Query, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pathvalidate import sanitize_filepath
|
from pathvalidate import sanitize_filepath
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
@ -20,6 +20,7 @@ from frigate.api.auth import (
|
|||||||
require_role,
|
require_role,
|
||||||
)
|
)
|
||||||
from frigate.api.defs.request.export_case_body import (
|
from frigate.api.defs.request.export_case_body import (
|
||||||
|
ExportCaseAssignBody,
|
||||||
ExportCaseCreateBody,
|
ExportCaseCreateBody,
|
||||||
ExportCaseUpdateBody,
|
ExportCaseUpdateBody,
|
||||||
)
|
)
|
||||||
@ -60,14 +61,32 @@ router = APIRouter(tags=[Tags.export])
|
|||||||
)
|
)
|
||||||
def get_exports(
|
def get_exports(
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
|
export_case_id: Optional[str] = None,
|
||||||
|
camera: Optional[List[str]] = Query(default=None),
|
||||||
|
start_date: Optional[float] = None,
|
||||||
|
end_date: Optional[float] = None,
|
||||||
):
|
):
|
||||||
exports = (
|
query = Export.select().where(Export.camera << allowed_cameras)
|
||||||
Export.select()
|
|
||||||
.where(Export.camera << allowed_cameras)
|
if export_case_id is not None:
|
||||||
.order_by(Export.date.desc())
|
if export_case_id == "unassigned":
|
||||||
.dicts()
|
query = query.where(Export.export_case.is_null(True))
|
||||||
.iterator()
|
else:
|
||||||
)
|
query = query.where(Export.export_case == export_case_id)
|
||||||
|
|
||||||
|
if camera:
|
||||||
|
filtered_cameras = [c for c in camera if c in allowed_cameras]
|
||||||
|
if not filtered_cameras:
|
||||||
|
return JSONResponse(content=[])
|
||||||
|
query = query.where(Export.camera << filtered_cameras)
|
||||||
|
|
||||||
|
if start_date is not None:
|
||||||
|
query = query.where(Export.date >= start_date)
|
||||||
|
|
||||||
|
if end_date is not None:
|
||||||
|
query = query.where(Export.date <= end_date)
|
||||||
|
|
||||||
|
exports = query.order_by(Export.date.desc()).dicts().iterator()
|
||||||
return JSONResponse(content=[e for e in exports])
|
return JSONResponse(content=[e for e in exports])
|
||||||
|
|
||||||
|
|
||||||
@ -175,6 +194,48 @@ def delete_export_case(case_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/export/{export_id}/case",
|
||||||
|
response_model=GenericResponse,
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
summary="Assign export to case",
|
||||||
|
description=(
|
||||||
|
"Assigns an export to a case, or unassigns it if export_case_id is null."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def assign_export_case(
|
||||||
|
export_id: str,
|
||||||
|
body: ExportCaseAssignBody,
|
||||||
|
request: Request,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
export: Export = Export.get(Export.id == export_id)
|
||||||
|
await require_camera_access(export.camera, request=request)
|
||||||
|
except DoesNotExist:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Export not found."},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
if body.export_case_id is not None:
|
||||||
|
try:
|
||||||
|
ExportCase.get(ExportCase.id == body.export_case_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Export case not found."},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
export.export_case = body.export_case_id
|
||||||
|
else:
|
||||||
|
export.export_case = None
|
||||||
|
|
||||||
|
export.save()
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": True, "message": "Successfully updated export case."}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/export/{camera_name}/start/{start_time}/end/{end_time}",
|
"/export/{camera_name}/start/{start_time}/end/{end_time}",
|
||||||
response_model=StartExportResponse,
|
response_model=StartExportResponse,
|
||||||
@ -205,6 +266,16 @@ def export_recording(
|
|||||||
friendly_name = body.name
|
friendly_name = body.name
|
||||||
existing_image = sanitize_filepath(body.image_path) if body.image_path else None
|
existing_image = sanitize_filepath(body.image_path) if body.image_path else None
|
||||||
|
|
||||||
|
export_case_id = body.export_case_id
|
||||||
|
if export_case_id is not None:
|
||||||
|
try:
|
||||||
|
ExportCase.get(ExportCase.id == export_case_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Export case not found"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure that existing_image is a valid path
|
# Ensure that existing_image is a valid path
|
||||||
if existing_image and not existing_image.startswith(CLIPS_DIR):
|
if existing_image and not existing_image.startswith(CLIPS_DIR):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -273,6 +344,7 @@ def export_recording(
|
|||||||
if playback_source in PlaybackSourceEnum.__members__.values()
|
if playback_source in PlaybackSourceEnum.__members__.values()
|
||||||
else PlaybackSourceEnum.recordings
|
else PlaybackSourceEnum.recordings
|
||||||
),
|
),
|
||||||
|
export_case_id,
|
||||||
)
|
)
|
||||||
exporter.start()
|
exporter.start()
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
@ -64,6 +64,7 @@ class RecordingExporter(threading.Thread):
|
|||||||
end_time: int,
|
end_time: int,
|
||||||
playback_factor: PlaybackFactorEnum,
|
playback_factor: PlaybackFactorEnum,
|
||||||
playback_source: PlaybackSourceEnum,
|
playback_source: PlaybackSourceEnum,
|
||||||
|
export_case_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = config
|
self.config = config
|
||||||
@ -75,6 +76,7 @@ class RecordingExporter(threading.Thread):
|
|||||||
self.end_time = end_time
|
self.end_time = end_time
|
||||||
self.playback_factor = playback_factor
|
self.playback_factor = playback_factor
|
||||||
self.playback_source = playback_source
|
self.playback_source = playback_source
|
||||||
|
self.export_case_id = export_case_id
|
||||||
|
|
||||||
# ensure export thumb dir
|
# ensure export thumb dir
|
||||||
Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True)
|
Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True)
|
||||||
@ -348,17 +350,20 @@ class RecordingExporter(threading.Thread):
|
|||||||
video_path = f"{EXPORT_DIR}/{self.camera}_{filename_start_datetime}-{filename_end_datetime}_{cleaned_export_id}.mp4"
|
video_path = f"{EXPORT_DIR}/{self.camera}_{filename_start_datetime}-{filename_end_datetime}_{cleaned_export_id}.mp4"
|
||||||
thumb_path = self.save_thumbnail(self.export_id)
|
thumb_path = self.save_thumbnail(self.export_id)
|
||||||
|
|
||||||
Export.insert(
|
export_values = {
|
||||||
{
|
Export.id: self.export_id,
|
||||||
Export.id: self.export_id,
|
Export.camera: self.camera,
|
||||||
Export.camera: self.camera,
|
Export.name: export_name,
|
||||||
Export.name: export_name,
|
Export.date: self.start_time,
|
||||||
Export.date: self.start_time,
|
Export.video_path: video_path,
|
||||||
Export.video_path: video_path,
|
Export.thumb_path: thumb_path,
|
||||||
Export.thumb_path: thumb_path,
|
Export.in_progress: True,
|
||||||
Export.in_progress: True,
|
}
|
||||||
}
|
|
||||||
).execute()
|
if self.export_case_id is not None:
|
||||||
|
export_values[Export.export_case] = self.export_case_id
|
||||||
|
|
||||||
|
Export.insert(export_values).execute()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.playback_source == PlaybackSourceEnum.recordings:
|
if self.playback_source == PlaybackSourceEnum.recordings:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user