Implement local server file browsing and selection to bypass client uploads
This commit is contained in:
+93
-37
@@ -261,7 +261,8 @@ worker_thread.start()
|
|||||||
|
|
||||||
|
|
||||||
class StartUpscaleRequest(BaseModel):
|
class StartUpscaleRequest(BaseModel):
|
||||||
file_id: str
|
file_id: str | None = None
|
||||||
|
server_file_path: str | None = None
|
||||||
model: str = "realesr-animevideov3"
|
model: str = "realesr-animevideov3"
|
||||||
scale: int = 4
|
scale: int = 4
|
||||||
tile_size: int = 256
|
tile_size: int = 256
|
||||||
@@ -290,7 +291,9 @@ class StartUpscaleRequest(BaseModel):
|
|||||||
output_dir: str | None = None
|
output_dir: str | None = None
|
||||||
|
|
||||||
class StartBatchUpscaleRequest(BaseModel):
|
class StartBatchUpscaleRequest(BaseModel):
|
||||||
file_ids: List[str]
|
file_ids: List[str] | None = None
|
||||||
|
server_file_paths: List[str] | None = None
|
||||||
|
|
||||||
model: str = "realesr-animevideov3"
|
model: str = "realesr-animevideov3"
|
||||||
scale: int = 4
|
scale: int = 4
|
||||||
tile_size: int = 256
|
tile_size: int = 256
|
||||||
@@ -319,7 +322,8 @@ class StartBatchUpscaleRequest(BaseModel):
|
|||||||
output_dir: str | None = None
|
output_dir: str | None = None
|
||||||
|
|
||||||
class PreviewRequest(BaseModel):
|
class PreviewRequest(BaseModel):
|
||||||
file_id: str
|
file_id: str | None = None
|
||||||
|
server_file_path: str | None = None
|
||||||
timestamp_sec: float = 1.0
|
timestamp_sec: float = 1.0
|
||||||
model: str = "realesr-animevideov3"
|
model: str = "realesr-animevideov3"
|
||||||
scale: int = 4
|
scale: int = 4
|
||||||
@@ -327,6 +331,7 @@ class PreviewRequest(BaseModel):
|
|||||||
gpu_ids: str | None = None
|
gpu_ids: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
UPLOAD_METADATA_FILE = os.path.join(upscaler.UPLOAD_DIR, "metadata.json")
|
UPLOAD_METADATA_FILE = os.path.join(upscaler.UPLOAD_DIR, "metadata.json")
|
||||||
|
|
||||||
@@ -467,22 +472,31 @@ def start_upscale(req: StartUpscaleRequest):
|
|||||||
"""Queue upscaling task"""
|
"""Queue upscaling task"""
|
||||||
# Find uploaded file
|
# Find uploaded file
|
||||||
file_path = None
|
file_path = None
|
||||||
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
|
if req.server_file_path:
|
||||||
for ext in exts:
|
if os.path.exists(req.server_file_path) and os.path.isfile(req.server_file_path):
|
||||||
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}")
|
file_path = req.server_file_path
|
||||||
if os.path.exists(test_path):
|
else:
|
||||||
file_path = test_path
|
raise HTTPException(status_code=404, detail="Server file path not found.")
|
||||||
break
|
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:
|
if not file_path:
|
||||||
raise HTTPException(status_code=404, detail="Uploaded file not found.")
|
raise HTTPException(status_code=404, detail="Uploaded file not found.")
|
||||||
|
|
||||||
metadata = load_upload_metadata()
|
metadata = load_upload_metadata()
|
||||||
source_filename = None
|
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")
|
source_filename = metadata[req.file_id].get("original_filename")
|
||||||
if not source_filename and file_path:
|
if not source_filename and file_path:
|
||||||
source_filename = os.path.basename(file_path)
|
source_filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
output_dir = req.output_dir or settings.get("default_output_dir") or None
|
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"""
|
"""Queue multiple upscaling tasks with one set of settings"""
|
||||||
queued_jobs = []
|
queued_jobs = []
|
||||||
|
|
||||||
# 1. Validation loop
|
file_sources = [] # list of (file_id, file_path)
|
||||||
for file_id in req.file_ids:
|
if req.server_file_paths:
|
||||||
file_path = None
|
for path in req.server_file_paths:
|
||||||
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
|
if os.path.exists(path) and os.path.isfile(path):
|
||||||
for ext in exts:
|
file_sources.append((None, path))
|
||||||
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
|
else:
|
||||||
if os.path.exists(test_path):
|
raise HTTPException(status_code=404, detail=f"Server file {path} not found.")
|
||||||
file_path = test_path
|
else:
|
||||||
break
|
if not req.file_ids:
|
||||||
if not file_path:
|
raise HTTPException(status_code=400, detail="Either file_ids or server_file_paths must be provided.")
|
||||||
raise HTTPException(status_code=404, detail=f"Uploaded file {file_id} not found.")
|
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()
|
settings = load_settings()
|
||||||
output_dir = req.output_dir or settings.get("default_output_dir") or None
|
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()
|
output_dir = output_dir.strip()
|
||||||
|
|
||||||
# 2. Queueing loop
|
# 2. Queueing loop
|
||||||
for file_id in req.file_ids:
|
for file_id, file_path in file_sources:
|
||||||
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
|
|
||||||
|
|
||||||
metadata = load_upload_metadata()
|
metadata = load_upload_metadata()
|
||||||
source_filename = None
|
source_filename = None
|
||||||
if file_id in metadata:
|
if file_id and file_id in metadata:
|
||||||
source_filename = metadata[file_id].get("original_filename")
|
source_filename = metadata[file_id].get("original_filename")
|
||||||
if not source_filename and file_path:
|
if not source_filename and file_path:
|
||||||
source_filename = os.path.basename(file_path)
|
source_filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
|
||||||
job_id = str(uuid.uuid4())
|
job_id = str(uuid.uuid4())
|
||||||
job = upscaler.UpscaleJob(
|
job = upscaler.UpscaleJob(
|
||||||
@@ -674,15 +692,24 @@ def cancel_job(job_id: str):
|
|||||||
async def generate_preview(req: PreviewRequest):
|
async def generate_preview(req: PreviewRequest):
|
||||||
"""Generate high quality upscaled single frame preview"""
|
"""Generate high quality upscaled single frame preview"""
|
||||||
file_path = None
|
file_path = None
|
||||||
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
|
if req.server_file_path:
|
||||||
for ext in exts:
|
if os.path.exists(req.server_file_path) and os.path.isfile(req.server_file_path):
|
||||||
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}")
|
file_path = req.server_file_path
|
||||||
if os.path.exists(test_path):
|
else:
|
||||||
file_path = test_path
|
raise HTTPException(status_code=404, detail="Server file path not found.")
|
||||||
break
|
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:
|
if not file_path:
|
||||||
raise HTTPException(status_code=404, detail="Video file not found.")
|
raise HTTPException(status_code=404, detail="Video file not found.")
|
||||||
|
|
||||||
|
|
||||||
preview_id = str(uuid.uuid4())
|
preview_id = str(uuid.uuid4())
|
||||||
preview_temp_dir = os.path.join(upscaler.TEMP_DIR, f"preview_{preview_id}")
|
preview_temp_dir = os.path.join(upscaler.TEMP_DIR, f"preview_{preview_id}")
|
||||||
@@ -1414,7 +1441,7 @@ class CreateDirRequest(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
|
|
||||||
@app.get("/api/system/browse-dir")
|
@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"""
|
"""Browse directories on the system"""
|
||||||
if not path:
|
if not path:
|
||||||
path = os.path.expanduser("~")
|
path = os.path.expanduser("~")
|
||||||
@@ -1438,6 +1465,8 @@ def browse_dir(path: str = ""):
|
|||||||
"is_dir": True
|
"is_dir": True
|
||||||
})
|
})
|
||||||
|
|
||||||
|
video_extensions = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.m4v', '.wmv', '.mpg', '.mpeg'}
|
||||||
|
|
||||||
for name in sorted(os.listdir(path)):
|
for name in sorted(os.listdir(path)):
|
||||||
full_path = os.path.join(path, name)
|
full_path = os.path.join(path, name)
|
||||||
try:
|
try:
|
||||||
@@ -1447,6 +1476,14 @@ def browse_dir(path: str = ""):
|
|||||||
"path": full_path,
|
"path": full_path,
|
||||||
"is_dir": True
|
"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:
|
except PermissionError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1457,6 +1494,25 @@ def browse_dir(path: str = ""):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(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")
|
@app.post("/api/system/create-dir")
|
||||||
def create_dir(req: CreateDirRequest):
|
def create_dir(req: CreateDirRequest):
|
||||||
"""Create a new directory"""
|
"""Create a new directory"""
|
||||||
|
|||||||
+267
-6
@@ -595,7 +595,7 @@ window.addEventListener("mousemove", (e) => {
|
|||||||
|
|
||||||
// Generate single frame preview trigger
|
// Generate single frame preview trigger
|
||||||
generatePreviewBtn.addEventListener("click", async () => {
|
generatePreviewBtn.addEventListener("click", async () => {
|
||||||
if (!uploadFileId) return;
|
if (!uploadFileId && !window.selectedServerFilePath) return;
|
||||||
|
|
||||||
const loaderTitle = document.getElementById("preview-loader-title");
|
const loaderTitle = document.getElementById("preview-loader-title");
|
||||||
const loaderContainer = document.getElementById("preview-loader-progress-container");
|
const loaderContainer = document.getElementById("preview-loader-progress-container");
|
||||||
@@ -618,7 +618,8 @@ generatePreviewBtn.addEventListener("click", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
file_id: uploadFileId,
|
file_id: uploadFileId || null,
|
||||||
|
server_file_path: window.selectedServerFilePath || null,
|
||||||
timestamp_sec: parseFloat(timelineSlider.value),
|
timestamp_sec: parseFloat(timelineSlider.value),
|
||||||
model: modelSelect.value,
|
model: modelSelect.value,
|
||||||
scale: parseInt(scaleRange.value),
|
scale: parseInt(scaleRange.value),
|
||||||
@@ -801,7 +802,7 @@ function initComparisonSlider() {
|
|||||||
6. Video Pipeline Jobs Orchestration
|
6. Video Pipeline Jobs Orchestration
|
||||||
---------------------------------------------------- */
|
---------------------------------------------------- */
|
||||||
startUpscaleBtn.addEventListener("click", async () => {
|
startUpscaleBtn.addEventListener("click", async () => {
|
||||||
if (!uploadFileId) return;
|
if (!uploadFileId && !window.selectedServerFilePath && (!window.selectedServerFilePaths || window.selectedServerFilePaths.length === 0)) return;
|
||||||
|
|
||||||
startUpscaleBtn.disabled = true;
|
startUpscaleBtn.disabled = true;
|
||||||
startUpscaleBtn.innerHTML = `<div class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;"></div> Starting Job...`;
|
startUpscaleBtn.innerHTML = `<div class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;"></div> Starting Job...`;
|
||||||
@@ -821,12 +822,13 @@ startUpscaleBtn.addEventListener("click", async () => {
|
|||||||
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
const aiAudioDenoise = document.getElementById("ai-audio-denoise");
|
||||||
|
|
||||||
try {
|
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", {
|
const res = await fetch("/api/upscale/start/batch", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
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,
|
model: modelSelect.value,
|
||||||
scale: parseInt(scaleRange.value),
|
scale: parseInt(scaleRange.value),
|
||||||
tile_size: parseInt(tileRange.value),
|
tile_size: parseInt(tileRange.value),
|
||||||
@@ -857,6 +859,7 @@ startUpscaleBtn.addEventListener("click", async () => {
|
|||||||
|
|
||||||
window.isBatchMode = false;
|
window.isBatchMode = false;
|
||||||
window.batchFileIds = [];
|
window.batchFileIds = [];
|
||||||
|
window.selectedServerFilePaths = null;
|
||||||
|
|
||||||
document.querySelectorAll(".batch-select-checkbox").forEach(cb => cb.checked = false);
|
document.querySelectorAll(".batch-select-checkbox").forEach(cb => cb.checked = false);
|
||||||
const batchConfigBar = document.getElementById("batch-config-bar");
|
const batchConfigBar = document.getElementById("batch-config-bar");
|
||||||
@@ -875,7 +878,8 @@ startUpscaleBtn.addEventListener("click", async () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
file_id: uploadFileId,
|
file_id: uploadFileId || null,
|
||||||
|
server_file_path: window.selectedServerFilePath || null,
|
||||||
model: modelSelect.value,
|
model: modelSelect.value,
|
||||||
scale: parseInt(scaleRange.value),
|
scale: parseInt(scaleRange.value),
|
||||||
tile_size: parseInt(tileRange.value),
|
tile_size: parseInt(tileRange.value),
|
||||||
@@ -2077,6 +2081,8 @@ async function initializeApp() {
|
|||||||
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
||||||
document.getElementById("step-upload").classList.add("active");
|
document.getElementById("step-upload").classList.add("active");
|
||||||
uploadFileId = null;
|
uploadFileId = null;
|
||||||
|
window.selectedServerFilePath = null;
|
||||||
|
window.selectedServerFilePaths = null;
|
||||||
loadUploadsBrowser();
|
loadUploadsBrowser();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3052,3 +3058,258 @@ if (reimportForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeApp();
|
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 = '<div style="text-align: center; padding: 2rem; color: var(--text-muted);"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';
|
||||||
|
|
||||||
|
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 = '<div style="text-align: center; padding: 2rem; color: var(--text-muted);">Empty directory</div>';
|
||||||
|
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 = '<div style="text-align: center; padding: 2rem; color: var(--danger);"><i class="fa-solid fa-circle-exclamation"></i> Error loading directory</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = '<i class="fa-solid fa-spinner fa-spin"></i> 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);
|
||||||
|
|
||||||
|
|||||||
+43
-4
@@ -46,10 +46,15 @@
|
|||||||
<div id="drop-zone" class="upload-area">
|
<div id="drop-zone" class="upload-area">
|
||||||
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
|
||||||
<h3>Drag & Drop Video File(s)</h3>
|
<h3>Drag & Drop Video File(s)</h3>
|
||||||
<p>Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)</p>
|
<p>Supports MP4, MKV, AVI, MOV, WEBM</p>
|
||||||
<button type="button" class="btn" onclick="document.getElementById('file-input').click()">
|
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 1rem; flex-wrap: wrap;">
|
||||||
<i class="fa-solid fa-folder-open"></i> Browse Files
|
<button type="button" class="btn" onclick="document.getElementById('file-input').click()">
|
||||||
</button>
|
<i class="fa-solid fa-cloud-arrow-up"></i> Upload From Device
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="browse-server-btn">
|
||||||
|
<i class="fa-solid fa-server"></i> Choose From Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -871,6 +876,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server File Browser Modal -->
|
||||||
|
<div id="file-browser-modal" class="modal-overlay" style="display: none; z-index: 1100;">
|
||||||
|
<div class="modal-card" style="max-width: 700px; display: flex; flex-direction: column; max-height: 80vh;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fa-solid fa-server"></i> Choose Video Files from Server</h3>
|
||||||
|
<button type="button" class="close-btn" id="file-browser-modal-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="overflow-y: hidden; display: flex; flex-direction: column; height: 60vh; padding: 1.5rem; gap: 1rem;">
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
|
<button type="button" id="file-browser-up-btn" class="btn btn-secondary btn-small" style="padding: 0.5rem 0.75rem;">
|
||||||
|
<i class="fa-solid fa-arrow-up"></i> Up
|
||||||
|
</button>
|
||||||
|
<input type="text" id="file-browser-current-path" style="flex: 1; padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-family: monospace; font-size: 0.85rem;" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-browser-list" style="flex: 1; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 8px; background: rgba(0, 0, 0, 0.2); padding: 0.5rem;">
|
||||||
|
<!-- File items list -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-browser-selected-info" style="font-size: 0.88rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span>Selected: <strong id="file-browser-selected-count" style="color: var(--primary);">0</strong> video file(s)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-browser-modal-status" style="font-size: 0.78rem; color: var(--danger); min-height: 1.2em;"></div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 0.75rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||||
|
<button type="button" id="file-browser-cancel-btn" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button type="button" id="file-browser-select-btn" class="btn btn-primary" disabled>Select File(s)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- SMB Share Modal -->
|
<!-- SMB Share Modal -->
|
||||||
<div id="smb-modal" class="modal-overlay" style="display: none; z-index: 1200;">
|
<div id="smb-modal" class="modal-overlay" style="display: none; z-index: 1200;">
|
||||||
<div class="modal-card" style="max-width: 500px;">
|
<div class="modal-card" style="max-width: 500px;">
|
||||||
|
|||||||
Reference in New Issue
Block a user