Download Queue & Progress UI (#123)
* Add download queue tracking and UI integration Introduces backend support for a download queue with item tracking, status updates, and session statistics. Adds frontend components and hooks for displaying and managing the download queue, including a dialog and toast indicator. Updates download logic to pre-add items to the queue, track progress, and handle completion, skipping, and failure states. Integrates @radix-ui/react-scroll-area for improved UI scrolling. * Add session stats to DownloadQueue dialog Introduces session statistics (downloaded amount, speed, and duration) to the DownloadQueue dialog for improved user feedback. Also adjusts dialog sizing for better display and removes the sm:max-w-lg restriction in dialog.tsx for more flexible width.
This commit is contained in:
+318
-1
@@ -7,6 +7,34 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DownloadStatus represents the status of a download item
|
||||
type DownloadStatus string
|
||||
|
||||
const (
|
||||
StatusQueued DownloadStatus = "queued"
|
||||
StatusDownloading DownloadStatus = "downloading"
|
||||
StatusCompleted DownloadStatus = "completed"
|
||||
StatusFailed DownloadStatus = "failed"
|
||||
StatusSkipped DownloadStatus = "skipped"
|
||||
)
|
||||
|
||||
// DownloadItem represents a single item in the download queue
|
||||
type DownloadItem struct {
|
||||
ID string `json:"id"`
|
||||
TrackName string `json:"track_name"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumName string `json:"album_name"`
|
||||
ISRC string `json:"isrc"`
|
||||
Status DownloadStatus `json:"status"`
|
||||
Progress float64 `json:"progress"` // MB downloaded
|
||||
TotalSize float64 `json:"total_size"` // MB total (if known)
|
||||
Speed float64 `json:"speed"` // MB/s
|
||||
StartTime int64 `json:"start_time"` // Unix timestamp
|
||||
EndTime int64 `json:"end_time"` // Unix timestamp
|
||||
ErrorMessage string `json:"error_message"` // If failed
|
||||
FilePath string `json:"file_path"` // Final file path
|
||||
}
|
||||
|
||||
// Global progress tracker
|
||||
var (
|
||||
currentProgress float64
|
||||
@@ -15,6 +43,16 @@ var (
|
||||
downloadingLock sync.RWMutex
|
||||
currentSpeed float64
|
||||
speedLock sync.RWMutex
|
||||
|
||||
// Download queue tracking
|
||||
downloadQueue []DownloadItem
|
||||
downloadQueueLock sync.RWMutex
|
||||
currentItemID string
|
||||
currentItemLock sync.RWMutex
|
||||
totalDownloaded float64
|
||||
totalDownloadedLock sync.RWMutex
|
||||
sessionStartTime int64
|
||||
sessionStartLock sync.RWMutex
|
||||
)
|
||||
|
||||
// ProgressInfo represents download progress information
|
||||
@@ -24,6 +62,19 @@ type ProgressInfo struct {
|
||||
SpeedMBps float64 `json:"speed_mbps"`
|
||||
}
|
||||
|
||||
// DownloadQueueInfo represents the complete download queue state
|
||||
type DownloadQueueInfo struct {
|
||||
IsDownloading bool `json:"is_downloading"`
|
||||
Queue []DownloadItem `json:"queue"`
|
||||
CurrentSpeed float64 `json:"current_speed"` // MB/s
|
||||
TotalDownloaded float64 `json:"total_downloaded"` // MB this session
|
||||
SessionStartTime int64 `json:"session_start_time"` // Unix timestamp
|
||||
QueuedCount int `json:"queued_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
FailedCount int `json:"failed_count"`
|
||||
SkippedCount int `json:"skipped_count"`
|
||||
}
|
||||
|
||||
// GetDownloadProgress returns current download progress
|
||||
func GetDownloadProgress() ProgressInfo {
|
||||
downloadingLock.RLock()
|
||||
@@ -80,6 +131,7 @@ type ProgressWriter struct {
|
||||
startTime int64
|
||||
lastTime int64
|
||||
lastBytes int64
|
||||
itemID string // Track which download item this belongs to
|
||||
}
|
||||
|
||||
func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
||||
@@ -91,9 +143,17 @@ func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
||||
startTime: now,
|
||||
lastTime: now,
|
||||
lastBytes: 0,
|
||||
itemID: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NewProgressWriterWithID creates a progress writer with an item ID for queue tracking
|
||||
func NewProgressWriterWithID(writer io.Writer, itemID string) *ProgressWriter {
|
||||
pw := NewProgressWriter(writer)
|
||||
pw.itemID = itemID
|
||||
return pw
|
||||
}
|
||||
|
||||
func getCurrentTimeMillis() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
@@ -111,8 +171,9 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
||||
timeDiff := float64(now-pw.lastTime) / 1000.0 // seconds
|
||||
bytesDiff := float64(pw.total - pw.lastBytes)
|
||||
|
||||
var speedMBps float64
|
||||
if timeDiff > 0 {
|
||||
speedMBps := (bytesDiff / (1024 * 1024)) / timeDiff
|
||||
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
|
||||
SetDownloadSpeed(speedMBps)
|
||||
fmt.Printf("\rDownloaded: %.2f MB (%.2f MB/s)", mbDownloaded, speedMBps)
|
||||
} else {
|
||||
@@ -122,6 +183,11 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
||||
// Update global progress
|
||||
SetDownloadProgress(mbDownloaded)
|
||||
|
||||
// Update individual item progress if we have an item ID
|
||||
if pw.itemID != "" {
|
||||
UpdateItemProgress(pw.itemID, mbDownloaded, speedMBps)
|
||||
}
|
||||
|
||||
pw.lastPrinted = pw.total
|
||||
pw.lastTime = now
|
||||
pw.lastBytes = pw.total
|
||||
@@ -133,3 +199,254 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
||||
func (pw *ProgressWriter) GetTotal() int64 {
|
||||
return pw.total
|
||||
}
|
||||
|
||||
// Queue management functions
|
||||
|
||||
// AddToQueue adds a new item to the download queue
|
||||
func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
item := DownloadItem{
|
||||
ID: id,
|
||||
TrackName: trackName,
|
||||
ArtistName: artistName,
|
||||
AlbumName: albumName,
|
||||
ISRC: isrc,
|
||||
Status: StatusQueued,
|
||||
Progress: 0,
|
||||
TotalSize: 0,
|
||||
Speed: 0,
|
||||
StartTime: 0,
|
||||
EndTime: 0,
|
||||
}
|
||||
|
||||
downloadQueue = append(downloadQueue, item)
|
||||
|
||||
// Initialize session start time if this is the first item
|
||||
sessionStartLock.Lock()
|
||||
if sessionStartTime == 0 {
|
||||
sessionStartTime = time.Now().Unix()
|
||||
}
|
||||
sessionStartLock.Unlock()
|
||||
}
|
||||
|
||||
// StartDownloadItem marks an item as currently downloading
|
||||
func StartDownloadItem(id string) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].ID == id {
|
||||
downloadQueue[i].Status = StatusDownloading
|
||||
downloadQueue[i].StartTime = time.Now().Unix()
|
||||
downloadQueue[i].Progress = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentItemLock.Lock()
|
||||
currentItemID = id
|
||||
currentItemLock.Unlock()
|
||||
}
|
||||
|
||||
// UpdateItemProgress updates the progress of the current download item
|
||||
func UpdateItemProgress(id string, progress, speed float64) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].ID == id {
|
||||
downloadQueue[i].Progress = progress
|
||||
downloadQueue[i].Speed = speed
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteDownloadItem marks an item as completed
|
||||
func CompleteDownloadItem(id, filePath string, finalSize float64) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].ID == id {
|
||||
downloadQueue[i].Status = StatusCompleted
|
||||
downloadQueue[i].EndTime = time.Now().Unix()
|
||||
downloadQueue[i].FilePath = filePath
|
||||
downloadQueue[i].Progress = finalSize
|
||||
downloadQueue[i].TotalSize = finalSize
|
||||
|
||||
// Add to total downloaded
|
||||
totalDownloadedLock.Lock()
|
||||
totalDownloaded += finalSize
|
||||
totalDownloadedLock.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FailDownloadItem marks an item as failed
|
||||
func FailDownloadItem(id, errorMsg string) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].ID == id {
|
||||
downloadQueue[i].Status = StatusFailed
|
||||
downloadQueue[i].EndTime = time.Now().Unix()
|
||||
downloadQueue[i].ErrorMessage = errorMsg
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SkipDownloadItem marks an item as skipped (already exists)
|
||||
func SkipDownloadItem(id, filePath string) {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].ID == id {
|
||||
downloadQueue[i].Status = StatusSkipped
|
||||
downloadQueue[i].EndTime = time.Now().Unix()
|
||||
downloadQueue[i].FilePath = filePath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDownloadQueue returns the complete download queue state
|
||||
func GetDownloadQueue() DownloadQueueInfo {
|
||||
// Auto-reset session if all downloads are complete
|
||||
ResetSessionIfComplete()
|
||||
|
||||
downloadQueueLock.RLock()
|
||||
defer downloadQueueLock.RUnlock()
|
||||
|
||||
downloadingLock.RLock()
|
||||
downloading := isDownloading
|
||||
downloadingLock.RUnlock()
|
||||
|
||||
speedLock.RLock()
|
||||
speed := currentSpeed
|
||||
speedLock.RUnlock()
|
||||
|
||||
totalDownloadedLock.RLock()
|
||||
total := totalDownloaded
|
||||
totalDownloadedLock.RUnlock()
|
||||
|
||||
sessionStartLock.RLock()
|
||||
sessionStart := sessionStartTime
|
||||
sessionStartLock.RUnlock()
|
||||
|
||||
// Count statuses
|
||||
var queued, completed, failed, skipped int
|
||||
for _, item := range downloadQueue {
|
||||
switch item.Status {
|
||||
case StatusQueued:
|
||||
queued++
|
||||
case StatusCompleted:
|
||||
completed++
|
||||
case StatusFailed:
|
||||
failed++
|
||||
case StatusSkipped:
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
|
||||
// Create a copy of the queue
|
||||
queueCopy := make([]DownloadItem, len(downloadQueue))
|
||||
copy(queueCopy, downloadQueue)
|
||||
|
||||
return DownloadQueueInfo{
|
||||
IsDownloading: downloading,
|
||||
Queue: queueCopy,
|
||||
CurrentSpeed: speed,
|
||||
TotalDownloaded: total,
|
||||
SessionStartTime: sessionStart,
|
||||
QueuedCount: queued,
|
||||
CompletedCount: completed,
|
||||
FailedCount: failed,
|
||||
SkippedCount: skipped,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearDownloadQueue clears all completed, failed, and skipped items from the queue
|
||||
func ClearDownloadQueue() {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
// Keep only queued and downloading items
|
||||
newQueue := make([]DownloadItem, 0)
|
||||
for _, item := range downloadQueue {
|
||||
if item.Status == StatusQueued || item.Status == StatusDownloading {
|
||||
newQueue = append(newQueue, item)
|
||||
}
|
||||
}
|
||||
downloadQueue = newQueue
|
||||
}
|
||||
|
||||
// ClearAllDownloads clears the entire queue and resets session stats
|
||||
func ClearAllDownloads() {
|
||||
downloadQueueLock.Lock()
|
||||
downloadQueue = []DownloadItem{}
|
||||
downloadQueueLock.Unlock()
|
||||
|
||||
totalDownloadedLock.Lock()
|
||||
totalDownloaded = 0
|
||||
totalDownloadedLock.Unlock()
|
||||
|
||||
sessionStartLock.Lock()
|
||||
sessionStartTime = 0
|
||||
sessionStartLock.Unlock()
|
||||
|
||||
currentItemLock.Lock()
|
||||
currentItemID = ""
|
||||
currentItemLock.Unlock()
|
||||
|
||||
// Reset current progress and speed
|
||||
SetDownloadProgress(0)
|
||||
SetDownloadSpeed(0)
|
||||
}
|
||||
|
||||
// CancelAllQueuedItems marks all queued items as skipped (cancelled)
|
||||
// This is called when user stops a download or when batch download completes
|
||||
func CancelAllQueuedItems() {
|
||||
downloadQueueLock.Lock()
|
||||
defer downloadQueueLock.Unlock()
|
||||
|
||||
for i := range downloadQueue {
|
||||
if downloadQueue[i].Status == StatusQueued {
|
||||
downloadQueue[i].Status = StatusSkipped
|
||||
downloadQueue[i].EndTime = time.Now().Unix()
|
||||
downloadQueue[i].ErrorMessage = "Cancelled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetSessionIfComplete resets session stats if no active or queued downloads
|
||||
// Note: Does NOT clear the queue - items remain visible for history
|
||||
func ResetSessionIfComplete() {
|
||||
downloadQueueLock.RLock()
|
||||
hasActiveOrQueued := false
|
||||
for _, item := range downloadQueue {
|
||||
if item.Status == StatusQueued || item.Status == StatusDownloading {
|
||||
hasActiveOrQueued = true
|
||||
break
|
||||
}
|
||||
}
|
||||
downloadQueueLock.RUnlock()
|
||||
|
||||
// If no active or queued items, reset session stats
|
||||
// But keep the queue items for history visibility
|
||||
if !hasActiveOrQueued {
|
||||
sessionStartLock.Lock()
|
||||
sessionStartTime = 0
|
||||
sessionStartLock.Unlock()
|
||||
|
||||
totalDownloadedLock.Lock()
|
||||
totalDownloaded = 0
|
||||
totalDownloadedLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user