react-native-nitro-player 0.5.5 → 0.5.7
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 +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +43 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +8 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +345 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +43 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +76 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +9 -2
- package/ios/HybridTrackPlayer.swift +54 -1
- package/ios/core/TrackPlayerCore.swift +254 -2
- package/ios/download/DownloadDatabase.swift +79 -2
- package/ios/download/DownloadManagerCore.swift +81 -2
- package/ios/playlist/PlaylistManager.swift +68 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +47 -0
- package/lib/types/PlayerQueue.d.ts +5 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +104 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +160 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +8 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +6 -3
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +16 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +65 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +62 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +8 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +173 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +24 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +8 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +6 -2
- package/package.json +1 -1
- package/src/specs/TrackPlayer.nitro.ts +57 -0
- package/src/types/PlayerQueue.ts +5 -0
|
@@ -302,6 +302,66 @@ class PlaylistManager private constructor(
|
|
|
302
302
|
return true
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Update entire track objects across all playlists
|
|
307
|
+
* Matches by track.id and replaces the entire track object
|
|
308
|
+
* @param tracks List of full TrackItem objects to update
|
|
309
|
+
* @return Map of playlistId -> count of tracks updated
|
|
310
|
+
*/
|
|
311
|
+
fun updateTracks(tracks: List<TrackItem>): Map<String, Int> {
|
|
312
|
+
val tracksMap = tracks.associateBy { it.id }
|
|
313
|
+
val affectedPlaylists = mutableMapOf<String, Int>()
|
|
314
|
+
|
|
315
|
+
synchronized(playlists) {
|
|
316
|
+
playlists.forEach { (playlistId, playlist) ->
|
|
317
|
+
var updateCount = 0
|
|
318
|
+
val newTracks =
|
|
319
|
+
playlist.tracks
|
|
320
|
+
.map { track ->
|
|
321
|
+
tracksMap[track.id]?.also { updateCount++ } ?: track
|
|
322
|
+
}.toMutableList()
|
|
323
|
+
|
|
324
|
+
if (updateCount > 0) {
|
|
325
|
+
affectedPlaylists[playlistId] = updateCount
|
|
326
|
+
playlists[playlistId] = playlist.copy(tracks = newTracks)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (affectedPlaylists.isNotEmpty()) {
|
|
332
|
+
scheduleSave()
|
|
333
|
+
affectedPlaylists.keys.forEach { playlistId ->
|
|
334
|
+
notifyPlaylistChanged(playlistId, QueueOperation.UPDATE)
|
|
335
|
+
}
|
|
336
|
+
notifyPlaylistsChanged(QueueOperation.UPDATE)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return affectedPlaylists
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get tracks by IDs from all playlists
|
|
344
|
+
* @param trackIds List of track IDs to fetch
|
|
345
|
+
* @return List of matching TrackItem objects
|
|
346
|
+
*/
|
|
347
|
+
fun getTracksById(trackIds: List<String>): List<TrackItem> {
|
|
348
|
+
val trackIdSet = trackIds.toSet()
|
|
349
|
+
val foundTracks = mutableMapOf<String, TrackItem>()
|
|
350
|
+
|
|
351
|
+
synchronized(playlists) {
|
|
352
|
+
playlists.values.forEach { playlist ->
|
|
353
|
+
playlist.tracks.forEach { track ->
|
|
354
|
+
if (trackIdSet.contains(track.id) && !foundTracks.containsKey(track.id)) {
|
|
355
|
+
foundTracks[track.id] = track
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Return in same order as requested
|
|
362
|
+
return trackIds.mapNotNull { foundTracks[it] }
|
|
363
|
+
}
|
|
364
|
+
|
|
305
365
|
/**
|
|
306
366
|
* Load a playlist for playback (sets it as current)
|
|
307
367
|
*/
|
|
@@ -405,10 +465,11 @@ class PlaylistManager private constructor(
|
|
|
405
465
|
}
|
|
406
466
|
}
|
|
407
467
|
|
|
408
|
-
val wrapper =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
468
|
+
val wrapper =
|
|
469
|
+
JSONObject().apply {
|
|
470
|
+
put("playlists", jsonArray)
|
|
471
|
+
put("currentPlaylistId", currentPlaylistId)
|
|
472
|
+
}
|
|
412
473
|
NitroPlayerStorage.write(context, "playlists.json", wrapper.toString())
|
|
413
474
|
} catch (e: Exception) {
|
|
414
475
|
e.printStackTrace()
|
|
@@ -423,8 +484,12 @@ class PlaylistManager private constructor(
|
|
|
423
484
|
val wrapper = JSONObject(json)
|
|
424
485
|
val jsonArray = wrapper.optJSONArray("playlists") ?: JSONArray()
|
|
425
486
|
parseAndLoadPlaylists(jsonArray)
|
|
426
|
-
currentPlaylistId =
|
|
427
|
-
|
|
487
|
+
currentPlaylistId =
|
|
488
|
+
if (wrapper.isNull("currentPlaylistId")) {
|
|
489
|
+
null
|
|
490
|
+
} else {
|
|
491
|
+
wrapper.optString("currentPlaylistId", null.toString()).takeIf { it != "null" }
|
|
492
|
+
}
|
|
428
493
|
} catch (e: Exception) {
|
|
429
494
|
e.printStackTrace()
|
|
430
495
|
}
|
|
@@ -440,7 +505,11 @@ class PlaylistManager private constructor(
|
|
|
440
505
|
parseAndLoadPlaylists(jsonArray)
|
|
441
506
|
currentPlaylistId = prefs.getString("currentPlaylistId", null)
|
|
442
507
|
// Remove old SharedPreferences data to free space
|
|
443
|
-
prefs
|
|
508
|
+
prefs
|
|
509
|
+
.edit()
|
|
510
|
+
.remove("playlists")
|
|
511
|
+
.remove("currentPlaylistId")
|
|
512
|
+
.apply()
|
|
444
513
|
// Persist in new format
|
|
445
514
|
saveToFile()
|
|
446
515
|
} catch (e: Exception) {
|
|
@@ -9,7 +9,10 @@ object NitroPlayerStorage {
|
|
|
9
9
|
private const val DIR_NAME = "nitroplayer"
|
|
10
10
|
|
|
11
11
|
/** Reads the contents of [filename] from the NitroPlayer storage directory, or null if absent. */
|
|
12
|
-
fun read(
|
|
12
|
+
fun read(
|
|
13
|
+
context: Context,
|
|
14
|
+
filename: String,
|
|
15
|
+
): String? {
|
|
13
16
|
val file = File(storageDirectory(context), filename)
|
|
14
17
|
return if (file.exists()) {
|
|
15
18
|
try {
|
|
@@ -28,7 +31,11 @@ object NitroPlayerStorage {
|
|
|
28
31
|
* Writes to `<filename>.tmp` first, then renames — leaving the prior file
|
|
29
32
|
* untouched on failure (crash-safe).
|
|
30
33
|
*/
|
|
31
|
-
fun write(
|
|
34
|
+
fun write(
|
|
35
|
+
context: Context,
|
|
36
|
+
filename: String,
|
|
37
|
+
json: String,
|
|
38
|
+
) {
|
|
32
39
|
try {
|
|
33
40
|
val dir = storageDirectory(context)
|
|
34
41
|
dir.mkdirs()
|
|
@@ -88,7 +88,8 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
88
88
|
core.configure(
|
|
89
89
|
androidAutoEnabled: config.androidAutoEnabled,
|
|
90
90
|
carPlayEnabled: config.carPlayEnabled,
|
|
91
|
-
showInNotification: config.showInNotification
|
|
91
|
+
showInNotification: config.showInNotification,
|
|
92
|
+
lookaheadCount: config.lookaheadCount.map { Int($0) }
|
|
92
93
|
)
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -139,4 +140,56 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
139
140
|
func setVolume(volume: Double) throws -> Bool {
|
|
140
141
|
return core.setVolume(volume: volume)
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Lazy URL Loading
|
|
145
|
+
|
|
146
|
+
func updateTracks(tracks: [TrackItem]) throws -> Promise<Void> {
|
|
147
|
+
return Promise.async {
|
|
148
|
+
self.core.updateTracks(tracks: tracks)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func getTracksById(trackIds: [String]) throws -> Promise<[TrackItem]> {
|
|
153
|
+
return Promise.async {
|
|
154
|
+
return self.core.getTracksById(trackIds: trackIds)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func getTracksNeedingUrls() throws -> Promise<[TrackItem]> {
|
|
159
|
+
return Promise.async {
|
|
160
|
+
return self.core.getTracksNeedingUrls()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func getNextTracks(count: Double) throws -> Promise<[TrackItem]> {
|
|
165
|
+
return Promise.async {
|
|
166
|
+
return self.core.getNextTracks(count: Int(count))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func getCurrentTrackIndex() throws -> Promise<Double> {
|
|
171
|
+
return Promise.async {
|
|
172
|
+
return Double(self.core.getCurrentTrackIndex())
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func onTracksNeedUpdate(callback: @escaping ([TrackItem], Double) -> Void) throws {
|
|
177
|
+
core.addOnTracksNeedUpdateListener { tracks, lookahead in
|
|
178
|
+
callback(tracks, Double(lookahead))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func setPlaybackSpeed(speed: Double) throws -> Promise<Void> {
|
|
183
|
+
Promise.async{
|
|
184
|
+
self.core.setPlaybackSpeed(speed)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func getPlaybackSpeed() throws -> Promise<Double> {
|
|
190
|
+
return Promise.async{
|
|
191
|
+
return self.core.getPlaybackSpeed()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
}
|
|
142
195
|
}
|
|
@@ -50,6 +50,7 @@ class TrackPlayerCore: NSObject {
|
|
|
50
50
|
private var currentTracks: [TrackItem] = []
|
|
51
51
|
private var isManuallySeeked = false
|
|
52
52
|
private var currentRepeatMode: RepeatMode = .off
|
|
53
|
+
private var lookaheadCount: Int = 5 // Number of tracks to preload ahead
|
|
53
54
|
private var boundaryTimeObserver: Any?
|
|
54
55
|
private var currentItemObservers: [NSKeyValueObservation] = []
|
|
55
56
|
|
|
@@ -310,6 +311,9 @@ class TrackPlayerCore: NSObject {
|
|
|
310
311
|
if let player = player {
|
|
311
312
|
NitroPlayerLogger.log("TrackPlayerCore", "📋 Remaining items in queue: \(player.items().count)")
|
|
312
313
|
}
|
|
314
|
+
|
|
315
|
+
// Check if upcoming tracks need URLs
|
|
316
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
@objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
|
|
@@ -571,6 +575,14 @@ class TrackPlayerCore: NSObject {
|
|
|
571
575
|
}
|
|
572
576
|
}
|
|
573
577
|
}
|
|
578
|
+
|
|
579
|
+
func setPlaybackSpeed(_ speed: Double) {
|
|
580
|
+
player?.rate = Float(speed)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
func getPlaybackSpeed() -> Double {
|
|
584
|
+
return Double(player?.rate ?? 1.0)
|
|
585
|
+
}
|
|
574
586
|
|
|
575
587
|
private func loadPlaylistInternal(playlistId: String) {
|
|
576
588
|
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
|
|
@@ -596,6 +608,9 @@ class TrackPlayerCore: NSObject {
|
|
|
596
608
|
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
597
609
|
// Emit initial state (paused/stopped before play)
|
|
598
610
|
self.emitStateChange()
|
|
611
|
+
|
|
612
|
+
// Check if upcoming tracks need URLs
|
|
613
|
+
self.checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
599
614
|
} else {
|
|
600
615
|
NitroPlayerLogger.log("TrackPlayerCore", " ❌ Playlist NOT FOUND")
|
|
601
616
|
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
@@ -1301,6 +1316,9 @@ class TrackPlayerCore: NSObject {
|
|
|
1301
1316
|
queuePlayer.pause()
|
|
1302
1317
|
self.notifyPlaybackStateChange(.stopped, .end)
|
|
1303
1318
|
}
|
|
1319
|
+
|
|
1320
|
+
// Check if upcoming tracks need URLs
|
|
1321
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1304
1322
|
}
|
|
1305
1323
|
|
|
1306
1324
|
func skipToPrevious() {
|
|
@@ -1343,6 +1361,9 @@ class TrackPlayerCore: NSObject {
|
|
|
1343
1361
|
// Already at first track, restart it
|
|
1344
1362
|
queuePlayer.seek(to: .zero)
|
|
1345
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
// Check if upcoming tracks need URLs
|
|
1366
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1346
1367
|
}
|
|
1347
1368
|
|
|
1348
1369
|
func seek(position: Double) {
|
|
@@ -1469,9 +1490,14 @@ class TrackPlayerCore: NSObject {
|
|
|
1469
1490
|
func configure(
|
|
1470
1491
|
androidAutoEnabled: Bool?,
|
|
1471
1492
|
carPlayEnabled: Bool?,
|
|
1472
|
-
showInNotification: Bool
|
|
1493
|
+
showInNotification: Bool?,
|
|
1494
|
+
lookaheadCount: Int? = nil
|
|
1473
1495
|
) {
|
|
1474
1496
|
DispatchQueue.main.async { [weak self] in
|
|
1497
|
+
if let lookahead = lookaheadCount {
|
|
1498
|
+
self?.lookaheadCount = lookahead
|
|
1499
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: \(lookahead)")
|
|
1500
|
+
}
|
|
1475
1501
|
self?.mediaSessionManager?.configure(
|
|
1476
1502
|
androidAutoEnabled: androidAutoEnabled,
|
|
1477
1503
|
carPlayEnabled: carPlayEnabled,
|
|
@@ -1619,9 +1645,17 @@ class TrackPlayerCore: NSObject {
|
|
|
1619
1645
|
upNextQueue.removeAll()
|
|
1620
1646
|
currentTemporaryType = .none
|
|
1621
1647
|
|
|
1622
|
-
|
|
1648
|
+
let result = playFromIndexInternalWithResult(index: originalIndex)
|
|
1649
|
+
|
|
1650
|
+
// Check if upcoming tracks need URLs
|
|
1651
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1652
|
+
|
|
1653
|
+
return result
|
|
1623
1654
|
}
|
|
1624
1655
|
|
|
1656
|
+
// Check if upcoming tracks need URLs after any successful skip
|
|
1657
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
1658
|
+
|
|
1625
1659
|
return false
|
|
1626
1660
|
}
|
|
1627
1661
|
|
|
@@ -1857,6 +1891,224 @@ class TrackPlayerCore: NSObject {
|
|
|
1857
1891
|
return .none
|
|
1858
1892
|
}
|
|
1859
1893
|
|
|
1894
|
+
// MARK: - Lazy URL Loading Support
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Update entire track objects and rebuild queue if needed
|
|
1898
|
+
* Skips currently playing track to preserve gapless playback
|
|
1899
|
+
* CRITICAL: Invalidates preloaded assets and re-preloads for gapless
|
|
1900
|
+
*/
|
|
1901
|
+
func updateTracks(tracks: [TrackItem]) {
|
|
1902
|
+
DispatchQueue.main.async { [weak self] in
|
|
1903
|
+
guard let self = self else { return }
|
|
1904
|
+
|
|
1905
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: \(tracks.count) updates")
|
|
1906
|
+
|
|
1907
|
+
// Get current track ID to avoid updating it (preserves gapless playback)
|
|
1908
|
+
let currentTrackId = self.getCurrentTrack()?.id
|
|
1909
|
+
|
|
1910
|
+
// Filter out current track and validate
|
|
1911
|
+
let safeTracks = tracks.filter { track in
|
|
1912
|
+
switch true {
|
|
1913
|
+
case track.id == currentTrackId:
|
|
1914
|
+
NitroPlayerLogger.log(
|
|
1915
|
+
"TrackPlayerCore",
|
|
1916
|
+
"⚠️ Skipping update for currently playing track: \(track.id) (preserves gapless)")
|
|
1917
|
+
return false
|
|
1918
|
+
case track.url.isEmpty:
|
|
1919
|
+
NitroPlayerLogger.log(
|
|
1920
|
+
"TrackPlayerCore", "⚠️ Skipping track with empty URL: \(track.id)")
|
|
1921
|
+
return false
|
|
1922
|
+
default:
|
|
1923
|
+
return true
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
guard !safeTracks.isEmpty else {
|
|
1928
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
|
|
1929
|
+
return
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Invalidate preloaded assets for tracks with updated data
|
|
1933
|
+
// This is CRITICAL for gapless playback - old assets might use old URLs
|
|
1934
|
+
let updatedTrackIds = Set(safeTracks.map { $0.id })
|
|
1935
|
+
for trackId in updatedTrackIds {
|
|
1936
|
+
if self.preloadedAssets[trackId] != nil {
|
|
1937
|
+
NitroPlayerLogger.log(
|
|
1938
|
+
"TrackPlayerCore", "🗑️ Invalidating preloaded asset for track: \(trackId)")
|
|
1939
|
+
self.preloadedAssets.removeValue(forKey: trackId)
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Update in PlaylistManager
|
|
1944
|
+
let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
|
|
1945
|
+
|
|
1946
|
+
// Rebuild queue if current playlist was affected
|
|
1947
|
+
if let currentId = self.currentPlaylistId,
|
|
1948
|
+
let updateCount = affectedPlaylists[currentId]
|
|
1949
|
+
{
|
|
1950
|
+
NitroPlayerLogger.log(
|
|
1951
|
+
"TrackPlayerCore",
|
|
1952
|
+
"🔄 Rebuilding queue - \(updateCount) tracks updated in current playlist")
|
|
1953
|
+
|
|
1954
|
+
// This method preserves current item
|
|
1955
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1956
|
+
|
|
1957
|
+
// Re-preload upcoming tracks for gapless playback
|
|
1958
|
+
// CRITICAL: This restores gapless buffering after queue rebuild
|
|
1959
|
+
self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
|
|
1960
|
+
|
|
1961
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
NitroPlayerLogger.log(
|
|
1965
|
+
"TrackPlayerCore",
|
|
1966
|
+
"✅ Track updates complete - \(affectedPlaylists.count) playlists affected")
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Get tracks by IDs from all playlists
|
|
1972
|
+
*/
|
|
1973
|
+
func getTracksById(trackIds: [String]) -> [TrackItem] {
|
|
1974
|
+
if Thread.isMainThread {
|
|
1975
|
+
return playlistManager.getTracksById(trackIds: trackIds)
|
|
1976
|
+
} else {
|
|
1977
|
+
var tracks: [TrackItem] = []
|
|
1978
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1979
|
+
tracks = self?.playlistManager.getTracksById(trackIds: trackIds) ?? []
|
|
1980
|
+
}
|
|
1981
|
+
return tracks
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/**
|
|
1986
|
+
* Get tracks needing URLs from current playlist
|
|
1987
|
+
*/
|
|
1988
|
+
func getTracksNeedingUrls() -> [TrackItem] {
|
|
1989
|
+
if Thread.isMainThread {
|
|
1990
|
+
return getTracksNeedingUrlsInternal()
|
|
1991
|
+
} else {
|
|
1992
|
+
var tracks: [TrackItem] = []
|
|
1993
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1994
|
+
tracks = self?.getTracksNeedingUrlsInternal() ?? []
|
|
1995
|
+
}
|
|
1996
|
+
return tracks
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
private func getTracksNeedingUrlsInternal() -> [TrackItem] {
|
|
2001
|
+
guard let currentId = currentPlaylistId,
|
|
2002
|
+
let playlist = playlistManager.getPlaylist(playlistId: currentId)
|
|
2003
|
+
else {
|
|
2004
|
+
return []
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return playlist.tracks.filter { $0.url.isEmpty }
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* Get next N tracks from current position
|
|
2012
|
+
*/
|
|
2013
|
+
func getNextTracks(count: Int) -> [TrackItem] {
|
|
2014
|
+
if Thread.isMainThread {
|
|
2015
|
+
return getNextTracksInternal(count: count)
|
|
2016
|
+
} else {
|
|
2017
|
+
var tracks: [TrackItem] = []
|
|
2018
|
+
DispatchQueue.main.sync { [weak self] in
|
|
2019
|
+
tracks = self?.getNextTracksInternal(count: count) ?? []
|
|
2020
|
+
}
|
|
2021
|
+
return tracks
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
private func getNextTracksInternal(count: Int) -> [TrackItem] {
|
|
2026
|
+
let actualQueue = getActualQueueInternal()
|
|
2027
|
+
guard !actualQueue.isEmpty else { return [] }
|
|
2028
|
+
|
|
2029
|
+
guard let currentTrack = getCurrentTrack(),
|
|
2030
|
+
let currentIndex = actualQueue.firstIndex(where: { $0.id == currentTrack.id })
|
|
2031
|
+
else {
|
|
2032
|
+
return []
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
let startIndex = currentIndex + 1
|
|
2036
|
+
let endIndex = min(startIndex + count, actualQueue.count)
|
|
2037
|
+
|
|
2038
|
+
return startIndex < actualQueue.count ? Array(actualQueue[startIndex..<endIndex]) : []
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Get current track index in playlist
|
|
2043
|
+
*/
|
|
2044
|
+
func getCurrentTrackIndex() -> Int {
|
|
2045
|
+
if Thread.isMainThread {
|
|
2046
|
+
return currentTrackIndex
|
|
2047
|
+
} else {
|
|
2048
|
+
var index = -1
|
|
2049
|
+
DispatchQueue.main.sync { [weak self] in
|
|
2050
|
+
index = self?.currentTrackIndex ?? -1
|
|
2051
|
+
}
|
|
2052
|
+
return index
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Callback for tracks needing update
|
|
2058
|
+
*/
|
|
2059
|
+
typealias OnTracksNeedUpdateCallback = ([TrackItem], Int) -> Void
|
|
2060
|
+
|
|
2061
|
+
// Add to class properties
|
|
2062
|
+
private var onTracksNeedUpdateListeners: [(callback: OnTracksNeedUpdateCallback, isAlive: Bool)] =
|
|
2063
|
+
[]
|
|
2064
|
+
private let tracksNeedUpdateQueue = DispatchQueue(
|
|
2065
|
+
label: "com.nitroplayer.tracksneedupdate", attributes: .concurrent)
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Register listener for when tracks need update
|
|
2069
|
+
*/
|
|
2070
|
+
func addOnTracksNeedUpdateListener(callback: @escaping OnTracksNeedUpdateCallback) {
|
|
2071
|
+
tracksNeedUpdateQueue.async(flags: .barrier) { [weak self] in
|
|
2072
|
+
self?.onTracksNeedUpdateListeners.append((callback: callback, isAlive: true))
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Notify listeners that tracks need updating
|
|
2078
|
+
*/
|
|
2079
|
+
private func notifyTracksNeedUpdate(tracks: [TrackItem], lookahead: Int) {
|
|
2080
|
+
tracksNeedUpdateQueue.async(flags: .barrier) { [weak self] in
|
|
2081
|
+
guard let self = self else { return }
|
|
2082
|
+
|
|
2083
|
+
// Clean up dead listeners
|
|
2084
|
+
self.onTracksNeedUpdateListeners.removeAll { !$0.isAlive }
|
|
2085
|
+
let liveCallbacks = self.onTracksNeedUpdateListeners.map { $0.callback }
|
|
2086
|
+
|
|
2087
|
+
if !liveCallbacks.isEmpty {
|
|
2088
|
+
DispatchQueue.main.async {
|
|
2089
|
+
for callback in liveCallbacks {
|
|
2090
|
+
callback(tracks, lookahead)
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Check if upcoming tracks need URLs and notify listeners
|
|
2099
|
+
* Call this in playerItemDidPlayToEndTime or after skip operations
|
|
2100
|
+
*/
|
|
2101
|
+
private func checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
2102
|
+
let nextTracks = getNextTracksInternal(count: lookahead)
|
|
2103
|
+
let tracksNeedingUrls = nextTracks.filter { $0.url.isEmpty }
|
|
2104
|
+
|
|
2105
|
+
if !tracksNeedingUrls.isEmpty {
|
|
2106
|
+
NitroPlayerLogger.log(
|
|
2107
|
+
"TrackPlayerCore", "⚠️ \(tracksNeedingUrls.count) upcoming tracks need URLs")
|
|
2108
|
+
notifyTracksNeedUpdate(tracks: tracksNeedingUrls, lookahead: lookahead)
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1860
2112
|
// MARK: - Cleanup
|
|
1861
2113
|
|
|
1862
2114
|
deinit {
|
|
@@ -490,6 +490,11 @@ final class DownloadDatabase {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
private func trackItemToRecord(_ track: TrackItem) -> TrackItemRecord {
|
|
493
|
+
var extraPayloadDict: [String: Any]? = nil
|
|
494
|
+
if let extraPayload = track.extraPayload {
|
|
495
|
+
extraPayloadDict = extraPayload.toDictionary()
|
|
496
|
+
}
|
|
497
|
+
|
|
493
498
|
return TrackItemRecord(
|
|
494
499
|
id: track.id,
|
|
495
500
|
title: track.title,
|
|
@@ -497,11 +502,28 @@ final class DownloadDatabase {
|
|
|
497
502
|
album: track.album,
|
|
498
503
|
duration: track.duration,
|
|
499
504
|
url: track.url,
|
|
500
|
-
artwork: variantToString(track.artwork)
|
|
505
|
+
artwork: variantToString(track.artwork),
|
|
506
|
+
extraPayload: extraPayloadDict
|
|
501
507
|
)
|
|
502
508
|
}
|
|
503
509
|
|
|
504
510
|
private func recordToTrackItem(_ record: TrackItemRecord) -> TrackItem {
|
|
511
|
+
var extraPayload: AnyMap? = nil
|
|
512
|
+
if let extraPayloadDict = record.extraPayload {
|
|
513
|
+
extraPayload = AnyMap()
|
|
514
|
+
for (key, value) in extraPayloadDict {
|
|
515
|
+
if let stringValue = value as? String {
|
|
516
|
+
extraPayload?.setString(key: key, value: stringValue)
|
|
517
|
+
} else if let doubleValue = value as? Double {
|
|
518
|
+
extraPayload?.setDouble(key: key, value: doubleValue)
|
|
519
|
+
} else if let intValue = value as? Int {
|
|
520
|
+
extraPayload?.setDouble(key: key, value: Double(intValue))
|
|
521
|
+
} else if let boolValue = value as? Bool {
|
|
522
|
+
extraPayload?.setBoolean(key: key, value: boolValue)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
505
527
|
return TrackItem(
|
|
506
528
|
id: record.id,
|
|
507
529
|
title: record.title,
|
|
@@ -510,7 +532,7 @@ final class DownloadDatabase {
|
|
|
510
532
|
duration: record.duration,
|
|
511
533
|
url: record.url,
|
|
512
534
|
artwork: stringToVariant(record.artwork),
|
|
513
|
-
extraPayload:
|
|
535
|
+
extraPayload: extraPayload
|
|
514
536
|
)
|
|
515
537
|
}
|
|
516
538
|
|
|
@@ -547,4 +569,59 @@ private struct TrackItemRecord: Codable {
|
|
|
547
569
|
let duration: Double
|
|
548
570
|
let url: String
|
|
549
571
|
let artwork: String?
|
|
572
|
+
let extraPayload: [String: Any]?
|
|
573
|
+
|
|
574
|
+
enum CodingKeys: String, CodingKey {
|
|
575
|
+
case id, title, artist, album, duration, url, artwork, extraPayload
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Manual encoding to handle [String: Any]
|
|
579
|
+
func encode(to encoder: Encoder) throws {
|
|
580
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
581
|
+
try container.encode(id, forKey: .id)
|
|
582
|
+
try container.encode(title, forKey: .title)
|
|
583
|
+
try container.encode(artist, forKey: .artist)
|
|
584
|
+
try container.encode(album, forKey: .album)
|
|
585
|
+
try container.encode(duration, forKey: .duration)
|
|
586
|
+
try container.encode(url, forKey: .url)
|
|
587
|
+
try container.encodeIfPresent(artwork, forKey: .artwork)
|
|
588
|
+
|
|
589
|
+
if let extraPayload = extraPayload {
|
|
590
|
+
let jsonData = try JSONSerialization.data(withJSONObject: extraPayload)
|
|
591
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
592
|
+
try container.encode(jsonString, forKey: .extraPayload)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Manual decoding to handle [String: Any]
|
|
598
|
+
init(from decoder: Decoder) throws {
|
|
599
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
600
|
+
id = try container.decode(String.self, forKey: .id)
|
|
601
|
+
title = try container.decode(String.self, forKey: .title)
|
|
602
|
+
artist = try container.decode(String.self, forKey: .artist)
|
|
603
|
+
album = try container.decode(String.self, forKey: .album)
|
|
604
|
+
duration = try container.decode(Double.self, forKey: .duration)
|
|
605
|
+
url = try container.decode(String.self, forKey: .url)
|
|
606
|
+
artwork = try container.decodeIfPresent(String.self, forKey: .artwork)
|
|
607
|
+
|
|
608
|
+
if let jsonString = try? container.decodeIfPresent(String.self, forKey: .extraPayload),
|
|
609
|
+
let jsonData = jsonString.data(using: .utf8) {
|
|
610
|
+
extraPayload = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
|
611
|
+
} else {
|
|
612
|
+
extraPayload = nil
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Initializer for code creation
|
|
617
|
+
init(id: String, title: String, artist: String, album: String, duration: Double, url: String, artwork: String?, extraPayload: [String: Any]?) {
|
|
618
|
+
self.id = id
|
|
619
|
+
self.title = title
|
|
620
|
+
self.artist = artist
|
|
621
|
+
self.album = album
|
|
622
|
+
self.duration = duration
|
|
623
|
+
self.url = url
|
|
624
|
+
self.artwork = artwork
|
|
625
|
+
self.extraPayload = extraPayload
|
|
626
|
+
}
|
|
550
627
|
}
|