diff --git a/src/system/tonedownload.py b/src/system/tonedownload.py index 51c866c..14b3b9a 100644 --- a/src/system/tonedownload.py +++ b/src/system/tonedownload.py @@ -293,6 +293,7 @@ class Tone3000Client: page: int = 0, page_size: int = 20, force_refresh: bool = False, + arch: Optional[str] = None, ) -> list[ModelResult]: """Search Tone3000 for NAM models matching the query. @@ -300,7 +301,7 @@ class Tone3000Client: Returns empty list on any error (logged as warning). """ try: - cache_key = f"models:{query}:{page}" + cache_key = f"models:{query}:{page}:{arch}" if not force_refresh: cached = self._get_cached(cache_key) if cached is not None: @@ -310,13 +311,16 @@ class Tone3000Client: # Replace spaces with wildcards so multi-word queries work # (e.g. "vox ac30" → ilike.*vox*ac30* → ILIKE '%vox%ac30%') ilike_query = query.replace(" ", "*") - raw_models = await self._supabase_get("models", { + params: dict[str, str] = { "select": "id,name,model_url,size,pack_name,user_id,tone_id,architecture_version,created_at", "name": f"ilike.*{ilike_query}*", "order": "created_at.desc", "limit": str(page_size), "offset": str(page * page_size), - }) + } + if arch: + params["architecture_version"] = f"eq.{arch}" + raw_models = await self._supabase_get("models", params) results = await self._enrich_models(raw_models) self._set_cache(cache_key, [r.__dict__ for r in results]) @@ -399,14 +403,17 @@ class Tone3000Client: # ── Trending / Browse ──────────────────────────────────────── - async def get_trending_models(self, limit: int = 20) -> list[ModelResult]: + async def get_trending_models(self, limit: int = 20, arch: Optional[str] = None) -> list[ModelResult]: """Get trending NAM models. Returns empty list on error.""" try: - raw = await self._supabase_get("models", { + params: dict[str, str] = { "select": "id,name,model_url,size,pack_name,user_id,tone_id,architecture_version,created_at", "order": "created_at.desc", "limit": str(limit), - }) + } + if arch: + params["architecture_version"] = f"eq.{arch}" + raw = await self._supabase_get("models", params) return await self._enrich_models(raw) except Exception as e: logger.warning("get_trending_models failed: %s", e) diff --git a/src/web/server.py b/src/web/server.py index 94bf547..720fc6a 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -1751,7 +1751,7 @@ class WebServer: return {"online": ok} @app.get("/api/models/tonedownload/search") - async def tonedownload_search_models(q: str = "", page: int = 0): + async def tonedownload_search_models(q: str = "", page: int = 0, arch: str = ""): """Search Tone3000 for NAM models.""" try: client = await self._get_tonedownload() @@ -1764,10 +1764,11 @@ class WebServer: "message": "Tone3000 unavailable - using local defaults", }, ) + arch_val = arch.strip() or None if not q.strip(): - results = await client.get_trending_models() + results = await client.get_trending_models(arch=arch_val) else: - results = await client.search_models(q.strip(), page=page) + results = await client.search_models(q.strip(), page=page, arch=arch_val) return { "results": [ { @@ -1781,6 +1782,7 @@ class WebServer: "thumbnail": r.thumbnail_url, "download_url": r.download_url, "tone_description": r.tone_description, + "tone_gear": r.tone_gear, "pack_name": r.pack_name, "architecture": r.architecture, "created_at": r.created_at, @@ -1995,6 +1997,23 @@ class WebServer: path = await client.download_ir(ir) if not path: raise HTTPException(status_code=500, detail="Download failed") + # Save IR metadata + import json + meta = { + "id": data.get("id"), + "name": data.get("name"), + "filename": data.get("filename"), + "size_bytes": data.get("size"), + "author": data.get("author"), + "author_avatar": data.get("author_avatar"), + "thumbnail": data.get("thumbnail"), + "tone_description": data.get("tone_description"), + "created_at": data.get("created_at"), + "download_date": datetime.datetime.now().isoformat(), + "source": "tone3000", + } + meta_path = path.with_suffix(".meta.json") + meta_path.write_text(json.dumps(meta, indent=2)) ir_loader = self.deps.ir_loader if ir_loader: ir_loader.get_irs() diff --git a/src/web/ui-v2-dist/index.html b/src/web/ui-v2-dist/index.html index c0d90fc..e6486b2 100644 --- a/src/web/ui-v2-dist/index.html +++ b/src/web/ui-v2-dist/index.html @@ -226,7 +226,18 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
⬇ Downloads
-
+ +
+ + +
+
+ + + + +
+
@@ -235,6 +246,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
+ +
+
Model
+
+
+
+
+
⚙ Settings
@@ -1006,31 +1025,80 @@ function init(){ try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);} }; - // Downloads search - document.getElementById('dlSearchBtn').onclick=async()=>{ + // Downloads - state + let dlTab='nam', dlArch=''; + + function switchDlTab(tab){ + dlTab=tab; + document.getElementById('dlTabNam').className='sn-btn'+(tab==='nam'?' active':''); + document.getElementById('dlTabIr').className='sn-btn'+(tab==='ir'?' active':''); + const archRow=document.querySelectorAll('#overlayDownloads > .settings-nav')[1]; + if(archRow)archRow.style.display=tab==='nam'?'flex':'none'; + doDlSearch(); + } + function setArch(btn,arch){ + document.querySelectorAll('#overlayDownloads .settings-nav')[1].querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active')); + btn.classList.add('active'); + dlArch=arch; + doDlSearch(); + } + async function doDlSearch(){ const q=document.getElementById('dlSearch').value.trim(); - if(!q)return; const res=document.getElementById('dlResults'); res.innerHTML='
'; try{ - const data=await API._fetch('/api/models/tonedownload/search?q='+encodeURIComponent(q)+'&page=0'); + const endpoint=dlTab==='nam'?'/api/models/tonedownload/search':'/api/irs/tonedownload/search'; + const params='?q='+encodeURIComponent(q)+(dlArch?'&arch='+dlArch:'')+'&page=0'; + const data=await API._fetch(endpoint+params); const items=data.results||[]; res.innerHTML=''; - if(items.length===0){res.innerHTML='
No results
';return;} + if(items.length===0){res.innerHTML='
No results'+(q?'':' \u2014 tap Search or type a query')+'
';return;} items.forEach(item=>{ const d=document.createElement('div'); d.className='preset-card'; - d.style.cursor='default'; - d.innerHTML='
'+(item.name||'Unknown')+'
'+(item.author||'')+(item.pack_name?' · '+item.pack_name:'')+'
'+(item.size_display||'')+(item.architecture?' · '+item.architecture:'')+'
'; - d.querySelector('button').onclick=async()=>{ - d.querySelector('button').textContent='...'; - try{await API._fetch('/api/models/tonedownload/install',{method:'POST',body:JSON.stringify({id:item.id,download_url:item.download_url,name:item.name,filename:item.filename,size:item.size,author:item.author,author_avatar:item.author_avatar,thumbnail:item.thumbnail,tone_description:item.tone_description,pack_name:item.pack_name,architecture:item.architecture,created_at:item.created_at}),timeout:60000});toast('Downloaded!');d.querySelector('button').textContent='✓';}catch(e){toast(e.message);d.querySelector('button').textContent='Retry';} - }; + d.style.cursor='pointer'; + const meta=[]; + if(item.size_display)meta.push(item.size_display); + if(item.architecture)meta.push(item.architecture); + if(item.tone_gear&&item.tone_gear!=='nam')meta.push(item.tone_gear); + d.innerHTML='
'+(item.name||'Unknown')+'
'+(item.author||'')+(item.pack_name?' \u00b7 '+item.pack_name:'')+'
'+meta.join(' \u00b7 ')+'
'; + d.onclick=()=>showDlDetail(item); res.appendChild(d); }); }catch(e){res.innerHTML='
Error
';} - }; - document.getElementById('dlSearch').onkeydown=(e)=>{if(e.key==='Enter')document.getElementById('dlSearchBtn').click();}; + } + function showDlDetail(item){ + const c=document.getElementById('dlDetailContent'); + const isIr=dlTab==='ir'; + let html='
'; + if(item.thumbnail)html+=''; + html+='
'+(item.name||'Unknown')+'
'; + html+='
by '+(item.author||'Unknown')+'
'; + if(item.pack_name)html+='
Pack: '+item.pack_name+'
'; + if(item.architecture)html+='
Arch: '+item.architecture+'
'; + if(item.size_display)html+='
Size: '+item.size_display+'
'; + if(item.tone_gear)html+='
Gear: '+item.tone_gear+'
'; + if(item.tone_description)html+='
'+(item.tone_description||'')+'
'; + html+='
'; + html+=''; + c.innerHTML=html; + document.getElementById('dlDetailTitle').textContent=(item.name||'Model'); + document.getElementById('overlayDetail').classList.add('open'); + document.getElementById('dlDetailBtn').onclick=async()=>{ + const btn=document.getElementById('dlDetailBtn'); + btn.textContent='...'; + try{ + const endpoint=isIr?'/api/irs/tonedownload/install':'/api/models/tonedownload/install'; + await API._fetch(endpoint,{method:'POST',body:JSON.stringify({id:item.id,download_url:item.download_url,name:item.name,filename:item.filename,size:item.size,author:item.author,author_avatar:item.author_avatar,thumbnail:item.thumbnail,tone_description:item.tone_description,pack_name:item.pack_name,architecture:item.architecture,created_at:item.created_at}),timeout:60000}); + toast('Downloaded!'); + btn.textContent='Downloaded'; + btn.className='sr-action'; + }catch(e){toast(e.message);btn.textContent='Retry';} + }; + } + // Wire downloads search + document.getElementById('dlSearchBtn').onclick=doDlSearch; + document.getElementById('dlSearch').onkeydown=(e)=>{if(e.key==='Enter')doDlSearch();}; // Start polling startPolling();