mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 09:02:15 +03:00
Frigate HTTP API using FastAPI (#13871)
* POC: Added FastAPI with one endpoint (get /logs/service) * POC: Revert error_log * POC: Converted preview related endpoints to FastAPI * POC: Converted two more endpoints to FastAPI * POC: lint * Convert all media endpoints to FastAPI. Added /media prefix (/media/camera && media/events && /media/preview) * Convert all notifications API endpoints to FastAPI * Convert first review API endpoints to FastAPI * Convert remaining review API endpoints to FastAPI * Convert export endpoints to FastAPI * Fix path parameters * Convert events endpoints to FastAPI * Use body for multiple events endpoints * Use body for multiple events endpoints (create and end event) * Convert app endpoints to FastAPI * Convert app endpoints to FastAPI * Convert auth endpoints to FastAPI * Removed flask app in favour of FastAPI app. Implemented FastAPI middleware to check CSRF, connect and disconnect from DB. Added middleware x-forwared-for headers * Added starlette plugin to expose custom headers * Use slowapi as the limiter * Use query parameters for the frame latest endpoint * Use query parameters for the media snapshot.jpg endpoint * Use query parameters for the media MJPEG feed endpoint * Revert initial nginx.conf change * Added missing even_id for /events/search endpoint * Removed left over comment * Use FastAPI TestClient * severity query parameter should be a string * Use the same pattern for all tests * Fix endpoint * 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 * Reverted paths for media on tsx files * Deleted file * Fix test_http to use TestClient * Formatting * Bind timeline to DB * Fix http tests * Replace filename with pathvalidate * Fix latest.ext handling and disable uvicorn access logs * Add cosntraints to api provided values * Formatting * Remove unused * Remove unused * Get rate limiter working --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
co-authored by
Nicolas Mowen
parent
dc54981784
commit
cffc431bf0
+269
-267
@@ -4,24 +4,32 @@ import base64
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
jsonify,
|
||||
make_response,
|
||||
request,
|
||||
)
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from PIL import Image
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.api.defs.events_body import (
|
||||
EventsCreateBody,
|
||||
EventsDescriptionBody,
|
||||
EventsEndBody,
|
||||
EventsSubLabelBody,
|
||||
)
|
||||
from frigate.api.defs.events_query_parameters import (
|
||||
DEFAULT_TIME_RANGE,
|
||||
EventsQueryParams,
|
||||
EventsSearchQueryParams,
|
||||
EventsSummaryQueryParams,
|
||||
)
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.const import (
|
||||
CLIPS_DIR,
|
||||
)
|
||||
@@ -33,57 +41,55 @@ from frigate.util.builtin import get_tz_modifiers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EventBp = Blueprint("events", __name__)
|
||||
|
||||
DEFAULT_TIME_RANGE = "00:00,24:00"
|
||||
router = APIRouter(tags=[Tags.events])
|
||||
|
||||
|
||||
@EventBp.route("/events")
|
||||
def events():
|
||||
camera = request.args.get("camera", "all")
|
||||
cameras = request.args.get("cameras", "all")
|
||||
@router.get("/events")
|
||||
def events(params: EventsQueryParams = Depends()):
|
||||
camera = params.camera
|
||||
cameras = params.cameras
|
||||
|
||||
# handle old camera arg
|
||||
if cameras == "all" and camera != "all":
|
||||
cameras = camera
|
||||
|
||||
label = unquote(request.args.get("label", "all"))
|
||||
labels = request.args.get("labels", "all")
|
||||
label = unquote(params.label)
|
||||
labels = params.labels
|
||||
|
||||
# handle old label arg
|
||||
if labels == "all" and label != "all":
|
||||
labels = label
|
||||
|
||||
sub_label = request.args.get("sub_label", "all")
|
||||
sub_labels = request.args.get("sub_labels", "all")
|
||||
sub_label = params.sub_label
|
||||
sub_labels = params.sub_labels
|
||||
|
||||
# handle old sub_label arg
|
||||
if sub_labels == "all" and sub_label != "all":
|
||||
sub_labels = sub_label
|
||||
|
||||
zone = request.args.get("zone", "all")
|
||||
zones = request.args.get("zones", "all")
|
||||
zone = params.zone
|
||||
zones = params.zones
|
||||
|
||||
# handle old label arg
|
||||
if zones == "all" and zone != "all":
|
||||
zones = zone
|
||||
|
||||
limit = request.args.get("limit", 100)
|
||||
after = request.args.get("after", type=float)
|
||||
before = request.args.get("before", type=float)
|
||||
time_range = request.args.get("time_range", DEFAULT_TIME_RANGE)
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
in_progress = request.args.get("in_progress", type=int)
|
||||
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
||||
favorites = request.args.get("favorites", type=int)
|
||||
min_score = request.args.get("min_score", type=float)
|
||||
max_score = request.args.get("max_score", type=float)
|
||||
is_submitted = request.args.get("is_submitted", type=int)
|
||||
min_length = request.args.get("min_length", type=float)
|
||||
max_length = request.args.get("max_length", type=float)
|
||||
limit = params.limit
|
||||
after = params.after
|
||||
before = params.before
|
||||
time_range = params.time_range
|
||||
has_clip = params.has_clip
|
||||
has_snapshot = params.has_snapshot
|
||||
in_progress = params.in_progress
|
||||
include_thumbnails = params.include_thumbnails
|
||||
favorites = params.favorites
|
||||
min_score = params.min_score
|
||||
max_score = params.max_score
|
||||
is_submitted = params.is_submitted
|
||||
min_length = params.min_length
|
||||
max_length = params.max_length
|
||||
|
||||
sort = request.args.get("sort", type=str)
|
||||
sort = params.sort
|
||||
|
||||
clauses = []
|
||||
|
||||
@@ -163,7 +169,7 @@ def events():
|
||||
|
||||
if time_range != DEFAULT_TIME_RANGE:
|
||||
# get timezone arg to ensure browser times are used
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
tz_name = params.timezone
|
||||
hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name)
|
||||
|
||||
times = time_range.split(",")
|
||||
@@ -248,13 +254,11 @@ def events():
|
||||
.iterator()
|
||||
)
|
||||
|
||||
return jsonify(list(events))
|
||||
return JSONResponse(content=list(events))
|
||||
|
||||
|
||||
@EventBp.route("/events/explore")
|
||||
def events_explore():
|
||||
limit = request.args.get("limit", 10, type=int)
|
||||
|
||||
@router.get("/events/explore")
|
||||
def events_explore(limit: int = 10):
|
||||
subquery = Event.select(
|
||||
Event.id,
|
||||
Event.camera,
|
||||
@@ -316,69 +320,68 @@ def events_explore():
|
||||
for event in events
|
||||
]
|
||||
|
||||
return jsonify(processed_events)
|
||||
return JSONResponse(content=processed_events)
|
||||
|
||||
|
||||
@EventBp.route("/event_ids")
|
||||
def event_ids():
|
||||
idString = request.args.get("ids")
|
||||
ids = idString.split(",")
|
||||
@router.get("/event_ids")
|
||||
def event_ids(ids: str):
|
||||
ids = ids.split(",")
|
||||
|
||||
if not ids:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Valid list of ids must be sent"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Valid list of ids must be sent"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
events = Event.select().where(Event.id << ids).dicts().iterator()
|
||||
return jsonify(list(events))
|
||||
return JSONResponse(list(events))
|
||||
except Exception:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Events not found"}), 400
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Events not found"}), status_code=400
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/search")
|
||||
def events_search():
|
||||
query = request.args.get("query", type=str)
|
||||
search_type = request.args.get("search_type", "thumbnail,description", type=str)
|
||||
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
||||
limit = request.args.get("limit", 50, type=int)
|
||||
@router.get("/events/search")
|
||||
def events_search(request: Request, params: EventsSearchQueryParams = Depends()):
|
||||
query = params.query
|
||||
search_type = params.search_type
|
||||
include_thumbnails = params.include_thumbnails
|
||||
limit = params.limit
|
||||
|
||||
# Filters
|
||||
cameras = request.args.get("cameras", "all", type=str)
|
||||
labels = request.args.get("labels", "all", type=str)
|
||||
zones = request.args.get("zones", "all", type=str)
|
||||
after = request.args.get("after", type=float)
|
||||
before = request.args.get("before", type=float)
|
||||
cameras = params.cameras
|
||||
labels = params.labels
|
||||
zones = params.zones
|
||||
after = params.after
|
||||
before = params.before
|
||||
|
||||
# for similarity search
|
||||
event_id = request.args.get("event_id", type=str)
|
||||
event_id = params.event_id
|
||||
|
||||
if not query and not event_id:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "A search query must be supplied",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if not current_app.frigate_config.semantic_search.enabled:
|
||||
return make_response(
|
||||
jsonify(
|
||||
if not request.app.frigate_config.semantic_search.enabled:
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Semantic search is not enabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
|
||||
selected_columns = [
|
||||
Event.id,
|
||||
@@ -437,14 +440,14 @@ def events_search():
|
||||
try:
|
||||
search_event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Event not found",
|
||||
}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
thumbnail = base64.b64decode(search_event.thumbnail)
|
||||
img = np.array(Image.open(io.BytesIO(thumbnail)).convert("RGB"))
|
||||
@@ -504,7 +507,7 @@ def events_search():
|
||||
}
|
||||
|
||||
if not results:
|
||||
return jsonify([])
|
||||
return JSONResponse(content=[])
|
||||
|
||||
# Get the event data
|
||||
events = (
|
||||
@@ -537,15 +540,15 @@ def events_search():
|
||||
]
|
||||
events = sorted(events, key=lambda x: x["search_distance"])[:limit]
|
||||
|
||||
return jsonify(events)
|
||||
return JSONResponse(content=events)
|
||||
|
||||
|
||||
@EventBp.route("/events/summary")
|
||||
def events_summary():
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
@router.get("/events/summary")
|
||||
def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
tz_name = params.timezone
|
||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
has_clip = params.has_clip
|
||||
has_snapshot = params.has_snapshot
|
||||
|
||||
clauses = []
|
||||
|
||||
@@ -582,47 +585,49 @@ def events_summary():
|
||||
)
|
||||
)
|
||||
|
||||
return jsonify([e for e in groups.dicts()])
|
||||
return JSONResponse(content=[e for e in groups.dicts()])
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>", methods=("GET",))
|
||||
def event(id):
|
||||
@router.get("/events/{event_id}")
|
||||
def event(event_id: str):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == id))
|
||||
return model_to_dict(Event.get(Event.id == event_id))
|
||||
except DoesNotExist:
|
||||
return "Event not found", 404
|
||||
return JSONResponse(content="Event not found", status_code=404)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/retain", methods=("POST",))
|
||||
def set_retain(id):
|
||||
@router.post("/events/{event_id}/retain")
|
||||
def set_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
event.retain_indefinitely = True
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " retained"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/plus", methods=("POST",))
|
||||
def send_to_plus(id):
|
||||
if not current_app.frigate_config.plus_api.is_active():
|
||||
@router.post("/events/{event_id}/plus")
|
||||
def send_to_plus(request: Request, event_id: str):
|
||||
if not request.app.frigate_config.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
include_annotation = (
|
||||
@@ -630,11 +635,13 @@ def send_to_plus(id):
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
message = f"Event {id} not found"
|
||||
message = f"Event {event_id} not found"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}), status_code=404
|
||||
)
|
||||
|
||||
# events from before the conversion to relative dimensions cant include annotations
|
||||
if event.data.get("box") is None:
|
||||
@@ -642,20 +649,22 @@ def send_to_plus(id):
|
||||
|
||||
if event.end_time is None:
|
||||
logger.error(f"Unable to load clean png for in-progress event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Unable to load clean png for in-progress event",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if event.plus_id:
|
||||
message = "Already submitted to plus"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}), status_code=400
|
||||
)
|
||||
|
||||
# load clean.png
|
||||
try:
|
||||
@@ -663,29 +672,29 @@ def send_to_plus(id):
|
||||
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
|
||||
except Exception:
|
||||
logger.error(f"Unable to load clean png for event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": "Unable to load clean png for event"}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if image is None or image.size == 0:
|
||||
logger.error(f"Unable to load clean png for event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": "Unable to load clean png for event"}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
plus_id = current_app.frigate_config.plus_api.upload_image(image, event.camera)
|
||||
plus_id = request.app.frigate_config.plus_api.upload_image(image, event.camera)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading image"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading image"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# store image id in the database
|
||||
@@ -696,7 +705,7 @@ def send_to_plus(id):
|
||||
box = event.data["box"]
|
||||
|
||||
try:
|
||||
current_app.frigate_config.plus_api.add_annotation(
|
||||
request.app.frigate_config.plus_api.add_annotation(
|
||||
event.plus_id,
|
||||
box,
|
||||
event.label,
|
||||
@@ -704,59 +713,67 @@ def send_to_plus(id):
|
||||
except ValueError:
|
||||
message = "Error uploading annotation, unsupported label provided."
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": message}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}),
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading annotation"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading annotation"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
|
||||
return JSONResponse(
|
||||
content=({"success": True, "plus_id": plus_id}), status_code=200
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/false_positive", methods=("PUT",))
|
||||
def false_positive(id):
|
||||
if not current_app.frigate_config.plus_api.is_active():
|
||||
@router.put("/events/{event_id}/false_positive")
|
||||
def false_positive(request: Request, event_id: str):
|
||||
if not request.app.frigate_config.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
message = f"Event {id} not found"
|
||||
message = f"Event {event_id} not found"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}), status_code=404
|
||||
)
|
||||
|
||||
# events from before the conversion to relative dimensions cant include annotations
|
||||
if event.data.get("box") is None:
|
||||
message = "Events prior to 0.13 cannot be submitted as false positives"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}), status_code=400
|
||||
)
|
||||
|
||||
if event.false_positive:
|
||||
message = "False positive already submitted to Frigate+"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}), status_code=400
|
||||
)
|
||||
|
||||
if not event.plus_id:
|
||||
plus_response = send_to_plus(id)
|
||||
plus_response = send_to_plus(event_id)
|
||||
if plus_response.status_code != 200:
|
||||
return plus_response
|
||||
# need to refetch the event now that it has a plus_id
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
|
||||
region = event.data["region"]
|
||||
box = event.data["box"]
|
||||
@@ -769,7 +786,7 @@ def false_positive(id):
|
||||
)
|
||||
|
||||
try:
|
||||
current_app.frigate_config.plus_api.add_false_positive(
|
||||
request.app.frigate_config.plus_api.add_false_positive(
|
||||
event.plus_id,
|
||||
region,
|
||||
box,
|
||||
@@ -782,92 +799,65 @@ def false_positive(id):
|
||||
except ValueError:
|
||||
message = "Error uploading false positive, unsupported label provided."
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": message}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": message}),
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error uploading false positive"}),
|
||||
400,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Error uploading false positive"}),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
event.false_positive = True
|
||||
event.save()
|
||||
|
||||
return make_response(jsonify({"success": True, "plus_id": event.plus_id}), 200)
|
||||
return JSONResponse(
|
||||
content=({"success": True, "plus_id": event.plus_id}), status_code=200
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/retain", methods=("DELETE",))
|
||||
def delete_retain(id):
|
||||
@router.delete("/events/{event_id}/retain")
|
||||
def delete_retain(event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
event.retain_indefinitely = False
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " un-retained"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/sub_label", methods=("POST",))
|
||||
def set_sub_label(id):
|
||||
@router.post("/events/{event_id}/sub_label")
|
||||
def set_sub_label(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
body: EventsSubLabelBody,
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
new_sub_label = json.get("subLabel")
|
||||
new_score = json.get("subLabelScore")
|
||||
|
||||
if new_sub_label is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "A sub label must be supplied",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
if new_sub_label and len(new_sub_label) > 100:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": new_sub_label
|
||||
+ " exceeds the 100 character limit for sub_label",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
if new_score is not None and (new_score > 1.0 or new_score < 0):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": new_score
|
||||
+ " does not fit within the expected bounds 0 <= score <= 1.0",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
new_sub_label = body.subLabel
|
||||
new_score = body.subLabelScore
|
||||
|
||||
if not event.end_time:
|
||||
# update tracked object
|
||||
tracked_obj: TrackedObject = (
|
||||
current_app.detected_frames_processor.camera_states[
|
||||
request.app.detected_frames_processor.camera_states[
|
||||
event.camera
|
||||
].tracked_objects.get(event.id)
|
||||
)
|
||||
@@ -878,7 +868,7 @@ def set_sub_label(id):
|
||||
# update timeline items
|
||||
Timeline.update(
|
||||
data=Timeline.data.update({"sub_label": (new_sub_label, new_score)})
|
||||
).where(Timeline.source_id == id).execute()
|
||||
).where(Timeline.source_id == event_id).execute()
|
||||
|
||||
event.sub_label = new_sub_label
|
||||
|
||||
@@ -888,70 +878,78 @@ def set_sub_label(id):
|
||||
event.data = data
|
||||
|
||||
event.save()
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Event " + id + " sub label set to " + new_sub_label,
|
||||
"message": "Event " + event_id + " sub label set to " + new_sub_label,
|
||||
}
|
||||
),
|
||||
200,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>/description", methods=("POST",))
|
||||
def set_description(id):
|
||||
@router.post("/events/{event_id}/description")
|
||||
def set_description(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
body: EventsDescriptionBody,
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
new_description = json.get("description")
|
||||
new_description = body.description
|
||||
|
||||
if new_description is None or len(new_description) == 0:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": False,
|
||||
"message": "description cannot be empty",
|
||||
}
|
||||
),
|
||||
400,
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
event.data["description"] = new_description
|
||||
event.save()
|
||||
|
||||
# If semantic search is enabled, update the index
|
||||
if current_app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
context.embeddings.description.upsert(
|
||||
documents=[new_description],
|
||||
metadatas=[get_metadata(event)],
|
||||
ids=[id],
|
||||
ids=[event_id],
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Event " + id + " description set to " + new_description,
|
||||
"message": "Event "
|
||||
+ event_id
|
||||
+ " description set to "
|
||||
+ new_description,
|
||||
}
|
||||
),
|
||||
200,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<id>", methods=("DELETE",))
|
||||
def delete_event(id):
|
||||
@router.delete("/events/{event_id}")
|
||||
def delete_event(request: Request, event_id: str):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Event " + event_id + " not found"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
@@ -965,82 +963,86 @@ def delete_event(id):
|
||||
media.unlink(missing_ok=True)
|
||||
|
||||
event.delete_instance()
|
||||
Timeline.delete().where(Timeline.source_id == id).execute()
|
||||
Timeline.delete().where(Timeline.source_id == event_id).execute()
|
||||
# If semantic search is enabled, update the index
|
||||
if current_app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = current_app.embeddings
|
||||
context.embeddings.thumbnail.delete(ids=[id])
|
||||
context.embeddings.description.delete(ids=[id])
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
context.embeddings.thumbnail.delete(ids=[event_id])
|
||||
context.embeddings.description.delete(ids=[event_id])
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event " + event_id + " deleted"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<camera_name>/<label>/create", methods=["POST"])
|
||||
def create_event(camera_name, label):
|
||||
if not camera_name or not current_app.frigate_config.cameras.get(camera_name):
|
||||
return make_response(
|
||||
jsonify(
|
||||
@router.post("/events/{camera_name}/{label}/create")
|
||||
def create_event(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
label: str,
|
||||
body: EventsCreateBody = None,
|
||||
):
|
||||
if not camera_name or not request.app.frigate_config.cameras.get(camera_name):
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": f"{camera_name} is not a valid camera."}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
if not label:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": f"{label} must be set."}), 404
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": f"{label} must be set."}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
|
||||
try:
|
||||
frame = current_app.detected_frames_processor.get_current_frame(camera_name)
|
||||
frame = request.app.detected_frames_processor.get_current_frame(camera_name)
|
||||
|
||||
event_id = current_app.external_processor.create_manual_event(
|
||||
event_id = request.app.external_processor.create_manual_event(
|
||||
camera_name,
|
||||
label,
|
||||
json.get("source_type", "api"),
|
||||
json.get("sub_label", None),
|
||||
json.get("score", 0),
|
||||
json.get("duration", 30),
|
||||
json.get("include_recording", True),
|
||||
json.get("draw", {}),
|
||||
body.source_type,
|
||||
body.sub_label,
|
||||
body.score,
|
||||
body.duration,
|
||||
body.include_recording,
|
||||
body.draw,
|
||||
frame,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "An unknown error occurred"}),
|
||||
500,
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "An unknown error occurred"}),
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Successfully created event.",
|
||||
"event_id": event_id,
|
||||
}
|
||||
),
|
||||
200,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/<event_id>/end", methods=["PUT"])
|
||||
def end_event(event_id):
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
|
||||
@router.put("/events/{event_id}/end")
|
||||
def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
try:
|
||||
end_time = json.get("end_time", datetime.now().timestamp())
|
||||
current_app.external_processor.finish_manual_event(event_id, end_time)
|
||||
end_time = body.end_time
|
||||
request.app.external_processor.finish_manual_event(event_id, end_time)
|
||||
except Exception:
|
||||
return make_response(
|
||||
jsonify(
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{"success": False, "message": f"{event_id} must be set and valid."}
|
||||
),
|
||||
404,
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event successfully ended."}), 200
|
||||
return JSONResponse(
|
||||
content=({"success": True, "message": "Event successfully ended."}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user