import Foundation import SwiftUI /// Observable manager for tracking active cloud track downloads. /// Coordinates single, album-level, and playlist-level downloads with bounded concurrency. @MainActor @Observable final class DownloadManager { static let shared = DownloadManager() /// Per-track download progress (0.0–1.0). Keyed by Track persistent model ID. var trackProgress: [UUID: Double] = [:] /// Active download tasks, keyed by Track ID. Used for cancellation. @ObservationIgnored private var activeTasks: [UUID: Task] = [:] // MARK: - Single Track func download(track: Track, apiClient: ChadMusicAPIClient) { guard track.downloadState != .downloading else { return } guard track.downloadState != .downloaded else { return } let trackId = track.id trackProgress[trackId] = 0 let task = Task { do { _ = try await DownloadService.downloadPersistent( track: track, apiClient: apiClient ) { [weak self] progress in Task { @MainActor in self?.trackProgress[trackId] = progress } } trackProgress.removeValue(forKey: trackId) activeTasks.removeValue(forKey: trackId) } catch { if Task.isCancelled { track.downloadState = .none } trackProgress.removeValue(forKey: trackId) activeTasks.removeValue(forKey: trackId) } } activeTasks[trackId] = task } // MARK: - Cancel func cancel(track: Track) { let trackId = track.id activeTasks[trackId]?.cancel() activeTasks.removeValue(forKey: trackId) trackProgress.removeValue(forKey: trackId) track.downloadState = .none } // MARK: - Remove func removeDownload(track: Track) { cancel(track: track) DownloadService.removeDownload(track: track) } // MARK: - Batch Download (album / playlist) /// Download multiple tracks with bounded concurrency (max 3). func downloadBatch(tracks: [Track], apiClient: ChadMusicAPIClient) { let toDownload = tracks.filter { $0.isCloud && $0.downloadState != .downloaded && $0.downloadState != .downloading } guard !toDownload.isEmpty else { return } for track in toDownload { download(track: track, apiClient: apiClient) } } /// Cancel all active batch downloads for the given tracks. func cancelBatch(tracks: [Track]) { for track in tracks { if activeTasks[track.id] != nil { cancel(track: track) } } } // MARK: - Query func isDownloading(track: Track) -> Bool { activeTasks[track.id] != nil } func progress(for track: Track) -> Double { trackProgress[track.id] ?? 0 } }