Convert remaining review API endpoints to FastAPI

This commit is contained in:
Rui Alves 2024-09-15 18:29:42 +01:00
parent 640dce6bc2
commit 349891b0a6
3 changed files with 114 additions and 85 deletions

View File

@ -23,7 +23,6 @@ from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.api.event import EventBp from frigate.api.event import EventBp
from frigate.api.export import ExportBp from frigate.api.export import ExportBp
from frigate.api.review import ReviewBp
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR from frigate.const import CONFIG_DIR
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
@ -47,7 +46,6 @@ logger = logging.getLogger(__name__)
bp = Blueprint("frigate", __name__) bp = Blueprint("frigate", __name__)
bp.register_blueprint(EventBp) bp.register_blueprint(EventBp)
bp.register_blueprint(ExportBp) bp.register_blueprint(ExportBp)
bp.register_blueprint(ReviewBp)
bp.register_blueprint(AuthBp) bp.register_blueprint(AuthBp)
router = APIRouter() router = APIRouter()

View File

@ -0,0 +1,31 @@
from datetime import datetime, timedelta
from typing import Optional
from pydantic import BaseModel
class ReviewQueryParams(BaseModel):
cameras: Optional[str] = "all"
labels: Optional[str] = "all"
zones: Optional[str] = "all"
reviewed: Optional[int] = 0
limit: Optional[int] = None
severity: Optional[int] = None
before: Optional[float] = datetime.now().timestamp()
after: Optional[float] = (datetime.now() - timedelta(hours=24)).timestamp()
class ReviewSummaryQueryParams(BaseModel):
cameras: Optional[str] = "all"
labels: Optional[str] = "all"
zones: Optional[str] = "all"
timezone: Optional[str] = "utc"
day_ago: Optional[int] = (datetime.now() - timedelta(hours=24)).timestamp()
month_ago: Optional[int] = (datetime.now() - timedelta(days=30)).timestamp()
class ReviewActivityMotionQueryParams(BaseModel):
cameras: Optional[str] = "all"
before: Optional[float] = datetime.now().timestamp()
after: Optional[float] = (datetime.now() - timedelta(hours=1)).timestamp()
scale: Optional[int] = 30

View File

