Files
ytdlp/app/static/app.js
T

213 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const form = document.querySelector("#inspect-form");
const urlInput = document.querySelector("#source-url");
const submitButton = document.querySelector("#inspect-button");
const errorNotice = document.querySelector("#error-notice");
const errorMessage = document.querySelector("#error-message");
const resultSection = document.querySelector("#result");
const heroSection = document.querySelector(".hero");
const newSourceButton = document.querySelector("#new-source");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const fields = {
title: document.querySelector("#result-title"),
sourceLink: document.querySelector("#source-link"),
thumbnail: document.querySelector("#thumbnail"),
thumbnailShell: document.querySelector("#thumbnail-shell"),
duration: document.querySelector("#duration"),
extractor: document.querySelector("#extractor"),
uploader: document.querySelector("#uploader"),
uploadDate: document.querySelector("#upload-date"),
topQuality: document.querySelector("#top-quality"),
bestSpecs: document.querySelector("#best-specs"),
compatibleOption: document.querySelector("#compatible-option"),
compatibleSpecs: document.querySelector("#compatible-specs"),
};
function displayValue(value, fallback = "Not available") {
return value === null || value === undefined || value === "" ? fallback : String(value);
}
function formatDuration(seconds) {
if (!Number.isFinite(seconds)) return "Unknown duration";
const total = Math.max(0, Math.round(seconds));
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const remainingSeconds = total % 60;
const parts = hours > 0 ? [hours, minutes, remainingSeconds] : [minutes, remainingSeconds];
return parts.map((part) => String(part).padStart(2, "0")).join(":");
}
function formatUploadDate(value) {
if (!/^\d{8}$/.test(value || "")) return displayValue(value);
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
}
function resolutionLabel(format) {
if (format.width && format.height) return `${format.width} × ${format.height}`;
if (format.audio_codec && format.audio_codec !== "none") return "Audio only";
return "Unknown";
}
function isVideo(format) {
return format.video_codec && format.video_codec !== "none";
}
function isAudio(format) {
return format.audio_codec && format.audio_codec !== "none";
}
function sortByVisualQuality(formats) {
return [...formats].sort((left, right) => {
return (right.height || 0) - (left.height || 0) || (right.fps || 0) - (left.fps || 0);
});
}
function codecFamily(codec) {
if (!codec || codec === "none") return null;
const normalized = codec.toLowerCase();
if (normalized.startsWith("avc") || normalized === "h264") return "H.264";
if (normalized.startsWith("av01")) return "AV1";
if (normalized.startsWith("vp9") || normalized.startsWith("vp09")) return "VP9";
if (normalized.startsWith("mp4a") || normalized === "aac") return "AAC";
if (normalized.startsWith("opus")) return "Opus";
return codec;
}
function addSpec(list, label, value) {
const wrapper = document.createElement("div");
const term = document.createElement("dt");
const detail = document.createElement("dd");
term.textContent = label;
detail.textContent = value;
wrapper.append(term, detail);
list.append(wrapper);
}
function renderOption(list, video, audio) {
list.replaceChildren();
addSpec(list, "Quality", resolutionLabel(video));
if (video.fps) addSpec(list, "FPS", String(video.fps));
addSpec(list, "Video", displayValue(codecFamily(video.video_codec), "Unknown"));
const audioCodec = isAudio(video) ? video.audio_codec : audio?.audio_codec;
addSpec(list, "Audio", displayValue(codecFamily(audioCodec), "Unknown"));
}
function renderOutputOptions(formats) {
const orderedVideo = sortByVisualQuality(formats.filter(isVideo));
const audioOnly = formats.filter((format) => !isVideo(format) && isAudio(format));
const bestVideo = orderedVideo[0];
const bestAudio = audioOnly.at(-1);
if (!bestVideo) {
fields.topQuality.textContent = "Audio only";
fields.bestSpecs.replaceChildren();
fields.compatibleOption.hidden = true;
return;
}
fields.topQuality.textContent = resolutionLabel(bestVideo);
renderOption(fields.bestSpecs, bestVideo, bestAudio);
const compatibleVideo = orderedVideo.find((format) => {
const videoCodec = codecFamily(format.video_codec);
const audioCodec = codecFamily(format.audio_codec);
return videoCodec === "H.264" && (!isAudio(format) || audioCodec === "AAC");
});
const compatibleAudio = audioOnly.findLast((format) => codecFamily(format.audio_codec) === "AAC");
if (compatibleVideo && (isAudio(compatibleVideo) || compatibleAudio)) {
renderOption(fields.compatibleSpecs, compatibleVideo, compatibleAudio);
fields.compatibleOption.hidden = false;
} else {
fields.compatibleOption.hidden = true;
}
}
function renderResult(data) {
fields.title.textContent = data.title;
fields.sourceLink.href = data.source_url;
fields.extractor.textContent = displayValue(data.extractor);
fields.uploader.textContent = displayValue(data.uploader);
fields.uploadDate.textContent = formatUploadDate(data.upload_date);
fields.duration.textContent = formatDuration(data.duration);
if (data.thumbnail) {
fields.thumbnail.src = data.thumbnail;
fields.thumbnail.alt = `Preview for ${data.title}`;
fields.thumbnail.hidden = false;
fields.thumbnailShell.classList.remove("no-image");
} else {
fields.thumbnail.removeAttribute("src");
fields.thumbnail.alt = "";
fields.thumbnail.hidden = true;
fields.thumbnailShell.classList.add("no-image");
}
renderOutputOptions(data.formats);
}
function transitionDelay(milliseconds) {
return new Promise((resolve) => {
window.setTimeout(resolve, reducedMotion.matches ? 0 : milliseconds);
});
}
async function showResult(data) {
renderResult(data);
heroSection.classList.add("is-leaving");
await transitionDelay(180);
heroSection.hidden = true;
heroSection.classList.remove("is-leaving");
resultSection.hidden = false;
window.scrollTo({ top: 0, behavior: "auto" });
window.requestAnimationFrame(() => resultSection.classList.add("is-visible"));
}
async function showInspector() {
resultSection.classList.remove("is-visible");
resultSection.classList.add("is-leaving");
await transitionDelay(180);
resultSection.hidden = true;
resultSection.classList.remove("is-leaving");
heroSection.classList.add("is-leaving");
heroSection.hidden = false;
window.requestAnimationFrame(() => {
heroSection.classList.remove("is-leaving");
urlInput.focus();
});
}
function setLoading(isLoading) {
submitButton.disabled = isLoading;
urlInput.disabled = isLoading;
form.setAttribute("aria-busy", String(isLoading));
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
errorNotice.hidden = true;
resultSection.hidden = true;
setLoading(true);
try {
const response = await fetch("/api/inspect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: urlInput.value.trim() }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || "The source could not be inspected.");
}
await showResult(data);
} catch (error) {
errorMessage.textContent = error instanceof Error ? error.message : "Inspection failed.";
errorNotice.hidden = false;
} finally {
setLoading(false);
}
});
newSourceButton.addEventListener("click", showInspector);