130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
# 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}
|