// App State let uploadFileId = null; let uploadMetadata = null; let currentJobId = null; let socket = null; let sliderInitialized = false; // Error formatting helper function getDownloadUrl(filePath) { if (!filePath) return ""; if (filePath.startsWith("http://") || filePath.startsWith("https://")) return filePath; if (filePath.startsWith("/")) { const filename = filePath.split("/").pop(); return `/api/download/${encodeURIComponent(filename)}?path=${encodeURIComponent(filePath)}`; } return `/api/download/${encodeURIComponent(filePath)}`; } function formatFetchError(err, defaultMsg) { if (!err) return defaultMsg; if (typeof err === "string") return err; if (err.detail) { if (typeof err.detail === "string") { return err.detail; } if (Array.isArray(err.detail)) { return err.detail.map(d => { const locStr = (d.loc && Array.isArray(d.loc)) ? d.loc.join(".") : ""; return locStr ? `${locStr}: ${d.msg}` : (d.msg || JSON.stringify(d)); }).join("\n"); } if (typeof err.detail === "object") { return err.detail.message || err.detail.error || JSON.stringify(err.detail); } } if (err.message) return err.message; return JSON.stringify(err) || defaultMsg; } // DOM Elements const stepUpload = document.getElementById("step-upload"); const stepConfig = document.getElementById("step-config"); const stepProgress = document.getElementById("step-progress"); const stepFinished = document.getElementById("step-finished"); const dropZone = document.getElementById("drop-zone"); const fileInput = document.getElementById("file-input"); // Meta fields const metaName = document.getElementById("meta-name"); const metaSize = document.getElementById("meta-size"); const metaRes = document.getElementById("meta-res"); const metaDuration = document.getElementById("meta-duration"); const metaFps = document.getElementById("meta-fps"); // Config inputs const modelSelect = document.getElementById("model-select"); const scaleRange = document.getElementById("scale-range"); const scaleValue = document.getElementById("scale-value"); const tileRange = document.getElementById("tile-range"); const tileValue = document.getElementById("tile-value"); const audioToggle = document.getElementById("audio-toggle"); // Timeline and Preview elements const timelineSlider = document.getElementById("timeline-slider"); const timelineTime = document.getElementById("timeline-time"); const generatePreviewBtn = document.getElementById("generate-preview-btn"); const generateVideoPreviewBtn = document.getElementById("generate-video-preview-btn"); const previewPlaceholderBox = document.getElementById("preview-placeholder-box"); const comparisonSliderContainer = document.getElementById("comparison-slider-container"); const previewLoader = document.getElementById("preview-loader"); const imgOriginal = document.getElementById("img-original"); const imgUpscaled = document.getElementById("img-upscaled"); const sliderSplit = document.getElementById("slider-split"); // Progress elements const jobIdDisplay = document.getElementById("job-id-display"); const statusBadge = document.getElementById("status-badge"); const progressPercent = document.getElementById("progress-percent"); const progressBarFill = document.getElementById("progress-bar-fill"); const statStatus = document.getElementById("stat-status"); const statFrames = document.getElementById("stat-frames"); const statEta = document.getElementById("stat-eta"); const logTerminal = document.getElementById("log-terminal"); const cancelUpscaleBtn = document.getElementById("cancel-upscale-btn"); const startUpscaleBtn = document.getElementById("start-upscale-btn"); // Finished elements const downloadLink = document.getElementById("download-link"); const finishedPlayComparisonBtn = document.getElementById("finished-play-comparison-btn"); // Synced Playback Modal Elements const videoModal = document.getElementById("video-modal"); const modalVideoOriginal = document.getElementById("modal-video-original"); const modalVideoUpscaled = document.getElementById("modal-video-upscaled"); const modalPlayBtn = document.getElementById("modal-play-btn"); const modalTimeSlider = document.getElementById("modal-time-slider"); const modalTimeCurrent = document.getElementById("modal-time-current"); const modalTimeDuration = document.getElementById("modal-time-duration"); const modalMuteBtn = document.getElementById("modal-mute-btn"); const modalCloseBtn = document.getElementById("modal-close-btn"); /* ---------------------------------------------------- 1. Theme Selector ---------------------------------------------------- */ const themeSelect = document.getElementById("theme-select"); function applyTheme(theme) { document.body.className = ""; if (theme && theme !== "cyberpunk") { document.body.classList.add(`theme-${theme}`); } else { document.body.classList.add("theme-cyberpunk"); } localStorage.setItem("selected-theme", theme); if (themeSelect) themeSelect.value = theme; } if (themeSelect) { themeSelect.addEventListener("change", (e) => { applyTheme(e.target.value); }); } applyTheme(localStorage.getItem("selected-theme") || "cyberpunk"); /* ---------------------------------------------------- 2. Dynamic Sliders & Parameter Listeners ---------------------------------------------------- */ scaleRange.addEventListener("input", (e) => { scaleValue.textContent = `${e.target.value}x`; // Lock model scaling if needed: Real-ESRGAN x4 models are scale 4. Real-ESRGAN-animevideov3 supports 2,3,4. const model = modelSelect.value; if (model.includes("x4plus") && e.target.value !== "4") { scaleRange.value = 4; scaleValue.textContent = "4x (Locked)"; } }); modelSelect.addEventListener("change", (e) => { const val = e.target.value; if (val.includes("x4plus")) { scaleRange.value = 4; scaleRange.disabled = true; scaleValue.textContent = "4x (Fixed)"; } else { scaleRange.disabled = false; scaleValue.textContent = `${scaleRange.value}x`; } }); tileRange.addEventListener("input", (e) => { tileValue.textContent = e.target.value === "0" ? "Auto" : e.target.value; }); const crfRange = document.getElementById("crf-range"); const crfValue = document.getElementById("crf-value"); if (crfRange && crfValue) { crfRange.addEventListener("input", (e) => { let label = e.target.value; if (e.target.value === "0") label += " (Lossless)"; else if (e.target.value === "18") label += " (Pristine)"; else if (e.target.value > "28") label += " (Compress)"; crfValue.textContent = label; }); } /* ---------------------------------------------------- 3. Custom Model Upload Action ---------------------------------------------------- */ const customModelInput = document.getElementById("custom-model-input"); const modelUploadFilename = document.getElementById("model-upload-filename"); const uploadModelBtnWrapper = document.getElementById("upload-model-btn-wrapper"); const uploadModelBtn = document.getElementById("upload-model-btn"); if (customModelInput) { customModelInput.addEventListener("change", (e) => { const files = e.target.files; if (files.length > 0) { const names = Array.from(files).map(f => f.name).join(", "); modelUploadFilename.textContent = names; uploadModelBtnWrapper.style.display = "block"; } else { modelUploadFilename.textContent = "No files selected"; uploadModelBtnWrapper.style.display = "none"; } }); } if (uploadModelBtn) { uploadModelBtn.addEventListener("click", async () => { const files = customModelInput.files; if (files.length === 0) return; uploadModelBtn.disabled = true; uploadModelBtn.innerHTML = `
Uploading...`; const formData = new FormData(); for (let i = 0; i < files.length; i++) { const ext = files[i].name.split('.').pop().toLowerCase(); if (ext === 'param') { formData.append("param_file", files[i]); } else if (ext === 'bin') { formData.append("bin_file", files[i]); } else { formData.append("files", files[i]); } } try { const res = await fetch("/api/models/upload", { method: "POST", body: formData }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Model upload failed.")); } alert("Custom model uploaded successfully!"); modelUploadFilename.textContent = "Uploaded successfully!"; uploadModelBtnWrapper.style.display = "none"; await loadModels(); } catch (err) { alert(err.message); } finally { uploadModelBtn.disabled = false; uploadModelBtn.innerHTML = ` Upload Model Files`; } }); } /* ---------------------------------------------------- 4. Video File Upload Routing ---------------------------------------------------- */ ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }, false); }); dropZone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files.length > 1) { handleBatchUpload(files); } else if (files.length === 1) { handleVideoUpload(files[0]); } }); fileInput.addEventListener('change', (e) => { const files = e.target.files; if (files.length > 1) { handleBatchUpload(files); } else if (files.length === 1) { handleVideoUpload(files[0]); } }); function resetDropZone() { dropZone.innerHTML = `

Drag & Drop Video File(s)

Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)

`; } async function handleBatchUpload(files) { const totalFiles = files.length; let uploadedCount = 0; dropZone.innerHTML = `

Uploading 1 of ${totalFiles} Files...

Preparing batch upload...

`; const progressText = document.getElementById("upload-progress-text"); const progressBar = document.getElementById("upload-bar"); const currentText = document.getElementById("batch-upload-current"); for (let i = 0; i < totalFiles; i++) { const file = files[i]; if (currentText) currentText.textContent = i + 1; if (progressText) progressText.textContent = `Uploading "${file.name}"...`; if (progressBar) progressBar.style.width = "0%"; try { await new Promise((resolve, reject) => { const formData = new FormData(); formData.append("file", file); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload", true); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; if (progressText) { progressText.textContent = `Uploading "${file.name}": ${formatBytes(e.loaded)} / ${formatBytes(e.total)} (${Math.round(percentComplete)}%)`; } if (progressBar) progressBar.style.width = `${percentComplete}%`; } }); xhr.onload = function() { if (xhr.status === 200) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(`Upload failed for ${file.name}`)); } }; xhr.onerror = function() { reject(new Error(`Network error uploading ${file.name}`)); }; xhr.send(formData); }); uploadedCount++; } catch (err) { console.error(err); alert(`Error uploading "${file.name}": ${err.message}`); } } await loadUploadsBrowser(); resetDropZone(); } function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } function handleVideoUpload(file) { dropZone.innerHTML = `

Uploading Video File...

Preparing upload...

