460 lines
12 KiB
Go
460 lines
12 KiB
Go
package backend
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"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
|
|
currentProgressLock sync.RWMutex
|
|
isDownloading bool
|
|
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
|
|
type ProgressInfo struct {
|
|
IsDownloading bool `json:"is_downloading"`
|
|
MBDownloaded float64 `json:"mb_downloaded"`
|
|
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()
|
|
downloading := isDownloading
|
|
downloadingLock.RUnlock()
|
|
|
|
currentProgressLock.RLock()
|
|
progress := currentProgress
|
|
currentProgressLock.RUnlock()
|
|
|
|
speedLock.RLock()
|
|
speed := currentSpeed
|
|
speedLock.RUnlock()
|
|
|
|
return ProgressInfo{
|
|
IsDownloading: downloading,
|
|
MBDownloaded: progress,
|
|
SpeedMBps: speed,
|
|
}
|
|
}
|
|
|
|
// SetDownloadSpeed updates the current download speed
|
|
func SetDownloadSpeed(mbps float64) {
|
|
speedLock.Lock()
|
|
currentSpeed = mbps
|
|
speedLock.Unlock()
|
|
}
|
|
|
|
// SetDownloadProgress updates the current download progress
|
|
func SetDownloadProgress(mbDownloaded float64) {
|
|
currentProgressLock.Lock()
|
|
currentProgress = mbDownloaded
|
|
currentProgressLock.Unlock()
|
|
}
|
|
|
|
// SetDownloading sets the downloading state
|
|
func SetDownloading(downloading bool) {
|
|
downloadingLock.Lock()
|
|
isDownloading = downloading
|
|
downloadingLock.Unlock()
|
|
|
|
if !downloading {
|
|
// Reset progress when download completes
|
|
SetDownloadProgress(0)
|
|
SetDownloadSpeed(0)
|
|
}
|
|
}
|
|
|
|
// ProgressWriter wraps an io.Writer and reports download progress
|
|
type ProgressWriter struct {
|
|
writer io.Writer
|
|
total int64
|
|
lastPrinted int64
|
|
startTime int64
|
|
lastTime int64
|
|
lastBytes int64
|
|
itemID string // Track which download item this belongs to
|
|
}
|
|
|
|
func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
|
now := getCurrentTimeMillis()
|
|
return &ProgressWriter{
|
|
writer: writer,
|
|
total: 0,
|
|
lastPrinted: 0,
|
|
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()
|
|
}
|
|
|
|
func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
|
n, err := pw.writer.Write(p)
|
|
pw.total += int64(n)
|
|
|
|
// Report progress every 256KB for smoother updates
|
|
if pw.total-pw.lastPrinted >= 256*1024 {
|
|
mbDownloaded := float64(pw.total) / (1024 * 1024)
|
|
|
|
// Calculate speed (MB/s)
|
|
now := getCurrentTimeMillis()
|
|
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
|
|
SetDownloadSpeed(speedMBps)
|
|
fmt.Printf("\rDownloaded: %.2f MB (%.2f MB/s)", mbDownloaded, speedMBps)
|
|
} else {
|
|
fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return n, err
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetCurrentItemID returns the ID of the currently downloading item
|
|
func GetCurrentItemID() string {
|
|
currentItemLock.RLock()
|
|
defer currentItemLock.RUnlock()
|
|
return currentItemID
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|