// 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(); 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); async function browseWavs() { const dir = document.getElementById('wav-dir').value; const list = document.getElementById('wav-list'); list.innerHTML = 'Loading...'; console.log('browseWavs: dir =', dir); try { const url = `${API}/list-wavs?dir=${encodeURIComponent(dir)}`; console.log('browseWavs: fetching', url); const resp = await fetch(url); console.log('browseWavs: response status', resp.status, 'ok', resp.ok); if (!resp.ok) { list.innerHTML = 'HTTP ' + resp.status + ''; return; } const data = await resp.json(); console.log('browseWavs: parsed data', data, 'type', typeof data, 'isArray', Array.isArray(data)); if (!data || data.error) { list.innerHTML = 'Error: ' + (data && data.error ? data.error : 'server error') + ''; return; } if (!Array.isArray(data) || data.length === 0) { list.innerHTML = 'No WAV files found'; return; } console.log('browseWavs: found', data.length, 'wavs'); list.innerHTML = data.map(f => `
${esc(f)}
`).join(''); } catch (e) { console.error('browseWavs: error', e); list.innerHTML = 'Failed: ' + e.message; } } 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 nowPlaying = document.getElementById('wav-now-playing'); nowPlaying.textContent = 'Loading: ' + filename; 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(); } }; startTime = audioCtx.currentTime - pauseOffset; currentSource.start(0, pauseOffset); isPlaying = true; pauseOffset = 0; nowPlaying.textContent = '▶ ' + filename; 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 formatTime(secs) { const m = Math.floor(secs / 60); const s = Math.floor(secs % 60); return m + ':' + s.toString().padStart(2, '0'); } function setActiveItem(filename) { document.querySelectorAll('.wav-item').forEach(el => { el.classList.toggle('playing', el.dataset.file === filename); }); } function clearActiveItem() { document.querySelectorAll('.wav-item').forEach(el => { el.classList.remove('playing'); }); } // ─── Helpers ────────────────────────────────────────────────────── function esc(s) { if (!s) return ''; return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function sanitize(s) { return s.replace(/[^a-zA-Z0-9_-]/g, '_'); } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // 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); } });