`; const formData = new FormData(); formData.append("file", file); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/upload", true); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; const progressText = document.getElementById("upload-progress-text"); const progressBar = document.getElementById("upload-bar"); if (progressText) progressText.textContent = `Uploaded ${formatBytes(e.loaded)} / ${formatBytes(e.total)} (${Math.round(percentComplete)}%)`; if (progressBar) progressBar.style.width = `${percentComplete}%`; } }); xhr.onload = function() { if (xhr.status === 200) { const res = JSON.parse(xhr.responseText); uploadFileId = res.file_id; uploadMetadata = res.metadata; // Populate Config Step Info metaName.textContent = res.filename; metaSize.textContent = formatBytes(file.size); // Populate Accordions Stream details const streams = res.metadata.streams || []; const videoStream = streams.find(s => s.codec_type === "video") || {}; const audioStream = streams.find(s => s.codec_type === "audio") || {}; // Video stream metadata document.getElementById("meta-res-codec").textContent = (videoStream.codec_name || "unknown").toUpperCase(); document.getElementById("meta-res").textContent = `${res.metadata.width} x ${res.metadata.height}`; document.getElementById("meta-fps").textContent = `${res.metadata.fps} FPS`; document.getElementById("meta-duration").textContent = `${res.metadata.duration}s`; document.getElementById("meta-total-frames").textContent = res.metadata.total_frames || "-"; document.getElementById("meta-aspect-ratio").textContent = videoStream.display_aspect_ratio || "N/A"; // Audio stream metadata if (audioStream.codec_name) { document.getElementById("meta-audio-codec").textContent = (audioStream.codec_name || "unknown").toUpperCase(); document.getElementById("meta-audio-channels").textContent = audioStream.channels || "N/A"; document.getElementById("meta-audio-samplerate").textContent = audioStream.sample_rate ? `${audioStream.sample_rate} Hz` : "N/A"; document.getElementById("meta-audio-bitrate").textContent = audioStream.bit_rate ? formatBytes(parseInt(audioStream.bit_rate)) + "/s" : "N/A"; } else { document.getElementById("meta-audio-codec").textContent = "No Audio Stream"; document.getElementById("meta-audio-channels").textContent = "-"; document.getElementById("meta-audio-samplerate").textContent = "-"; document.getElementById("meta-audio-bitrate").textContent = "-"; } // Configure timeline slider constraints timelineSlider.min = 0.1; timelineSlider.max = res.metadata.duration - 0.5; timelineSlider.value = (res.metadata.duration * 0.15).toFixed(2); timelineTime.textContent = `${timelineSlider.value}s`; // Configure Trimming panel range values const trimStartRange = document.getElementById("trim-start-range"); const trimEndRange = document.getElementById("trim-end-range"); const trimStartValue = document.getElementById("trim-start-value"); const trimEndValue = document.getElementById("trim-end-value"); trimStartRange.min = 0; trimStartRange.max = res.metadata.duration; trimStartRange.value = 0; trimStartValue.textContent = "0.00s"; trimEndRange.min = 0; trimEndRange.max = res.metadata.duration; trimEndRange.value = res.metadata.duration; trimEndValue.textContent = `${res.metadata.duration.toFixed(2)}s`; // Show config view stepUpload.classList.remove("active"); stepConfig.classList.add("active"); loadUploadsBrowser(); } else { alert("Upload failed: " + (JSON.parse(xhr.responseText).detail || xhr.statusText)); window.location.reload(); } }; xhr.onerror = function() { alert("Upload error occurred."); window.location.reload(); }; xhr.send(formData); } // Timeline slider change timelineSlider.addEventListener("input", (e) => { timelineTime.textContent = `${parseFloat(e.target.value).toFixed(2)}s`; }); // Trimming sliders inputs listeners const trimStartRange = document.getElementById("trim-start-range"); const trimEndRange = document.getElementById("trim-end-range"); const trimStartValue = document.getElementById("trim-start-value"); const trimEndValue = document.getElementById("trim-end-value"); trimStartRange.addEventListener("input", (e) => { let val = parseFloat(e.target.value); const endVal = parseFloat(trimEndRange.value); if (val >= endVal) { val = endVal - 0.05; trimStartRange.value = val; } trimStartValue.textContent = `${val.toFixed(2)}s`; }); trimEndRange.addEventListener("input", (e) => { let val = parseFloat(e.target.value); const startVal = parseFloat(trimStartRange.value); if (val <= startVal) { val = startVal + 0.05; trimEndRange.value = val; } trimEndValue.textContent = `${val.toFixed(2)}s`; }); /* ---------------------------------------------------- 5. Single Frame Preview Comparisons & Synced Zoom/Pan ---------------------------------------------------- */ const zoomSlider = document.getElementById("zoom-slider"); const zoomVal = document.getElementById("zoom-val"); const panXSlider = document.getElementById("pan-x-slider"); const panXVal = document.getElementById("pan-x-val"); const panYSlider = document.getElementById("pan-y-slider"); const panYVal = document.getElementById("pan-y-val"); const resetZoomPanBtn = document.getElementById("reset-zoom-pan-btn"); let previewZoom = 1.0; let previewPanX = 0; let previewPanY = 0; function updatePreviewTransform() { const transformStr = `translate(${previewPanX}px, ${previewPanY}px) scale(${previewZoom})`; imgOriginal.style.transform = transformStr; imgOriginal.style.transformOrigin = "center center"; imgOriginal.style.transition = "transform 0.1s ease-out"; imgUpscaled.style.transform = transformStr; imgUpscaled.style.transformOrigin = "center center"; imgUpscaled.style.transition = "transform 0.1s ease-out"; const vidOriginal = document.getElementById("video-preview-original"); const vidUpscaled = document.getElementById("video-preview-upscaled"); if (vidOriginal) { vidOriginal.style.transform = transformStr; vidOriginal.style.transformOrigin = "center center"; vidOriginal.style.transition = "transform 0.1s ease-out"; } if (vidUpscaled) { vidUpscaled.style.transform = transformStr; vidUpscaled.style.transformOrigin = "center center"; vidUpscaled.style.transition = "transform 0.1s ease-out"; } zoomSlider.value = previewZoom; zoomVal.textContent = `${previewZoom.toFixed(1)}x`; panXSlider.value = previewPanX; panXVal.textContent = `${previewPanX}px`; panYSlider.value = previewPanY; panYVal.textContent = `${previewPanY}px`; } zoomSlider.addEventListener("input", (e) => { previewZoom = parseFloat(e.target.value); updatePreviewTransform(); }); panXSlider.addEventListener("input", (e) => { previewPanX = parseInt(e.target.value); updatePreviewTransform(); }); panYSlider.addEventListener("input", (e) => { previewPanY = parseInt(e.target.value); updatePreviewTransform(); }); resetZoomPanBtn.addEventListener("click", () => { previewZoom = 1.0; previewPanX = 0; previewPanY = 0; updatePreviewTransform(); }); // Dragging to pan comparison image let isDraggingPreview = false; let startDragX = 0; let startDragY = 0; comparisonSliderContainer.addEventListener("mousedown", (e) => { if (e.target.id === "slider-split" || e.target.classList.contains("slider-handle")) { return; } isDraggingPreview = true; startDragX = e.clientX - previewPanX; startDragY = e.clientY - previewPanY; comparisonSliderContainer.style.cursor = "grabbing"; }); window.addEventListener("mouseup", () => { if (isDraggingPreview) { isDraggingPreview = false; comparisonSliderContainer.style.cursor = "default"; } }); window.addEventListener("mousemove", (e) => { if (!isDraggingPreview) return; previewPanX = e.clientX - startDragX; previewPanY = e.clientY - startDragY; if (previewPanX < -400) previewPanX = -400; if (previewPanX > 400) previewPanX = 400; if (previewPanY < -400) previewPanY = -400; if (previewPanY > 400) previewPanY = 400; updatePreviewTransform(); }); // Generate single frame preview trigger generatePreviewBtn.addEventListener("click", async () => { if (!uploadFileId && !window.selectedServerFilePath) return; const loaderTitle = document.getElementById("preview-loader-title"); const loaderContainer = document.getElementById("preview-loader-progress-container"); const loaderText = document.getElementById("preview-loader-text"); previewLoader.style.display = "flex"; previewPlaceholderBox.style.display = "none"; comparisonSliderContainer.style.display = "none"; document.getElementById("zoom-pan-controls").style.display = "none"; document.getElementById("video-preview-controls").style.display = "none"; if (loaderTitle) loaderTitle.textContent = "Orchestrating Vulkan GPU..."; if (loaderContainer) loaderContainer.style.display = "none"; if (loaderText) loaderText.textContent = "Extracting and upscaling frame. Please wait..."; const gpuSelect = document.getElementById("gpu-select"); try { const res = await fetch("/api/preview/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_id: uploadFileId || null, server_file_path: window.selectedServerFilePath || null, timestamp_sec: parseFloat(timelineSlider.value), model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), gpu_ids: gpuSelect.value === 'auto' ? null : gpuSelect.value }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Frame preview rendering failed.")); } const data = await res.json(); const previewId = data.preview_id; imgOriginal.src = `/api/preview/original/${previewId}?t=${Date.now()}`; imgUpscaled.src = `/api/preview/upscaled/${previewId}?t=${Date.now()}`; await Promise.all([ new Promise(resolve => imgOriginal.onload = resolve), new Promise(resolve => imgUpscaled.onload = resolve) ]); // Hide videos, show images imgOriginal.style.display = "block"; document.getElementById("img-upscaled-wrapper").style.display = "block"; document.getElementById("video-preview-original").style.display = "none"; document.getElementById("video-preview-upscaled-wrapper").style.display = "none"; previewPlaceholderBox.style.display = "none"; comparisonSliderContainer.style.display = "block"; document.getElementById("zoom-pan-controls").style.display = "flex"; initComparisonSlider(); // Reset transforms previewZoom = 1.0; previewPanX = 0; previewPanY = 0; updatePreviewTransform(); } catch (err) { alert(err.message); } finally { previewLoader.style.display = "none"; } }); generateVideoPreviewBtn.addEventListener("click", async () => { if (!uploadFileId) return; const loaderTitle = document.getElementById("preview-loader-title"); const loaderContainer = document.getElementById("preview-loader-progress-container"); const loaderBar = document.getElementById("preview-loader-progress-bar"); const loaderText = document.getElementById("preview-loader-text"); previewLoader.style.display = "flex"; previewPlaceholderBox.style.display = "none"; comparisonSliderContainer.style.display = "none"; document.getElementById("zoom-pan-controls").style.display = "none"; document.getElementById("video-preview-controls").style.display = "none"; if (loaderTitle) loaderTitle.textContent = "Generating Video Preview"; if (loaderContainer) loaderContainer.style.display = "block"; if (loaderBar) loaderBar.style.width = "0%"; if (loaderText) loaderText.textContent = "Queuing preview job in pipeline..."; const gpuSelect = document.getElementById("gpu-select"); const crfRange = document.getElementById("crf-range"); const presetSelect = document.getElementById("preset-select"); const transcodeSelect = document.getElementById("transcode-select"); const filterDenoise = document.getElementById("filter-denoise"); const filterSharpen = document.getElementById("filter-sharpen"); const filterFps = document.getElementById("filter-fps"); const aiFaceRestoration = document.getElementById("ai-face-restoration"); const aiRifeInterpolation = document.getElementById("ai-rife-interpolation"); const aiAudioDenoise = document.getElementById("ai-audio-denoise"); const transcodeFormat = transcodeSelect.value; try { const res = await fetch("/api/upscale/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_id: uploadFileId, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, start_sec: parseFloat(timelineSlider.value), end_sec: parseFloat(timelineSlider.value) + 5.0, gpu_ids: gpuSelect.value === 'auto' ? null : gpuSelect.value, tta: false, unsharp: false, double_fps: false, preserve_subtitles: false, crf: parseInt(crfRange.value), preset: presetSelect.value, denoise: filterDenoise.checked, sharpen: filterSharpen.checked, interpolation: filterFps.checked, webhook_url: null, transcode_format: transcodeFormat, is_preview: true, ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false, ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false, ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Failed to start video preview.")); } const data = await res.json(); currentJobId = data.job_id; connectProgressWebSocket(currentJobId); } catch (err) { alert(err.message); previewLoader.style.display = "none"; previewPlaceholderBox.style.display = "flex"; } }); function initComparisonSlider() { const upscaledWrapper = document.getElementById("img-upscaled-wrapper"); const videoUpscaledWrapper = document.getElementById("video-preview-upscaled-wrapper"); // Reset split positions to 50% if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; sliderSplit.style.left = `50%`; if (sliderInitialized) return; sliderInitialized = true; let active = false; function setSliderPosition(x) { const rect = comparisonSliderContainer.getBoundingClientRect(); let posX = x - rect.left; if (posX < 0) posX = 0; if (posX > rect.width) posX = rect.width; const pct = (posX / rect.width) * 100; if (upscaledWrapper) upscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`; if (videoUpscaledWrapper) videoUpscaledWrapper.style.clipPath = `inset(0 0 0 ${pct}%)`; sliderSplit.style.left = `${pct}%`; } sliderSplit.addEventListener('mousedown', (e) => { e.stopPropagation(); active = true; }); window.addEventListener('mouseup', () => { active = false; }); window.addEventListener('mousemove', (e) => { if (!active) return; setSliderPosition(e.pageX); }); sliderSplit.addEventListener('touchstart', (e) => { e.stopPropagation(); active = true; }); window.addEventListener('touchend', () => { active = false; }); window.addEventListener('touchmove', (e) => { if (!active) return; setSliderPosition(e.touches[0].pageX); }); comparisonSliderContainer.addEventListener('dblclick', () => { upscaledWrapper.style.clipPath = `inset(0 0 0 50%)`; sliderSplit.style.left = `50%`; }); } /* ---------------------------------------------------- 6. Video Pipeline Jobs Orchestration ---------------------------------------------------- */ startUpscaleBtn.addEventListener("click", async () => { if (!uploadFileId && !window.selectedServerFilePath && (!window.selectedServerFilePaths || window.selectedServerFilePaths.length === 0)) return; startUpscaleBtn.disabled = true; startUpscaleBtn.innerHTML = `
Starting Job...`; const gpuSelect = document.getElementById("gpu-select"); const crfRange = document.getElementById("crf-range"); const presetSelect = document.getElementById("preset-select"); const transcodeSelect = document.getElementById("transcode-select"); const filterDenoise = document.getElementById("filter-denoise"); const filterSharpen = document.getElementById("filter-sharpen"); const filterFps = document.getElementById("filter-fps"); const webhookUrl = document.getElementById("webhook-url"); const tempDirInput = document.getElementById("temp-dir-input"); const aiFaceRestoration = document.getElementById("ai-face-restoration"); const aiRifeInterpolation = document.getElementById("ai-rife-interpolation"); const aiAudioDenoise = document.getElementById("ai-audio-denoise"); try { if (window.isBatchMode && ((window.batchFileIds && window.batchFileIds.length > 0) || (window.selectedServerFilePaths && window.selectedServerFilePaths.length > 0))) { const res = await fetch("/api/upscale/start/batch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_ids: (window.batchFileIds && window.batchFileIds.length > 0) ? window.batchFileIds : null, server_file_paths: (window.selectedServerFilePaths && window.selectedServerFilePaths.length > 0) ? window.selectedServerFilePaths : null, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, // Advanced fields (disabled for batch) start_sec: null, end_sec: null, gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value, crf: parseInt(crfRange.value), preset: presetSelect.value, transcode_format: transcodeSelect.value, denoise: filterDenoise.checked, sharpen: filterSharpen.checked, interpolation: filterFps.checked, webhook_url: webhookUrl.value.trim() || null, ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false, ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false, ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false, temp_dir: (tempDirInput && tempDirInput.value.trim()) || null }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Orchestrator failed to launch batch jobs.")); } window.isBatchMode = false; window.batchFileIds = []; window.selectedServerFilePaths = null; document.querySelectorAll(".batch-select-checkbox").forEach(cb => cb.checked = false); const batchConfigBar = document.getElementById("batch-config-bar"); if (batchConfigBar) batchConfigBar.style.display = "none"; document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active")); stepUpload.classList.add("active"); alert("Batch jobs successfully queued!"); loadQueue(); startUpscaleBtn.disabled = false; startUpscaleBtn.innerHTML = ` Start Full Upscale`; } else { const res = await fetch("/api/upscale/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_id: uploadFileId || null, server_file_path: window.selectedServerFilePath || null, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), preserve_audio: audioToggle.checked, // Advanced fields start_sec: parseFloat(trimStartRange.value), end_sec: parseFloat(trimEndRange.value), gpu_ids: gpuSelect.value === "auto" ? null : gpuSelect.value, crf: parseInt(crfRange.value), preset: presetSelect.value, transcode_format: transcodeSelect.value, denoise: filterDenoise.checked, sharpen: filterSharpen.checked, interpolation: filterFps.checked, webhook_url: webhookUrl.value.trim() || null, ai_face_restoration: aiFaceRestoration ? aiFaceRestoration.checked : false, ai_rife_interpolation: aiRifeInterpolation ? aiRifeInterpolation.checked : false, ai_audio_denoise: aiAudioDenoise ? aiAudioDenoise.checked : false, temp_dir: (tempDirInput && tempDirInput.value.trim()) || null }) }); if (!res.ok) { const err = await res.json(); throw new Error(formatFetchError(err, "Orchestrator failed to launch job.")); } const data = await res.json(); currentJobId = data.job_id; stepConfig.classList.remove("active"); stepProgress.classList.add("active"); jobIdDisplay.textContent = currentJobId; connectProgressWebSocket(currentJobId); } } catch (err) { alert(err.message); startUpscaleBtn.disabled = false; startUpscaleBtn.innerHTML = ` Start Full Upscale`; } }); function connectProgressWebSocket(jobId) { const loc = window.location; const protocol = loc.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${loc.host}/ws/progress/${jobId}`; socket = new WebSocket(wsUrl); logTerminal.innerHTML = `
Connected to backend job orchestrator...
`; socket.onmessage = function(event) { const data = JSON.parse(event.data); updateProgressUI(data); }; socket.onclose = function() { console.log("WebSocket progress connection closed"); }; socket.onerror = function(err) { console.error("WebSocket error:", err); startFallbackPolling(jobId); }; } function appendLogLine(text, type = "info") { const line = document.createElement("div"); line.className = `log-line ${type}`; line.textContent = `[${new Date().toLocaleTimeString()}] ${text}`; logTerminal.appendChild(line); logTerminal.scrollTop = logTerminal.scrollHeight; } function updateProgressUI(data) { if (data.is_preview) { // Update inline loader inside the preview panel const loaderTitle = document.getElementById("preview-loader-title"); const loaderContainer = document.getElementById("preview-loader-progress-container"); const loaderBar = document.getElementById("preview-loader-progress-bar"); const loaderText = document.getElementById("preview-loader-text"); if (previewLoader.style.display !== "flex") { previewLoader.style.display = "flex"; previewPlaceholderBox.style.display = "none"; comparisonSliderContainer.style.display = "none"; document.getElementById("zoom-pan-controls").style.display = "none"; document.getElementById("video-preview-controls").style.display = "none"; } if (loaderTitle) loaderTitle.textContent = "Generating Video Preview"; if (loaderContainer) loaderContainer.style.display = "block"; if (loaderBar) loaderBar.style.width = `${Math.round(data.progress || 0)}%`; let statusText = `Status: ${data.status}`; if (data.status === "upscaling" && data.total_frames > 0) { statusText = `Upscaling frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})`; } else if (data.status === "queued") { statusText = "Queued in pipeline. Waiting for GPU lock..."; } else if (data.status === "extracting") { statusText = "Extracting video frames..."; } else if (data.status === "restoring_faces") { statusText = "GFPGAN: Running Face Restoration on upscaled frames..."; } else if (data.status === "interpolating") { statusText = "RIFE: Interpolating frames for smooth motion..."; } else if (data.status === "assembling") { statusText = "Assembling video preview and audio tracks..."; } if (loaderText) loaderText.textContent = statusText; if (data.status === "completed") { if (socket) socket.close(); // Hide loader previewLoader.style.display = "none"; // Show comparison elements previewPlaceholderBox.style.display = "none"; comparisonSliderContainer.style.display = "block"; document.getElementById("zoom-pan-controls").style.display = "flex"; document.getElementById("video-preview-controls").style.display = "flex"; // Toggle elements visibility: hide images, show videos imgOriginal.style.display = "none"; document.getElementById("img-upscaled-wrapper").style.display = "none"; const vidOriginal = document.getElementById("video-preview-original"); const vidUpscaledWrapper = document.getElementById("video-preview-upscaled-wrapper"); const vidUpscaled = document.getElementById("video-preview-upscaled"); vidOriginal.style.display = "block"; vidUpscaledWrapper.style.display = "block"; vidOriginal.src = getDownloadUrl(data.original_preview_file); vidUpscaled.src = getDownloadUrl(data.output_file); vidOriginal.load(); vidUpscaled.load(); initComparisonSlider(); initVideoPreviewControls(vidOriginal, vidUpscaled); // Reset zoom/pan previewZoom = 1.0; previewPanX = 0; previewPanY = 0; updatePreviewTransform(); } else if (data.status === "failed") { alert(`Preview generation failed: ${data.error}`); previewLoader.style.display = "none"; previewPlaceholderBox.style.display = "flex"; } loadQueue(); return; } statusBadge.className = `status-badge ${data.status}`; statusBadge.textContent = data.status; statStatus.textContent = data.status.toUpperCase(); if (data.total_frames > 0) { statFrames.textContent = `${data.current_frame} / ${data.total_frames}`; } else { statFrames.textContent = "-"; } statEta.textContent = data.eta || "Calculating..."; const progressVal = Math.round(data.progress || 0); progressPercent.textContent = `${progressVal}%`; progressBarFill.style.width = `${progressVal}%`; if (data.status === "queued") { appendLogLine("Job added to FIFO queue. Waiting for GPU lock...", "info"); } else if (data.status === "analyzing") { appendLogLine("GPU locked. Analyzing video parameters & framerates...", "info"); } else if (data.status === "extracting") { appendLogLine("FFmpeg: Splitting video stream to high-fidelity JPG frames...", "info"); } else if (data.status === "upscaling") { appendLogLine(`Vulkan GPU: Processing frame ${data.current_frame} of ${data.total_frames} (ETA: ${data.eta})...`, "info"); } else if (data.status === "restoring_faces") { appendLogLine("GFPGAN: Running Face Restoration on upscaled frames...", "info"); } else if (data.status === "interpolating") { appendLogLine("RIFE: Interpolating frames for smooth motion...", "info"); } else if (data.status === "assembling") { appendLogLine("FFmpeg: Reassembling enhanced frames & merging audio tracks...", "info"); } else if (data.status === "completed") { appendLogLine("Success! Upscaling process finished cleanly. Temp files deleted.", "success"); setTimeout(() => { if (socket) socket.close(); stepProgress.classList.remove("active"); stepFinished.classList.add("active"); downloadLink.href = getDownloadUrl(data.output_file); loadGallery(); }, 1500); } else if (data.status === "failed") { appendLogLine(`Critical Error: ${data.error}`, "error"); alert(`Upscaling job failed: ${data.error}`); } else if (data.status === "cancelled") { appendLogLine("Process aborted by user.", "error"); alert("Upscaling job cancelled."); window.location.reload(); } loadQueue(); } let vprepScrubbing = false; function initVideoPreviewControls(vidOriginal, vidUpscaled) { const playBtn = document.getElementById("vprep-play-btn"); const scrubber = document.getElementById("vprep-scrubber"); const timeCurrent = document.getElementById("vprep-time-current"); const timeDuration = document.getElementById("vprep-time-duration"); // Play/Pause button click playBtn.onclick = () => { if (vidOriginal.paused) { vidOriginal.play(); vidUpscaled.play(); playBtn.innerHTML = ``; } else { vidOriginal.pause(); vidUpscaled.pause(); playBtn.innerHTML = ``; } }; // Time Sync logic vidOriginal.onplay = () => { vidUpscaled.play(); playBtn.innerHTML = ``; }; vidOriginal.onpause = () => { vidUpscaled.pause(); playBtn.innerHTML = ``; }; vidOriginal.onloadedmetadata = () => { scrubber.max = vidOriginal.duration || 5; timeDuration.textContent = formatTime(vidOriginal.duration || 5); }; vidOriginal.ontimeupdate = () => { if (!vprepScrubbing) { scrubber.value = vidOriginal.currentTime; timeCurrent.textContent = formatTime(vidOriginal.currentTime); } // Sync check const diff = Math.abs(vidOriginal.currentTime - vidUpscaled.currentTime); if (diff > 0.15 && !vidOriginal.seeking && !vidUpscaled.seeking) { vidUpscaled.currentTime = vidOriginal.currentTime; } }; // Scrubber drag seeking scrubber.onmousedown = () => { vprepScrubbing = true; }; scrubber.onmouseup = () => { vprepScrubbing = false; vidOriginal.currentTime = parseFloat(scrubber.value); vidUpscaled.currentTime = parseFloat(scrubber.value); }; scrubber.oninput = () => { timeCurrent.textContent = formatTime(parseFloat(scrubber.value)); }; // Mobile touch scrubber events scrubber.ontouchstart = () => { vprepScrubbing = true; }; scrubber.ontouchend = () => { vprepScrubbing = false; vidOriginal.currentTime = parseFloat(scrubber.value); vidUpscaled.currentTime = parseFloat(scrubber.value); }; } let pollingInterval = null; function startFallbackPolling(jobId) { if (pollingInterval) clearInterval(pollingInterval); appendLogLine("Fallback: WebSockets offline. Switched to REST polling...", "warning"); pollingInterval = setInterval(async () => { try { const res = await fetch(`/api/upscale/status/${jobId}`); if (res.ok) { const data = await res.json(); updateProgressUI(data); if (data.status === "completed" || data.status === "failed" || data.status === "cancelled") { clearInterval(pollingInterval); } } } catch (err) { console.error("Polling error:", err); } }, 1000); } cancelUpscaleBtn.addEventListener("click", async () => { if (!currentJobId) return; if (confirm("Are you sure you want to abort the upscaling process? All progress will be lost.")) { try { const res = await fetch(`/api/upscale/cancel/${currentJobId}`, { method: "POST" }); if (res.ok) { appendLogLine("Sending abort command to worker...", "error"); } } catch (err) { console.error("Error cancelling job:", err); } } }); async function checkActiveJobs() { try { const res = await fetch("/api/jobs"); if (!res.ok) return; const jobs = await res.json(); const activeJob = jobs.find(job => ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status) ); if (activeJob) { currentJobId = activeJob.job_id; } } catch (err) { console.error("Error checking active jobs:", err); } } /* ---------------------------------------------------- 7. Synced Playback Modal & Control Handlers ---------------------------------------------------- */ let isScrubbing = false; let syncInterval = null; function formatTime(seconds) { if (isNaN(seconds)) return "00:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } function openVideoComparisonModal(originalSrc, upscaledSrc) { modalVideoOriginal.src = originalSrc; modalVideoUpscaled.src = upscaledSrc; modalVideoOriginal.load(); modalVideoUpscaled.load(); videoModal.style.display = "flex"; document.body.style.overflow = "hidden"; modalPlayBtn.innerHTML = ``; modalTimeSlider.value = 0; modalTimeCurrent.textContent = "00:00"; modalTimeDuration.textContent = "00:00"; modalVideoOriginal.onloadedmetadata = () => { modalTimeSlider.max = modalVideoOriginal.duration; modalTimeDuration.textContent = formatTime(modalVideoOriginal.duration); }; // Strict periodic sync check syncInterval = setInterval(() => { if (!modalVideoOriginal.paused && !modalVideoOriginal.seeking && !modalVideoUpscaled.seeking) { const diff = Math.abs(modalVideoOriginal.currentTime - modalVideoUpscaled.currentTime); if (diff > 0.15) { console.log(`Syncing video channels: diff was ${diff}s`); modalVideoUpscaled.currentTime = modalVideoOriginal.currentTime; } } if (!isScrubbing && !modalVideoOriginal.paused) { modalTimeSlider.value = modalVideoOriginal.currentTime; modalTimeCurrent.textContent = formatTime(modalVideoOriginal.currentTime); } }, 100); } function closeVideoModal() { if (syncInterval) clearInterval(syncInterval); modalVideoOriginal.pause(); modalVideoUpscaled.pause(); modalVideoOriginal.removeAttribute("src"); modalVideoUpscaled.removeAttribute("src"); modalVideoOriginal.load(); modalVideoUpscaled.load(); videoModal.style.display = "none"; document.body.style.overflow = ""; } modalCloseBtn.addEventListener("click", closeVideoModal); videoModal.addEventListener("click", (e) => { if (e.target === videoModal) closeVideoModal(); }); modalPlayBtn.addEventListener("click", () => { if (modalVideoOriginal.paused) { modalVideoOriginal.play(); modalVideoUpscaled.play(); modalPlayBtn.innerHTML = ``; } else { modalVideoOriginal.pause(); modalVideoUpscaled.pause(); modalPlayBtn.innerHTML = ``; } }); modalTimeSlider.addEventListener("mousedown", () => { isScrubbing = true; }); modalTimeSlider.addEventListener("mouseup", () => { isScrubbing = false; }); modalTimeSlider.addEventListener("input", (e) => { const val = parseFloat(e.target.value); modalVideoOriginal.currentTime = val; modalVideoUpscaled.currentTime = val; modalTimeCurrent.textContent = formatTime(val); }); modalMuteBtn.addEventListener("click", () => { const isMuted = modalVideoOriginal.muted; modalVideoOriginal.muted = !isMuted; modalVideoUpscaled.muted = !isMuted; modalMuteBtn.innerHTML = isMuted ? `` : ``; }); /* ---------------------------------------------------- 8. Finished/Completed Runs Direct Comparison Launch ---------------------------------------------------- */ if (finishedPlayComparisonBtn) { finishedPlayComparisonBtn.addEventListener("click", async () => { if (!currentJobId) return; try { const res = await fetch("/api/jobs"); if (!res.ok) return; const jobs = await res.json(); const job = jobs.find(j => j.job_id === currentJobId); if (job) { const originalFilename = job.video_path.split("/").pop(); const originalUrl = `/api/uploads/${originalFilename}`; const upscaledUrl = getDownloadUrl(job.output_file); openVideoComparisonModal(originalUrl, upscaledUrl); } } catch (err) { console.error("Comparison playback trigger error:", err); } }); } /* ---------------------------------------------------- 9. Visual Outputs Gallery & Delete Action ---------------------------------------------------- */ async function loadGallery() { const galleryGrid = document.getElementById("gallery-grid"); if (!galleryGrid) return; try { const [outputsRes, jobsRes] = await Promise.all([ fetch("/api/outputs"), fetch("/api/jobs") ]); if (!outputsRes.ok || !jobsRes.ok) return; const outputs = await outputsRes.json(); const jobs = await jobsRes.json(); if (outputs.length === 0) { galleryGrid.innerHTML = ` `; return; } galleryGrid.innerHTML = ""; outputs.forEach(file => { const matchingJob = jobs.find(j => { if (j.output_file) { const jobFilename = j.output_file.split("/").pop(); if (jobFilename === file.filename) return true; } const basename = file.filename.replace("upscaled_", ""); const jobId = basename.split(".")[0]; return j.job_id === jobId; }); const card = document.createElement("div"); card.className = "gallery-card"; let modelInfo = "AI Upscaled"; let originalFilename = null; let originalUrl = null; let upscaledUrl = file.url; if (matchingJob) { modelInfo = `${matchingJob.model || 'AI Model'} (${matchingJob.scale || 4}x)`; originalFilename = matchingJob.video_path ? matchingJob.video_path.split("/").pop() : null; originalUrl = originalFilename ? `/api/uploads/${originalFilename}` : null; } card.innerHTML = ` `; const playOverlay = card.querySelector(".play-overlay"); const playCompareBtn = card.querySelector(".btn-play-compare"); const playAction = () => { if (originalUrl) { openVideoComparisonModal(originalUrl, upscaledUrl); } else { alert("Original video source file not found on disk. Performing upscaled output playback comparison."); openVideoComparisonModal(upscaledUrl, upscaledUrl); } }; playOverlay.addEventListener("click", playAction); playCompareBtn.addEventListener("click", playAction); const deleteBtn = card.querySelector(".btn-delete"); deleteBtn.addEventListener("click", async () => { if (confirm(`Are you sure you want to delete ${file.filename}? This will remove all related uploads, outputs, and logs permanently.`)) { try { let delSuccess = false; if (matchingJob) { const delJobRes = await fetch(`/api/jobs/${matchingJob.job_id}`, { method: "DELETE" }); delSuccess = delJobRes.ok; } else { const delOutputRes = await fetch(`/api/outputs/delete/${file.filename}`, { method: "DELETE" }); delSuccess = delOutputRes.ok; } if (delSuccess) { loadGallery(); } else { alert("Failed to delete video file and related resources."); } } catch (err) { console.error("Delete error:", err); } } }); galleryGrid.appendChild(card); }); } catch (err) { console.error("Error loading gallery:", err); } } async function moveQueueItem(jobId, direction) { try { const res = await fetch("/api/queue"); if (!res.ok) return; const data = await res.json(); const queue = data.queue; const index = queue.indexOf(jobId); if (index === -1) return; const newIndex = index + direction; if (newIndex < 0 || newIndex >= queue.length) return; // Swap const temp = queue[index]; queue[index] = queue[newIndex]; queue[newIndex] = temp; const reorderRes = await fetch("/api/queue/reorder", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ job_ids: queue }) }); if (reorderRes.ok) { loadQueue(); } } catch (err) { console.error("Error reordering queue:", err); } } async function loadQueue() { const queueList = document.getElementById("queue-list"); if (!queueList) return; try { const res = await fetch("/api/jobs"); if (!res.ok) return; const jobs = await res.json(); const displayJobs = jobs.filter(job => ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling", "completed", "failed", "cancelled", "interrupted", "paused"].includes(job.status) ); if (displayJobs.length === 0) { queueList.innerHTML = `

