"""Asset Libraries API — CRUD + .blend upload + catalog refresh.""" import uuid import shutil from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel from app.database import get_db from app.config import settings from app.domains.materials.models import AssetLibrary from app.utils.auth import require_admin_or_pm router = APIRouter(prefix="/asset-libraries", tags=["asset-libraries"]) ASSET_LIB_DIR = "asset-libraries" def _asset_lib_dir() -> Path: d = Path(settings.upload_dir) / ASSET_LIB_DIR d.mkdir(parents=True, exist_ok=True) return d # ── Schemas ────────────────────────────────────────────────────────────────── class AssetLibraryOut(BaseModel): id: str name: str description: str | None original_filename: str | None catalog: dict is_active: bool created_at: str updated_at: str class AssetLibraryUpdate(BaseModel): name: str | None = None description: str | None = None is_active: bool | None = None def _to_out(lib: AssetLibrary) -> dict: return { "id": str(lib.id), "name": lib.name, "description": lib.description, "original_filename": lib.original_filename, "catalog": lib.catalog or {"materials": [], "node_groups": []}, "is_active": lib.is_active, "created_at": lib.created_at.isoformat(), "updated_at": lib.updated_at.isoformat(), } # ── Endpoints ───────────────────────────────────────────────────────────────── @router.get("", response_model=list[AssetLibraryOut]) async def list_asset_libraries( db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): result = await db.execute(select(AssetLibrary).order_by(AssetLibrary.name)) return [_to_out(lib) for lib in result.scalars().all()] @router.post("", response_model=AssetLibraryOut, status_code=status.HTTP_201_CREATED) async def create_asset_library( name: str = Form(...), description: str | None = Form(None), blend_file: UploadFile = File(...), db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = AssetLibrary( name=name, description=description, original_filename=blend_file.filename, catalog={"materials": [], "node_groups": []}, ) db.add(lib) await db.flush() # get the id # Save .blend file dest = _asset_lib_dir() / f"{lib.id}.blend" with dest.open("wb") as f: shutil.copyfileobj(blend_file.file, f) lib.blend_file_path = str(dest) await db.commit() await db.refresh(lib) # Queue catalog refresh try: from app.domains.materials.tasks import refresh_asset_library_catalog refresh_asset_library_catalog.delay(str(lib.id)) except Exception: pass # task queuing failure is non-blocking return _to_out(lib) @router.get("/{lib_id}", response_model=AssetLibraryOut) async def get_asset_library( lib_id: uuid.UUID, db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = await db.get(AssetLibrary, lib_id) if not lib: raise HTTPException(status_code=404, detail="Asset library not found") return _to_out(lib) @router.patch("/{lib_id}", response_model=AssetLibraryOut) async def update_asset_library( lib_id: uuid.UUID, body: AssetLibraryUpdate, db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = await db.get(AssetLibrary, lib_id) if not lib: raise HTTPException(status_code=404, detail="Asset library not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(lib, field, value) await db.commit() await db.refresh(lib) return _to_out(lib) @router.post("/{lib_id}/upload-blend", response_model=AssetLibraryOut) async def upload_blend( lib_id: uuid.UUID, blend_file: UploadFile = File(...), db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = await db.get(AssetLibrary, lib_id) if not lib: raise HTTPException(status_code=404, detail="Asset library not found") dest = _asset_lib_dir() / f"{lib.id}.blend" with dest.open("wb") as f: shutil.copyfileobj(blend_file.file, f) lib.blend_file_path = str(dest) lib.original_filename = blend_file.filename await db.commit() await db.refresh(lib) try: from app.domains.materials.tasks import refresh_asset_library_catalog refresh_asset_library_catalog.delay(str(lib.id)) except Exception: pass return _to_out(lib) @router.post("/{lib_id}/refresh-catalog", response_model=AssetLibraryOut) async def refresh_catalog( lib_id: uuid.UUID, db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = await db.get(AssetLibrary, lib_id) if not lib: raise HTTPException(status_code=404, detail="Asset library not found") if not lib.blend_file_path: raise HTTPException(status_code=400, detail="No .blend file uploaded yet") from app.domains.materials.tasks import refresh_asset_library_catalog refresh_asset_library_catalog.delay(str(lib.id)) return _to_out(lib) @router.delete("/{lib_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_asset_library( lib_id: uuid.UUID, db: AsyncSession = Depends(get_db), _user=Depends(require_admin_or_pm), ): lib = await db.get(AssetLibrary, lib_id) if not lib: raise HTTPException(status_code=404, detail="Asset library not found") # Remove .blend file from disk if lib.blend_file_path: p = Path(lib.blend_file_path) if p.exists(): p.unlink() await db.delete(lib) await db.commit()