react-native-nitro-player 0.6.1 → 0.7.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 (164) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  23. package/ios/HybridDownloadManager.swift +32 -26
  24. package/ios/HybridEqualizer.swift +48 -35
  25. package/ios/HybridTrackPlayer.swift +127 -102
  26. package/ios/core/ListenerRegistry.swift +60 -0
  27. package/ios/core/TrackPlayerCore.swift +130 -2356
  28. package/ios/core/TrackPlayerListener.swift +395 -0
  29. package/ios/core/TrackPlayerNotify.swift +52 -0
  30. package/ios/core/TrackPlayerPlayback.swift +274 -0
  31. package/ios/core/TrackPlayerQueue.swift +212 -0
  32. package/ios/core/TrackPlayerQueueBuild.swift +482 -0
  33. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  34. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  35. package/ios/equalizer/EqualizerCore.swift +24 -89
  36. package/ios/media/MediaSessionManager.swift +32 -49
  37. package/ios/playlist/PlaylistManager.swift +2 -9
  38. package/ios/queue/HybridPlayerQueue.swift +69 -66
  39. package/lib/hooks/useDownloadedTracks.js +16 -13
  40. package/lib/hooks/useEqualizer.d.ts +4 -4
  41. package/lib/hooks/useEqualizer.js +12 -12
  42. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  43. package/lib/hooks/useEqualizerPresets.js +12 -18
  44. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  45. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  46. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  47. package/lib/specs/Equalizer.nitro.d.ts +9 -9
  48. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  49. package/nitro.json +44 -11
  50. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +63 -24
  51. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +1 -1
  52. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +1 -1
  53. package/nitrogen/generated/android/c++/JDownloadError.hpp +1 -1
  54. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +1 -1
  55. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +1 -1
  56. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +1 -1
  57. package/nitrogen/generated/android/c++/JDownloadState.hpp +1 -1
  58. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +1 -1
  59. package/nitrogen/generated/android/c++/JDownloadTask.hpp +1 -1
  60. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +1 -1
  61. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +1 -1
  62. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +1 -1
  63. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +1 -1
  64. package/nitrogen/generated/android/c++/JEqualizerState.hpp +1 -1
  65. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +2 -2
  66. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +2 -2
  67. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -2
  68. package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +2 -2
  69. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  70. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  71. package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +2 -2
  72. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +2 -2
  73. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -2
  74. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +2 -2
  75. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +2 -2
  76. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -2
  77. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +2 -2
  78. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  79. package/nitrogen/generated/android/c++/JGainRange.hpp +1 -1
  80. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +49 -30
  81. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +21 -24
  82. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +35 -28
  83. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +20 -23
  84. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +197 -93
  85. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +29 -32
  86. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +157 -67
  87. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +28 -31
  88. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +138 -53
  89. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +27 -30
  90. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +282 -69
  91. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +35 -30
  92. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +1 -1
  93. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +1 -1
  94. package/nitrogen/generated/android/c++/JPlayerState.hpp +1 -1
  95. package/nitrogen/generated/android/c++/JPlaylist.hpp +1 -1
  96. package/nitrogen/generated/android/c++/JPresetType.hpp +1 -1
  97. package/nitrogen/generated/android/c++/JQueueOperation.hpp +1 -1
  98. package/nitrogen/generated/android/c++/JReason.hpp +1 -1
  99. package/nitrogen/generated/android/c++/JRepeatMode.hpp +1 -1
  100. package/nitrogen/generated/android/c++/JStorageLocation.hpp +1 -1
  101. package/nitrogen/generated/android/c++/JTAudioDevice.hpp +1 -1
  102. package/nitrogen/generated/android/c++/JTrackItem.hpp +1 -1
  103. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +1 -1
  104. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  105. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +3 -3
  106. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +3 -3
  107. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +3 -3
  108. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +3 -3
  109. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +3 -3
  110. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +3 -3
  111. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +3 -3
  112. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  113. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +18 -20
  114. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +17 -19
  115. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +25 -28
  116. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +25 -27
  117. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +24 -26
  118. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +60 -26
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +0 -6
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +0 -6
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +0 -6
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +0 -6
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +0 -6
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +0 -6
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +0 -6
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +0 -6
  127. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  128. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  129. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  130. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  131. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  132. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  133. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  134. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  135. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  136. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  137. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  138. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  139. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  140. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  141. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  142. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  143. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  144. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  145. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  146. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  147. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  148. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  149. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  150. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  151. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  152. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  153. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  154. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  155. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  156. package/package.json +3 -3
  157. package/src/hooks/useDownloadedTracks.ts +17 -13
  158. package/src/hooks/useEqualizer.ts +16 -16
  159. package/src/hooks/useEqualizerPresets.ts +15 -21
  160. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  161. package/src/specs/AudioDevices.nitro.ts +2 -2
  162. package/src/specs/DownloadManager.nitro.ts +10 -10
  163. package/src/specs/Equalizer.nitro.ts +9 -9
  164. package/src/specs/TrackPlayer.nitro.ts +52 -16
