1013 lines
34 KiB
JavaScript
1013 lines
34 KiB
JavaScript
const statusBar = document.getElementById("statusBar");
|
|
const statusLabel = document.getElementById("statusLabel");
|
|
const searchForm = document.getElementById("searchForm");
|
|
const searchQuery = document.getElementById("searchQuery");
|
|
const searchResults = document.getElementById("searchResults");
|
|
const searchWarning = document.getElementById("searchWarning");
|
|
const queryVariants = document.getElementById("queryVariants");
|
|
const platformToggles = Array.from(document.querySelectorAll("[data-platform-toggle]"));
|
|
const dropzone = document.getElementById("dropzone");
|
|
const fileInput = document.getElementById("fileInput");
|
|
const uploadResult = document.getElementById("uploadResult");
|
|
const downloadForm = document.getElementById("downloadForm");
|
|
const downloadUrl = document.getElementById("downloadUrl");
|
|
const downloadResult = document.getElementById("downloadResult");
|
|
const cardTemplate = document.getElementById("searchCardTemplate");
|
|
const previewModal = document.getElementById("previewModal");
|
|
const previewMediaFrame = document.getElementById("previewMediaFrame");
|
|
const previewTitle = document.getElementById("previewTitle");
|
|
const previewVideo = document.getElementById("previewVideo");
|
|
const previewThumbnail = document.getElementById("previewThumbnail");
|
|
const previewDuration = document.getElementById("previewDuration");
|
|
const qualitySelect = document.getElementById("qualitySelect");
|
|
const confirmDownload = document.getElementById("confirmDownload");
|
|
const closePreviewModal = document.getElementById("closePreviewModal");
|
|
const rangeSummary = document.getElementById("rangeSummary");
|
|
const rangeFill = document.getElementById("rangeFill");
|
|
const startThumb = document.getElementById("startThumb");
|
|
const endThumb = document.getElementById("endThumb");
|
|
const startLabel = document.getElementById("startLabel");
|
|
const endLabel = document.getElementById("endLabel");
|
|
const setStartFromPreview = document.getElementById("setStartFromPreview");
|
|
const setEndFromPreview = document.getElementById("setEndFromPreview");
|
|
const debugToggle = document.getElementById("debugToggle");
|
|
const debugPanel = document.getElementById("debugPanel");
|
|
const closeDebugPanel = document.getElementById("closeDebugPanel");
|
|
const clearLogs = document.getElementById("clearLogs");
|
|
const downloadLogs = document.getElementById("downloadLogs");
|
|
const debugLogList = document.getElementById("debugLogList");
|
|
const debugSummary = document.getElementById("debugSummary");
|
|
const resultModal = document.getElementById("resultModal");
|
|
const resultModalTitle = document.getElementById("resultModalTitle");
|
|
const resultModalSource = document.getElementById("resultModalSource");
|
|
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
|
const resultModalReason = document.getElementById("resultModalReason");
|
|
const resultModalFrame = document.getElementById("resultModalFrame");
|
|
const resultModalMediaFrame = document.getElementById("resultModalMediaFrame");
|
|
const resultModalVideo = document.getElementById("resultModalVideo");
|
|
const resultModalThumbnail = document.getElementById("resultModalThumbnail");
|
|
const resultModalGooglePanel = document.getElementById("resultModalGooglePanel");
|
|
const resultModalGoogleImage = document.getElementById("resultModalGoogleImage");
|
|
const resultModalGoogleText = document.getElementById("resultModalGoogleText");
|
|
const resultModalFallbackLabel = document.getElementById("resultModalFallbackLabel");
|
|
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
|
const resultModalDownload = document.getElementById("resultModalDownload");
|
|
const resultModalSecondaryAction = document.getElementById("resultModalSecondaryAction");
|
|
const closeResultModal = document.getElementById("closeResultModal");
|
|
const resultModalReady = Boolean(
|
|
resultModal &&
|
|
resultModalTitle &&
|
|
resultModalSource &&
|
|
resultModalSnippet &&
|
|
resultModalReason &&
|
|
resultModalFrame &&
|
|
resultModalMediaFrame &&
|
|
resultModalVideo &&
|
|
resultModalThumbnail &&
|
|
resultModalGooglePanel &&
|
|
resultModalGoogleImage &&
|
|
resultModalGoogleText &&
|
|
resultModalFallbackLabel &&
|
|
resultModalOpenExternal &&
|
|
resultModalDownload &&
|
|
resultModalSecondaryAction &&
|
|
closeResultModal,
|
|
);
|
|
|
|
let pendingDownload = null;
|
|
let cropStart = 0;
|
|
let cropEnd = 0;
|
|
let cropMax = 0;
|
|
let activeThumb = null;
|
|
let activeResultItem = null;
|
|
let activeResultModalSummaryRequest = 0;
|
|
const activePlatforms = new Set(["envato", "artgrid", "google video"]);
|
|
const hlsInstances = new WeakMap();
|
|
const debugEntries = [];
|
|
const summaryTranslationCache = new Map();
|
|
const summaryTranslationInflight = new Map();
|
|
let cardSummaryObserver = null;
|
|
const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
|
|
|
|
function proxiedPreviewURL(src) {
|
|
if (!src) {
|
|
return "";
|
|
}
|
|
return `/api/preview/stream?url=${encodeURIComponent(src)}`;
|
|
}
|
|
|
|
function isLowValueThumbnailURL(src) {
|
|
const lower = String(src || "").toLowerCase();
|
|
if (!lower) {
|
|
return true;
|
|
}
|
|
return [
|
|
"favicon",
|
|
"apple-touch-icon",
|
|
"/logo",
|
|
"/icon",
|
|
"icon.",
|
|
"logo.",
|
|
"placehold.co",
|
|
].some((token) => lower.includes(token)) ||
|
|
((lower.includes("googleusercontent.com") || lower.includes("gstatic.com") || lower.includes("bing.com") || lower.includes("duckduckgo.com")) && !lower.includes("ytimg.com"));
|
|
}
|
|
|
|
function hasUsableThumbnail(src) {
|
|
return Boolean(src) && !isLowValueThumbnailURL(src);
|
|
}
|
|
|
|
function summarizeReason(reason) {
|
|
const text = String(reason || "").trim();
|
|
if (!text) {
|
|
return "";
|
|
}
|
|
if (text === "Fallback due to missing provider preview.") {
|
|
return "Provider preview missing";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function setStatus(label, progress) {
|
|
statusLabel.textContent = label;
|
|
statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
logEvent("status", { label, progress });
|
|
}
|
|
|
|
function setHidden(element, hidden, visibleDisplayClass = "flex") {
|
|
element.classList.toggle("hidden", hidden);
|
|
if (visibleDisplayClass) {
|
|
element.classList.toggle(visibleDisplayClass, !hidden);
|
|
}
|
|
}
|
|
|
|
function showWarning(message) {
|
|
searchWarning.textContent = message;
|
|
setHidden(searchWarning, !message, "");
|
|
}
|
|
|
|
function logEvent(type, payload) {
|
|
const entry = {
|
|
ts: new Date().toISOString(),
|
|
type,
|
|
payload,
|
|
};
|
|
debugEntries.push(entry);
|
|
if (debugEntries.length > 400) {
|
|
debugEntries.shift();
|
|
}
|
|
renderLogs();
|
|
}
|
|
|
|
function safeStringify(value) {
|
|
try {
|
|
return JSON.stringify(compactPayload(value), null, 2);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function compactPayload(value, depth = 0) {
|
|
if (depth > 3) {
|
|
return "[truncated]";
|
|
}
|
|
if (Array.isArray(value)) {
|
|
if (value.length > 8) {
|
|
return {
|
|
type: "array",
|
|
length: value.length,
|
|
sample: value.slice(0, 5).map((item) => compactPayload(item, depth + 1)),
|
|
};
|
|
}
|
|
return value.map((item) => compactPayload(item, depth + 1));
|
|
}
|
|
if (value && typeof value === "object") {
|
|
const entries = Object.entries(value);
|
|
return Object.fromEntries(entries.map(([key, item]) => [key, compactPayload(item, depth + 1)]));
|
|
}
|
|
if (typeof value === "string" && value.length > 500) {
|
|
return `${value.slice(0, 500)}...`;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function renderLogs() {
|
|
debugSummary.textContent = `${debugEntries.length} events captured`;
|
|
debugLogList.innerHTML = "";
|
|
for (const entry of debugEntries.slice().reverse()) {
|
|
const node = document.createElement("div");
|
|
node.className = "debug-entry";
|
|
node.innerHTML = `
|
|
<div class="debug-entry__meta">
|
|
<span>${entry.type}</span>
|
|
<span>${entry.ts}</span>
|
|
</div>
|
|
<div class="debug-entry__payload">${safeStringify(entry.payload)}</div>
|
|
`;
|
|
debugLogList.appendChild(node);
|
|
}
|
|
}
|
|
|
|
function toClock(totalSeconds) {
|
|
const seconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
|
|
const hours = String(Math.floor(seconds / 3600)).padStart(2, "0");
|
|
const minutes = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0");
|
|
const secs = String(seconds % 60).padStart(2, "0");
|
|
return `${hours}:${minutes}:${secs}`;
|
|
}
|
|
|
|
function extractYouTubeID(link) {
|
|
if (!link) {
|
|
return "";
|
|
}
|
|
const patterns = [
|
|
/(?:v=|\/shorts\/|\/embed\/)([A-Za-z0-9_-]{11})/,
|
|
/youtu\.be\/([A-Za-z0-9_-]{11})/,
|
|
];
|
|
for (const pattern of patterns) {
|
|
const match = link.match(pattern);
|
|
if (match?.[1]) {
|
|
return match[1];
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function syncRanges() {
|
|
let start = cropStart;
|
|
let end = cropEnd;
|
|
if (start > end) {
|
|
if (activeThumb === "start") {
|
|
end = start;
|
|
} else {
|
|
start = end;
|
|
}
|
|
}
|
|
cropStart = start;
|
|
cropEnd = end;
|
|
const startPct = cropMax > 0 ? (cropStart / cropMax) * 100 : 0;
|
|
const endPct = cropMax > 0 ? (cropEnd / cropMax) * 100 : 0;
|
|
startThumb.style.left = `calc(${startPct}% - 10px)`;
|
|
endThumb.style.left = `calc(${endPct}% - 10px)`;
|
|
rangeFill.style.left = `${startPct}%`;
|
|
rangeFill.style.width = `${Math.max(0, endPct - startPct)}%`;
|
|
rangeSummary.textContent = `${toClock(start)} - ${toClock(end)}`;
|
|
startLabel.textContent = `Start ${toClock(start)}`;
|
|
endLabel.textContent = `End ${toClock(end)}`;
|
|
}
|
|
|
|
function renderQueryVariants(queries = []) {
|
|
queryVariants.classList.add("hidden");
|
|
}
|
|
|
|
function syncPlatformButtons() {
|
|
for (const button of platformToggles) {
|
|
const platform = button.dataset.platformToggle;
|
|
const active = activePlatforms.has(platform);
|
|
button.classList.toggle("bg-white", active);
|
|
button.classList.toggle("text-black", active);
|
|
button.classList.toggle("border-white", active);
|
|
button.classList.toggle("bg-transparent", !active);
|
|
button.classList.toggle("text-zinc-300", !active);
|
|
button.classList.toggle("border-white/20", !active);
|
|
}
|
|
logEvent("platforms:update", { active: Array.from(activePlatforms) });
|
|
}
|
|
|
|
function connectWS() {
|
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
const socket = new WebSocket(`${protocol}://${window.location.host}/ws`);
|
|
socket.addEventListener("message", (event) => {
|
|
const payload = JSON.parse(event.data);
|
|
if (payload.event !== "progress") {
|
|
logEvent("ws:message:ignored", payload);
|
|
return;
|
|
}
|
|
logEvent("ws:message", payload);
|
|
const data = payload.data;
|
|
const label = data.status || `${data.type || "task"} in progress`;
|
|
setStatus(label, Number(data.progress ?? 0));
|
|
if (data.type === "upload" && data.status === "completed") {
|
|
uploadResult.textContent = `${data.filename} saved successfully`;
|
|
}
|
|
if (data.type === "download" && data.status === "completed") {
|
|
downloadResult.textContent = data.output || "download completed";
|
|
}
|
|
if (data.status === "error") {
|
|
downloadResult.textContent = data.message || "task failed";
|
|
}
|
|
});
|
|
socket.addEventListener("close", () => {
|
|
logEvent("ws:close", { reason: "socket closed" });
|
|
setTimeout(connectWS, 1000);
|
|
});
|
|
socket.addEventListener("open", () => {
|
|
logEvent("ws:open", { url: socket.url });
|
|
});
|
|
socket.addEventListener("error", () => {
|
|
logEvent("ws:error", { url: socket.url });
|
|
});
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
logEvent("api:request", {
|
|
path,
|
|
method: options.method || "GET",
|
|
hasBody: Boolean(options.body),
|
|
bodyPreview: typeof options.body === "string" ? options.body.slice(0, 800) : "[non-string body]",
|
|
});
|
|
const response = await fetch(path, options);
|
|
const rawText = await response.text();
|
|
let data = {};
|
|
if (rawText) {
|
|
try {
|
|
data = JSON.parse(rawText);
|
|
} catch {
|
|
data = { rawText };
|
|
}
|
|
}
|
|
logEvent("api:response", {
|
|
path,
|
|
status: response.status,
|
|
ok: response.ok,
|
|
body: compactPayload(data),
|
|
});
|
|
if (!response.ok) {
|
|
const message = data.error || data.rawText || `request failed (${response.status})`;
|
|
const error = new Error(message);
|
|
error.status = response.status;
|
|
error.data = data;
|
|
throw error;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function attachVideoSource(video, src) {
|
|
detachVideoSource(video);
|
|
if (!src) {
|
|
logEvent("preview:attach:skipped", { reason: "empty src" });
|
|
return;
|
|
}
|
|
if (src.endsWith(".m3u8")) {
|
|
if (window.Hls && window.Hls.isSupported()) {
|
|
const hls = new window.Hls({ enableWorker: false });
|
|
hls.loadSource(src);
|
|
hls.attachMedia(video);
|
|
hlsInstances.set(video, hls);
|
|
logEvent("preview:attach:hls", { src, mode: "hls.js" });
|
|
return;
|
|
}
|
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
video.src = src;
|
|
logEvent("preview:attach:hls", { src, mode: "native" });
|
|
return;
|
|
}
|
|
logEvent("preview:attach:skipped", { reason: "hls unsupported", src });
|
|
return;
|
|
}
|
|
video.src = src;
|
|
logEvent("preview:attach:file", { src });
|
|
}
|
|
|
|
function startHoverPreview(video, src) {
|
|
if (!src) {
|
|
return;
|
|
}
|
|
attachVideoSource(video, src);
|
|
video.classList.remove("hidden");
|
|
const attemptPlay = () => {
|
|
video.play().catch((error) => {
|
|
logEvent("preview:hover:play:error", { src, message: String(error) });
|
|
});
|
|
};
|
|
if (video.readyState >= 2) {
|
|
attemptPlay();
|
|
return;
|
|
}
|
|
const onReady = () => {
|
|
video.removeEventListener("loadeddata", onReady);
|
|
video.removeEventListener("canplay", onReady);
|
|
attemptPlay();
|
|
};
|
|
video.addEventListener("loadeddata", onReady, { once: true });
|
|
video.addEventListener("canplay", onReady, { once: true });
|
|
if (video.load) {
|
|
video.load();
|
|
}
|
|
}
|
|
|
|
function detachVideoSource(video) {
|
|
const existing = hlsInstances.get(video);
|
|
if (existing) {
|
|
existing.destroy();
|
|
hlsInstances.delete(video);
|
|
logEvent("preview:detach:hls", { ok: true });
|
|
}
|
|
video.removeAttribute("src");
|
|
video.load();
|
|
}
|
|
|
|
function resetPreviewPlayer() {
|
|
previewVideo.pause();
|
|
detachVideoSource(previewVideo);
|
|
previewMediaFrame.style.aspectRatio = "";
|
|
}
|
|
|
|
function showModal(element) {
|
|
setHidden(element, false);
|
|
}
|
|
|
|
function hideModal(element) {
|
|
setHidden(element, true);
|
|
}
|
|
|
|
function buildResultModalEmbedURL(item) {
|
|
if (item?.embedUrl) {
|
|
if (item.source === "Google Video" && item.embedUrl.includes("youtube-nocookie.com/embed/")) {
|
|
return `${item.embedUrl}&origin=${encodeURIComponent(window.location.origin)}`;
|
|
}
|
|
return item.embedUrl;
|
|
}
|
|
if (!item?.link) {
|
|
return "about:blank";
|
|
}
|
|
if (item.source === "Google Video") {
|
|
const videoId = extractYouTubeID(item.link);
|
|
if (videoId) {
|
|
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&modestbranding=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`;
|
|
}
|
|
}
|
|
return item.link;
|
|
}
|
|
|
|
function resetResultModalMedia() {
|
|
if (!resultModalReady) {
|
|
return;
|
|
}
|
|
resultModalFrame.onload = null;
|
|
resultModalFrame.src = "about:blank";
|
|
resultModalVideo.pause();
|
|
detachVideoSource(resultModalVideo);
|
|
resultModalThumbnail.removeAttribute("src");
|
|
resultModalGoogleImage.removeAttribute("src");
|
|
resultModalGoogleText.textContent = "";
|
|
resultModalFallbackLabel.textContent = "Preview Fallback";
|
|
resultModalMediaFrame.style.aspectRatio = "";
|
|
setHidden(resultModalFrame, true, "");
|
|
setHidden(resultModalVideo, true, "");
|
|
setHidden(resultModalThumbnail, true, "");
|
|
setHidden(resultModalGooglePanel, true, "flex");
|
|
}
|
|
|
|
function showResultModalFrame(src) {
|
|
if (!src) {
|
|
return;
|
|
}
|
|
resultModalFrame.src = src;
|
|
setHidden(resultModalFrame, false, "");
|
|
}
|
|
|
|
function showResultModalVideo(src) {
|
|
if (!src) {
|
|
return;
|
|
}
|
|
attachVideoSource(resultModalVideo, proxiedPreviewURL(src));
|
|
setHidden(resultModalVideo, false, "");
|
|
}
|
|
|
|
function showResultModalThumbnail(src, alt) {
|
|
resultModalThumbnail.src = src || PREVIEW_PLACEHOLDER;
|
|
resultModalThumbnail.alt = alt || "";
|
|
setHidden(resultModalThumbnail, false, "");
|
|
}
|
|
|
|
function showResultModalGooglePanel(item, message = "") {
|
|
resultModalFallbackLabel.textContent = item.source || "Preview Fallback";
|
|
resultModalGoogleImage.src = hasUsableThumbnail(item.thumbnailUrl) ? item.thumbnailUrl : PREVIEW_PLACEHOLDER;
|
|
resultModalGoogleImage.alt = item.title || "";
|
|
resultModalGoogleText.textContent = summarizeReason(message || item.previewBlockedReason || item.snippet || item.reason || "Open source page or use the primary action.");
|
|
setHidden(resultModalGooglePanel, false, "flex");
|
|
}
|
|
|
|
async function translateSummaryForModal(item, originalText, requestId) {
|
|
const translated = await translateSummaryText(originalText);
|
|
if (!translated) {
|
|
return;
|
|
}
|
|
if (activeResultItem?.link === item.link && activeResultModalSummaryRequest === requestId) {
|
|
resultModalSnippet.textContent = translated;
|
|
logEvent("result:modal:summary_translated", { title: item.title, source: item.source });
|
|
}
|
|
}
|
|
|
|
async function translateSummaryText(originalText) {
|
|
const trimmed = String(originalText || "").trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
if (summaryTranslationCache.has(trimmed)) {
|
|
return summaryTranslationCache.get(trimmed);
|
|
}
|
|
if (summaryTranslationInflight.has(trimmed)) {
|
|
return summaryTranslationInflight.get(trimmed);
|
|
}
|
|
|
|
const request = (async () => {
|
|
try {
|
|
const data = await api("/api/translate/summary", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text: trimmed }),
|
|
});
|
|
const translated = String(data.translatedText || "").trim();
|
|
if (translated) {
|
|
summaryTranslationCache.set(trimmed, translated);
|
|
}
|
|
return translated;
|
|
} catch {
|
|
return "";
|
|
} finally {
|
|
summaryTranslationInflight.delete(trimmed);
|
|
}
|
|
})();
|
|
summaryTranslationInflight.set(trimmed, request);
|
|
|
|
try {
|
|
return await request;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
async function translateCardSummary(node) {
|
|
if (!node || node.dataset.summaryTranslated === "true") {
|
|
return;
|
|
}
|
|
node.dataset.summaryTranslated = "true";
|
|
const originalText = node.dataset.summaryOriginal || "";
|
|
const translated = await translateSummaryText(originalText);
|
|
if (!translated) {
|
|
return;
|
|
}
|
|
const summaryNode = node.querySelector(".result-reason");
|
|
if (summaryNode) {
|
|
summaryNode.textContent = translated;
|
|
}
|
|
}
|
|
|
|
function ensureCardSummaryObserver() {
|
|
if (cardSummaryObserver || typeof IntersectionObserver === "undefined") {
|
|
return;
|
|
}
|
|
cardSummaryObserver = new IntersectionObserver((entries) => {
|
|
for (const entry of entries) {
|
|
if (!entry.isIntersecting) {
|
|
continue;
|
|
}
|
|
cardSummaryObserver.unobserve(entry.target);
|
|
void translateCardSummary(entry.target);
|
|
}
|
|
}, { rootMargin: "160px 0px" });
|
|
}
|
|
|
|
function fallbackResultModalMedia(item, reason) {
|
|
logEvent("result:modal:fallback", { title: item.title, source: item.source, reason, mediaMode: item.mediaMode });
|
|
if (item.previewVideoUrl) {
|
|
showResultModalVideo(item.previewVideoUrl);
|
|
return;
|
|
}
|
|
if (hasUsableThumbnail(item.thumbnailUrl)) {
|
|
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
|
|
return;
|
|
}
|
|
showResultModalGooglePanel(item, reason || "Preview fallback is being shown.");
|
|
}
|
|
|
|
function renderResults(results) {
|
|
searchResults.innerHTML = "";
|
|
ensureCardSummaryObserver();
|
|
if (!results.length) {
|
|
searchResults.innerHTML = `<div class="rounded-3xl border border-white/10 bg-black/30 p-5 text-sm text-zinc-400">No results matched the current search sources.</div>`;
|
|
return;
|
|
}
|
|
for (const item of results) {
|
|
const node = cardTemplate.content.firstElementChild.cloneNode(true);
|
|
const image = node.querySelector("img");
|
|
const previewVideo = node.querySelector(".preview-hover");
|
|
const mediaFallback = node.querySelector(".media-fallback");
|
|
const overlays = node.querySelectorAll(".preview-overlay");
|
|
const usableThumbnail = hasUsableThumbnail(item.thumbnailUrl);
|
|
if (usableThumbnail) {
|
|
image.src = item.thumbnailUrl;
|
|
image.alt = item.title;
|
|
image.classList.remove("hidden");
|
|
mediaFallback.classList.add("hidden");
|
|
} else {
|
|
image.removeAttribute("src");
|
|
image.alt = "";
|
|
image.classList.add("hidden");
|
|
mediaFallback.classList.remove("hidden");
|
|
mediaFallback.classList.add("flex");
|
|
mediaFallback.textContent = item.source === "Envato" || item.source === "Artgrid" ? `${item.source} preview unavailable` : "Preview unavailable";
|
|
}
|
|
node.querySelector("h3").textContent = item.title;
|
|
node.querySelector(".result-snippet").textContent = summarizeReason(item.reason) || item.source || "";
|
|
node.querySelector(".result-reason").textContent = item.snippet || item.previewBlockedReason || "";
|
|
node.querySelector(".source-badge").textContent = item.source;
|
|
node.dataset.summaryOriginal = item.snippet || "";
|
|
node.dataset.summaryTranslated = "false";
|
|
node.addEventListener("click", () => openResultModal(item));
|
|
previewVideo.poster = usableThumbnail ? item.thumbnailUrl : "";
|
|
if (item.previewVideoUrl) {
|
|
const mediaArea = node.querySelector(".relative");
|
|
mediaArea.addEventListener("mouseenter", () => {
|
|
logEvent("preview:hover:start", { title: item.title, source: item.source, previewVideoUrl: item.previewVideoUrl });
|
|
overlays.forEach((overlay) => overlay.classList.add("hidden"));
|
|
startHoverPreview(previewVideo, proxiedPreviewURL(item.previewVideoUrl));
|
|
});
|
|
mediaArea.addEventListener("mouseleave", () => {
|
|
logEvent("preview:hover:end", { title: item.title, source: item.source });
|
|
previewVideo.pause();
|
|
previewVideo.currentTime = 0;
|
|
previewVideo.classList.add("hidden");
|
|
detachVideoSource(previewVideo);
|
|
overlays.forEach((overlay) => overlay.classList.remove("hidden"));
|
|
});
|
|
}
|
|
if (cardSummaryObserver && item.snippet) {
|
|
cardSummaryObserver.observe(node);
|
|
} else if (item.snippet) {
|
|
void translateCardSummary(node);
|
|
}
|
|
searchResults.appendChild(node);
|
|
}
|
|
}
|
|
|
|
async function prepareDirectDownload(targetUrl) {
|
|
downloadUrl.value = targetUrl;
|
|
downloadResult.textContent = "checking duplicate history...";
|
|
const dup = await api(`/api/history/check?url=${encodeURIComponent(targetUrl)}`);
|
|
let force = false;
|
|
if (dup.exists) {
|
|
force = window.confirm("동일 URL 다운로드 이력이 있습니다. 계속 진행할까요?");
|
|
if (!force) {
|
|
downloadResult.textContent = "cancelled";
|
|
return;
|
|
}
|
|
}
|
|
pendingDownload = { url: targetUrl, force };
|
|
downloadResult.textContent = "loading preview...";
|
|
const preview = await api("/api/download/preview", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url: targetUrl }),
|
|
});
|
|
openPreviewModal(preview);
|
|
downloadResult.textContent = "preview loaded";
|
|
}
|
|
|
|
function openResultModal(item) {
|
|
if (!resultModalReady) {
|
|
logEvent("result:modal:error", { message: "result modal is not fully initialized" });
|
|
return;
|
|
}
|
|
activeResultItem = item;
|
|
activeResultModalSummaryRequest += 1;
|
|
const summaryRequestId = activeResultModalSummaryRequest;
|
|
resultModalTitle.textContent = item.title || "Untitled";
|
|
resultModalSource.textContent = item.source || "";
|
|
resultModalReason.textContent = summarizeReason(item.reason) || "AI 노트가 없습니다.";
|
|
const originalSummary = item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다.";
|
|
resultModalSnippet.textContent = originalSummary;
|
|
resultModalOpenExternal.href = item.link || "#";
|
|
resultModalDownload.classList.toggle("hidden", !item.actionType);
|
|
resultModalDownload.textContent = item.actionLabel || "Open Source";
|
|
const showSecondary = Boolean(item.secondaryActionLabel && item.link);
|
|
resultModalSecondaryAction.classList.toggle("hidden", !showSecondary);
|
|
resultModalSecondaryAction.textContent = item.secondaryActionLabel || "Open Source";
|
|
resetResultModalMedia();
|
|
const embedURL = buildResultModalEmbedURL(item);
|
|
const fallbackReason = item.previewBlockedReason || "Embedded view was unavailable, switched to fallback preview.";
|
|
if (item.source === "Google Video" && item.mediaMode === "thumbnail") {
|
|
showResultModalGooglePanel(item, item.snippet || "Open source page or download directly.");
|
|
} else if (item.mediaMode === "embed" && embedURL && embedURL !== "about:blank") {
|
|
showResultModalFrame(embedURL);
|
|
const timeout = window.setTimeout(() => {
|
|
logEvent("result:modal:iframe_timeout", { title: item.title, source: item.source, embedURL });
|
|
if (activeResultItem?.link === item.link) {
|
|
resetResultModalMedia();
|
|
fallbackResultModalMedia(item, fallbackReason);
|
|
}
|
|
}, item.source === "Google Video" ? 5000 : 2000);
|
|
resultModalFrame.onload = () => {
|
|
window.clearTimeout(timeout);
|
|
};
|
|
} else if (item.mediaMode === "preview_video" && item.previewVideoUrl) {
|
|
showResultModalVideo(item.previewVideoUrl);
|
|
} else if (item.mediaMode === "thumbnail" && hasUsableThumbnail(item.thumbnailUrl)) {
|
|
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
|
|
} else {
|
|
fallbackResultModalMedia(item, fallbackReason);
|
|
}
|
|
showModal(resultModal);
|
|
void translateSummaryForModal(item, item.snippet, summaryRequestId);
|
|
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
|
}
|
|
|
|
function closeResultViewer() {
|
|
if (!resultModalReady) {
|
|
return;
|
|
}
|
|
if (!resultModal.classList.contains("hidden")) {
|
|
logEvent("result:modal:close", { title: activeResultItem?.title || "" });
|
|
}
|
|
activeResultModalSummaryRequest += 1;
|
|
activeResultItem = null;
|
|
resetResultModalMedia();
|
|
hideModal(resultModal);
|
|
}
|
|
|
|
searchForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
setStatus("preparing search", 5);
|
|
showWarning("");
|
|
try {
|
|
const data = await api("/api/search", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query: searchQuery.value, platforms: Array.from(activePlatforms) }),
|
|
});
|
|
renderResults(data.results || []);
|
|
renderQueryVariants(data.queries || []);
|
|
showWarning(data.warning || "");
|
|
logEvent("search:completed", { results: data.results?.length || 0, queries: data.queries || [] });
|
|
setStatus("search complete", 100);
|
|
} catch (error) {
|
|
showWarning(error.message);
|
|
renderQueryVariants([]);
|
|
setStatus("search failed", 100);
|
|
}
|
|
});
|
|
|
|
async function uploadFile(file) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
uploadResult.textContent = "uploading...";
|
|
try {
|
|
await api("/api/upload", { method: "POST", body: formData });
|
|
logEvent("upload:completed", { fileName: file.name, size: file.size });
|
|
} catch (error) {
|
|
uploadResult.textContent = error.message;
|
|
}
|
|
}
|
|
|
|
function openPreviewModal(preview) {
|
|
logEvent("preview:modal:open", preview);
|
|
previewTitle.textContent = preview.title;
|
|
previewThumbnail.src = preview.thumbnail || PREVIEW_PLACEHOLDER;
|
|
previewThumbnail.alt = preview.title;
|
|
resetPreviewPlayer();
|
|
if (preview.previewStreamUrl) {
|
|
attachVideoSource(previewVideo, proxiedPreviewURL(preview.previewStreamUrl));
|
|
setHidden(previewVideo, false, "");
|
|
setHidden(previewThumbnail, true, "");
|
|
} else {
|
|
setHidden(previewVideo, true, "");
|
|
setHidden(previewThumbnail, false, "");
|
|
}
|
|
previewDuration.textContent = preview.duration;
|
|
qualitySelect.innerHTML = "";
|
|
for (const item of preview.qualities || []) {
|
|
const option = document.createElement("option");
|
|
option.value = item.value;
|
|
option.textContent = item.label;
|
|
qualitySelect.appendChild(option);
|
|
}
|
|
cropMax = Number(preview.durationSeconds || 0);
|
|
cropStart = 0;
|
|
cropEnd = cropMax;
|
|
syncRanges();
|
|
showModal(previewModal);
|
|
}
|
|
|
|
function closeModal() {
|
|
logEvent("preview:modal:close", { title: previewTitle.textContent });
|
|
resetPreviewPlayer();
|
|
hideModal(previewModal);
|
|
cropStart = 0;
|
|
cropEnd = 0;
|
|
cropMax = 0;
|
|
syncRanges();
|
|
pendingDownload = null;
|
|
}
|
|
|
|
dropzone.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
dropzone.classList.add("border-white/60", "bg-white/[0.08]");
|
|
});
|
|
|
|
dropzone.addEventListener("dragleave", () => {
|
|
dropzone.classList.remove("border-white/60", "bg-white/[0.08]");
|
|
});
|
|
|
|
dropzone.addEventListener("drop", async (event) => {
|
|
event.preventDefault();
|
|
dropzone.classList.remove("border-white/60", "bg-white/[0.08]");
|
|
const file = event.dataTransfer.files[0];
|
|
if (file) {
|
|
await uploadFile(file);
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener("change", async () => {
|
|
const [file] = fileInput.files;
|
|
if (file) {
|
|
await uploadFile(file);
|
|
}
|
|
});
|
|
|
|
downloadForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
await prepareDirectDownload(downloadUrl.value);
|
|
} catch (error) {
|
|
downloadResult.textContent = error.message;
|
|
logEvent("download:preview:error", { message: error.message, data: error.data || null });
|
|
}
|
|
});
|
|
|
|
confirmDownload.addEventListener("click", async () => {
|
|
if (!pendingDownload) {
|
|
return;
|
|
}
|
|
try {
|
|
const data = await api("/api/download", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
url: pendingDownload.url,
|
|
start: toClock(cropStart),
|
|
end: toClock(cropEnd),
|
|
quality: qualitySelect.value,
|
|
force: pendingDownload.force,
|
|
}),
|
|
});
|
|
closeModal();
|
|
downloadResult.textContent = data.message;
|
|
} catch (error) {
|
|
downloadResult.textContent = error.message;
|
|
logEvent("download:start:error", { message: error.message, data: error.data || null });
|
|
}
|
|
});
|
|
|
|
closePreviewModal.addEventListener("click", closeModal);
|
|
if (resultModalReady) {
|
|
closeResultModal.addEventListener("click", closeResultViewer);
|
|
resultModal.addEventListener("click", (event) => {
|
|
if (event.target === resultModal) {
|
|
closeResultViewer();
|
|
}
|
|
});
|
|
resultModalDownload.addEventListener("click", async () => {
|
|
if (!activeResultItem?.link) {
|
|
return;
|
|
}
|
|
const currentItem = activeResultItem;
|
|
if (currentItem.actionType === "download") {
|
|
try {
|
|
closeResultViewer();
|
|
await prepareDirectDownload(currentItem.link);
|
|
} catch (error) {
|
|
downloadResult.textContent = error.message;
|
|
logEvent("download:preview:error", { message: error.message, data: error.data || null, source: currentItem?.source || "" });
|
|
}
|
|
return;
|
|
}
|
|
window.open(currentItem.link, "_blank", "noopener,noreferrer");
|
|
});
|
|
resultModalSecondaryAction.addEventListener("click", () => {
|
|
if (!activeResultItem?.link) {
|
|
return;
|
|
}
|
|
window.open(activeResultItem.link, "_blank", "noopener,noreferrer");
|
|
});
|
|
}
|
|
previewModal.addEventListener("click", (event) => {
|
|
if (event.target === previewModal) {
|
|
closeModal();
|
|
}
|
|
});
|
|
setStartFromPreview.addEventListener("click", () => {
|
|
cropStart = Math.floor(previewVideo.currentTime || 0);
|
|
activeThumb = "start";
|
|
syncRanges();
|
|
});
|
|
setEndFromPreview.addEventListener("click", () => {
|
|
cropEnd = Math.floor(previewVideo.currentTime || 0);
|
|
activeThumb = "end";
|
|
syncRanges();
|
|
});
|
|
for (const [thumb, name] of [[startThumb, "start"], [endThumb, "end"]]) {
|
|
thumb.addEventListener("pointerdown", (event) => {
|
|
event.preventDefault();
|
|
activeThumb = name;
|
|
thumb.setPointerCapture(event.pointerId);
|
|
});
|
|
thumb.addEventListener("pointermove", (event) => {
|
|
if (activeThumb !== name || cropMax <= 0) {
|
|
return;
|
|
}
|
|
const track = thumb.parentElement.getBoundingClientRect();
|
|
const ratio = Math.max(0, Math.min(1, (event.clientX - track.left) / track.width));
|
|
const value = Math.round(ratio * cropMax);
|
|
if (name === "start") {
|
|
cropStart = value;
|
|
} else {
|
|
cropEnd = value;
|
|
}
|
|
syncRanges();
|
|
});
|
|
thumb.addEventListener("pointerup", () => {
|
|
activeThumb = null;
|
|
});
|
|
}
|
|
previewVideo.addEventListener("loadedmetadata", () => {
|
|
logEvent("preview:modal:loadedmetadata", {
|
|
width: previewVideo.videoWidth,
|
|
height: previewVideo.videoHeight,
|
|
src: previewVideo.currentSrc || previewVideo.src,
|
|
});
|
|
if (previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) {
|
|
previewMediaFrame.style.aspectRatio = `${previewVideo.videoWidth} / ${previewVideo.videoHeight}`;
|
|
}
|
|
});
|
|
previewThumbnail.addEventListener("load", () => {
|
|
logEvent("preview:thumbnail:loaded", {
|
|
width: previewThumbnail.naturalWidth,
|
|
height: previewThumbnail.naturalHeight,
|
|
src: previewThumbnail.currentSrc || previewThumbnail.src,
|
|
});
|
|
if (!previewVideo.src && previewThumbnail.naturalWidth > 0 && previewThumbnail.naturalHeight > 0) {
|
|
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
|
}
|
|
});
|
|
if (resultModalReady) {
|
|
resultModalVideo.addEventListener("loadedmetadata", () => {
|
|
if (resultModalVideo.videoWidth > 0 && resultModalVideo.videoHeight > 0) {
|
|
resultModalMediaFrame.style.aspectRatio = `${resultModalVideo.videoWidth} / ${resultModalVideo.videoHeight}`;
|
|
}
|
|
});
|
|
resultModalThumbnail.addEventListener("load", () => {
|
|
if (resultModalThumbnail.naturalWidth > 0 && resultModalThumbnail.naturalHeight > 0) {
|
|
resultModalMediaFrame.style.aspectRatio = `${resultModalThumbnail.naturalWidth} / ${resultModalThumbnail.naturalHeight}`;
|
|
}
|
|
});
|
|
}
|
|
for (const button of platformToggles) {
|
|
button.addEventListener("click", () => {
|
|
const platform = button.dataset.platformToggle;
|
|
if (activePlatforms.has(platform) && activePlatforms.size > 1) {
|
|
activePlatforms.delete(platform);
|
|
} else {
|
|
activePlatforms.add(platform);
|
|
}
|
|
syncPlatformButtons();
|
|
setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0);
|
|
});
|
|
}
|
|
|
|
debugToggle.addEventListener("click", () => {
|
|
debugPanel.classList.remove("hidden");
|
|
logEvent("debug:panel:open", {});
|
|
});
|
|
closeDebugPanel.addEventListener("click", () => {
|
|
debugPanel.classList.add("hidden");
|
|
});
|
|
clearLogs.addEventListener("click", () => {
|
|
debugEntries.length = 0;
|
|
renderLogs();
|
|
});
|
|
downloadLogs.addEventListener("click", () => {
|
|
const blob = new Blob(
|
|
[debugEntries.map((entry) => `[${entry.ts}] ${entry.type}\n${safeStringify(entry.payload)}\n`).join("\n")],
|
|
{ type: "text/plain;charset=utf-8" },
|
|
);
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = `ai-media-hub-${new Date().toISOString().replace(/[:.]/g, "-")}.log`;
|
|
anchor.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
window.addEventListener("error", (event) => {
|
|
logEvent("window:error", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno });
|
|
});
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
logEvent("window:unhandledrejection", { reason: String(event.reason) });
|
|
});
|
|
|
|
connectWS();
|
|
syncPlatformButtons();
|
|
setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0);
|
|
logEvent("app:ready", { activePlatforms: Array.from(activePlatforms) });
|