No active or queued jobs at the moment.

`; return; } queueList.innerHTML = ""; displayJobs.forEach(job => { const item = document.createElement("div"); item.className = "queue-item"; const titleLabel = job.is_preview ? `Preview (5s) - ${job.job_id.substring(0, 8)}...` : `Job - ${job.job_id.substring(0, 8)}...`; const modelLabel = job.model; const scaleLabel = `${job.scale}x`; const progress = Math.round(job.progress || 0); let metaHtml = ` Model: ${modelLabel} (${scaleLabel}) Source: ${job.source_filename || (job.video_path ? job.video_path.split('/').pop() : 'Unknown')} `; let errorHtml = ""; if (job.status === "failed" && job.error) { errorHtml = `
Failure Cause: ${job.error}
`; } else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed" && job.status !== "interrupted") { metaHtml += ` ETA: ${job.eta || 'Calculating...'}`; } let actionsHtml = ""; let reorderHtml = ""; const isActiveOrQueued = ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status); if (isActiveOrQueued && job.queue_position !== null && job.queue_position !== undefined) { const activeQueuedJobs = displayJobs.filter(j => ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(j.status) ).filter(j => j.queue_position !== null && j.queue_position !== undefined); const minPos = Math.min(...activeQueuedJobs.map(j => j.queue_position)); const maxPos = Math.max(...activeQueuedJobs.map(j => j.queue_position)); const isFirst = job.queue_position === minPos; const isLast = job.queue_position === maxPos; reorderHtml = ` `; } if (job.status === "interrupted" || job.status === "failed" || job.status === "paused") { const btnLabel = job.status === "failed" ? "Retry" : "Resume"; const btnIcon = job.status === "failed" ? "fa-rotate-right" : "fa-play"; actionsHtml = ` `; } else if (["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status)) { actionsHtml = ` ${reorderHtml} `; } else { actionsHtml = ` `; } const showProgress = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling", "paused", "interrupted"].includes(job.status); const progressStyle = showProgress ? "display: block;" : "display: none;"; item.innerHTML = `
${titleLabel} ${job.status}
${metaHtml}
${errorHtml}
${actionsHtml}
`; const upBtn = item.querySelector(".btn-move-up"); if (upBtn) { upBtn.addEventListener("click", async (e) => { e.stopPropagation(); await moveQueueItem(job.job_id, -1); }); } const downBtn = item.querySelector(".btn-move-down"); if (downBtn) { downBtn.addEventListener("click", async (e) => { e.stopPropagation(); await moveQueueItem(job.job_id, 1); }); } const resumeBtn = item.querySelector(".btn-resume-job"); if (resumeBtn) { resumeBtn.addEventListener("click", async (e) => { e.stopPropagation(); try { const resumeRes = await fetch(`/api/upscale/resume/${job.job_id}`, { method: "POST" }); if (resumeRes.ok) { loadQueue(); } else { const err = await resumeRes.json(); alert("Failed to resume job: " + formatFetchError(err, "unknown error")); } } catch (err) { console.error("Error resuming job:", err); } }); } const abortBtn = item.querySelector(".btn-abort-job"); if (abortBtn) { abortBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (confirm("Are you sure you want to abort this upscaling job?")) { try { const abortRes = await fetch(`/api/upscale/cancel/${job.job_id}`, { method: "POST" }); if (abortRes.ok) { loadQueue(); } } catch (err) { console.error("Error aborting job:", err); } } }); } const pauseBtn = item.querySelector(".btn-pause-job"); if (pauseBtn) { pauseBtn.addEventListener("click", async (e) => { e.stopPropagation(); try { const pauseRes = await fetch(`/api/upscale/pause/${job.job_id}`, { method: "POST" }); if (pauseRes.ok) { loadQueue(); } else { const err = await pauseRes.json(); alert("Failed to pause job: " + formatFetchError(err, "unknown error")); } } catch (err) { console.error("Error pausing job:", err); } }); } const deleteBtn = item.querySelector(".btn-delete-job"); if (deleteBtn) { deleteBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (confirm("Are you sure you want to clear this job from history?")) { try { const delRes = await fetch(`/api/jobs/${job.job_id}`, { method: "DELETE" }); if (delRes.ok) { loadQueue(); loadGallery(); } } catch (err) { console.error("Error clearing job:", err); } } }); } const viewBtn = item.querySelector(".btn-view-job"); if (viewBtn) { viewBtn.addEventListener("click", (e) => { e.stopPropagation(); currentJobId = job.job_id; if (socket) { try { socket.close(); } catch (e) {} } document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active")); if (job.status === "completed") { document.getElementById("step-finished").classList.add("active"); document.getElementById("download-link").href = getDownloadUrl(job.output_file); } else { document.getElementById("step-progress").classList.add("active"); document.getElementById("job-id-display").textContent = currentJobId; connectProgressWebSocket(currentJobId); } }); } queueList.appendChild(item); }); } catch (err) { console.error("Error loading queue:", err); } } const refreshGalleryBtn = document.getElementById("refresh-gallery-btn"); if (refreshGalleryBtn) { refreshGalleryBtn.addEventListener("click", loadGallery); } const refreshQueueBtn = document.getElementById("refresh-queue-btn"); if (refreshQueueBtn) { refreshQueueBtn.addEventListener("click", loadQueue); } const purgeAllBtn = document.getElementById("purge-all-btn"); if (purgeAllBtn) { purgeAllBtn.addEventListener("click", async () => { if (confirm("⚠️ WARNING: This will permanently delete ALL uploaded source videos, completed outputs, temp frames, and history logs. Are you sure you want to proceed?")) { try { const res = await fetch("/api/jobs/purge-all", { method: "POST" }); if (res.ok) { alert("System files purged successfully!"); loadGallery(); } else { alert("Failed to purge system files."); } } catch (err) { console.error("Purge all error:", err); } } }); } /* ---------------------------------------------------- 10. Core Setup & Models Loading ---------------------------------------------------- */ const standardModelNames = { "realesr-animevideov3": "Real-ESRGAN Anime Video V3 (Fast / Anime & Video)", "realesrgan-x4plus": "Real-ESRGAN X4 Plus (Ultra Quality / Real World)", "realesrgan-x4plus-anime": "Real-ESRGAN X4 Plus Anime (Ultra Quality / Illustration)", "realesrnet-x4plus": "Real-ESRNet X4 Plus (Alternate / Real World)" }; async function loadModels() { try { const res = await fetch("/api/models"); if (!res.ok) return; const models = await res.json(); if (models && models.length > 0) { modelSelect.innerHTML = ""; models.forEach(model => { const opt = document.createElement("option"); opt.value = model; opt.textContent = standardModelNames[model] || `Custom Model: ${model}`; if (model === "realesr-animevideov3") { opt.selected = true; } modelSelect.appendChild(opt); }); modelSelect.dispatchEvent(new Event("change")); } } catch (err) { console.error("Error loading models list:", err); } } function useUploadedVideo(entry) { window.isBatchMode = false; window.batchFileIds = []; // Enable inputs timelineSlider.disabled = false; generatePreviewBtn.disabled = false; generateVideoPreviewBtn.disabled = false; document.getElementById("trim-start-range").disabled = false; document.getElementById("trim-end-range").disabled = false; previewPlaceholderBox.style.display = "flex"; previewPlaceholderBox.querySelector("p").textContent = "Move timeline slider and generate frame preview to calibrate parameters."; comparisonSliderContainer.style.display = "none"; uploadFileId = entry.file_id; uploadMetadata = entry.metadata; // Populate Config Step Info metaName.textContent = entry.original_filename; metaSize.textContent = formatBytes(entry.size_bytes); // Populate Accordions Stream details const streams = entry.metadata.streams || []; const videoStream = streams.find(s => s.codec_type === "video") || {}; const audioStream = streams.find(s => s.codec_type === "audio") || {}; // Video stream metadata document.getElementById("meta-res-codec").textContent = (videoStream.codec_name || "unknown").toUpperCase(); document.getElementById("meta-res").textContent = `${entry.metadata.width} x ${entry.metadata.height}`; document.getElementById("meta-fps").textContent = `${entry.metadata.fps} FPS`; document.getElementById("meta-duration").textContent = `${entry.metadata.duration}s`; document.getElementById("meta-total-frames").textContent = entry.metadata.total_frames || "-"; document.getElementById("meta-aspect-ratio").textContent = videoStream.display_aspect_ratio || "N/A"; // Audio stream metadata if (audioStream.codec_name) { document.getElementById("meta-audio-codec").textContent = (audioStream.codec_name || "unknown").toUpperCase(); document.getElementById("meta-audio-channels").textContent = audioStream.channels || "N/A"; document.getElementById("meta-audio-samplerate").textContent = audioStream.sample_rate ? `${audioStream.sample_rate} Hz` : "N/A"; document.getElementById("meta-audio-bitrate").textContent = audioStream.bit_rate ? formatBytes(parseInt(audioStream.bit_rate)) + "/s" : "N/A"; } else { document.getElementById("meta-audio-codec").textContent = "No Audio Stream"; document.getElementById("meta-audio-channels").textContent = "-"; document.getElementById("meta-audio-samplerate").textContent = "-"; document.getElementById("meta-audio-bitrate").textContent = "-"; } // Configure timeline slider constraints timelineSlider.min = 0.1; timelineSlider.max = entry.metadata.duration - 0.5; timelineSlider.value = (entry.metadata.duration * 0.15).toFixed(2); timelineTime.textContent = `${timelineSlider.value}s`; // Configure Trimming panel range values const trimStartRange = document.getElementById("trim-start-range"); const trimEndRange = document.getElementById("trim-end-range"); const trimStartValue = document.getElementById("trim-start-value"); const trimEndValue = document.getElementById("trim-end-value"); trimStartRange.min = 0; trimStartRange.max = entry.metadata.duration; trimStartRange.value = 0; trimStartValue.textContent = "0.00s"; trimEndRange.min = 0; trimEndRange.max = entry.metadata.duration; trimEndRange.value = entry.metadata.duration; trimEndValue.textContent = `${entry.metadata.duration.toFixed(2)}s`; // Show config view stepUpload.classList.remove("active"); stepConfig.classList.add("active"); } function useBatchUploadedVideos(entries) { window.isBatchMode = true; window.batchFileIds = entries.map(e => e.file_id); uploadFileId = entries[0].file_id; // Set fallback file ID for safety // Set configuration name metaName.textContent = `Batch Processing - ${entries.length} video(s)`; metaSize.textContent = `Total size: ${formatBytes(entries.reduce((acc, e) => acc + (e.size_bytes || 0), 0))}`; // Set video/audio stream specs to default/mixed document.getElementById("meta-res-codec").textContent = "MIXED"; document.getElementById("meta-res").textContent = "Varies"; document.getElementById("meta-fps").textContent = "Varies"; document.getElementById("meta-duration").textContent = "Varies"; document.getElementById("meta-total-frames").textContent = "Varies"; document.getElementById("meta-aspect-ratio").textContent = "Varies"; document.getElementById("meta-audio-codec").textContent = "Varies"; document.getElementById("meta-audio-channels").textContent = "Varies"; document.getElementById("meta-audio-samplerate").textContent = "Varies"; document.getElementById("meta-audio-bitrate").textContent = "Varies"; // Disable timeline and previews timelineSlider.disabled = true; generatePreviewBtn.disabled = true; generateVideoPreviewBtn.disabled = true; // Hide trimming panel or disable inputs document.getElementById("trim-start-range").disabled = true; document.getElementById("trim-end-range").disabled = true; // Hide preview placeholders previewPlaceholderBox.style.display = "flex"; previewPlaceholderBox.querySelector("p").textContent = "Preview and trimming are disabled in Batch Mode."; comparisonSliderContainer.style.display = "none"; // Show config view stepUpload.classList.remove("active"); stepConfig.classList.add("active"); } async function loadUploadsBrowser() { const browserList = document.getElementById("uploads-browser-list"); if (!browserList) return; const batchConfigBar = document.getElementById("batch-config-bar"); const batchSelectedCount = document.getElementById("batch-selected-count"); const batchConfigureBtn = document.getElementById("batch-configure-btn"); try { const res = await fetch("/api/uploads"); if (!res.ok) return; const uploads = await res.json(); if (uploads.length === 0) { browserList.innerHTML = `

