From 3a54b9251a9aa4cc0adb7483ad86a09050ae3ede Mon Sep 17 00:00:00 2001 From: Rui Alves Date: Sun, 22 Sep 2024 16:13:27 +0100 Subject: [PATCH] Revert media routers to old names. Order routes to make sure the dynamic ones from media.py are only used whenever there's no match on auth/etc --- frigate/api/defs/media_query_parameters.py | 6 +- frigate/api/fastapi_app.py | 8 +- frigate/api/media.py | 181 +++++++++--------- web/src/components/camera/CameraImage.tsx | 2 +- .../components/camera/ResizingCameraImage.tsx | 2 +- web/src/components/card/AnimatedEventCard.tsx | 2 +- .../overlay/detail/ObjectLifecycle.tsx | 4 +- .../overlay/detail/ReviewDetailDialog.tsx | 8 +- .../overlay/dialog/FrigatePlusDialog.tsx | 2 +- .../player/dynamic/DynamicVideoPlayer.tsx | 4 +- .../components/preview/ScrubbablePreview.tsx | 2 +- web/src/components/settings/PolygonCanvas.tsx | 2 +- web/src/views/live/LiveCameraView.tsx | 2 +- 13 files changed, 114 insertions(+), 111 deletions(-) diff --git a/frigate/api/defs/media_query_parameters.py b/frigate/api/defs/media_query_parameters.py index 0b3e821c2..b7df85d30 100644 --- a/frigate/api/defs/media_query_parameters.py +++ b/frigate/api/defs/media_query_parameters.py @@ -12,7 +12,6 @@ class Extension(str, Enum): class MediaLatestFrameQueryParams(BaseModel): - extension: Extension = Extension.webp bbox: Optional[int] = None timestamp: Optional[int] = None zones: Optional[int] = None @@ -22,14 +21,16 @@ class MediaLatestFrameQueryParams(BaseModel): quality: Optional[int] = 70 height: Optional[int] = None + class MediaEventsSnapshotQueryParams(BaseModel): - download: bool = False + download: Optional[bool] = False timestamp: Optional[int] = None bbox: Optional[int] = None crop: Optional[int] = None height: Optional[int] = None quality: Optional[int] = 70 + class MediaMjpegFeedQueryParams(BaseModel): fps: int = 3 height: int = 360 @@ -39,4 +40,3 @@ class MediaMjpegFeedQueryParams(BaseModel): mask: Optional[int] = None motion: Optional[int] = None regions: Optional[int] = None - diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 20e1b5fba..94fffa1e7 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -79,19 +79,21 @@ def create_fastapi_app( database.close() return response + # Rate limiter (used for login endpoint) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) # Routes + # Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters + app.include_router(auth.router) + app.include_router(review.router) app.include_router(main_app.router) - app.include_router(media.router) app.include_router(preview.router) app.include_router(notification.router) - app.include_router(review.router) app.include_router(export.router) app.include_router(event.router) - app.include_router(auth.router) + app.include_router(media.router) # App Properties app.frigate_config = frigate_config app.embeddings = embeddings diff --git a/frigate/api/media.py b/frigate/api/media.py index 40779a16f..bfea0f185 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -20,6 +20,7 @@ from peewee import DoesNotExist, fn from tzlocal import get_localzone_name from frigate.api.defs.media_query_parameters import ( + Extension, MediaEventsSnapshotQueryParams, MediaLatestFrameQueryParams, MediaMjpegFeedQueryParams, @@ -48,7 +49,7 @@ def secure_filename(file_name: str): return file_name -@router.get("/media/camera/{camera_name}") +@router.get("{camera_name}") def mjpeg_feed( request: Request, camera_name: str, @@ -101,7 +102,7 @@ def imagestream( ) -@router.get("/media/camera/{camera_name}/ptz/info") +@router.get("/{camera_name}/ptz/info") def camera_ptz_info(request: Request, camera_name: str): if camera_name in request.app.frigate_config.cameras: return JSONResponse( @@ -114,10 +115,11 @@ def camera_ptz_info(request: Request, camera_name: str): ) -@router.get("/media/camera/{camera_name}/frame/latest") +@router.get("/{camera_name}/latest.{extension}") def latest_frame( request: Request, camera_name: str, + extension: Extension, params: MediaLatestFrameQueryParams = Depends(), ): draw_options = { @@ -129,7 +131,6 @@ def latest_frame( "regions": params.regions, } quality = params.quality - extension = params.extension if camera_name in request.app.frigate_config.cameras: frame = request.app.detected_frames_processor.get_current_frame( @@ -207,7 +208,7 @@ def latest_frame( ) -@router.get("/media/camera/{camera_name}/recordings/{frame_time}/snapshot.{format}") +@router.get("/{camera_name}/recordings/{frame_time}/snapshot.{format}") def get_snapshot_from_recording( request: Request, camera_name: str, @@ -268,7 +269,7 @@ def get_snapshot_from_recording( ) -@router.post("/media/camera/{camera_name}/plus/{frame_time}") +@router.post("/{camera_name}/plus/{frame_time}") def submit_recording_snapshot_to_plus( request: Request, camera_name: str, frame_time: str ): @@ -332,7 +333,7 @@ def submit_recording_snapshot_to_plus( ) -@router.get("/media/recordings/storage") +@router.get("/recordings/storage") def get_recordings_storage_usage(request: Request): recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ "storage" @@ -356,7 +357,7 @@ def get_recordings_storage_usage(request: Request): return JSONResponse(content=camera_usages) -@router.get("/media/camera/{camera_name}/recordings/summary") +@router.get("/{camera_name}/recordings/summary") def recordings_summary(camera_name: str, timezone: str = "utc"): """Returns hourly summary for recordings of given camera""" hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(timezone) @@ -418,7 +419,7 @@ def recordings_summary(camera_name: str, timezone: str = "utc"): return JSONResponse(content=list(days.values())) -@router.get("/media/camera/{camera_name}/recordings") +@router.get("/{camera_name}/recordings") def recordings( camera_name: str, after: float = (datetime.now() - timedelta(hours=1)).timestamp(), @@ -448,7 +449,7 @@ def recordings( return JSONResponse(content=list(recordings)) -@router.get("/media/camera/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4") +@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4") def recording_clip( request: Request, camera_name: str, @@ -696,7 +697,73 @@ def vod_event(event_id: str): ) -@router.get("/media/camera/{camera_name}/label/{label}/snapshot.jpg") +@router.get("/events/{event_id}/snapshot.jpg") +def event_snapshot( + request: Request, + event_id: str, + params: MediaEventsSnapshotQueryParams = Depends(), +): + event_complete = False + jpg_bytes = None + try: + event = Event.get(Event.id == event_id, Event.end_time != None) + event_complete = True + if not event.has_snapshot: + return JSONResponse( + content={"success": False, "message": "Snapshot not available"}, + status_code=404, + ) + # read snapshot from disk + with open( + os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg"), "rb" + ) as image_file: + jpg_bytes = image_file.read() + except DoesNotExist: + # see if the object is currently being tracked + try: + camera_states = request.app.detected_frames_processor.camera_states.values() + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + jpg_bytes = tracked_obj.get_jpg_bytes( + timestamp=params.timestamp, + bounding_box=params.bbox, + crop=params.crop, + height=params.height, + quality=params.quality, + ) + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + if jpg_bytes is None: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + headers = { + "Content-Type": "image/jpeg", + "Cache-Control": "private, max-age=31536000" if event_complete else "no-store", + } + + if params.download: + headers["Content-Disposition"] = f"attachment; filename=snapshot-{event_id}.jpg" + + return StreamingResponse( + io.BytesIO(jpg_bytes), + media_type="image/jpeg", + headers=headers, + ) + + +@router.get("/{camera_name}/{label}/snapshot.jpg") def label_snapshot(request: Request, camera_name: str, label: str): """Returns the snapshot image from the latest event for the given camera and label combo""" label = unquote(label) @@ -729,8 +796,8 @@ def label_snapshot(request: Request, camera_name: str, label: str): ) -@router.get("/media/camera/{camera_name}/label/{label}/best.jpg") -@router.get("/media/camera/{camera_name}/label/{label}/thumbnail.jpg") +@router.get("/{camera_name}/{label}/best.jpg") +@router.get("/{camera_name}/{label}/thumbnail.jpg") def label_thumbnail(request: Request, camera_name: str, label: str): label = unquote(label) event_query = Event.select(fn.MAX(Event.id)).where(Event.camera == camera_name) @@ -752,7 +819,7 @@ def label_thumbnail(request: Request, camera_name: str, label: str): ) -@router.get("/media/camera/{camera_name}/label/{label}/clip.mp4") +@router.get("/{camera_name}/{label}/clip.mp4") def label_clip(request: Request, camera_name: str, label: str): label = unquote(label) event_query = Event.select(fn.MAX(Event.id)).where( @@ -771,7 +838,7 @@ def label_clip(request: Request, camera_name: str, label: str): ) -@router.get("/media/camera/{camera_name}/grid.jpg") +@router.get("/{camera_name}/grid.jpg") def grid_snapshot( request: Request, camera_name: str, color: str = "green", font_scale: float = 0.5 ): @@ -892,7 +959,7 @@ def grid_snapshot( ) -@router.get("/media/events/{event_id}/snapshot-clean.png") +@router.get("/events/{event_id}/snapshot-clean.png") def event_snapshot_clean(request: Request, event_id: str, download: bool = False): png_bytes = None try: @@ -976,73 +1043,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False ) -@router.get("/media/events/{event_id}/snapshot.jpg") -def event_snapshot( - request: Request, - event_id: str, - params: MediaEventsSnapshotQueryParams = Depends(), -): - event_complete = False - jpg_bytes = None - try: - event = Event.get(Event.id == event_id, Event.end_time != None) - event_complete = True - if not event.has_snapshot: - return JSONResponse( - content={"success": False, "message": "Snapshot not available"}, - status_code=404, - ) - # read snapshot from disk - with open( - os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg"), "rb" - ) as image_file: - jpg_bytes = image_file.read() - except DoesNotExist: - # see if the object is currently being tracked - try: - camera_states = request.app.detected_frames_processor.camera_states.values() - for camera_state in camera_states: - if event_id in camera_state.tracked_objects: - tracked_obj = camera_state.tracked_objects.get(event_id) - if tracked_obj is not None: - jpg_bytes = tracked_obj.get_jpg_bytes( - timestamp=params.timestamp, - bounding_box=params.bbox, - crop=params.crop, - height=params.height, - quality=params.quality, - ) - except Exception: - return JSONResponse( - content={"success": False, "message": "Event not found"}, - status_code=404, - ) - except Exception: - return JSONResponse( - content={"success": False, "message": "Event not found"}, status_code=404 - ) - - if jpg_bytes is None: - return JSONResponse( - content={"success": False, "message": "Event not found"}, status_code=404 - ) - - headers = { - "Content-Type": "image/jpeg", - "Cache-Control": "private, max-age=31536000" if event_complete else "no-store", - } - - if params.download: - headers["Content-Disposition"] = f"attachment; filename=snapshot-{event_id}.jpg" - - return StreamingResponse( - io.BytesIO(jpg_bytes), - media_type="image/jpeg", - headers=headers, - ) - - -@router.get("/media/events/{event_id}/clip.mp4") +@router.get("/events/{event_id}/clip.mp4") def event_clip(request: Request, event_id: str, download: bool = False): try: event: Event = Event.get(Event.id == event_id) @@ -1085,7 +1086,7 @@ def event_clip(request: Request, event_id: str, download: bool = False): ) -@router.get("/media/events/{event_id}/thumbnail.jpg") +@router.get("/events/{event_id}/thumbnail.jpg") def event_thumbnail( request: Request, event_id: str, @@ -1150,7 +1151,7 @@ def event_thumbnail( ) -@router.get("/media/events/{event_id}/preview.gif") +@router.get("/events/{event_id}/preview.gif") def event_preview(request: Request, event_id: str): try: event: Event = Event.get(Event.id == event_id) @@ -1166,7 +1167,7 @@ def event_preview(request: Request, event_id: str): return preview_gif(request, event.camera, start_ts, end_ts) -@router.get("/media/camera/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif") +@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif") def preview_gif( request: Request, camera_name: str, @@ -1322,7 +1323,7 @@ def preview_gif( ) -@router.get("/media/camera/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4") +@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4") def preview_mp4( request: Request, camera_name: str, @@ -1498,7 +1499,7 @@ def preview_mp4( ) -@router.get("/media/review/{event_id}/preview") +@router.get("/review/{event_id}/preview") def review_preview( request: Request, event_id: str, @@ -1524,8 +1525,8 @@ def review_preview( return preview_mp4(request, review.camera, start_ts, end_ts) -@router.get("/media/preview/{file_name}/thumbnail.jpg") -@router.get("/media/preview/{file_name}/thumbnail.webp") +@router.get("/preview/{file_name}/thumbnail.jpg") +@router.get("/preview/{file_name}/thumbnail.webp") def preview_thumbnail(file_name: str): """Get a thumbnail from the cached preview frames.""" if len(file_name) > 1000: diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 9999d6cf1..ba35d643e 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -54,7 +54,7 @@ export default function CameraImage({ return; } - const newSrc = `${apiHost}api/media/camera/${name}/frame/latest?extension=webp&?height=${requestHeight}${ + const newSrc = `${apiHost}api/${name}/latest.webp?height=${requestHeight}${ searchParams ? `&${searchParams}` : "" }`; diff --git a/web/src/components/camera/ResizingCameraImage.tsx b/web/src/components/camera/ResizingCameraImage.tsx index 69ce64690..81545c625 100644 --- a/web/src/components/camera/ResizingCameraImage.tsx +++ b/web/src/components/camera/ResizingCameraImage.tsx @@ -89,7 +89,7 @@ export default function CameraImage({ if (!config || scaledHeight === 0 || !canvasRef.current) { return; } - img.src = `${apiHost}api/media/camera/${name}/frame/latest?extension=webp&height=${scaledHeight}${ + img.src = `${apiHost}api/${name}/latest.webp?height=${scaledHeight}${ searchParams ? `&${searchParams}` : "" }`; }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index 761f461ff..fd8096ebc 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -184,7 +184,7 @@ export function AnimatedEventCard({ }} > diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index c63a2f44c..3c642c12b 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -163,13 +163,13 @@ export default function ObjectLifecycle({ // image const [src, setSrc] = useState( - `${apiHost}api/media/camera/${event.camera}/recordings/${event.start_time + annotationOffset / 1000}/snapshot.jpg?height=500`, + `${apiHost}api/${event.camera}/recordings/${event.start_time + annotationOffset / 1000}/snapshot.jpg?height=500`, ); const [hasError, setHasError] = useState(false); useEffect(() => { if (timeIndex) { - const newSrc = `${apiHost}api/media/camera/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`; + const newSrc = `${apiHost}api/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`; setSrc(newSrc); } setImgLoaded(false); diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index 492ccc992..6701fa6d7 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -302,8 +302,8 @@ function EventItem({ draggable={false} src={ event.has_snapshot - ? `${apiHost}api/media/events/${event.id}/snapshot.jpg` - : `${apiHost}api/media/events/${event.id}/thumbnail.jpg` + ? `${apiHost}api/events/${event.id}/snapshot.jpg` + : `${apiHost}api/events/${event.id}/thumbnail.jpg` } /> {hovered && ( @@ -317,8 +317,8 @@ function EventItem({ download href={ event.has_snapshot - ? `${apiHost}api/media/events/${event.id}/snapshot.jpg` - : `${apiHost}api/media/events/${event.id}/thumbnail.jpg` + ? `${apiHost}api/events/${event.id}/snapshot.jpg` + : `${apiHost}api/events/${event.id}/thumbnail.jpg` } > diff --git a/web/src/components/overlay/dialog/FrigatePlusDialog.tsx b/web/src/components/overlay/dialog/FrigatePlusDialog.tsx index 5d6883cf6..42f17109b 100644 --- a/web/src/components/overlay/dialog/FrigatePlusDialog.tsx +++ b/web/src/components/overlay/dialog/FrigatePlusDialog.tsx @@ -117,7 +117,7 @@ export function FrigatePlusDialog({ {upload?.id && ( {`${upload?.label}`} )} diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 063734476..6c4e28e27 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -149,7 +149,7 @@ export default function DynamicVideoPlayer({ } const time = controller.getProgress(playTime); - return axios.post(`/media/camera/${camera}/plus/${time}`); + return axios.post(`/${camera}/plus/${time}`); }, [camera, controller], ); @@ -164,7 +164,7 @@ export default function DynamicVideoPlayer({ [timeRange], ); const { data: recordings } = useSWR( - [`media/camera/${camera}/recordings`, recordingParams], + [`${camera}/recordings`, recordingParams], { revalidateOnFocus: false }, ); diff --git a/web/src/components/preview/ScrubbablePreview.tsx b/web/src/components/preview/ScrubbablePreview.tsx index fb6073869..f3aa4f158 100644 --- a/web/src/components/preview/ScrubbablePreview.tsx +++ b/web/src/components/preview/ScrubbablePreview.tsx @@ -432,7 +432,7 @@ export function InProgressPreview({
{showProgress && ( diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 8cf7faac1..27f01ab97 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -42,7 +42,7 @@ export function PolygonCanvas({ const element = new window.Image(); element.width = width; element.height = height; - element.src = `${apiHost}api/media/camera/${camera}/frame/latest?extension=webp&?cache=${Date.now()}`; + element.src = `${apiHost}api/${camera}/latest?extension=webp&?cache=${Date.now()}`; return element; } // we know that these deps are correct diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 7e493615f..7bc81c39c 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -500,7 +500,7 @@ function PtzControlPanel({ setClickOverlay: React.Dispatch>; }) { const { data: ptz } = useSWR( - `/media/camera/${camera}/ptz/info`, + `${camera}/ptz/info`, ); const { send: sendPtz } = usePtzCommand(camera);