react-native-nitro-player 0.7.1-alpha.0 → 0.7.1-alpha.2

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 (39) hide show
  1. package/README.md +47 -46
  2. package/android/src/main/AndroidManifest.xml +14 -1
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +70 -29
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +70 -65
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +23 -12
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +64 -30
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +54 -28
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +4 -3
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +12 -3
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +169 -98
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +30 -178
  25. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +40 -0
  26. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
  27. package/ios/core/TrackPlayerQueue.swift +27 -18
  28. package/ios/core/TrackPlayerQueueBuild.swift +16 -5
  29. package/ios/equalizer/EqualizerCore.swift +39 -34
  30. package/lib/hooks/useEqualizer.js +10 -5
  31. package/lib/specs/Equalizer.nitro.d.ts +1 -1
  32. package/lib/types/EqualizerTypes.d.ts +3 -3
  33. package/package.json +5 -5
  34. package/src/hooks/useEqualizer.ts +25 -17
  35. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
  36. package/src/specs/DownloadManager.nitro.ts +4 -2
  37. package/src/specs/Equalizer.nitro.ts +5 -3
  38. package/src/specs/TrackPlayer.nitro.ts +18 -6
  39. package/src/types/EqualizerTypes.ts +17 -13
@@ -35,18 +35,24 @@ internal fun TrackPlayerCore.getStateInternal(): PlayerState {
35
35
  val currentTrack: Variant_NullType_TrackItem? = track?.let { Variant_NullType_TrackItem.create(it) }
36
36
  val position = exo.currentPosition / 1000.0
37
37
  val duration = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
38
- val state = when (exo.playbackState) {
39
- Player.STATE_IDLE -> TrackPlayerState.STOPPED
40
- Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
41
- Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
42
- Player.STATE_ENDED -> TrackPlayerState.STOPPED
43
- else -> TrackPlayerState.STOPPED
44
- }
45
- val playingType = if (track == null) CurrentPlayingType.NOT_PLAYING else when (currentTemporaryType) {
46
- TrackPlayerCore.TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
47
- TrackPlayerCore.TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
48
- TrackPlayerCore.TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
49
- }
38
+ val state =
39
+ when (exo.playbackState) {
40
+ Player.STATE_IDLE -> TrackPlayerState.STOPPED
41
+ Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
42
+ Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
43
+ Player.STATE_ENDED -> TrackPlayerState.STOPPED
44
+ else -> TrackPlayerState.STOPPED
45
+ }
46
+ val playingType =
47
+ if (track == null) {
48
+ CurrentPlayingType.NOT_PLAYING
49
+ } else {
50
+ when (currentTemporaryType) {
51
+ TrackPlayerCore.TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
52
+ TrackPlayerCore.TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
53
+ TrackPlayerCore.TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
54
+ }
55
+ }
50
56
  return PlayerState(
51
57
  currentTrack = currentTrack,
52
58
  currentPosition = position,
@@ -70,26 +76,43 @@ internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
70
76
  val queue = ArrayList<TrackItem>(currentTracks.size + playNextStack.size + upNextQueue.size)
71
77
 
72
78
  // Tracks before current (include currentTrackIndex when a temp track is playing)
73
- val beforeEnd = if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
74
- minOf(currentIndex + 1, currentTracks.size)
75
- } else {
76
- currentIndex
77
- }
79
+ val beforeEnd =
80
+ if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
81
+ minOf(currentIndex + 1, currentTracks.size)
82
+ } else {
83
+ currentIndex
84
+ }
78
85
  if (beforeEnd > 0) queue.addAll(currentTracks.subList(0, beforeEnd))
79
86
 
80
87
  // Current track
81
88
  getCurrentTrack()?.let { queue.add(it) }
82
89
 
83
- // playNext skip index 0 if it is the current item
84
- if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
85
- queue.addAll(playNextStack.subList(1, playNextStack.size))
90
+ val currentId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
91
+
92
+ // playNext — skip the currently playing track by ID (not position)
93
+ if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && currentId != null) {
94
+ var skipped = false
95
+ for (track in playNextStack) {
96
+ if (!skipped && track.id == currentId) {
97
+ skipped = true
98
+ continue
99
+ }
100
+ queue.add(track)
101
+ }
86
102
  } else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
