const form = document.querySelector("#convert-form"); const urlInput = document.querySelector("#url"); const formatSelect = document.querySelector("#format"); const qualityField = document.querySelector("#quality-field"); const qualitySelect = document.querySelector("#quality"); const convertButton = document.querySelector("#convert-button"); const metadataPanel = document.querySelector("#metadata"); const thumbnail = document.querySelector("#thumbnail"); const videoTitle = document.querySelector("#video-title"); const videoChannel = document.querySelector("#video-channel"); const videoDuration = document.querySelector("#video-duration"); const statusDot = document.querySelector("#status-dot"); const statusText = document.querySelector("#status-text"); const errorText = document.querySelector("#error-text"); const downloadLink = document.querySelector("#download-link"); let pollTimer = null; formatSelect.addEventListener("change", () => { qualityField.classList.toggle("hidden", formatSelect.value !== "mp3"); }); form.addEventListener("submit", async (event) => { event.preventDefault(); clearPolling(); resetDownload(); setBusy(true); setStatus("active", "Validating URL"); const payload = { url: urlInput.value.trim(), format: formatSelect.value, quality: qualitySelect.value, }; try { setStatus("active", "Fetching video info"); const info = await postJson("/api/info", { url: payload.url }); renderMetadata(info); setStatus("active", "Queued"); const { job_id: jobId } = await postJson("/api/convert", payload); pollStatus(jobId); } catch (error) { showError(error.message); setBusy(false); } }); async function pollStatus(jobId) { try { const status = await getJson(`/api/status/${jobId}`); if (status.metadata) { renderMetadata(status.metadata); } if (status.status === "complete") { setStatus("done", "Ready to download"); downloadLink.href = status.download_url; downloadLink.classList.remove("hidden"); setBusy(false); return; } if (status.status === "error") { showError(status.error || "Conversion failed."); setBusy(false); return; } setStatus("active", status.message || "Working"); pollTimer = window.setTimeout(() => pollStatus(jobId), 1200); } catch (error) { showError(error.message); setBusy(false); } } async function postJson(url, body) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); return parseJsonResponse(response); } async function getJson(url) { const response = await fetch(url); return parseJsonResponse(response); } async function parseJsonResponse(response) { const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.detail || "Request failed."); } return data; } function renderMetadata(info) { videoTitle.textContent = info.title || "Untitled YouTube video"; videoChannel.textContent = info.channel || "Unknown channel"; videoDuration.textContent = info.duration ? `Duration ${info.duration}` : "Duration unknown"; if (info.thumbnail) { thumbnail.src = info.thumbnail; thumbnail.classList.remove("hidden"); } else { thumbnail.removeAttribute("src"); thumbnail.classList.add("hidden"); } metadataPanel.classList.remove("hidden"); } function setStatus(kind, message) { statusDot.className = `status-dot ${kind}`; statusText.textContent = message; errorText.classList.add("hidden"); errorText.textContent = ""; } function showError(message) { setStatus("error", "Error"); errorText.textContent = message; errorText.classList.remove("hidden"); } function resetDownload() { downloadLink.classList.add("hidden"); downloadLink.removeAttribute("href"); } function setBusy(isBusy) { convertButton.disabled = isBusy; convertButton.textContent = isBusy ? "Converting..." : "Convert"; } function clearPolling() { if (pollTimer) { window.clearTimeout(pollTimer); pollTimer = null; } }