From a89cc144488bcf3947bfaadbe972fa16518d77bd Mon Sep 17 00:00:00 2001 From: peterino2 Date: Fri, 6 Feb 2026 23:25:50 -0800 Subject: [PATCH] progress tracker for playlists --- public/auth.js | 6 +++ public/styles.css | 10 +++++ public/upload.js | 103 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/public/auth.js b/public/auth.js index b60216a..dda2df3 100644 --- a/public/auth.js +++ b/public/auth.js @@ -14,6 +14,10 @@ M.currentUser.permissions = data.permissions; } M.updateAuthUI(); + // Start slow queue polling if logged in + if (M.currentUser && !M.currentUser.isGuest && M.startSlowQueuePoll) { + M.startSlowQueuePoll(); + } } catch (e) { M.currentUser = null; M.updateAuthUI(); @@ -108,6 +112,8 @@ const wasGuest = M.currentUser?.isGuest; await fetch("/api/auth/logout", { method: "POST" }); M.currentUser = null; + // Stop slow queue polling on logout + if (M.stopSlowQueuePoll) M.stopSlowQueuePoll(); if (wasGuest) { // Guest clicking "Sign In" - show login panel M.updateAuthUI(); diff --git a/public/styles.css b/public/styles.css index 489c1a3..ab62b2a 100644 --- a/public/styles.css +++ b/public/styles.css @@ -70,6 +70,16 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe .task-item .task-progress { font-size: 0.7rem; color: #888; flex-shrink: 0; } .task-item .task-bar { position: absolute; left: 0; bottom: 0; height: 2px; background: #ea4; transition: width 0.2s; } .task-item.complete .task-bar { background: #4e8; } +.slow-queue-section { margin-top: 0.5rem; border-top: 1px solid #333; padding-top: 0.5rem; } +.slow-queue-section.hidden { display: none; } +.slow-queue-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.3rem; padding: 0 0.2rem; } +.slow-queue-title { font-size: 0.75rem; color: #888; font-weight: 500; } +.slow-queue-timer { font-size: 0.7rem; color: #6af; } +.slow-queue-list { display: flex; flex-direction: column; gap: 0.15rem; max-height: 150px; overflow-y: auto; } +.slow-queue-item { display: flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.4rem; background: #1a1a2a; border-radius: 3px; font-size: 0.75rem; color: #6af; } +.slow-queue-item.next { background: #1a2a2a; color: #4cf; } +.slow-queue-item-icon { flex-shrink: 0; font-size: 0.7rem; } +.slow-queue-item-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .scan-progress { font-size: 0.7rem; color: #ea4; padding: 0.2rem 0.4rem; background: #2a2a1a; border-radius: 3px; margin-bottom: 0.3rem; display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; } .scan-progress.hidden { display: none; } .scan-progress.complete { color: #4e8; background: #1a2a1a; } diff --git a/public/upload.js b/public/upload.js index 7e3c33d..03c3645 100644 --- a/public/upload.js +++ b/public/upload.js @@ -12,6 +12,10 @@ const tasksList = M.$("#tasks-list"); const tasksEmpty = M.$("#tasks-empty"); + // Slow queue section elements (created dynamically) + let slowQueueSection = null; + let slowQueuePollInterval = null; + if (!addBtn || !fileInput || !dropzone) return; function openPanel() { @@ -200,10 +204,101 @@ const fetchTasks = new Map(); // Map function updateTasksEmpty() { - const hasTasks = tasksList.children.length > 0; - tasksEmpty.classList.toggle("hidden", hasTasks); + const hasActiveTasks = tasksList.children.length > 0; + const hasSlowQueue = slowQueueSection && !slowQueueSection.classList.contains("hidden"); + tasksEmpty.classList.toggle("hidden", hasActiveTasks || hasSlowQueue); } + // Slow queue display + function createSlowQueueSection() { + if (slowQueueSection) return slowQueueSection; + + slowQueueSection = document.createElement("div"); + slowQueueSection.className = "slow-queue-section hidden"; + slowQueueSection.innerHTML = ` +
+ Playlist Queue + +
+
+ `; + + // Insert before tasks-empty + tasksEmpty.parentNode.insertBefore(slowQueueSection, tasksEmpty); + return slowQueueSection; + } + + function formatTime(seconds) { + if (seconds <= 0) return "now"; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins > 0) return `${mins}m ${secs}s`; + return `${secs}s`; + } + + function updateSlowQueueDisplay(slowQueue, slowQueueNextIn) { + const section = createSlowQueueSection(); + const queuedItems = slowQueue.filter(i => i.status === "queued"); + + if (queuedItems.length === 0) { + section.classList.add("hidden"); + updateTasksEmpty(); + return; + } + + section.classList.remove("hidden"); + + // Update header with count and timer + const timerEl = section.querySelector(".slow-queue-timer"); + timerEl.textContent = `${queuedItems.length} queued · next in ${formatTime(slowQueueNextIn)}`; + + // Update list + const listEl = section.querySelector(".slow-queue-list"); + listEl.innerHTML = queuedItems.map((item, i) => ` +
+ ${i === 0 ? '⏳' : '·'} + ${item.title} +
+ `).join(""); + + updateTasksEmpty(); + } + + async function pollSlowQueue() { + if (!M.currentUser || M.currentUser.isGuest) return; + + try { + const res = await fetch("/api/fetch"); + if (res.ok) { + const data = await res.json(); + updateSlowQueueDisplay(data.slowQueue || [], data.slowQueueNextIn || 0); + } + } catch (e) { + // Ignore poll errors + } + } + + function startSlowQueuePoll() { + if (slowQueuePollInterval) return; + pollSlowQueue(); + slowQueuePollInterval = setInterval(pollSlowQueue, 5000); + } + + function stopSlowQueuePoll() { + if (slowQueuePollInterval) { + clearInterval(slowQueuePollInterval); + slowQueuePollInterval = null; + } + if (slowQueueSection) { + slowQueueSection.classList.add("hidden"); + updateTasksEmpty(); + } + } + + // Expose start/stop for auth module to call + M.startSlowQueuePoll = startSlowQueuePoll; + M.stopSlowQueuePoll = stopSlowQueuePoll; + // Handle WebSocket fetch progress messages M.handleFetchProgress = function(data) { let task = fetchTasks.get(data.id); @@ -220,9 +315,13 @@ } else if (data.status === "complete") { task.setComplete(); fetchTasks.delete(data.id); + // Refresh slow queue on completion + pollSlowQueue(); } else if (data.status === "error") { task.setError(data.error || "Failed"); fetchTasks.delete(data.id); + // Refresh slow queue on error + pollSlowQueue(); } };