// AKAI Utils — Web UI const API = '/api'; let eventSource = null; // ─── Search ──────────────────────────────────────────────────────── document.getElementById('search-btn').addEventListener('click', doSearch); document.getElementById('search-query').addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); }); document.getElementById('search-dl-all').addEventListener('click', downloadTop10); async function doSearch() { const q = document.getElementById('search-query').value; const filter = document.getElementById('search-filter').value; const status = document.getElementById('search-status'); const results = document.getElementById('search-results'); status.textContent = 'Searching...'; results.innerHTML = ''; try { const params = new URLSearchParams(); if (q) params.set('q', q); if (filter) params.set('filter', filter); params.set('limit', '50'); const resp = await fetch(`${API}/search?${params}`); const data = await resp.json(); status.textContent = `Found ${data.length} items`; document.getElementById('result-count').textContent = `(${data.length})`; renderResults(data); } catch (e) { status.textContent = 'Search failed: ' + e.message; } } function renderResults(items) { const container = document.getElementById('search-results'); container.innerHTML = items.map(r => `
${esc(r.title)}
${r.identifier} ${r.downloads.toLocaleString()} downloads
`).join(''); } async function downloadTop10() { const q = document.getElementById('search-query').value; const filter = document.getElementById('search-filter').value; const params = new URLSearchParams(); if (q) params.set('q', q); if (filter) params.set('filter', filter); params.set('limit', '10'); try { const resp = await fetch(`${API}/search?${params}`); const data = await resp.json(); for (const item of data) { await downloadItem(item.identifier); } } catch (e) { alert('Bulk download failed: ' + e.message); } } // ─── List ────────────────────────────────────────────────────────── async function listItem(identifier) { const el = document.getElementById('list-' + sanitize(identifier)); el.innerHTML = 'Loading...'; try { const resp = await fetch(`${API}/list?identifier=${encodeURIComponent(identifier)}`); const files = await resp.json(); if (!Array.isArray(files) || files.length === 0) { el.innerHTML = 'No ISO files'; return; } el.innerHTML = files.map(f => `
${esc(f.name)} ${f.size || '?'}
` ).join(''); } catch (e) { el.innerHTML = 'Failed: ' + e.message; } } // ─── Download ────────────────────────────────────────────────────── async function downloadItem(identifier) { try { const resp = await fetch(`${API}/download?identifier=${encodeURIComponent(identifier)}&dir=./output/isos`); const data = await resp.json(); if (data.error) { alert(data.error); return; } startProgressStream(); } catch (e) { alert('Download failed: ' + e.message); } } function startProgressStream() { if (eventSource) eventSource.close(); eventSource = new EventSource(`${API}/progress`); eventSource.onmessage = function(e) { const jobs = JSON.parse(e.data); renderDownloadQueue(jobs); const allDone = Object.values(jobs).every(j => j.done || j.error); if (allDone && Object.keys(jobs).length > 0) { eventSource.close(); eventSource = null; } }; eventSource.onerror = function() { if (eventSource) { eventSource.close(); eventSource = null; } }; } function renderDownloadQueue(jobs) { const container = document.getElementById('download-queue'); const count = document.getElementById('dl-count'); const entries = Object.values(jobs); count.textContent = `(${entries.length})`; container.innerHTML = entries.map(j => { const pct = j.total_size > 0 ? Math.round(j.downloaded / j.total_size * 100) : 0; let cls = ''; if (j.error) cls = 'error'; else if (j.done) cls = 'done'; let info = ''; if (j.error) info = j.error; else if (j.done) info = formatBytes(j.total_size) + ' — complete'; else info = formatBytes(j.downloaded) + ' / ' + formatBytes(j.total_size) + (j.speed > 0 ? ' — ' + formatBytes(j.speed) + '/s' : ''); return `
${esc(j.file_name)}
${info}${j.done && !j.error ? '✓' : pct + '%'}
`; }).join(''); } // ─── Extract ─────────────────────────────────────────────────────── document.getElementById('extract-btn').addEventListener('click', doExtract); async function doExtract() { const isoDir = document.getElementById('extract-iso-dir').value; const wavDir = document.getElementById('extract-wav-dir').value; const status = document.getElementById('extract-status'); status.textContent = 'Extracting... (check server console for output)'; status.style.color = 'var(--yellow)'; try { const resp = await fetch(`${API}/extract?dir=${encodeURIComponent(isoDir)}&out=${encodeURIComponent(wavDir)}`); const data = await resp.json(); if (data.error) { status.textContent = data.error; status.style.color = 'var(--accent)'; return; } status.textContent = data.message || 'Extraction complete'; status.style.color = 'var(--green)'; } catch (e) { status.textContent = 'Extraction failed: ' + e.message; status.style.color = 'var(--accent)'; } } // ─── WAV Browser ────────────────────────────────────────────────── let audioCtx = null; let currentSource = null; let currentAudioBuffer = null; let startTime = 0; let pauseOffset = 0; let isPlaying = false; let animationFrame = null; document.getElementById('wav-browse-btn').addEventListener('click', browseWavs); function renderSampleCards(samples) { return samples.map(s => `
${esc(s.name)}
`).join(''); } function renderPrograms(programs) { const progNames = Object.keys(programs).sort(); return progNames.map(prog => { const samples = programs[prog]; return `

