# SonicForge Preset Library API — thư viện preset VST3 (.vstpreset) nằm trong # storage/presets (mount qua volume trong docker; thư mục storage trên Windows). # # Vai trò: cầu nối Carla → pedalboard. User chỉnh preset trong Carla (native # GUI) → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong # synth_engine) → render_engine tải qua load_preset → âm render = âm đã chỉnh. import os import uuid import json import time from fastapi import APIRouter, HTTPException, UploadFile, File, Depends from fastapi.responses import FileResponse from typing import Optional from app.core.vst_engine import preset_library_dir, PRESET_EXTENSIONS from app.api.v1.auth import get_current_user, enforce_password_changed router = APIRouter() def _safe_preset_path(preset_id: str) -> str: """Chống path traversal: chỉ cho phép tên file (không chứa separator).""" if not preset_id or os.path.basename(preset_id) != preset_id: return "" d = preset_library_dir() p = os.path.join(d, preset_id) if os.path.isfile(p) and os.path.dirname(os.path.abspath(p)) == os.path.abspath(d): return p return "" @router.get("") async def list_presets(): """Danh sách preset trong thư viện (public — frontend cần trước login).""" d = preset_library_dir() items = [] try: names = sorted(os.listdir(d)) except Exception: names = [] for f in names: low = f.lower() if not low.endswith(PRESET_EXTENSIONS): continue meta = {} meta_path = os.path.join(d, os.path.splitext(f)[0] + ".meta") if os.path.isfile(meta_path): try: with open(meta_path, "r", encoding="utf-8") as mf: meta = json.load(mf) except Exception: meta = {} try: size = os.path.getsize(os.path.join(d, f)) except Exception: size = 0 items.append({ "id": f, "name": meta.get("original_name", f), "plugin_hint": meta.get("plugin_hint", ""), "size_bytes": size, "created_at": meta.get("created_at", ""), }) return {"success": True, "presets": items} @router.post("/upload") async def upload_preset( file: UploadFile = File(...), plugin_hint: Optional[str] = None, current_user: dict = Depends(get_current_user), ): """Upload preset (.vstpreset / .fxp / .fxb / .dspreset) vào thư viện.""" enforce_password_changed(current_user) filename = (file.filename or "preset.vstpreset").replace("\\", "/").split("/")[-1] ext = os.path.splitext(filename)[1].lower() if ext not in PRESET_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Định dạng preset không hỗ trợ: {ext or '(không có đuôi)'} — hỗ trợ: {', '.join(PRESET_EXTENSIONS)}", ) contents = await file.read() if not contents: raise HTTPException(status_code=400, detail="File rỗng") d = preset_library_dir() preset_id = uuid.uuid4().hex + ext dest = os.path.join(d, preset_id) try: with open(dest, "wb") as fh: fh.write(contents) except Exception as e: raise HTTPException(status_code=500, detail=f"Không lưu được preset: {e}") meta = { "original_name": filename, "plugin_hint": plugin_hint or "", "size_bytes": len(contents), "created_at": time.strftime("%Y-%m-%d %H:%M:%S"), } try: with open(os.path.join(d, os.path.splitext(preset_id)[0] + ".meta"), "w", encoding="utf-8") as mf: json.dump(meta, mf, ensure_ascii=False, indent=2) except Exception: pass return {"success": True, "preset_id": preset_id, **meta} @router.get("/{preset_id}/download") async def download_preset(preset_id: str): path = _safe_preset_path(preset_id) if not path: raise HTTPException(status_code=404, detail="Preset không tồn tại") return FileResponse(path, filename=preset_id, media_type="application/octet-stream") @router.delete("/{preset_id}") async def delete_preset(preset_id: str, current_user: dict = Depends(get_current_user)): enforce_password_changed(current_user) path = _safe_preset_path(preset_id) if not path: raise HTTPException(status_code=404, detail="Preset không tồn tại") try: os.remove(path) mp = os.path.join(preset_library_dir(), os.path.splitext(preset_id)[0] + ".meta") if os.path.isfile(mp): os.remove(mp) except Exception as e: raise HTTPException(status_code=500, detail=f"Không xóa được preset: {e}") return {"success": True, "preset_id": preset_id}