FIX: Carla sử dụng bridge
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# 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}
|
||||
Reference in New Issue
Block a user