No uploaded files found. Upload a video above to get started!

`; if (batchConfigBar) batchConfigBar.style.display = "none"; return; } browserList.innerHTML = ""; let selectedEntries = []; uploads.forEach(entry => { const item = document.createElement("div"); item.className = "queue-item"; const sizeStr = formatBytes(entry.size_bytes || 0); const durationStr = entry.metadata ? `${entry.metadata.duration}s` : "-"; const resStr = entry.metadata ? `${entry.metadata.width}x${entry.metadata.height}` : "-"; const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-"; item.innerHTML = `
${entry.original_filename}
Size: ${sizeStr} Resolution: ${resStr} Duration: ${durationStr} FPS: ${fpsStr}
`; const checkbox = item.querySelector(".batch-select-checkbox"); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedEntries.push(entry); } else { selectedEntries = selectedEntries.filter(se => se.file_id !== entry.file_id); } if (selectedEntries.length > 0) { if (batchSelectedCount) batchSelectedCount.textContent = selectedEntries.length; if (batchConfigBar) batchConfigBar.style.display = "flex"; } else { if (batchConfigBar) batchConfigBar.style.display = "none"; } }); item.querySelector(".btn-use-upload").addEventListener("click", () => { useUploadedVideo(entry); }); item.querySelector(".btn-delete-upload").addEventListener("click", async () => { if (confirm(`Are you sure you want to delete "${entry.original_filename}" from the workspace?`)) { try { const delRes = await fetch(`/api/uploads/${entry.file_id}`, { method: "DELETE" }); if (delRes.ok) { loadUploadsBrowser(); } } catch (err) { console.error("Error deleting upload:", err); } } }); browserList.appendChild(item); }); if (batchConfigureBtn) { const newBtn = batchConfigureBtn.cloneNode(true); batchConfigureBtn.parentNode.replaceChild(newBtn, batchConfigureBtn); newBtn.addEventListener("click", () => { if (selectedEntries.length > 0) { useBatchUploadedVideos(selectedEntries); } }); } } catch (err) { console.error("Error loading uploads browser:", err); } } async function initializeApp() { try { await loadModels(); } catch(e) { console.error("initializeApp: loadModels failed:", e); } try { await checkActiveJobs(); } catch(e) { console.error("initializeApp: checkActiveJobs failed:", e); } try { await loadGallery(); } catch(e) { console.error("initializeApp: loadGallery failed:", e); } try { await loadQueue(); } catch(e) { console.error("initializeApp: loadQueue failed:", e); } try { await loadUploadsBrowser(); } catch(e) { console.error("initializeApp: loadUploadsBrowser failed:", e); } // Load GPU Diagnostics try { const res = await fetch("/api/diagnostics"); if (res.ok) { const diag = await res.json(); const gpuInfo = document.getElementById("gpu-info"); if (gpuInfo) { if (diag.gpu && diag.gpu.available && diag.gpu.gpus.length > 0) { const gpuName = diag.gpu.gpus[0].name; gpuInfo.textContent = `GPU Active: ${gpuName} (Vulkan)`; } else { gpuInfo.textContent = "GPU Active: Vulkan Device"; } } } } catch (err) { console.error("Error loading GPU diagnostics:", err); } // Register btn-refresh-uploads click handler const refreshUploadsBtn = document.getElementById("refresh-uploads-btn"); if (refreshUploadsBtn) { refreshUploadsBtn.addEventListener("click", loadUploadsBrowser); } // Register back-to-workspace-btn click handler const backToWorkspaceBtn = document.getElementById("back-to-workspace-btn"); if (backToWorkspaceBtn) { backToWorkspaceBtn.addEventListener("click", () => { if (socket) { try { socket.close(); } catch(e) {} } document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active")); document.getElementById("step-upload").classList.add("active"); uploadFileId = null; window.selectedServerFilePath = null; window.selectedServerFilePaths = null; loadUploadsBrowser(); }); } // Register documentation modal handlers const docsBtn = document.getElementById("docs-btn"); const docsModal = document.getElementById("docs-modal"); const docsCloseBtn = document.getElementById("docs-modal-close-btn"); if (docsBtn && docsModal) { docsBtn.addEventListener("click", () => { docsModal.style.display = "flex"; document.body.style.overflow = "hidden"; }); } if (docsCloseBtn && docsModal) { docsCloseBtn.addEventListener("click", () => { docsModal.style.display = "none"; document.body.style.overflow = ""; }); docsModal.addEventListener("click", (e) => { if (e.target === docsModal) { docsModal.style.display = "none"; document.body.style.overflow = ""; } }); } // Temporary storage maintenance handlers const refreshTempBtn = document.getElementById("refresh-temp-btn"); if (refreshTempBtn) { refreshTempBtn.addEventListener("click", loadTempInfo); } const cleanupTempBtn = document.getElementById("cleanup-temp-btn"); if (cleanupTempBtn) { cleanupTempBtn.addEventListener("click", async () => { if (confirm("Are you sure you want to clean up all inactive temporary directories?\nThis will clear any completed, cancelled, failed, or orphaned job frames. Paused or interrupted jobs will have to restart from the beginning on resume.")) { try { const cleanRes = await fetch("/api/system/temp-cleanup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paths: null }) }); if (cleanRes.ok) { loadTempInfo(); loadQueue(); } else { alert("Cleanup failed."); } } catch (err) { console.error(err); } } }); } // Refresh queue button should also refresh temp files info const refreshQueueBtn = document.getElementById("refresh-queue-btn"); if (refreshQueueBtn) { refreshQueueBtn.addEventListener("click", loadTempInfo); } // Load initial temp info loadTempInfo(); // Default temp directory setting const saveTempDirBtn = document.getElementById("save-temp-dir-btn"); const defaultTempDirInput = document.getElementById("default-temp-dir-input"); const tempDirStatus = document.getElementById("temp-dir-status"); // Load saved default temp dir and cluster settings on init loadDefaultTempDir(); loadClusterSettings(); if (saveTempDirBtn) { saveTempDirBtn.addEventListener("click", async () => { const newDir = defaultTempDirInput ? defaultTempDirInput.value.trim() : ""; tempDirStatus.textContent = ""; tempDirStatus.style.color = ""; saveTempDirBtn.disabled = true; saveTempDirBtn.innerHTML = ' Saving...'; try { // Fetch current settings to preserve other values const currentRes = await fetch("/api/settings"); const currentSettings = currentRes.ok ? await currentRes.json() : {}; const updatedSettings = { ...currentSettings, default_temp_dir: newDir }; const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedSettings) }); if (res.ok) { tempDirStatus.style.color = "var(--success, #4caf50)"; tempDirStatus.textContent = newDir ? `\u2713 Default temp directory saved: ${newDir}` : "\u2713 Reset to project default temp/ directory."; // Also update the per-job input placeholder/value const perJobInput = document.getElementById("temp-dir-input"); if (perJobInput && !perJobInput.value.trim()) { perJobInput.placeholder = newDir ? `Default: ${newDir} (override here)` : "e.g. /mnt/fast_ssd/temp (Leave empty for default temp/)"; } } else { const errData = await res.json().catch(() => ({})); tempDirStatus.style.color = "var(--danger, #f44336)"; tempDirStatus.textContent = `\u2717 ${errData.detail || "Failed to save setting."}`; } } catch (err) { tempDirStatus.style.color = "var(--danger, #f44336)"; tempDirStatus.textContent = "\u2717 Network error saving setting."; console.error(err); } finally { saveTempDirBtn.disabled = false; saveTempDirBtn.innerHTML = ' Save'; } }); } // Default output directory setting const saveOutputDirBtn = document.getElementById("save-output-dir-btn"); const defaultOutputDirInput = document.getElementById("default-output-dir-input"); const outputDirStatus = document.getElementById("output-dir-status"); if (saveOutputDirBtn) { saveOutputDirBtn.addEventListener("click", async () => { const newDir = defaultOutputDirInput ? defaultOutputDirInput.value.trim() : ""; outputDirStatus.textContent = ""; outputDirStatus.style.color = ""; saveOutputDirBtn.disabled = true; saveOutputDirBtn.innerHTML = ' Saving...'; try { // Fetch current settings to preserve other values const currentRes = await fetch("/api/settings"); const currentSettings = currentRes.ok ? await currentRes.json() : {}; const updatedSettings = { ...currentSettings, default_output_dir: newDir }; const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedSettings) }); if (res.ok) { outputDirStatus.style.color = "var(--success, #4caf50)"; outputDirStatus.textContent = newDir ? `\u2713 Default output directory saved: ${newDir}` : "\u2713 Reset to project default outputs/ directory."; const perJobInput = document.getElementById("output-dir-input"); if (perJobInput && !perJobInput.value.trim()) { perJobInput.placeholder = newDir ? `Default: ${newDir} (override here)` : "e.g. /mnt/fast_ssd/outputs (Leave empty for default outputs/)"; } } else { const errData = await res.json().catch(() => ({})); outputDirStatus.style.color = "var(--danger, #f44336)"; outputDirStatus.textContent = `\u2717 ${errData.detail || "Failed to save setting."}`; } } catch (err) { outputDirStatus.style.color = "var(--danger, #f44336)"; outputDirStatus.textContent = "\u2717 Network error saving setting."; console.error(err); } finally { saveOutputDirBtn.disabled = false; saveOutputDirBtn.innerHTML = ' Save'; } }); } // Wire up folder browser modals const browseJobTempBtn = document.getElementById("browse-job-temp-btn"); const browseDefaultTempBtn = document.getElementById("browse-default-temp-btn"); const browseJobOutputBtn = document.getElementById("browse-job-output-btn"); const browseDefaultOutputBtn = document.getElementById("browse-default-output-btn"); const browseSharedStorageBtn = document.getElementById("browse-shared-storage-btn"); const browseSmbMountBtn = document.getElementById("browse-smb-mount-btn"); const folderModalCloseBtn = document.getElementById("folder-modal-close-btn"); const folderCancelBtn = document.getElementById("folder-cancel-btn"); const folderSelectBtn = document.getElementById("folder-select-btn"); const folderUpBtn = document.getElementById("folder-up-btn"); const createFolderBtn = document.getElementById("create-folder-btn"); const newFolderNameInput = document.getElementById("new-folder-name"); if (browseJobTempBtn) { browseJobTempBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("temp-dir-input")); }); } if (browseDefaultTempBtn) { browseDefaultTempBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("default-temp-dir-input")); }); } if (browseJobOutputBtn) { browseJobOutputBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("output-dir-input")); }); } if (browseDefaultOutputBtn) { browseDefaultOutputBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("default-output-dir-input")); }); } if (browseSharedStorageBtn) { browseSharedStorageBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("shared-storage-path-input")); }); } if (browseSmbMountBtn) { browseSmbMountBtn.addEventListener("click", () => { openFolderBrowser(document.getElementById("smb-mount-path")); }); } if (folderModalCloseBtn) { folderModalCloseBtn.addEventListener("click", closeFolderBrowser); } if (folderCancelBtn) { folderCancelBtn.addEventListener("click", closeFolderBrowser); } // Close modal when clicking outside card const folderModal = document.getElementById("folder-modal"); if (folderModal) { folderModal.addEventListener("click", (e) => { if (e.target === folderModal) { closeFolderBrowser(); } }); } if (folderSelectBtn) { folderSelectBtn.addEventListener("click", () => { if (activeTargetInput && currentBrowsePath) { activeTargetInput.value = currentBrowsePath; activeTargetInput.dispatchEvent(new Event("change")); activeTargetInput.dispatchEvent(new Event("input")); } closeFolderBrowser(); }); } if (folderUpBtn) { folderUpBtn.addEventListener("click", () => { if (currentBrowsePath) { const parts = currentBrowsePath.split("/"); if (parts.length > 1) { parts.pop(); const parent = parts.join("/") || "/"; loadFolderDir(parent); } } }); } if (createFolderBtn) { createFolderBtn.addEventListener("click", createFolder); } if (newFolderNameInput) { newFolderNameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { createFolder(); } }); } // Cluster and SMB UI Settings const nodeRoleSelect = document.getElementById("node-role-select"); const useSharedStorageCheckbox = document.getElementById("use-shared-storage-checkbox"); const coordUrlGroup = document.getElementById("coord-url-group"); const workerUrlGroup = document.getElementById("worker-url-group"); const sharedStoragePathGroup = document.getElementById("shared-storage-path-group"); // Toggle fields based on selects if (nodeRoleSelect) { nodeRoleSelect.addEventListener("change", (e) => { const role = e.target.value; coordUrlGroup.style.display = role === "worker" ? "block" : "none"; workerUrlGroup.style.display = role === "worker" || role === "coordinator" ? "block" : "none"; }); } if (useSharedStorageCheckbox) { useSharedStorageCheckbox.addEventListener("change", (e) => { sharedStoragePathGroup.style.display = e.target.checked ? "flex" : "none"; }); } // SMB Modals const addSmbShareBtn = document.getElementById("add-smb-share-btn"); const smbModal = document.getElementById("smb-modal"); const smbModalCloseBtn = document.getElementById("smb-modal-close-btn"); const smbCancelBtn = document.getElementById("smb-cancel-btn"); const smbSaveBtn = document.getElementById("smb-save-btn"); const smbModalStatus = document.getElementById("smb-modal-status"); if (addSmbShareBtn) { addSmbShareBtn.addEventListener("click", () => { if (smbModal) { document.getElementById("smb-name").value = ""; document.getElementById("smb-server").value = ""; document.getElementById("smb-share-name").value = ""; document.getElementById("smb-username").value = ""; document.getElementById("smb-password").value = ""; document.getElementById("smb-mount-path").value = ""; smbModalStatus.textContent = ""; smbModalStatus.style.color = ""; smbModal.style.display = "flex"; document.body.style.overflow = "hidden"; } }); } const closeSmbModal = () => { if (smbModal) { smbModal.style.display = "none"; document.body.style.overflow = ""; } }; if (smbModalCloseBtn) smbModalCloseBtn.addEventListener("click", closeSmbModal); if (smbCancelBtn) smbCancelBtn.addEventListener("click", closeSmbModal); if (smbModal) { smbModal.addEventListener("click", (e) => { if (e.target === smbModal) { closeSmbModal(); } }); } if (smbSaveBtn) { smbSaveBtn.addEventListener("click", async () => { const name = document.getElementById("smb-name").value.trim(); const server = document.getElementById("smb-server").value.trim(); const share = document.getElementById("smb-share-name").value.trim(); const username = document.getElementById("smb-username").value.trim(); const password = document.getElementById("smb-password").value; const mountPath = document.getElementById("smb-mount-path").value.trim(); if (!name || !server || !share || !mountPath) { smbModalStatus.style.color = "var(--danger)"; smbModalStatus.textContent = "Please fill in all required fields."; return; } smbModalStatus.style.color = "var(--text-muted)"; smbModalStatus.textContent = "Attempting to mount SMB share..."; try { // Client-side SMB share management const getRes = await fetch("/api/settings"); if (!getRes.ok) throw new Error("Failed to load settings."); const settings = await getRes.json(); if (!settings.smb_shares) { settings.smb_shares = []; } const newShare = { name, server, share, username, password, mount_path: mountPath }; const idx = settings.smb_shares.findIndex(s => s.name === name); if (idx !== -1) { settings.smb_shares[idx] = newShare; } else { settings.smb_shares.push(newShare); } const saveRes = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) }); if (saveRes.ok) { closeSmbModal(); loadClusterSettings(); // Reload SMB list } else { const err = await saveRes.json().catch(() => ({})); smbModalStatus.style.color = "var(--danger)"; smbModalStatus.textContent = `\u2717 ${err.detail || "Mount failed."}`; } } catch (ex) { smbModalStatus.style.color = "var(--danger)"; smbModalStatus.textContent = "\u2717 Network error mapping share."; console.error(ex); } }); } const saveClusterSettingsBtn = document.getElementById("save-cluster-settings-btn"); const clusterSettingsStatus = document.getElementById("cluster-settings-status"); if (saveClusterSettingsBtn) { saveClusterSettingsBtn.addEventListener("click", async () => { const role = nodeRoleSelect ? nodeRoleSelect.value : "standalone"; const coordinatorUrl = document.getElementById("coordinator-url-input") ? document.getElementById("coordinator-url-input").value.trim() : ""; const workerNodeUrl = document.getElementById("worker-url-input") ? document.getElementById("worker-url-input").value.trim() : ""; const useSharedStorage = useSharedStorageCheckbox ? useSharedStorageCheckbox.checked : false; const sharedStoragePath = document.getElementById("shared-storage-path-input") ? document.getElementById("shared-storage-path-input").value.trim() : ""; clusterSettingsStatus.textContent = ""; clusterSettingsStatus.style.color = ""; saveClusterSettingsBtn.disabled = true; saveClusterSettingsBtn.innerHTML = ' Saving...'; try { // Fetch current settings to preserve other values const currentRes = await fetch("/api/settings"); const currentSettings = currentRes.ok ? await currentRes.json() : {}; const updatedSettings = { ...currentSettings, role, coordinator_url: coordinatorUrl, worker_node_url: workerNodeUrl, use_shared_storage: useSharedStorage, shared_storage_path: sharedStoragePath }; const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedSettings) }); if (res.ok) { clusterSettingsStatus.style.color = "var(--success, #4caf50)"; clusterSettingsStatus.textContent = "\u2713 Cluster settings saved successfully."; } else { const errData = await res.json().catch(() => ({})); clusterSettingsStatus.style.color = "var(--danger, #f44336)"; clusterSettingsStatus.textContent = `\u2717 ${errData.detail || "Failed to save setting."}`; } } catch (err) { clusterSettingsStatus.style.color = "var(--danger, #f44336)"; clusterSettingsStatus.textContent = "\u2717 Network error saving setting."; console.error(err); } finally { saveClusterSettingsBtn.disabled = false; saveClusterSettingsBtn.innerHTML = ' Save Cluster Settings'; } }); } } /* ---------------------------------------------------- Folder Browser Modal & API Helpers ---------------------------------------------------- */ let activeTargetInput = null; let currentBrowsePath = ""; function openFolderBrowser(targetInput) { activeTargetInput = targetInput; const modal = document.getElementById("folder-modal"); if (!modal) return; const statusDiv = document.getElementById("folder-modal-status"); if (statusDiv) statusDiv.textContent = ""; let initialPath = targetInput ? targetInput.value.trim() : ""; modal.style.display = "flex"; document.body.style.overflow = "hidden"; loadFolderDir(initialPath); } function closeFolderBrowser() { const modal = document.getElementById("folder-modal"); if (modal) { modal.style.display = "none"; document.body.style.overflow = ""; } activeTargetInput = null; } async function loadFolderDir(path) { const listContainer = document.getElementById("folder-list"); const currentPathInput = document.getElementById("folder-current-path"); const statusDiv = document.getElementById("folder-modal-status"); if (!listContainer) return; listContainer.innerHTML = '
Loading...
'; try { const url = `/api/system/browse-dir?path=${encodeURIComponent(path)}`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Failed to load directory."); } const data = await res.json(); currentBrowsePath = data.current_path; if (currentPathInput) currentPathInput.value = currentBrowsePath; listContainer.innerHTML = ""; const entries = data.entries || []; if (entries.length === 0) { listContainer.innerHTML = '
Empty directory
'; return; } entries.forEach(entry => { const row = document.createElement("div"); row.style.display = "flex"; row.style.alignItems = "center"; row.style.gap = "0.75rem"; row.style.padding = "0.6rem 0.85rem"; row.style.borderRadius = "6px"; row.style.cursor = "pointer"; row.style.transition = "background 0.2s"; row.addEventListener("mouseenter", () => { row.style.background = "rgba(255, 255, 255, 0.05)"; }); row.addEventListener("mouseleave", () => { row.style.background = ""; }); const icon = document.createElement("i"); if (entry.name === "..") { icon.className = "fa-solid fa-arrow-turn-up"; icon.style.color = "var(--text-muted)"; icon.style.transform = "rotate(-90deg)"; } else { icon.className = "fa-solid fa-folder"; icon.style.color = "var(--primary)"; } row.appendChild(icon); const label = document.createElement("span"); label.textContent = entry.name; label.style.fontSize = "0.9rem"; label.style.fontFamily = "monospace"; row.appendChild(label); row.addEventListener("click", () => { loadFolderDir(entry.path); }); listContainer.appendChild(row); }); } catch (err) { listContainer.innerHTML = `
Error: ${err.message}
`; if (statusDiv) statusDiv.textContent = err.message; } } async function createFolder() { const nameInput = document.getElementById("new-folder-name"); const statusDiv = document.getElementById("folder-modal-status"); if (!nameInput || !nameInput.value.trim() || !currentBrowsePath) return; const folderName = nameInput.value.trim(); if (statusDiv) statusDiv.textContent = ""; try { const res = await fetch("/api/system/create-dir", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parent_path: currentBrowsePath, name: folderName }) }); if (res.ok) { nameInput.value = ""; const data = await res.json(); loadFolderDir(data.path); } else { const err = await res.json().catch(() => ({})); if (statusDiv) statusDiv.textContent = err.detail || "Failed to create folder."; } } catch (err) { console.error(err); if (statusDiv) statusDiv.textContent = "Network error creating folder."; } } async function loadTempInfo() { const listContainer = document.getElementById("temp-files-list"); const totalSizeBadge = document.getElementById("temp-total-size"); const folderCountBadge = document.getElementById("temp-folder-count"); if (!listContainer) return; try { const res = await fetch("/api/system/temp-info"); if (!res.ok) return; const data = await res.json(); const folders = data.temp_folders || []; let totalSize = 0; folders.forEach(f => totalSize += (f.size_bytes || 0)); if (totalSizeBadge) totalSizeBadge.textContent = formatBytes(totalSize); if (folderCountBadge) folderCountBadge.textContent = folders.length; if (folders.length === 0) { listContainer.innerHTML = `