${esc(cleanName(prog))}

${samples.length} samples
${renderSampleCards(samples)}
`; }).join(''); } function renderLibraries(libs) { const libNames = Object.keys(libs).sort(); return libNames.map(lib => { const programs = libs[lib]; const progCount = Object.keys(programs).length; const sampleCount = Object.values(programs).reduce((sum, s) => sum + s.length, 0); return `

${esc(cleanName(lib))}

${progCount} programs · ${sampleCount} samples
${renderPrograms(programs)}
`; }).join(''); } async function browseWavs() { const dir = document.getElementById('wav-dir').value; const results = document.getElementById('wav-results'); const summary = document.getElementById('wav-summary'); const list = document.getElementById('wav-list'); list.innerHTML = 'Loading...'; try { const url = `${API}/list-wavs?dir=${encodeURIComponent(dir)}`; const resp = await fetch(url); if (!resp.ok) { list.innerHTML = 'HTTP ' + resp.status + ''; results.style.display = 'block'; return; } const data = await resp.json(); if (!data || data.error) { list.innerHTML = 'Error: ' + (data && data.error ? data.error : 'server error') + ''; results.style.display = 'block'; return; } if (!Array.isArray(data) || data.length === 0) { list.innerHTML = 'No WAV files found'; results.style.display = 'block'; return; } const libs = groupWavs(data); const libCount = Object.keys(libs).length; summary.textContent = libCount + ' librar' + (libCount === 1 ? 'y' : 'ies') + ' · ' + data.length + ' samples'; list.innerHTML = renderLibraries(libs); results.style.display = 'block'; } catch (e) { list.innerHTML = 'Failed: ' + e.message; results.style.display = 'block'; } } async function playWav(filename) { console.log('playWav:', filename); if (!audioCtx) { audioCtx = new AudioContext(); console.log('playWav: created AudioContext'); } if (currentSource) { currentSource.stop(); currentSource = null; } if (animationFrame) { cancelAnimationFrame(animationFrame); animationFrame = null; } const name = filename.split('/').pop().replace(/\.wav$/i, ''); const player = document.getElementById('wav-player'); const nowPlaying = document.getElementById('wav-now-playing'); player.style.display = 'block'; nowPlaying.textContent = 'Loading: ' + name; try { const resp = await fetch(`/wavs/${encodeURIComponent(filename)}`); if (!resp.ok) throw new Error('HTTP ' + resp.status); const arrayBuffer = await resp.arrayBuffer(); currentAudioBuffer = await audioCtx.decodeAudioData(arrayBuffer); currentSource = audioCtx.createBufferSource(); currentSource.buffer = currentAudioBuffer; currentSource.connect(audioCtx.destination); currentSource.onended = () => { if (isPlaying) { isPlaying = false; pauseOffset = 0; nowPlaying.textContent = ''; document.getElementById('wav-progress-fill').style.width = '0%'; document.getElementById('wav-time').textContent = formatTime(0); clearActiveItem(); player.style.display = 'none'; } }; startTime = audioCtx.currentTime - pauseOffset; currentSource.start(0, pauseOffset); isPlaying = true; pauseOffset = 0; nowPlaying.textContent = '▶ ' + name; setActiveItem(filename); updateProgress(); } catch (e) { nowPlaying.textContent = 'Error: ' + e.message; } } function updateProgress() { if (!isPlaying || !currentAudioBuffer) return; const elapsed = audioCtx.currentTime - startTime; const duration = currentAudioBuffer.duration; const pct = Math.min((elapsed / duration) * 100, 100); document.getElementById('wav-progress-fill').style.width = pct + '%'; document.getElementById('wav-time').textContent = formatTime(elapsed); if (elapsed < duration) { animationFrame = requestAnimationFrame(updateProgress); } } function setActiveItem(filename) { document.querySelectorAll('.sample-card').forEach(el => { el.classList.toggle('playing', el.dataset.wav === filename); }); } function clearActiveItem() { document.querySelectorAll('.sample-card').forEach(el => { el.classList.remove('playing'); }); } // Initial load doSearch(); // Event delegation for dynamically generated buttons document.addEventListener('click', function(e) { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; if (action === 'download') { downloadItem(btn.dataset.identifier); } else if (action === 'list') { listItem(btn.dataset.identifier); } else if (action === 'play') { playWav(btn.dataset.wav); } }); // Section collapse toggle document.querySelectorAll('.section-toggle').forEach(h => { h.addEventListener('click', function () { this.classList.toggle('collapsed'); const target = document.getElementById(this.dataset.section); target.style.display = target.style.display === 'none' ? '' : 'none'; }); }); // Collapse all libraries document.getElementById('wav-collapse-all').addEventListener('click', function () { document.querySelectorAll('.library-card').forEach(c => c.classList.add('collapsed')); });