react-native-nitro-player 0.3.0-alpha.9 → 0.4.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 (257) 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 +970 -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 +998 -276
  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/downloadCallbackManager.d.ts +36 -0
  31. package/lib/hooks/downloadCallbackManager.js +108 -0
  32. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  33. package/lib/hooks/equalizerCallbackManager.js +109 -0
  34. package/lib/hooks/index.d.ts +16 -0
  35. package/lib/hooks/index.js +10 -0
  36. package/lib/hooks/useActualQueue.d.ts +48 -0
  37. package/lib/hooks/useActualQueue.js +98 -0
  38. package/lib/hooks/useDownloadActions.d.ts +26 -0
  39. package/lib/hooks/useDownloadActions.js +117 -0
  40. package/lib/hooks/useDownloadProgress.d.ts +25 -0
  41. package/lib/hooks/useDownloadProgress.js +79 -0
  42. package/lib/hooks/useDownloadStorage.d.ts +19 -0
  43. package/lib/hooks/useDownloadStorage.js +60 -0
  44. package/lib/hooks/useDownloadedTracks.d.ts +25 -0
  45. package/lib/hooks/useDownloadedTracks.js +69 -0
  46. package/lib/hooks/useEqualizer.d.ts +25 -0
  47. package/lib/hooks/useEqualizer.js +124 -0
  48. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  49. package/lib/hooks/useEqualizerPresets.js +96 -0
  50. package/lib/hooks/useNowPlaying.js +3 -2
  51. package/lib/hooks/useOnChangeTrack.js +15 -12
  52. package/lib/hooks/useOnPlaybackStateChange.js +16 -13
  53. package/lib/hooks/usePlaylist.d.ts +48 -0
  54. package/lib/hooks/usePlaylist.js +136 -0
  55. package/lib/index.d.ts +6 -0
  56. package/lib/index.js +6 -0
  57. package/lib/specs/DownloadManager.nitro.d.ts +152 -0
  58. package/lib/specs/DownloadManager.nitro.js +1 -0
  59. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  60. package/lib/specs/Equalizer.nitro.js +1 -0
  61. package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
  62. package/lib/types/DownloadTypes.d.ts +110 -0
  63. package/lib/types/DownloadTypes.js +1 -0
  64. package/lib/types/EqualizerTypes.d.ts +52 -0
  65. package/lib/types/EqualizerTypes.js +1 -0
  66. package/lib/types/PlayerQueue.d.ts +4 -0
  67. package/nitro.json +8 -0
  68. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
  69. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
  70. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  71. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
  72. package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
  73. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
  74. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
  75. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
  76. package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
  77. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
  78. package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
  79. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
  80. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
  81. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  82. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  83. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  84. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
  85. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
  89. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
  90. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  91. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
  92. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  93. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
  94. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
  95. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  96. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  97. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
  98. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
  99. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
  100. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
  101. package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
  102. package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
  103. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  104. package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
  105. package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
  106. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
  107. package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
  108. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
  109. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
  110. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
  111. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
  112. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
  113. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
  114. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
  115. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
  116. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
  117. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
  118. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
  152. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
  153. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
  154. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
  155. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
  156. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
  157. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
  158. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
  160. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  162. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
  163. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
  164. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  165. package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
  166. package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
  167. package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
  168. package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
  169. package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
  170. package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
  171. package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
  172. package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
  173. package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
  174. package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
  175. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  176. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  177. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  178. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  179. package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
  180. package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
  181. package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
  182. package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
  183. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  184. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  185. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  186. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  187. package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
  188. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  189. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
  190. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  191. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  192. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
  193. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
  194. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  195. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  196. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
  197. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
  198. package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
  199. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  200. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  201. package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
  202. package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
  203. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
  204. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
  205. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
  206. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
  207. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
  208. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
  209. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  210. package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
  211. package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
  212. package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
  213. package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
  214. package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
  215. package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
  216. package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
  217. package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
  218. package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
  219. package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
  220. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  221. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  222. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  223. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  224. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
  225. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
  226. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  227. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  228. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
  229. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
  230. package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
  231. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  232. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  233. package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
  234. package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
  235. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
  236. package/package.json +1 -1
  237. package/src/hooks/downloadCallbackManager.ts +149 -0
  238. package/src/hooks/equalizerCallbackManager.ts +138 -0
  239. package/src/hooks/index.ts +23 -0
  240. package/src/hooks/useActualQueue.ts +116 -0
  241. package/src/hooks/useDownloadActions.ts +179 -0
  242. package/src/hooks/useDownloadProgress.ts +126 -0
  243. package/src/hooks/useDownloadStorage.ts +84 -0
  244. package/src/hooks/useDownloadedTracks.ts +138 -0
  245. package/src/hooks/useEqualizer.ts +173 -0
  246. package/src/hooks/useEqualizerPresets.ts +140 -0
  247. package/src/hooks/useNowPlaying.ts +3 -2
  248. package/src/hooks/useOnChangeTrack.ts +15 -11
  249. package/src/hooks/useOnPlaybackStateChange.ts +19 -15
  250. package/src/hooks/usePlaylist.ts +161 -0
  251. package/src/index.ts +12 -0
  252. package/src/specs/DownloadManager.nitro.ts +203 -0
  253. package/src/specs/Equalizer.nitro.ts +69 -0
  254. package/src/specs/TrackPlayer.nitro.ts +6 -2
  255. package/src/types/DownloadTypes.ts +135 -0
  256. package/src/types/EqualizerTypes.ts +72 -0
  257. 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
@@ -251,10 +285,22 @@ class TrackPlayerCore: NSObject {
251
285
  return
252
286
  }
253
287
 
254
- if let trackId = finishedItem.trackId,
255
- let track = currentTracks.first(where: { $0.id == trackId })
256
- {
257
- print("🏁 Finished: \(track.title)")
288
+ // Determine what type of track just finished and remove it from temporary lists
289
+ if let trackId = finishedItem.trackId {
290
+ // Check if it was a playNext track
291
+ if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
292
+ let track = playNextStack.remove(at: index)
293
+ print("🏁 Finished playNext track: \(track.title) - removed from stack")
294
+ }
295
+ // Check if it was an upNext track
296
+ else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
297
+ let track = upNextQueue.remove(at: index)
298
+ print("🏁 Finished upNext track: \(track.title) - removed from queue")
299
+ }
300
+ // Otherwise it was from original playlist
301
+ else if let track = currentTracks.first(where: { $0.id == trackId }) {
302
+ print("🏁 Finished original track: \(track.title)")
303
+ }
258
304
  }
259
305
 
260
306
  // Check remaining queue
@@ -269,23 +315,35 @@ class TrackPlayerCore: NSObject {
269
315
  print("🔁 TrackPlayerCore: Repeat mode is TRACK - replaying current track")
270
316
  DispatchQueue.main.async { [weak self] in
271
317
  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)
318
+ // For temporary tracks, just seek to beginning
319
+ if self.currentTemporaryType != .none {
320
+ player.seek(to: .zero)
321
+ player.play()
322
+ } else {
323
+ // For original tracks, recreate via playFromIndex
324
+ self.playFromIndex(index: self.currentTrackIndex)
325
+ }
274
326
  }
275
327
  return
276
328
 
277
329
  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)