87
103
  queue.addAll(playNextStack)
88
104
  }
89
105
 
90
- // upNext — skip index 0 if it is the current item
91
- if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && upNextQueue.size > 1) {
92
- queue.addAll(upNextQueue.subList(1, upNextQueue.size))
106
+ // upNext — skip the currently playing track by ID (not position)
107
+ if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && currentId != null) {
108
+ var skipped = false
109
+ for (track in upNextQueue) {
110
+ if (!skipped && track.id == currentId) {
111
+ skipped = true
112
+ continue
113
+ }
114
+ queue.add(track)
115
+ }
93
116
  } else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
94
117
  queue.addAll(upNextQueue)
95
118
  }
@@ -120,21 +143,31 @@ private fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
120
143
  val upNextEnd = upNextStart + effectiveUpNextSize
121
144
  val originalRemainingStart = upNextEnd
122
145
 
123
- if (index < currentPos) { playFromIndexInternal(index); return true }
124
- if (index == currentPos) { exo.seekTo(0); return true }
146
+ if (index < currentPos) {
147
+ playFromIndexInternal(index)
148
+ return true
149
+ }
150
+ if (index == currentPos) {
151
+ exo.seekTo(0)
152
+ return true
153
+ }
125
154
 
126
155
  if (index in playNextStart until playNextEnd) {
127
- val listIndex = (index - playNextStart) + if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT) 1 else 0
128
- if (listIndex > 0) playNextStack.subList(0, listIndex).clear()
156
+ val targetTrack = actualQueue[index]
157
+ // Remove all playNext tracks before the target (by ID lookup, not position)
158
+ val targetIdx = playNextStack.indexOfFirst { it.id == targetTrack.id }
159
+ if (targetIdx > 0) playNextStack.subList(0, targetIdx).clear()
129
160
  rebuildQueueFromCurrentPosition()
130
161
  exo.seekToNext()
131
162
  return true
132
163
  }
133
164
 
134
165
  if (index in upNextStart until upNextEnd) {
135
- val listIndex = (index - upNextStart) + if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT) 1 else 0
166
+ val targetTrack = actualQueue[index]
136
167
  playNextStack.clear()
137
- if (listIndex > 0) upNextQueue.subList(0, listIndex).clear()
168
+ // Remove all upNext tracks before the target (by ID lookup, not position)
169
+ val targetIdx = upNextQueue.indexOfFirst { it.id == targetTrack.id }
170
+ if (targetIdx > 0) upNextQueue.subList(0, targetIdx).clear()
138
171
  rebuildQueueFromCurrentPosition()
139
172
  exo.seekToNext()
140
173
  return true
@@ -144,7 +177,8 @@ private fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
144
177
  val targetTrack = actualQueue[index]
145
178
  val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
146
179
  if (originalIndex == -1) return false
147
- playNextStack.clear(); upNextQueue.clear()
180
+ playNextStack.clear()
181
+ upNextQueue.clear()
148
182
  currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
149
183
  rebuildQueueAndPlayFromIndex(originalIndex)
150
184
  checkUpcomingTracksForUrls(lookaheadCount)
@@ -22,10 +22,11 @@ internal fun TrackPlayerCore.rebuildQueueAndPlayFromIndex(index: Int) {
22
22
  if (index < 0 || index >= currentTracks.size) return
23
23
 
24
24
  val playlistId = currentPlaylistId ?: ""
25
- val mediaItems = currentTracks.subList(index, currentTracks.size).map { track ->
26
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
27
- makeMediaItem(track, mediaId)
28
- }
25
+ val mediaItems =
26
+ currentTracks.subList(index, currentTracks.size).map { track ->
27
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
28
+ makeMediaItem(track, mediaId)
29
+ }
29
30
 
30
31
  currentTrackIndex = index
31
32
  exo.clearMediaItems()
@@ -51,17 +52,32 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
51
52
  }
