diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 5f651a7..6f7f2c2 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -69,7 +69,19 @@ def _effective_dirs() -> dict: plugin_dirs = [d for d in (user.get("plugin_dirs") or []) if d] vst_dir = settings.VST_DIR soundfont_dir = settings.SOUNDFONT_DIR - # Backward compat: file cũ lưu vst_dir/soundfont_dir riêng → gộp vào list. + # User ĐÃ lưu plugin_dirs (kể cả [] — xóa hết dir) → dùng ĐÚNG danh sách + # đó, KHÔNG fallback default: nếu fallback, remove hết dir rồi re-scan + # vẫn còn VST trong thư mục mặc định (vd C:\Program Files\Common Files\ + # VST3 → iZotope Ozone Pro load fail 0x7E vẫn hiện trong MASTERING PANEL). + if "plugin_dirs" in user: + return { + "plugin_dirs": plugin_dirs, + "vst_dir": vst_dir, + "soundfont_dir": soundfont_dir, + "plugin_dirs_user_set": True, + } + # Chưa từng lưu → backward compat (file cũ lưu vst_dir/soundfont_dir + # riêng) → gộp vào list; không có gì → fallback default env. if not plugin_dirs: if user.get("vst_dir"): plugin_dirs.append(user["vst_dir"]) @@ -81,7 +93,7 @@ def _effective_dirs() -> dict: "plugin_dirs": plugin_dirs, "vst_dir": vst_dir, "soundfont_dir": soundfont_dir, - "plugin_dirs_user_set": bool(user.get("plugin_dirs")), + "plugin_dirs_user_set": False, } class DirsRequest(BaseModel): @@ -184,7 +196,7 @@ def get_scanner(): @router.get("/available") async def list_plugins(current_user: dict = Depends(get_current_user)): d = _effective_dirs() - pm = PluginManager(vst_dir=d["vst_dir"], sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR, extra_vst_dirs=d["plugin_dirs"]) + pm = PluginManager(vst_dir=d["vst_dir"], sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR, extra_vst_dirs=d["plugin_dirs"], user_dirs_override=d.get("plugin_dirs_user_set", False)) avail = pm.list_available() # Modal chọn instrument (nút Synth TCP) chỉ hiện soundfont + VSTi — loại # VST FX (Ozone/Sonible/...) khỏi vst_instruments theo is_fx của bridge scan. diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 70a854e..b6f5649 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -210,6 +210,26 @@ def _load_user_plugin_dirs() -> list: return [] +def _user_plugin_dirs_configured() -> bool: + """User ĐÃ lưu plugin_dirs trong Plugin Manager (kể cả danh sách rỗng)? + + Phân biệt "chưa cấu hình" (fallback thư mục mặc định VST_DIR) với "đã + xóa hết dir" (chỉ scan đúng dir user khai - rỗng thì không scan gì). + Nếu không phân biệt, remove hết dir rồi re-scan vẫn còn VST trong thư + mục mặc định (vd C:\\Program Files\\Common Files\\VST3, iZotope Ozone Pro + load fail 0x7E vẫn hiện trong MASTERING PANEL).""" + try: + from app.config import settings as _st + path = os.path.join(_st.STORAGE_DIR, "plugin_dirs.json") + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return "plugin_dirs" in data + except Exception: + pass + return False + + def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager": """Singleton: reuse PluginManager when args match, else create new. Default dirs từ settings (env/.env/docker-compose hoặc user override). @@ -220,12 +240,13 @@ def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "Plugin sf_dir = sf_dir or _st.SOUNDFONT_DIR upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts" extra = _load_user_plugin_dirs() + user_override = _user_plugin_dirs_configured() global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS - args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra)) + args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra), user_override) if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args: return _PLUGIN_MANAGER_INSTANCE _PLUGIN_MANAGER_ARGS = args - _PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra) + _PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra, user_dirs_override=user_override) return _PLUGIN_MANAGER_INSTANCE def load_soundfont_cached(path: str): @@ -289,7 +310,7 @@ def _classify_fx_category(name, path=""): return "utility" class PluginManager: - def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None): + def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None, user_dirs_override=False): from app.config import settings as _st self.vst_dir = vst_dir or _st.VST_DIR self.sf_dir = sf_dir or _st.SOUNDFONT_DIR @@ -298,11 +319,23 @@ class PluginManager: # list_available() phải thấy VSTi user đã scan (bug: nút # Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3). self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d] + # user_dirs_override=True: user ĐÃ lưu plugin_dirs (kể cả rỗng) → + # chỉ scan đúng dir user khai, KHÔNG thêm vst_dir mặc định. Nếu không, + # remove hết dir rồi re-scan vẫn còn VST trong thư mục mặc định + # (vd C:\Program Files\Common Files\VST3 → iZotope 0x7E vẫn hiện). + self.user_dirs_override = bool(user_dirs_override) self._sf_scan_cache = None # cache for _scan_soundfonts() self._fx_scan_cache = None # cache for list_fx() self._fx_scan_all = None # cache for list_scan_all() self._fx_scan_all_ts = 0.0 # timestamp cache (TTL — tránh kẹt [] vĩnh viễn) + def _vst_scan_dirs(self) -> list: + """Thư mục quét VST: user đã lưu plugin_dirs → chỉ đúng dir user khai + (rỗng = không scan gì); chưa cấu hình → vst_dir mặc định + dir user.""" + if self.user_dirs_override: + return list(self.extra_vst_dirs) + return [self.vst_dir] + list(self.extra_vst_dirs) + FX_SCAN_TTL = 300.0 # giây: cache scan VST tự hết hạn → MASTERING PANEL # không bao giờ kẹt danh sách cũ/rỗng; refresh=true vẫn ép scan ngay. @@ -315,7 +348,7 @@ class PluginManager: def _scan_plugins(self) -> dict: plugins = {} - for scan_dir in [self.vst_dir] + self.extra_vst_dirs: + for scan_dir in self._vst_scan_dirs(): if not scan_dir or not os.path.isdir(scan_dir): continue for root, dirs, files in os.walk(scan_dir): @@ -545,8 +578,7 @@ class PluginManager: exe = find_fx_bridge_exe() entries = [] if exe: - dirs = [d for d in [self.vst_dir] + self.extra_vst_dirs - if d and os.path.isdir(d)] + dirs = [d for d in self._vst_scan_dirs() if d and os.path.isdir(d)] for d in dirs: try: r = subprocess.run([exe, "--scan", d], capture_output=True,