tone3000: IR downloads, model detail, arch filter, metadata
- NAM/IR tab toggle in Downloads overlay - Architecture filter (All/Nano/Feather/Standard) - filters via Supabase architecture_version column - Model detail overlay: tap a search result to see full description, gear info, author, pack, architecture, thumbnail before downloading - IR search + install with .meta.json metadata saving - Backend: search_models/get_trending_models accept arch param - Backend: tone_gear field added to model search response
This commit is contained in:
@@ -293,6 +293,7 @@ class Tone3000Client:
|
|||||||
page: int = 0,
|
page: int = 0,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
|
arch: Optional[str] = None,
|
||||||
) -> list[ModelResult]:
|
) -> list[ModelResult]:
|
||||||
"""Search Tone3000 for NAM models matching the query.
|
"""Search Tone3000 for NAM models matching the query.
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ class Tone3000Client:
|
|||||||
Returns empty list on any error (logged as warning).
|
Returns empty list on any error (logged as warning).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cache_key = f"models:{query}:{page}"
|
cache_key = f"models:{query}:{page}:{arch}"
|
||||||
if not force_refresh:
|
if not force_refresh:
|
||||||
cached = self._get_cached(cache_key)
|
cached = self._get_cached(cache_key)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
@@ -310,13 +311,16 @@ class Tone3000Client:
|
|||||||
# Replace spaces with wildcards so multi-word queries work
|
# Replace spaces with wildcards so multi-word queries work
|
||||||
# (e.g. "vox ac30" → ilike.*vox*ac30* → ILIKE '%vox%ac30%')
|
# (e.g. "vox ac30" → ilike.*vox*ac30* → ILIKE '%vox%ac30%')
|
||||||
ilike_query = query.replace(" ", "*")
|
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",
|
"select": "id,name,model_url,size,pack_name,user_id,tone_id,architecture_version,created_at",
|
||||||
"name": f"ilike.*{ilike_query}*",
|
"name": f"ilike.*{ilike_query}*",
|
||||||
"order": "created_at.desc",
|
"order": "created_at.desc",
|
||||||
"limit": str(page_size),
|
"limit": str(page_size),
|
||||||
"offset": str(page * 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)
|
results = await self._enrich_models(raw_models)
|
||||||
self._set_cache(cache_key, [r.__dict__ for r in results])
|
self._set_cache(cache_key, [r.__dict__ for r in results])
|
||||||
@@ -399,14 +403,17 @@ class Tone3000Client:
|
|||||||
|
|
||||||
# ── Trending / Browse ────────────────────────────────────────
|
# ── 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."""
|
"""Get trending NAM models. Returns empty list on error."""
|
||||||
try:
|
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",
|
"select": "id,name,model_url,size,pack_name,user_id,tone_id,architecture_version,created_at",
|
||||||
"order": "created_at.desc",
|
"order": "created_at.desc",
|
||||||
"limit": str(limit),
|
"limit": str(limit),
|
||||||
})
|
}
|
||||||
|
if arch:
|
||||||
|
params["architecture_version"] = f"eq.{arch}"
|
||||||
|
raw = await self._supabase_get("models", params)
|
||||||
return await self._enrich_models(raw)
|
return await self._enrich_models(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("get_trending_models failed: %s", e)
|
logger.warning("get_trending_models failed: %s", e)
|
||||||
|
|||||||
+22
-3
@@ -1751,7 +1751,7 @@ class WebServer:
|
|||||||
return {"online": ok}
|
return {"online": ok}
|
||||||
|
|
||||||
@app.get("/api/models/tonedownload/search")
|
@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."""
|
"""Search Tone3000 for NAM models."""
|
||||||
try:
|
try:
|
||||||
client = await self._get_tonedownload()
|
client = await self._get_tonedownload()
|
||||||
@@ -1764,10 +1764,11 @@ class WebServer:
|
|||||||
"message": "Tone3000 unavailable - using local defaults",
|
"message": "Tone3000 unavailable - using local defaults",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
arch_val = arch.strip() or None
|
||||||
if not q.strip():
|
if not q.strip():
|
||||||
results = await client.get_trending_models()
|
results = await client.get_trending_models(arch=arch_val)
|
||||||
else:
|
else:
|
||||||
results = await client.search_models(q.strip(), page=page)
|
results = await client.search_models(q.strip(), page=page, arch=arch_val)
|
||||||
return {
|
return {
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
@@ -1781,6 +1782,7 @@ class WebServer:
|
|||||||
"thumbnail": r.thumbnail_url,
|
"thumbnail": r.thumbnail_url,
|
||||||
"download_url": r.download_url,
|
"download_url": r.download_url,
|
||||||
"tone_description": r.tone_description,
|
"tone_description": r.tone_description,
|
||||||
|
"tone_gear": r.tone_gear,
|
||||||
"pack_name": r.pack_name,
|
"pack_name": r.pack_name,
|
||||||
"architecture": r.architecture,
|
"architecture": r.architecture,
|
||||||
"created_at": r.created_at,
|
"created_at": r.created_at,
|
||||||
@@ -1995,6 +1997,23 @@ class WebServer:
|
|||||||
path = await client.download_ir(ir)
|
path = await client.download_ir(ir)
|
||||||
if not path:
|
if not path:
|
||||||
raise HTTPException(status_code=500, detail="Download failed")
|
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
|
ir_loader = self.deps.ir_loader
|
||||||
if ir_loader:
|
if ir_loader:
|
||||||
ir_loader.get_irs()
|
ir_loader.get_irs()
|
||||||
|
|||||||
@@ -226,7 +226,18 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
|
|||||||
<!-- Downloads Overlay -->
|
<!-- Downloads Overlay -->
|
||||||
<div class="overlay" id="overlayDownloads">
|
<div class="overlay" id="overlayDownloads">
|
||||||
<div class="overlay-hdr"><span class="overlay-title">⬇ Downloads</span><button class="overlay-close" id="closeDownloads">✕</button></div>
|
<div class="overlay-hdr"><span class="overlay-title">⬇ Downloads</span><button class="overlay-close" id="closeDownloads">✕</button></div>
|
||||||
<div class="overlay-body">
|
<!-- Tab + filter nav -->
|
||||||
|
<div class="settings-nav">
|
||||||
|
<button class="sn-btn active" id="dlTabNam" onclick="switchDlTab('nam')">🎛 NAM</button>
|
||||||
|
<button class="sn-btn" id="dlTabIr" onclick="switchDlTab('ir')">📦 IR</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-nav" style="border-bottom:1px solid var(--border);padding-top:0">
|
||||||
|
<button class="sn-btn active" data-arch="" onclick="setArch(this,'')">All</button>
|
||||||
|
<button class="sn-btn" data-arch="nano" onclick="setArch(this,'nano')">Nano</button>
|
||||||
|
<button class="sn-btn" data-arch="feather" onclick="setArch(this,'feather')">Feather</button>
|
||||||
|
<button class="sn-btn" data-arch="standard" onclick="setArch(this,'standard')">Standard</button>
|
||||||
|
</div>
|
||||||
|
<div class="overlay-body" style="padding-top:4px">
|
||||||
<div style="display:flex;gap:6px;margin-bottom:6px">
|
<div style="display:flex;gap:6px;margin-bottom:6px">
|
||||||
<input id="dlSearch" placeholder="Search Tone3000..." style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px">
|
<input id="dlSearch" placeholder="Search Tone3000..." style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px">
|
||||||
<button class="sr-action primary" id="dlSearchBtn">🔍</button>
|
<button class="sr-action primary" id="dlSearchBtn">🔍</button>
|
||||||
@@ -235,6 +246,14 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Detail Overlay -->
|
||||||
|
<div class="overlay" id="overlayDetail">
|
||||||
|
<div class="overlay-hdr"><span class="overlay-title" id="dlDetailTitle">Model</span><button class="overlay-close">✕</button></div>
|
||||||
|
<div class="overlay-body">
|
||||||
|
<div id="dlDetailContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings Overlay -->
|
<!-- Settings Overlay -->
|
||||||
<div class="overlay" id="overlaySettings">
|
<div class="overlay" id="overlaySettings">
|
||||||
<div class="overlay-hdr"><span class="overlay-title">⚙ Settings</span><button class="overlay-close" id="closeSettings">✕</button></div>
|
<div class="overlay-hdr"><span class="overlay-title">⚙ Settings</span><button class="overlay-close" id="closeSettings">✕</button></div>
|
||||||
@@ -1006,31 +1025,80 @@ function init(){
|
|||||||
try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);}
|
try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Downloads search
|
// Downloads - state
|
||||||
document.getElementById('dlSearchBtn').onclick=async()=>{
|
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();
|
const q=document.getElementById('dlSearch').value.trim();
|
||||||
if(!q)return;
|
|
||||||
const res=document.getElementById('dlResults');
|
const res=document.getElementById('dlResults');
|
||||||
res.innerHTML='<div style="text-align:center;padding:16px"><span class="spinner"></span></div>';
|
res.innerHTML='<div style="text-align:center;padding:16px"><span class="spinner"></span></div>';
|
||||||
try{
|
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||[];
|
const items=data.results||[];
|
||||||
res.innerHTML='';
|
res.innerHTML='';
|
||||||
if(items.length===0){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No results</div>';return;}
|
if(items.length===0){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No results'+(q?'':' \u2014 tap Search or type a query')+'</div>';return;}
|
||||||
items.forEach(item=>{
|
items.forEach(item=>{
|
||||||
const d=document.createElement('div');
|
const d=document.createElement('div');
|
||||||
d.className='preset-card';
|
d.className='preset-card';
|
||||||
d.style.cursor='default';
|
d.style.cursor='pointer';
|
||||||
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+(item.pack_name?' · '+item.pack_name:'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+(item.size_display||'')+(item.architecture?' · '+item.architecture:'')+'</div><button class="sr-action primary" style="margin-top:4px;align-self:flex-end">Get</button>';
|
const meta=[];
|
||||||
d.querySelector('button').onclick=async()=>{
|
if(item.size_display)meta.push(item.size_display);
|
||||||
d.querySelector('button').textContent='...';
|
if(item.architecture)meta.push(item.architecture);
|
||||||
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';}
|
if(item.tone_gear&&item.tone_gear!=='nam')meta.push(item.tone_gear);
|
||||||
};
|
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+(item.pack_name?' \u00b7 '+item.pack_name:'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' \u00b7 ')+'</div>';
|
||||||
|
d.onclick=()=>showDlDetail(item);
|
||||||
res.appendChild(d);
|
res.appendChild(d);
|
||||||
});
|
});
|
||||||
}catch(e){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim)">Error</div>';}
|
}catch(e){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim)">Error</div>';}
|
||||||
};
|
}
|
||||||
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='<div style="margin-bottom:8px">';
|
||||||
|
if(item.thumbnail)html+='<img src="'+item.thumbnail+'" style="width:100%;border-radius:8px;margin-bottom:6px;max-height:160px;object-fit:cover">';
|
||||||
|
html+='<div style="font-size:14px;font-weight:700;margin-bottom:4px">'+(item.name||'Unknown')+'</div>';
|
||||||
|
html+='<div style="font-size:11px;color:var(--text-sec);margin-bottom:6px">by '+(item.author||'Unknown')+'</div>';
|
||||||
|
if(item.pack_name)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Pack: '+item.pack_name+'</div>';
|
||||||
|
if(item.architecture)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Arch: '+item.architecture+'</div>';
|
||||||
|
if(item.size_display)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Size: '+item.size_display+'</div>';
|
||||||
|
if(item.tone_gear)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Gear: '+item.tone_gear+'</div>';
|
||||||
|
if(item.tone_description)html+='<div style="font-size:10px;color:var(--text2);margin-top:6px;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);line-height:1.4">'+(item.tone_description||'')+'</div>';
|
||||||
|
html+='</div>';
|
||||||
|
html+='<button class="sr-action primary" id="dlDetailBtn" style="width:100%;min-height:42px;font-size:13px">Download'+(isIr?' IR':'')+'</button>';
|
||||||
|
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
|
// Start polling
|
||||||
startPolling();
|
startPolling();
|
||||||
|
|||||||
Reference in New Issue
Block a user