Files
ai-media-hub/frontend/app.js
AI Assistant e136650790
Some checks failed
build-push / docker (push) Has been cancelled
Add download preview flow and search fallback
2026-03-12 16:01:19 +09:00

224 lines
7.3 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 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);