mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Users who use OrcaSlicer without Bambu Cloud can now import slicer presets directly into Bambuddy. Supports .orca_filament, .bbscfg, .bbsflmt, .zip, and .json exports with automatic inheritance resolution via OrcaSlicer's GitHub base profiles (cached with 7-day TTL).
201 lines
6.6 KiB
Python
201 lines
6.6 KiB
Python
"""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)
|