diff --git a/lightsync/frontend/app.js b/lightsync/frontend/app.js index 69267f9..f6c0b79 100644 --- a/lightsync/frontend/app.js +++ b/lightsync/frontend/app.js @@ -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 ──────────────────────────────────────────────────── diff --git a/lightsync/frontend/index.html b/lightsync/frontend/index.html index e0aa550..0ba4fe5 100644 --- a/lightsync/frontend/index.html +++ b/lightsync/frontend/index.html @@ -100,6 +100,11 @@ +
+ + + +
[SPACE] PLAY [←→] SEEK [^Z] UNDO [^S] SAVE diff --git a/lightsync/frontend/style.css b/lightsync/frontend/style.css index 189942f..acdd088 100644 --- a/lightsync/frontend/style.css +++ b/lightsync/frontend/style.css @@ -553,3 +553,29 @@ button:hover { white-space: nowrap; flex-shrink: 0; } + +/* YouTube URL input (Phase 6) */ +.yt-url-input { + background: var(--bg-panel); + border: 1px solid var(--border-dim); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 12px; + padding: 2px 6px; + width: 180px; + outline: none; +} +.yt-url-input:focus { + border-color: var(--accent); +} +.yt-url-input:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.yt-progress { + font-family: var(--font-mono); + font-size: 11px; + color: var(--accent); + white-space: nowrap; + min-width: 200px; +}