blakeblackshear.frigate/frigate/api/export.py
Rui Alves cffc431bf0
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>
2024-09-24 07:05:30 -06:00

179 lines
4.7 KiB
Python

"""Export apis."""
import logging
from pathlib import Path
from typing import Optional
import psutil
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__)
router = APIRouter(tags=[Tags.export])
@router.get("/exports")
def get_exports():
exports = Export.select().order_by(Export.date.desc()).dicts().iterator()
return JSONResponse(content=[e for e in exports])
@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."}
),
status_code=404,
)
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 JSONResponse(
content=({"success": False, "message": "File name is too long."}),
status_code=401,
)
existing_image = json.get("image_path")
recordings_count = (
Recordings.select()
.where(
Recordings.start_time.between(start_time, end_time)
| Recordings.end_time.between(start_time, end_time)
| ((start_time > Recordings.start_time) & (end_time < Recordings.end_time))
)
.where(Recordings.camera == camera_name)
.count()
)
if recordings_count <= 0:
return JSONResponse(
content=(
{"success": False, "message": "No recordings found for time range"}
),
status_code=400,
)
exporter = RecordingExporter(
request.app.frigate_config,
camera_name,
friendly_name,
existing_image,
int(start_time),
int(end_time),
(
PlaybackFactorEnum[playback_factor]
if playback_factor in PlaybackFactorEnum.__members__.values()
else PlaybackFactorEnum.realtime
),
)
exporter.start()
return JSONResponse(
content=(
{
"success": True,
"message": "Starting export of recording.",
}
),
status_code=200,
)
@router.patch("/export/{event_id}/{new_name}")
def export_rename(event_id: str, new_name: str):
try:
export: Export = Export.get(Export.id == event_id)
except DoesNotExist:
return JSONResponse(
content=(
{
"success": False,
"message": "Export not found.",
}
),
status_code=404,
)
export.name = new_name
export.save()
return JSONResponse(
content=(
{
"success": True,
"message": "Successfully renamed export.",
}
),
status_code=200,
)
@router.delete("/export/{event_id}")
def export_delete(event_id: str):
try:
export: Export = Export.get(Export.id == event_id)
except DoesNotExist:
return JSONResponse(
content=(
{
"success": False,
"message": "Export not found.",
}
),
status_code=404,
)
files_in_use = []
for process in psutil.process_iter():
try:
if process.name() != "ffmpeg":
continue
file_list = process.open_files()
if file_list:
for nt in file_list:
if nt.path.startswith(EXPORT_DIR):
files_in_use.append(nt.path.split("/")[-1])
except psutil.Error:
continue
if export.video_path.split("/")[-1] in files_in_use:
return JSONResponse(
content=(
{"success": False, "message": "Can not delete in progress export."}
),
status_code=400,
)
Path(export.video_path).unlink(missing_ok=True)
if export.thumb_path:
Path(export.thumb_path).unlink(missing_ok=True)
export.delete_instance()
return JSONResponse(
content=(
{
"success": True,
"message": "Successfully deleted export.",
}
),
status_code=200,
)