From 2ad29226c4775c6bf58402aef48d6add5814c9b2 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 27 Jul 2026 09:36:58 +0700 Subject: [PATCH] fix: deduplicate soundfont scan + source label + delete only uploads - _scan_soundfonts: dict-keyed by base_id to prevent duplicates - Add 'source' field ('system' | 'upload') to each soundfont entry - Delete endpoint: only allow deleting upload soundfonts (403 for system) - Frontend: show (system)/(upload) tag, hide Delete for system fonts --- app/api/v1/plugins.py | 6 ++++-- app/core/vst_engine.py | 22 ++++++++++++++-------- app/static/js/app.jsx | 17 +++++++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 5aa9f2c..ea49b39 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -103,14 +103,16 @@ async def upload_soundfont( async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_user)): base_id = sf_id.replace("sf_", "") deleted = False - for d in [UPLOAD_SF_DIR, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts")]: + for d in [UPLOAD_SF_DIR, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts"), SYSTEM_SF_DIR]: if not os.path.isdir(d): continue for f in os.listdir(d): if os.path.splitext(f)[0] == base_id: + # Skip system dir — only allow deleting uploads + if d == SYSTEM_SF_DIR: + raise HTTPException(status_code=403, detail="System soundfonts cannot be deleted via this endpoint") path = os.path.join(d, f) os.remove(path) - # Remove associated .meta file meta_path = os.path.join(d, os.path.splitext(f)[0] + ".meta") if os.path.isfile(meta_path): os.remove(meta_path) diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 1831162..4a1a00e 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -163,12 +163,11 @@ class PluginManager: return plugins def _scan_soundfonts(self) -> list: - sfonts = [] - dirs = [self.sf_dir] + sf_map = {} + dirs = [("system", self.sf_dir)] if self.upload_sf_dir and self.upload_sf_dir != self.sf_dir: - dirs.append(self.upload_sf_dir) + dirs.append(("upload", self.upload_sf_dir)) - # Load metadata cache for upload soundfonts meta_cache = {} if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir): for f in os.listdir(self.upload_sf_dir): @@ -180,21 +179,28 @@ class PluginManager: except Exception: pass - for d in dirs: + for source, d in dirs: if not os.path.isdir(d): continue for f in os.listdir(d): if f.endswith(".sf2") or f.endswith(".sf3"): base_id = os.path.splitext(f)[0] + if base_id in sf_map: + continue meta = meta_cache.get(base_id, None) if meta: display_name = meta.get("original_name", f) else: - # Generate a friendly name from UUID: truncate to first 8 chars short_id = base_id[:8] if len(base_id) > 8 else base_id display_name = f"SoundFont_{short_id}" - sfonts.append({"id": base_id, "name": display_name, "file": f, "display": os.path.splitext(display_name)[0][:40]}) - return sfonts + sf_map[base_id] = { + "id": base_id, + "name": display_name, + "file": f, + "display": os.path.splitext(display_name)[0][:40], + "source": source + } + return list(sf_map.values()) def load_vst(self, plugin_name: str, preset_data: dict = None): if not HAS_PEDALBOARD: diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index e59addb..f812bfd 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -3459,12 +3459,17 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { React.createElement('div', { className: 'text-xs font-semibold text-slate-200' }, sf.display || sf.name || sf.id), React.createElement('div', { className: 'text-[10px] text-zinc-500' }, sf.file || sf.name) ) - ), - React.createElement('div', { className: 'flex items-center gap-2' }, - React.createElement('button', { - onClick: () => setSfToDelete(sf), - className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1' - }, 'Delete') + ), React.createElement('div', { + className: 'text-[10px] text-zinc-500' + }, sf.file || sf.name, ' ', React.createElement('span', { + className: sf.source === 'system' ? 'text-blue-400' : 'text-amber-500' + }, sf.source === 'system' ? '(system)' : '(upload)')) + ), + React.createElement('div', { className: 'flex items-center gap-2' }, + sf.source === 'upload' ? React.createElement('button', { + onClick: () => setSfToDelete(sf), + className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1' + }, 'Delete') : React.createElement('span', { className: 'text-[10px] text-zinc-700' }, 'System') ) ) )