feat(06-01): add YouTube URL input with terminal progress bar
- Add yt-url input, FETCH button, and yt-progress span to transport bar - Add fetchWithProgress() SSE ReadableStream reader function - Add renderProgressBar() terminal-style block fill renderer (DOWNLOADING ████) - Wire FETCH button click handler with D-02 disable-during-download behavior - Auto-load audio into player after download (D-01 same pipeline as upload) - Add .yt-url-input and .yt-progress CSS styles (flat, monospace, no border-radius) - Enable yt-url input on page load via JS
This commit is contained in:
@@ -284,6 +284,90 @@ document.getElementById('audio-upload')?.addEventListener('change', async (e) =>
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
// ── YouTube fetch ────────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchWithProgress(url, body, onProgress, onDone, onError) {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const lines = buf.split('\n\n');
|
||||
buf = lines.pop();
|
||||
for (const chunk of lines) {
|
||||
if (chunk.startsWith('data: ')) {
|
||||
const msg = JSON.parse(chunk.slice(6));
|
||||
if (msg.type === 'progress') onProgress(msg.pct);
|
||||
if (msg.type === 'done') onDone(msg);
|
||||
if (msg.type === 'error') onError(msg.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderProgressBar(pct, width = 20) {
|
||||
const filled = Math.round((pct / 100) * width);
|
||||
const empty = width - filled;
|
||||
return `DOWNLOADING ${'█'.repeat(filled)}${'░'.repeat(empty)} ${Math.round(pct)}%`;
|
||||
}
|
||||
|
||||
document.getElementById('btn-fetch')?.addEventListener('click', async () => {
|
||||
const urlInput = document.getElementById('yt-url');
|
||||
const btn = document.getElementById('btn-fetch');
|
||||
const progress = document.getElementById('yt-progress');
|
||||
const url = urlInput?.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
// D-02: Disable input during download
|
||||
urlInput.disabled = true;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
progress.style.display = 'inline';
|
||||
progress.textContent = 'DOWNLOADING ░░░░░░░░░░░░░░░░░░░░ 0%';
|
||||
|
||||
try {
|
||||
await fetchWithProgress('/api/audio/fetch', { url },
|
||||
(pct) => {
|
||||
progress.textContent = renderProgressBar(pct);
|
||||
},
|
||||
(msg) => {
|
||||
// D-01: After download, load identically to local upload
|
||||
progress.textContent = 'COMPLETE';
|
||||
const audio = document.getElementById('player');
|
||||
if (audio && msg.filename) {
|
||||
audio.src = `/shows/${msg.filename}`;
|
||||
audio.load();
|
||||
}
|
||||
refreshFiles(msg.path);
|
||||
setTimeout(() => { progress.style.display = 'none'; }, 2000);
|
||||
},
|
||||
(errMsg) => {
|
||||
progress.textContent = `ERROR: ${errMsg}`;
|
||||
progress.style.color = '#ff4444';
|
||||
setTimeout(() => {
|
||||
progress.style.display = 'none';
|
||||
progress.style.color = '';
|
||||
}, 5000);
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
progress.textContent = `ERROR: ${e.message}`;
|
||||
setTimeout(() => { progress.style.display = 'none'; }, 5000);
|
||||
} finally {
|
||||
urlInput.disabled = false;
|
||||
urlInput.value = '';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'FETCH';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Load selected ────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-load')?.addEventListener('click', async () => {
|
||||
@@ -349,6 +433,7 @@ document.getElementById('add-device-form')?.addEventListener('submit', async (e)
|
||||
|
||||
wireAudio();
|
||||
wireKeyboard(); // keyboard shortcuts (Phase 5)
|
||||
document.getElementById('yt-url').disabled = false;
|
||||
|
||||
// ── Preview strip builder ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user