318 lines
11 KiB
JavaScript
318 lines
11 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 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 startRange = document.getElementById("startRange");
|
|
const endRange = document.getElementById("endRange");
|
|
const rangeSummary = document.getElementById("rangeSummary");
|
|
const setStartFromPreview = document.getElementById("setStartFromPreview");
|
|
const setEndFromPreview = document.getElementById("setEndFromPreview");
|
|
|
|
let pendingDownload = null;
|
|
|
|
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 = Number(startRange.value || 0);
|
|
let end = Number(endRange.value || 0);
|
|
if (start > end) {
|
|
if (document.activeElement === startRange) {
|
|
end = start;
|
|
endRange.value = String(end);
|
|
} else {
|
|
start = end;
|
|
startRange.value = String(start);
|
|
}
|
|
}
|
|
rangeSummary.textContent = `${toClock(start)} - ${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 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 = `<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);
|
|
node.href = item.link;
|
|
node.querySelector("img").src = item.thumbnailUrl || "https://placehold.co/1280x720/0a0a0a/ffffff?text=No+Preview";
|
|
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("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 }),
|
|
});
|
|
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);
|
|
}
|
|
const maxDuration = Number(preview.durationSeconds || 0);
|
|
startRange.max = String(maxDuration);
|
|
endRange.max = String(maxDuration);
|
|
startRange.value = "0";
|
|
endRange.value = String(maxDuration);
|
|
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");
|
|
startRange.value = "0";
|
|
endRange.value = "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(startRange.value),
|
|
end: toClock(endRange.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();
|
|
}
|
|
});
|
|
startRange.addEventListener("input", syncRanges);
|
|
endRange.addEventListener("input", syncRanges);
|
|
setStartFromPreview.addEventListener("click", () => {
|
|
startRange.value = String(Math.floor(previewVideo.currentTime || 0));
|
|
syncRanges();
|
|
});
|
|
setEndFromPreview.addEventListener("click", () => {
|
|
endRange.value = String(Math.floor(previewVideo.currentTime || 0));
|
|
syncRanges();
|
|
});
|
|
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}`;
|
|
}
|
|
});
|
|
|
|
connectWS();
|
|
setStatus("idle", 0);
|