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"); let pendingDownload = null; let cropStart = 0; let cropEnd = 0; let cropMax = 0; let activeThumb = null; const activePlatforms = new Set(["envato", "artgrid", "google video"]); function setStatus(label, progress) { statusLabel.textContent = label; statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; } 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 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.innerHTML = ""; if (!queries.length) { queryVariants.classList.add("hidden"); return; } for (const item of queries) { const badge = document.createElement("span"); badge.className = "rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs uppercase tracking-[0.18em] text-zinc-300"; badge.textContent = item; queryVariants.appendChild(badge); } queryVariants.classList.remove("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); } } 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") { return; } 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", () => { setTimeout(connectWS, 1000); }); } async function api(path, options = {}) { const response = await fetch(path, options); const data = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(data.error || "request failed"); error.status = response.status; error.data = data; throw error; } return data; } function renderResults(results) { searchResults.innerHTML = ""; if (!results.length) { searchResults.innerHTML = `
No results matched the current search sources.
`; 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 overlays = node.querySelectorAll(".preview-overlay"); node.href = item.link; image.src = item.thumbnailUrl || "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview"; image.alt = item.title; node.querySelector("h3").textContent = item.title; node.querySelector("p").textContent = item.reason; node.querySelector(".source-badge").textContent = item.source; if (item.previewVideoUrl) { previewVideo.src = item.previewVideoUrl; previewVideo.poster = item.thumbnailUrl || ""; const mediaArea = node.querySelector(".relative"); mediaArea.addEventListener("mouseenter", () => { overlays.forEach((overlay) => overlay.classList.add("hidden")); previewVideo.classList.remove("hidden"); previewVideo.play().catch(() => {}); }); mediaArea.addEventListener("mouseleave", () => { previewVideo.pause(); previewVideo.currentTime = 0; previewVideo.classList.add("hidden"); overlays.forEach((overlay) => overlay.classList.remove("hidden")); }); } searchResults.appendChild(node); } } searchForm.addEventListener("submit", async (event) => { event.preventDefault(); setStatus("preparing search", 5); searchWarning.classList.add("hidden"); 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 || []); if (data.warning) { searchWarning.textContent = data.warning; searchWarning.classList.remove("hidden"); } setStatus("search complete", 100); } catch (error) { searchWarning.textContent = error.message; searchWarning.classList.remove("hidden"); 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 }); } catch (error) { uploadResult.textContent = error.message; } } function openPreviewModal(preview) { previewTitle.textContent = preview.title; previewThumbnail.src = preview.thumbnail; previewThumbnail.alt = preview.title; previewVideo.pause(); previewVideo.removeAttribute("src"); previewVideo.load(); previewMediaFrame.style.aspectRatio = ""; if (preview.previewStreamUrl) { previewVideo.src = preview.previewStreamUrl; previewVideo.classList.remove("hidden"); previewThumbnail.classList.add("hidden"); } else { previewVideo.classList.add("hidden"); previewThumbnail.classList.remove("hidden"); } 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(); previewModal.classList.remove("hidden"); previewModal.classList.add("flex"); } function closeModal() { previewVideo.pause(); previewVideo.removeAttribute("src"); previewVideo.load(); previewMediaFrame.style.aspectRatio = ""; previewModal.classList.add("hidden"); previewModal.classList.remove("flex"); 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(); downloadResult.textContent = "checking duplicate history..."; try { const dup = await api(`/api/history/check?url=${encodeURIComponent(downloadUrl.value)}`); let force = false; if (dup.exists) { force = window.confirm("동일 URL 다운로드 이력이 있습니다. 계속 진행할까요?"); if (!force) { downloadResult.textContent = "cancelled"; return; } } pendingDownload = { url: downloadUrl.value, force }; downloadResult.textContent = "loading preview..."; const preview = await api("/api/download/preview", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: downloadUrl.value }), }); openPreviewModal(preview); downloadResult.textContent = "preview loaded"; } catch (error) { downloadResult.textContent = error.message; } }); 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; } }); closePreviewModal.addEventListener("click", closeModal); 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", () => { if (previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) { previewMediaFrame.style.aspectRatio = `${previewVideo.videoWidth} / ${previewVideo.videoHeight}`; } }); previewThumbnail.addEventListener("load", () => { if (!previewVideo.src && previewThumbnail.naturalWidth > 0 && previewThumbnail.naturalHeight > 0) { previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.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(); }); } connectWS(); syncPlatformButtons(); setStatus("idle", 0);