No temporary directories found.

`; return; } listContainer.innerHTML = ""; folders.forEach(f => { const item = document.createElement("div"); item.className = "queue-item"; const sizeStr = formatBytes(f.size_bytes || 0); const isActive = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(f.status); let statusLabel = f.status.toUpperCase(); let statusClass = f.status; if (f.status === "orphaned") { statusLabel = "Orphaned (No Job)"; statusClass = "interrupted"; } item.innerHTML = `
${f.path}
Video: ${f.video_name} Size: ${sizeStr} (${f.file_count} files)
${statusLabel}
${f.status === "orphaned" ? ` ` : ''}
`; const deleteBtn = item.querySelector(".btn-delete-temp"); if (deleteBtn && !isActive) { deleteBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (confirm(`Are you sure you want to delete this temporary folder?\nPath: ${f.path}`)) { try { const cleanRes = await fetch("/api/system/temp-cleanup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paths: [f.path] }) }); if (cleanRes.ok) { loadTempInfo(); loadQueue(); } else { alert("Failed to delete temp folder."); } } catch (err) { console.error(err); } } }); } const reimportBtn = item.querySelector(".btn-reimport-temp"); if (reimportBtn) { reimportBtn.addEventListener("click", async (e) => { e.stopPropagation(); showReimportModal(f.job_id, f.path); }); } listContainer.appendChild(item); }); } catch (err) { console.error("Error loading temp files info:", err); } } async function loadClusterSettings() { const nodeRoleSelect = document.getElementById("node-role-select"); const coordinatorUrlInput = document.getElementById("coordinator-url-input"); const workerUrlInput = document.getElementById("worker-url-input"); const useSharedStorageCheckbox = document.getElementById("use-shared-storage-checkbox"); const sharedStoragePathInput = document.getElementById("shared-storage-path-input"); const smbSharesList = document.getElementById("smb-shares-list"); const coordUrlGroup = document.getElementById("coord-url-group"); const workerUrlGroup = document.getElementById("worker-url-group"); const sharedStoragePathGroup = document.getElementById("shared-storage-path-group"); try { const res = await fetch("/api/settings"); if (!res.ok) return; const settings = await res.json(); // Node role if (nodeRoleSelect) { nodeRoleSelect.value = settings.role || "standalone"; const role = nodeRoleSelect.value; if (coordUrlGroup) coordUrlGroup.style.display = role === "worker" ? "block" : "none"; if (workerUrlGroup) workerUrlGroup.style.display = role === "worker" || role === "coordinator" ? "block" : "none"; } if (coordinatorUrlInput) coordinatorUrlInput.value = settings.coordinator_url || ""; if (workerUrlInput) workerUrlInput.value = settings.worker_node_url || "http://localhost:8000"; // Shared Storage if (useSharedStorageCheckbox) { useSharedStorageCheckbox.checked = !!settings.use_shared_storage; if (sharedStoragePathGroup) sharedStoragePathGroup.style.display = settings.use_shared_storage ? "flex" : "none"; } if (sharedStoragePathInput) sharedStoragePathInput.value = settings.shared_storage_path || ""; // Render SMB Shares if (smbSharesList) { const shares = settings.smb_shares || []; if (shares.length === 0) { smbSharesList.innerHTML = `
No mapped network shares.
`; } else { smbSharesList.innerHTML = ""; shares.forEach(share => { const item = document.createElement("div"); item.className = "queue-item"; item.style.padding = "0.75rem"; item.style.display = "flex"; item.style.justifyContent = "space-between"; item.style.alignItems = "center"; item.style.gap = "1rem"; item.innerHTML = `
${share.name}
//${share.server}/${share.share} Mount: ${share.mount_path}
`; const deleteBtn = item.querySelector(".btn-delete-smb"); if (deleteBtn) { deleteBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (confirm(`Are you sure you want to unmap SMB share "${share.name}"?`)) { await deleteSmbShare(share.name); } }); } smbSharesList.appendChild(item); }); } } } catch (err) { console.error("Error loading cluster settings:", err); } } async function deleteSmbShare(shareName) { try { const getRes = await fetch("/api/settings"); if (!getRes.ok) throw new Error("Failed to load settings."); const settings = await getRes.json(); if (settings.smb_shares) { settings.smb_shares = settings.smb_shares.filter(s => s.name !== shareName); } const saveRes = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) }); if (saveRes.ok) { loadClusterSettings(); } else { const err = await saveRes.json().catch(() => ({})); alert(`Failed to delete SMB share: ${err.detail || "Unknown error"}`); } } catch (err) { console.error("Error deleting SMB share:", err); alert("Network error deleting SMB share."); } } async function loadDefaultTempDir() { try { const res = await fetch("/api/settings"); if (!res.ok) return; const settings = await res.json(); // Temp dir settings const savedDir = settings.default_temp_dir || ""; const defaultInput = document.getElementById("default-temp-dir-input"); if (defaultInput) defaultInput.value = savedDir; const perJobInput = document.getElementById("temp-dir-input"); if (perJobInput && savedDir && !perJobInput.value.trim()) { perJobInput.placeholder = `Default: ${savedDir} (override here)`; } // Output dir settings const savedOutputDir = settings.default_output_dir || ""; const defaultOutputDirInput = document.getElementById("default-output-dir-input"); if (defaultOutputDirInput) defaultOutputDirInput.value = savedOutputDir; const perJobOutputDirInput = document.getElementById("output-dir-input"); if (perJobOutputDirInput && savedOutputDir && !perJobOutputDirInput.value.trim()) { perJobOutputDirInput.placeholder = `Default: ${savedOutputDir} (override here)`; } } catch (err) { console.error("Failed to load settings:", err); } } async function showReimportModal(jobId, tempPath) { const modal = document.getElementById("reimport-modal"); const jobIdInput = document.getElementById("reimport-job-id"); const tempDirInput = document.getElementById("reimport-temp-dir"); const videoSelect = document.getElementById("reimport-video-select"); if (!modal || !jobIdInput || !tempDirInput || !videoSelect) return; jobIdInput.value = jobId; // Resolve custom parent directory if tempPath contains jobId const parentDir = tempPath.substring(0, tempPath.lastIndexOf("/" + jobId)); tempDirInput.value = parentDir; // Populate video files dropdown videoSelect.innerHTML = ''; try { const res = await fetch("/api/uploads"); if (res.ok) { const uploads = await res.json(); if (uploads.length === 0) { videoSelect.innerHTML = ''; } else { videoSelect.innerHTML = ''; uploads.forEach(u => { // Reimport requires the absolute path to the uploaded video file const videoPath = `/var/home/david/Documents/vscode/video_upscaler/uploads/${u.original_filename}`; const opt = document.createElement("option"); opt.value = videoPath; opt.textContent = `${u.original_filename} (${formatBytes(u.size_bytes)})`; videoSelect.appendChild(opt); }); // Try to auto-match files by looking for standard uploaded prefix or creation time try { const matches = Array.from(videoSelect.options).filter(opt => opt.value.includes("8d4d3e0e-e5a5-4101-9e2d-550f96c1674f")); if (matches.length > 0) { matches[0].selected = true; } } catch(e) {} } } else { videoSelect.innerHTML = ''; } } catch (err) { console.error(err); videoSelect.innerHTML = ''; } modal.style.display = "flex"; document.body.style.overflow = "hidden"; } // Re-import modal close event const reimportModal = document.getElementById("reimport-modal"); const reimportCloseBtn = document.getElementById("reimport-modal-close-btn"); if (reimportModal && reimportCloseBtn) { reimportCloseBtn.addEventListener("click", () => { reimportModal.style.display = "none"; document.body.style.overflow = ""; }); reimportModal.addEventListener("click", (e) => { if (e.target === reimportModal) { reimportModal.style.display = "none"; document.body.style.overflow = ""; } }); } // Re-import form submission handler const reimportForm = document.getElementById("reimport-form"); if (reimportForm) { reimportForm.addEventListener("submit", async (e) => { e.preventDefault(); const jobId = document.getElementById("reimport-job-id").value; const tempDir = document.getElementById("reimport-temp-dir").value; const videoPath = document.getElementById("reimport-video-select").value; const model = document.getElementById("reimport-model-select").value; const faceRestoration = document.getElementById("reimport-face-restoration").checked; const rifeInterpolation = document.getElementById("reimport-rife-interpolation").checked; const audioDenoise = document.getElementById("reimport-audio-denoise").checked; if (!videoPath) { alert("Please select a video file."); return; } try { const res = await fetch(`/api/system/temp-reimport/${jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ video_path: videoPath, model: model, scale: 4, ai_face_restoration: faceRestoration, ai_rife_interpolation: rifeInterpolation, ai_audio_denoise: audioDenoise, temp_dir: tempDir }) }); if (res.ok) { reimportModal.style.display = "none"; document.body.style.overflow = ""; loadQueue(); loadTempInfo(); } else { const err = await res.json(); alert("Failed to reimport job: " + (err.detail || "unknown error")); } } catch (err) { console.error(err); alert("Network error occurred."); } }); } initializeApp(); /* ---------------------------------------------------- 7. Server File Browser logic ---------------------------------------------------- */ let selectedServerFiles = []; let fileBrowserCurrentPath = ""; window.selectedServerFilePath = null; window.selectedServerFilePaths = null; function openServerFileBrowser() { const modal = document.getElementById("file-browser-modal"); if (!modal) return; selectedServerFiles = []; document.getElementById("file-browser-selected-count").textContent = "0"; document.getElementById("file-browser-select-btn").disabled = true; document.getElementById("file-browser-modal-status").textContent = ""; modal.style.display = "flex"; document.body.style.overflow = "hidden"; loadFileBrowserDir(""); } function closeServerFileBrowser() { const modal = document.getElementById("file-browser-modal"); if (modal) { modal.style.display = "none"; document.body.style.overflow = ""; } } async function loadFileBrowserDir(path) { const listContainer = document.getElementById("file-browser-list"); const currentPathInput = document.getElementById("file-browser-current-path"); const statusDiv = document.getElementById("file-browser-modal-status"); if (!listContainer) return; listContainer.innerHTML = '
Loading...
'; try { const url = `/api/system/browse-dir?path=${encodeURIComponent(path)}&show_files=true`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Failed to load directory."); } const data = await res.json(); fileBrowserCurrentPath = data.current_path; if (currentPathInput) currentPathInput.value = fileBrowserCurrentPath; listContainer.innerHTML = ""; const entries = data.entries || []; if (entries.length === 0) { listContainer.innerHTML = '
Empty directory
'; return; } // Parent directory entry first const parentEntry = entries.find(e => e.name === ".."); const restEntries = entries.filter(e => e.name !== ".."); const allEntries = parentEntry ? [parentEntry, ...restEntries] : restEntries; allEntries.forEach(entry => { const row = document.createElement("div"); row.style.display = "flex"; row.style.alignItems = "center"; row.style.gap = "0.75rem"; row.style.padding = "0.6rem 0.85rem"; row.style.borderRadius = "6px"; row.style.cursor = "pointer"; row.style.transition = "background 0.2s"; row.style.borderBottom = "1px solid rgba(255, 255, 255, 0.02)"; const isSelected = selectedServerFiles.includes(entry.path); if (isSelected) { row.style.background = "rgba(var(--primary-rgb), 0.15)"; row.style.borderLeft = "3px solid var(--primary)"; } row.addEventListener("mouseenter", () => { if (!selectedServerFiles.includes(entry.path)) { row.style.background = "rgba(255, 255, 255, 0.05)"; } }); row.addEventListener("mouseleave", () => { if (!selectedServerFiles.includes(entry.path)) { row.style.background = ""; } }); const icon = document.createElement("i"); if (entry.name === "..") { icon.className = "fa-solid fa-arrow-turn-up"; icon.style.color = "var(--text-muted)"; icon.style.transform = "rotate(-90deg)"; } else if (entry.is_dir) { icon.className = "fa-solid fa-folder"; icon.style.color = "var(--primary)"; } else { icon.className = "fa-solid fa-file-video"; icon.style.color = "#00bcd4"; } row.appendChild(icon); const label = document.createElement("span"); label.textContent = entry.name; label.style.fontSize = "0.9rem"; label.style.fontFamily = "monospace"; row.appendChild(label); row.addEventListener("click", () => { if (entry.name === "..") { loadFileBrowserDir(entry.path); } else if (entry.is_dir) { loadFileBrowserDir(entry.path); } else { const idx = selectedServerFiles.indexOf(entry.path); if (idx > -1) { selectedServerFiles.splice(idx, 1); row.style.background = ""; row.style.borderLeft = ""; } else { selectedServerFiles.push(entry.path); row.style.background = "rgba(var(--primary-rgb), 0.15)"; row.style.borderLeft = "3px solid var(--primary)"; } const countSpan = document.getElementById("file-browser-selected-count"); if (countSpan) countSpan.textContent = selectedServerFiles.length; const selectBtn = document.getElementById("file-browser-select-btn"); if (selectBtn) selectBtn.disabled = selectedServerFiles.length === 0; } }); listContainer.appendChild(row); }); } catch (err) { if (statusDiv) statusDiv.textContent = err.message; listContainer.innerHTML = '
Error loading directory
'; } } // Wire up events once DOM is ready or after initializeApp setTimeout(() => { const browseServerBtn = document.getElementById("browse-server-btn"); const fileBrowserCloseBtn = document.getElementById("file-browser-modal-close-btn"); const fileBrowserCancelBtn = document.getElementById("file-browser-cancel-btn"); const fileBrowserSelectBtn = document.getElementById("file-browser-select-btn"); const fileBrowserUpBtn = document.getElementById("file-browser-up-btn"); if (browseServerBtn) { browseServerBtn.addEventListener("click", openServerFileBrowser); } if (fileBrowserCloseBtn) { fileBrowserCloseBtn.addEventListener("click", closeServerFileBrowser); } if (fileBrowserCancelBtn) { fileBrowserCancelBtn.addEventListener("click", closeServerFileBrowser); } // Close modal when clicking outside card const fileBrowserModal = document.getElementById("file-browser-modal"); if (fileBrowserModal) { fileBrowserModal.addEventListener("click", (e) => { if (e.target === fileBrowserModal) { closeServerFileBrowser(); } }); } if (fileBrowserUpBtn) { fileBrowserUpBtn.addEventListener("click", () => { if (fileBrowserCurrentPath) { const parts = fileBrowserCurrentPath.split("/"); if (parts.length > 1) { parts.pop(); const parent = parts.join("/") || "/"; loadFileBrowserDir(parent); } } }); } if (fileBrowserSelectBtn) { fileBrowserSelectBtn.addEventListener("click", async () => { if (selectedServerFiles.length === 0) return; fileBrowserSelectBtn.disabled = true; fileBrowserSelectBtn.innerHTML = ' Analyzing...'; try { if (selectedServerFiles.length === 1) { const path = selectedServerFiles[0]; const url = `/api/system/file-metadata?path=${encodeURIComponent(path)}`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Failed to read file metadata."); } const data = await res.json(); window.selectedServerFilePath = path; window.selectedServerFilePaths = null; uploadFileId = null; useUploadedVideo({ file_id: null, original_filename: data.original_filename, size_bytes: data.size_bytes, metadata: data.metadata }); } else { const entries = []; for (const path of selectedServerFiles) { const url = `/api/system/file-metadata?path=${encodeURIComponent(path)}`; const res = await fetch(url); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(`Failed to read metadata for: ${path}`); } const data = await res.json(); entries.push({ file_id: null, server_file_path: path, original_filename: data.original_filename, size_bytes: data.size_bytes, metadata: data.metadata }); } window.selectedServerFilePath = null; window.selectedServerFilePaths = selectedServerFiles; uploadFileId = null; useBatchUploadedVideos(entries); } closeServerFileBrowser(); } catch (err) { const statusDiv = document.getElementById("file-browser-modal-status"); if (statusDiv) statusDiv.textContent = err.message; } finally { fileBrowserSelectBtn.disabled = false; fileBrowserSelectBtn.innerHTML = 'Select File(s)'; } }); } }, 500);