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

This commit is contained in:
Rui Alves 2024-09-22 16:13:27 +01:00
parent fcb54adb5f
commit 3a54b9251a
13 changed files with 114 additions and 111 deletions

View File

@ -12,7 +12,6 @@ class Extension(str, Enum):
class MediaLatestFrameQueryParams(BaseModel): class MediaLatestFrameQueryParams(BaseModel):
extension: Extension = Extension.webp
bbox: Optional[int] = None bbox: Optional[int] = None
timestamp: Optional[int] = None timestamp: Optional[int] = None
zones: Optional[int] = None zones: Optional[int] = None
@ -22,14 +21,16 @@ class MediaLatestFrameQueryParams(BaseModel):
quality: Optional[int] = 70 quality: Optional[int] = 70
height: Optional[int] = None height: Optional[int] = None
class MediaEventsSnapshotQueryParams(BaseModel): class MediaEventsSnapshotQueryParams(BaseModel):
download: bool = False download: Optional[bool] = False
timestamp: Optional[int] = None timestamp: Optional[int] = None
bbox: Optional[int] = None bbox: Optional[int] = None
crop: Optional[int] = None crop: Optional[int] = None
height: Optional[int] = None height: Optional[int] = None
quality: Optional[int] = 70 quality: Optional[int] = 70
class MediaMjpegFeedQueryParams(BaseModel): class MediaMjpegFeedQueryParams(BaseModel):
fps: int = 3 fps: int = 3
height: int = 360 height: int = 360
@ -39,4 +40,3 @@ class MediaMjpegFeedQueryParams(BaseModel):
mask: Optional[int] = None mask: Optional[int] = None
motion: Optional[int] = None motion: Optional[int] = None
regions: Optional[int] = None regions: Optional[int] = None

View File

@ -79,19 +79,21 @@ def create_fastapi_app(
database.close() database.close()
return response return response
# Rate limiter (used for login endpoint)
app.state.limiter = limiter app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware) app.add_middleware(SlowAPIMiddleware)
# Routes # 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(main_app.router)
app.include_router(media.router)
app.include_router(preview.router) app.include_router(preview.router)
app.include_router(notification.router) app.include_router(notification.router)
app.include_router(review.router)
app.include_router(export.router) app.include_router(export.router)
app.include_router(event.router) app.include_router(event.router)
app.include_router(auth.router) app.include_router(media.router)
# App Properties # App Properties
app.frigate_config = frigate_config app.frigate_config = frigate_config
app.embeddings = embeddings app.embeddings = embeddings

View File

