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:
Rui Alves
2024-09-24 07:05:30 -06:00
committed by GitHub
co-authored by Nicolas Mowen
parent dc54981784
commit cffc431bf0
24 changed files with 1654 additions and 1321 deletions
+50 -54
View File
@@ -5,54 +5,50 @@ from pathlib import Path
from typing import Optional
import psutil
from flask import (
Blueprint,
current_app,
jsonify,
make_response,
request,
)
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from peewee import DoesNotExist
from frigate.api.defs.tags import Tags
from frigate.const import EXPORT_DIR
from frigate.models import Export, Recordings
from frigate.record.export import PlaybackFactorEnum, RecordingExporter
logger = logging.getLogger(__name__)
ExportBp = Blueprint("exports", __name__)
router = APIRouter(tags=[Tags.export])
@ExportBp.route("/exports")
@router.get("/exports")
def get_exports():
exports = Export.select().order_by(Export.date.desc()).dicts().iterator()
return jsonify([e for e in exports])
return JSONResponse(content=[e for e in exports])
@ExportBp.route(
"/export/<camera_name>/start/<int:start_time>/end/<int:end_time>", methods=["POST"]
)
@ExportBp.route(
"/export/<camera_name>/start/<float:start_time>/end/<float:end_time>",
methods=["POST"],
)
def export_recording(camera_name: str, start_time, end_time):
if not camera_name or not current_app.frigate_config.cameras.get(camera_name):
return make_response(
jsonify(
@router.post("/export/{camera_name}/start/{start_time}/end/{end_time}")
def export_recording(
request: Request,
camera_name: str,
start_time: float,
end_time: float,
body: dict = 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,
)
json: dict[str, any] = request.get_json(silent=True) or {}
json: dict[str, any] = body or {}
playback_factor = json.get("playback", "realtime")
friendly_name: Optional[str] = json.get("name")
if len(friendly_name or "") > 256:
return make_response(
jsonify({"success": False, "message": "File name is too long."}),
401,
return JSONResponse(
content=({"success": False, "message": "File name is too long."}),
status_code=401,
)
existing_image = json.get("image_path")
@@ -69,15 +65,15 @@ def export_recording(camera_name: str, start_time, end_time):
)
if recordings_count <= 0:
return make_response(
jsonify(
return JSONResponse(
content=(
{"success": False, "message": "No recordings found for time range"}
),
400,
status_code=400,
)
exporter = RecordingExporter(
current_app.frigate_config,
request.app.frigate_config,
camera_name,
friendly_name,
existing_image,
@@ -90,58 +86,58 @@ def export_recording(camera_name: str, start_time, end_time):
),
)
exporter.start()
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Starting export of recording.",
}
),
200,
status_code=200,
)
@ExportBp.route("/export/<id>/<new_name>", methods=["PATCH"])
def export_rename(id, new_name: str):
@router.patch("/export/{event_id}/{new_name}")
def export_rename(event_id: str, new_name: str):
try:
export: Export = Export.get(Export.id == id)
export: Export = Export.get(Export.id == event_id)
except DoesNotExist:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "Export not found.",
}
),
404,
status_code=404,
)
export.name = new_name
export.save()
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Successfully renamed export.",
}
),
200,
status_code=200,
)
@ExportBp.route("/export/<id>", methods=["DELETE"])
def export_delete(id: str):
@router.delete("/export/{event_id}")
def export_delete(event_id: str):
try:
export: Export = Export.get(Export.id == id)
export: Export = Export.get(Export.id == event_id)
except DoesNotExist:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "Export not found.",
}
),
404,
status_code=404,
)
files_in_use = []
@@ -158,11 +154,11 @@ def export_delete(id: str):
continue
if export.video_path.split("/")[-1] in files_in_use:
return make_response(
jsonify(
return JSONResponse(
content=(
{"success": False, "message": "Can not delete in progress export."}
),
400,
status_code=400,
)
Path(export.video_path).unlink(missing_ok=True)
@@ -171,12 +167,12 @@ def export_delete(id: str):
Path(export.thumb_path).unlink(missing_ok=True)
export.delete_instance()
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Successfully deleted export.",
}
),
200,
status_code=200,
)