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.
- package/README.md +47 -46
- package/android/src/main/AndroidManifest.xml +14 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +70 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +70 -65
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +64 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +54 -28
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +12 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +169 -98
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +30 -178
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +40 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- package/ios/core/TrackPlayerQueue.swift +27 -18
- package/ios/core/TrackPlayerQueueBuild.swift +16 -5
- package/ios/equalizer/EqualizerCore.swift +39 -34
- package/lib/hooks/useEqualizer.js +10 -5
- package/lib/specs/Equalizer.nitro.d.ts +1 -1
- package/lib/types/EqualizerTypes.d.ts +3 -3
- package/package.json +5 -5
- package/src/hooks/useEqualizer.ts +25 -17
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
- package/src/specs/DownloadManager.nitro.ts +4 -2
- package/src/specs/Equalizer.nitro.ts +5 -3
- package/src/specs/TrackPlayer.nitro.ts +18 -6
- 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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
91
|
-
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT &&
|
|
92
|
-
|
|
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) {
|
|
124
|
-
|
|
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
|
|
128
|
-
|
|
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
|
|
166
|
+
val targetTrack = actualQueue[index]
|
|
136
167
|
playNextStack.clear()
|
|
137
|
-
|
|
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()
|
|
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 =
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
63
|
-
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT &&
|
|
64
|
-
|
|
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 =
|
|
76
|
-
|
|
77
|
-
|
|
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 =
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
14
|
-
|
|
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) =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
41
|
-
|
|
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 =
|
|
53
|
-
|
|
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 =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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>) =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 ->
|
|
240
|
-
|
|
241
|
-
|
|
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
|