mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
Reorganize apis to use IDs
This commit is contained in:
parent
2fa5ea5200
commit
a87ec6b8ab
@ -15,6 +15,7 @@ from peewee import operator
|
|||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
from frigate.api.event import EventBp
|
from frigate.api.event import EventBp
|
||||||
|
from frigate.api.export import ExportBp
|
||||||
from frigate.api.media import MediaBp
|
from frigate.api.media import MediaBp
|
||||||
from frigate.api.preview import PreviewBp
|
from frigate.api.preview import PreviewBp
|
||||||
from frigate.api.review import ReviewBp
|
from frigate.api.review import ReviewBp
|
||||||
@ -39,6 +40,7 @@ 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(MediaBp)
|
bp.register_blueprint(MediaBp)
|
||||||
bp.register_blueprint(PreviewBp)
|
bp.register_blueprint(PreviewBp)
|
||||||
bp.register_blueprint(ReviewBp)
|
bp.register_blueprint(ReviewBp)
|
||||||
|
|||||||
155
frigate/api/export.py
Normal file
155
frigate/api/export.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
"""Export apis."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
jsonify,
|
||||||
|
make_response,
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
from peewee import DoesNotExist
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from frigate.models import Export, Recordings
|
||||||
|
from frigate.record.export import PlaybackFactorEnum, RecordingExporter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ExportBp = Blueprint("exports", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ExportBp.route("/exports")
|
||||||
|
def get_exports():
|
||||||
|
exports = Export.select().order_by(Export.date.desc()).dicts().iterator()
|
||||||
|
return jsonify([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(
|
||||||
|
{"success": False, "message": f"{camera_name} is not a valid camera."}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||||
|
playback_factor = json.get("playback", "realtime")
|
||||||
|
name: Optional[str] = json.get("name")
|
||||||
|
|
||||||
|
if len(name or "") > 256:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "File name is too long."}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 make_response(
|
||||||
|
jsonify(
|
||||||
|
{"success": False, "message": "No recordings found for time range"}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
exporter = RecordingExporter(
|
||||||
|
current_app.frigate_config,
|
||||||
|
camera_name,
|
||||||
|
secure_filename(name.replace(" ", "_")) if name else None,
|
||||||
|
int(start_time),
|
||||||
|
int(end_time),
|
||||||
|
(
|
||||||
|
PlaybackFactorEnum[playback_factor]
|
||||||
|
if playback_factor in PlaybackFactorEnum.__members__.values()
|
||||||
|
else PlaybackFactorEnum.realtime
|
||||||
|
),
|
||||||
|
)
|
||||||
|
exporter.start()
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Starting export of recording.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ExportBp.route("/export/<id>/<new_name>", methods=["POST"])
|
||||||
|
def export_rename(id, new_name: str):
|
||||||
|
try:
|
||||||
|
export: Export = Export.get(Export.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Export not found.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
export.name = new_name
|
||||||
|
export.save()
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Successfully renamed export.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ExportBp.route("/export/<id>", methods=["DELETE"])
|
||||||
|
def export_delete(id: str):
|
||||||
|
try:
|
||||||
|
export: Export = Export.get(Export.id == id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "Export not found.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
Path(export.video_path).unlink(missing_ok=True)
|
||||||
|
Path(export.thumb_path).unlink(missing_ok=True)
|
||||||
|
export.delete()
|
||||||
|
return make_response(
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Successfully deleted export.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
@ -4,11 +4,9 @@ import base64
|
|||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -22,13 +20,11 @@ from werkzeug.utils import secure_filename
|
|||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
CLIPS_DIR,
|
CLIPS_DIR,
|
||||||
EXPORT_DIR,
|
|
||||||
MAX_SEGMENT_DURATION,
|
MAX_SEGMENT_DURATION,
|
||||||
PREVIEW_FRAME_TYPE,
|
PREVIEW_FRAME_TYPE,
|
||||||
RECORD_DIR,
|
RECORD_DIR,
|
||||||
)
|
)
|
||||||
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
|
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
|
||||||
from frigate.record.export import PlaybackFactorEnum, RecordingExporter
|
|
||||||
from frigate.util.builtin import get_tz_modifiers
|
from frigate.util.builtin import get_tz_modifiers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -592,157 +588,6 @@ def vod_event(id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route(
|
|
||||||
"/export/<camera_name>/start/<int:start_time>/end/<int:end_time>", methods=["POST"]
|
|
||||||
)
|
|
||||||
@MediaBp.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(
|
|
||||||
{"success": False, "message": f"{camera_name} is not a valid camera."}
|
|
||||||
),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
|
||||||
playback_factor = json.get("playback", "realtime")
|
|
||||||
name: Optional[str] = json.get("name")
|
|
||||||
|
|
||||||
if len(name or "") > 256:
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": "File name is too long."}),
|
|
||||||
401,
|
|
||||||
)
|
|
||||||
|
|
||||||
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 make_response(
|
|
||||||
jsonify(
|
|
||||||
{"success": False, "message": "No recordings found for time range"}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
exporter = RecordingExporter(
|
|
||||||
current_app.frigate_config,
|
|
||||||
camera_name,
|
|
||||||
secure_filename(name.replace(" ", "_")) if name else None,
|
|
||||||
int(start_time),
|
|
||||||
int(end_time),
|
|
||||||
(
|
|
||||||
PlaybackFactorEnum[playback_factor]
|
|
||||||
if playback_factor in PlaybackFactorEnum.__members__.values()
|
|
||||||
else PlaybackFactorEnum.realtime
|
|
||||||
),
|
|
||||||
)
|
|
||||||
exporter.start()
|
|
||||||
return make_response(
|
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Starting export of recording.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def export_filename_check_extension(filename: str):
|
|
||||||
if filename.endswith(".mp4"):
|
|
||||||
return filename
|
|
||||||
else:
|
|
||||||
return filename + ".mp4"
|
|
||||||
|
|
||||||
|
|
||||||
def export_filename_is_valid(filename: str):
|
|
||||||
if re.search(r"[^:_A-Za-z0-9]", filename) or filename.startswith("in_progress."):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route("/export/<file_name_current>/<file_name_new>", methods=["PATCH"])
|
|
||||||
def export_rename(file_name_current, file_name_new: str):
|
|
||||||
safe_file_name_current = secure_filename(
|
|
||||||
export_filename_check_extension(file_name_current)
|
|
||||||
)
|
|
||||||
file_current = os.path.join(EXPORT_DIR, safe_file_name_current)
|
|
||||||
|
|
||||||
if not os.path.exists(file_current):
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": f"{file_name_current} not found."}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not export_filename_is_valid(file_name_new):
|
|
||||||
return make_response(
|
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"{file_name_new} contains illegal characters.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_file_name_new = secure_filename(export_filename_check_extension(file_name_new))
|
|
||||||
file_new = os.path.join(EXPORT_DIR, safe_file_name_new)
|
|
||||||
|
|
||||||
if os.path.exists(file_new):
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": f"{file_name_new} already exists."}),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
os.rename(file_current, file_new)
|
|
||||||
return make_response(
|
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Successfully renamed file.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route("/export/<file_name>", methods=["DELETE"])
|
|
||||||
def export_delete(file_name: str):
|
|
||||||
safe_file_name = secure_filename(export_filename_check_extension(file_name))
|
|
||||||
file = os.path.join(EXPORT_DIR, safe_file_name)
|
|
||||||
|
|
||||||
if not os.path.exists(file):
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": f"{file_name} not found."}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
os.unlink(file)
|
|
||||||
return make_response(
|
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Successfully deleted file.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route("/<camera_name>/<label>/snapshot.jpg")
|
@MediaBp.route("/<camera_name>/<label>/snapshot.jpg")
|
||||||
def label_snapshot(camera_name, label):
|
def label_snapshot(camera_name, label):
|
||||||
label = unquote(label)
|
label = unquote(label)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user