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 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 startTime = document.getElementById("startTime"); const endTime = document.getElementById("endTime"); const downloadResult = document.getElementById("downloadResult"); const cardTemplate = document.getElementById("searchCardTemplate"); const previewModal = document.getElementById("previewModal"); const previewTitle = document.getElementById("previewTitle"); 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"); let pendingDownload = null; function setStatus(label, progress) { statusLabel.textContent = label; statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; } 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; setStatus(`${data.type || "task"}: ${data.status}`, 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 = ""; for (const item of results) { const node = cardTemplate.content.firstElementChild.cloneNode(true); node.href = item.link; node.querySelector("img").src = item.thumbnailUrl; node.querySelector("img").alt = item.title; node.querySelector("h3").textContent = item.title; node.querySelector("p").textContent = item.reason; node.querySelector(".source-badge").textContent = item.source; searchResults.appendChild(node); } } searchForm.addEventListener("submit", async (event) => { event.preventDefault(); setStatus("searching", 20); searchWarning.classList.add("hidden"); try { const data = await api("/api/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: searchQuery.value }), }); renderResults(data.results || []); 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"); 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; 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); } startTime.value = preview.startDefault || "00:00:00"; endTime.value = preview.endDefault || "00:00:00"; previewModal.classList.remove("hidden"); previewModal.classList.add("flex"); } function closeModal() { previewModal.classList.add("hidden"); previewModal.classList.remove("flex"); 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: startTime.value, end: endTime.value, 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(); } }); connectWS(); setStatus("idle", 0);