diff --git a/frigate/api/defs/tags.py b/frigate/api/defs/tags.py index 27cb6cb74..aeef87462 100644 --- a/frigate/api/defs/tags.py +++ b/frigate/api/defs/tags.py @@ -7,3 +7,4 @@ class Tags(Enum): logs = "Logs" media = "Media" notifications = "Notifications" + review = "Review" diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 3ec96dfee..7ba07c4e7 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -3,7 +3,7 @@ import logging from fastapi import FastAPI from frigate.api import app as main_app -from frigate.api import media, notification, preview +from frigate.api import media, notification, preview, review from frigate.plus import PlusApi from frigate.ptz.onvif import OnvifController from frigate.stats.emitter import StatsEmitter @@ -30,6 +30,7 @@ def create_fastapi_app( app.include_router(media.router) app.include_router(preview.router) app.include_router(notification.router) + app.include_router(review.router) # App Properties app.frigate_config = frigate_config app.detected_frames_processor = detected_frames_processor diff --git a/frigate/api/review.py b/frigate/api/review.py index d391828d5..0aa36e79d 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -4,53 +4,59 @@ import logging from datetime import datetime, timedelta from functools import reduce from pathlib import Path +from typing import Optional import pandas as pd +from fastapi import APIRouter, Request +from fastapi.params import Depends +from fastapi.responses import JSONResponse from flask import Blueprint, jsonify, make_response, request from peewee import Case, DoesNotExist, fn, operator from playhouse.shortcuts import model_to_dict +from pydantic import BaseModel +from frigate.api.defs.tags import Tags from frigate.models import Recordings, ReviewSegment from frigate.util.builtin import get_tz_modifiers logger = logging.getLogger(__name__) ReviewBp = Blueprint("reviews", __name__) +router = APIRouter(tags=[Tags.review]) -@ReviewBp.route("/review") -def review(): - cameras = request.args.get("cameras", "all") - labels = request.args.get("labels", "all") - zones = request.args.get("zones", "all") - reviewed = request.args.get("reviewed", type=int, default=0) - limit = request.args.get("limit", type=int, default=None) - severity = request.args.get("severity", None) +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() - before = request.args.get("before", type=float, default=datetime.now().timestamp()) - after = request.args.get( - "after", type=float, default=(datetime.now() - timedelta(hours=24)).timestamp() - ) +@router.get("/review") +def review(params: ItemQueryParams = Depends()): clauses = [ ( - (ReviewSegment.start_time > after) + (ReviewSegment.start_time > params.after) & ( (ReviewSegment.end_time.is_null(True)) - | (ReviewSegment.end_time < before) + | (ReviewSegment.end_time < params.before) ) ) ] - if cameras != "all": - camera_list = cameras.split(",") + if params.cameras != "all": + camera_list = params.cameras.split(",") clauses.append((ReviewSegment.camera << camera_list)) - if labels != "all": + if params.labels != "all": # use matching so segments with multiple labels # still match on a search where any label matches label_clauses = [] - filtered_labels = labels.split(",") + filtered_labels = params.labels.split(",") for label in filtered_labels: label_clauses.append( @@ -61,11 +67,11 @@ def review(): label_clause = reduce(operator.or_, label_clauses) clauses.append((label_clause)) - if zones != "all": + if params.zones != "all": # use matching so segments with multiple zones # still match on a search where any zone matches zone_clauses = [] - filtered_zones = zones.split(",") + filtered_zones = params.zones.split(",") for zone in filtered_zones: zone_clauses.append( @@ -75,41 +81,41 @@ def review(): zone_clause = reduce(operator.or_, zone_clauses) clauses.append((zone_clause)) - if reviewed == 0: + if params.reviewed == 0: clauses.append((ReviewSegment.has_been_reviewed == False)) - if severity: - clauses.append((ReviewSegment.severity == severity)) + if params.severity: + clauses.append((ReviewSegment.severity == params.severity)) review = ( ReviewSegment.select() .where(reduce(operator.and_, clauses)) .order_by(ReviewSegment.severity.asc()) .order_by(ReviewSegment.start_time.desc()) - .limit(limit) + .limit(params.limit) .dicts() .iterator() ) - return jsonify([r for r in review]) + return JSONResponse(content=[r for r in review]) -@ReviewBp.route("/review/event/") -def get_review_from_event(id: str): +@router.get("/review/event/{event_id}") +def get_review_from_event(event_id: str): try: return model_to_dict( ReviewSegment.get( - ReviewSegment.data["detections"].cast("text") % f'*"{id}"*' + ReviewSegment.data["detections"].cast("text") % f'*"{event_id}"*' ) ) except DoesNotExist: return "Review item not found", 404 -@ReviewBp.route("/review/") -def get_review(id: str): +@router.get("/review/{event_id}") +def get_review(event_id: str): try: - return model_to_dict(ReviewSegment.get(ReviewSegment.id == id)) + return model_to_dict(ReviewSegment.get(ReviewSegment.id == event_id)) except DoesNotExist: return "Review item not found", 404