react-native-nitro-player 0.3.0-alpha.9 → 0.4.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +444 -4
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/AndroidManifest.xml +16 -1
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +944 -213
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
  19. package/ios/HybridDownloadManager.swift +226 -0
  20. package/ios/HybridEqualizer.swift +111 -0
  21. package/ios/HybridTrackPlayer.swift +36 -8
  22. package/ios/core/TrackPlayerCore.swift +996 -288
  23. package/ios/download/DownloadDatabase.swift +493 -0
  24. package/ios/download/DownloadFileManager.swift +241 -0
  25. package/ios/download/DownloadManagerCore.swift +923 -0
  26. package/ios/equalizer/EqualizerCore.swift +685 -0
  27. package/ios/media/MediaSessionManager.swift +40 -28
  28. package/ios/playlist/PlaylistManager.swift +40 -9
  29. package/ios/queue/HybridPlayerQueue.swift +33 -13
  30. package/lib/hooks/callbackManager.d.ts +18 -0
  31. package/lib/hooks/callbackManager.js +66 -0
  32. package/lib/hooks/downloadCallbackManager.d.ts +36 -0
  33. package/lib/hooks/downloadCallbackManager.js +108 -0
  34. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  35. package/lib/hooks/equalizerCallbackManager.js +109 -0
  36. package/lib/hooks/index.d.ts +16 -0
  37. package/lib/hooks/index.js +10 -0
  38. package/lib/hooks/useActualQueue.d.ts +48 -0
  39. package/lib/hooks/useActualQueue.js +98 -0
  40. package/lib/hooks/useDownloadActions.d.ts +26 -0
  41. package/lib/hooks/useDownloadActions.js +117 -0
  42. package/lib/hooks/useDownloadProgress.d.ts +25 -0
  43. package/lib/hooks/useDownloadProgress.js +79 -0
  44. package/lib/hooks/useDownloadStorage.d.ts +19 -0
  45. package/lib/hooks/useDownloadStorage.js +60 -0
  46. package/lib/hooks/useDownloadedTracks.d.ts +25 -0
  47. package/lib/hooks/useDownloadedTracks.js +69 -0
  48. package/lib/hooks/useEqualizer.d.ts +25 -0
  49. package/lib/hooks/useEqualizer.js +124 -0
  50. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  51. package/lib/hooks/useEqualizerPresets.js +96 -0
  52. package/lib/hooks/useNowPlaying.js +32 -19
  53. package/lib/hooks/useOnChangeTrack.js +15 -12
  54. package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
  55. package/lib/hooks/useOnPlaybackStateChange.js +16 -13
  56. package/lib/hooks/usePlaylist.d.ts +48 -0
  57. package/lib/hooks/usePlaylist.js +136 -0
  58. package/lib/index.d.ts +6 -0
  59. package/lib/index.js +6 -0
  60. package/lib/specs/DownloadManager.nitro.d.ts +152 -0
  61. package/lib/specs/DownloadManager.nitro.js +1 -0
  62. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  63. package/lib/specs/Equalizer.nitro.js +1 -0
  64. package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
  65. package/lib/types/DownloadTypes.d.ts +110 -0
  66. package/lib/types/DownloadTypes.js +1 -0
  67. package/lib/types/EqualizerTypes.d.ts +52 -0
  68. package/lib/types/EqualizerTypes.js +1 -0
  69. package/lib/types/PlayerQueue.d.ts +4 -0
  70. package/nitro.json +8 -0
  71. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
  72. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
  73. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  74. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
  75. package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
  76. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
  77. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
  78. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
  79. package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
  80. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
  81. package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
  82. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
  83. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
  84. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  85. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  86. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
  89. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
  90. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  91. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
  92. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
  93. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  94. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
  95. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  96. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
  97. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
  98. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  99. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  100. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
  101. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
  102. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
  103. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
  104. package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
  105. package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
  106. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  107. package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
  108. package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
  109. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
  110. package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
  111. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
  112. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
  113. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
  114. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
  115. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
  116. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
  117. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
  118. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
  119. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
  120. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
  121. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
  152. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
  153. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
  154. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
  155. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
  156. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
  157. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
  158. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
  159. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
  160. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
  161. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
  162. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
  163. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  164. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  165. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
  166. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
  167. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  168. package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
  169. package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
  170. package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
  171. package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
  172. package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
  173. package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
  174. package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
  175. package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
  176. package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
  177. package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
  178. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  179. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  180. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  181. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  182. package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
  183. package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
  184. package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
  185. package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
  186. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  187. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  188. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  189. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  190. package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
  191. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  192. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
  193. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  194. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  195. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
  196. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
  197. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  198. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  199. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
  200. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
  201. package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
  202. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  203. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  204. package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
  205. package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
  206. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
  207. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
  208. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
  209. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
  210. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
  211. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
  212. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  213. package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
  214. package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
  215. package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
  216. package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
  217. package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
  218. package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
  219. package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
  220. package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
  221. package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
  222. package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
  223. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  224. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  225. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  226. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  227. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
  228. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
  229. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  230. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  231. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
  232. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
  233. package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
  234. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  235. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  236. package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
  237. package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
  238. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
  239. package/package.json +1 -1
  240. package/src/hooks/callbackManager.ts +87 -0
  241. package/src/hooks/downloadCallbackManager.ts +149 -0
  242. package/src/hooks/equalizerCallbackManager.ts +138 -0
  243. package/src/hooks/index.ts +23 -0
  244. package/src/hooks/useActualQueue.ts +116 -0
  245. package/src/hooks/useDownloadActions.ts +179 -0
  246. package/src/hooks/useDownloadProgress.ts +126 -0
  247. package/src/hooks/useDownloadStorage.ts +84 -0
  248. package/src/hooks/useDownloadedTracks.ts +138 -0
  249. package/src/hooks/useEqualizer.ts +173 -0
  250. package/src/hooks/useEqualizerPresets.ts +140 -0
  251. package/src/hooks/useNowPlaying.ts +33 -20
  252. package/src/hooks/useOnChangeTrack.ts +15 -11
  253. package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
  254. package/src/hooks/useOnPlaybackStateChange.ts +19 -15
  255. package/src/hooks/usePlaylist.ts +161 -0
  256. package/src/index.ts +12 -0
  257. package/src/specs/DownloadManager.nitro.ts +203 -0
  258. package/src/specs/Equalizer.nitro.ts +69 -0
  259. package/src/specs/TrackPlayer.nitro.ts +6 -2
  260. package/src/types/DownloadTypes.ts +135 -0
  261. package/src/types/EqualizerTypes.ts +72 -0
  262. package/src/types/PlayerQueue.ts +9 -0