52
53
 
53
54
  val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
54
-
55
- // playNext stack — skip index 0 if it is the currently playing item
56
- if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
57
- newQueueTracks.addAll(playNextStack.subList(1, playNextStack.size))
55
+ val currentId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
56
+
57
+ // playNext stack skip the currently playing track by ID (not position)
58
+ if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && currentId != null) {
59
+ var skipped = false
60
+ for (track in playNextStack) {
61
+ if (!skipped && track.id == currentId) {
62
+ skipped = true
63
+ continue
64
+ }
65
+ newQueueTracks.add(track)
66
+ }
58
67
  } else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
59
68
  newQueueTracks.addAll(playNextStack)
60
69
  }
61
70
 
62
- // upNext queue — skip index 0 if it is the currently playing item
63
- if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && upNextQueue.size > 1) {
64
- newQueueTracks.addAll(upNextQueue.subList(1, upNextQueue.size))
71
+ // upNext queue — skip the currently playing track by ID (not position)
72
+ if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && currentId != null) {
73
+ var skipped = false
74
+ for (track in upNextQueue) {
75
+ if (!skipped && track.id == currentId) {
76
+ skipped = true
77
+ continue
78
+ }
79
+ newQueueTracks.add(track)
80
+ }
65
81
  } else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
66
82
  newQueueTracks.addAll(upNextQueue)
67
83
  }
@@ -72,10 +88,11 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
72
88
  }
73
89
 
74
90
  val playlistId = currentPlaylistId ?: ""
75
- val newMediaItems = newQueueTracks.map { track ->
76
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
77
- makeMediaItem(track, mediaId)
78
- }
91
+ val newMediaItems =
92
+ newQueueTracks.map { track ->
93
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
94
+ makeMediaItem(track, mediaId)
95
+ }
79
96
 
