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
@@ -57,10 +57,44 @@ class TrackPlayerCore: NSObject {
57
57
  private var preloadedAssets: [String: AVURLAsset] = [:]
58
58
  private let preloadQueue = DispatchQueue(label: "com.nitroplayer.preload", qos: .utility)
59
59
 
60
- var onChangeTrack: ((TrackItem, Reason?) -> Void)?
61
- var onPlaybackStateChange: ((TrackPlayerState, Reason?) -> Void)?
62
- var onSeek: ((Double, Double) -> Void)?
63
- var onPlaybackProgressChange: ((Double, Double, Bool?) -> Void)?
60
+ // Temporary tracks for addToUpNext and playNext
61
+ private var playNextStack: [TrackItem] = [] // LIFO - last added plays first
62
+ private var upNextQueue: [TrackItem] = [] // FIFO - first added plays first
63
+ private var currentTemporaryType: TemporaryType = .none
64
+
65
+ // Enum to track what type of track is currently playing
66
+ private enum TemporaryType {
67
+ case none // Playing from original playlist
68
+ case playNext // Currently in playNextStack
69
+ case upNext // Currently in upNextQueue
70
+ }
71
+
72
+ // MARK: - Weak Callback Wrapper
73
+
74
+ /// Wrapper to hold callbacks with weak reference for auto-cleanup
75
+ private class WeakCallbackBox<T> {
76
+ private(set) weak var owner: AnyObject?
77
+ let callback: T
78
+
79
+ init(owner: AnyObject, callback: T) {
80
+ self.owner = owner
81
+ self.callback = callback
82
+ }
83
+
84
+ var isAlive: Bool { owner != nil }
85
+ }
86
+
87
+ // Event callbacks - support multiple listeners with auto-cleanup
88
+ private var onChangeTrackListeners: [WeakCallbackBox<(TrackItem, Reason?) -> Void>] = []
89
+ private var onPlaybackStateChangeListeners:
90
+ [WeakCallbackBox<(TrackPlayerState, Reason?) -> Void>] = []
91
+ private var onSeekListeners: [WeakCallbackBox<(Double, Double) -> Void>] = []
92
+ private var onPlaybackProgressChangeListeners:
93
+ [WeakCallbackBox<(Double, Double, Bool?) -> Void>] = []
94
+
95
+ // Thread-safe queue for listener access
96
+ private let listenersQueue = DispatchQueue(
97
+ label: "com.trackplayer.listeners", attributes: .concurrent)
64
98
 
65
99
  static let shared = TrackPlayerCore()
66
100
 
@@ -229,10 +263,10 @@ class TrackPlayerCore: NSObject {
229
263
  guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
230
264
 
231
265
  print(
232
- "⏱️ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(onPlaybackProgressChange != nil)"
266
+ "⏱️ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(!onPlaybackProgressChangeListeners.isEmpty)"
233
267
  )
234
268
 
235
- onPlaybackProgressChange?(
269
+ notifyPlaybackProgress(
236
270
  position,
237
271
  duration,
238
272
  isManuallySeeked ? true : nil
@@ -246,15 +280,26 @@ class TrackPlayerCore: NSObject {
246
280
  print("\n🏁 TrackPlayerCore: Track finished playing")
247
281
 
248
282
  guard let finishedItem = notification.object as? AVPlayerItem else {
249
- print("⚠️ Cannot identify finished item")
250
- skipToNext()
283
+ // Don't call skipToNext — AVQueuePlayer with actionAtItemEnd = .advance already auto-advances
251
284
  return
252
285
  }
253
286
 
254
- if let trackId = finishedItem.trackId,
255
- let track = currentTracks.first(where: { $0.id == trackId })
256
- {
257
- print("🏁 Finished: \(track.title)")
287
+ // Determine what type of track just finished and remove it from temporary lists
288
+ if let trackId = finishedItem.trackId {
289
+ // Check if it was a playNext track
290
+ if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
291
+ let track = playNextStack.remove(at: index)
292
+ print("🏁 Finished playNext track: \(track.title) - removed from stack")
293
+ }
294
+ // Check if it was an upNext track
295
+ else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
296
+ let track = upNextQueue.remove(at: index)
297
+ print("🏁 Finished upNext track: \(track.title) - removed from queue")
298
+ }
299
+ // Otherwise it was from original playlist
300
+ else if let track = currentTracks.first(where: { $0.id == trackId }) {
301
+ print("🏁 Finished original track: \(track.title)")
302
+ }
258
303
  }
259
304
 
260
305
  // Check remaining queue
@@ -269,23 +314,35 @@ class TrackPlayerCore: NSObject {
269
314
  print("🔁 TrackPlayerCore: Repeat mode is TRACK - replaying current track")
270
315
  DispatchQueue.main.async { [weak self] in
271
316
  guard let self = self, let player = self.player else { return }
272
- // Recreate the current track item and play it
273
- self.playFromIndex(index: self.currentTrackIndex)
317
+ // For temporary tracks, just seek to beginning
318
+ if self.currentTemporaryType != .none {
319
+ player.seek(to: .zero)
320
+ player.play()
321
+ } else {
322
+ // For original tracks, recreate via playFromIndex
323
+ self.playFromIndex(index: self.currentTrackIndex)
324
+ }
274
325
  }
275
326
  return
276
327
 
277
328
  case .playlist:
278
- // Check if we're at the end of the playlist
279
- if currentTrackIndex >= currentTracks.count - 1 {
280
- print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
281
- DispatchQueue.main.async { [weak self] in
282
- guard let self = self else { return }
283
- // Go back to the first track
284
- self.playFromIndex(index: 0)
329
+ // Check if we're at the end of the ORIGINAL playlist (ignore temps)
330
+ if currentTemporaryType == .none && currentTrackIndex >= currentTracks.count - 1 {
331
+ // Check if there are still temporary tracks
332
+ if !playNextStack.isEmpty || !upNextQueue.isEmpty {
333
+ print("🔁 TrackPlayerCore: Temporary tracks remaining, continuing...")
334
+ } else {
335
+ print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
336
+ // Clear temps and restart
337
+ playNextStack.removeAll()
338
+ upNextQueue.removeAll()
339
+ DispatchQueue.main.async { [weak self] in
340
+ guard let self = self else { return }
341
+ self.playFromIndex(index: 0)
342
+ }
343
+ return
285
344
  }
286
- return
287
345
  } else {
288
- // Not at end, just continue to next track
289
346
  print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - continuing to next track")
290
347
  }
291
348
 
@@ -294,8 +351,10 @@ class TrackPlayerCore: NSObject {
294
351
  print("🔁 TrackPlayerCore: Repeat mode is OFF")
295
352
  }
296
353
 
297
- // Track ended naturally
298
- onChangeTrack?(
354
+ // Track ended naturally — notify with .end reason
355
+ // AVQueuePlayer with actionAtItemEnd = .advance auto-advances to next item
356
+ // The KVO observer (currentItemDidChange) will handle the track change notification
357
+ notifyTrackChange(
299
358
  getCurrentTrack()
300
359
  ?? TrackItem(
301
360
  id: "",
@@ -304,17 +363,15 @@ class TrackPlayerCore: NSObject {
304
363
  album: "",
305
364
  duration: 0,
306
365
  url: "",
307
- artwork: nil
366
+ artwork: nil,
367
+ extraPayload: nil
308
368
  ), .end)
309
-
310
- // Try to play next track
311
- skipToNext()
312
369
  }
313
370
 
314
371
  @objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
315
372
  if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
316
373
  print("❌ TrackPlayerCore: Playback failed - \(error)")
317
- onPlaybackStateChange?(.stopped, .error)
374
+ notifyPlaybackStateChange(.stopped, .error)
318
375
  }
319
376
  }
320
377
 
@@ -346,7 +403,7 @@ class TrackPlayerCore: NSObject {
346
403
  print("🎯 TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
347
404
 
348
405
  // Call onSeek callback immediately
349
- onSeek?(position, duration)
406
+ notifySeek(position, duration)
350
407
 
351
408
  // Mark that this was a manual seek
352
409
  isManuallySeeked = true
@@ -371,7 +428,7 @@ class TrackPlayerCore: NSObject {
371
428
  emitStateChange()
372
429
  } else if player.status == .failed {
373
430
  print("❌ TrackPlayerCore: Player failed")
374
- onPlaybackStateChange?(.stopped, .error)
431
+ notifyPlaybackStateChange(.stopped, .error)
375
432
  }
376
433
  } else if keyPath == "rate" {
377
434
  print("👀 TrackPlayerCore: Rate changed to: \(player.rate)")
@@ -437,12 +494,34 @@ class TrackPlayerCore: NSObject {
437
494
  // Setup KVO observers for current item
438
495
  setupCurrentItemObservers(item: currentItem)
439
496
 
440
- // Update track index
497
+ // Update track index and determine temporary type
441
498
  if let trackId = currentItem.trackId {
442
499
  print("🔍 TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
443
500
  print(" Current index BEFORE lookup: \(currentTrackIndex)")
444
501
 
445
- if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
502
+ // Update temporary type
503
+ currentTemporaryType = determineCurrentTemporaryType()
504
+ print(" 🎯 Track type: \(currentTemporaryType)")
505
+
506
+ // If it's a temporary track, don't update currentTrackIndex
507
+ if currentTemporaryType != .none {
508
+ // Find and emit the temporary track
509
+ var tempTrack: TrackItem? = nil
510
+ if currentTemporaryType == .playNext {
511
+ tempTrack = playNextStack.first(where: { $0.id == trackId })
512
+ } else if currentTemporaryType == .upNext {
513
+ tempTrack = upNextQueue.first(where: { $0.id == trackId })
514
+ }
515
+
516
+ if let track = tempTrack {
517
+ print(" 🎵 Temporary track: \(track.title) - \(track.artist)")
518
+ print(" 📢 Emitting onChangeTrack for temporary track")
519
+ notifyTrackChange(track, .skip)
520
+ mediaSessionManager?.onTrackChanged()
521
+ }
522
+ }
523
+ // It's an original playlist track
524
+ else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
446
525
  print(" ✅ Found track at index: \(index)")
447
526
  print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
448
527
 
@@ -456,7 +535,7 @@ class TrackPlayerCore: NSObject {
456
535
  // This prevents duplicate emissions
457
536
  if oldIndex != index {
458
537
  print(" 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
459
- onChangeTrack?(track, .skip)
538
+ notifyTrackChange(track, .skip)
460
539
  mediaSessionManager?.onTrackChanged()
461
540
  } else {
462
541
  print(" ⏭️ Skipping onChangeTrack emission (index unchanged)")
@@ -485,14 +564,16 @@ class TrackPlayerCore: NSObject {
485
564
  private func setupCurrentItemObservers(item: AVPlayerItem) {
486
565
  print("📱 TrackPlayerCore: Setting up item observers")
487
566
 
488
- // Observe status - recreate boundaries when ready
567
+ // Observe status - recreate boundaries when ready and update now playing info
489
568
  let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
490
569
  if item.status == .readyToPlay {
491
570
  print("✅ TrackPlayerCore: Item ready, setting up boundaries")
492
571
  self?.setupBoundaryTimeObserver()
572
+ // Update now playing info now that duration is available
573
+ self?.mediaSessionManager?.updateNowPlayingInfo()
493
574
  } else if item.status == .failed {
494
575
  print("❌ TrackPlayerCore: Item failed")
495
- self?.onPlaybackStateChange?(.stopped, .error)
576
+ self?.notifyPlaybackStateChange(.stopped, .error)
496
577
  }
497
578
  }
498
579
  currentItemObservers.append(statusObserver)
@@ -517,44 +598,63 @@ class TrackPlayerCore: NSObject {
517
598
  // MARK: - Playlist Management
518
599
 
519
600
  func loadPlaylist(playlistId: String) {
520
- DispatchQueue.main.async { [weak self] in
521
- guard let self = self else { return }
522
-
523
- print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
524
- print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
525
- print(" Playlist ID: \(playlistId)")
526
-
527
- let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
528
- if let playlist = playlist {
529
- print(" ✅ Found playlist: \(playlist.name)")
530
- print(" 📋 Contains \(playlist.tracks.count) tracks:")
531
- for (index, track) in playlist.tracks.enumerated() {
532
- print(" [\(index + 1)] \(track.title) - \(track.artist)")
533
- }
534
- print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
601
+ if Thread.isMainThread {
602
+ loadPlaylistInternal(playlistId: playlistId)
603
+ } else {
604
+ DispatchQueue.main.sync { [weak self] in
605
+ self?.loadPlaylistInternal(playlistId: playlistId)
606
+ }
607
+ }
608
+ }
535
609
 
536
- self.currentPlaylistId = playlistId
537
- self.updatePlayerQueue(tracks: playlist.tracks)
538
- // Emit initial state (paused/stopped before play)
539
- self.emitStateChange()
540
- // Automatically start playback after loading
541
- self.play()
542
- } else {
543
- print(" ❌ Playlist NOT FOUND")
544
- print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
610
+ private func loadPlaylistInternal(playlistId: String) {
611
+ print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
612
+ print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
613
+ print(" Playlist ID: \(playlistId)")
614
+
615
+ // Clear temporary tracks when loading new playlist
616
+ self.playNextStack.removeAll()
617
+ self.upNextQueue.removeAll()
618
+ self.currentTemporaryType = .none
619
+ print(" 🧹 Cleared temporary tracks")
620
+
621
+ let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
622
+ if let playlist = playlist {
623
+ print(" ✅ Found playlist: \(playlist.name)")
624
+ print(" 📋 Contains \(playlist.tracks.count) tracks:")
625
+ for (index, track) in playlist.tracks.enumerated() {
626
+ print(" [\(index + 1)] \(track.title) - \(track.artist)")
545
627
  }
628
+ print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
629
+
630
+ self.currentPlaylistId = playlistId
631
+ self.updatePlayerQueue(tracks: playlist.tracks)
632
+ // Emit initial state (paused/stopped before play)
633
+ self.emitStateChange()
634
+ } else {
635
+ print(" ❌ Playlist NOT FOUND")
636
+ print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
546
637
  }
547
638
  }
548
639
 
549
640
  func updatePlaylist(playlistId: String) {
550
641
  DispatchQueue.main.async { [weak self] in
551
642
  guard let self = self else { return }
552
- if self.currentPlaylistId == playlistId {
643
+ guard self.currentPlaylistId == playlistId,
553
644
  let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
554
- if let playlist = playlist {
555
- self.updatePlayerQueue(tracks: playlist.tracks)
556
- }
645
+ else { return }
646
+
647
+ // If nothing is playing yet, do a full load
648
+ guard let player = self.player, player.currentItem != nil else {
649
+ self.updatePlayerQueue(tracks: playlist.tracks)
650
+ return
557
651
  }
652
+
653
+ // Update tracks list without interrupting playback
654
+ self.currentTracks = playlist.tracks
655
+
656
+ // Rebuild only the items after the currently playing item
657
+ self.rebuildAVQueueFromCurrentPosition()
558
658
  }
559
659
  }
560
660
 
@@ -583,8 +683,8 @@ class TrackPlayerCore: NSObject {
583
683
  }
584
684
 
585
685
  print("🔔 TrackPlayerCore: Emitting state change: \(state)")
586
- print("🔔 TrackPlayerCore: Callback exists: \(onPlaybackStateChange != nil)")
587
- onPlaybackStateChange?(state, reason)
686
+ print("🔔 TrackPlayerCore: Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
687
+ notifyPlaybackStateChange(state, reason)
588
688
  mediaSessionManager?.onPlaybackStateChanged()
589
689
  }
590
690
 
@@ -594,9 +694,40 @@ class TrackPlayerCore: NSObject {
594
694
  private func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false)
595
695
  -> AVPlayerItem?
596
696
  {
597
- guard let url = URL(string: track.url) else {
598
- print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(track.url)")
599
- return nil
697
+ // Get effective URL - uses local path if downloaded, otherwise remote URL
698
+ let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
699
+
700
+ // Create URL - use fileURLWithPath for local files, URL(string:) for remote
701
+ let url: URL
702
+ let isLocal = effectiveUrlString.hasPrefix("/")
703
+
704
+ if isLocal {
705
+ // Local file - use fileURLWithPath
706
+ print("📥 TrackPlayerCore: Using DOWNLOADED version for \(track.title)")
707
+ print(" Local path: \(effectiveUrlString)")
708
+
709
+ // Verify file exists
710
+ if FileManager.default.fileExists(atPath: effectiveUrlString) {
711
+ url = URL(fileURLWithPath: effectiveUrlString)
712
+ print(" File URL: \(url.absoluteString)")
713
+ print(" ✅ File verified to exist")
714
+ } else {
715
+ print(" ❌ Downloaded file does NOT exist at path!")
716
+ print(" Falling back to remote URL: \(track.url)")
717
+ guard let remoteUrl = URL(string: track.url) else {
718
+ print("❌ TrackPlayerCore: Invalid remote URL: \(track.url)")
719
+ return nil
720
+ }
721
+ url = remoteUrl
722
+ }
723
+ } else {
724
+ // Remote URL
725
+ guard let remoteUrl = URL(string: effectiveUrlString) else {
726
+ print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(effectiveUrlString)")
727
+ return nil
728
+ }
729
+ url = remoteUrl
730
+ print("🌐 TrackPlayerCore: Using REMOTE version for \(track.title)")
600
731
  }
601
732
 
602
733
  // Check if we have a preloaded asset for this track
@@ -625,6 +756,13 @@ class TrackPlayerCore: NSObject {
625
756
  // Store track ID for later reference
626
757
  item.trackId = track.id
627
758
 
759
+ // Apply equalizer audio mix to the player item
760
+ // This enables real-time EQ processing via MTAudioProcessingTap
761
+ // Apply equalizer audio mix to the player item
762
+ // This enables real-time EQ processing via MTAudioProcessingTap
763
+ EqualizerCore.shared.applyAudioMix(to: item)
764
+ print("🎛️ TrackPlayerCore: Requesting EQ audio mix application for \(track.title)")
765
+
628
766
  // If this is a preload request, start loading asset keys asynchronously
629
767
  if isPreload {
630
768
  asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
@@ -654,10 +792,13 @@ class TrackPlayerCore: NSObject {
654
792
  preloadQueue.async { [weak self] in
655
793
  guard let self = self else { return }
656
794
 
657
- let endIndex = min(startIndex + Constants.gaplessPreloadCount, self.currentTracks.count)
795
+ // Capture currentTracks to avoid race condition with main thread
796
+ let tracks = self.currentTracks
797
+ let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
658
798
 
659
799
  for i in startIndex..<endIndex {
660
- let track = self.currentTracks[i]
800
+ guard i < tracks.count else { break }
801
+ let track = tracks[i]
661
802
 
662
803
  // Skip if already preloaded
663
804
  if self.preloadedAssets[track.id] != nil {
@@ -717,6 +858,141 @@ class TrackPlayerCore: NSObject {
717
858
  }
718
859
  }
719
860
 
861
+ // MARK: - Listener Registration
862
+
863
+ func addOnChangeTrackListener(
864
+ owner: AnyObject, _ listener: @escaping (TrackItem, Reason?) -> Void
865
+ ) {
866
+ let box = WeakCallbackBox(owner: owner, callback: listener)
867
+ listenersQueue.async(flags: .barrier) { [weak self] in
868
+ self?.onChangeTrackListeners.append(box)
869
+ print(
870
+ "🎯 TrackPlayerCore: Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))"
871
+ )
872
+ }
873
+ }
874
+
875
+ func addOnPlaybackStateChangeListener(
876
+ owner: AnyObject,
877
+ _ listener: @escaping (TrackPlayerState, Reason?) -> Void
878
+ ) {
879
+ let box = WeakCallbackBox(owner: owner, callback: listener)
880
+ listenersQueue.async(flags: .barrier) { [weak self] in
881
+ self?.onPlaybackStateChangeListeners.append(box)
882
+ print(
883
+ "🎯 TrackPlayerCore: Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))"
884
+ )
885
+ }
886
+ }
887
+
888
+ func addOnSeekListener(owner: AnyObject, _ listener: @escaping (Double, Double) -> Void) {
889
+ let box = WeakCallbackBox(owner: owner, callback: listener)
890
+ listenersQueue.async(flags: .barrier) { [weak self] in
891
+ self?.onSeekListeners.append(box)
892
+ print("🎯 TrackPlayerCore: Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
893
+ }
894
+ }
895
+
896
+ func addOnPlaybackProgressChangeListener(
897
+ owner: AnyObject,
898
+ _ listener: @escaping (Double, Double, Bool?) -> Void
899
+ ) {
900
+ let box = WeakCallbackBox(owner: owner, callback: listener)
901
+ listenersQueue.async(flags: .barrier) { [weak self] in
902
+ self?.onPlaybackProgressChangeListeners.append(box)
903
+ print(
904
+ "🎯 TrackPlayerCore: Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))"
905
+ )
906
+ }
907
+ }
908
+
909
+ // MARK: - Listener Notification Helpers
910
+
911
+ private func notifyTrackChange(_ track: TrackItem, _ reason: Reason?) {
912
+ listenersQueue.async(flags: .barrier) { [weak self] in
913
+ guard let self = self else { return }
914
+
915
+ // Remove dead listeners
916
+ self.onChangeTrackListeners.removeAll { !$0.isAlive }
917
+
918
+ // Get live callbacks
919
+ let liveCallbacks = self.onChangeTrackListeners.compactMap {
920
+ $0.isAlive ? $0.callback : nil
921
+ }
922
+
923
+ // Call on main thread
924
+ if !liveCallbacks.isEmpty {
925
+ DispatchQueue.main.async {
926
+ for callback in liveCallbacks {
927
+ callback(track, reason)
928
+ }
929
+ }
930
+ }
931
+ }
932
+ }
933
+
934
+ private func notifyPlaybackStateChange(_ state: TrackPlayerState, _ reason: Reason?) {
935
+ listenersQueue.async(flags: .barrier) { [weak self] in
936
+ guard let self = self else { return }
937
+
938
+ self.onPlaybackStateChangeListeners.removeAll { !$0.isAlive }
939
+
940
+ let liveCallbacks = self.onPlaybackStateChangeListeners.compactMap {
941
+ $0.isAlive ? $0.callback : nil
942
+ }
943
+
944
+ if !liveCallbacks.isEmpty {
945
+ DispatchQueue.main.async {
946
+ for callback in liveCallbacks {
947
+ callback(state, reason)
948
+ }
949
+ }
950
+ }
951
+ }
952
+ }
953
+
954
+ private func notifySeek(_ position: Double, _ duration: Double) {
955
+ listenersQueue.async(flags: .barrier) { [weak self] in
956
+ guard let self = self else { return }
957
+
958
+ self.onSeekListeners.removeAll { !$0.isAlive }
959
+
960
+ let liveCallbacks = self.onSeekListeners.compactMap {
961
+ $0.isAlive ? $0.callback : nil
962
+ }
963
+
964
+ if !liveCallbacks.isEmpty {
965
+ DispatchQueue.main.async {
966
+ for callback in liveCallbacks {
967
+ callback(position, duration)
968
+ }
969
+ }
970
+ }
971
+ }
972
+ }
973
+
974
+ private func notifyPlaybackProgress(_ position: Double, _ duration: Double, _ isPlaying: Bool?) {
975
+ listenersQueue.async(flags: .barrier) { [weak self] in
976
+ guard let self = self else { return }
977
+
978
+ self.onPlaybackProgressChangeListeners.removeAll { !$0.isAlive }
979
+
980
+ let liveCallbacks = self.onPlaybackProgressChangeListeners.compactMap {
981
+ $0.isAlive ? $0.callback : nil
982
+ }
983
+
984
+ if !liveCallbacks.isEmpty {
985
+ DispatchQueue.main.async {
986
+ for callback in liveCallbacks {
987
+ callback(position, duration, isPlaying)
988
+ }
989
+ }
990
+ }
991
+ }
992
+ }
993
+
994
+ // MARK: - State Management
995
+
720
996
  // MARK: - Queue Management
721
997
 
722
998
  private func updatePlayerQueue(tracks: [TrackItem]) {
@@ -724,9 +1000,17 @@ class TrackPlayerCore: NSObject {
724
1000
  print("📋 TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
725
1001
  print(String(repeating: "=", count: Constants.separatorLineLength))
726
1002
 
727
- // Print the full playlist being fed
1003
+ // Print the full playlist being fed and check download status
728
1004
  for (index, track) in tracks.enumerated() {
729
- print(" [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id))")
1005
+ let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
1006
+ let downloadStatus = isDownloaded ? "📥 DOWNLOADED" : "🌐 REMOTE"
1007
+ print(
1008
+ " [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
1009
+ if isDownloaded {
1010
+ if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
1011
+ print(" Local path: \(localPath)")
1012
+ }
1013
+ }
730
1014
  }
731
1015
  print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
732
1016
 
@@ -808,8 +1092,8 @@ class TrackPlayerCore: NSObject {
808
1092
  // Notify track change
809
1093
  if let firstTrack = tracks.first {
810
1094
  print("🎵 TrackPlayerCore: Emitting track change: \(firstTrack.title)")
811
- print("🎵 TrackPlayerCore: onChangeTrack callback exists: \(onChangeTrack != nil)")
812
- onChangeTrack?(firstTrack, nil)
1095
+ print("🎵 TrackPlayerCore: onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
1096
+ notifyTrackChange(firstTrack, nil)
813
1097
  mediaSessionManager?.onTrackChanged()
814
1098
  }
815
1099
 
@@ -820,226 +1104,323 @@ class TrackPlayerCore: NSObject {
820
1104
  }
821
1105
 
822
1106
  func getCurrentTrack() -> TrackItem? {
1107
+ // If playing a temporary track, return that
1108
+ if currentTemporaryType != .none,
1109
+ let currentItem = player?.currentItem,
1110
+ let trackId = currentItem.trackId
1111
+ {
1112
+ if currentTemporaryType == .playNext {
1113
+ return playNextStack.first(where: { $0.id == trackId })
1114
+ } else if currentTemporaryType == .upNext {
1115
+ return upNextQueue.first(where: { $0.id == trackId })
1116
+ }
1117
+ }
1118
+
1119
+ // Otherwise return from original playlist
823
1120
  guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else {
824
1121
  return nil
825
1122
  }
826
1123
  return currentTracks[currentTrackIndex]
827
1124
  }
828
1125
 
1126
+ func getActualQueue() -> [TrackItem] {
1127
+ // Called from Promise.async background thread
1128
+ // Schedule on main thread and wait for result
1129
+ if Thread.isMainThread {
1130
+ return getActualQueueInternal()
1131
+ } else {
1132
+ var queue: [TrackItem] = []
1133
+ DispatchQueue.main.sync { [weak self] in
1134
+ queue = self?.getActualQueueInternal() ?? []
1135
+ }
1136
+ return queue
1137
+ }
1138
+ }
1139
+
1140
+ private func getActualQueueInternal() -> [TrackItem] {
1141
+ var queue: [TrackItem] = []
1142
+
1143
+ // Add tracks before current (original playlist)
1144
+ // When a temp track is playing, include the original track at currentTrackIndex
1145
+ // (it already played before the temp track started)
1146
+ let beforeEnd = currentTemporaryType != .none
1147
+ ? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
1148
+ if beforeEnd > 0 {
1149
+ queue.append(contentsOf: Array(currentTracks[0..<beforeEnd]))
1150
+ }
1151
+
1152
+ // Add current track (temp or original)
1153
+ if let current = getCurrentTrack() {
1154
+ queue.append(current)
1155
+ }
1156
+
1157
+ // Add playNext stack (LIFO - most recently added plays first)
1158
+ // Skip index 0 if current track is from playNext (it's already added as current)
1159
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1160
+ queue.append(contentsOf: Array(playNextStack.dropFirst()))
1161
+ } else if currentTemporaryType != .playNext {
1162
+ queue.append(contentsOf: playNextStack)
1163
+ }
1164
+
1165
+ // Add upNext queue (in order, FIFO)
1166
+ // Skip index 0 if current track is from upNext (it's already added as current)
1167
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1168
+ queue.append(contentsOf: Array(upNextQueue.dropFirst()))
1169
+ } else if currentTemporaryType != .upNext {
1170
+ queue.append(contentsOf: upNextQueue)
1171
+ }
1172
+
1173
+ // Add remaining original tracks
1174
+ if currentTrackIndex + 1 < currentTracks.count {
1175
+ queue.append(contentsOf: Array(currentTracks[(currentTrackIndex + 1)...]))
1176
+ }
1177
+
1178
+ return queue
1179
+ }
1180
+
829
1181
  func play() {
830
1182
  print("▶️ TrackPlayerCore: play() called")
831
- DispatchQueue.main.async { [weak self] in
832
- guard let self = self else { return }
833
- print("▶️ TrackPlayerCore: Calling player.play()")
834
- if let player = self.player {
835
- print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
836
- if let currentItem = player.currentItem {
837
- print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
838
- if let error = currentItem.error {
839
- print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
840
- }
841
- }
842
- player.play()
843
- // Emit state change immediately for responsive UI
844
- // KVO will also fire, but this ensures immediate feedback
845
- DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
846
- [weak self] in
847
- self?.emitStateChange()
1183
+ if Thread.isMainThread {
1184
+ playInternal()
1185
+ } else {
1186
+ DispatchQueue.main.sync { [weak self] in
1187
+ self?.playInternal()
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ private func playInternal() {
1193
+ print("▶️ TrackPlayerCore: Calling player.play()")
1194
+ if let player = self.player {
1195
+ print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
1196
+ if let currentItem = player.currentItem {
1197
+ print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
1198
+ if let error = currentItem.error {
1199
+ print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
848
1200
  }
849
- } else {
850
- print("❌ TrackPlayerCore: No player available")
851
1201
  }
1202
+ player.play()
1203
+ // Emit state change immediately for responsive UI
1204
+ // KVO will also fire, but this ensures immediate feedback
1205
+ DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
1206
+ [weak self] in
1207
+ self?.emitStateChange()
1208
+ }
1209
+ } else {
1210
+ print("❌ TrackPlayerCore: No player available")
852
1211
  }
853
1212
  }
854
1213
 
855
1214
  func pause() {
856
1215
  print("⏸️ TrackPlayerCore: pause() called")
857
- DispatchQueue.main.async { [weak self] in
858
- guard let self = self else { return }
859
- self.player?.pause()
860
- // Emit state change immediately for responsive UI
861
- DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
862
- self?.emitStateChange()
1216
+ if Thread.isMainThread {
1217
+ pauseInternal()
1218
+ } else {
1219
+ DispatchQueue.main.sync { [weak self] in
1220
+ self?.pauseInternal()
863
1221
  }
864
1222
  }
865
1223
  }
866
1224
 
867
- func playSong(songId: String, fromPlaylist: String?) {
868
- print(
869
- "🎵 TrackPlayerCore: playSong() called - songId: \(songId), fromPlaylist: \(fromPlaylist ?? "nil")"
870
- )
1225
+ private func pauseInternal() {
1226
+ self.player?.pause()
1227
+ // Emit state change immediately for responsive UI
1228
+ DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
1229
+ self?.emitStateChange()
1230
+ }
1231
+ }
871
1232
 
1233
+ func playSong(songId: String, fromPlaylist: String?) {
872
1234
  DispatchQueue.main.async { [weak self] in
873
- guard let self = self else { return }
874
-
875
- var targetPlaylistId: String?
876
- var songIndex: Int = -1
1235
+ self?.playSongInternal(songId: songId, fromPlaylist: fromPlaylist)
1236
+ }
1237
+ }
877
1238
 
878
- // Case 1: If fromPlaylist is provided, use that playlist
879
- if let playlistId = fromPlaylist {
880
- print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
881
- if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
882
- if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
883
- targetPlaylistId = playlistId
884
- songIndex = index
885
- print("✅ Found song at index \(index) in playlist \(playlistId)")
886
- } else {
887
- print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
888
- return
889
- }
1239
+ private func playSongInternal(songId: String, fromPlaylist: String?) {
1240
+ // Clear temporary tracks when directly playing a song
1241
+ self.playNextStack.removeAll()
1242
+ self.upNextQueue.removeAll()
1243
+ self.currentTemporaryType = .none
1244
+ print(" 🧹 Cleared temporary tracks")
1245
+
1246
+ var targetPlaylistId: String?
1247
+ var songIndex: Int = -1
1248
+
1249
+ // Case 1: If fromPlaylist is provided, use that playlist
1250
+ if let playlistId = fromPlaylist {
1251
+ print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
1252
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1253
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1254
+ targetPlaylistId = playlistId
1255
+ songIndex = index
1256
+ print("✅ Found song at index \(index) in playlist \(playlistId)")
890
1257
  } else {
891
- print("⚠️ Playlist \(playlistId) not found")
1258
+ print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
892
1259
  return
893
1260
  }
1261
+ } else {
1262
+ print("⚠️ Playlist \(playlistId) not found")
1263
+ return
894
1264
  }
895
- // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
896
- else {
897
- print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
898
-
899
- // Check if song exists in currently loaded playlist
900
- if let currentId = self.currentPlaylistId,
901
- let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
902
- {
903
- if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
904
- targetPlaylistId = currentId
905
- songIndex = index
906
- print("✅ Found song at index \(index) in current playlist \(currentId)")
907
- }
1265
+ }
1266
+ // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
1267
+ else {
1268
+ print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
1269
+
1270
+ // Check if song exists in currently loaded playlist
1271
+ if let currentId = self.currentPlaylistId,
1272
+ let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
1273
+ {
1274
+ if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
1275
+ targetPlaylistId = currentId
1276
+ songIndex = index
1277
+ print("✅ Found song at index \(index) in current playlist \(currentId)")
908
1278
  }
1279
+ }
909
1280
 
910
- // If not found in current playlist, search in all playlists
911
- if songIndex == -1 {
912
- print("🔍 Song not found in current playlist, searching all playlists...")
913
- let allPlaylists = self.playlistManager.getAllPlaylists()
1281
+ // If not found in current playlist, search in all playlists
1282
+ if songIndex == -1 {
1283
+ print("🔍 Song not found in current playlist, searching all playlists...")
1284
+ let allPlaylists = self.playlistManager.getAllPlaylists()
914
1285
 
915
- for playlist in allPlaylists {
916
- if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
917
- targetPlaylistId = playlist.id
918
- songIndex = index
919
- print("✅ Found song at index \(index) in playlist \(playlist.id)")
920
- break
921
- }
1286
+ for playlist in allPlaylists {
1287
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1288
+ targetPlaylistId = playlist.id
1289
+ songIndex = index
1290
+ print("✅ Found song at index \(index) in playlist \(playlist.id)")
1291
+ break
922
1292
  }
1293
+ }
923
1294
 
924
- // If still not found, just use the first playlist if available
925
- if songIndex == -1 && !allPlaylists.isEmpty {
926
- targetPlaylistId = allPlaylists[0].id
927
- songIndex = 0
928
- print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
929
- }
1295
+ // If still not found, just use the first playlist if available
1296
+ if songIndex == -1 && !allPlaylists.isEmpty {
1297
+ targetPlaylistId = allPlaylists[0].id
1298
+ songIndex = 0
1299
+ print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
930
1300
  }
931
1301
  }
1302
+ }
932
1303
 
933
- // Now play the song
934
- guard let playlistId = targetPlaylistId, songIndex >= 0 else {
935
- print("❌ Could not determine playlist or song index")
936
- return
937
- }
1304
+ // Now play the song
1305
+ guard let playlistId = targetPlaylistId, songIndex >= 0 else {
1306
+ print("❌ Could not determine playlist or song index")
1307
+ return
1308
+ }
938
1309
 
939
- // Load playlist if it's different from current
940
- if self.currentPlaylistId != playlistId {
941
- print("🔄 Loading new playlist: \(playlistId)")
942
- if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
943
- self.currentPlaylistId = playlistId
944
- self.updatePlayerQueue(tracks: playlist.tracks)
945
- }
1310
+ // Load playlist if it's different from current
1311
+ if self.currentPlaylistId != playlistId {
1312
+ print("🔄 Loading new playlist: \(playlistId)")
1313
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1314
+ self.currentPlaylistId = playlistId
1315
+ self.updatePlayerQueue(tracks: playlist.tracks)
946
1316
  }
947
-
948
- // Play from the found index
949
- print("▶️ Playing from index: \(songIndex)")
950
- self.playFromIndex(index: songIndex)
951
1317
  }
1318
+
1319
+ // Play from the found index
1320
+ print("▶️ Playing from index: \(songIndex)")
1321
+ self.playFromIndex(index: songIndex)
952
1322
  }
953
1323
 
954
1324
  func skipToNext() {
955
- DispatchQueue.main.async { [weak self] in
956
- guard let self = self,
957
- let queuePlayer = self.player
958
- else {
959
- return
1325
+ if Thread.isMainThread {
1326
+ skipToNextInternal()
1327
+ } else {
1328
+ DispatchQueue.main.sync { [weak self] in
1329
+ self?.skipToNextInternal()
960
1330
  }
1331
+ }
1332
+ }
961
1333
 
962
- print("\n⏭️ TrackPlayerCore: SKIP TO NEXT")
963
- print(" BEFORE:")
964
- print(" currentTrackIndex: \(self.currentTrackIndex)")
965
- print(" Total tracks in currentTracks: \(self.currentTracks.count)")
966
- print(" Items in player queue: \(queuePlayer.items().count)")
1334
+ private func skipToNextInternal() {
1335
+ guard let queuePlayer = self.player else { return }
967
1336
 
968
- if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
969
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
970
- print(" Currently playing: \(track.title) (ID: \(track.id))")
1337
+ // Remove current temp track from its list before advancing
1338
+ if let trackId = queuePlayer.currentItem?.trackId {
1339
+ if currentTemporaryType == .playNext {
1340
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1341
+ playNextStack.remove(at: idx)
1342
+ }
1343
+ } else if currentTemporaryType == .upNext {
1344
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1345
+ upNextQueue.remove(at: idx)
971
1346
  }
972
1347
  }
1348
+ }
973
1349
 
974
- // Check if there are more items in the queue
975
- if self.currentTrackIndex + 1 < self.currentTracks.count {
976
- print(" 🔄 Calling advanceToNextItem()...")
977
- queuePlayer.advanceToNextItem()
978
-
979
- // NOTE: Don't manually update currentTrackIndex here!
980
- // The KVO observer (currentItemDidChange) will update it automatically
1350
+ // Check if there are more items in the player queue
1351
+ if queuePlayer.items().count > 1 {
1352
+ queuePlayer.advanceToNextItem()
1353
+ } else {
1354
+ queuePlayer.pause()
1355
+ self.notifyPlaybackStateChange(.stopped, .end)
1356
+ }
1357
+ }
981
1358
 
982
- print(" AFTER advanceToNextItem():")
983
- print(" Items in player queue: \(queuePlayer.items().count)")
1359
+ func skipToPrevious() {
1360
+ if Thread.isMainThread {
1361
+ skipToPreviousInternal()
1362
+ } else {
1363
+ DispatchQueue.main.sync { [weak self] in
1364
+ self?.skipToPreviousInternal()
1365
+ }
1366
+ }
1367
+ }
984
1368
 
985
- if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
986
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
987
- print(" New current item: \(track.title) (ID: \(track.id))")
1369
+ private func skipToPreviousInternal() {
1370
+ guard let queuePlayer = self.player else { return }
1371
+
1372
+ let currentTime = queuePlayer.currentTime()
1373
+ if currentTime.seconds > Constants.skipToPreviousThreshold {
1374
+ // If more than threshold seconds in, restart current track
1375
+ queuePlayer.seek(to: .zero)
1376
+ } else if self.currentTemporaryType != .none {
1377
+ // Playing temporary track — remove from its list, then restart
1378
+ if let trackId = queuePlayer.currentItem?.trackId {
1379
+ if currentTemporaryType == .playNext {
1380
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1381
+ playNextStack.remove(at: idx)
1382
+ }
1383
+ } else if currentTemporaryType == .upNext {
1384
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1385
+ upNextQueue.remove(at: idx)
988
1386
  }
989
1387
  }
990
-
991
- print(" ⏳ Waiting for KVO observer to update index...")
992
- } else {
993
- print(" ⚠️ No more tracks in playlist")
994
- // At end of playlist - stop or loop
995
- queuePlayer.pause()
996
- self.onPlaybackStateChange?(.stopped, .end)
997
1388
  }
1389
+ // Go to current original track position (skip back from temp)
1390
+ self.playFromIndex(index: self.currentTrackIndex)
1391
+ } else if self.currentTrackIndex > 0 {
1392
+ // Go to previous track in original playlist
1393
+ let previousIndex = self.currentTrackIndex - 1
1394
+ self.playFromIndex(index: previousIndex)
1395
+ } else {
1396
+ // Already at first track, restart it
1397
+ queuePlayer.seek(to: .zero)
998
1398
  }
999
1399
  }
1000
1400
 
1001
- func skipToPrevious() {
1002
- DispatchQueue.main.async { [weak self] in
1003
- guard let self = self,
1004
- let queuePlayer = self.player
1005
- else {
1006
- return
1007
- }
1008
-
1009
- print("\n⏮️ TrackPlayerCore: SKIP TO PREVIOUS")
1010
- print(" Current index: \(self.currentTrackIndex)")
1011
- print(" Current time: \(queuePlayer.currentTime().seconds)s")
1012
-
1013
- let currentTime = queuePlayer.currentTime()
1014
- if currentTime.seconds > Constants.skipToPreviousThreshold {
1015
- // If more than threshold seconds in, restart current track
1016
- print(
1017
- " 🔄 More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
1018
- queuePlayer.seek(to: .zero)
1019
- } else if self.currentTrackIndex > 0 {
1020
- // Go to previous track
1021
- let previousIndex = self.currentTrackIndex - 1
1022
- print(" ⏮️ Going to previous track at index \(previousIndex)")
1023
- self.playFromIndex(index: previousIndex)
1024
- } else {
1025
- // Already at first track, restart it
1026
- print(" 🔄 Already at first track, restarting it")
1027
- queuePlayer.seek(to: .zero)
1401
+ func seek(position: Double) {
1402
+ if Thread.isMainThread {
1403
+ seekInternal(position: position)
1404
+ } else {
1405
+ DispatchQueue.main.sync { [weak self] in
1406
+ self?.seekInternal(position: position)
1028
1407
  }
1029
1408
  }
1030
1409
  }
1031
1410
 
1032
- func seek(position: Double) {
1033
- DispatchQueue.main.async { [weak self] in
1034
- guard let self = self, let player = self.player else { return }
1035
-
1036
- self.isManuallySeeked = true
1037
- let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
1038
- player.seek(to: time) { [weak self] completed in
1039
- if completed {
1040
- let duration = player.currentItem?.duration.seconds ?? 0.0
1041
- self?.onSeek?(position, duration)
1042
- }
1411
+ private func seekInternal(position: Double) {
1412
+ guard let player = self.player else { return }
1413
+
1414
+ self.isManuallySeeked = true
1415
+ let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
1416
+ player.seek(to: time) { [weak self] completed in
1417
+ // Always update now playing info to restore playback rate after seek
1418
+ // This ensures the scrubber animation resumes correctly
1419
+ self?.mediaSessionManager?.updateNowPlayingInfo()
1420
+
1421
+ if completed {
1422
+ let duration = player.currentItem?.duration.seconds ?? 0.0
1423
+ self?.notifySeek(position, duration)
1043
1424
  }
1044
1425
  }
1045
1426
  }
@@ -1048,13 +1429,41 @@ class TrackPlayerCore: NSObject {
1048
1429
 
1049
1430
  func setRepeatMode(mode: RepeatMode) -> Bool {
1050
1431
  print("🔁 TrackPlayerCore: setRepeatMode called with mode: \(mode)")
1051
- self.repeatMode = mode
1432
+ if Thread.isMainThread {
1433
+ self.repeatMode = mode
1434
+ } else {
1435
+ DispatchQueue.main.sync { [weak self] in
1436
+ self?.repeatMode = mode
1437
+ }
1438
+ }
1052
1439
  return true
1053
1440
  }
1054
1441
 
1055
- // MARK: - State Management
1056
-
1057
1442
  func getState() -> PlayerState {
1443
+ // Called from Promise.async background thread
1444
+ // Schedule on main thread and wait for result
1445
+ if Thread.isMainThread {
1446
+ return getStateInternal()
1447
+ } else {
1448
+ var state: PlayerState!
1449
+ DispatchQueue.main.sync { [weak self] in
1450
+ state =
1451
+ self?.getStateInternal()
1452
+ ?? PlayerState(
1453
+ currentTrack: nil,
1454
+ currentPosition: 0.0,
1455
+ totalDuration: 0.0,
1456
+ currentState: .stopped,
1457
+ currentPlaylistId: nil,
1458
+ currentIndex: -1.0,
1459
+ currentPlayingType: .notPlaying
1460
+ )
1461
+ }
1462
+ return state
1463
+ }
1464
+ }
1465
+
1466
+ private func getStateInternal() -> PlayerState {
1058
1467
  guard let player = player else {
1059
1468
  return PlayerState(
1060
1469
  currentTrack: nil,
@@ -1062,7 +1471,8 @@ class TrackPlayerCore: NSObject {
1062
1471
  totalDuration: 0.0,
1063
1472
  currentState: .stopped,
1064
1473
  currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
1065
- currentIndex: -1.0
1474
+ currentIndex: -1.0,
1475
+ currentPlayingType: .notPlaying
1066
1476
  )
1067
1477
  }
1068
1478
 
@@ -1082,13 +1492,29 @@ class TrackPlayerCore: NSObject {
1082
1492
  // Get current index
1083
1493
  let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
1084
1494
 
1495
+ // Map internal temporary type to CurrentPlayingType
1496
+ let currentPlayingType: CurrentPlayingType
1497
+ if currentTrack == nil {
1498
+ currentPlayingType = .notPlaying
1499
+ } else {
1500
+ switch currentTemporaryType {
1501
+ case .none:
1502
+ currentPlayingType = .playlist
1503
+ case .playNext:
1504
+ currentPlayingType = .playNext
1505
+ case .upNext:
1506
+ currentPlayingType = .upNext
1507
+ }
1508
+ }
1509
+
1085
1510
  return PlayerState(
1086
1511
  currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
1087
1512
  currentPosition: currentPosition,
1088
1513
  totalDuration: totalDuration,
1089
1514
  currentState: currentState,
1090
1515
  currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
1091
- currentIndex: currentIndex
1516
+ currentIndex: currentIndex,
1517
+ currentPlayingType: currentPlayingType
1092
1518
  )
1093
1519
  }
1094
1520
 
@@ -1133,72 +1559,354 @@ class TrackPlayerCore: NSObject {
1133
1559
  }
1134
1560
 
1135
1561
  func playFromIndex(index: Int) {
1136
- DispatchQueue.main.async { [weak self] in
1137
- guard let self = self,
1138
- index >= 0 && index < self.currentTracks.count
1139
- else {
1140
- print("❌ TrackPlayerCore: playFromIndex - invalid index \(index)")
1141
- return
1562
+ if Thread.isMainThread {
1563
+ playFromIndexInternal(index: index)
1564
+ } else {
1565
+ DispatchQueue.main.async { [weak self] in
1566
+ self?.playFromIndexInternal(index: index)
1142
1567
  }
1568
+ }
1569
+ }
1570
+
1571
+ // MARK: - Skip to Index in Actual Queue
1143
1572
 
1144
- print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
1145
- print(" Total tracks in playlist: \(self.currentTracks.count)")
1146
- print(" Current index: \(self.currentTrackIndex), target index: \(index)")
1573
+ func skipToIndex(index: Int) -> Bool {
1574
+ if Thread.isMainThread {
1575
+ return skipToIndexInternal(index: index)
1576
+ } else {
1577
+ var result = false
1578
+ DispatchQueue.main.sync { [weak self] in
1579
+ result = self?.skipToIndexInternal(index: index) ?? false
1580
+ }
1581
+ return result
1582
+ }
1583
+ }
1147
1584
 
1148
- // Store the full playlist
1149
- let fullPlaylist = self.currentTracks
1585
+ private func skipToIndexInternal(index: Int) -> Bool {
1586
+ // Get actual queue to validate index and determine position
1587
+ let actualQueue = getActualQueueInternal()
1588
+ let totalQueueSize = actualQueue.count
1589
+
1590
+ // Validate index
1591
+ guard index >= 0 && index < totalQueueSize else { return false }
1592
+
1593
+ // Calculate queue section boundaries using effective sizes
1594
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
1595
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
1596
+ // so the current playing position shifts by 1
1597
+ let currentPos = currentTemporaryType != .none
1598
+ ? currentTrackIndex + 1 : currentTrackIndex
1599
+ let effectivePlayNextSize = currentTemporaryType == .playNext
1600
+ ? max(0, playNextStack.count - 1) : playNextStack.count
1601
+ let effectiveUpNextSize = currentTemporaryType == .upNext
1602
+ ? max(0, upNextQueue.count - 1) : upNextQueue.count
1603
+
1604
+ let playNextStart = currentPos + 1
1605
+ let playNextEnd = playNextStart + effectivePlayNextSize
1606
+ let upNextStart = playNextEnd
1607
+ let upNextEnd = upNextStart + effectiveUpNextSize
1608
+ let originalRemainingStart = upNextEnd
1609
+
1610
+ // Case 1: Target is before current - use playFromIndex on original
1611
+ if index < currentPos {
1612
+ playFromIndexInternal(index: index)
1613
+ return true
1614
+ }
1150
1615
 
1151
- // Update currentTrackIndex BEFORE updating queue
1152
- self.currentTrackIndex = index
1616
+ // Case 2: Target is current - seek to beginning
1617
+ if index == currentPos {
1618
+ player?.seek(to: .zero)
1619
+ return true
1620
+ }
1153
1621
 
1154
- // Recreate the queue starting from the target index
1155
- // This ensures all remaining tracks are in the queue
1156
- let tracksToPlay = Array(fullPlaylist[index...])
1157
- print(
1158
- " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
1159
- )
1622
+ // Case 3: Target is in playNext section
1623
+ if index >= playNextStart && index < playNextEnd {
1624
+ let playNextIndex = index - playNextStart
1625
+ // Offset by 1 if current is from playNext (index 0 is already playing)
1626
+ let actualListIndex = currentTemporaryType == .playNext
1627
+ ? playNextIndex + 1 : playNextIndex
1160
1628
 
1161
- // Create gapless-optimized player items
1162
- let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
1163
- // First few items get preload treatment for faster playback
1164
- let isPreload = offset < Constants.gaplessPreloadCount
1165
- return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
1629
+ // Remove tracks before the target from playNext (they're being skipped)
1630
+ if actualListIndex > 0 {
1631
+ playNextStack.removeFirst(actualListIndex)
1166
1632
  }
1167
1633
 
1168
- guard let player = self.player, !items.isEmpty else {
1169
- print("❌ No player or no items to play")
1170
- return
1634
+ // Rebuild queue and advance
1635
+ rebuildAVQueueFromCurrentPosition()
1636
+ player?.advanceToNextItem()
1637
+ return true
1638
+ }
1639
+
1640
+ // Case 4: Target is in upNext section
1641
+ if index >= upNextStart && index < upNextEnd {
1642
+ let upNextIndex = index - upNextStart
1643
+ // Offset by 1 if current is from upNext (index 0 is already playing)
1644
+ let actualListIndex = currentTemporaryType == .upNext
1645
+ ? upNextIndex + 1 : upNextIndex
1646
+
1647
+ // Clear all playNext tracks (they're being skipped)
1648
+ playNextStack.removeAll()
1649
+
1650
+ // Remove tracks before target from upNext
1651
+ if actualListIndex > 0 {
1652
+ upNextQueue.removeFirst(actualListIndex)
1171
1653
  }
1172
1654
 
1173
- // Remove old boundary observer
1174
- if let boundaryObserver = self.boundaryTimeObserver {
1175
- player.removeTimeObserver(boundaryObserver)
1176
- self.boundaryTimeObserver = nil
1655
+ // Rebuild queue and advance
1656
+ rebuildAVQueueFromCurrentPosition()
1657
+ player?.advanceToNextItem()
1658
+ return true
1659
+ }
1660
+
1661
+ // Case 5: Target is in remaining original tracks
1662
+ if index >= originalRemainingStart {
1663
+ let targetTrack = actualQueue[index]
1664
+
1665
+ // Find this track's index in the original playlist
1666
+ guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
1667
+ return false
1177
1668
  }
1178
1669
 
1179
- // Clear and rebuild queue
1180
- player.removeAllItems()
1181
- var lastItem: AVPlayerItem? = nil
1182
- for item in items {
1670
+ // Clear all temporary tracks (they're being skipped)
1671
+ playNextStack.removeAll()
1672
+ upNextQueue.removeAll()
1673
+ currentTemporaryType = .none
1674
+
1675
+ return playFromIndexInternalWithResult(index: originalIndex)
1676
+ }
1677
+
1678
+ return false
1679
+ }
1680
+
1681
+ private func playFromIndexInternal(index: Int) {
1682
+ _ = playFromIndexInternalWithResult(index: index)
1683
+ }
1684
+
1685
+ private func playFromIndexInternalWithResult(index: Int) -> Bool {
1686
+ guard index >= 0 && index < self.currentTracks.count else {
1687
+ print(
1688
+ "❌ TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)"
1689
+ )
1690
+ return false
1691
+ }
1692
+
1693
+ print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
1694
+ print(" Total tracks in playlist: \(self.currentTracks.count)")
1695
+ print(" Current index: \(self.currentTrackIndex), target index: \(index)")
1696
+
1697
+ // Clear temporary tracks when jumping to specific index
1698
+ self.playNextStack.removeAll()
1699
+ self.upNextQueue.removeAll()
1700
+ self.currentTemporaryType = .none
1701
+ print(" 🧹 Cleared temporary tracks")
1702
+
1703
+ // Store the full playlist
1704
+ let fullPlaylist = self.currentTracks
1705
+
1706
+ // Update currentTrackIndex BEFORE updating queue
1707
+ self.currentTrackIndex = index
1708
+
1709
+ // Recreate the queue starting from the target index
1710
+ // This ensures all remaining tracks are in the queue
1711
+ let tracksToPlay = Array(fullPlaylist[index...])
1712
+ print(
1713
+ " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
1714
+ )
1715
+
1716
+ // Create gapless-optimized player items
1717
+ let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
1718
+ // First few items get preload treatment for faster playback
1719
+ let isPreload = offset < Constants.gaplessPreloadCount
1720
+ return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
1721
+ }
1722
+
1723
+ guard let player = self.player, !items.isEmpty else {
1724
+ print("❌ No player or no items to play")
1725
+ return false
1726
+ }
1727
+
1728
+ // Remove old boundary observer
1729
+ if let boundaryObserver = self.boundaryTimeObserver {
1730
+ player.removeTimeObserver(boundaryObserver)
1731
+ self.boundaryTimeObserver = nil
1732
+ }
1733
+
1734
+ // Clear and rebuild queue
1735
+ player.removeAllItems()
1736
+ var lastItem: AVPlayerItem? = nil
1737
+ for item in items {
1738
+ player.insert(item, after: lastItem)
1739
+ lastItem = item
1740
+ }
1741
+
1742
+ // Restore the full playlist reference (don't slice it!)
1743
+ self.currentTracks = fullPlaylist
1744
+
1745
+ print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1746
+ if let track = self.getCurrentTrack() {
1747
+ print(" 🎵 Playing: \(track.title)")
1748
+ notifyTrackChange(track, .skip)
1749
+ self.mediaSessionManager?.onTrackChanged()
1750
+ }
1751
+
1752
+ // Start preloading upcoming tracks for gapless playback
1753
+ self.preloadUpcomingTracks(from: index + 1)
1754
+
1755
+ player.play()
1756
+ return true
1757
+ }
1758
+
1759
+ // MARK: - Temporary Track Management
1760
+
1761
+ /**
1762
+ * Add a track to the up-next queue (FIFO - first added plays first)
1763
+ * Track will be inserted after currently playing track and any playNext tracks
1764
+ */
1765
+ func addToUpNext(trackId: String) {
1766
+ DispatchQueue.main.async { [weak self] in
1767
+ self?.addToUpNextInternal(trackId: trackId)
1768
+ }
1769
+ }
1770
+
1771
+ private func addToUpNextInternal(trackId: String) {
1772
+ print("📋 TrackPlayerCore: addToUpNext(\(trackId))")
1773
+
1774
+ // Find the track from current playlist or all playlists
1775
+ guard let track = self.findTrackById(trackId) else {
1776
+ print("❌ TrackPlayerCore: Track \(trackId) not found")
1777
+ return
1778
+ }
1779
+
1780
+ // Add to end of upNext queue (FIFO)
1781
+ self.upNextQueue.append(track)
1782
+ print(" ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
1783
+
1784
+ // Rebuild the player queue if actively playing
1785
+ if self.player?.currentItem != nil {
1786
+ self.rebuildAVQueueFromCurrentPosition()
1787
+ }
1788
+ }
1789
+
1790
+ /**
1791
+ * Add a track to play next (LIFO - last added plays first)
1792
+ * Track will be inserted immediately after currently playing track
1793
+ */
1794
+ func playNext(trackId: String) {
1795
+ DispatchQueue.main.async { [weak self] in
1796
+ self?.playNextInternal(trackId: trackId)
1797
+ }
1798
+ }
1799
+
1800
+ private func playNextInternal(trackId: String) {
1801
+ print("⏭️ TrackPlayerCore: playNext(\(trackId))")
1802
+
1803
+ // Find the track from current playlist or all playlists
1804
+ guard let track = self.findTrackById(trackId) else {
1805
+ print("❌ TrackPlayerCore: Track \(trackId) not found")
1806
+ return
1807
+ }
1808
+
1809
+ // Insert at beginning of playNext stack (LIFO)
1810
+ self.playNextStack.insert(track, at: 0)
1811
+ print(" ✅ Added '\(track.title)' to playNext stack (position: 1)")
1812
+
1813
+ // Rebuild the player queue if actively playing
1814
+ if self.player?.currentItem != nil {
1815
+ self.rebuildAVQueueFromCurrentPosition()
1816
+ }
1817
+ }
1818
+
1819
+ /**
1820
+ * Rebuild the AVQueuePlayer from current position with temporary tracks
1821
+ * Order: [current] + [playNext stack reversed] + [upNext queue] + [remaining original]
1822
+ */
1823
+ private func rebuildAVQueueFromCurrentPosition() {
1824
+ guard let player = self.player else { return }
1825
+
1826
+ let currentItem = player.currentItem
1827
+ let playingItems = player.items()
1828
+
1829
+ var newQueueTracks: [TrackItem] = []
1830
+
1831
+ // Add playNext stack (LIFO - most recently added plays first)
1832
+ // Skip index 0 if current track is from playNext (it's already playing)
1833
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1834
+ newQueueTracks.append(contentsOf: Array(playNextStack.dropFirst()))
1835
+ } else if currentTemporaryType != .playNext {
1836
+ newQueueTracks.append(contentsOf: playNextStack)
1837
+ }
1838
+
1839
+ // Add upNext queue (in order, FIFO)
1840
+ // Skip index 0 if current track is from upNext (it's already playing)
1841
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1842
+ newQueueTracks.append(contentsOf: Array(upNextQueue.dropFirst()))
1843
+ } else if currentTemporaryType != .upNext {
1844
+ newQueueTracks.append(contentsOf: upNextQueue)
1845
+ }
1846
+
1847
+ // Add remaining original tracks
1848
+ if currentTrackIndex + 1 < currentTracks.count {
1849
+ let remainingOriginal = Array(currentTracks[(currentTrackIndex + 1)...])
1850
+ newQueueTracks.append(contentsOf: remainingOriginal)
1851
+ }
1852
+
1853
+ // Remove all items from player EXCEPT the currently playing one
1854
+ for item in playingItems where item != currentItem {
1855
+ player.remove(item)
1856
+ }
1857
+
1858
+ // Insert new items in order
1859
+ var lastItem = currentItem
1860
+ for track in newQueueTracks {
1861
+ if let item = createGaplessPlayerItem(for: track, isPreload: false) {
1183
1862
  player.insert(item, after: lastItem)
1184
1863
  lastItem = item
1185
1864
  }
1865
+ }
1866
+
1867
+ }
1186
1868
 
1187
- // Restore the full playlist reference (don't slice it!)
1188
- self.currentTracks = fullPlaylist
1869
+ /**
1870
+ * Find a track by ID from current playlist or all playlists
1871
+ */
1872
+ private func findTrackById(_ trackId: String) -> TrackItem? {
1873
+ // First check current playlist
1874
+ if let track = currentTracks.first(where: { $0.id == trackId }) {
1875
+ return track
1876
+ }
1189
1877
 
1190
- print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1191
- if let track = self.getCurrentTrack() {
1192
- print(" 🎵 Playing: \(track.title)")
1193
- self.onChangeTrack?(track, .skip)
1194
- self.mediaSessionManager?.onTrackChanged()
1878
+ // Then check all playlists
1879
+ let allPlaylists = playlistManager.getAllPlaylists()
1880
+ for playlist in allPlaylists {
1881
+ if let track = playlist.tracks.first(where: { $0.id == trackId }) {
1882
+ return track
1195
1883
  }
1884
+ }
1196
1885
 
1197
- // Start preloading upcoming tracks for gapless playback
1198
- self.preloadUpcomingTracks(from: index + 1)
1886
+ return nil
1887
+ }
1199
1888
 
1200
- player.play()
1889
+ /**
1890
+ * Determine what type of track is currently playing
1891
+ */
1892
+ private func determineCurrentTemporaryType() -> TemporaryType {
1893
+ guard let currentItem = player?.currentItem,
1894
+ let trackId = currentItem.trackId
1895
+ else {
1896
+ return .none
1201
1897
  }
1898
+
1899
+ // Check if in playNext stack
1900
+ if playNextStack.contains(where: { $0.id == trackId }) {
1901
+ return .playNext
1902
+ }
1903
+
1904
+ // Check if in upNext queue
1905
+ if upNextQueue.contains(where: { $0.id == trackId }) {
1906
+ return .upNext
1907
+ }
1908
+
1909
+ return .none
1202
1910
  }
1203
1911
 
1204
1912
  // MARK: - Cleanup