@@ -0,0 +1,121 @@
1
+ @file:Suppress("ktlint:standard:max-line-length")
2
+
3
+ package com.margelo.nitro.nitroplayer.core
4
+
5
+ import com.margelo.nitro.nitroplayer.TrackItem
6
+
7
+ /**
8
+ * Temporary queue management (playNext stack + upNext queue) and playlist loading.
9
+ * All public functions are suspend and execute on the player thread.
10
+ */
11
+
12
+ // ── Playlist loading ──────────────────────────────────────────────────────
13
+
14
+ suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) = withPlayerContext {
15
+ playNextStack.clear()
16
+ upNextQueue.clear()
17
+ currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
18
+ val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
19
+ currentPlaylistId = playlistId
20
+ updatePlayerQueue(playlist.tracks)
21
+ checkUpcomingTracksForUrls(lookaheadCount)
22
+ notifyTemporaryQueueChange()
23
+ }
24
+
25
+ /**
26
+ * Debounced update — coalesces rapid back-to-back mutations into one player rebuild.
27
+ * Called by HybridPlayerQueue when playlist data changes.
28
+ */
29
+ fun TrackPlayerCore.updatePlaylist(playlistId: String) {
30
+ if (currentPlaylistId != playlistId) return
31
+ playerHandler.removeCallbacks(updateCurrentPlaylistRunnable)
32
+ playerHandler.post(updateCurrentPlaylistRunnable)
33
+ }
34
+
35
+ // ── playNext (LIFO) ────────────────────────────────────────────────────────
36
+
37
+ suspend fun TrackPlayerCore.playNext(trackId: String) = withPlayerContext { playNextInternal(trackId) }
38
+
39
+ internal fun TrackPlayerCore.playNextInternal(trackId: String) {
40
+ val track = findTrackById(trackId)
41
+ ?: throw IllegalArgumentException("Track $trackId not found")
42
+ playNextStack.add(0, track)
43
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
44
+ notifyTemporaryQueueChange()
45
+ }
46
+
47
+ // ── addToUpNext (FIFO) ────────────────────────────────────────────────────
48
+
49
+ suspend fun TrackPlayerCore.addToUpNext(trackId: String) = withPlayerContext { addToUpNextInternal(trackId) }
50
+
51
+ internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
52
+ val track = findTrackById(trackId)
53
+ ?: throw IllegalArgumentException("Track $trackId not found")
54
+ upNextQueue.add(track)
55
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
56
+ notifyTemporaryQueueChange()
57
+ }
58
+
59
+ // ── Remove / clear ────────────────────────────────────────────────────────
60
+
61
+ suspend fun TrackPlayerCore.removeFromPlayNext(trackId: String): Boolean = withPlayerContext {
62
+ val idx = playNextStack.indexOfFirst { it.id == trackId }
63
+ if (idx < 0) return@withPlayerContext false
64
+ playNextStack.removeAt(idx)
65
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
66
+ notifyTemporaryQueueChange()
67
+ true
68
+ }
69
+
70
+ suspend fun TrackPlayerCore.removeFromUpNext(trackId: String): Boolean = withPlayerContext {
71
+ val idx = upNextQueue.indexOfFirst { it.id == trackId }
72
+ if (idx < 0) return@withPlayerContext false
73
+ upNextQueue.removeAt(idx)
74
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
75
+ notifyTemporaryQueueChange()
76
+ true
77
+ }
78
+
79
+ suspend fun TrackPlayerCore.clearPlayNext() = withPlayerContext {
80
+ playNextStack.clear()
81
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
82
+ notifyTemporaryQueueChange()
83
+ }
84
+
85
+ suspend fun TrackPlayerCore.clearUpNext() = withPlayerContext {
86
+ upNextQueue.clear()
87
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
88
+ notifyTemporaryQueueChange()
89
+ }
90
+
91
+ // ── Reorder ───────────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Reorder within the combined virtual list [playNextStack + upNextQueue].
95
+ * newIndex is 0-based within that combined list.
96
+ */
97
+ suspend fun TrackPlayerCore.reorderTemporaryTrack(trackId: String, newIndex: Int): Boolean = withPlayerContext {
98
+ val combined = (playNextStack + upNextQueue).toMutableList()
99
+ val fromIdx = combined.indexOfFirst { it.id == trackId }
100
+ if (fromIdx < 0) return@withPlayerContext false
101
+ val track = combined.removeAt(fromIdx)
102
+ val clampedIndex = newIndex.coerceIn(0, combined.size)
103
+ combined.add(clampedIndex, track)
104
+
105
+ // Split back at original playNextStack.size boundary (reduced if an item was moved out)
106
+ val pnSize = playNextStack.size
107
+ playNextStack.clear()
108
+ upNextQueue.clear()
109
+ playNextStack.addAll(combined.take(pnSize))
110
+ upNextQueue.addAll(combined.drop(pnSize))
111
+
112
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
113
+ notifyTemporaryQueueChange()
114
+ true
115
+ }
116
+
117
+ // ── Read-only accessors ────────────────────────────────────────────────────
118
+
119
+ suspend fun TrackPlayerCore.getPlayNextQueue(): List<TrackItem> = withPlayerContext { playNextStack.toList() }
120
+
121
+ suspend fun TrackPlayerCore.getUpNextQueue(): List<TrackItem> = withPlayerContext { upNextQueue.toList() }
@@ -0,0 +1,98 @@
1
+ @file:Suppress("ktlint:standard:max-line-length")
2
+
3
+ package com.margelo.nitro.nitroplayer.core
4
+
5
+ import androidx.media3.common.Player
6
+ import com.margelo.nitro.nitroplayer.TrackItem
7
+
8
+ /**
9
+ * Lazy URL loading support, track queries, and playback speed.
10
+ * All public functions are suspend and execute on the player thread.
11
+ */
12
+
13
+ // ── Track updates (URL resolution) ────────────────────────────────────────
14
+
15
+ suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) = withPlayerContext {
16
+ val currentTrack = getCurrentTrack()
17
+ val currentTrackId = currentTrack?.id
18
+ val currentTrackIsEmpty = currentTrack?.url.isNullOrEmpty()
19
+ val currentTrackUpdate = if (currentTrackId != null) tracks.find { it.id == currentTrackId } else null
20
+
21
+ val safeTracks = tracks.filter { track ->
22
+ when {
23
+ track.id == currentTrackId && !currentTrackIsEmpty -> false // preserve gapless
24
+ track.id == currentTrackId && currentTrackIsEmpty -> track.url.isNotEmpty()
25
+ track.url.isEmpty() -> false
26
+ else -> true
27
+ }
28
+ }
29
+ if (safeTracks.isEmpty()) return@withPlayerContext
30
+
31
+ val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
32
+
33
+ // Replace current track's MediaItem if it was empty-URL and now has a URL
34
+ if (currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()) {
35
+ val exoIndex = exo.currentMediaItemIndex
36
+ if (exoIndex >= 0) {
37
+ val playlistId = currentPlaylistId ?: ""
38
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate.id}" else currentTrackUpdate.id
39
+ exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
40
+ if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
41
+ }
42
+ }
43
+
44
+ if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
45
+ val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
46
+ if (refreshedPlaylist != null) {
47
+ currentTracks = refreshedPlaylist.tracks
48
+ val updatedById = currentTracks.associateBy { it.id }
49
+ playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
50
+ upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
51
+ }
52
+ rebuildQueueFromCurrentPosition()
53
+ }
54
+ }
55
+
56
+ // ── Track queries ─────────────────────────────────────────────────────────
57
+
58
+ suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> =
59
+ withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
60
+
61
+ suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> =
62
+ withPlayerContext { getTracksNeedingUrlsInternal() }
63
+
64
+ internal fun TrackPlayerCore.getTracksNeedingUrlsInternal(): List<TrackItem> {
65
+ val pid = currentPlaylistId ?: return emptyList()
66
+ return playlistManager.getPlaylist(pid)?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
67
+ }
68
+
69
+ suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> =
70
+ withPlayerContext { getNextTracksInternal(count) }
71
+
72
+ internal fun TrackPlayerCore.getNextTracksInternal(count: Int): List<TrackItem> {
73
+ val actualQueue = getActualQueueInternal()
74
+ if (actualQueue.isEmpty()) return emptyList()
75
+ val currentIdx = actualQueue.indexOfFirst { it.id == getCurrentTrack()?.id }
76
+ if (currentIdx == -1) return emptyList()
77
+ val start = currentIdx + 1
78
+ val end = minOf(start + count, actualQueue.size)
79
+ return if (start < actualQueue.size) actualQueue.subList(start, end) else emptyList()
80
+ }
81
+
82
+ suspend fun TrackPlayerCore.getCurrentTrackIndex(): Int = withPlayerContext { currentTrackIndex }
83
+
84
+ // ── URL lookahead ─────────────────────────────────────────────────────────
85
+
86
+ internal fun TrackPlayerCore.checkUpcomingTracksForUrls(lookahead: Int = 5) {
87
+ val upcomingTracks = if (currentTrackIndex < 0) {
88
+ currentTracks.take(lookahead)
89
+ } else {
90
+ getNextTracksInternal(lookahead)
91
+ }
92
+ val currentTrack = getCurrentTrack()
93
+ val currentNeedsUrl = currentTrack != null && currentTrack.url.isEmpty()
94
+ val candidates = if (currentNeedsUrl) listOf(currentTrack!!) + upcomingTracks else upcomingTracks
95
+ val needUrls = candidates.filter { it.url.isEmpty() }
96
+ if (needUrls.isNotEmpty()) notifyTracksNeedUpdate(needUrls, lookahead)
97
+ }
98
+
@@ -10,6 +10,10 @@ import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
10
10
  import org.json.JSONArray
