// 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 `
${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 `
${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);
} else if (action === 'tag-part') {
selectPartition(btn.dataset.iso, btn.dataset.part);
} else if (action === 'tag-vol') {
viewVolumeTags(btn.dataset.iso, btn.dataset.part, btn.dataset.vol);
}
});
// 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'));
});
// ─── Tag Viewer ─────────────────────────────────────────────────────
const TAG_COLORS = [
'#e94560', '#4ecca3', '#f0c040', '#7c3aed', '#06b6d4',
'#f97316', '#84cc16', '#ec4899', '#3b82f6', '#14b8a6',
'#a855f7', '#ef4444', '#22c55e', '#eab308', '#6366f1',
'#10b981', '#f43f5e', '#8b5cf6', '#0ea5e9', '#d946ef'
]
document.getElementById('tag-browse-btn').addEventListener('click', browseISO)
async function browseISO() {
const iso = document.getElementById('tag-iso-path').value
const results = document.getElementById('tag-results')
const status = document.getElementById('tag-status')
const partsEl = document.getElementById('tag-partitions')
const volsEl = document.getElementById('tag-volumes')
const detailEl = document.getElementById('tag-detail')
partsEl.innerHTML = ''
volsEl.innerHTML = ''
detailEl.innerHTML = ''
status.textContent = 'Loading partitions...'
results.style.display = 'block'
try {
const resp = await fetch(`${API}/disk/partitions?iso=${encodeURIComponent(iso)}`)
if (!resp.ok) {
status.textContent = 'HTTP ' + resp.status
return
}
const data = await resp.json()
if (!data || data.error) {
status.textContent = (data && data.error) || 'server error'
return
}
status.textContent = data.length + ' partition' + (data.length === 1 ? '' : 's')
partsEl.innerHTML = data.map(p => `
`).join('')
} catch (e) {
status.textContent = 'Failed: ' + e.message
}
}
async function selectPartition(iso, part) {
const volsEl = document.getElementById('tag-volumes')
const detailEl = document.getElementById('tag-detail')
document.querySelectorAll('.tag-part-btn').forEach(b => {
b.classList.toggle('active', b.dataset.part === part)
})
volsEl.innerHTML = 'Loading volumes...'
detailEl.innerHTML = ''
try {
const resp = await fetch(`${API}/disk/volumes?iso=${encodeURIComponent(iso)}&part=${encodeURIComponent(part)}`)
if (!resp.ok) {
volsEl.innerHTML = 'HTTP ' + resp.status + ''
return
}
const data = await resp.json()
if (!data || data.error) {
volsEl.innerHTML = '' + (data && data.error ? data.error : 'server error') + ''
return
}
if (!Array.isArray(data) || data.length === 0) {
volsEl.innerHTML = 'No volumes on Partition ' + part + ''
return
}
volsEl.innerHTML = data.map(v => `
${esc(v.name)}
`).join('')
} catch (e) {
volsEl.innerHTML = 'Failed: ' + e.message
}
}
async function viewVolumeTags(iso, part, vol) {
const detailEl = document.getElementById('tag-detail')
document.querySelectorAll('.vol-item').forEach(el => {
el.classList.toggle('selected', el.textContent === vol)
})
detailEl.innerHTML = 'Loading tags...'
try {
const resp = await fetch(`${API}/volume/tags?iso=${encodeURIComponent(iso)}&part=${encodeURIComponent(part)}&vol=${encodeURIComponent(vol)}`)
if (!resp.ok) {
detailEl.innerHTML = 'HTTP ' + resp.status + ''
return
}
const data = await resp.json()
if (!data || data.error) {
detailEl.innerHTML = '' + (data && data.error ? data.error : 'server error') + ''
return
}
if (!Array.isArray(data) || data.length === 0) {
detailEl.innerHTML = 'No tags on this volume'
return
}
const colorKeys = TAG_COLORS
detailEl.innerHTML = `
Tags: ${esc(vol)}
${data.map(t => {
const color = colorKeys[(t.index - 1) % colorKeys.length]
return `
${esc(t.name)}
`
}).join('')}
`
} catch (e) {
detailEl.innerHTML = 'Failed: ' + e.message
}
}