diff --git a/app/main.py b/app/main.py index d5a54e0..40c43e1 100644 --- a/app/main.py +++ b/app/main.py @@ -166,9 +166,28 @@ class PreviewRequest(BaseModel): # Endpoints +UPLOAD_METADATA_FILE = os.path.join(upscaler.UPLOAD_DIR, "metadata.json") + +def load_upload_metadata(): + if os.path.exists(UPLOAD_METADATA_FILE): + try: + with open(UPLOAD_METADATA_FILE, "r") as f: + return json.load(f) + except Exception: + pass + return {} + +def save_upload_metadata(metadata): + try: + with open(UPLOAD_METADATA_FILE, "w") as f: + json.dump(metadata, f, indent=4) + except Exception: + pass + @app.post("/api/upload") async def upload_video(file: UploadFile = File(...)): """Upload video to workspace directory and parse metadata""" + import time file_id = str(uuid.uuid4()) ext = os.path.splitext(file.filename)[1].lower() if ext not in [".mp4", ".mkv", ".avi", ".mov", ".webm"]: @@ -186,6 +205,18 @@ async def upload_video(file: UploadFile = File(...)): os.remove(save_path) raise HTTPException(status_code=400, detail="Could not read video file metadata. File may be corrupted.") + # Save upload metadata + metadata = load_upload_metadata() + metadata[file_id] = { + "file_id": file_id, + "original_filename": file.filename, + "ext": ext, + "size_bytes": os.path.getsize(save_path), + "upload_time": time.time(), + "metadata": info + } + save_upload_metadata(metadata) + return { "file_id": file_id, "filename": file.filename, @@ -193,6 +224,82 @@ async def upload_video(file: UploadFile = File(...)): "metadata": info } +@app.get("/api/uploads") +def list_uploads(): + """List all uploaded video source files""" + metadata = load_upload_metadata() + valid_uploads = [] + metadata_updated = False + + if os.path.exists(upscaler.UPLOAD_DIR): + all_files = os.listdir(upscaler.UPLOAD_DIR) + + for filename in all_files: + if filename == "metadata.json": + continue + + file_path = os.path.join(upscaler.UPLOAD_DIR, filename) + file_id, ext = os.path.splitext(filename) + + if file_id in metadata: + valid_uploads.append(metadata[file_id]) + else: + info = upscaler.get_video_info(file_path) + if info: + entry = { + "file_id": file_id, + "original_filename": filename, + "ext": ext, + "size_bytes": os.path.getsize(file_path), + "upload_time": os.path.getmtime(file_path), + "metadata": info + } + metadata[file_id] = entry + valid_uploads.append(entry) + metadata_updated = True + + # Clean up missing files from metadata + for fid in list(metadata.keys()): + ext = metadata[fid].get("ext", ".mp4") + expected_file = os.path.join(upscaler.UPLOAD_DIR, f"{fid}{ext}") + if not os.path.exists(expected_file): + del metadata[fid] + metadata_updated = True + + if metadata_updated: + save_upload_metadata(metadata) + + # Sort by upload time desc + valid_uploads.sort(key=lambda x: x.get("upload_time", 0), reverse=True) + return valid_uploads + +@app.delete("/api/uploads/{file_id}") +@app.post("/api/uploads/delete/{file_id}") +def delete_upload(file_id: str): + """Delete an uploaded source file""" + metadata = load_upload_metadata() + if file_id not in metadata: + exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"] + file_path = None + for ext in exts: + p = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}") + if os.path.exists(p): + file_path = p + break + if not file_path: + raise HTTPException(status_code=404, detail="Upload file not found.") + os.remove(file_path) + return {"file_id": file_id, "status": "deleted"} + + ext = metadata[file_id].get("ext", ".mp4") + file_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}") + if os.path.exists(file_path): + os.remove(file_path) + + del metadata[file_id] + save_upload_metadata(metadata) + return {"file_id": file_id, "status": "deleted"} + @app.post("/api/upscale/start") def start_upscale(req: StartUpscaleRequest): """Queue upscaling task""" @@ -405,10 +512,6 @@ def delete_job(job_id: str): if not job: raise HTTPException(status_code=404, detail="Job not found.") - # Safely delete uploaded input file - if job.video_path: - safe_delete_file(job.video_path) - # Safely delete original preview video if present for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]: orig_prev_path = os.path.join(upscaler.OUTPUT_DIR, f"original_{job_id}{ext}") @@ -459,6 +562,9 @@ def purge_all_jobs(): # Reset in-memory database jobs_db.clear() + # Reset upload metadata file + save_upload_metadata({}) + return {"status": "all purged"} # Websocket endpoint for real-time progress updates diff --git a/static/app.js b/static/app.js index 4a1487f..3726b48 100644 --- a/static/app.js +++ b/static/app.js @@ -344,6 +344,7 @@ function handleVideoUpload(file) { // 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(); @@ -1025,12 +1026,6 @@ async function checkActiveJobs() { if (activeJob) { currentJobId = activeJob.job_id; - - document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active")); - stepProgress.classList.add("active"); - jobIdDisplay.textContent = currentJobId; - - connectProgressWebSocket(currentJobId); } } catch (err) { console.error("Error checking active jobs:", err); @@ -1292,7 +1287,7 @@ async function loadQueue() { const jobs = await res.json(); const displayJobs = jobs.filter(job => - ["queued", "analyzing", "extracting", "upscaling", "assembling", "failed", "cancelled"].includes(job.status) + ["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled"].includes(job.status) ); if (displayJobs.length === 0) { @@ -1323,19 +1318,25 @@ async function loadQueue() { if (job.status === "failed" && job.error) { metaHtml += ` Error: ${job.error}`; - } else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled") { + } else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed") { metaHtml += ` ETA: ${job.eta || 'Calculating...'}`; } let actionsHtml = ""; if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) { actionsHtml = ` + `; } else { actionsHtml = ` + @@ -1398,6 +1399,27 @@ async function loadQueue() { }); } + const viewBtn = item.querySelector(".btn-view-job"); + if (viewBtn) { + viewBtn.addEventListener("click", () => { + 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 = `/api/download/${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) { @@ -1473,11 +1495,173 @@ async function loadModels() { } } +function useUploadedVideo(entry) { + 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"); +} + +async function loadUploadsBrowser() { + const browserList = document.getElementById("uploads-browser-list"); + if (!browserList) return; + + 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!

+
+ `; + return; + } + + browserList.innerHTML = ""; + + 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} +
+
+
+
+ + +
+
+ `; + + 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); + }); + } catch (err) { + console.error("Error loading uploads browser:", err); + } +} + async function initializeApp() { await loadModels(); await checkActiveJobs(); await loadGallery(); // Load enhancement gallery at page load await loadQueue(); // Load active jobs queue at page load + await loadUploadsBrowser(); // Load previously uploaded videos browser + + // 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; + loadUploadsBrowser(); + }); + } } initializeApp(); diff --git a/static/index.html b/static/index.html index f33d67a..ad1aa08 100644 --- a/static/index.html +++ b/static/index.html @@ -49,6 +49,23 @@ + + +
+
+

Previously Uploaded Source Videos

+ +
+
+ +
+ +

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

+
+
+
@@ -455,7 +472,10 @@ -
+
+