11
11
  import org.json.JSONObject
12
12
  import java.io.File
13
+ import kotlinx.coroutines.CoroutineScope
14
+ import kotlinx.coroutines.Dispatchers
15
+ import kotlinx.coroutines.SupervisorJob
16
+ import kotlinx.coroutines.launch
13
17
 
14
18
  /**
15
19
  * Manages persistence of downloaded track metadata using file storage
@@ -37,6 +41,7 @@ class DownloadDatabase private constructor(
37
41
  private val downloadedTracks = mutableMapOf<String, DownloadedTrackRecord>()
38
42
  private val playlistTracks = mutableMapOf<String, MutableSet<String>>()
39
43
  private val fileManager = DownloadFileManager.getInstance(context)
44
+ private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
40
45
 
41
46
  init {
42
47
  loadFromDisk()
@@ -295,27 +300,31 @@ class DownloadDatabase private constructor(
295
300
  }
296
301
  }
297
302
 
298
- // Persistence
303
+ // Persistence — called while holding synchronized(this); snapshots data then writes on IO.
299
304
  private fun saveToDisk() {
300
- try {
301
- val tracksJson = JSONObject()
302
- for ((trackId, record) in downloadedTracks) {
303
- tracksJson.put(trackId, record.toJson())
304
- }
305
-
306
- val playlistJson = JSONObject()
307
- for ((playlistId, trackIds) in playlistTracks) {
308
- playlistJson.put(playlistId, JSONArray(trackIds.toList()))
309
- }
305
+ val trackSnapshot = downloadedTracks.toMap()
306
+ val playlistSnapshot = playlistTracks.mapValues { it.value.toSet() }
307
+ ioScope.launch {
308
+ try {
309
+ val tracksJson = JSONObject()
310
+ for ((trackId, record) in trackSnapshot) {
311
+ tracksJson.put(trackId, record.toJson())
312
+ }
310
313
 
311
- val wrapper =
312
- JSONObject().apply {
313
- put("downloadedTracks", tracksJson)
314
- put("playlistTracks", playlistJson)
314
+ val playlistJson = JSONObject()
315
+ for ((playlistId, trackIds) in playlistSnapshot) {
316
+ playlistJson.put(playlistId, JSONArray(trackIds.toList()))
315
317
  }
316
- NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
317
- } catch (e: Exception) {
318
- e.printStackTrace()
318
+
319
+ val wrapper =
320
+ JSONObject().apply {
321
+ put("downloadedTracks", tracksJson)
322
+ put("playlistTracks", playlistJson)
323
+ }
324
+ NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
325
+ } catch (e: Exception) {
326
+ e.printStackTrace()
327
+ }
319
328
  }
320
329
  }
321
330
 
@@ -14,8 +14,7 @@ import com.margelo.nitro.nitroplayer.Variant_NullType_String
14
14
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
15
15
  import org.json.JSONArray
16
16
  import org.json.JSONObject
17
- import java.lang.ref.WeakReference
18
- import java.util.Collections
17
+ import com.margelo.nitro.nitroplayer.core.ListenerRegistry
19
18
 
20
19
  class EqualizerCore private constructor(
21
20
  private val context: Context,
@@ -35,21 +34,10 @@ class EqualizerCore private constructor(
35
34
  private val prefs: SharedPreferences =
36
35
  context.getSharedPreferences("equalizer_settings", Context.MODE_PRIVATE)
37
36
 
38
- // Weak callback wrapper for auto-cleanup
39
- private data class WeakCallbackBox<T>(
40
- private val ownerRef: WeakReference<Any>,
41
- val callback: T,
42
- ) {
43
- val isAlive: Boolean get() = ownerRef.get() != null
44
- }
45
-
46
37
  // Event listeners
47
- private val onEnabledChangeListeners =
48
- Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Boolean) -> Unit>>())
49
- private val onBandChangeListeners =
50
- Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>())
51
- private val onPresetChangeListeners =
52
- Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Variant_NullType_String?) -> Unit>>())
38
+ private val onEnabledChangeListeners = ListenerRegistry<(Boolean) -> Unit>()
39
+ private val onBandChangeListeners = ListenerRegistry<(Array<EqualizerBand>) -> Unit>()
40
+ private val onPresetChangeListeners = ListenerRegistry<(Variant_NullType_String?) -> Unit>()
53
41
 
54
42
  companion object {
55
43
  private const val TAG = "EqualizerCore"
@@ -429,63 +417,28 @@ class EqualizerCore private constructor(
429
417
  // === Callback management ===
430
418
 
431
419
  fun addOnEnabledChangeListener(callback: (Boolean) -> Unit) {
432
- val box = WeakCallbackBox(WeakReference(callback as Any), callback)
433
- onEnabledChangeListeners.add(box)
420
+ onEnabledChangeListeners.add(callback)
434
421
  }
435
422
 
436
423
  fun addOnBandChangeListener(callback: (Array<EqualizerBand>) -> Unit) {
437
- val box = WeakCallbackBox(WeakReference(callback as Any), callback)
438
- synchronized(onBandChangeListeners) {
439
- @Suppress("UNCHECKED_CAST")
440
- (onBandChangeListeners as MutableList<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>).add(box)
441
- }
424
+ onBandChangeListeners.add(callback)
442
425
  }
443
426
 
444
427
  fun addOnPresetChangeListener(callback: (Variant_NullType_String?) -> Unit) {
445
- val box = WeakCallbackBox(WeakReference(callback as Any), callback)
446
- onPresetChangeListeners.add(box)
428
+ onPresetChangeListeners.add(callback)
447
429
  }
448
430
 
449
431
  private fun notifyEnabledChange(enabled: Boolean) {
450
- synchronized(onEnabledChangeListeners) {
451
- onEnabledChangeListeners.removeAll { !it.isAlive }
452
- onEnabledChangeListeners.forEach { box ->
453
- try {
454
- box.callback(enabled)
455
- } catch (e: Exception) {
456
- // Ignore callback errors
457
- }
458
- }
459
- }
432
+ onEnabledChangeListeners.forEach { it(enabled) }
460
433
  }
461
434
 
462
435
  private fun notifyBandChange(bands: Array<EqualizerBand>) {
463
- synchronized(onBandChangeListeners) {
464
- @Suppress("UNCHECKED_CAST")
465
- val listeners = onBandChangeListeners as MutableList<WeakCallbackBox<(Array<EqualizerBand>) -> Unit>>
466
- listeners.removeAll { !it.isAlive }
467
- listeners.forEach { box ->
468
- try {
469
- box.callback(bands)
470
- } catch (e: Exception) {
471
- // Ignore callback errors
472
- }
473
- }
474
- }
436
+ onBandChangeListeners.forEach { it(bands) }
475
437
  }
476
438
 
477
439
  private fun notifyPresetChange(presetName: String?) {
478
- synchronized(onPresetChangeListeners) {
479
- onPresetChangeListeners.removeAll { !it.isAlive }
480
- onPresetChangeListeners.forEach { box ->
481
- try {
482
- val variant = presetName?.let { Variant_NullType_String.create(it) }
483
- box.callback(variant)
484
- } catch (e: Exception) {
485
- // Ignore callback errors
486
- }
487
- }
488
- }
440
+ val variant = presetName?.let { Variant_NullType_String.create(it) }
441
+ onPresetChangeListeners.forEach { it(variant) }
489
442
  }
490
443
 
491
444
  fun release() {
@@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ListenableFuture
26
26
  import com.margelo.nitro.nitroplayer.TrackItem
27
27
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
28
28
  import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
29
+ import com.margelo.nitro.nitroplayer.core.loadPlaylist
29
30
  import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
30
31
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
31
32
  import kotlinx.coroutines.CoroutineScope
@@ -50,6 +51,8 @@ class MediaSessionManager(
50
51
  private set
51
52
  private var notificationManager: NotificationManager? = null
52
53
  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
54
+ @Volatile private var currentTrack: TrackItem? = null
55
+ @Volatile private var isPlaying: Boolean = false
53
56
  private val artworkCache = object : LruCache<String, Bitmap>(20) {
54
57
  override fun sizeOf(key: String, value: Bitmap): Int = 1
55
58
  }
@@ -216,7 +219,7 @@ class MediaSessionManager(
216
219
 
217
220
  if (trackIndex >= 0) {
218
221
  // Load the entire playlist into TrackPlayerCore
219
- trackPlayerCore?.loadPlaylist(playlistId)
222
+ trackPlayerCore?.let { core -> scope.launch { core.loadPlaylist(playlistId) } }
220
223
 
221
224
  // Create MediaItems for the entire playlist
222
225
  val playlistMediaItems =
@@ -324,28 +327,7 @@ class MediaSessionManager(
324
327
  }
325
328
  }
326
329
 
327
- private fun getCurrentTrack(): TrackItem? {
328
- val currentMediaItem = player.currentMediaItem ?: return null
329
- val mediaId = currentMediaItem.mediaId
330
-
331
- // Parse mediaId format: "playlistId:trackId" or just "trackId"
332
- val trackId =
333
- if (mediaId.contains(':')) {
334
- mediaId.substring(mediaId.indexOf(':') + 1)
335
- } else {
336
- mediaId
337
- }
338
-
339
- // Find track in current playlist or all playlists
340
- return trackPlayerCore?.getCurrentPlaylistId()?.let { playlistId ->
341
- playlistManager.getPlaylist(playlistId)?.tracks?.find { it.id == trackId }
342
- } ?: run {
343
- for (playlist in playlistManager.getAllPlaylists()) {
344
- playlist.tracks.find { it.id == trackId }?.let { return it }
345
- }
346
- null
347
- }
348
- }
330
+ private fun getCurrentTrack(): TrackItem? = currentTrack
349
331
 
350
332
  private fun updateNotification() {
351
333
  if (!showInNotification) return
@@ -376,7 +358,7 @@ class MediaSessionManager(
376
358
  .setSmallIcon(android.R.drawable.ic_media_play)
377
359
  .setContentIntent(contentIntent)
378
360
  .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
379
- .setOngoing(player.isPlaying)
361
+ .setOngoing(isPlaying)
380
362
  .setShowWhen(false)
381
363
  .setPriority(NotificationCompat.PRIORITY_LOW)
382
364
  .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
@@ -401,7 +383,7 @@ class MediaSessionManager(
401
383
  createMediaAction(ACTION_PREVIOUS),
402
384
  )
403
385
 
404
- if (player.isPlaying) {
386
+ if (isPlaying) {
405
387
  builder.addAction(
406
388
  android.R.drawable.ic_media_pause,
407
389
  "Pause",
@@ -459,12 +441,12 @@ class MediaSessionManager(
459
441
  notificationManager?.cancel(NOTIFICATION_ID)
460
442
  }
461
443
 
462
- fun onTrackChanged() {
444
+ fun onTrackChanged(track: TrackItem?) {
445
+ currentTrack = track
463
446
  // Preload artwork for better notification display
464
- val currentTrack = getCurrentTrack()
465
- if (currentTrack != null) {
447
+ if (track != null) {
466
448
  scope.launch {
467
- currentTrack.artwork?.asSecondOrNull()?.let { artworkUrl ->
449
+ track.artwork?.asSecondOrNull()?.let { artworkUrl ->
468
450
  loadArtworkBitmap(artworkUrl)
469
451
  }
470
452
  updateNotification()
@@ -474,7 +456,8 @@ class MediaSessionManager(
474
456
  }
475
457
  }
476
458
 
477
- fun onPlaybackStateChanged() {
459
+ fun onPlaybackStateChanged(playing: Boolean) {
460
+ isPlaying = playing
478
461
  updateNotification()
479
462
  }
480
463