330
+ // Check if we're at the end of the ORIGINAL playlist (ignore temps)
331
+ if currentTemporaryType == .none && currentTrackIndex >= currentTracks.count - 1 {
332
+ // Check if there are still temporary tracks
333
+ if !playNextStack.isEmpty || !upNextQueue.isEmpty {
334
+ print("🔁 TrackPlayerCore: Temporary tracks remaining, continuing...")
335
+ } else {
336
+ print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
337
+ // Clear temps and restart
338
+ playNextStack.removeAll()
339
+ upNextQueue.removeAll()
340
+ DispatchQueue.main.async { [weak self] in
341
+ guard let self = self else { return }
342
+ self.playFromIndex(index: 0)
343
+ }
344
+ return
285
345
  }
286
- return
287
346
  } else {
288
- // Not at end, just continue to next track
289
347
  print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - continuing to next track")
290
348
  }
291
349
 
@@ -295,7 +353,7 @@ class TrackPlayerCore: NSObject {
295
353
  }
296
354
 
297
355
  // Track ended naturally
298
- onChangeTrack?(
356
+ notifyTrackChange(
299
357
  getCurrentTrack()
300
358
  ?? TrackItem(
301
359
  id: "",
@@ -304,7 +362,8 @@ class TrackPlayerCore: NSObject {
304
362
  album: "",
305
363
  duration: 0,
306
364
  url: "",
307
- artwork: nil
365
+ artwork: nil,
366
+ extraPayload: nil
308
367
  ), .end)
309
368
 
310
369
  // Try to play next track
@@ -314,7 +373,7 @@ class TrackPlayerCore: NSObject {
314
373
  @objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
315
374
  if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
316
375
  print("❌ TrackPlayerCore: Playback failed - \(error)")
317
- onPlaybackStateChange?(.stopped, .error)
376
+ notifyPlaybackStateChange(.stopped, .error)
318
377
  }
319
378
  }
320
379
 
@@ -346,7 +405,7 @@ class TrackPlayerCore: NSObject {
346
405
  print("🎯 TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
347
406
 
348
407
  // Call onSeek callback immediately
349
- onSeek?(position, duration)
408
+ notifySeek(position, duration)
350
409
 
351
410
  // Mark that this was a manual seek
352
411
  isManuallySeeked = true
@@ -371,7 +430,7 @@ class TrackPlayerCore: NSObject {
371
430
  emitStateChange()
372
431
  } else if player.status == .failed {
373
432
  print("❌ TrackPlayerCore: Player failed")
374
- onPlaybackStateChange?(.stopped, .error)
433
+ notifyPlaybackStateChange(.stopped, .error)
375
434
  }
376
435
  } else if keyPath == "rate" {
377
436
  print("👀 TrackPlayerCore: Rate changed to: \(player.rate)")
@@ -437,12 +496,34 @@ class TrackPlayerCore: NSObject {
437
496
  // Setup KVO observers for current item
438
497
  setupCurrentItemObservers(item: currentItem)
439
498
 
440
- // Update track index
499
+ // Update track index and determine temporary type
441
500
  if let trackId = currentItem.trackId {
442
501
  print("🔍 TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
443
502
  print(" Current index BEFORE lookup: \(currentTrackIndex)")
444
503
 
445
- if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
504
+ // Update temporary type
505
+ currentTemporaryType = determineCurrentTemporaryType()
506
+ print(" 🎯 Track type: \(currentTemporaryType)")
507
+
508
+ // If it's a temporary track, don't update currentTrackIndex
509
+ if currentTemporaryType != .none {
510
+ // Find and emit the temporary track
511
+ var tempTrack: TrackItem? = nil
512
+ if currentTemporaryType == .playNext {
513
+ tempTrack = playNextStack.first(where: { $0.id == trackId })
514
+ } else if currentTemporaryType == .upNext {
515
+ tempTrack = upNextQueue.first(where: { $0.id == trackId })
516
+ }
517
+
518
+ if let track = tempTrack {
519
+ print(" 🎵 Temporary track: \(track.title) - \(track.artist)")
520
+ print(" 📢 Emitting onChangeTrack for temporary track")
521
+ notifyTrackChange(track, .skip)
522
+ mediaSessionManager?.onTrackChanged()
523
+ }
524
+ }
525
+ // It's an original playlist track
526
+ else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
446
527
  print(" ✅ Found track at index: \(index)")
447
528
  print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
448
529
 
@@ -456,7 +537,7 @@ class TrackPlayerCore: NSObject {
456
537
  // This prevents duplicate emissions
457
538
  if oldIndex != index {
458
539
  print(" 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
459
- onChangeTrack?(track, .skip)
540
+ notifyTrackChange(track, .skip)
460
541
  mediaSessionManager?.onTrackChanged()
461
542
  } else {
462
543
  print(" ⏭️ Skipping onChangeTrack emission (index unchanged)")
@@ -485,14 +566,16 @@ class TrackPlayerCore: NSObject {
485
566
  private func setupCurrentItemObservers(item: AVPlayerItem) {
486
567
  print("📱 TrackPlayerCore: Setting up item observers")
487
568
 
488
- // Observe status - recreate boundaries when ready
569
+ // Observe status - recreate boundaries when ready and update now playing info
489
570
  let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
490
571
  if item.status == .readyToPlay {
491
572
  print("✅ TrackPlayerCore: Item ready, setting up boundaries")
492
573
  self?.setupBoundaryTimeObserver()
574
+ // Update now playing info now that duration is available
575
+ self?.mediaSessionManager?.updateNowPlayingInfo()
493
576
  } else if item.status == .failed {
494
577
  print("❌ TrackPlayerCore: Item failed")
495
- self?.onPlaybackStateChange?(.stopped, .error)
578
+ self?.notifyPlaybackStateChange(.stopped, .error)
496
579
  }
497
580
  }
498
581
  currentItemObservers.append(statusObserver)
@@ -517,32 +600,42 @@ class TrackPlayerCore: NSObject {
517
600
  // MARK: - Playlist Management
518
601
 
519
602
  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")
603
+ if Thread.isMainThread {
604
+ loadPlaylistInternal(playlistId: playlistId)
605
+ } else {
606
+ DispatchQueue.main.sync { [weak self] in
607
+ self?.loadPlaylistInternal(playlistId: playlistId)
608
+ }
609
+ }
610
+ }
535
611
 
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")
612
+ private func loadPlaylistInternal(playlistId: String) {
613
+ print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
614
+ print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
615
+ print(" Playlist ID: \(playlistId)")
616
+
617
+ // Clear temporary tracks when loading new playlist
618
+ self.playNextStack.removeAll()
619
+ self.upNextQueue.removeAll()
620
+ self.currentTemporaryType = .none
621
+ print(" 🧹 Cleared temporary tracks")
622
+
623
+ let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
624
+ if let playlist = playlist {
625
+ print(" ✅ Found playlist: \(playlist.name)")
626
+ print(" 📋 Contains \(playlist.tracks.count) tracks:")
627
+ for (index, track) in playlist.tracks.enumerated() {
628
+ print(" [\(index + 1)] \(track.title) - \(track.artist)")
545
629
  }
630
+ print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
631
+
632
+ self.currentPlaylistId = playlistId
633
+ self.updatePlayerQueue(tracks: playlist.tracks)
634
+ // Emit initial state (paused/stopped before play)
635
+ self.emitStateChange()
636
+ } else {
637
+ print(" ❌ Playlist NOT FOUND")
638
+ print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
546
639
  }
547
640
  }
548
641
 
@@ -583,8 +676,8 @@ class TrackPlayerCore: NSObject {
583
676
  }
584
677
 
585
678
  print("🔔 TrackPlayerCore: Emitting state change: \(state)")
586
- print("🔔 TrackPlayerCore: Callback exists: \(onPlaybackStateChange != nil)")
587
- onPlaybackStateChange?(state, reason)
679
+ print("🔔 TrackPlayerCore: Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
680
+ notifyPlaybackStateChange(state, reason)
588
681
  mediaSessionManager?.onPlaybackStateChanged()
589
682
  }
590
683
 
@@ -594,9 +687,40 @@ class TrackPlayerCore: NSObject {
594
687
  private func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false)
595
688
  -> AVPlayerItem?
596
689
  {
597
- guard let url = URL(string: track.url) else {
598
- print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(track.url)")
599
- return nil
690
+ // Get effective URL - uses local path if downloaded, otherwise remote URL
691
+ let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
692
+
693
+ // Create URL - use fileURLWithPath for local files, URL(string:) for remote
694
+ let url: URL
695
+ let isLocal = effectiveUrlString.hasPrefix("/")
696
+
697
+ if isLocal {
698
+ // Local file - use fileURLWithPath
699
+ print("📥 TrackPlayerCore: Using DOWNLOADED version for \(track.title)")
700
+ print(" Local path: \(effectiveUrlString)")
701
+
702
+ // Verify file exists
703
+ if FileManager.default.fileExists(atPath: effectiveUrlString) {
704
+ url = URL(fileURLWithPath: effectiveUrlString)
705
+ print(" File URL: \(url.absoluteString)")
706
+ print(" ✅ File verified to exist")
707
+ } else {
708
+ print(" ❌ Downloaded file does NOT exist at path!")
709
+ print(" Falling back to remote URL: \(track.url)")
710
+ guard let remoteUrl = URL(string: track.url) else {
711
+ print("❌ TrackPlayerCore: Invalid remote URL: \(track.url)")
712
+ return nil
713
+ }
714
+ url = remoteUrl
715
+ }
716
+ } else {
717
+ // Remote URL
718
+ guard let remoteUrl = URL(string: effectiveUrlString) else {
719
+ print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(effectiveUrlString)")
720
+ return nil
721
+ }
722
+ url = remoteUrl
723
+ print("🌐 TrackPlayerCore: Using REMOTE version for \(track.title)")
600
724
  }
601
725
 
602
726
  // Check if we have a preloaded asset for this track
@@ -625,6 +749,13 @@ class TrackPlayerCore: NSObject {
625
749
  // Store track ID for later reference
626
750
  item.trackId = track.id
627
751
 
752
+ // Apply equalizer audio mix to the player item
753
+ // This enables real-time EQ processing via MTAudioProcessingTap
754
+ // Apply equalizer audio mix to the player item
755
+ // This enables real-time EQ processing via MTAudioProcessingTap
756
+ EqualizerCore.shared.applyAudioMix(to: item)
757
+ print("🎛️ TrackPlayerCore: Requesting EQ audio mix application for \(track.title)")
758
+
628
759
  // If this is a preload request, start loading asset keys asynchronously
629
760
  if isPreload {
630
761
  asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
@@ -654,10 +785,13 @@ class TrackPlayerCore: NSObject {
654
785
  preloadQueue.async { [weak self] in
655
786
  guard let self = self else { return }
656
787
 
657
- let endIndex = min(startIndex + Constants.gaplessPreloadCount, self.currentTracks.count)
788
+ // Capture currentTracks to avoid race condition with main thread
789
+ let tracks = self.currentTracks
790
+ let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
658
791
 
659
792
  for i in startIndex..<endIndex {
660
- let track = self.currentTracks[i]
793
+ guard i < tracks.count else { break }
794
+ let track = tracks[i]
661
795
 
662
796
  // Skip if already preloaded
663
797
  if self.preloadedAssets[track.id] != nil {
@@ -717,6 +851,141 @@ class TrackPlayerCore: NSObject {
717
851
  }
718
852
  }
719
853
 
854
+ // MARK: - Listener Registration
855
+
856
+ func addOnChangeTrackListener(
857
+ owner: AnyObject, _ listener: @escaping (TrackItem, Reason?) -> Void
858
+ ) {
859
+ let box = WeakCallbackBox(owner: owner, callback: listener)
860
+ listenersQueue.async(flags: .barrier) { [weak self] in
861
+ self?.onChangeTrackListeners.append(box)
862
+ print(
863
+ "🎯 TrackPlayerCore: Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))"
864
+ )
865
+ }
866
+ }
867
+
868
+ func addOnPlaybackStateChangeListener(
869
+ owner: AnyObject,
870
+ _ listener: @escaping (TrackPlayerState, Reason?) -> Void
871
+ ) {
872
+ let box = WeakCallbackBox(owner: owner, callback: listener)
873
+ listenersQueue.async(flags: .barrier) { [weak self] in
874
+ self?.onPlaybackStateChangeListeners.append(box)
875
+ print(
876
+ "🎯 TrackPlayerCore: Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))"
877
+ )
878
+ }
879
+ }
880
+
881
+ func addOnSeekListener(owner: AnyObject, _ listener: @escaping (Double, Double) -> Void) {
882
+ let box = WeakCallbackBox(owner: owner, callback: listener)
883
+ listenersQueue.async(flags: .barrier) { [weak self] in
884
+ self?.onSeekListeners.append(box)
885
+ print("🎯 TrackPlayerCore: Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
886
+ }
887
+ }
888
+
889
+ func addOnPlaybackProgressChangeListener(
890
+ owner: AnyObject,
891
+ _ listener: @escaping (Double, Double, Bool?) -> Void
892
+ ) {
893
+ let box = WeakCallbackBox(owner: owner, callback: listener)
894
+ listenersQueue.async(flags: .barrier) { [weak self] in
895
+ self?.onPlaybackProgressChangeListeners.append(box)
896
+ print(
897
+ "🎯 TrackPlayerCore: Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))"
898
+ )
899
+ }
900
+ }
901
+
902
+ // MARK: - Listener Notification Helpers
903
+
904
+ private func notifyTrackChange(_ track: TrackItem, _ reason: Reason?) {
905
+ listenersQueue.async(flags: .barrier) { [weak self] in
906
+ guard let self = self else { return }
907
+
908
+ // Remove dead listeners
909
+ self.onChangeTrackListeners.removeAll { !$0.isAlive }
910
+
911
+ // Get live callbacks
912
+ let liveCallbacks = self.onChangeTrackListeners.compactMap {
913
+ $0.isAlive ? $0.callback : nil
914
+ }
915
+
916
+ // Call on main thread
917
+ if !liveCallbacks.isEmpty {
918
+ DispatchQueue.main.async {
919
+ for callback in liveCallbacks {
920
+ callback(track, reason)
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
926
+
927
+ private func notifyPlaybackStateChange(_ state: TrackPlayerState, _ reason: Reason?) {
928
+ listenersQueue.async(flags: .barrier) { [weak self] in
929
+ guard let self = self else { return }
930
+
931
+ self.onPlaybackStateChangeListeners.removeAll { !$0.isAlive }
932
+
933
+ let liveCallbacks = self.onPlaybackStateChangeListeners.compactMap {
934
+ $0.isAlive ? $0.callback : nil
935
+ }
936
+
937
+ if !liveCallbacks.isEmpty {
938
+ DispatchQueue.main.async {
939
+ for callback in liveCallbacks {
940
+ callback(state, reason)
941
+ }
942
+ }
943
+ }
944
+ }
945
+ }
946
+
947
+ private func notifySeek(_ position: Double, _ duration: Double) {
948
+ listenersQueue.async(flags: .barrier) { [weak self] in
949
+ guard let self = self else { return }
950
+
951
+ self.onSeekListeners.removeAll { !$0.isAlive }
952
+
953
+ let liveCallbacks = self.onSeekListeners.compactMap {
954
+ $0.isAlive ? $0.callback : nil
955
+ }
956
+
957
+ if !liveCallbacks.isEmpty {
958
+ DispatchQueue.main.async {
959
+ for callback in liveCallbacks {
960
+ callback(position, duration)
961
+ }
962
+ }
963
+ }
964
+ }
965
+ }
966
+
967
+ private func notifyPlaybackProgress(_ position: Double, _ duration: Double, _ isPlaying: Bool?) {
968
+ listenersQueue.async(flags: .barrier) { [weak self] in
969
+ guard let self = self else { return }
970
+
971
+ self.onPlaybackProgressChangeListeners.removeAll { !$0.isAlive }
972
+
973
+ let liveCallbacks = self.onPlaybackProgressChangeListeners.compactMap {
974
+ $0.isAlive ? $0.callback : nil
975
+ }
976
+
977
+ if !liveCallbacks.isEmpty {
978
+ DispatchQueue.main.async {
979
+ for callback in liveCallbacks {
980
+ callback(position, duration, isPlaying)
981
+ }
982
+ }
983
+ }
984
+ }
985
+ }
986
+
987
+ // MARK: - State Management
988
+
720
989
  // MARK: - Queue Management
721
990
 
722
991
  private func updatePlayerQueue(tracks: [TrackItem]) {
@@ -724,9 +993,17 @@ class TrackPlayerCore: NSObject {
724
993
  print("📋 TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
725
994
  print(String(repeating: "=", count: Constants.separatorLineLength))
726
995
 
727
- // Print the full playlist being fed
996
+ // Print the full playlist being fed and check download status
728
997
  for (index, track) in tracks.enumerated() {
729
- print(" [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id))")
998
+ let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
999
+ let downloadStatus = isDownloaded ? "📥 DOWNLOADED" : "🌐 REMOTE"
1000
+ print(
1001
+ " [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
1002
+ if isDownloaded {
1003
+ if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
1004
+ print(" Local path: \(localPath)")
1005
+ }
1006
+ }
730
1007
  }
731
1008
  print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
732
1009
 
@@ -808,8 +1085,8 @@ class TrackPlayerCore: NSObject {
808
1085
  // Notify track change
809
1086
  if let firstTrack = tracks.first {
810
1087
  print("🎵 TrackPlayerCore: Emitting track change: \(firstTrack.title)")
811
- print("🎵 TrackPlayerCore: onChangeTrack callback exists: \(onChangeTrack != nil)")
812
- onChangeTrack?(firstTrack, nil)
1088
+ print("🎵 TrackPlayerCore: onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
1089
+ notifyTrackChange(firstTrack, nil)
813
1090
  mediaSessionManager?.onTrackChanged()
814
1091
  }
815
1092
 
@@ -820,226 +1097,324 @@ class TrackPlayerCore: NSObject {
820
1097
  }
821
1098
 
822
1099
  func getCurrentTrack() -> TrackItem? {
1100
+ // If playing a temporary track, return that
1101
+ if currentTemporaryType != .none,
1102
+ let currentItem = player?.currentItem,
1103
+ let trackId = currentItem.trackId
1104
+ {
1105
+ if currentTemporaryType == .playNext {
1106
+ return playNextStack.first(where: { $0.id == trackId })
1107
+ } else if currentTemporaryType == .upNext {
1108
+ return upNextQueue.first(where: { $0.id == trackId })
1109
+ }
1110
+ }
1111
+
1112
+ // Otherwise return from original playlist
823
1113
  guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else {
824
1114
  return nil
825
1115
  }
826
1116
  return currentTracks[currentTrackIndex]
827
1117
  }
828
1118
 
1119
+ func getActualQueue() -> [TrackItem] {
1120
+ // Called from Promise.async background thread
1121
+ // Schedule on main thread and wait for result
1122
+ if Thread.isMainThread {
1123
+ return getActualQueueInternal()
1124
+ } else {
1125
+ var queue: [TrackItem] = []
1126
+ DispatchQueue.main.sync { [weak self] in
1127
+ queue = self?.getActualQueueInternal() ?? []
1128
+ }
1129
+ return queue
1130
+ }
1131
+ }
1132
+
1133
+ private func getActualQueueInternal() -> [TrackItem] {
1134
+ var queue: [TrackItem] = []
1135
+
1136
+ // Add tracks before current (original playlist)
1137
+ if currentTrackIndex > 0 {
1138
+ queue.append(contentsOf: Array(currentTracks[0..<currentTrackIndex]))
1139
+ }
1140
+
1141
+ // Add current track
1142
+ if let current = getCurrentTrack() {
1143
+ queue.append(current)
1144
+ }
1145
+
1146
+ // Add playNext stack (LIFO - most recently added plays first)
1147
+ // Stack is already in correct order since we insert at position 0
1148
+ queue.append(contentsOf: playNextStack)
1149
+
1150
+ // Add upNext queue (in order, FIFO)
1151
+ queue.append(contentsOf: upNextQueue)
1152
+
1153
+ // Add remaining original tracks
1154
+ if currentTrackIndex + 1 < currentTracks.count {
1155
+ queue.append(contentsOf: Array(currentTracks[(currentTrackIndex + 1)...]))
1156
+ }
1157
+
1158
+ return queue
1159
+ }
1160
+
829
1161
  func play() {
830
1162
  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()
1163
+ if Thread.isMainThread {
1164
+ playInternal()
1165
+ } else {
1166
+ DispatchQueue.main.sync { [weak self] in
1167
+ self?.playInternal()
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ private func playInternal() {
1173
+ print("▶️ TrackPlayerCore: Calling player.play()")
1174
+ if let player = self.player {
1175
+ print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
1176
+ if let currentItem = player.currentItem {
1177
+ print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
1178
+ if let error = currentItem.error {
1179
+ print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
848
1180
  }
849
- } else {
850
- print("❌ TrackPlayerCore: No player available")
851
1181
  }
1182
+ player.play()
1183
+ // Emit state change immediately for responsive UI
1184
+ // KVO will also fire, but this ensures immediate feedback
1185
+ DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
1186
+ [weak self] in
1187
+ self?.emitStateChange()
1188
+ }
1189
+ } else {
1190
+ print("❌ TrackPlayerCore: No player available")
852
1191
  }
853
1192
  }
854
1193
 
855
1194
  func pause() {
856
1195
  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()
1196
+ if Thread.isMainThread {
1197
+ pauseInternal()
1198
+ } else {
1199
+ DispatchQueue.main.sync { [weak self] in
1200
+ self?.pauseInternal()
863
1201
  }
864
1202
  }
865
1203
  }
866
1204
 
867
- func playSong(songId: String, fromPlaylist: String?) {
868
- print(
869
- "🎵 TrackPlayerCore: playSong() called - songId: \(songId), fromPlaylist: \(fromPlaylist ?? "nil")"
870
- )
1205
+ private func pauseInternal() {
1206
+ self.player?.pause()
1207
+ // Emit state change immediately for responsive UI
1208
+ DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
1209
+ self?.emitStateChange()
1210
+ }
1211
+ }
871
1212
 
1213
+ func playSong(songId: String, fromPlaylist: String?) {
872
1214
  DispatchQueue.main.async { [weak self] in
873
- guard let self = self else { return }
874
-
875
- var targetPlaylistId: String?
876
- var songIndex: Int = -1
1215
+ self?.playSongInternal(songId: songId, fromPlaylist: fromPlaylist)
1216
+ }
1217
+ }
877
1218
 
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
- }
1219
+ private func playSongInternal(songId: String, fromPlaylist: String?) {
1220
+ // Clear temporary tracks when directly playing a song
1221
+ self.playNextStack.removeAll()
1222
+ self.upNextQueue.removeAll()
1223
+ self.currentTemporaryType = .none
1224
+ print(" 🧹 Cleared temporary tracks")
1225
+
1226
+ var targetPlaylistId: String?
1227
+ var songIndex: Int = -1
1228
+
1229
+ // Case 1: If fromPlaylist is provided, use that playlist
1230
+ if let playlistId = fromPlaylist {
1231
+ print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
1232
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1233
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1234
+ targetPlaylistId = playlistId
1235
+ songIndex = index
1236
+ print("✅ Found song at index \(index) in playlist \(playlistId)")
890
1237
  } else {
891
- print("⚠️ Playlist \(playlistId) not found")
1238
+ print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
892
1239
  return
893
1240
  }
1241
+ } else {
1242
+ print("⚠️ Playlist \(playlistId) not found")
1243
+ return
894
1244
  }
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
- }
1245
+ }
1246
+ // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
1247
+ else {
1248
+ print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
1249
+
1250
+ // Check if song exists in currently loaded playlist
1251
+ if let currentId = self.currentPlaylistId,
1252
+ let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
1253
+ {
1254
+ if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
1255
+ targetPlaylistId = currentId
1256
+ songIndex = index
1257
+ print("✅ Found song at index \(index) in current playlist \(currentId)")
908
1258
  }
1259
+ }
909
1260
 
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()
1261
+ // If not found in current playlist, search in all playlists
1262
+ if songIndex == -1 {
1263
+ print("🔍 Song not found in current playlist, searching all playlists...")
1264
+ let allPlaylists = self.playlistManager.getAllPlaylists()
914
1265
 
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
- }
1266
+ for playlist in allPlaylists {
1267
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1268
+ targetPlaylistId = playlist.id
1269
+ songIndex = index
1270
+ print("✅ Found song at index \(index) in playlist \(playlist.id)")
1271
+ break
922
1272
  }
1273
+ }
923
1274
 
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
- }
1275
+ // If still not found, just use the first playlist if available
1276
+ if songIndex == -1 && !allPlaylists.isEmpty {
1277
+ targetPlaylistId = allPlaylists[0].id
1278
+ songIndex = 0
1279
+ print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
930
1280
  }
931
1281
  }
1282
+ }
932
1283
 
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
- }
1284
+ // Now play the song
1285
+ guard let playlistId = targetPlaylistId, songIndex >= 0 else {
1286
+ print("❌ Could not determine playlist or song index")
1287
+ return
1288
+ }
938
1289
 
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
- }
1290
+ // Load playlist if it's different from current
1291
+ if self.currentPlaylistId != playlistId {
1292
+ print("🔄 Loading new playlist: \(playlistId)")
1293
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1294
+ self.currentPlaylistId = playlistId
1295
+ self.updatePlayerQueue(tracks: playlist.tracks)
946
1296
  }
947
-
948
- // Play from the found index
949
- print("▶️ Playing from index: \(songIndex)")
950
- self.playFromIndex(index: songIndex)
951
1297
  }
1298
+
1299
+ // Play from the found index
1300
+ print("▶️ Playing from index: \(songIndex)")
1301
+ self.playFromIndex(index: songIndex)
952
1302
  }
953
1303
 
954
1304
  func skipToNext() {
955
- DispatchQueue.main.async { [weak self] in
956
- guard let self = self,
957
- let queuePlayer = self.player
958
- else {
959
- return
1305
+ if Thread.isMainThread {
1306
+ skipToNextInternal()
1307
+ } else {
1308
+ DispatchQueue.main.sync { [weak self] in
1309
+ self?.skipToNextInternal()
960
1310
  }
1311
+ }
1312
+ }
961
1313
 
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)")
1314
+ private func skipToNextInternal() {
1315
+ guard let queuePlayer = self.player else { return }
967
1316
 
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))")
971
- }
1317
+ print("\n⏭️ TrackPlayerCore: SKIP TO NEXT")
1318
+ print(" BEFORE:")
1319
+ print(" currentTrackIndex: \(self.currentTrackIndex)")
1320
+ print(" Total tracks in currentTracks: \(self.currentTracks.count)")
1321
+ print(" Items in player queue: \(queuePlayer.items().count)")
1322
+
1323
+ if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
1324
+ if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1325
+ print(" Currently playing: \(track.title) (ID: \(track.id))")
972
1326
  }
1327
+ }
973
1328
 
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()
1329
+ // Check if there are more items in the queue
1330
+ if self.currentTrackIndex + 1 < self.currentTracks.count {
1331
+ print(" 🔄 Calling advanceToNextItem()...")
1332
+ queuePlayer.advanceToNextItem()
978
1333
 
979
- // NOTE: Don't manually update currentTrackIndex here!
980
- // The KVO observer (currentItemDidChange) will update it automatically
1334
+ // NOTE: Don't manually update currentTrackIndex here!
1335
+ // The KVO observer (currentItemDidChange) will update it automatically
981
1336
 
982
- print(" AFTER advanceToNextItem():")
983
- print(" Items in player queue: \(queuePlayer.items().count)")
1337
+ print(" AFTER advanceToNextItem():")
1338
+ print(" Items in player queue: \(queuePlayer.items().count)")
984
1339
 
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))")
988
- }
1340
+ if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
1341
+ if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1342
+ print(" New current item: \(track.title) (ID: \(track.id))")
989
1343
  }
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
1344
  }
1345
+
1346
+ print(" ⏳ Waiting for KVO observer to update index...")
1347
+ } else {
1348
+ print(" ⚠️ No more tracks in playlist")
1349
+ // At end of playlist - stop or loop
1350
+ queuePlayer.pause()
1351
+ self.notifyPlaybackStateChange(.stopped, .end)
998
1352
  }
999
1353
  }
1000
1354
 
1001
1355
  func skipToPrevious() {
1002
- DispatchQueue.main.async { [weak self] in
1003
- guard let self = self,
1004
- let queuePlayer = self.player
1005
- else {
1006
- return
1356
+ if Thread.isMainThread {
1357
+ skipToPreviousInternal()
1358
+ } else {
1359
+ DispatchQueue.main.sync { [weak self] in
1360
+ self?.skipToPreviousInternal()
1007
1361
  }
1362
+ }
1363
+ }
1008
1364
 
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)
1028
- }
1365
+ private func skipToPreviousInternal() {
1366
+ guard let queuePlayer = self.player else { return }
1367
+
1368
+ print("\n⏮️ TrackPlayerCore: SKIP TO PREVIOUS")
1369
+ print(" Current index: \(self.currentTrackIndex)")
1370
+ print(" Temporary type: \(self.currentTemporaryType)")
1371
+ print(" Current time: \(queuePlayer.currentTime().seconds)s")
1372
+
1373
+ let currentTime = queuePlayer.currentTime()
1374
+ if currentTime.seconds > Constants.skipToPreviousThreshold {
1375
+ // If more than threshold seconds in, restart current track
1376
+ print(
1377
+ " 🔄 More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
1378
+ queuePlayer.seek(to: .zero)
1379
+ } else if self.currentTemporaryType != .none {
1380
+ // Playing temporary track - just restart it (temps are not navigable backwards)
1381
+ print(" 🔄 Playing temporary track - restarting it (temps not navigable backwards)")
1382
+ queuePlayer.seek(to: .zero)
1383
+ } else if self.currentTrackIndex > 0 {
1384
+ // Go to previous track in original playlist
1385
+ let previousIndex = self.currentTrackIndex - 1
1386
+ print(" ⏮️ Going to previous track at index \(previousIndex)")
1387
+ self.playFromIndex(index: previousIndex)
1388
+ } else {
1389
+ // Already at first track, restart it
1390
+ print(" 🔄 Already at first track, restarting it")
1391
+ queuePlayer.seek(to: .zero)
1029
1392
  }
1030
1393
  }
1031
1394
 
1032
1395
  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
- }
1396
+ if Thread.isMainThread {
1397
+ seekInternal(position: position)
1398
+ } else {
1399
+ DispatchQueue.main.sync { [weak self] in
1400
+ self?.seekInternal(position: position)
1401
+ }
1402
+ }
1403
+ }
1404
+
1405
+ private func seekInternal(position: Double) {
1406
+ guard let player = self.player else { return }
1407
+
1408
+ self.isManuallySeeked = true
1409
+ let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
1410
+ player.seek(to: time) { [weak self] completed in
1411
+ // Always update now playing info to restore playback rate after seek
1412
+ // This ensures the scrubber animation resumes correctly
1413
+ self?.mediaSessionManager?.updateNowPlayingInfo()
1414
+
1415
+ if completed {
1416
+ let duration = player.currentItem?.duration.seconds ?? 0.0
1417
+ self?.notifySeek(position, duration)
1043
1418
  }
1044
1419
  }
1045
1420
  }
@@ -1048,13 +1423,41 @@ class TrackPlayerCore: NSObject {
1048
1423
 
1049
1424
  func setRepeatMode(mode: RepeatMode) -> Bool {
1050
1425
  print("🔁 TrackPlayerCore: setRepeatMode called with mode: \(mode)")
1051
- self.repeatMode = mode
1426
+ if Thread.isMainThread {
1427
+ self.repeatMode = mode
1428
+ } else {
1429
+ DispatchQueue.main.sync { [weak self] in
1430
+ self?.repeatMode = mode
1431
+ }
1432
+ }
1052
1433
  return true
1053
1434
  }
1054
1435
 
1055
- // MARK: - State Management
1056
-
1057
1436
  func getState() -> PlayerState {
1437
+ // Called from Promise.async background thread
1438
+ // Schedule on main thread and wait for result
1439
+ if Thread.isMainThread {
1440
+ return getStateInternal()
1441
+ } else {
1442
+ var state: PlayerState!
1443
+ DispatchQueue.main.sync { [weak self] in
1444
+ state =
1445
+ self?.getStateInternal()
1446
+ ?? PlayerState(
1447
+ currentTrack: nil,
1448
+ currentPosition: 0.0,
1449
+ totalDuration: 0.0,
1450
+ currentState: .stopped,
1451
+ currentPlaylistId: nil,
1452
+ currentIndex: -1.0,
1453
+ currentPlayingType: .notPlaying
1454
+ )
1455
+ }
1456
+ return state
1457
+ }
1458
+ }
1459
+
1460
+ private func getStateInternal() -> PlayerState {
1058
1461
  guard let player = player else {
1059
1462
  return PlayerState(
1060
1463
  currentTrack: nil,
@@ -1062,7 +1465,8 @@ class TrackPlayerCore: NSObject {
1062
1465
  totalDuration: 0.0,
1063
1466
  currentState: .stopped,
1064
1467
  currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
1065
- currentIndex: -1.0
1468
+ currentIndex: -1.0,
1469
+ currentPlayingType: .notPlaying
1066
1470
  )
1067
1471
  }
1068
1472
 
@@ -1082,13 +1486,29 @@ class TrackPlayerCore: NSObject {
1082
1486
  // Get current index
1083
1487
  let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
1084
1488
 
1489
+ // Map internal temporary type to CurrentPlayingType
1490
+ let currentPlayingType: CurrentPlayingType
1491
+ if currentTrack == nil {
1492
+ currentPlayingType = .notPlaying
1493
+ } else {
1494
+ switch currentTemporaryType {
1495
+ case .none:
1496
+ currentPlayingType = .playlist
1497
+ case .playNext:
1498
+ currentPlayingType = .playNext
1499
+ case .upNext:
1500
+ currentPlayingType = .upNext
1501
+ }
1502
+ }
1503
+
1085
1504
  return PlayerState(
1086
1505
  currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
1087
1506
  currentPosition: currentPosition,
1088
1507
  totalDuration: totalDuration,
1089
1508
  currentState: currentState,
1090
1509
  currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
1091
- currentIndex: currentIndex
1510
+ currentIndex: currentIndex,
1511
+ currentPlayingType: currentPlayingType
1092
1512
  )
1093
1513
  }
1094
1514
 
@@ -1133,72 +1553,374 @@ class TrackPlayerCore: NSObject {
1133
1553
  }
1134
1554
 
1135
1555
  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
1556
+ if Thread.isMainThread {
1557
+ playFromIndexInternal(index: index)
1558
+ } else {
1559
+ DispatchQueue.main.async { [weak self] in
1560
+ self?.playFromIndexInternal(index: index)
1142
1561
  }
1562
+ }
1563
+ }
1143
1564
 
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)")
1565
+ // MARK: - Skip to Index in Actual Queue
1147
1566
 
1148
- // Store the full playlist
1149
- let fullPlaylist = self.currentTracks
1567
+ func skipToIndex(index: Int) -> Bool {
1568
+ if Thread.isMainThread {
1569
+ return skipToIndexInternal(index: index)
1570
+ } else {
1571
+ var result = false
1572
+ DispatchQueue.main.sync { [weak self] in
1573
+ result = self?.skipToIndexInternal(index: index) ?? false
1574
+ }
1575
+ return result
1576
+ }
1577
+ }
1150
1578
 
1151
- // Update currentTrackIndex BEFORE updating queue
1152
- self.currentTrackIndex = index
1579
+ private func skipToIndexInternal(index: Int) -> Bool {
1580
+ print("\n🎯 TrackPlayerCore: SKIP TO INDEX \(index)")
1153
1581
 
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
- )
1582
+ // Get actual queue to validate index and determine position
1583
+ let actualQueue = getActualQueueInternal()
1584
+ let totalQueueSize = actualQueue.count
1585
+
1586
+ // Validate index
1587
+ guard index >= 0 && index < totalQueueSize else {
1588
+ print(" ❌ Invalid index \(index), queue size is \(totalQueueSize)")
1589
+ return false
1590
+ }
1160
1591
 
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)
1592
+ // Calculate queue section boundaries
1593
+ // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
1594
+ let currentPos = currentTrackIndex
1595
+ let playNextStart = currentPos + 1
1596
+ let playNextEnd = playNextStart + playNextStack.count
1597
+ let upNextStart = playNextEnd
1598
+ let upNextEnd = upNextStart + upNextQueue.count
1599
+ let originalRemainingStart = upNextEnd
1600
+
1601
+ print(" Queue structure:")
1602
+ print(" currentPos: \(currentPos)")
1603
+ print(" playNextStart: \(playNextStart), playNextEnd: \(playNextEnd)")
1604
+ print(" upNextStart: \(upNextStart), upNextEnd: \(upNextEnd)")
1605
+ print(" originalRemainingStart: \(originalRemainingStart)")
1606
+ print(" totalQueueSize: \(totalQueueSize)")
1607
+
1608
+ // Case 1: Target is before current - use playFromIndex on original
1609
+ if index < currentPos {
1610
+ print(" 📍 Target is before current, jumping to original playlist index \(index)")
1611
+ playFromIndexInternal(index: index)
1612
+ return true
1613
+ }
1614
+
1615
+ // Case 2: Target is current - seek to beginning
1616
+ if index == currentPos {
1617
+ print(" 📍 Target is current track, seeking to beginning")
1618
+ player?.seek(to: .zero)
1619
+ return true
1620
+ }
1621
+
1622
+ // Case 3: Target is in playNext section
1623
+ if index >= playNextStart && index < playNextEnd {
1624
+ let playNextIndex = index - playNextStart
1625
+ print(" 📍 Target is in playNext section at position \(playNextIndex)")
1626
+
1627
+ // Remove tracks before the target from playNext (they're being skipped)
1628
+ if playNextIndex > 0 {
1629
+ playNextStack.removeFirst(playNextIndex)
1630
+ print(" Removed \(playNextIndex) tracks from playNext stack")
1166
1631
  }
1167
1632
 
1168
- guard let player = self.player, !items.isEmpty else {
1169
- print("❌ No player or no items to play")
1170
- return
1633
+ // Rebuild queue and advance
1634
+ rebuildAVQueueFromCurrentPosition()
1635
+ player?.advanceToNextItem()
1636
+ return true
1637
+ }
1638
+
1639
+ // Case 4: Target is in upNext section
1640
+ if index >= upNextStart && index < upNextEnd {
1641
+ let upNextIndex = index - upNextStart
1642
+ print(" 📍 Target is in upNext section at position \(upNextIndex)")
1643
+
1644
+ // Clear all playNext tracks (they're being skipped)
1645
+ playNextStack.removeAll()
1646
+ print(" Cleared all playNext tracks")
1647
+
1648
+ // Remove tracks before target from upNext
1649
+ if upNextIndex > 0 {
1650
+ upNextQueue.removeFirst(upNextIndex)
1651
+ print(" Removed \(upNextIndex) tracks from upNext queue")
1171
1652
  }
1172
1653
 
1173
- // Remove old boundary observer
1174
- if let boundaryObserver = self.boundaryTimeObserver {
1175
- player.removeTimeObserver(boundaryObserver)
1176
- self.boundaryTimeObserver = nil
1654
+ // Rebuild queue and advance
1655
+ rebuildAVQueueFromCurrentPosition()
1656
+ player?.advanceToNextItem()
1657
+ return true
1658
+ }
1659
+
1660
+ // Case 5: Target is in remaining original tracks
1661
+ if index >= originalRemainingStart {
1662
+ // Get the target track directly from actualQueue
1663
+ let targetTrack = actualQueue[index]
1664
+
1665
+ print(" 📍 Case 5: Target is in remaining original tracks")
1666
+ print(" targetTrack.id: \(targetTrack.id)")
1667
+ print(" currentTracks.count: \(currentTracks.count)")
1668
+ print(" currentTracks IDs: \(currentTracks.map { $0.id })")
1669
+
1670
+ // Find this track's index in the original playlist
1671
+ guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
1672
+ print(" ❌ Could not find track \(targetTrack.id) in original playlist")
1673
+ print(" Available tracks: \(currentTracks.map { $0.id })")
1674
+ return false
1177
1675
  }
1178
1676
 
1179
- // Clear and rebuild queue
1180
- player.removeAllItems()
1181
- var lastItem: AVPlayerItem? = nil
1182
- for item in items {
1677
+ print(" originalIndex found: \(originalIndex)")
1678
+
1679
+ // Clear all temporary tracks (they're being skipped)
1680
+ playNextStack.removeAll()
1681
+ upNextQueue.removeAll()
1682
+ currentTemporaryType = .none
1683
+ print(" Cleared all temporary tracks")
1684
+
1685
+ // Play from the original playlist index
1686
+ let success = playFromIndexInternalWithResult(index: originalIndex)
1687
+ return success
1688
+ }
1689
+
1690
+ print(" ❌ Unexpected case, index \(index) not handled")
1691
+ return false
1692
+ }
1693
+
1694
+ private func playFromIndexInternal(index: Int) {
1695
+ _ = playFromIndexInternalWithResult(index: index)
1696
+ }
1697
+
1698
+ private func playFromIndexInternalWithResult(index: Int) -> Bool {
1699
+ guard index >= 0 && index < self.currentTracks.count else {
1700
+ print(
1701
+ "❌ TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)"
1702
+ )
1703
+ return false
1704
+ }
1705
+
1706
+ print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
1707
+ print(" Total tracks in playlist: \(self.currentTracks.count)")
1708
+ print(" Current index: \(self.currentTrackIndex), target index: \(index)")
1709
+
1710
+ // Clear temporary tracks when jumping to specific index
1711
+ self.playNextStack.removeAll()
1712
+ self.upNextQueue.removeAll()
1713
+ self.currentTemporaryType = .none
1714
+ print(" 🧹 Cleared temporary tracks")
1715
+
1716
+ // Store the full playlist
1717
+ let fullPlaylist = self.currentTracks
1718
+
1719
+ // Update currentTrackIndex BEFORE updating queue
1720
+ self.currentTrackIndex = index
1721
+
1722
+ // Recreate the queue starting from the target index
1723
+ // This ensures all remaining tracks are in the queue
1724
+ let tracksToPlay = Array(fullPlaylist[index...])
1725
+ print(
1726
+ " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
1727
+ )
1728
+
1729
+ // Create gapless-optimized player items
1730
+ let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
1731
+ // First few items get preload treatment for faster playback
1732
+ let isPreload = offset < Constants.gaplessPreloadCount
1733
+ return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
1734
+ }
1735
+
1736
+ guard let player = self.player, !items.isEmpty else {
1737
+ print("❌ No player or no items to play")
1738
+ return false
1739
+ }
1740
+
1741
+ // Remove old boundary observer
1742
+ if let boundaryObserver = self.boundaryTimeObserver {
1743
+ player.removeTimeObserver(boundaryObserver)
1744
+ self.boundaryTimeObserver = nil
1745
+ }
1746
+
1747
+ // Clear and rebuild queue
1748
+ player.removeAllItems()
1749
+ var lastItem: AVPlayerItem? = nil
1750
+ for item in items {
1751
+ player.insert(item, after: lastItem)
1752
+ lastItem = item
1753
+ }
1754
+
1755
+ // Restore the full playlist reference (don't slice it!)
1756
+ self.currentTracks = fullPlaylist
1757
+
1758
+ print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1759
+ if let track = self.getCurrentTrack() {
1760
+ print(" 🎵 Playing: \(track.title)")
1761
+ notifyTrackChange(track, .skip)
1762
+ self.mediaSessionManager?.onTrackChanged()
1763
+ }
1764
+
1765
+ // Start preloading upcoming tracks for gapless playback
1766
+ self.preloadUpcomingTracks(from: index + 1)
1767
+
1768
+ player.play()
1769
+ return true
1770
+ }
1771
+
1772
+ // MARK: - Temporary Track Management
1773
+
1774
+ /**
1775
+ * Add a track to the up-next queue (FIFO - first added plays first)
1776
+ * Track will be inserted after currently playing track and any playNext tracks
1777
+ */
1778
+ func addToUpNext(trackId: String) {
1779
+ DispatchQueue.main.async { [weak self] in
1780
+ self?.addToUpNextInternal(trackId: trackId)
1781
+ }
1782
+ }
1783
+
1784
+ private func addToUpNextInternal(trackId: String) {
1785
+ print("📋 TrackPlayerCore: addToUpNext(\(trackId))")
1786
+
1787
+ // Find the track from current playlist or all playlists
1788
+ guard let track = self.findTrackById(trackId) else {
1789
+ print("❌ TrackPlayerCore: Track \(trackId) not found")
1790
+ return
1791
+ }
1792
+
1793
+ // Add to end of upNext queue (FIFO)
1794
+ self.upNextQueue.append(track)
1795
+ print(" ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
1796
+
1797
+ // Rebuild the player queue if actively playing
1798
+ if self.player?.currentItem != nil {
1799
+ self.rebuildAVQueueFromCurrentPosition()
1800
+ }
1801
+ }
1802
+
1803
+ /**
1804
+ * Add a track to play next (LIFO - last added plays first)
1805
+ * Track will be inserted immediately after currently playing track
1806
+ */
1807
+ func playNext(trackId: String) {
1808
+ DispatchQueue.main.async { [weak self] in
1809
+ self?.playNextInternal(trackId: trackId)
1810
+ }
1811
+ }
1812
+
1813
+ private func playNextInternal(trackId: String) {
1814
+ print("⏭️ TrackPlayerCore: playNext(\(trackId))")
1815
+
1816
+ // Find the track from current playlist or all playlists
1817
+ guard let track = self.findTrackById(trackId) else {
1818
+ print("❌ TrackPlayerCore: Track \(trackId) not found")
1819
+ return
1820
+ }
1821
+
1822
+ // Insert at beginning of playNext stack (LIFO)
1823
+ self.playNextStack.insert(track, at: 0)
1824
+ print(" ✅ Added '\(track.title)' to playNext stack (position: 1)")
1825
+
1826
+ // Rebuild the player queue if actively playing
1827
+ if self.player?.currentItem != nil {
1828
+ self.rebuildAVQueueFromCurrentPosition()
1829
+ }
1830
+ }
1831
+
1832
+ /**
1833
+ * Rebuild the AVQueuePlayer from current position with temporary tracks
1834
+ * Order: [current] + [playNext stack reversed] + [upNext queue] + [remaining original]
1835
+ */
1836
+ private func rebuildAVQueueFromCurrentPosition() {
1837
+ guard let player = self.player else { return }
1838
+
1839
+ print("\n🔄 TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1840
+ print(" playNext stack: \(playNextStack.count) tracks")
1841
+ print(" upNext queue: \(upNextQueue.count) tracks")
1842
+
1843
+ // Don't interrupt currently playing item
1844
+ let currentItem = player.currentItem
1845
+ let playingItems = player.items()
1846
+
1847
+ // Build new queue order:
1848
+ // [playNext stack] + [upNext queue] + [remaining original tracks]
1849
+ var newQueueTracks: [TrackItem] = []
1850
+
1851
+ // Add playNext stack (LIFO - most recently added plays first)
1852
+ // Stack is already in correct order since we insert at position 0
1853
+ newQueueTracks.append(contentsOf: playNextStack)
1854
+
1855
+ // Add upNext queue (in order, FIFO)
1856
+ newQueueTracks.append(contentsOf: upNextQueue)
1857
+
1858
+ // Add remaining original tracks
1859
+ if currentTrackIndex + 1 < currentTracks.count {
1860
+ let remainingOriginal = Array(currentTracks[(currentTrackIndex + 1)...])
1861
+ newQueueTracks.append(contentsOf: remainingOriginal)
1862
+ }
1863
+
1864
+ print(" New queue: \(newQueueTracks.count) tracks total")
1865
+
1866
+ // Remove all items from player EXCEPT the currently playing one
1867
+ for item in playingItems where item != currentItem {
1868
+ player.remove(item)
1869
+ }
1870
+
1871
+ // Insert new items in order
1872
+ var lastItem = currentItem
1873
+ for track in newQueueTracks {
1874
+ if let item = createGaplessPlayerItem(for: track, isPreload: false) {
1183
1875
  player.insert(item, after: lastItem)
1184
1876
  lastItem = item
1185
1877
  }
1878
+ }
1879
+
1880
+ print(" ✅ Queue rebuilt successfully")
1881
+ }
1186
1882
 
1187
- // Restore the full playlist reference (don't slice it!)
1188
- self.currentTracks = fullPlaylist
1883
+ /**
1884
+ * Find a track by ID from current playlist or all playlists
1885
+ */
1886
+ private func findTrackById(_ trackId: String) -> TrackItem? {
1887
+ // First check current playlist
1888
+ if let track = currentTracks.first(where: { $0.id == trackId }) {
1889
+ return track
1890
+ }
1189
1891
 
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()
1892
+ // Then check all playlists
1893
+ let allPlaylists = playlistManager.getAllPlaylists()
1894
+ for playlist in allPlaylists {
1895
+ if let track = playlist.tracks.first(where: { $0.id == trackId }) {
1896
+ return track
1195
1897
  }
1898
+ }
1196
1899
 
1197
- // Start preloading upcoming tracks for gapless playback
1198
- self.preloadUpcomingTracks(from: index + 1)
1900
+ return nil
1901
+ }
1199
1902
 
1200
- player.play()
1903
+ /**
1904
+ * Determine what type of track is currently playing
1905
+ */
1906
+ private func determineCurrentTemporaryType() -> TemporaryType {
1907
+ guard let currentItem = player?.currentItem,
1908
+ let trackId = currentItem.trackId
1909
+ else {
1910
+ return .none
1201
1911
  }
1912
+
1913
+ // Check if in playNext stack
1914
+ if playNextStack.contains(where: { $0.id == trackId }) {
1915
+ return .playNext
1916
+ }
1917
+
1918
+ // Check if in upNext queue
1919
+ if upNextQueue.contains(where: { $0.id == trackId }) {
1920
+ return .upNext
1921
+ }
1922
+
1923
+ return .none
1202
1924
  }
1203
1925
 
1204
1926
  // MARK: - Cleanup