mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-15 07:35:27 +03:00
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:
parent
fcb54adb5f
commit
3a54b9251a
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}` : ""
|
||||
}`;
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -184,7 +184,7 @@ export function AnimatedEventCard({
|
||||
}}
|
||||
>
|
||||
<source
|
||||
src={`${baseUrl}api/media/review/${event.id}/preview?format=mp4`}
|
||||
src={`${baseUrl}api/review/${event.id}/preview?format=mp4`}
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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`
|
||||
}
|
||||
>
|
||||
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
|
||||
|
||||
@ -117,7 +117,7 @@ export function FrigatePlusDialog({
|
||||
{upload?.id && (
|
||||
<img
|
||||
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}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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<Recording[]>(
|
||||
[`media/camera/${camera}/recordings`, recordingParams],
|
||||
[`${camera}/recordings`, recordingParams],
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
|
||||
@ -432,7 +432,7 @@ export function InProgressPreview({
|
||||
<div className="relative flex size-full items-center bg-black">
|
||||
<img
|
||||
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}
|
||||
/>
|
||||
{showProgress && (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -500,7 +500,7 @@ function PtzControlPanel({
|
||||
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { data: ptz } = useSWR<CameraPtzInfo>(
|
||||
`/media/camera/${camera}/ptz/info`,
|
||||
`${camera}/ptz/info`,
|
||||
);
|
||||
|
||||
const { send: sendPtz } = usePtzCommand(camera);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user