"""API routes for local slicer presets (imported from OrcaSlicer, etc.).""" import json import logging from fastapi import APIRouter, Depends, HTTPException, UploadFile from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from backend.app.core.auth import RequirePermissionIfAuthEnabled from backend.app.core.database import get_db from backend.app.core.permissions import Permission from backend.app.models.local_preset import LocalPreset from backend.app.models.user import User from backend.app.schemas.local_preset import ( ImportResponse, LocalPresetCreate, LocalPresetDetail, LocalPresetResponse, LocalPresetsResponse, LocalPresetUpdate, ) from backend.app.services.orca_profiles import ( extract_core_fields, get_cache_status, import_orca_file, reclassify_presets, refresh_base_cache, resolve_preset, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/local-presets", tags=["Local Presets"]) @router.get("/", response_model=LocalPresetsResponse) async def list_local_presets( _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), db: AsyncSession = Depends(get_db), ): """List all local presets grouped by type.""" result = await db.execute(select(LocalPreset).order_by(LocalPreset.name)) presets = result.scalars().all() grouped = LocalPresetsResponse() for p in presets: resp = LocalPresetResponse.model_validate(p) if p.preset_type == "filament": grouped.filament.append(resp) elif p.preset_type == "printer": grouped.printer.append(resp) elif p.preset_type == "process": grouped.process.append(resp) return grouped @router.get("/{preset_id}", response_model=LocalPresetDetail) async def get_local_preset( preset_id: int, _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), db: AsyncSession = Depends(get_db), ): """Get full detail for a local preset including the setting JSON.""" result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id)) preset = result.scalar_one_or_none() if not preset: raise HTTPException(404, "Local preset not found") data = LocalPresetResponse.model_validate(preset).model_dump() try: data["setting"] = json.loads(preset.setting) except Exception: data["setting"] = {} return LocalPresetDetail(**data) @router.post("/import", response_model=ImportResponse) async def import_presets( file: UploadFile, _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Import presets from an OrcaSlicer export file (.json, .orca_filament, .bbscfg, .bbsflmt, .zip).""" if not file.filename: raise HTTPException(400, "No filename provided") content = await file.read() if not content: raise HTTPException(400, "Empty file") result = await import_orca_file(file.filename, content, db) return ImportResponse(**result) @router.post("/", response_model=LocalPresetResponse) async def create_local_preset( data: LocalPresetCreate, _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Manually create a local preset.""" if data.preset_type not in ("filament", "printer", "process"): raise HTTPException(400, "preset_type must be filament, printer, or process") # Extract core fields core = extract_core_fields(data.setting) preset = LocalPreset( name=data.name, preset_type=data.preset_type, source="manual", setting=json.dumps(data.setting), **core, ) db.add(preset) await db.flush() await db.refresh(preset) return LocalPresetResponse.model_validate(preset) @router.put("/{preset_id}", response_model=LocalPresetResponse) async def update_local_preset( preset_id: int, data: LocalPresetUpdate, _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Update a local preset's name or settings.""" result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id)) preset = result.scalar_one_or_none() if not preset: raise HTTPException(404, "Local preset not found") if data.name is not None: preset.name = data.name if data.setting is not None: # Re-resolve and extract core fields resolved = await resolve_preset(data.setting, preset.preset_type, db) core = extract_core_fields(resolved) preset.setting = json.dumps(resolved) preset.filament_type = core.get("filament_type") preset.filament_vendor = core.get("filament_vendor") preset.nozzle_temp_min = core.get("nozzle_temp_min") preset.nozzle_temp_max = core.get("nozzle_temp_max") preset.pressure_advance = core.get("pressure_advance") preset.default_filament_colour = core.get("default_filament_colour") preset.filament_cost = core.get("filament_cost") preset.filament_density = core.get("filament_density") preset.compatible_printers = core.get("compatible_printers") await db.flush() await db.refresh(preset) return LocalPresetResponse.model_validate(preset) @router.delete("/{preset_id}") async def delete_local_preset( preset_id: int, _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Delete a local preset.""" result = await db.execute(select(LocalPreset).where(LocalPreset.id == preset_id)) preset = result.scalar_one_or_none() if not preset: raise HTTPException(404, "Local preset not found") await db.delete(preset) return {"success": True} @router.get("/base-cache/status") async def base_cache_status( _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), db: AsyncSession = Depends(get_db), ): """Get the status of the OrcaSlicer base profile cache.""" return await get_cache_status(db) @router.post("/base-cache/refresh") async def refresh_cache( _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Force refresh all cached base profiles from GitHub.""" return await refresh_base_cache(db) @router.post("/reclassify") async def reclassify( _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE), db: AsyncSession = Depends(get_db), ): """Re-evaluate preset types for all local presets using the improved heuristic.""" return await reclassify_presets(db)