@ -1,62 +1,60 @@
"""Review apis.""" """Review apis."""
import logging import logging
from datetime import datetime, timedelta
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from typing import Optional
import pandas as pd import pandas as pd
from fastapi import APIRouter, Request from fastapi import APIRouter
from fastapi.params import Depends from fastapi.params import Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from flask import Blueprint, jsonify, make_response, request
from peewee import Case, DoesNotExist, fn, operator from peewee import Case, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel
from frigate.api.defs.review_query_parameters import (
ReviewActivityMotionQueryParams,
ReviewQueryParams,
ReviewSummaryQueryParams,
)
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.models import Recordings, ReviewSegment from frigate.models import Recordings, ReviewSegment
from frigate.util.builtin import get_tz_modifiers from frigate.util.builtin import get_tz_modifiers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ReviewBp = Blueprint("reviews", __name__)
router = APIRouter(tags=[Tags.review]) router = APIRouter(tags=[Tags.review])
class ItemQueryParams(BaseModel):
cameras: Optional[str] = "all"
labels: Optional[str] = "all"
zones: Optional[str] = "all"
reviewed: Optional[int] = 0
limit: Optional[int] = None
severity: Optional[int] = None
before: Optional[float] = datetime.now().timestamp()
after: Optional[float] = (datetime.now() - timedelta(hours=24)).timestamp()
@router.get("/review") @router.get("/review")
def review(params: ItemQueryParams = Depends()): def review(params: ReviewQueryParams = Depends()):
cameras = params.cameras
labels = params.labels
zones = params.zones
reviewed = params.reviewed
limit = params.limit
severity = params.severity
before = params.before
after = params.after
clauses = [ clauses = [
( (
(ReviewSegment.start_time > params.after) (ReviewSegment.start_time > after)
& ( & (
(ReviewSegment.end_time.is_null(True)) (ReviewSegment.end_time.is_null(True))
| (ReviewSegment.end_time < params.before) | (ReviewSegment.end_time < before)
) )
) )
] ]
if params.cameras != "all": if cameras != "all":
camera_list = params.cameras.split(",") camera_list = cameras.split(",")
clauses.append((ReviewSegment.camera << camera_list)) clauses.append((ReviewSegment.camera << camera_list))
if params.labels != "all": if labels != "all":
# use matching so segments with multiple labels # use matching so segments with multiple labels
# still match on a search where any label matches # still match on a search where any label matches
label_clauses = [] label_clauses = []
filtered_labels = params.labels.split(",") filtered_labels = labels.split(",")
for label in filtered_labels: for label in filtered_labels:
label_clauses.append( label_clauses.append(
@ -67,11 +65,11 @@ def review(params: ItemQueryParams = Depends()):
label_clause = reduce(operator.or_, label_clauses) label_clause = reduce(operator.or_, label_clauses)
clauses.append((label_clause)) clauses.append((label_clause))
if params.zones != "all": if zones != "all":
# use matching so segments with multiple zones # use matching so segments with multiple zones
# still match on a search where any zone matches # still match on a search where any zone matches
zone_clauses = [] zone_clauses = []
filtered_zones = params.zones.split(",") filtered_zones = zones.split(",")
for zone in filtered_zones: for zone in filtered_zones:
zone_clauses.append( zone_clauses.append(
@ -81,18 +79,18 @@ def review(params: ItemQueryParams = Depends()):
zone_clause = reduce(operator.or_, zone_clauses) zone_clause = reduce(operator.or_, zone_clauses)
clauses.append((zone_clause)) clauses.append((zone_clause))
if params.reviewed == 0: if reviewed == 0:
clauses.append((ReviewSegment.has_been_reviewed == False)) clauses.append((ReviewSegment.has_been_reviewed == False))
if params.severity: if severity:
clauses.append((ReviewSegment.severity == params.severity)) clauses.append((ReviewSegment.severity == severity))
review = ( review = (
ReviewSegment.select() ReviewSegment.select()
.where(reduce(operator.and_, clauses)) .where(reduce(operator.and_, clauses))
.order_by(ReviewSegment.severity.asc()) .order_by(ReviewSegment.severity.asc())
.order_by(ReviewSegment.start_time.desc()) .order_by(ReviewSegment.start_time.desc())
.limit(params.limit) .limit(limit)
.dicts() .dicts()
.iterator() .iterator()
) )
@ -120,16 +118,15 @@ def get_review(event_id: str):
return "Review item not found", 404 return "Review item not found", 404
@ReviewBp.route("/review/summary") @router.get("/review/summary")
def review_summary(): def review_summary(params: ReviewSummaryQueryParams = Depends()):
tz_name = request.args.get("timezone", default="utc", type=str) hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(params.timezone)
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name) day_ago = params.day_ago
day_ago = (datetime.now() - timedelta(hours=24)).timestamp() month_ago = params.month_ago
month_ago = (datetime.now() - timedelta(days=30)).timestamp()
cameras = request.args.get("cameras", "all") cameras = params.cameras
labels = request.args.get("labels", "all") labels = params.labels
zones = request.args.get("zones", "all") zones = params.zones
clauses = [(ReviewSegment.start_time > day_ago)] clauses = [(ReviewSegment.start_time > day_ago)]
@ -364,53 +361,60 @@ def review_summary():
for e in last_month.dicts().iterator(): for e in last_month.dicts().iterator():
data[e["day"]] = e data[e["day"]] = e
return jsonify(data) return JSONResponse(content=data)
@ReviewBp.route("/reviews/viewed", methods=("POST",)) @router.post("/reviews/viewed")
def set_multiple_reviewed(): def set_multiple_reviewed(body: dict = None):
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
list_of_ids = json.get("ids", "") list_of_ids = json.get("ids", "")
if not list_of_ids or len(list_of_ids) == 0: if not list_of_ids or len(list_of_ids) == 0:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Not a valid list of ids"}), 404 context=({"success": False, "message": "Not a valid list of ids"}),
status_code=404,
) )
ReviewSegment.update(has_been_reviewed=True).where( ReviewSegment.update(has_been_reviewed=True).where(
ReviewSegment.id << list_of_ids ReviewSegment.id << list_of_ids
).execute() ).execute()
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Reviewed multiple items"}), 200 content=({"success": True, "message": "Reviewed multiple items"}),
status_code=200,
) )
@ReviewBp.route("/review/<id>/viewed", methods=("DELETE",)) @router.delete("/review/{event_id}/viewed")
def set_not_reviewed(id): def set_not_reviewed(event_id: str):
try: try:
review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == id) review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == event_id)
except DoesNotExist: except DoesNotExist:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Review " + id + " not found"}), 404 content=(
{"success": False, "message": "Review " + event_id + " not found"}
),
status_code=404,
) )
review.has_been_reviewed = False review.has_been_reviewed = False
review.save() review.save()
return make_response( return JSONResponse(
jsonify({"success": True, "message": "Reviewed " + id + " not viewed"}), 200 content=({"success": True, "message": "Reviewed " + event_id + " not viewed"}),
status_code=200,
) )
@ReviewBp.route("/reviews/delete", methods=("POST",)) @router.post("/reviews/delete")
def delete_reviews(): def delete_reviews(body: dict = None):
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = body or {}
list_of_ids = json.get("ids", "") list_of_ids = json.get("ids", "")
if not list_of_ids or len(list_of_ids) == 0: if not list_of_ids or len(list_of_ids) == 0:
return make_response( return JSONResponse(
jsonify({"success": False, "message": "Not a valid list of ids"}), 404 content=({"success": False, "message": "Not a valid list of ids"}),
status_code=404,
) )
reviews = ( reviews = (
@ -452,18 +456,20 @@ def delete_reviews():
Recordings.delete().where(Recordings.id << recording_ids).execute() Recordings.delete().where(Recordings.id << recording_ids).execute()
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200) return JSONResponse(
content=({"success": True, "message": "Delete reviews"}), status_code=200
@ReviewBp.route("/review/activity/motion")
def motion_activity():
"""Get motion and audio activity."""
cameras = request.args.get("cameras", "all")
before = request.args.get("before", type=float, default=datetime.now().timestamp())
after = request.args.get(
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
) )
@router.get("/review/activity/motion")
def motion_activity(params: ReviewActivityMotionQueryParams = Depends()):
"""Get motion and audio activity."""
cameras = params.cameras
before = params.before
after = params.after
# get scale in seconds
scale = params.scale
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
clauses.append((Recordings.motion > 0)) clauses.append((Recordings.motion > 0))
@ -483,15 +489,12 @@ def motion_activity():
.iterator() .iterator()
) )
# get scale in seconds
scale = request.args.get("scale", type=int, default=30)
# resample data using pandas to get activity on scaled basis # resample data using pandas to get activity on scaled basis
df = pd.DataFrame(data, columns=["start_time", "motion", "camera"]) df = pd.DataFrame(data, columns=["start_time", "motion", "camera"])
if df.empty: if df.empty:
logger.warning("No motion data found for the requested time range") logger.warning("No motion data found for the requested time range")
return jsonify([]) return JSONResponse(content=[])
df = df.astype(dtype={"motion": "float32"}) df = df.astype(dtype={"motion": "float32"})
@ -529,14 +532,14 @@ def motion_activity():
return jsonify(normalized) return jsonify(normalized)
@ReviewBp.route("/review/activity/audio") @router.get("/review/activity/audio")
def audio_activity(): def audio_activity(params: ReviewActivityMotionQueryParams = Depends()):
"""Get motion and audio activity.""" """Get motion and audio activity."""
cameras = request.args.get("cameras", "all") cameras = params.cameras
before = request.args.get("before", type=float, default=datetime.now().timestamp()) before = params.before
after = request.args.get( after = params.after
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp() # get scale in seconds
) scale = params.scale
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
@ -568,9 +571,6 @@ def audio_activity():
} }
) )
# get scale in seconds
scale = request.args.get("scale", type=int, default=30)
# resample data using pandas to get activity on scaled basis # resample data using pandas to get activity on scaled basis
df = pd.DataFrame(data, columns=["start_time", "audio"]) df = pd.DataFrame(data, columns=["start_time", "audio"])
df = df.astype(dtype={"audio": "float16"}) df = df.astype(dtype={"audio": "float16"})