DownloadManager.swift 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import Foundation
  2. import SwiftUI
  3. /// Observable manager for tracking active cloud track downloads.
  4. /// Coordinates single, album-level, and playlist-level downloads with bounded concurrency.
  5. @MainActor
  6. @Observable
  7. final class DownloadManager {
  8. static let shared = DownloadManager()
  9. /// Per-track download progress (0.0–1.0). Keyed by Track persistent model ID.
  10. var trackProgress: [UUID: Double] = [:]
  11. /// Active download tasks, keyed by Track ID. Used for cancellation.
  12. @ObservationIgnored
  13. private var activeTasks: [UUID: Task<Void, Never>] = [:]
  14. // MARK: - Single Track
  15. func download(track: Track, apiClient: ChadMusicAPIClient) {
  16. guard track.downloadState != .downloading else { return }
  17. guard track.downloadState != .downloaded else { return }
  18. let trackId = track.id
  19. trackProgress[trackId] = 0
  20. let task = Task {
  21. do {
  22. _ = try await DownloadService.downloadPersistent(
  23. track: track,
  24. apiClient: apiClient
  25. ) { [weak self] progress in
  26. Task { @MainActor in
  27. self?.trackProgress[trackId] = progress
  28. }
  29. }
  30. trackProgress.removeValue(forKey: trackId)
  31. activeTasks.removeValue(forKey: trackId)
  32. } catch {
  33. if Task.isCancelled {
  34. track.downloadState = .none
  35. }
  36. trackProgress.removeValue(forKey: trackId)
  37. activeTasks.removeValue(forKey: trackId)
  38. }
  39. }
  40. activeTasks[trackId] = task
  41. }
  42. // MARK: - Cancel
  43. func cancel(track: Track) {
  44. let trackId = track.id
  45. activeTasks[trackId]?.cancel()
  46. activeTasks.removeValue(forKey: trackId)
  47. trackProgress.removeValue(forKey: trackId)
  48. track.downloadState = .none
  49. }
  50. // MARK: - Remove
  51. func removeDownload(track: Track) {
  52. cancel(track: track)
  53. DownloadService.removeDownload(track: track)
  54. }
  55. // MARK: - Batch Download (album / playlist)
  56. /// Download multiple tracks with bounded concurrency (max 3).
  57. func downloadBatch(tracks: [Track], apiClient: ChadMusicAPIClient) {
  58. let toDownload = tracks.filter { $0.isCloud && $0.downloadState != .downloaded && $0.downloadState != .downloading }
  59. guard !toDownload.isEmpty else { return }
  60. for track in toDownload {
  61. download(track: track, apiClient: apiClient)
  62. }
  63. }
  64. /// Cancel all active batch downloads for the given tracks.
  65. func cancelBatch(tracks: [Track]) {
  66. for track in tracks {
  67. if activeTasks[track.id] != nil {
  68. cancel(track: track)
  69. }
  70. }
  71. }
  72. // MARK: - Query
  73. func isDownloading(track: Track) -> Bool {
  74. activeTasks[track.id] != nil
  75. }
  76. func progress(for track: Track) -> Double {
  77. trackProgress[track.id] ?? 0
  78. }
  79. }