Improved SSE and queue handling. Updated to use callbacks.ts

This commit is contained in:
Xoconoch
2025-08-02 17:12:44 -06:00
parent 9fdc0bde42
commit 5abc62d8be
5 changed files with 1078 additions and 1179 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,113 +1,145 @@
import { createContext, useContext } from "react";
import type { SummaryObject } from "@/types/callbacks";
import type { SummaryObject, CallbackObject, TrackCallbackObject, AlbumCallbackObject, PlaylistCallbackObject, ProcessingCallbackObject } from "@/types/callbacks";
export type DownloadType = "track" | "album" | "artist" | "playlist";
export type QueueStatus =
| "initializing"
| "pending"
| "downloading"
| "processing"
| "completed"
| "error"
| "skipped"
| "cancelled"
| "done"
| "queued"
| "retrying"
| "real-time"
| "progress"
| "track_progress";
export type DownloadType = "track" | "album" | "playlist";
// 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<QueueStatus> = new Set([
"initializing", // task is starting up
"processing", // task is being processed
"downloading", // actively downloading
"progress", // album/playlist progress updates
"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
]);
// Type guards for callback objects
const isProcessingCallback = (obj: CallbackObject): obj is ProcessingCallbackObject => {
return "status" in obj && typeof obj.status === "string";
};
/**
* Determine if a task status represents an active (working/processing) task
*/
export function isActiveTaskStatus(status: string): boolean {
return ACTIVE_TASK_STATUSES.has(status as QueueStatus);
}
const isTrackCallback = (obj: CallbackObject): obj is TrackCallbackObject => {
return "track" in obj && "status_info" in obj;
};
const isAlbumCallback = (obj: CallbackObject): obj is AlbumCallbackObject => {
return "album" in obj && "status_info" in obj;
};
const isPlaylistCallback = (obj: CallbackObject): obj is PlaylistCallbackObject => {
return "playlist" in obj && "status_info" in obj;
};
// Simplified queue item that works directly with callback objects
export interface QueueItem {
id: string;
name: string;
type: DownloadType;
spotifyId: string;
// Display Info
artist?: string;
albumName?: string;
playlistOwner?: string;
currentTrackTitle?: string;
// Status and Progress
status: QueueStatus;
taskId?: string;
error?: string;
canRetry?: boolean;
progress?: number;
speed?: string;
size?: string;
eta?: string;
currentTrackNumber?: number;
totalTracks?: number;
summary?: SummaryObject;
// Real-time download data
last_line?: {
// Direct status and error fields
status?: string;
error?: string;
id?: number;
timestamp?: number;
// Album/playlist progress fields
current_track?: number;
total_tracks?: number;
parent?: any; // Parent album/playlist information
// Real-time progress data (when status is "real-time")
status_info?: {
progress?: number;
status?: string;
time_elapsed?: number;
error?: string;
timestamp?: number;
ids?: {
isrc?: string;
spotify?: string;
};
};
track?: any; // Contains detailed track information
};
id: string;
taskId?: string;
downloadType: DownloadType;
spotifyId: string;
// Current callback data - this is the source of truth
lastCallback?: CallbackObject;
// Derived display properties (computed from callback)
name: string;
artist: string;
// Summary data for completed downloads
summary?: SummaryObject;
// Error state
error?: string;
}
// Status extraction utilities
export const getStatus = (item: QueueItem): string => {
if (!item.lastCallback) {
// Only log if this seems problematic (task has been around for a while)
return "initializing";
}
if (isProcessingCallback(item.lastCallback)) {
return item.lastCallback.status;
}
if (isTrackCallback(item.lastCallback)) {
// For parent downloads, if we're getting track callbacks, the parent is "downloading"
if (item.downloadType === "album" || item.downloadType === "playlist") {
return item.lastCallback.status_info.status === "done" ? "downloading" : "downloading";
}
return item.lastCallback.status_info.status;
}
if (isAlbumCallback(item.lastCallback)) {
return item.lastCallback.status_info.status;
}
if (isPlaylistCallback(item.lastCallback)) {
return item.lastCallback.status_info.status;
}
console.warn(`getStatus: Unknown callback type for item ${item.id}:`, item.lastCallback);
return "unknown";
};
export const isActiveStatus = (status: string): boolean => {
return ["initializing", "processing", "downloading", "real-time", "progress", "track_progress", "retrying", "queued"].includes(status);
};
export const isTerminalStatus = (status: string): boolean => {
// Handle both "complete" (backend) and "completed" (frontend) for compatibility
return ["completed", "complete", "done", "error", "cancelled", "skipped"].includes(status);
};
// Progress calculation utilities
export const getProgress = (item: QueueItem): number | undefined => {
if (!item.lastCallback) return undefined;
// For individual tracks
if (item.downloadType === "track" && isTrackCallback(item.lastCallback)) {
if (item.lastCallback.status_info.status === "real-time" && "progress" in item.lastCallback.status_info) {
return item.lastCallback.status_info.progress;
}
return undefined;
}
// For parent downloads (albums/playlists) - calculate based on track callbacks
if ((item.downloadType === "album" || item.downloadType === "playlist") && isTrackCallback(item.lastCallback)) {
const callback = item.lastCallback;
const currentTrack = callback.current_track || 1;
const totalTracks = callback.total_tracks || 1;
const trackProgress = (callback.status_info.status === "real-time" && "progress" in callback.status_info)
? callback.status_info.progress : 0;
// Formula: ((completed tracks) + (current track progress / 100)) / total tracks * 100
const completedTracks = currentTrack - 1;
return ((completedTracks + (trackProgress / 100)) / totalTracks) * 100;
}
return undefined;
};
// Display info extraction
export const getCurrentTrackInfo = (item: QueueItem): { current?: number; total?: number; title?: string } => {
if (!item.lastCallback) return {};
if (isTrackCallback(item.lastCallback)) {
return {
current: item.lastCallback.current_track,
total: item.lastCallback.total_tracks,
title: item.lastCallback.track.title
};
}
return {};
};
export interface QueueContextType {
items: QueueItem[];
isVisible: boolean;
activeCount: number;
totalTasks: number;
hasMore: boolean;
isLoadingMore: boolean;
// Actions
addItem: (item: { name: string; type: DownloadType; spotifyId: string; artist?: string }) => void;
removeItem: (id: string) => void;
retryItem: (id: string) => void;
cancelItem: (id: string) => void;
toggleVisibility: () => void;
clearCompleted: () => void;
cancelAll: () => void;
cancelItem: (id: string) => void;
// Pagination
hasMore: boolean;
isLoadingMore: boolean;
loadMoreTasks: () => void;
totalTasks: number;
}
export const QueueContext = createContext<QueueContextType | undefined>(undefined);