feat(F2): recording list, download (decrypt), and delete API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import time as time_mod
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, render_template, request
|
||||
from flask import Blueprint, Response, current_app, jsonify, render_template, request
|
||||
|
||||
recordings_bp = Blueprint("recordings", __name__, url_prefix="/recordings")
|
||||
|
||||
@@ -18,10 +18,46 @@ def recordings_list():
|
||||
@recordings_bp.route("/api/list")
|
||||
def recordings_api():
|
||||
"""JSON API: recording list."""
|
||||
camera_id = request.args.get("camera")
|
||||
limit = request.args.get("limit", 50, type=int)
|
||||
# TODO: query from DB
|
||||
return jsonify([])
|
||||
engine = current_app.config.get("DB_ENGINE")
|
||||
if engine is None:
|
||||
return jsonify([])
|
||||
|
||||
from sqlalchemy import desc, select
|
||||
from vigilar.storage.schema import recordings
|
||||
|
||||
query = select(recordings).order_by(desc(recordings.c.started_at))
|
||||
|
||||
camera_id = request.args.get("camera_id")
|
||||
if camera_id:
|
||||
query = query.where(recordings.c.camera_id == camera_id)
|
||||
|
||||
date_str = request.args.get("date")
|
||||
if date_str:
|
||||
day = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
day_start = int(day.timestamp())
|
||||
day_end = int((day + timedelta(days=1)).timestamp())
|
||||
query = query.where(recordings.c.started_at >= day_start, recordings.c.started_at < day_end)
|
||||
|
||||
detection_type = request.args.get("detection_type")
|
||||
if detection_type:
|
||||
query = query.where(recordings.c.detection_type == detection_type)
|
||||
|
||||
starred = request.args.get("starred")
|
||||
if starred is not None:
|
||||
query = query.where(recordings.c.starred == int(starred))
|
||||
|
||||
limit = min(request.args.get("limit", 50, type=int), 500)
|
||||
query = query.limit(limit)
|
||||
|
||||
with engine.connect() as conn:
|
||||
rows = conn.execute(query).mappings().all()
|
||||
|
||||
return jsonify([{
|
||||
"id": r["id"], "camera_id": r["camera_id"],
|
||||
"started_at": r["started_at"], "ended_at": r["ended_at"],
|
||||
"duration_s": r["duration_s"], "detection_type": r["detection_type"],
|
||||
"starred": bool(r["starred"]), "file_size": r["file_size"], "trigger": r["trigger"],
|
||||
} for r in rows])
|
||||
|
||||
|
||||
@recordings_bp.route("/api/timeline")
|
||||
@@ -63,12 +99,69 @@ def timeline_api():
|
||||
@recordings_bp.route("/<int:recording_id>/download")
|
||||
def recording_download(recording_id: int):
|
||||
"""Stream decrypted recording for download/playback."""
|
||||
# TODO: Phase 6 — decrypt and stream
|
||||
return "Recording not available", 503
|
||||
engine = current_app.config.get("DB_ENGINE")
|
||||
if engine is None:
|
||||
return "Database not available", 503
|
||||
|
||||
from pathlib import Path
|
||||
from sqlalchemy import select
|
||||
from vigilar.storage.schema import recordings
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(select(recordings).where(recordings.c.id == recording_id)).mappings().first()
|
||||
|
||||
if not row:
|
||||
return "Recording not found", 404
|
||||
|
||||
file_path = row["file_path"]
|
||||
if not Path(file_path).exists():
|
||||
return "Recording file not found", 404
|
||||
|
||||
is_encrypted = row["encrypted"] and file_path.endswith(".vge")
|
||||
|
||||
if is_encrypted:
|
||||
import os
|
||||
key_hex = os.environ.get("VIGILAR_ENCRYPTION_KEY")
|
||||
if not key_hex:
|
||||
return "Encryption key not configured", 500
|
||||
from vigilar.storage.encryption import decrypt_stream
|
||||
return Response(decrypt_stream(file_path, key_hex), mimetype="video/mp4",
|
||||
headers={"Content-Disposition": f"inline; filename=recording_{recording_id}.mp4"})
|
||||
else:
|
||||
return Response(_read_file_chunks(file_path), mimetype="video/mp4",
|
||||
headers={"Content-Disposition": f"inline; filename=recording_{recording_id}.mp4"})
|
||||
|
||||
|
||||
def _read_file_chunks(path: str, chunk_size: int = 64 * 1024):
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
|
||||
@recordings_bp.route("/<int:recording_id>", methods=["DELETE"])
|
||||
def recording_delete(recording_id: int):
|
||||
"""Delete a recording."""
|
||||
# TODO: delete from DB + filesystem
|
||||
engine = current_app.config.get("DB_ENGINE")
|
||||
if engine is None:
|
||||
return jsonify({"error": "Database not available"}), 503
|
||||
|
||||
from pathlib import Path
|
||||
from sqlalchemy import select
|
||||
from vigilar.storage.schema import recordings
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(select(recordings).where(recordings.c.id == recording_id)).mappings().first()
|
||||
|
||||
if not row:
|
||||
return jsonify({"error": "Recording not found"}), 404
|
||||
|
||||
file_path = Path(row["file_path"])
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
from vigilar.storage.queries import delete_recording
|
||||
delete_recording(engine, recording_id)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
Reference in New Issue
Block a user