@ -20,6 +20,7 @@ from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name from tzlocal import get_localzone_name
from frigate.api.defs.media_query_parameters import ( from frigate.api.defs.media_query_parameters import (
Extension,
MediaEventsSnapshotQueryParams, MediaEventsSnapshotQueryParams,
MediaLatestFrameQueryParams, MediaLatestFrameQueryParams,
MediaMjpegFeedQueryParams, MediaMjpegFeedQueryParams,
@ -48,7 +49,7 @@ def secure_filename(file_name: str):
return file_name return file_name
@router.get("/media/camera/{camera_name}") @router.get("{camera_name}")
def mjpeg_feed( def mjpeg_feed(
request: Request, request: Request,
camera_name: str, 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): def camera_ptz_info(request: Request, camera_name: str):
if camera_name in request.app.frigate_config.cameras: if camera_name in request.app.frigate_config.cameras:
return JSONResponse( 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( def latest_frame(
request: Request, request: Request,
camera_name: str, camera_name: str,
extension: Extension,
params: MediaLatestFrameQueryParams = Depends(), params: MediaLatestFrameQueryParams = Depends(),
): ):
draw_options = { draw_options = {
@ -129,7 +131,6 @@ def latest_frame(
"regions": params.regions, "regions": params.regions,
} }
quality = params.quality quality = params.quality
extension = params.extension
if camera_name in request.app.frigate_config.cameras: if camera_name in request.app.frigate_config.cameras:
frame = request.app.detected_frames_processor.get_current_frame( 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( def get_snapshot_from_recording(
request: Request, request: Request,
camera_name: str, 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( def submit_recording_snapshot_to_plus(
request: Request, camera_name: str, frame_time: str 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): def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage" "storage"
@ -356,7 +357,7 @@ def get_recordings_storage_usage(request: Request):
return JSONResponse(content=camera_usages) 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"): def recordings_summary(camera_name: str, timezone: str = "utc"):
"""Returns hourly summary for recordings of given camera""" """Returns hourly summary for recordings of given camera"""
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(timezone) 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())) return JSONResponse(content=list(days.values()))
@router.get("/media/camera/{camera_name}/recordings") @router.get("/{camera_name}/recordings")
def recordings( def recordings(
camera_name: str, camera_name: str,
after: float = (datetime.now() - timedelta(hours=1)).timestamp(), after: float = (datetime.now() - timedelta(hours=1)).timestamp(),
@ -448,7 +449,7 @@ def recordings(
return JSONResponse(content=list(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( def recording_clip(
request: Request, request: Request,
camera_name: str, 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): 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""" """Returns the snapshot image from the latest event for the given camera and label combo"""
label = unquote(label) 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("/{camera_name}/{label}/best.jpg")
@router.get("/media/camera/{camera_name}/label/{label}/thumbnail.jpg") @router.get("/{camera_name}/{label}/thumbnail.jpg")
def label_thumbnail(request: Request, camera_name: str, label: str): def label_thumbnail(request: Request, camera_name: str, label: str):
label = unquote(label) label = unquote(label)
event_query = Event.select(fn.MAX(Event.id)).where(Event.camera == camera_name) 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): def label_clip(request: Request, camera_name: str, label: str):
label = unquote(label) label = unquote(label)
event_query = Event.select(fn.MAX(Event.id)).where( 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( def grid_snapshot(
request: Request, camera_name: str, color: str = "green", font_scale: float = 0.5 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): def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
png_bytes = None png_bytes = None
try: 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") @router.get("/events/{event_id}/clip.mp4")
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")
def event_clip(request: Request, event_id: str, download: bool = False): def event_clip(request: Request, event_id: str, download: bool = False):
try: try:
event: Event = Event.get(Event.id == event_id) 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( def event_thumbnail(
request: Request, request: Request,
event_id: str, 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): def event_preview(request: Request, event_id: str):
try: try:
event: Event = Event.get(Event.id == event_id) 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) 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( def preview_gif(
request: Request, request: Request,
camera_name: str, 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( def preview_mp4(
request: Request, request: Request,
camera_name: str, 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( def review_preview(
request: Request, request: Request,
event_id: str, event_id: str,
@ -1524,8 +1525,8 @@ def review_preview(
return preview_mp4(request, review.camera, start_ts, end_ts) return preview_mp4(request, review.camera, start_ts, end_ts)
@router.get("/media/preview/{file_name}/thumbnail.jpg") @router.get("/preview/{file_name}/thumbnail.jpg")
@router.get("/media/preview/{file_name}/thumbnail.webp") @router.get("/preview/{file_name}/thumbnail.webp")
def preview_thumbnail(file_name: str): def preview_thumbnail(file_name: str):
"""Get a thumbnail from the cached preview frames.""" """Get a thumbnail from the cached preview frames."""
if len(file_name) > 1000: if len(file_name) > 1000:

View File

@ -54,7 +54,7 @@ export default function CameraImage({
return; 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}` : "" searchParams ? `&${searchParams}` : ""
}`; }`;

View File

@ -89,7 +89,7 @@ export default function CameraImage({
if (!config || scaledHeight === 0 || !canvasRef.current) { if (!config || scaledHeight === 0 || !canvasRef.current) {
return; 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}` : "" searchParams ? `&${searchParams}` : ""
}`; }`;
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);

View File

@ -184,7 +184,7 @@ export function AnimatedEventCard({
}} }}
> >
<source <source
src={`${baseUrl}api/media/review/${event.id}/preview?format=mp4`} src={`${baseUrl}api/review/${event.id}/preview?format=mp4`}
type="video/mp4" type="video/mp4"
/> />
</video> </video>

View File

@ -163,13 +163,13 @@ export default function ObjectLifecycle({
// image // image
const [src, setSrc] = useState( 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); const [hasError, setHasError] = useState(false);
useEffect(() => { useEffect(() => {
if (timeIndex) { 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); setSrc(newSrc);
} }
setImgLoaded(false); setImgLoaded(false);

View File

@ -302,8 +302,8 @@ function EventItem({
draggable={false} draggable={false}
src={ src={
event.has_snapshot event.has_snapshot
? `${apiHost}api/media/events/${event.id}/snapshot.jpg` ? `${apiHost}api/events/${event.id}/snapshot.jpg`
: `${apiHost}api/media/events/${event.id}/thumbnail.jpg` : `${apiHost}api/events/${event.id}/thumbnail.jpg`
} }
/> />
{hovered && ( {hovered && (
@ -317,8 +317,8 @@ function EventItem({
download download
href={ href={
event.has_snapshot event.has_snapshot
? `${apiHost}api/media/events/${event.id}/snapshot.jpg` ? `${apiHost}api/events/${event.id}/snapshot.jpg`
: `${apiHost}api/media/events/${event.id}/thumbnail.jpg` : `${apiHost}api/events/${event.id}/thumbnail.jpg`
} }
> >
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">

View File

@ -117,7 +117,7 @@ export function FrigatePlusDialog({
{upload?.id && ( {upload?.id && (
<img <img
className={`w-full ${grow} bg-black`} className={`w-full ${grow} bg-black`}
src={`${baseUrl}api/media/events/${upload?.id}/snapshot.jpg`} src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
alt={`${upload?.label}`} alt={`${upload?.label}`}
/> />
)} )}

View File

@ -149,7 +149,7 @@ export default function DynamicVideoPlayer({
} }
const time = controller.getProgress(playTime); const time = controller.getProgress(playTime);
return axios.post(`/media/camera/${camera}/plus/${time}`); return axios.post(`/${camera}/plus/${time}`);
}, },
[camera, controller], [camera, controller],
); );
@ -164,7 +164,7 @@ export default function DynamicVideoPlayer({
[timeRange], [timeRange],
); );
const { data: recordings } = useSWR<Recording[]>( const { data: recordings } = useSWR<Recording[]>(
[`media/camera/${camera}/recordings`, recordingParams], [`${camera}/recordings`, recordingParams],
{ revalidateOnFocus: false }, { revalidateOnFocus: false },
); );

View File

@ -432,7 +432,7 @@ export function InProgressPreview({
<div className="relative flex size-full items-center bg-black"> <div className="relative flex size-full items-center bg-black">
<img <img
className="pointer-events-none size-full object-contain" className="pointer-events-none size-full object-contain"
src={`${apiHost}api/media/preview/${previewFrames[key]}/thumbnail.webp`} src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
onLoad={handleLoad} onLoad={handleLoad}
/> />
{showProgress && ( {showProgress && (

View File

@ -42,7 +42,7 @@ export function PolygonCanvas({
const element = new window.Image(); const element = new window.Image();
element.width = width; element.width = width;
element.height = height; 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; return element;
} }
// we know that these deps are correct // we know that these deps are correct

View File

@ -500,7 +500,7 @@ function PtzControlPanel({
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>; setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
}) { }) {
const { data: ptz } = useSWR<CameraPtzInfo>( const { data: ptz } = useSWR<CameraPtzInfo>(
`/media/camera/${camera}/ptz/info`, `${camera}/ptz/info`,
); );
const { send: sendPtz } = usePtzCommand(camera); const { send: sendPtz } = usePtzCommand(camera);