Files
2026-06-12 15:29:30 -07:00

147 lines
4.0 KiB
JavaScript

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;
}
}