feat: add dark source inspection interface

This commit is contained in:
bot
2026-08-12 18:42:03 +03:00
parent db75f2c4f8
commit 277aa8a568
9 changed files with 1090 additions and 16 deletions
+212
View File
@@ -0,0 +1,212 @@
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);