80
97
  if (exo.mediaItemCount > currentIndex + 1) {
81
98
  exo.removeMediaItems(currentIndex + 1, exo.mediaItemCount)
@@ -88,10 +105,11 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
88
105
  internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
89
106
  currentTracks = tracks
90
107
  val playlistId = currentPlaylistId ?: ""
91
- val mediaItems = tracks.map { track ->
92
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
93
- makeMediaItem(track, mediaId)
94
- }
108
+ val mediaItems =
109
+ tracks.map { track ->
110
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
111
+ makeMediaItem(track, mediaId)
112
+ }
95
113
  exo.setMediaItems(mediaItems, false)
96
114
  if (exo.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
97
115
  exo.prepare()
@@ -100,19 +118,28 @@ internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
100
118
 
101
119
  // ── MediaItem construction (member extension to access downloadManager) ────
102
120
 
103
- internal fun TrackPlayerCore.makeMediaItem(track: TrackItem, customMediaId: String? = null): MediaItem {
104
- val metaBuilder = MediaMetadata.Builder()
105
- .setTitle(track.title)
106
- .setArtist(track.artist)
107
- .setAlbumTitle(track.album)
121
+ internal fun TrackPlayerCore.makeMediaItem(
122
+ track: TrackItem,
123
+ customMediaId: String? = null,
124
+ ): MediaItem {
125
+ val metaBuilder =
126
+ MediaMetadata
127
+ .Builder()
128
+ .setTitle(track.title)
129
+ .setArtist(track.artist)
130
+ .setAlbumTitle(track.album)
108
131
 
109
132
  track.artwork?.asSecondOrNull()?.let { artworkUrl ->
110
- try { metaBuilder.setArtworkUri(Uri.parse(artworkUrl)) } catch (_: Exception) {}
133
+ try {
134
+ metaBuilder.setArtworkUri(Uri.parse(artworkUrl))
135
+ } catch (_: Exception) {
136
+ }
111
137
  }
112
138
 
113
139
  val effectiveUrl = downloadManager.getEffectiveUrl(track)
114
140
 
115
- return MediaItem.Builder()
141
+ return MediaItem
142
+ .Builder()
116
143
  .setMediaId(customMediaId ?: track.id)
117
144
  .setUri(effectiveUrl)
118
145
  .setMediaMetadata(metaBuilder.build())
@@ -157,5 +184,4 @@ internal fun TrackPlayerCore.determineCurrentTemporaryType(): TrackPlayerCore.Te
157
184
  return TrackPlayerCore.TemporaryType.NONE
158
185
  }
159
186
 
160
- internal fun TrackPlayerCore.extractTrackId(mediaId: String): String =
161
- if (mediaId.contains(':')) mediaId.substring(mediaId.indexOf(':') + 1) else mediaId
187
+ internal fun TrackPlayerCore.extractTrackId(mediaId: String): String = if (mediaId.contains(':')) mediaId.substring(mediaId.indexOf(':') + 1) else mediaId
@@ -10,9 +10,10 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
10
10
  internal fun TrackPlayerCore.initExoAndMedia() {
11
11
  exo = ExoPlayerCore(context, playerThread)
12
12
 
13
- mediaSessionManager = MediaSessionManager(context, exo.player, playlistManager).apply {
14
- setTrackPlayerCore(this@initExoAndMedia)
15
- }
13
+ mediaSessionManager =
14
+ MediaSessionManager(context, exo.player, playlistManager).apply {
15
+ setTrackPlayerCore(this@initExoAndMedia)
16
+ }
16
17
 
17
18
  // Give MediaBrowserService access to this core and media session
18
19
  NitroPlayerMediaBrowserService.trackPlayerCore = this
@@ -11,16 +11,17 @@ import com.margelo.nitro.nitroplayer.TrackItem
11
11
 
12
12
  // ── Playlist loading ──────────────────────────────────────────────────────
13
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
- }
14
+ suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) =
15
+ withPlayerContext {
16
+ playNextStack.clear()
17
+ upNextQueue.clear()
18
+ currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
19
+ val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
20
+ currentPlaylistId = playlistId
21
+ updatePlayerQueue(playlist.tracks)
22
+ checkUpcomingTracksForUrls(lookaheadCount)
23
+ notifyTemporaryQueueChange()
24
+ }
24
25
 
25
26
  /**
26
27
  * Debounced update — coalesces rapid back-to-back mutations into one player rebuild.
@@ -37,8 +38,9 @@ fun TrackPlayerCore.updatePlaylist(playlistId: String) {
37
38
  suspend fun TrackPlayerCore.playNext(trackId: String) = withPlayerContext { playNextInternal(trackId) }
38
39
 
39
40
  internal fun TrackPlayerCore.playNextInternal(trackId: String) {
40
- val track = findTrackById(trackId)
41
- ?: throw IllegalArgumentException("Track $trackId not found")
41
+ val track =
42
+ findTrackById(trackId)
43
+ ?: throw IllegalArgumentException("Track $trackId not found")
42
44
  playNextStack.add(0, track)
43
45
  if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
44
46
  notifyTemporaryQueueChange()
@@ -49,8 +51,9 @@ internal fun TrackPlayerCore.playNextInternal(trackId: String) {
49
51
  suspend fun TrackPlayerCore.addToUpNext(trackId: String) = withPlayerContext { addToUpNextInternal(trackId) }
50
52
 
51
53
  internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
52
- val track = findTrackById(trackId)
53
- ?: throw IllegalArgumentException("Track $trackId not found")
54
+ val track =
55
+ findTrackById(trackId)
56
+ ?: throw IllegalArgumentException("Track $trackId not found")
54
57
  upNextQueue.add(track)
55
58
  if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
56
59
  notifyTemporaryQueueChange()
@@ -58,35 +61,39 @@ internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
58
61
 
59
62
  // ── Remove / clear ────────────────────────────────────────────────────────
60
63
 
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
- }
64
+ suspend fun TrackPlayerCore.removeFromPlayNext(trackId: String): Boolean =
65
+ withPlayerContext {
66
+ val idx = playNextStack.indexOfFirst { it.id == trackId }
67
+ if (idx < 0) return@withPlayerContext false
68
+ playNextStack.removeAt(idx)
69
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
70
+ notifyTemporaryQueueChange()
71
+ true
72
+ }
73
+
74
+ suspend fun TrackPlayerCore.removeFromUpNext(trackId: String): Boolean =
75
+ withPlayerContext {
76
+ val idx = upNextQueue.indexOfFirst { it.id == trackId }
77
+ if (idx < 0) return@withPlayerContext false
78
+ upNextQueue.removeAt(idx)
79
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
80
+ notifyTemporaryQueueChange()
81
+ true
82
+ }
83
+
84
+ suspend fun TrackPlayerCore.clearPlayNext() =
85
+ withPlayerContext {
86
+ playNextStack.clear()
87
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
88
+ notifyTemporaryQueueChange()
89
+ }
90
+
91
+ suspend fun TrackPlayerCore.clearUpNext() =
92
+ withPlayerContext {
93
+ upNextQueue.clear()
94
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
95
+ notifyTemporaryQueueChange()
96
+ }
90
97
 
91
98
  // ── Reorder ───────────────────────────────────────────────────────────────
92
99
 
@@ -94,25 +101,29 @@ suspend fun TrackPlayerCore.clearUpNext() = withPlayerContext {
94
101
  * Reorder within the combined virtual list [playNextStack + upNextQueue].
95
102
  * newIndex is 0-based within that combined list.
96
103
  */
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
- }
104
+ suspend fun TrackPlayerCore.reorderTemporaryTrack(
105
+ trackId: String,
106
+ newIndex: Int,
107
+ ): Boolean =
108
+ withPlayerContext {
109
+ val combined = (playNextStack + upNextQueue).toMutableList()
110
+ val fromIdx = combined.indexOfFirst { it.id == trackId }
111
+ if (fromIdx < 0) return@withPlayerContext false
112
+ val track = combined.removeAt(fromIdx)
113
+ val clampedIndex = newIndex.coerceIn(0, combined.size)
114
+ combined.add(clampedIndex, track)
115
+
116
+ // Split back at original playNextStack.size boundary (reduced if an item was moved out)
117
+ val pnSize = playNextStack.size
118
+ playNextStack.clear()
119
+ upNextQueue.clear()
120
+ playNextStack.addAll(combined.take(pnSize))
121
+ upNextQueue.addAll(combined.drop(pnSize))
122
+
123
+ if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
124
+ notifyTemporaryQueueChange()
125
+ true
126
+ }
116
127
 
117
128
  // ── Read-only accessors ────────────────────────────────────────────────────
118
129
 
@@ -12,62 +12,65 @@ import com.margelo.nitro.nitroplayer.TrackItem
12
12
 
13
13
  // ── Track updates (URL resolution) ────────────────────────────────────────
14
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()
15
+ suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
16
+ withPlayerContext {
17
+ val currentTrack = getCurrentTrack()
18
+ val currentTrackId = currentTrack?.id
19
+ val currentTrackIsEmpty = currentTrack?.url.isNullOrEmpty()
20
+ val currentTrackUpdate = if (currentTrackId != null) tracks.find { it.id == currentTrackId } else null
21
+
22
+ val safeTracks =
23
+ tracks.filter { track ->
24
+ when {
25
+ track.id == currentTrackId && !currentTrackIsEmpty -> false
26
+
27
+ // preserve gapless
28
+ track.id == currentTrackId && currentTrackIsEmpty -> track.url.isNotEmpty()
29
+
30
+ track.url.isEmpty() -> false
31
+
32
+ else -> true
33
+ }
34
+ }
35
+ if (safeTracks.isEmpty()) return@withPlayerContext
36
+
37
+ val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
38
+
39
+ // Replace current track's MediaItem if it was empty-URL and now has a URL
40
+ if (currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()) {
41
+ val exoIndex = exo.currentMediaItemIndex
42
+ if (exoIndex >= 0) {
43
+ val playlistId = currentPlaylistId ?: ""
44
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate.id}" else currentTrackUpdate.id
45
+ exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
46
+ if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
47
+ }
41
48
  }
42
- }
43
49
 
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 } }
50
+ if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
51
+ val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
52
+ if (refreshedPlaylist != null) {
53
+ currentTracks = refreshedPlaylist.tracks
54
+ val updatedById = currentTracks.associateBy { it.id }
55
+ playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
56
+ upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
57
+ }
58
+ rebuildQueueFromCurrentPosition()
51
59
  }
