2026-10-23 14:47:27 +08:00
|
|
|
import json
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
2026-10-23 15:00:29 +08:00
|
|
|
from fastapi import Depends, Query
|
2026-10-23 14:47:27 +08:00
|
|
|
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):
|
2026-10-23 15:00:29 +08:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 14:47:27 +08:00
|
|
|
@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):
|
2026-10-23 15:00:29 +08:00
|
|
|
asset = service.assets.put(
|
2026-10-23 14:47:27 +08:00
|
|
|
image(request), request.plan.name + ".jpg", "generated"
|
|
|
|
|
)
|
2026-10-23 15:00:29 +08:00
|
|
|
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
|