b7c729d331
Routes package with CRUD endpoints: event listing/filtering/tagging/dismissal, known object management with thumbnails, settings CRUD, and stats. 64 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
5.0 KiB
Python
178 lines
5.0 KiB
Python
"""Known objects API endpoints for frigate-notify v2."""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from fastapi.responses import Response
|
|
|
|
from database import get_connection
|
|
from models import KnownObjectResponse, KnownObjectUpdate
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/known-objects", tags=["known-objects"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_known_objects(
|
|
type: Optional[str] = Query(default=None, pattern="^(person|vehicle|pet)$"),
|
|
):
|
|
"""List all known objects with embedding_count. Optional type filter."""
|
|
conn = get_connection()
|
|
|
|
query = """
|
|
SELECT
|
|
ko.id, ko.name, ko.type, ko.action,
|
|
ko.created_at, ko.updated_at,
|
|
COUNT(e.id) AS embedding_count
|
|
FROM known_objects ko
|
|
LEFT JOIN embeddings e ON ko.id = e.known_object_id
|
|
"""
|
|
params: list = []
|
|
|
|
if type:
|
|
query += " WHERE ko.type = ?"
|
|
params.append(type)
|
|
|
|
query += " GROUP BY ko.id ORDER BY ko.name"
|
|
|
|
cursor = conn.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
return [
|
|
{
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"type": row["type"],
|
|
"action": row["action"],
|
|
"created_at": row["created_at"],
|
|
"updated_at": row["updated_at"],
|
|
"embedding_count": row["embedding_count"],
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
@router.get("/{object_id}")
|
|
async def get_known_object(object_id: int):
|
|
"""Get a single known object with list of its embeddings (id, created_at only)."""
|
|
conn = get_connection()
|
|
|
|
cursor = conn.execute(
|
|
"SELECT id, name, type, action, created_at, updated_at FROM known_objects WHERE id = ?",
|
|
(object_id,),
|
|
)
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Known object not found")
|
|
|
|
# Get embeddings list
|
|
emb_cursor = conn.execute(
|
|
"SELECT id, created_at FROM embeddings WHERE known_object_id = ? ORDER BY created_at",
|
|
(object_id,),
|
|
)
|
|
embeddings = [
|
|
{"id": e["id"], "created_at": e["created_at"]} for e in emb_cursor.fetchall()
|
|
]
|
|
|
|
return {
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"type": row["type"],
|
|
"action": row["action"],
|
|
"created_at": row["created_at"],
|
|
"updated_at": row["updated_at"],
|
|
"embeddings": embeddings,
|
|
}
|
|
|
|
|
|
@router.get("/{object_id}/thumbnails/{embedding_id}")
|
|
async def get_embedding_thumbnail(object_id: int, embedding_id: int):
|
|
"""Return raw JPEG thumbnail from embeddings table."""
|
|
conn = get_connection()
|
|
|
|
cursor = conn.execute(
|
|
"SELECT thumbnail FROM embeddings WHERE id = ? AND known_object_id = ?",
|
|
(embedding_id, object_id),
|
|
)
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Embedding not found")
|
|
if row["thumbnail"] is None:
|
|
raise HTTPException(status_code=404, detail="No thumbnail available")
|
|
|
|
return Response(content=row["thumbnail"], media_type="image/jpeg")
|
|
|
|
|
|
@router.put("/{object_id}")
|
|
async def update_known_object(object_id: int, body: KnownObjectUpdate):
|
|
"""Update name/type/action for a known object. Sets updated_at."""
|
|
conn = get_connection()
|
|
|
|
cursor = conn.execute(
|
|
"SELECT id FROM known_objects WHERE id = ?",
|
|
(object_id,),
|
|
)
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Known object not found")
|
|
|
|
updates = []
|
|
params: list = []
|
|
|
|
if body.name is not None:
|
|
updates.append("name = ?")
|
|
params.append(body.name)
|
|
if body.type is not None:
|
|
updates.append("type = ?")
|
|
params.append(body.type)
|
|
if body.action is not None:
|
|
updates.append("action = ?")
|
|
params.append(body.action)
|
|
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
|
|
|
updates.append("updated_at = CURRENT_TIMESTAMP")
|
|
params.append(object_id)
|
|
|
|
conn.execute(
|
|
f"UPDATE known_objects SET {', '.join(updates)} WHERE id = ?",
|
|
params,
|
|
)
|
|
conn.commit()
|
|
|
|
# Return the updated object
|
|
cursor = conn.execute(
|
|
"SELECT id, name, type, action, created_at, updated_at FROM known_objects WHERE id = ?",
|
|
(object_id,),
|
|
)
|
|
row = cursor.fetchone()
|
|
|
|
return KnownObjectResponse(
|
|
id=row["id"],
|
|
name=row["name"],
|
|
type=row["type"],
|
|
action=row["action"],
|
|
created_at=row["created_at"],
|
|
updated_at=row["updated_at"],
|
|
)
|
|
|
|
|
|
@router.delete("/{object_id}")
|
|
async def delete_known_object(object_id: int):
|
|
"""Delete a known object. Embeddings cascade delete automatically."""
|
|
conn = get_connection()
|
|
|
|
cursor = conn.execute(
|
|
"SELECT id FROM known_objects WHERE id = ?",
|
|
(object_id,),
|
|
)
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Known object not found")
|
|
|
|
conn.execute("DELETE FROM known_objects WHERE id = ?", (object_id,))
|
|
conn.commit()
|
|
|
|
return {"status": "ok"}
|