52
- rebuildQueueFromCurrentPosition()
53
60
  }
54
- }
55
61
 
56
62
  // ── Track queries ─────────────────────────────────────────────────────────
57
63
 
58
- suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> =
59
- withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
64
+ suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> = withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
60
65
 
61
- suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> =
62
- withPlayerContext { getTracksNeedingUrlsInternal() }
66
+ suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> = withPlayerContext { getTracksNeedingUrlsInternal() }
63
67
 
64
68
  internal fun TrackPlayerCore.getTracksNeedingUrlsInternal(): List<TrackItem> {
65
69
  val pid = currentPlaylistId ?: return emptyList()
66
70
  return playlistManager.getPlaylist(pid)?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
67
71
  }
68
72
 
69
- suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> =
70
- withPlayerContext { getNextTracksInternal(count) }
73
+ suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> = withPlayerContext { getNextTracksInternal(count) }
71
74
 
72
75
  internal fun TrackPlayerCore.getNextTracksInternal(count: Int): List<TrackItem> {
73
76
  val actualQueue = getActualQueueInternal()
@@ -84,15 +87,15 @@ suspend fun TrackPlayerCore.getCurrentTrackIndex(): Int = withPlayerContext { cu
84
87
  // ── URL lookahead ─────────────────────────────────────────────────────────
85
88
 
86
89
  internal fun TrackPlayerCore.checkUpcomingTracksForUrls(lookahead: Int = 5) {
87
- val upcomingTracks = if (currentTrackIndex < 0) {
88
- currentTracks.take(lookahead)
89
- } else {
90
- getNextTracksInternal(lookahead)
91
- }
90
+ val upcomingTracks =
91
+ if (currentTrackIndex < 0) {
92
+ currentTracks.take(lookahead)
93
+ } else {
94
+ getNextTracksInternal(lookahead)
95
+ }
92
96
  val currentTrack = getCurrentTrack()
93
97
  val currentNeedsUrl = currentTrack != null && currentTrack.url.isEmpty()
94
98
  val candidates = if (currentNeedsUrl) listOf(currentTrack!!) + upcomingTracks else upcomingTracks
95
99
  val needUrls = candidates.filter { it.url.isEmpty() }
96
100
  if (needUrls.isNotEmpty()) notifyTracksNeedUpdate(needUrls, lookahead)
97
101
  }
98
-
@@ -7,13 +7,13 @@ import com.margelo.nitro.nitroplayer.*
7
7
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
8
8
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
9
9
  import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
10
- import org.json.JSONArray
11
- import org.json.JSONObject
12
- import java.io.File
13
10
  import kotlinx.coroutines.CoroutineScope
14
11
  import kotlinx.coroutines.Dispatchers
15
12
  import kotlinx.coroutines.SupervisorJob
16
13
  import kotlinx.coroutines.launch
14
+ import org.json.JSONArray
15
+ import org.json.JSONObject
16
+ import java.io.File
17
17
 
18
18
  /**
19
19
  * Manages persistence of downloaded track metadata using file storage
@@ -236,9 +236,18 @@ class DownloadManagerCore private constructor(
236
236
 
237
237
  for (m in activeTasks.values) {
238
238
  when (m.state) {
239
- DownloadState.PENDING -> pendingCount++
240
- DownloadState.DOWNLOADING -> activeCount++
241
- DownloadState.FAILED -> failedCount++
239
+ DownloadState.PENDING -> {
240
+ pendingCount++
241
+ }
242
+
243
+ DownloadState.DOWNLOADING -> {
244
+ activeCount++
245
+ }
246
+
247
+ DownloadState.FAILED -> {
248
+ failedCount++
249
+ }
250
+
242
251
  else -> {}
243
252
  }
244
253
  totalBytes += m.totalBytes ?: 0.0