feat(tonedownload): Tone3000 NAM + IR browser, real anon key, tests

- Tonedownload.py: Tone3000 API client with search/download/cache/enrich
- Server.py: 4 endpoints (search models, search IRs, install model, install IR)
- Models.html + irs.html: Browse Tone3000 tab with search, trending, offline
- Models.js + irs.js: Tone3000 search UI with install tracking via localStorage
- Style.css: Card layout for search results with thumbnail, meta, install btn
- Tests: 47 tests covering data classes, cache, API, search, download, enrichment
- Anon key: Updated from placeholder to real Supabase anon key (verified: API works)
This commit is contained in:
2026-06-09 01:39:12 -04:00
parent b4237f2f1d
commit a31342b478
8 changed files with 1789 additions and 2 deletions
+111 -1
View File
@@ -92,4 +92,114 @@ async function uploadIR() {
}
}
document.addEventListener('DOMContentLoaded', loadIRs);
document.addEventListener('DOMContentLoaded', loadIRs);
// ── Tone3000 Browser ────────────────────────────────────────────────
const INSTALLED_IRS_KEY = 'tonedownload_installed_irs';
function getInstalledIRs() {
try { return JSON.parse(localStorage.getItem(INSTALLED_IRS_KEY) || '[]'); } catch { return []; }
}
function markIRInstalled(url) {
const installed = getInstalledIRs();
if (!installed.includes(url)) {
installed.push(url);
localStorage.setItem(INSTALLED_IRS_KEY, JSON.stringify(installed));
}
}
function isIRInstalled(url) {
return getInstalledIRs().includes(url);
}
async function searchToneIRs() {
const input = document.getElementById('ir-tone-search');
const query = input.value.trim();
const resultsEl = document.getElementById('ir-tone-results');
const loaderEl = document.getElementById('ir-tone-loader');
const offlineEl = document.getElementById('ir-tone-offline');
resultsEl.innerHTML = '';
offlineEl.style.display = 'none';
if (!query) {
loadTrendingIRs();
return;
}
loaderEl.style.display = 'block';
try {
const data = await apiGet('/irs/tonedownload/search?q=' + encodeURIComponent(query));
loaderEl.style.display = 'none';
renderToneIRResults(resultsEl, data.results || []);
} catch (e) {
loaderEl.style.display = 'none';
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
offlineEl.style.display = 'block';
} else {
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
}
}
}
async function loadTrendingIRs() {
const resultsEl = document.getElementById('ir-tone-results');
const loaderEl = document.getElementById('ir-tone-loader');
const offlineEl = document.getElementById('ir-tone-offline');
resultsEl.innerHTML = '';
offlineEl.style.display = 'none';
loaderEl.style.display = 'block';
try {
const data = await apiGet('/irs/tonedownload/search?q=');
loaderEl.style.display = 'none';
renderToneIRResults(resultsEl, data.results || []);
} catch (e) {
loaderEl.style.display = 'none';
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
offlineEl.style.display = 'block';
} else {
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
}
}
}
function renderToneIRResults(container, results) {
if (!results || results.length === 0) {
container.innerHTML = '<p class="text-muted">No results found.</p>';
return;
}
let html = '';
for (const r of results) {
const installed = isIRInstalled(r.download_url);
const thumb = r.thumbnail
? `<img src="${escapeHtml(r.thumbnail)}" alt="" class="tonedownload-thumb" loading="lazy" onerror="this.style.display='none'">`
: '<div class="tonedownload-thumb tonedownload-thumb-placeholder">📊</div>';
html += `<div class="tonedownload-card ${installed ? 'installed' : ''}">
${thumb}
<div class="tonedownload-card-body">
<div class="tonedownload-card-name">${escapeHtml(r.name)}</div>
<div class="tonedownload-card-meta">
${escapeHtml(r.author)} · ${escapeHtml(r.size_display)}
</div>
${r.tone_description ? `<div class="tonedownload-card-desc">${escapeHtml(r.tone_description.substring(0, 120))}</div>` : ''}
</div>
<div class="tonedownload-card-actions">
${installed
? '<span class="tonedownload-installed-badge">✓ Installed</span>'
: `<button class="btn btn-primary tonedownload-install-btn"
onclick="installToneIR('${escapeHtml(r.download_url)}', '${escapeHtml(r.name)}', ${r.size || 'null'}, '${escapeHtml(r.filename)}')">
Install
</button>`
}
</div>
</div>`;
}
container.innerHTML = html;
}
+163 -1
View File
@@ -92,4 +92,166 @@ async function uploadModel() {
}
}
document.addEventListener('DOMContentLoaded', loadModels);
document.addEventListener('DOMContentLoaded', loadModels);
// ── Tone3000 Browser ────────────────────────────────────────────────
const INSTALLED_MODELS_KEY = 'tonedownload_installed_models';
function getInstalledModels() {
try {
return JSON.parse(localStorage.getItem(INSTALLED_MODELS_KEY) || '[]');
} catch { return []; }
}
function markInstalled(url) {
const installed = getInstalledModels();
if (!installed.includes(url)) {
installed.push(url);
localStorage.setItem(INSTALLED_MODELS_KEY, JSON.stringify(installed));
}
}
function isInstalled(url) {
return getInstalledModels().includes(url);
}
async function searchToneModels() {
const input = document.getElementById('model-tone-search');
const query = input.value.trim();
const resultsEl = document.getElementById('model-tone-results');
const loaderEl = document.getElementById('model-tone-loader');
const offlineEl = document.getElementById('model-tone-offline');
resultsEl.innerHTML = '';
offlineEl.style.display = 'none';
if (!query) {
loadTrendingModels();
return;
}
loaderEl.style.display = 'block';
try {
const data = await apiGet('/models/tonedownload/search?q=' + encodeURIComponent(query));
loaderEl.style.display = 'none';
renderToneResults(resultsEl, data.results || [], 'model');
} catch (e) {
loaderEl.style.display = 'none';
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
offlineEl.style.display = 'block';
} else {
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
}
}
}
async function loadTrendingModels() {
const resultsEl = document.getElementById('model-tone-results');
const loaderEl = document.getElementById('model-tone-loader');
const offlineEl = document.getElementById('model-tone-offline');
resultsEl.innerHTML = '';
offlineEl.style.display = 'none';
loaderEl.style.display = 'block';
try {
const data = await apiGet('/models/tonedownload/search?q=');
loaderEl.style.display = 'none';
renderToneResults(resultsEl, data.results || [], 'model');
} catch (e) {
loaderEl.style.display = 'none';
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
offlineEl.style.display = 'block';
} else {
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
}
}
}
function renderToneResults(container, results, type) {
if (!results || results.length === 0) {
container.innerHTML = '<p class="text-muted">No results found.</p>';
return;
}
let html = '';
for (const r of results) {
const installed = isInstalled(r.download_url);
const thumb = r.thumbnail
? `<img src="${escapeHtml(r.thumbnail)}" alt="" class="tonedownload-thumb" loading="lazy" onerror="this.style.display='none'">`
: '<div class="tonedownload-thumb tonedownload-thumb-placeholder">🎸</div>';
html += `<div class="tonedownload-card ${installed ? 'installed' : ''}">
${thumb}
<div class="tonedownload-card-body">
<div class="tonedownload-card-name">${escapeHtml(r.name)}</div>
<div class="tonedownload-card-meta">
${escapeHtml(r.author)} · ${escapeHtml(r.size_display)}
${r.architecture ? ` · ${escapeHtml(r.architecture)}` : ''}
</div>
${r.tone_description ? `<div class="tonedownload-card-desc">${escapeHtml(r.tone_description.substring(0, 120))}</div>` : ''}
</div>
<div class="tonedownload-card-actions">
${installed
? '<span class="tonedownload-installed-badge">✓ Installed</span>'
: `<button class="btn btn-primary tonedownload-install-btn"
onclick="installToneModel('${escapeHtml(r.download_url)}', '${escapeHtml(r.name)}', ${r.size || 'null'}, '${escapeHtml(r.filename)}')">
Install
</button>`
}
</div>
</div>`;
}
container.innerHTML = html;
}
async function installToneModel(downloadUrl, displayName, size, filename) {
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Downloading...';
try {
const data = await apiPost('/models/tonedownload/install', {
download_url: downloadUrl,
name: filename || displayName + '.nam',
display_name: displayName,
size: size,
});
markInstalled(downloadUrl);
btn.textContent = '✓ Done';
btn.style.background = 'var(--success)';
btn.style.borderColor = 'var(--success)';
// Refresh model list
loadModels();
} catch (e) {
btn.disabled = false;
btn.textContent = 'Install';
alert('Install failed: ' + e.message);
}
}
async function installToneIR(downloadUrl, displayName, size, filename) {
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Downloading...';
try {
const data = await apiPost('/irs/tonedownload/install', {
download_url: downloadUrl,
name: filename || displayName + '.wav',
display_name: displayName,
size: size,
});
markInstalled(downloadUrl);
btn.textContent = '✓ Done';
btn.style.background = 'var(--success)';
btn.style.borderColor = 'var(--success)';
loadIRs();
} catch (e) {
btn.disabled = false;
btn.textContent = 'Install';
alert('Install failed: ' + e.message);
}
}
+127
View File
@@ -994,4 +994,131 @@ body {
.bt-midi-device code {
font-size: 0.72rem;
}
/* ── Tone3000 Downloader ──────────────────────────────────────────── */
.badge-tone3000 {
background: linear-gradient(135deg, #0000FF, #FF0000);
color: white;
font-size: 0.65rem;
vertical-align: middle;
margin-left: 6px;
}
.tonedownload-search-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
align-items: center;
}
.tonedownload-input {
flex: 1;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 0.85rem;
}
.tonedownload-input::placeholder {
color: var(--text-muted);
}
.tonedownload-offline {
padding: 10px;
background: rgba(255, 167, 38, 0.1);
border: 1px solid var(--warning);
border-radius: var(--radius-sm);
color: var(--warning);
font-size: 0.82rem;
margin-bottom: 10px;
}
.tonedownload-results {
display: flex;
flex-direction: column;
gap: 8px;
}
.tonedownload-card {
display: flex;
gap: 10px;
padding: 10px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
align-items: center;
}
.tonedownload-card.installed {
border-color: var(--success);
opacity: 0.8;
}
.tonedownload-thumb {
width: 48px;
height: 48px;
border-radius: var(--radius-sm);
object-fit: cover;
flex-shrink: 0;
background: var(--bg-card);
}
.tonedownload-thumb-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
}
.tonedownload-card-body {
flex: 1;
min-width: 0;
}
.tonedownload-card-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tonedownload-card-meta {
font-size: 0.72rem;
color: var(--text-muted);
margin-top: 2px;
}
.tonedownload-card-desc {
font-size: 0.72rem;
color: var(--text-secondary);
margin-top: 4px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tonedownload-card-actions {
flex-shrink: 0;
}
.tonedownload-install-btn {
padding: 4px 12px;
font-size: 0.78rem;
}
.tonedownload-installed-badge {
display: inline-block;
color: var(--success);
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
}