Files
frigate/frigate/api/export.py
T

289 lines
8.6 KiB
Python
Raw Normal View History

2024-04-19 16:11:41 -06:00
"""Export apis."""
import logging
2024-09-30 15:55:44 -06:00
import random
import string
2024-04-19 16:11:41 -06:00
from pathlib import Path
from typing import List
2024-04-19 16:11:41 -06:00
2024-06-04 09:10:19 -06:00
import psutil
2025-03-08 10:01:08 -06:00
from fastapi import APIRouter, Depends, Request
2024-09-24 14:05:30 +01:00
from fastapi.responses import JSONResponse
from pathvalidate import sanitize_filepath
2024-04-19 16:11:41 -06:00
from peewee import DoesNotExist
2024-12-06 08:04:02 -06:00
from playhouse.shortcuts import model_to_dict
2024-04-19 16:11:41 -06:00
from frigate.api.auth import (
get_allowed_cameras_for_filter,
require_camera_access,
require_role,
)
2024-11-11 12:02:46 +00:00
from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody
2025-02-14 20:18:14 -06:00
from frigate.api.defs.request.export_rename_body import ExportRenameBody
2025-10-08 13:55:38 -06:00
from frigate.api.defs.response.export_response import (
ExportModel,
ExportsResponse,
StartExportResponse,
)
from frigate.api.defs.response.generic_response import GenericResponse
2024-09-24 14:05:30 +01:00
from frigate.api.defs.tags import Tags
from frigate.const import CLIPS_DIR, EXPORT_DIR
2024-10-23 07:36:52 -06:00
from frigate.models import Export, Previews, Recordings
from frigate.record.export import (
PlaybackFactorEnum,
PlaybackSourceEnum,
RecordingExporter,
)
from frigate.util.builtin import is_current_hour
2024-04-19 16:11:41 -06:00
logger = logging.getLogger(__name__)
2024-09-24 14:05:30 +01:00
router = APIRouter(tags=[Tags.export])
2024-04-19 16:11:41 -06:00
2025-10-08 13:55:38 -06:00
@router.get(
"/exports",
response_model=ExportsResponse,
summary="Get exports",
description="""Gets all exports from the database for cameras the user has access to.
Returns a list of exports ordered by date (most recent first).""",
)
def get_exports(
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
):
exports = (
Export.select()
.where(Export.camera << allowed_cameras)
.order_by(Export.date.desc())
.dicts()
.iterator()
)
2024-09-24 14:05:30 +01:00
return JSONResponse(content=[e for e in exports])
2024-04-19 16:11:41 -06:00
@router.post(
"/export/{camera_name}/start/{start_time}/end/{end_time}",
2025-10-08 13:55:38 -06:00
response_model=StartExportResponse,
dependencies=[Depends(require_camera_access)],
2025-10-08 13:55:38 -06:00
summary="Start recording export",
description="""Starts an export of a recording for the specified time range.
The export can be from recordings or preview footage. Returns the export ID if
successful, or an error message if the camera is invalid or no recordings/previews
are found for the time range.""",
)
2024-09-24 14:05:30 +01:00
def export_recording(
request: Request,
camera_name: str,
start_time: float,
end_time: float,
body: ExportRecordingsBody,
2024-09-24 14:05:30 +01:00
):
if not camera_name or not request.app.frigate_config.cameras.get(camera_name):
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{"success": False, "message": f"{camera_name} is not a valid camera."}
),
2024-09-24 14:05:30 +01:00
status_code=404,
2024-04-19 16:11:41 -06:00
)
playback_factor = body.playback
playback_source = body.source
friendly_name = body.name
existing_image = sanitize_filepath(body.image_path) if body.image_path else None
# Ensure that existing_image is a valid path
if existing_image and not existing_image.startswith(CLIPS_DIR):
return JSONResponse(
content=({"success": False, "message": "Invalid image path"}),
status_code=400,
)
2024-10-23 07:36:52 -06:00
if playback_source == "recordings":
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()
2024-04-19 16:11:41 -06:00
)
2024-10-23 07:36:52 -06:00
if recordings_count <= 0:
return JSONResponse(
content=(
{"success": False, "message": "No recordings found for time range"}
),
status_code=400,
)
else:
previews_count = (
Previews.select()
.where(
Previews.start_time.between(start_time, end_time)
| Previews.end_time.between(start_time, end_time)
| ((start_time > Previews.start_time) & (end_time < Previews.end_time))
)
.where(Previews.camera == camera_name)
.count()
2024-04-19 16:11:41 -06:00
)
if not is_current_hour(start_time) and previews_count <= 0:
2024-10-23 07:36:52 -06:00
return JSONResponse(
content=(
{"success": False, "message": "No previews found for time range"}
),
status_code=400,
)
2024-09-30 15:55:44 -06:00
export_id = f"{camera_name}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}"
2024-04-19 16:11:41 -06:00
exporter = RecordingExporter(
2024-09-24 14:05:30 +01:00
request.app.frigate_config,
2024-09-30 15:55:44 -06:00
export_id,
2024-04-19 16:11:41 -06:00
camera_name,
2024-07-17 07:39:37 -06:00
friendly_name,
existing_image,
2024-04-19 16:11:41 -06:00
int(start_time),
int(end_time),
(
PlaybackFactorEnum[playback_factor]
if playback_factor in PlaybackFactorEnum.__members__.values()
else PlaybackFactorEnum.realtime
),
2024-10-23 07:36:52 -06:00
(
PlaybackSourceEnum[playback_source]
if playback_source in PlaybackSourceEnum.__members__.values()
else PlaybackSourceEnum.recordings
),
2024-04-19 16:11:41 -06:00
)
exporter.start()
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{
"success": True,
"message": "Starting export of recording.",
2024-09-30 15:55:44 -06:00
"export_id": export_id,
2024-04-19 16:11:41 -06:00
}
),
2024-09-24 14:05:30 +01:00
status_code=200,
2024-04-19 16:11:41 -06:00
)
2025-03-08 10:01:08 -06:00
@router.patch(
2025-10-08 13:55:38 -06:00
"/export/{event_id}/rename",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Rename export",
description="""Renames an export.
NOTE: This changes the friendly name of the export, not the filename.
""",
2025-03-08 10:01:08 -06:00
)
async def export_rename(event_id: str, body: ExportRenameBody, request: Request):
2024-04-19 16:11:41 -06:00
try:
2024-09-24 14:05:30 +01:00
export: Export = Export.get(Export.id == event_id)
await require_camera_access(export.camera, request=request)
2024-04-19 16:11:41 -06:00
except DoesNotExist:
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{
"success": False,
"message": "Export not found.",
}
),
2024-09-24 14:05:30 +01:00
status_code=404,
2024-04-19 16:11:41 -06:00
)
2025-02-14 20:18:14 -06:00
export.name = body.name
2024-04-19 16:11:41 -06:00
export.save()
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{
"success": True,
"message": "Successfully renamed export.",
}
),
2024-09-24 14:05:30 +01:00
status_code=200,
2024-04-19 16:11:41 -06:00
)
2025-10-08 13:55:38 -06:00
@router.delete(
"/export/{event_id}",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete export",
)
async def export_delete(event_id: str, request: Request):
2024-04-19 16:11:41 -06:00
try:
2024-09-24 14:05:30 +01:00
export: Export = Export.get(Export.id == event_id)
await require_camera_access(export.camera, request=request)
2024-04-19 16:11:41 -06:00
except DoesNotExist:
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{
"success": False,
"message": "Export not found.",
}
),
2024-09-24 14:05:30 +01:00
status_code=404,
2024-04-19 16:11:41 -06:00
)
2024-06-04 09:10:19 -06:00
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:
2024-06-04 09:10:19 -06:00
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:
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-06-04 09:10:19 -06:00
{"success": False, "message": "Can not delete in progress export."}
),
2024-09-24 14:05:30 +01:00
status_code=400,
2024-06-04 09:10:19 -06:00
)
2024-04-19 16:11:41 -06:00
Path(export.video_path).unlink(missing_ok=True)
if export.thumb_path:
Path(export.thumb_path).unlink(missing_ok=True)
export.delete_instance()
2024-09-24 14:05:30 +01:00
return JSONResponse(
content=(
2024-04-19 16:11:41 -06:00
{
"success": True,
"message": "Successfully deleted export.",
}
),
2024-09-24 14:05:30 +01:00
status_code=200,
2024-04-19 16:11:41 -06:00
)
2024-12-06 08:04:02 -06:00
2025-10-08 13:55:38 -06:00
@router.get(
"/exports/{export_id}",
response_model=ExportModel,
summary="Get a single export",
description="""Gets a specific export by ID. The user must have access to the camera
associated with the export.""",
)
async def get_export(export_id: str, request: Request):
2024-12-06 08:04:02 -06:00
try:
export = Export.get(Export.id == export_id)
await require_camera_access(export.camera, request=request)
return JSONResponse(content=model_to_dict(export))
2024-12-06 08:04:02 -06:00
except DoesNotExist:
return JSONResponse(
content={"success": False, "message": "Export not found"},
status_code=404,
)