import json from uuid import uuid4 from fastapi import Depends 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): @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): return service.assets.put( image(request), request.plan.name + ".jpg", "generated" )