From 969af7284e5c7f76e7a93e74c474fad5e5b5d417 Mon Sep 17 00:00:00 2001 From: David Kifer Date: Wed, 1 Jul 2026 10:53:19 -0400 Subject: [PATCH] Implement local server file browsing and selection to bypass client uploads --- app/main.py | 130 +++++++++++++++------- static/app.js | 273 +++++++++++++++++++++++++++++++++++++++++++++- static/index.html | 47 +++++++- 3 files changed, 403 insertions(+), 47 deletions(-) diff --git a/app/main.py b/app/main.py index aa7ac4d..fce0485 100644 --- a/app/main.py +++ b/app/main.py @@ -261,7 +261,8 @@ worker_thread.start() class StartUpscaleRequest(BaseModel): - file_id: str + file_id: str | None = None + server_file_path: str | None = None model: str = "realesr-animevideov3" scale: int = 4 tile_size: int = 256 @@ -290,7 +291,9 @@ class StartUpscaleRequest(BaseModel): output_dir: str | None = None class StartBatchUpscaleRequest(BaseModel): - file_ids: List[str] + file_ids: List[str] | None = None + server_file_paths: List[str] | None = None + model: str = "realesr-animevideov3" scale: int = 4 tile_size: int = 256 @@ -319,7 +322,8 @@ class StartBatchUpscaleRequest(BaseModel): output_dir: str | None = None class PreviewRequest(BaseModel): - file_id: str + file_id: str | None = None + server_file_path: str | None = None timestamp_sec: float = 1.0 model: str = "realesr-animevideov3" scale: int = 4 @@ -327,6 +331,7 @@ class PreviewRequest(BaseModel): gpu_ids: str | None = None + # Endpoints UPLOAD_METADATA_FILE = os.path.join(upscaler.UPLOAD_DIR, "metadata.json") @@ -467,22 +472,31 @@ def start_upscale(req: StartUpscaleRequest): """Queue upscaling task""" # Find uploaded file file_path = None - exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] - for ext in exts: - test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}") - if os.path.exists(test_path): - file_path = test_path - break + if req.server_file_path: + if os.path.exists(req.server_file_path) and os.path.isfile(req.server_file_path): + file_path = req.server_file_path + else: + raise HTTPException(status_code=404, detail="Server file path not found.") + else: + if not req.file_id: + raise HTTPException(status_code=400, detail="Either file_id or server_file_path must be provided.") + exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] + for ext in exts: + test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}") + if os.path.exists(test_path): + file_path = test_path + break if not file_path: raise HTTPException(status_code=404, detail="Uploaded file not found.") metadata = load_upload_metadata() source_filename = None - if req.file_id in metadata: + if req.file_id and req.file_id in metadata: source_filename = metadata[req.file_id].get("original_filename") if not source_filename and file_path: source_filename = os.path.basename(file_path) + settings = load_settings() output_dir = req.output_dir or settings.get("default_output_dir") or None @@ -545,17 +559,27 @@ def start_upscale_batch(req: StartBatchUpscaleRequest): """Queue multiple upscaling tasks with one set of settings""" queued_jobs = [] - # 1. Validation loop - for file_id in req.file_ids: - file_path = None - exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] - for ext in exts: - test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}") - if os.path.exists(test_path): - file_path = test_path - break - if not file_path: - raise HTTPException(status_code=404, detail=f"Uploaded file {file_id} not found.") + file_sources = [] # list of (file_id, file_path) + if req.server_file_paths: + for path in req.server_file_paths: + if os.path.exists(path) and os.path.isfile(path): + file_sources.append((None, path)) + else: + raise HTTPException(status_code=404, detail=f"Server file {path} not found.") + else: + if not req.file_ids: + raise HTTPException(status_code=400, detail="Either file_ids or server_file_paths must be provided.") + for file_id in req.file_ids: + file_path = None + exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] + for ext in exts: + test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}") + if os.path.exists(test_path): + file_path = test_path + break + if not file_path: + raise HTTPException(status_code=404, detail=f"Uploaded file {file_id} not found.") + file_sources.append((file_id, file_path)) settings = load_settings() output_dir = req.output_dir or settings.get("default_output_dir") or None @@ -563,20 +587,14 @@ def start_upscale_batch(req: StartBatchUpscaleRequest): output_dir = output_dir.strip() # 2. Queueing loop - for file_id in req.file_ids: - file_path = None - for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]: - test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}") - if os.path.exists(test_path): - file_path = test_path - break - + for file_id, file_path in file_sources: metadata = load_upload_metadata() source_filename = None - if file_id in metadata: + if file_id and file_id in metadata: source_filename = metadata[file_id].get("original_filename") if not source_filename and file_path: source_filename = os.path.basename(file_path) + job_id = str(uuid.uuid4()) job = upscaler.UpscaleJob( @@ -674,15 +692,24 @@ def cancel_job(job_id: str): async def generate_preview(req: PreviewRequest): """Generate high quality upscaled single frame preview""" file_path = None - exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] - for ext in exts: - test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}") - if os.path.exists(test_path): - file_path = test_path - break + if req.server_file_path: + if os.path.exists(req.server_file_path) and os.path.isfile(req.server_file_path): + file_path = req.server_file_path + else: + raise HTTPException(status_code=404, detail="Server file path not found.") + else: + if not req.file_id: + raise HTTPException(status_code=400, detail="Either file_id or server_file_path must be provided.") + exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] + for ext in exts: + test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}") + if os.path.exists(test_path): + file_path = test_path + break if not file_path: raise HTTPException(status_code=404, detail="Video file not found.") + preview_id = str(uuid.uuid4()) preview_temp_dir = os.path.join(upscaler.TEMP_DIR, f"preview_{preview_id}") @@ -1414,7 +1441,7 @@ class CreateDirRequest(BaseModel): name: str @app.get("/api/system/browse-dir") -def browse_dir(path: str = ""): +def browse_dir(path: str = "", show_files: bool = False): """Browse directories on the system""" if not path: path = os.path.expanduser("~") @@ -1438,6 +1465,8 @@ def browse_dir(path: str = ""): "is_dir": True }) + video_extensions = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.m4v', '.wmv', '.mpg', '.mpeg'} + for name in sorted(os.listdir(path)): full_path = os.path.join(path, name) try: @@ -1447,6 +1476,14 @@ def browse_dir(path: str = ""): "path": full_path, "is_dir": True }) + elif show_files: + _, ext = os.path.splitext(name.lower()) + if ext in video_extensions: + entries.append({ + "name": name, + "path": full_path, + "is_dir": False + }) except PermissionError: continue @@ -1457,6 +1494,25 @@ def browse_dir(path: str = ""): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@app.get("/api/system/file-metadata") +def get_file_metadata(path: str): + """Get video metadata for a local server file""" + if not os.path.exists(path) or not os.path.isfile(path): + raise HTTPException(status_code=404, detail="File not found.") + + info = upscaler.get_video_info(path) + if not info: + raise HTTPException(status_code=400, detail="Could not read video file metadata.") + + return { + "original_filename": os.path.basename(path), + "path": path, + "size_bytes": os.path.getsize(path), + "metadata": info + } + + + @app.post("/api/system/create-dir") def create_dir(req: CreateDirRequest): """Create a new directory""" diff --git a/static/app.js b/static/app.js index ffe43f0..29f2416 100644 --- a/static/app.js +++ b/static/app.js @@ -595,7 +595,7 @@ window.addEventListener("mousemove", (e) => { // Generate single frame preview trigger generatePreviewBtn.addEventListener("click", async () => { - if (!uploadFileId) return; + if (!uploadFileId && !window.selectedServerFilePath) return; const loaderTitle = document.getElementById("preview-loader-title"); const loaderContainer = document.getElementById("preview-loader-progress-container"); @@ -618,7 +618,8 @@ generatePreviewBtn.addEventListener("click", async () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - file_id: uploadFileId, + file_id: uploadFileId || null, + server_file_path: window.selectedServerFilePath || null, timestamp_sec: parseFloat(timelineSlider.value), model: modelSelect.value, scale: parseInt(scaleRange.value), @@ -801,7 +802,7 @@ function initComparisonSlider() { 6. Video Pipeline Jobs Orchestration ---------------------------------------------------- */ startUpscaleBtn.addEventListener("click", async () => { - if (!uploadFileId) return; + if (!uploadFileId && !window.selectedServerFilePath && (!window.selectedServerFilePaths || window.selectedServerFilePaths.length === 0)) return; startUpscaleBtn.disabled = true; startUpscaleBtn.innerHTML = `
Starting Job...`; @@ -821,12 +822,13 @@ startUpscaleBtn.addEventListener("click", async () => { const aiAudioDenoise = document.getElementById("ai-audio-denoise"); try { - if (window.isBatchMode && window.batchFileIds && window.batchFileIds.length > 0) { + 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, + 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), @@ -857,6 +859,7 @@ startUpscaleBtn.addEventListener("click", async () => { 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"); @@ -875,7 +878,8 @@ startUpscaleBtn.addEventListener("click", async () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - file_id: uploadFileId, + file_id: uploadFileId || null, + server_file_path: window.selectedServerFilePath || null, model: modelSelect.value, scale: parseInt(scaleRange.value), tile_size: parseInt(tileRange.value), @@ -2077,6 +2081,8 @@ async function initializeApp() { 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(); }); } @@ -3052,3 +3058,258 @@ if (reimportForm) { } 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); + diff --git a/static/index.html b/static/index.html index de3fd8b..0764193 100644 --- a/static/index.html +++ b/static/index.html @@ -46,10 +46,15 @@

Drag & Drop Video File(s)

-

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

- +

Supports MP4, MKV, AVI, MOV, WEBM

+
+ + +
@@ -871,6 +876,40 @@ + + + +