Files
SpotiFLAC/backend/progress.go
T
afkarxyz 8f10094e40 v6.7
2025-12-08 19:33:43 +07:00

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()
}
}