v1.5.0: UI rework, multi-progress tracking, performance optimizations

This commit is contained in:
zarzet
2026-01-02 02:54:50 +07:00
parent db1439e08f
commit d227d57545
38 changed files with 2801 additions and 833 deletions
+23 -8
View File
@@ -202,12 +202,18 @@ func (a *AmazonDownloader) downloadFromDoubleDoubleService(amazonURL, outputDir
// DownloadFile downloads a file from URL with User-Agent and progress tracking
func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string) error {
// Set current file being downloaded
func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) error {
// Set current file being downloaded (legacy)
SetCurrentFile(filepath.Base(outputPath))
SetDownloading(true)
defer SetDownloading(false)
// Initialize item progress if itemID provided
if itemID != "" {
StartItemProgress(itemID)
defer CompleteItemProgress(itemID)
}
req, err := http.NewRequest("GET", downloadURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
@@ -228,6 +234,9 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string) error {
// Set total bytes if available
if resp.ContentLength > 0 {
SetBytesTotal(resp.ContentLength)
if itemID != "" {
SetItemBytesTotal(itemID, resp.ContentLength)
}
}
out, err := os.Create(outputPath)
@@ -236,14 +245,20 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string) error {
}
defer out.Close()
// Track download progress
pw := NewProgressWriter(out)
_, err = io.Copy(pw, resp.Body)
// Use appropriate progress writer
var bytesWritten int64
if itemID != "" {
pw := NewItemProgressWriter(out, itemID)
bytesWritten, err = io.Copy(pw, resp.Body)
} else {
pw := NewProgressWriter(out)
bytesWritten, err = io.Copy(pw, resp.Body)
}
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
fmt.Printf("\r[Amazon] Downloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
fmt.Printf("\r[Amazon] Downloaded: %.2f MB (Complete)\n", float64(bytesWritten)/(1024*1024))
return nil
}
@@ -298,8 +313,8 @@ func downloadFromAmazon(req DownloadRequest) (string, error) {
return "EXISTS:" + outputPath, nil
}
// Download file
if err := downloader.DownloadFile(downloadURL, outputPath); err != nil {
// Download file with item ID for progress tracking
if err := downloader.DownloadFile(downloadURL, outputPath, req.ItemID); err != nil {
return "", fmt.Errorf("download failed: %w", err)
}
+22
View File
@@ -106,6 +106,7 @@ type DownloadRequest struct {
DiscNumber int `json:"disc_number"`
TotalTracks int `json:"total_tracks"`
ReleaseDate string `json:"release_date"`
ItemID string `json:"item_id"` // Unique ID for progress tracking
}
// DownloadResponse represents the result of a download
@@ -255,6 +256,27 @@ func GetDownloadProgress() string {
return string(jsonBytes)
}
// GetAllDownloadProgress returns progress for all active downloads (concurrent mode)
func GetAllDownloadProgress() string {
return GetMultiProgress()
}
// InitItemProgress initializes progress tracking for a download item
func InitItemProgress(itemID string) {
StartItemProgress(itemID)
}
// FinishItemProgress marks a download item as complete and removes tracking
func FinishItemProgress(itemID string) {
CompleteItemProgress(itemID)
// Don't remove immediately - let Flutter poll one more time to see 100%
}
// ClearItemProgress removes progress tracking for a specific item
func ClearItemProgress(itemID string) {
RemoveItemProgress(itemID)
}
// CleanupConnections closes idle HTTP connections
// Call this periodically during large batch downloads to prevent TCP exhaustion
func CleanupConnections() {
+139 -5
View File
@@ -1,10 +1,11 @@
package gobackend
import (
"encoding/json"
"sync"
)
// DownloadProgress represents current download progress
// DownloadProgress represents current download progress (legacy single download)
type DownloadProgress struct {
CurrentFile string `json:"current_file"`
Progress float64 `json:"progress"`
@@ -14,20 +15,128 @@ type DownloadProgress struct {
IsDownloading bool `json:"is_downloading"`
}
// ItemProgress represents progress for a single download item
type ItemProgress struct {
ItemID string `json:"item_id"`
BytesTotal int64 `json:"bytes_total"`
BytesReceived int64 `json:"bytes_received"`
Progress float64 `json:"progress"` // 0.0 to 1.0
IsDownloading bool `json:"is_downloading"`
}
// MultiProgress holds progress for multiple concurrent downloads
type MultiProgress struct {
Items map[string]*ItemProgress `json:"items"`
}
var (
currentProgress DownloadProgress
progressMu sync.RWMutex
downloadDir string
downloadDirMu sync.RWMutex
// Multi-download progress tracking
multiProgress = MultiProgress{Items: make(map[string]*ItemProgress)}
multiMu sync.RWMutex
)
// getProgress returns current download progress
// getProgress returns current download progress (legacy)
func getProgress() DownloadProgress {
progressMu.RLock()
defer progressMu.RUnlock()
return currentProgress
}
// GetMultiProgress returns progress for all active downloads as JSON
func GetMultiProgress() string {
multiMu.RLock()
defer multiMu.RUnlock()
jsonBytes, err := json.Marshal(multiProgress)
if err != nil {
return "{\"items\":{}}"
}
return string(jsonBytes)
}
// GetItemProgress returns progress for a specific item as JSON
func GetItemProgress(itemID string) string {
multiMu.RLock()
defer multiMu.RUnlock()
if item, ok := multiProgress.Items[itemID]; ok {
jsonBytes, _ := json.Marshal(item)
return string(jsonBytes)
}
return "{}"
}
// StartItemProgress initializes progress tracking for an item
func StartItemProgress(itemID string) {
multiMu.Lock()
defer multiMu.Unlock()
multiProgress.Items[itemID] = &ItemProgress{
ItemID: itemID,
BytesTotal: 0,
BytesReceived: 0,
Progress: 0,
IsDownloading: true,
}
}
// SetItemBytesTotal sets total bytes for an item
func SetItemBytesTotal(itemID string, total int64) {
multiMu.Lock()
defer multiMu.Unlock()
if item, ok := multiProgress.Items[itemID]; ok {
item.BytesTotal = total
}
}
// SetItemBytesReceived sets bytes received for an item
func SetItemBytesReceived(itemID string, received int64) {
multiMu.Lock()
defer multiMu.Unlock()
if item, ok := multiProgress.Items[itemID]; ok {
item.BytesReceived = received
if item.BytesTotal > 0 {
item.Progress = float64(received) / float64(item.BytesTotal)
}
}
}
// CompleteItemProgress marks an item as complete
func CompleteItemProgress(itemID string) {
multiMu.Lock()
defer multiMu.Unlock()
if item, ok := multiProgress.Items[itemID]; ok {
item.Progress = 1.0
item.IsDownloading = false
}
}
// RemoveItemProgress removes progress tracking for an item
func RemoveItemProgress(itemID string) {
multiMu.Lock()
defer multiMu.Unlock()
delete(multiProgress.Items, itemID)
}
// ClearAllItemProgress clears all item progress
func ClearAllItemProgress() {
multiMu.Lock()
defer multiMu.Unlock()
multiProgress.Items = make(map[string]*ItemProgress)
}
// Legacy functions for backward compatibility
// SetDownloadProgress sets the current download progress (MB downloaded)
func SetDownloadProgress(mbDownloaded float64) {
progressMu.Lock()
@@ -47,7 +156,6 @@ func SetDownloadSpeed(speedMBps float64) {
func SetCurrentFile(filename string) {
progressMu.Lock()
defer progressMu.Unlock()
// Reset progress for new file
currentProgress.BytesReceived = 0
currentProgress.BytesTotal = 0
currentProgress.Progress = 0
@@ -101,7 +209,7 @@ func SetBytesReceived(received int64) {
}
}
// ProgressWriter wraps io.Writer to track download progress
// ProgressWriter wraps io.Writer to track download progress (legacy single)
type ProgressWriter struct {
writer interface{ Write([]byte) (int, error) }
total int64
@@ -110,7 +218,6 @@ type ProgressWriter struct {
// NewProgressWriter creates a new progress writer wrapping an io.Writer
func NewProgressWriter(w interface{ Write([]byte) (int, error) }) *ProgressWriter {
// Reset bytes received when starting new download
SetBytesReceived(0)
return &ProgressWriter{
writer: w,
@@ -135,3 +242,30 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) {
func (pw *ProgressWriter) GetTotal() int64 {
return pw.total
}
// ItemProgressWriter wraps io.Writer to track download progress for a specific item
type ItemProgressWriter struct {
writer interface{ Write([]byte) (int, error) }
itemID string
current int64
}
// NewItemProgressWriter creates a new progress writer for a specific item
func NewItemProgressWriter(w interface{ Write([]byte) (int, error) }, itemID string) *ItemProgressWriter {
return &ItemProgressWriter{
writer: w,
itemID: itemID,
current: 0,
}
}
// Write implements io.Writer
func (pw *ItemProgressWriter) Write(p []byte) (int, error) {
n, err := pw.writer.Write(p)
if err != nil {
return n, err
}
pw.current += int64(n)
SetItemBytesReceived(pw.itemID, pw.current)
return n, nil
}
+21 -7
View File
@@ -261,12 +261,18 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
}
// DownloadFile downloads a file from URL with User-Agent and progress tracking
func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath string) error {
// Set current file being downloaded
func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) error {
// Set current file being downloaded (legacy)
SetCurrentFile(filepath.Base(outputPath))
SetDownloading(true)
defer SetDownloading(false)
// Initialize item progress if itemID provided
if itemID != "" {
StartItemProgress(itemID)
defer CompleteItemProgress(itemID)
}
req, err := http.NewRequest("GET", downloadURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
@@ -285,6 +291,9 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath string) error {
// Set total bytes if available
if resp.ContentLength > 0 {
SetBytesTotal(resp.ContentLength)
if itemID != "" {
SetItemBytesTotal(itemID, resp.ContentLength)
}
}
out, err := os.Create(outputPath)
@@ -293,9 +302,14 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath string) error {
}
defer out.Close()
// Use ProgressWriter for tracking
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
// Use appropriate progress writer
if itemID != "" {
progressWriter := NewItemProgressWriter(out, itemID)
_, err = io.Copy(progressWriter, resp.Body)
} else {
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
}
return err
}
@@ -366,8 +380,8 @@ func downloadFromQobuz(req DownloadRequest) (string, error) {
return "", fmt.Errorf("failed to get download URL: %w", err)
}
// Download file
if err := downloader.DownloadFile(downloadURL, outputPath); err != nil {
// Download file with item ID for progress tracking
if err := downloader.DownloadFile(downloadURL, outputPath, req.ItemID); err != nil {
return "", fmt.Errorf("download failed: %w", err)
}
+41 -13
View File
@@ -640,17 +640,23 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
// DownloadFile downloads a file from URL with progress tracking
func (t *TidalDownloader) DownloadFile(downloadURL, outputPath string) error {
func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) error {
// Handle manifest-based download
if strings.HasPrefix(downloadURL, "MANIFEST:") {
return t.downloadFromManifest(strings.TrimPrefix(downloadURL, "MANIFEST:"), outputPath)
return t.downloadFromManifest(strings.TrimPrefix(downloadURL, "MANIFEST:"), outputPath, itemID)
}
// Set current file being downloaded
// Set current file being downloaded (legacy)
SetCurrentFile(filepath.Base(outputPath))
SetDownloading(true)
defer SetDownloading(false)
// Initialize item progress if itemID provided
if itemID != "" {
StartItemProgress(itemID)
defer CompleteItemProgress(itemID)
}
req, err := http.NewRequest("GET", downloadURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
@@ -669,6 +675,9 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath string) error {
// Set total bytes if available
if resp.ContentLength > 0 {
SetBytesTotal(resp.ContentLength)
if itemID != "" {
SetItemBytesTotal(itemID, resp.ContentLength)
}
}
out, err := os.Create(outputPath)
@@ -677,13 +686,18 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath string) error {
}
defer out.Close()
// Use ProgressWriter for tracking
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
// Use appropriate progress writer
if itemID != "" {
progressWriter := NewItemProgressWriter(out, itemID)
_, err = io.Copy(progressWriter, resp.Body)
} else {
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
}
return err
}
func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) error {
func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID string) error {
directURL, initURL, mediaURLs, err := parseManifest(manifestB64)
if err != nil {
return fmt.Errorf("failed to parse manifest: %w", err)
@@ -695,11 +709,17 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) e
// If we have a direct URL (BTS format), download directly with progress tracking
if directURL != "" {
// Set current file being downloaded
// Set current file being downloaded (legacy)
SetCurrentFile(filepath.Base(outputPath))
SetDownloading(true)
defer SetDownloading(false)
// Initialize item progress if itemID provided
if itemID != "" {
StartItemProgress(itemID)
defer CompleteItemProgress(itemID)
}
req, err := http.NewRequest("GET", directURL, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
@@ -718,6 +738,9 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) e
// Set total bytes for progress tracking
if resp.ContentLength > 0 {
SetBytesTotal(resp.ContentLength)
if itemID != "" {
SetItemBytesTotal(itemID, resp.ContentLength)
}
}
out, err := os.Create(outputPath)
@@ -726,9 +749,14 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath string) e
}
defer out.Close()
// Use ProgressWriter for tracking
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
// Use appropriate progress writer
if itemID != "" {
progressWriter := NewItemProgressWriter(out, itemID)
_, err = io.Copy(progressWriter, resp.Body)
} else {
progressWriter := NewProgressWriter(out)
_, err = io.Copy(progressWriter, resp.Body)
}
return err
}
@@ -872,8 +900,8 @@ func downloadFromTidal(req DownloadRequest) (string, error) {
return "", fmt.Errorf("failed to get download URL: %w", err)
}
// Download file
if err := downloader.DownloadFile(downloadURL, outputPath); err != nil {
// Download file with item ID for progress tracking
if err := downloader.DownloadFile(downloadURL, outputPath, req.ItemID); err != nil {
return "", fmt.Errorf("download failed: %w", err)
}