Expand preview handling and search coverage
build-push / docker (push) Successful in 4m2s

This commit is contained in:
AI Assistant
2026-03-17 12:50:18 +09:00
parent 556d4d6c1b
commit 2fe1bd8ffe
5 changed files with 224 additions and 68 deletions
+76 -9
View File
@@ -86,6 +86,8 @@ const hlsInstances = new WeakMap();
const debugEntries = [];
const summaryTranslationCache = new Map();
const summaryTranslationInflight = new Map();
const resultPreviewCache = new Map();
const resultPreviewInflight = new Map();
let cardSummaryObserver = null;
const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
@@ -96,6 +98,25 @@ function proxiedPreviewURL(src) {
return `/api/preview/stream?url=${encodeURIComponent(src)}`;
}
function transcodedPreviewURL(src) {
if (!src) {
return "";
}
return `/api/preview/transcode?url=${encodeURIComponent(src)}`;
}
function buildPlayablePreviewURL(src, source = "") {
const trimmed = String(src || "").trim();
if (!trimmed) {
return "";
}
const lower = trimmed.toLowerCase();
if (lower.includes(".m3u8") && (String(source || "").toLowerCase() === "artgrid" || lower.includes("artgrid") || lower.includes("artlist"))) {
return transcodedPreviewURL(trimmed);
}
return proxiedPreviewURL(trimmed);
}
function isLowValueThumbnailURL(src) {
const lower = String(src || "").toLowerCase();
if (!lower) {
@@ -471,7 +492,7 @@ function showResultModalVideo(src) {
if (!src) {
return;
}
attachVideoSource(resultModalVideo, proxiedPreviewURL(src));
attachVideoSource(resultModalVideo, src);
setHidden(resultModalVideo, false, "");
}
@@ -555,6 +576,37 @@ async function translateCardSummary(node) {
}
}
async function fetchResultPreview(item) {
const key = String(item?.link || "").trim();
if (!key) {
return null;
}
if (resultPreviewCache.has(key)) {
return resultPreviewCache.get(key);
}
if (resultPreviewInflight.has(key)) {
return resultPreviewInflight.get(key);
}
const request = (async () => {
try {
const preview = await api("/api/download/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: key }),
});
resultPreviewCache.set(key, preview);
return preview;
} catch (error) {
logEvent("result:preview:fetch_failed", { link: key, source: item?.source || "", message: error.message });
return null;
} finally {
resultPreviewInflight.delete(key);
}
})();
resultPreviewInflight.set(key, request);
return request;
}
function ensureCardSummaryObserver() {
if (cardSummaryObserver || typeof IntersectionObserver === "undefined") {
return;
@@ -618,12 +670,20 @@ function renderResults(results) {
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 });
const mediaArea = node.querySelector(".relative");
if (item.previewVideoUrl || item.source === "Google Video") {
mediaArea.addEventListener("mouseenter", async () => {
let previewURL = item.previewVideoUrl || "";
if (!previewURL && item.source === "Google Video") {
const preview = await fetchResultPreview(item);
previewURL = preview?.previewStreamUrl || "";
}
logEvent("preview:hover:start", { title: item.title, source: item.source, previewVideoUrl: previewURL });
if (!previewURL) {
return;
}
overlays.forEach((overlay) => overlay.classList.add("hidden"));
startHoverPreview(previewVideo, proxiedPreviewURL(item.previewVideoUrl));
startHoverPreview(previewVideo, buildPlayablePreviewURL(previewURL, item.source));
});
mediaArea.addEventListener("mouseleave", () => {
logEvent("preview:hover:end", { title: item.title, source: item.source });
@@ -666,7 +726,7 @@ async function prepareDirectDownload(targetUrl) {
downloadResult.textContent = "preview loaded";
}
function openResultModal(item) {
async function openResultModal(item) {
if (!resultModalReady) {
logEvent("result:modal:error", { message: "result modal is not fully initialized" });
return;
@@ -688,7 +748,14 @@ function openResultModal(item) {
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") {
let resolvedPreviewURL = item.previewVideoUrl || "";
if (!resolvedPreviewURL && item.source === "Google Video") {
const preview = await fetchResultPreview(item);
resolvedPreviewURL = preview?.previewStreamUrl || "";
}
if (resolvedPreviewURL) {
showResultModalVideo(buildPlayablePreviewURL(resolvedPreviewURL, item.source));
} else 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);
@@ -703,7 +770,7 @@ function openResultModal(item) {
window.clearTimeout(timeout);
};
} else if (item.mediaMode === "preview_video" && item.previewVideoUrl) {
showResultModalVideo(item.previewVideoUrl);
showResultModalVideo(buildPlayablePreviewURL(item.previewVideoUrl, item.source));
} else if (item.mediaMode === "thumbnail" && hasUsableThumbnail(item.thumbnailUrl)) {
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
} else {
+9 -9
View File
@@ -151,7 +151,7 @@
<div id="resultModal" class="fixed inset-0 z-50 hidden items-start justify-center overflow-y-auto bg-black/80 px-2 py-2 sm:px-4 sm:py-4">
<div class="result-modal-shell flex w-full max-w-6xl min-h-0 flex-col overflow-hidden rounded-3xl border border-white/10 bg-zinc-950 shadow-2xl">
<div class="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div class="flex items-center justify-between border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
<div class="min-w-0">
<p id="resultModalSource" class="text-xs uppercase tracking-[0.25em] text-zinc-500"></p>
<h3 id="resultModalTitle" class="mt-1 truncate text-xl font-semibold text-white"></h3>
@@ -161,7 +161,7 @@
<button id="closeResultModal" type="button" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Close</button>
</div>
</div>
<div class="border-b border-white/10 bg-black/40 p-3 sm:p-4">
<div class="border-b border-white/10 bg-black/40 p-2 sm:p-3">
<div id="resultModalMediaFrame" class="result-modal-media-frame aspect-video overflow-hidden rounded-2xl border border-white/10 bg-black">
<iframe id="resultModalFrame" class="hidden h-full w-full bg-white" referrerpolicy="no-referrer" allow="autoplay; fullscreen; encrypted-media; picture-in-picture" allowfullscreen></iframe>
<video id="resultModalVideo" class="hidden h-full w-full bg-black object-contain" controls playsinline></video>
@@ -179,15 +179,15 @@
</div>
</div>
</div>
<div class="result-modal-details grid min-h-0 gap-4 px-4 py-4 sm:gap-5 sm:px-5 sm:py-5 lg:grid-cols-[1.6fr_0.8fr]">
<div class="flex min-h-[220px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-5">
<div class="result-modal-details grid min-h-0 gap-3 px-3 py-3 sm:gap-4 sm:px-4 sm:py-4 lg:grid-cols-[1.5fr_0.8fr]">
<div class="flex min-h-[180px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
<div class="result-panel-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
<p id="resultModalReason" class="whitespace-pre-wrap text-sm leading-7 text-zinc-200"></p>
<p id="resultModalReason" class="whitespace-pre-wrap text-xs leading-6 text-zinc-200 sm:text-sm"></p>
</div>
</div>
<div class="flex min-h-[240px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-5">
<div class="mb-4 flex flex-col gap-3">
<div class="flex min-h-[200px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div class="mb-3 flex flex-col gap-2">
<button id="resultModalDownload" type="button" class="hidden w-full rounded-2xl border border-white bg-white px-4 py-3 text-sm font-medium text-black transition hover:bg-zinc-200">
Primary Action
</button>
@@ -198,7 +198,7 @@
<div class="flex min-h-0 flex-1 flex-col">
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Source Summary</p>
<div class="result-summary-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
<p id="resultModalSnippet" class="text-sm leading-7 text-zinc-300"></p>
<p id="resultModalSnippet" class="text-xs leading-6 text-zinc-300 sm:text-sm"></p>
</div>
</div>
</div>
@@ -224,6 +224,6 @@
</button>
</template>
<script src="/app.js?v=20260317b" defer></script>
<script src="/app.js?v=20260317c" defer></script>
</body>
</html>
+5 -5
View File
@@ -60,7 +60,7 @@ body {
}
.result-modal-shell {
height: min(calc(100dvh - 1rem), 920px);
height: min(calc(100dvh - 0.5rem), 860px);
margin: auto;
}
@@ -69,7 +69,7 @@ body {
}
.result-modal-media-frame {
max-height: min(42dvh, 28rem);
max-height: min(34dvh, 22rem);
}
.result-modal-details {
@@ -110,16 +110,16 @@ body {
@media (max-height: 900px) {
.result-modal-media-frame {
max-height: min(34dvh, 22rem);
max-height: min(28dvh, 18rem);
}
}
@media (max-height: 720px) {
.result-modal-shell {
height: min(calc(100dvh - 0.5rem), 780px);
height: min(calc(100dvh - 0.25rem), 700px);
}
.result-modal-media-frame {
max-height: min(28dvh, 15rem);
max-height: min(22dvh, 11rem);
}
}