diff --git a/routes/prgs.py b/routes/prgs.py index 476918d..0a60247 100755 --- a/routes/prgs.py +++ b/routes/prgs.py @@ -27,8 +27,31 @@ ACTIVE_TASK_STATES = { ProgressState.TRACK_PROGRESS, # "track_progress" - real-time track progress ProgressState.REAL_TIME, # "real_time" - real-time download progress ProgressState.RETRYING, # "retrying" - task is retrying after error + "real-time", # "real-time" - real-time download progress (hyphenated version) } +def get_task_status_from_last_status(last_status): + """ + Extract the task status from last_status, checking both possible locations. + + Args: + last_status: The last status dict from get_last_task_status() + + Returns: + str: The task status string + """ + if not last_status: + return "unknown" + + # Check for status in nested status_info (for real-time downloads) + status_info = last_status.get("status_info", {}) + if isinstance(status_info, dict) and "status" in status_info: + return status_info["status"] + + # Fall back to top-level status (for other task types) + return last_status.get("status", "unknown") + + def is_task_active(task_status): """ Determine if a task is currently active (working/processing). @@ -138,7 +161,7 @@ def _build_task_response(task_info, last_status, task_id, current_time): # Determine last_line content if last_status and "raw_callback" in last_status: last_line_content = last_status["raw_callback"] - elif last_status and last_status.get("status") == "error": + elif last_status and get_task_status_from_last_status(last_status) == "error": last_line_content = _build_error_callback_object(last_status) else: last_line_content = last_status @@ -191,7 +214,7 @@ def get_paginated_tasks(page=1, limit=20, active_only=False): continue last_status = get_last_task_status(task_id) - task_status = last_status.get("status") if last_status else "unknown" + task_status = get_task_status_from_last_status(last_status) is_active_task = is_task_active(task_status) # Categorize tasks by status using ProgressState constants @@ -321,7 +344,7 @@ def get_task_details(task_id): # Determine last_line content if last_status and "raw_callback" in last_status: last_line_content = last_status["raw_callback"] - elif last_status and last_status.get("status") == "error": + elif last_status and get_task_status_from_last_status(last_status) == "error": last_line_content = _build_error_callback_object(last_status) else: # Fallback for non-error, no raw_callback, or if last_status is None @@ -405,7 +428,7 @@ def list_tasks(): continue last_status = get_last_task_status(task_id) - task_status = last_status.get("status") if last_status else "unknown" + task_status = get_task_status_from_last_status(last_status) is_active_task = is_task_active(task_status) # Categorize tasks by status using ProgressState constants @@ -621,7 +644,7 @@ def get_task_updates(): task_timestamp = last_status.get("timestamp") if last_status else task_info.get("created_at", 0) # Determine task status and categorize - task_status = last_status.get("status") if last_status else "unknown" + task_status = get_task_status_from_last_status(last_status) is_active_task = is_task_active(task_status) # Categorize tasks by status using ProgressState constants diff --git a/spotizerr-ui/src/components/Queue.tsx b/spotizerr-ui/src/components/Queue.tsx index 0545eef..bbd4f42 100644 --- a/spotizerr-ui/src/components/Queue.tsx +++ b/spotizerr-ui/src/components/Queue.tsx @@ -510,7 +510,13 @@ export const Queue = () => { if (!context) return null; if (!isVisible) return null; - const hasActive = items.some((item) => !isTerminalStatus(item.status)); + const hasActive = items.some((item) => { + // Check for status in both possible locations (nested status_info for real-time, or top-level for others) + const actualStatus = (item.last_line?.status_info?.status as QueueStatus) || + (item.last_line?.status as QueueStatus) || + item.status; + return isActiveTaskStatus(actualStatus); + }); const hasFinished = items.some((item) => isTerminalStatus(item.status)); // Handle mobile swipe-to-dismiss diff --git a/spotizerr-ui/src/contexts/QueueProvider.tsx b/spotizerr-ui/src/contexts/QueueProvider.tsx index ef0df48..e68bf01 100644 --- a/spotizerr-ui/src/contexts/QueueProvider.tsx +++ b/spotizerr-ui/src/contexts/QueueProvider.tsx @@ -55,9 +55,15 @@ export function QueueProvider({ children }: { children: ReactNode }) { const [totalTasks, setTotalTasks] = useState(0); const pageSize = 20; // Number of non-active tasks per page - // Calculate active downloads count + // Calculate active downloads count (active + queued) const activeCount = useMemo(() => { - return items.filter(item => !isTerminalStatus(item.status)).length; + return items.filter(item => { + // Check for status in both possible locations (nested status_info for real-time, or top-level for others) + const actualStatus = (item.last_line?.status_info?.status as QueueStatus) || + (item.last_line?.status as QueueStatus) || + item.status; + return isActiveTaskStatus(actualStatus); + }).length; }, [items]); const stopPolling = useCallback((internalId: string) => { @@ -156,15 +162,27 @@ export function QueueProvider({ children }: { children: ReactNode }) { total_tasks: number; active_tasks: number; updated_count: number; + task_counts?: { + active: number; + queued: number; + retrying: number; + completed: number; + error: number; + cancelled: number; + skipped: number; + }; }>(`/prgs/updates?since=${lastUpdateTimestamp.current}&active_only=true`); - const { tasks: updatedTasks, current_timestamp, total_tasks } = response.data; + const { tasks: updatedTasks, current_timestamp, total_tasks, task_counts } = response.data; // Update the last timestamp for next poll lastUpdateTimestamp.current = current_timestamp; - // Update total tasks count - setTotalTasks(total_tasks || 0); + // Update total tasks count - use active + queued if task_counts available + const calculatedTotal = task_counts ? + (task_counts.active + task_counts.queued) : + (total_tasks || 0); + setTotalTasks(calculatedTotal); if (updatedTasks.length > 0) { console.log(`Smart polling: ${updatedTasks.length} tasks updated (${response.data.active_tasks} active) out of ${response.data.total_tasks} total`); @@ -236,6 +254,15 @@ export function QueueProvider({ children }: { children: ReactNode }) { pagination: { has_more: boolean; }; + task_counts?: { + active: number; + queued: number; + retrying: number; + completed: number; + error: number; + cancelled: number; + skipped: number; + }; }>(`/prgs/list?page=${nextPage}&limit=${pageSize}`); const { tasks: newTasks, pagination } = response.data; @@ -302,9 +329,18 @@ export function QueueProvider({ children }: { children: ReactNode }) { }; total_tasks: number; timestamp: number; + task_counts?: { + active: number; + queued: number; + retrying: number; + completed: number; + error: number; + cancelled: number; + skipped: number; + }; }>(`/prgs/list?page=1&limit=${pageSize}`); - const { tasks, pagination, total_tasks, timestamp } = response.data; + const { tasks, pagination, total_tasks, timestamp, task_counts } = response.data; const backendItems = tasks .filter((task: any) => { @@ -329,7 +365,12 @@ export function QueueProvider({ children }: { children: ReactNode }) { setItems(backendItems); setHasMore(pagination.has_more); - setTotalTasks(total_tasks || 0); + + // Update total tasks count - use active + queued if task_counts available + const calculatedTotal = task_counts ? + (task_counts.active + task_counts.queued) : + (total_tasks || 0); + setTotalTasks(calculatedTotal); // Set initial timestamp to current time lastUpdateTimestamp.current = timestamp; @@ -489,7 +530,14 @@ export function QueueProvider({ children }: { children: ReactNode }) { }, []); const cancelAll = useCallback(async () => { - const activeItems = items.filter((item) => item.taskId && !isTerminalStatus(item.status)); + const activeItems = items.filter((item) => { + if (!item.taskId) return false; + // Check for status in both possible locations (nested status_info for real-time, or top-level for others) + const actualStatus = (item.last_line?.status_info?.status as QueueStatus) || + (item.last_line?.status as QueueStatus) || + item.status; + return isActiveTaskStatus(actualStatus); + }); if (activeItems.length === 0) { toast.info("No active downloads to cancel."); return; diff --git a/spotizerr-ui/src/contexts/queue-context.ts b/spotizerr-ui/src/contexts/queue-context.ts index 1f54c06..f443f2d 100644 --- a/spotizerr-ui/src/contexts/queue-context.ts +++ b/spotizerr-ui/src/contexts/queue-context.ts @@ -18,8 +18,8 @@ export type QueueStatus = | "progress" | "track_progress"; -// Active task statuses - tasks that are currently working/processing -// This matches the ACTIVE_TASK_STATES constant in the backend +// Active task statuses - tasks that are currently working/processing or queued +// This matches the ACTIVE_TASK_STATES constant in the backend plus queued tasks export const ACTIVE_TASK_STATUSES: Set = new Set([ "initializing", // task is starting up "processing", // task is being processed @@ -28,6 +28,7 @@ export const ACTIVE_TASK_STATUSES: Set = new Set([ "track_progress", // real-time track progress "real-time", // real-time download progress "retrying", // task is retrying after error + "queued", // task is queued and waiting to start ]); /**