Initial commit
This commit is contained in:
146
frontend/app.js
Normal file
146
frontend/app.js
Normal file
@@ -0,0 +1,146 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user