import json from uuid import uuid4 from fastapi import Depends, Query from fastapi.responses import Response from pydantic import Field from pigway_frame_flow.core.auth import require from pigway_frame_flow.display.thumbnail_plans import ( ThumbnailPlan, get_plan, render, save_plan, ) from pigway_frame_flow.errors import ErrorCode as E from pigway_frame_flow.errors import FlowError from pigway_frame_flow.models import StrictModel class ThumbnailRequest(StrictModel): source_id: str = Field(min_length=1, max_length=120) item_id: str = Field(min_length=1, max_length=2048) plan: ThumbnailPlan def install_thumbnails(app, service): from pigway_frame_flow.display.thumbnail_gallery import record, recover recover(service) @app.get( "/api/v1/thumbnail-devices", dependencies=[Depends(require("images:read"))] ) def devices(): return [ { "id": "watch", "name": "Watch", "width": 410, "height": 502, "transport_connected": False, "status": "generation_only", } ] @app.get("/api/v1/thumbnails", dependencies=[Depends(require("images:read"))]) def gallery(offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200)): rows = service.db.all( "SELECT assets.id,asset_sources.payload FROM assets JOIN asset_sources ON asset_sources.asset_id=assets.id " "WHERE json_extract(asset_sources.payload,'$.type')='thumbnail' " "ORDER BY assets.created_at DESC,assets.id DESC LIMIT ? OFFSET ?", (limit, offset), ) total = service.db.one( "SELECT COUNT(*) AS count FROM assets JOIN asset_sources ON asset_sources.asset_id=assets.id " "WHERE json_extract(asset_sources.payload,'$.type')='thumbnail'" )["count"] return { "items": [ { **service.assets.info(row["id"]), "thumbnail": json.loads(row["payload"]), } for row in rows ], "total": total, "offset": offset, "limit": limit, } @app.get("/api/v1/thumbnail-plans", dependencies=[Depends(require("images:read"))]) def plans(): return [ dict( id=r["key"].removeprefix("thumbnail-plan:"), **json.loads(r["payload"]) ) for r in service.db.all( "SELECT key,payload FROM app_config WHERE key LIKE 'thumbnail-plan:%' ORDER BY key" ) ] @app.post("/api/v1/thumbnail-plans", dependencies=[Depends(require("admin"))]) def create(plan: ThumbnailPlan): plan_id = uuid4().hex save_plan(service, plan_id, plan) return dict(id=plan_id, **plan.model_dump()) @app.get( "/api/v1/thumbnail-plans/{plan_id}", dependencies=[Depends(require("images:read"))], ) def detail(plan_id: str): return dict(id=plan_id, **get_plan(service, plan_id).model_dump()) @app.put( "/api/v1/thumbnail-plans/{plan_id}", dependencies=[Depends(require("admin"))] ) def update(plan_id: str, plan: ThumbnailPlan): with service.lock: get_plan(service, plan_id) save_plan(service, plan_id, plan) return dict(id=plan_id, **plan.model_dump()) @app.delete( "/api/v1/thumbnail-plans/{plan_id}", dependencies=[Depends(require("admin"))], status_code=204, ) def delete(plan_id: str): with service.lock: get_plan(service, plan_id) for row in service.db.all("SELECT payload FROM jobs"): job = json.loads(row["payload"]) if job.get("thumbnail_plan_id") == plan_id: raise FlowError( E.CONFLICT, "Thumbnail plan is used by a task", 409, {"reason": "thumbnailPlanInUse"}, ) for row in service.db.all( "SELECT payload FROM operations WHERE status IN ('QUEUED','RENDERING','UPLOADING','UNKNOWN')" ): if ( json.loads(row["payload"]) .get("options", {}) .get("thumbnail_plan_id") == plan_id ): raise FlowError( E.CONFLICT, "Thumbnail plan is used by a pending operation", 409, {"reason": "thumbnailPlanInUse"}, ) service.db.execute( "DELETE FROM app_config WHERE key=?", ("thumbnail-plan:" + plan_id,) ) def image(request): data, _, _ = service.photo_sources.read( request.source_id, request.item_id, target_size=(request.plan.width, request.plan.height), ) return render(data, request.plan) @app.post( "/api/v1/thumbnails/preview", dependencies=[Depends(require("images:read"))] ) def preview(request: ThumbnailRequest): return Response( image(request), media_type="image/jpeg", headers={"Cache-Control": "no-store"}, ) @app.post("/api/v1/thumbnails/generate", dependencies=[Depends(require("admin"))]) def generate(request: ThumbnailRequest): asset = service.assets.put( image(request), request.plan.name + ".jpg", "generated" ) record( service, asset["asset_id"], { "width": request.plan.width, "height": request.plan.height, "plan": request.plan.model_dump(), "generated_at": asset["created_at"], "source": {"source_id": request.source_id, "item_id": request.item_id}, }, ) return asset