@@ -0,0 +1,923 @@
1
+ //
2
+ // DownloadManagerCore.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 2026-01-23..
6
+ //
7
+
8
+ import Foundation
9
+ import NitroModules
10
+
11
+ /// Core download manager using URLSession background transfers
12
+ final class DownloadManagerCore: NSObject {
13
+
14
+ // MARK: - Singleton
15
+
16
+ static let shared = DownloadManagerCore()
17
+
18
+ // MARK: - Constants
19
+
20
+ private static let backgroundSessionIdentifier = "com.nitroplayer.backgroundDownloads"
21
+ private static let trackMetadataKey = "NitroPlayerTrackMetadata"
22
+ private static let playlistAssociationsKey = "NitroPlayerPlaylistAssociations"
23
+
24
+ // MARK: - Properties
25
+
26
+ private var config: DownloadConfig = DownloadConfig(
27
+ storageLocation: .private,
28
+ maxConcurrentDownloads: 3,
29
+ autoRetry: true,
30
+ maxRetryAttempts: 3,
31
+ backgroundDownloadsEnabled: true,
32
+ downloadArtwork: true,
33
+ customDownloadPath: nil,
34
+ wifiOnlyDownloads: false
35
+ )
36
+
37
+ private var playbackSourcePreference: PlaybackSource = .auto
38
+
39
+ private lazy var backgroundSession: URLSession = {
40
+ let configuration = URLSessionConfiguration.background(
41
+ withIdentifier: Self.backgroundSessionIdentifier)
42
+ configuration.isDiscretionary = false
43
+ configuration.sessionSendsLaunchEvents = true
44
+ configuration.allowsCellularAccess = !config.wifiOnlyDownloads!
45
+ return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
46
+ }()
47
+
48
+ /// Active download tasks mapped by downloadId
49
+ private var activeTasks: [String: URLSessionDownloadTask] = [:]
50
+
51
+ /// Download task metadata mapped by downloadId
52
+ private var taskMetadata: [String: DownloadTaskMetadata] = [:]
53
+
54
+ /// Track metadata for downloads (trackId -> TrackItem)
55
+ private var trackMetadata: [String: TrackItem] = [:]
56
+
57
+ /// Playlist associations (downloadId -> playlistId)
58
+ private var playlistAssociations: [String: String] = [:]
59
+
60
+ /// Background completion handler from AppDelegate
61
+ var backgroundCompletionHandler: (() -> Void)?
62
+
63
+ // MARK: - Callbacks
64
+
65
+ private var progressCallbacks: [(DownloadProgress) -> Void] = []
66
+ private var stateChangeCallbacks: [(String, String, DownloadState, DownloadError?) -> Void] = []
67
+ private var completeCallbacks: [(DownloadedTrack) -> Void] = []
68
+
69
+ // MARK: - Thread Safety
70
+
71
+ private let queue = DispatchQueue(
72
+ label: "com.nitroplayer.downloadManager", attributes: .concurrent)
73
+
74
+ // MARK: - Initialization
75
+
76
+ private override init() {
77
+ super.init()
78
+ // Load persisted metadata first (before restoring downloads)
79
+ loadPersistedMetadata()
80
+ // Restore any pending downloads
81
+ restorePendingDownloads()
82
+ }
83
+
84
+ // MARK: - Configuration
85
+
86
+ func configure(_ config: DownloadConfig) {
87
+ queue.async(flags: .barrier) {
88
+ self.config = config
89
+
90
+ // Update session configuration if needed
91
+ if let wifiOnly = config.wifiOnlyDownloads {
92
+ // Note: We can't change session config after creation
93
+ // User needs to restart app for this to take effect
94
+ }
95
+ }
96
+ }
97
+
98
+ func getConfig() -> DownloadConfig {
99
+ return queue.sync { config }
100
+ }
101
+
102
+ // MARK: - Download Operations
103
+
104
+ func downloadTrack(track: TrackItem, playlistId: String?) -> String {
105
+ let downloadId = UUID().uuidString
106
+
107
+ queue.async(flags: .barrier) {
108
+ // Store track metadata
109
+ self.trackMetadata[track.id] = track
110
+
111
+ // Store playlist association if provided
112
+ if let playlistId = playlistId {
113
+ self.playlistAssociations[downloadId] = playlistId
114
+ }
115
+
116
+ // Persist metadata (survives app restart)
117
+ self.savePersistedMetadata()
118
+
119
+ // Create download task
120
+ guard let url = URL(string: track.url) else {
121
+ self.notifyStateChange(
122
+ downloadId: downloadId, trackId: track.id, state: .failed,
123
+ error: DownloadError(
124
+ code: "INVALID_URL",
125
+ message: "Invalid track URL: \(track.url)",
126
+ reason: .invalidUrl,
127
+ isRetryable: false
128
+ ))
129
+ return
130
+ }
131
+
132
+ let task = self.backgroundSession.downloadTask(with: url)
133
+ task.taskDescription = "\(downloadId)|\(track.id)"
134
+
135
+ self.activeTasks[downloadId] = task
136
+ self.taskMetadata[downloadId] = DownloadTaskMetadata(
137
+ downloadId: downloadId,
138
+ trackId: track.id,
139
+ playlistId: playlistId,
140
+ state: .pending,
141
+ createdAt: Date().timeIntervalSince1970,
142
+ retryCount: 0
143
+ )
144
+
145
+ // Respect max concurrent downloads
146
+ let activeCount = self.activeTasks.values.filter { $0.state == .running }.count
147
+ if activeCount < Int(self.config.maxConcurrentDownloads ?? 3) {
148
+ task.resume()
149
+ self.taskMetadata[downloadId]?.state = .downloading
150
+ self.taskMetadata[downloadId]?.startedAt = Date().timeIntervalSince1970
151
+ }
152
+
153
+ self.notifyStateChange(
154
+ downloadId: downloadId, trackId: track.id,
155
+ state: self.taskMetadata[downloadId]?.state ?? .pending, error: nil)
156
+ }
157
+
158
+ return downloadId
159
+ }
160
+
161
+ func downloadPlaylist(playlistId: String, tracks: [TrackItem]) -> [String] {
162
+ var downloadIds: [String] = []
163
+
164
+ for track in tracks {
165
+ let downloadId = downloadTrack(track: track, playlistId: playlistId)
166
+ downloadIds.append(downloadId)
167
+ }
168
+
169
+ return downloadIds
170
+ }
171
+
172
+ // MARK: - Download Control
173
+
174
+ func pauseDownload(downloadId: String) {
175
+ queue.async(flags: .barrier) {
176
+ guard let task = self.activeTasks[downloadId] else { return }
177
+
178
+ task.cancel(byProducingResumeData: { resumeData in
179
+ // Store resume data for later
180
+ self.taskMetadata[downloadId]?.resumeData = resumeData
181
+ })
182
+
183
+ self.taskMetadata[downloadId]?.state = .paused
184
+
185
+ if let trackId = self.taskMetadata[downloadId]?.trackId {
186
+ self.notifyStateChange(downloadId: downloadId, trackId: trackId, state: .paused, error: nil)
187
+ }
188
+ }
189
+ }
190
+
191
+ func resumeDownload(downloadId: String) {
192
+ queue.async(flags: .barrier) {
193
+ guard let metadata = self.taskMetadata[downloadId] else { return }
194
+
195
+ var task: URLSessionDownloadTask
196
+
197
+ if let resumeData = metadata.resumeData {
198
+ task = self.backgroundSession.downloadTask(withResumeData: resumeData)
199
+ } else if let track = self.trackMetadata[metadata.trackId],
200
+ let url = URL(string: track.url)
201
+ {
202
+ task = self.backgroundSession.downloadTask(with: url)
203
+ } else {
204
+ return
205
+ }
206
+
207
+ task.taskDescription = "\(downloadId)|\(metadata.trackId)"
208
+ self.activeTasks[downloadId] = task
209
+ self.taskMetadata[downloadId]?.state = .downloading
210
+ self.taskMetadata[downloadId]?.resumeData = nil
211
+
212
+ task.resume()
213
+
214
+ self.notifyStateChange(
215
+ downloadId: downloadId, trackId: metadata.trackId, state: .downloading, error: nil)
216
+ }
217
+ }
218
+
219
+ func cancelDownload(downloadId: String) {
220
+ queue.async(flags: .barrier) {
221
+ guard let task = self.activeTasks[downloadId] else { return }
222
+
223
+ task.cancel()
224
+
225
+ if let trackId = self.taskMetadata[downloadId]?.trackId {
226
+ self.taskMetadata[downloadId]?.state = .cancelled
227
+ self.notifyStateChange(
228
+ downloadId: downloadId, trackId: trackId, state: .cancelled, error: nil)
229
+ // Clean up persisted metadata
230
+ self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
231
+ }
232
+
233
+ self.activeTasks.removeValue(forKey: downloadId)
234
+ self.taskMetadata.removeValue(forKey: downloadId)
235
+ }
236
+ }
237
+
238
+ func retryDownload(downloadId: String) {
239
+ queue.async(flags: .barrier) {
240
+ guard let metadata = self.taskMetadata[downloadId],
241
+ let track = self.trackMetadata[metadata.trackId],
242
+ let url = URL(string: track.url)
243
+ else { return }
244
+
245
+ let task = self.backgroundSession.downloadTask(with: url)
246
+ task.taskDescription = "\(downloadId)|\(metadata.trackId)"
247
+
248
+ self.activeTasks[downloadId] = task
249
+ self.taskMetadata[downloadId]?.state = .downloading
250
+ self.taskMetadata[downloadId]?.retryCount += 1
251
+ self.taskMetadata[downloadId]?.error = nil
252
+
253
+ task.resume()
254
+
255
+ self.notifyStateChange(
256
+ downloadId: downloadId, trackId: metadata.trackId, state: .downloading, error: nil)
257
+ }
258
+ }
259
+
260
+ func pauseAllDownloads() {
261
+ queue.async(flags: .barrier) {
262
+ for downloadId in self.activeTasks.keys {
263
+ self.pauseDownload(downloadId: downloadId)
264
+ }
265
+ }
266
+ }
267
+
268
+ func resumeAllDownloads() {
269
+ queue.async(flags: .barrier) {
270
+ for downloadId in self.taskMetadata.keys where self.taskMetadata[downloadId]?.state == .paused
271
+ {
272
+ self.resumeDownload(downloadId: downloadId)
273
+ }
274
+ }
275
+ }
276
+
277
+ func cancelAllDownloads() {
278
+ queue.async(flags: .barrier) {
279
+ for downloadId in self.activeTasks.keys {
280
+ self.cancelDownload(downloadId: downloadId)
281
+ }
282
+ }
283
+ }
284
+
285
+ // MARK: - Download Status
286
+
287
+ func getDownloadTask(downloadId: String) -> DownloadTask? {
288
+ return queue.sync {
289
+ guard let metadata = taskMetadata[downloadId] else { return nil }
290
+ return metadata.toDownloadTask()
291
+ }
292
+ }
293
+
294
+ func getActiveDownloads() -> [DownloadTask] {
295
+ return queue.sync {
296
+ return taskMetadata.values
297
+ .filter { $0.state == .downloading || $0.state == .pending || $0.state == .paused }
298
+ .map { $0.toDownloadTask() }
299
+ }
300
+ }
301
+
302
+ func getQueueStatus() -> DownloadQueueStatus {
303
+ return queue.sync {
304
+ let metadata = Array(taskMetadata.values)
305
+
306
+ let pendingCount = metadata.filter { $0.state == .pending }.count
307
+ let activeCount = metadata.filter { $0.state == .downloading }.count
308
+ let completedCount = DownloadDatabase.shared.getAllDownloadedTracks().count
309
+ let failedCount = metadata.filter { $0.state == .failed }.count
310
+
311
+ let totalBytes = metadata.reduce(0.0) { $0 + ($1.totalBytes ?? 0) }
312
+ let downloadedBytes = metadata.reduce(0.0) { $0 + $1.bytesDownloaded }
313
+
314
+ return DownloadQueueStatus(
315
+ pendingCount: Double(pendingCount),
316
+ activeCount: Double(activeCount),
317
+ completedCount: Double(completedCount),
318
+ failedCount: Double(failedCount),
319
+ totalBytesToDownload: totalBytes,
320
+ totalBytesDownloaded: downloadedBytes,
321
+ overallProgress: totalBytes > 0 ? downloadedBytes / totalBytes : 0
322
+ )
323
+ }
324
+ }
325
+
326
+ func isDownloading(trackId: String) -> Bool {
327
+ return queue.sync {
328
+ return taskMetadata.values.contains { $0.trackId == trackId && $0.state == .downloading }
329
+ }
330
+ }
331
+
332
+ func getDownloadState(trackId: String) -> DownloadState? {
333
+ return queue.sync {
334
+ if let metadata = taskMetadata.values.first(where: { $0.trackId == trackId }) {
335
+ return metadata.state
336
+ }
337
+ if DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) != nil {
338
+ return .completed
339
+ }
340
+ return nil
341
+ }
342
+ }
343
+
344
+ // MARK: - Downloaded Content Queries
345
+
346
+ func isTrackDownloaded(trackId: String) -> Bool {
347
+ return DownloadDatabase.shared.isTrackDownloaded(trackId: trackId)
348
+ }
349
+
350
+ func isPlaylistDownloaded(playlistId: String) -> Bool {
351
+ return DownloadDatabase.shared.isPlaylistDownloaded(playlistId: playlistId)
352
+ }
353
+
354
+ func isPlaylistPartiallyDownloaded(playlistId: String) -> Bool {
355
+ return DownloadDatabase.shared.isPlaylistPartiallyDownloaded(playlistId: playlistId)
356
+ }
357
+
358
+ func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
359
+ return DownloadDatabase.shared.getDownloadedTrack(trackId: trackId)
360
+ }
361
+
362
+ func getAllDownloadedTracks() -> [DownloadedTrack] {
363
+ return DownloadDatabase.shared.getAllDownloadedTracks()
364
+ }
365
+
366
+ func getDownloadedPlaylist(playlistId: String) -> DownloadedPlaylist? {
367
+ return DownloadDatabase.shared.getDownloadedPlaylist(playlistId: playlistId)
368
+ }
369
+
370
+ func getAllDownloadedPlaylists() -> [DownloadedPlaylist] {
371
+ return DownloadDatabase.shared.getAllDownloadedPlaylists()
372
+ }
373
+
374
+ func getLocalPath(trackId: String) -> String? {
375
+ print("🔍 DownloadManagerCore.getLocalPath() called for trackId: \(trackId)")
376
+ if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
377
+ print(" ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
378
+ return downloadedTrack.localPath
379
+ } else {
380
+ print(" ❌ No downloaded track found for trackId: \(trackId)")
381
+ return nil
382
+ }
383
+ }
384
+
385
+ // MARK: - Deletion
386
+
387
+ func deleteDownloadedTrack(trackId: String) {
388
+ DownloadDatabase.shared.deleteDownloadedTrack(trackId: trackId)
389
+ }
390
+
391
+ func deleteDownloadedPlaylist(playlistId: String) {
392
+ DownloadDatabase.shared.deleteDownloadedPlaylist(playlistId: playlistId)
393
+ }
394
+
395
+ func deleteAllDownloads() {
396
+ DownloadDatabase.shared.deleteAllDownloads()
397
+ }
398
+
399
+ // MARK: - Storage
400
+
401
+ func getStorageInfo() -> DownloadStorageInfo {
402
+ return DownloadFileManager.shared.getStorageInfo()
403
+ }
404
+
405
+ /// Validates all downloads and cleans up orphaned records (files that were manually deleted)
406
+ func syncDownloads() -> Int {
407
+ let removedFromDb = DownloadDatabase.shared.syncDownloads()
408
+ let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
409
+ print(
410
+ "🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
411
+ )
412
+ return removedFromDb
413
+ }
414
+
415
+ // MARK: - Playback Source Preference
416
+
417
+ func setPlaybackSourcePreference(_ preference: PlaybackSource) {
418
+ queue.async(flags: .barrier) {
419
+ self.playbackSourcePreference = preference
420
+ }
421
+ }
422
+
423
+ func getPlaybackSourcePreference() -> PlaybackSource {
424
+ return queue.sync { playbackSourcePreference }
425
+ }
426
+
427
+ func getEffectiveUrl(track: TrackItem) -> String {
428
+ let preference = getPlaybackSourcePreference()
429
+ print("🔍 DownloadManagerCore.getEffectiveUrl() for track: \(track.id)")
430
+ print(" Playback preference: \(preference)")
431
+
432
+ switch preference {
433
+ case .network:
434
+ print(" → Using network URL (preference=network)")
435
+ return track.url
436
+ case .download:
437
+ if let localPath = getLocalPath(trackId: track.id) {
438
+ print(" → Using local path: \(localPath)")
439
+ return localPath
440
+ } else {
441
+ print(" → Local path not found, falling back to network URL")
442
+ return track.url
443
+ }
444
+ case .auto:
445
+ if let localPath = getLocalPath(trackId: track.id) {
446
+ print(" → Using local path: \(localPath)")
447
+ return localPath
448
+ } else {
449
+ print(" → Local path not found, using network URL")
450
+ return track.url
451
+ }
452
+ }
453
+ }
454
+
455
+ // MARK: - Callbacks
456
+
457
+ func addProgressCallback(_ callback: @escaping (DownloadProgress) -> Void) {
458
+ queue.async(flags: .barrier) {
459
+ self.progressCallbacks.append(callback)
460
+ }
461
+ }
462
+
463
+ func addStateChangeCallback(
464
+ _ callback: @escaping (String, String, DownloadState, DownloadError?) -> Void
465
+ ) {
466
+ queue.async(flags: .barrier) {
467
+ self.stateChangeCallbacks.append(callback)
468
+ }
469
+ }
470
+
471
+ func addCompleteCallback(_ callback: @escaping (DownloadedTrack) -> Void) {
472
+ queue.async(flags: .barrier) {
473
+ self.completeCallbacks.append(callback)
474
+ }
475
+ }
476
+
477
+ // MARK: - Private Helpers
478
+
479
+ private func restorePendingDownloads() {
480
+ backgroundSession.getTasksWithCompletionHandler { [weak self] _, _, downloadTasks in
481
+ for task in downloadTasks {
482
+ guard let description = task.taskDescription else { continue }
483
+ let parts = description.split(separator: "|")
484
+ guard parts.count == 2 else { continue }
485
+
486
+ let downloadId = String(parts[0])
487
+ let trackId = String(parts[1])
488
+
489
+ self?.queue.async(flags: .barrier) {
490
+ self?.activeTasks[downloadId] = task
491
+ if self?.taskMetadata[downloadId] == nil {
492
+ self?.taskMetadata[downloadId] = DownloadTaskMetadata(
493
+ downloadId: downloadId,
494
+ trackId: trackId,
495
+ playlistId: nil,
496
+ state: task.state == .running ? .downloading : .paused,
497
+ createdAt: Date().timeIntervalSince1970,
498
+ retryCount: 0
499
+ )
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+
506
+ // MARK: - Metadata Persistence
507
+
508
+ /// Load persisted track metadata and playlist associations (survives app restart)
509
+ private func loadPersistedMetadata() {
510
+ print("📦 DownloadManagerCore: Loading persisted metadata...")
511
+
512
+ // Load track metadata
513
+ if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
514
+ do {
515
+ let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: data)
516
+ for (trackId, record) in records {
517
+ trackMetadata[trackId] = recordToTrackItem(record)
518
+ }
519
+ print(" ✅ Loaded \(trackMetadata.count) track metadata entries")
520
+ } catch {
521
+ print(" ❌ Failed to load track metadata: \(error)")
522
+ }
523
+ } else {
524
+ print(" ⚠️ No persisted track metadata found")
525
+ }
526
+
527
+ // Load playlist associations
528
+ if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
529
+ do {
530
+ playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
531
+ print(" ✅ Loaded \(playlistAssociations.count) playlist associations")
532
+ } catch {
533
+ print(" ❌ Failed to load playlist associations: \(error)")
534
+ }
535
+ } else {
536
+ print(" ⚠️ No persisted playlist associations found")
537
+ }
538
+ }
539
+
540
+ /// Persist track metadata and playlist associations to disk
541
+ private func savePersistedMetadata() {
542
+ // Convert TrackItem to TrackItemRecord for encoding
543
+ var records: [String: TrackItemRecord] = [:]
544
+ for (trackId, track) in trackMetadata {
545
+ records[trackId] = trackItemToRecord(track)
546
+ }
547
+
548
+ do {
549
+ let trackData = try JSONEncoder().encode(records)
550
+ UserDefaults.standard.set(trackData, forKey: Self.trackMetadataKey)
551
+
552
+ let playlistData = try JSONEncoder().encode(playlistAssociations)
553
+ UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
554
+ } catch {
555
+ print("❌ DownloadManagerCore: Failed to save metadata: \(error)")
556
+ }
557
+ }
558
+
559
+ /// Clean up persisted metadata for completed/cancelled downloads
560
+ private func cleanupPersistedMetadata(trackId: String, downloadId: String) {
561
+ trackMetadata.removeValue(forKey: trackId)
562
+ playlistAssociations.removeValue(forKey: downloadId)
563
+ savePersistedMetadata()
564
+ }
565
+
566
+ // MARK: - TrackItem Serialization
567
+
568
+ private func trackItemToRecord(_ track: TrackItem) -> TrackItemRecord {
569
+ var artworkString: String? = nil
570
+ if let artwork = track.artwork {
571
+ switch artwork {
572
+ case .first(_):
573
+ artworkString = nil
574
+ case .second(let value):
575
+ artworkString = value
576
+ }
577
+ }
578
+ return TrackItemRecord(
579
+ id: track.id,
580
+ title: track.title,
581
+ artist: track.artist,
582
+ album: track.album,
583
+ duration: track.duration,
584
+ url: track.url,
585
+ artwork: artworkString
586
+ )
587
+ }
588
+
589
+ private func recordToTrackItem(_ record: TrackItemRecord) -> TrackItem {
590
+ let artwork: Variant_NullType_String? = record.artwork.map { .second($0) }
591
+ return TrackItem(
592
+ id: record.id,
593
+ title: record.title,
594
+ artist: record.artist,
595
+ album: record.album,
596
+ duration: record.duration,
597
+ url: record.url,
598
+ artwork: artwork,
599
+ extraPayload: nil
600
+ )
601
+ }
602
+
603
+ private func notifyProgress(_ progress: DownloadProgress) {
604
+ DispatchQueue.main.async {
605
+ for callback in self.progressCallbacks {
606
+ callback(progress)
607
+ }
608
+ }
609
+ }
610
+
611
+ private func notifyStateChange(
612
+ downloadId: String, trackId: String, state: DownloadState, error: DownloadError?
613
+ ) {
614
+ DispatchQueue.main.async {
615
+ for callback in self.stateChangeCallbacks {
616
+ callback(downloadId, trackId, state, error)
617
+ }
618
+ }
619
+ }
620
+
621
+ private func notifyComplete(_ downloadedTrack: DownloadedTrack) {
622
+ DispatchQueue.main.async {
623
+ for callback in self.completeCallbacks {
624
+ callback(downloadedTrack)
625
+ }
626
+ }
627
+ }
628
+
629
+ private func startNextPendingDownload() {
630
+ queue.async(flags: .barrier) {
631
+ let activeCount = self.activeTasks.values.filter { $0.state == .running }.count
632
+ let maxConcurrent = Int(self.config.maxConcurrentDownloads ?? 3)
633
+
634
+ if activeCount >= maxConcurrent { return }
635
+
636
+ if let pendingId = self.taskMetadata.first(where: { $0.value.state == .pending })?.key,
637
+ let task = self.activeTasks[pendingId]
638
+ {
639
+ task.resume()
640
+ self.taskMetadata[pendingId]?.state = .downloading
641
+ self.taskMetadata[pendingId]?.startedAt = Date().timeIntervalSince1970
642
+
643
+ if let trackId = self.taskMetadata[pendingId]?.trackId {
644
+ self.notifyStateChange(
645
+ downloadId: pendingId, trackId: trackId, state: .downloading, error: nil)
646
+ }
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ // MARK: - URLSessionDownloadDelegate
653
+
654
+ extension DownloadManagerCore: URLSessionDownloadDelegate {
655
+
656
+ func urlSession(
657
+ _ session: URLSession, downloadTask: URLSessionDownloadTask,
658
+ didFinishDownloadingTo location: URL
659
+ ) {
660
+ print("🎯 DownloadManagerCore: didFinishDownloadingTo called")
661
+
662
+ guard let description = downloadTask.taskDescription else {
663
+ print("❌ DownloadManagerCore: No task description")
664
+ return
665
+ }
666
+ let parts = description.split(separator: "|")
667
+ guard parts.count == 2 else {
668
+ print("❌ DownloadManagerCore: Invalid task description format: \(description)")
669
+ return
670
+ }
671
+
672
+ let downloadId = String(parts[0])
673
+ let trackId = String(parts[1])
674
+
675
+ print(
676
+ "🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
677
+ )
678
+
679
+ // IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
680
+ // Get storage location and original URL from track metadata
681
+ let (storageLocation, originalURL) = queue.sync {
682
+ (self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
683
+ }
684
+ let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
685
+ from: location,
686
+ trackId: trackId,
687
+ storageLocation: storageLocation,
688
+ originalURL: originalURL
689
+ )
690
+
691
+ // Now handle the rest asynchronously
692
+ queue.async(flags: .barrier) {
693
+ guard let destinationPath = destinationPath else {
694
+ print("❌ DownloadManagerCore: Failed to save file for trackId=\(trackId)")
695
+ self.taskMetadata[downloadId]?.state = .failed
696
+ self.taskMetadata[downloadId]?.error = DownloadError(
697
+ code: "FILE_MOVE_FAILED",
698
+ message: "Failed to save downloaded file",
699
+ reason: .unknown,
700
+ isRetryable: true
701
+ )
702
+ self.notifyStateChange(
703
+ downloadId: downloadId, trackId: trackId, state: .failed,
704
+ error: self.taskMetadata[downloadId]?.error)
705
+ return
706
+ }
707
+
708
+ print("✅ DownloadManagerCore: File saved to \(destinationPath)")
709
+
710
+ guard let track = self.trackMetadata[trackId] else {
711
+ print("❌ DownloadManagerCore: No track metadata for trackId=\(trackId)")
712
+ print(" Available trackIds: \(Array(self.trackMetadata.keys))")
713
+
714
+ // Still mark as completed even if we don't have metadata
715
+ self.taskMetadata[downloadId]?.state = .completed
716
+ self.taskMetadata[downloadId]?.completedAt = Date().timeIntervalSince1970
717
+ self.activeTasks.removeValue(forKey: downloadId)
718
+ self.notifyStateChange(
719
+ downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
720
+ self.startNextPendingDownload()
721
+ return
722
+ }
723
+
724
+ let playlistId = self.playlistAssociations[downloadId]
725
+
726
+ // Get file size
727
+ let fileSize = DownloadFileManager.shared.getFileSize(at: destinationPath)
728
+
729
+ // Create downloaded track record
730
+ let downloadedTrack = DownloadedTrack(
731
+ trackId: trackId,
732
+ originalTrack: track,
733
+ localPath: destinationPath,
734
+ localArtworkPath: nil,
735
+ downloadedAt: Date().timeIntervalSince1970,
736
+ fileSize: Double(fileSize),
737
+ storageLocation: storageLocation
738
+ )
739
+
740
+ // Save to database
741
+ DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
742
+
743
+ print("✅ DownloadManagerCore: Track saved to database")
744
+
745
+ // Clean up persisted metadata (no longer needed after completion)
746
+ self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
747
+
748
+ // Update state
749
+ self.taskMetadata[downloadId]?.state = .completed
750
+ self.taskMetadata[downloadId]?.completedAt = Date().timeIntervalSince1970
751
+
752
+ // Clean up active task but keep metadata for state queries
753
+ self.activeTasks.removeValue(forKey: downloadId)
754
+
755
+ // Notify
756
+ print("✅ DownloadManagerCore: Notifying completion for trackId=\(trackId)")
757
+ self.notifyStateChange(
758
+ downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
759
+ self.notifyComplete(downloadedTrack)
760
+
761
+ // Start next download
762
+ self.startNextPendingDownload()
763
+ }
764
+ }
765
+
766
+ func urlSession(
767
+ _ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64,
768
+ totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64
769
+ ) {
770
+ guard let description = downloadTask.taskDescription else { return }
771
+ let parts = description.split(separator: "|")
772
+ guard parts.count == 2 else { return }
773
+
774
+ let downloadId = String(parts[0])
775
+ let trackId = String(parts[1])
776
+
777
+ queue.async(flags: .barrier) {
778
+ self.taskMetadata[downloadId]?.bytesDownloaded = Double(totalBytesWritten)
779
+ self.taskMetadata[downloadId]?.totalBytes =
780
+ totalBytesExpectedToWrite > 0 ? Double(totalBytesExpectedToWrite) : nil
781
+
782
+ let progress = DownloadProgress(
783
+ trackId: trackId,
784
+ downloadId: downloadId,
785
+ bytesDownloaded: Double(totalBytesWritten),
786
+ totalBytes: Double(totalBytesExpectedToWrite),
787
+ progress: totalBytesExpectedToWrite > 0
788
+ ? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) : 0,
789
+ state: .downloading
790
+ )
791
+
792
+ self.notifyProgress(progress)
793
+ }
794
+ }
795
+
796
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
797
+ guard let downloadTask = task as? URLSessionDownloadTask,
798
+ let description = downloadTask.taskDescription
799
+ else { return }
800
+
801
+ let parts = description.split(separator: "|")
802
+ guard parts.count == 2 else { return }
803
+
804
+ let downloadId = String(parts[0])
805
+ let trackId = String(parts[1])
806
+
807
+ guard let error = error else { return } // Success case handled in didFinishDownloadingTo
808
+
809
+ queue.async(flags: .barrier) {
810
+ let nsError = error as NSError
811
+
812
+ // Check if this is a cancellation
813
+ if nsError.code == NSURLErrorCancelled {
814
+ // Check if we have resume data (pause)
815
+ if self.taskMetadata[downloadId]?.resumeData != nil {
816
+ return // Already handled in pauseDownload
817
+ }
818
+ // Otherwise it's a cancellation
819
+ return
820
+ }
821
+
822
+ // Determine error reason
823
+ let errorReason: DownloadErrorReason
824
+ switch nsError.code {
825
+ case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
826
+ errorReason = .networkError
827
+ case NSURLErrorTimedOut:
828
+ errorReason = .timeout
829
+ case NSURLErrorFileDoesNotExist:
830
+ errorReason = .fileNotFound
831
+ default:
832
+ errorReason = .unknown
833
+ }
834
+
835
+ let downloadError = DownloadError(
836
+ code: String(nsError.code),
837
+ message: error.localizedDescription,
838
+ reason: errorReason,
839
+ isRetryable: errorReason == .networkError || errorReason == .timeout
840
+ )
841
+
842
+ self.taskMetadata[downloadId]?.state = .failed
843
+ self.taskMetadata[downloadId]?.error = downloadError
844
+
845
+ // Auto-retry if enabled
846
+ if let autoRetry = self.config.autoRetry, autoRetry,
847
+ downloadError.isRetryable,
848
+ let retryCount = self.taskMetadata[downloadId]?.retryCount,
849
+ retryCount < Int(self.config.maxRetryAttempts ?? 3)
850
+ {
851
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
852
+ self.retryDownload(downloadId: downloadId)
853
+ }
854
+ } else {
855
+ self.notifyStateChange(
856
+ downloadId: downloadId, trackId: trackId, state: .failed, error: downloadError)
857
+ }
858
+
859
+ // Start next download
860
+ self.startNextPendingDownload()
861
+ }
862
+ }
863
+
864
+ func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
865
+ DispatchQueue.main.async {
866
+ self.backgroundCompletionHandler?()
867
+ self.backgroundCompletionHandler = nil
868
+ }
869
+ }
870
+ }
871
+
872
+ // MARK: - Download Task Metadata
873
+
874
+ private struct DownloadTaskMetadata {
875
+ let downloadId: String
876
+ let trackId: String
877
+ let playlistId: String?
878
+ var state: DownloadState
879
+ let createdAt: Double
880
+ var startedAt: Double?
881
+ var completedAt: Double?
882
+ var retryCount: Int
883
+ var resumeData: Data?
884
+ var bytesDownloaded: Double = 0
885
+ var totalBytes: Double?
886
+ var error: DownloadError?
887
+
888
+ func toDownloadTask() -> DownloadTask {
889
+ let progress = DownloadProgress(
890
+ trackId: trackId,
891
+ downloadId: downloadId,
892
+ bytesDownloaded: bytesDownloaded,
893
+ totalBytes: totalBytes ?? 0,
894
+ progress: totalBytes != nil && totalBytes! > 0 ? bytesDownloaded / totalBytes! : 0,
895
+ state: state
896
+ )
897
+
898
+ return DownloadTask(
899
+ downloadId: downloadId,
900
+ trackId: trackId,
901
+ playlistId: playlistId.map { Variant_NullType_String.second($0) },
902
+ state: state,
903
+ progress: progress,
904
+ createdAt: createdAt,
905
+ startedAt: startedAt.map { Variant_NullType_Double.second($0) },
906
+ completedAt: completedAt.map { Variant_NullType_Double.second($0) },
907
+ error: error.map { Variant_NullType_DownloadError.second($0) },
908
+ retryCount: Double(retryCount)
909
+ )
910
+ }
911
+ }
912
+
913
+ // MARK: - Track Item Record (for persistence)
914
+
915
+ private struct TrackItemRecord: Codable {
916
+ let id: String
917
+ let title: String
918
+ let artist: String
919
+ let album: String
920
+ let duration: Double
921
+ let url: String
922
+ let artwork: String?
923
+ }