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.
Files changed (36) hide show
  1. package/README.md +2 -0
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +43 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +8 -2
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +345 -4
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +43 -10
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +76 -7
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +9 -2
  8. package/ios/HybridTrackPlayer.swift +54 -1
  9. package/ios/core/TrackPlayerCore.swift +254 -2
  10. package/ios/download/DownloadDatabase.swift +79 -2
  11. package/ios/download/DownloadManagerCore.swift +81 -2
  12. package/ios/playlist/PlaylistManager.swift +68 -0
  13. package/lib/specs/TrackPlayer.nitro.d.ts +47 -0
  14. package/lib/types/PlayerQueue.d.ts +5 -0
  15. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  16. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +104 -0
  17. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +160 -0
  18. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +8 -0
  19. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +7 -3
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__double.kt +80 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +37 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +6 -3
  23. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +16 -0
  24. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +65 -0
  25. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +62 -0
  26. package/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
  27. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double.swift +47 -0
  28. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +8 -0
  29. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +173 -0
  30. package/nitrogen/generated/ios/swift/PlayerConfig.swift +24 -1
  31. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  32. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +8 -0
  33. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +6 -2
  34. package/package.json +1 -1
  35. package/src/specs/TrackPlayer.nitro.ts +57 -0
  36. package/src/types/PlayerQueue.ts +5 -0
package/README.md CHANGED
@@ -50,6 +50,8 @@ npm install react-native-nitro-modules
50
50
  | `skipToNext()` | Both | Skips to the next track in the queue. |
51
51
  | `skipToPrevious()` | Both | Skips to the previous track. |
52
52
  | `seek(position)` | Both | Seeks to a specific time position in seconds. |
53
+ | `setPlaybackSpeed(speed)` | Both | **Async**. Sets playback speed (e.g. 0.5x, 1x, 1.5x, 2x). |
54
+ | `getPlaybackSpeed()` | Both | **Async**. Gets the current playback speed. |
53
55
  | `setVolume(0-100)` | Both | Sets playback volume (0-100). |
54
56
  | `setRepeatMode(mode)` | Both | Sets repeat mode (`off`, `track`, `Playlist`). |
55
57
  | `addToUpNext(id)` | Both | **Async**. Adds a track to the "up next" queue (FIFO). |
@@ -106,6 +106,7 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
106
106
  androidAutoEnabled = config.androidAutoEnabled,
107
107
  carPlayEnabled = config.carPlayEnabled,
108
108
  showInNotification = config.showInNotification,
109
+ lookaheadCount = config.lookaheadCount?.toInt(),
109
110
  )
110
111
  }
111
112
 
@@ -127,4 +128,46 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
127
128
  Promise.async {
128
129
  core.skipToIndex(index.toInt())
129
130
  }
131
+
132
+ override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> =
133
+ Promise.async {
134
+ core.updateTracks(tracks.toList())
135
+ }
136
+
137
+ override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> =
138
+ Promise.async {
139
+ core.getTracksById(trackIds.toList()).toTypedArray()
140
+ }
141
+
142
+ override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> =
143
+ Promise.async {
144
+ core.getTracksNeedingUrls().toTypedArray()
145
+ }
146
+
147
+ override fun getNextTracks(count: Double): Promise<Array<TrackItem>> =
148
+ Promise.async {
149
+ core.getNextTracks(count.toInt()).toTypedArray()
150
+ }
151
+
152
+ override fun getCurrentTrackIndex(): Promise<Double> =
153
+ Promise.async {
154
+ core.getCurrentTrackIndex().toDouble()
155
+ }
156
+
157
+ override fun setPlaybackSpeed(speed: Double): Promise<Unit> =
158
+ Promise.async {
159
+ core.setPlayBackSpeed(speed)
160
+ Unit
161
+ }
162
+
163
+ override fun getPlaybackSpeed(): Promise<Double> =
164
+ Promise.async {
165
+ core.getPlayBackSpeed()
166
+ }
167
+
168
+ override fun onTracksNeedUpdate(callback: (tracks: Array<TrackItem>, lookahead: Double) -> Unit) {
169
+ core.addOnTracksNeedUpdateListener { tracks, lookahead ->
170
+ callback(tracks.toTypedArray(), lookahead.toDouble())
171
+ }
172
+ }
130
173
  }
@@ -12,7 +12,10 @@ object NitroPlayerLogger {
12
12
  * Use trailing lambda syntax: NitroPlayerLogger.log("Tag") { "msg $value" }
13
13
  * The lambda is inlined (no heap allocation) and skipped entirely when disabled.
14
14
  */
15
- inline fun log(header: String = "NitroPlayer", message: () -> String) {
15
+ inline fun log(
16
+ header: String = "NitroPlayer",
17
+ message: () -> String,
18
+ ) {
16
19
  if (isEnabled) {
17
20
  Log.d(header, message())
18
21
  }
@@ -23,7 +26,10 @@ object NitroPlayerLogger {
23
26
  * Note: the String is evaluated at the call site before this function runs.
24
27
  * Migrate to the lambda overload for hot paths.
25
28
  */
26
- fun log(header: String = "NitroPlayer", message: String) {
29
+ fun log(
30
+ header: String = "NitroPlayer",
31
+ message: String,
32
+ ) {
27
33
  if (isEnabled) {
28
34
  Log.d(header, message)
29
35
  }
@@ -83,6 +83,7 @@ class TrackPlayerCore private constructor(
83
83
  Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Double, Double, Boolean?) -> Unit>>())
84
84
 
85
85
  private var currentRepeatMode: RepeatMode = RepeatMode.OFF
86
+ private var lookaheadCount: Int = 5 // Number of tracks to preload ahead
86
87
 
87
88
  // Temporary tracks for addToUpNext and playNext
88
89
  private var playNextStack: MutableList<TrackItem> = mutableListOf() // LIFO - last added plays first
@@ -283,6 +284,9 @@ class TrackPlayerCore private constructor(
283
284
  }
284
285
  notifyTrackChange(track, r)
285
286
  mediaSessionManager?.onTrackChanged()
287
+
288
+ // Check if upcoming tracks need URLs
289
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
286
290
  }
287
291
  }
288
292
 
@@ -380,6 +384,9 @@ class TrackPlayerCore private constructor(
380
384
  if (playlist != null) {
381
385
  currentPlaylistId = playlistId
382
386
  updatePlayerQueue(playlist.tracks)
387
+
388
+ // Check if upcoming tracks need URLs
389
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
383
390
  }
384
391
  }
385
392
  }
@@ -641,6 +648,9 @@ class TrackPlayerCore private constructor(
641
648
  handler.post {
642
649
  if (player.hasNextMediaItem()) {
643
650
  player.seekToNextMediaItem()
651
+
652
+ // Check if upcoming tracks need URLs
653
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
644
654
  }
645
655
  }
646
656
  }
@@ -684,6 +694,9 @@ class TrackPlayerCore private constructor(
684
694
  NitroPlayerLogger.log("TrackPlayerCore", "🔄 TrackPlayerCore: Already at first track, seeking to beginning")
685
695
  player.seekTo(0)
686
696
  }
697
+
698
+ // Check if upcoming tracks need URLs
699
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
687
700
  }
688
701
  }
689
702
 
@@ -698,10 +711,11 @@ class TrackPlayerCore private constructor(
698
711
  currentRepeatMode = mode
699
712
  if (::player.isInitialized) {
700
713
  handler.post {
701
- player.repeatMode = when (mode) {
702
- RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
703
- else -> Player.REPEAT_MODE_OFF
704
- }
714
+ player.repeatMode =
715
+ when (mode) {
716
+ RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
717
+ else -> Player.REPEAT_MODE_OFF
718
+ }
705
719
  }
706
720
  }
707
721
  NitroPlayerLogger.log("TrackPlayerCore", "🔁 setRepeatMode: $mode")
@@ -810,11 +824,16 @@ class TrackPlayerCore private constructor(
810
824
  androidAutoEnabled: Boolean?,
811
825
  carPlayEnabled: Boolean?,
812
826
  showInNotification: Boolean?,
827
+ lookaheadCount: Int? = null,
813
828
  ) {
814
829
  handler.post {
815
830
  androidAutoEnabled?.let {
816
831
  NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it
817
832
  }
833
+ lookaheadCount?.let {
834
+ this.lookaheadCount = it
835
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: $it")
836
+ }
818
837
  mediaSessionManager?.configure(
819
838
  androidAutoEnabled,
820
839
  carPlayEnabled,
@@ -1014,9 +1033,16 @@ class TrackPlayerCore private constructor(
1014
1033
  currentTemporaryType = TemporaryType.NONE
1015
1034
 
1016
1035
  rebuildQueueAndPlayFromIndex(originalIndex)
1036
+
1037
+ // Check if upcoming tracks need URLs
1038
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
1039
+
1017
1040
  return true
1018
1041
  }
1019
1042
 
1043
+ // Check if upcoming tracks need URLs after any successful skip
1044
+ checkUpcomingTracksForUrls(lookahead = lookaheadCount)
1045
+
1020
1046
  return false
1021
1047
  }
1022
1048
 
@@ -1467,4 +1493,319 @@ class TrackPlayerCore private constructor(
1467
1493
 
1468
1494
  return queue
1469
1495
  }
1496
+
1497
+ // MARK: - Lazy URL Loading Support
1498
+
1499
+ /**
1500
+ * Update entire track objects and rebuild queue if needed
1501
+ * Skips currently playing track to preserve gapless playback
1502
+ */
1503
+ fun updateTracks(tracks: List<TrackItem>) {
1504
+ handler.post {
1505
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: ${tracks.size} updates")
1506
+
1507
+ // Get current track ID to avoid updating it (preserves gapless playback)
1508
+ val currentTrackId = getCurrentTrack()?.id
1509
+
1510
+ // Filter out current track and validate
1511
+ val safeTracks =
1512
+ tracks.filter { track ->
1513
+ when {
1514
+ track.id == currentTrackId -> {
1515
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping update for currently playing track: ${track.id} (preserves gapless)")
1516
+ false
1517
+ }
1518
+
1519
+ track.url.isEmpty() -> {
1520
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping track with empty URL: ${track.id}")
1521
+ false
1522
+ }
1523
+
1524
+ else -> {
1525
+ true
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ if (safeTracks.isEmpty()) {
1531
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
1532
+ return@post
1533
+ }
1534
+
1535
+ // Update in PlaylistManager
1536
+ val affectedPlaylists = playlistManager.updateTracks(safeTracks)
1537
+
1538
+ // Rebuild queue if current playlist was affected
1539
+ if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
1540
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Rebuilding queue - ${affectedPlaylists[currentPlaylistId]} tracks updated in current playlist")
1541
+
1542
+ // This method preserves current item and gapless buffering
1543
+ rebuildQueueFromCurrentPosition()
1544
+
1545
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
1546
+ }
1547
+
1548
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Track updates complete - ${affectedPlaylists.size} playlists affected")
1549
+ }
1550
+ }
1551
+
1552
+ /**
1553
+ * Get tracks by IDs from all playlists
1554
+ */
1555
+ fun getTracksById(trackIds: List<String>): List<TrackItem> {
1556
+ if (android.os.Looper.myLooper() == handler.looper) {
1557
+ return playlistManager.getTracksById(trackIds)
1558
+ }
1559
+
1560
+ val latch = CountDownLatch(1)
1561
+ var result: List<TrackItem>? = null
1562
+
1563
+ handler.post {
1564
+ try {
1565
+ result = playlistManager.getTracksById(trackIds)
1566
+ } finally {
1567
+ latch.countDown()
1568
+ }
1569
+ }
1570
+
1571
+ try {
1572
+ latch.await(5, TimeUnit.SECONDS)
1573
+ } catch (e: InterruptedException) {
1574
+ Thread.currentThread().interrupt()
1575
+ }
1576
+
1577
+ return result ?: emptyList()
1578
+ }
1579
+
1580
+ /**
1581
+ * Get tracks needing URLs from current playlist
1582
+ */
1583
+ fun getTracksNeedingUrls(): List<TrackItem> {
1584
+ if (android.os.Looper.myLooper() == handler.looper) {
1585
+ return getTracksNeedingUrlsInternal()
1586
+ }
1587
+
1588
+ val latch = CountDownLatch(1)
1589
+ var result: List<TrackItem>? = null
1590
+
1591
+ handler.post {
1592
+ try {
1593
+ result = getTracksNeedingUrlsInternal()
1594
+ } finally {
1595
+ latch.countDown()
1596
+ }
1597
+ }
1598
+
1599
+ try {
1600
+ latch.await(5, TimeUnit.SECONDS)
1601
+ } catch (e: InterruptedException) {
1602
+ Thread.currentThread().interrupt()
1603
+ }
1604
+
1605
+ return result ?: emptyList()
1606
+ }
1607
+
1608
+ private fun getTracksNeedingUrlsInternal(): List<TrackItem> {
1609
+ if (currentPlaylistId == null) return emptyList()
1610
+
1611
+ val playlist = playlistManager.getPlaylist(currentPlaylistId!!)
1612
+ return playlist?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
1613
+ }
1614
+
1615
+ /**
1616
+ * Get next N tracks from current position
1617
+ */
1618
+ fun getNextTracks(count: Int): List<TrackItem> {
1619
+ if (android.os.Looper.myLooper() == handler.looper) {
1620
+ return getNextTracksInternal(count)
1621
+ }
1622
+
1623
+ val latch = CountDownLatch(1)
1624
+ var result: List<TrackItem>? = null
1625
+
1626
+ handler.post {
1627
+ try {
1628
+ result = getNextTracksInternal(count)
1629
+ } finally {
1630
+ latch.countDown()
1631
+ }
1632
+ }
1633
+
1634
+ try {
1635
+ latch.await(5, TimeUnit.SECONDS)
1636
+ } catch (e: InterruptedException) {
1637
+ Thread.currentThread().interrupt()
1638
+ }
1639
+
1640
+ return result ?: emptyList()
1641
+ }
1642
+
1643
+ private fun getNextTracksInternal(count: Int): List<TrackItem> {
1644
+ val actualQueue = getActualQueueInternal()
1645
+ if (actualQueue.isEmpty()) return emptyList()
1646
+
1647
+ val currentIndex = actualQueue.indexOfFirst { it.id == getCurrentTrack()?.id }
1648
+ if (currentIndex == -1) return emptyList()
1649
+
1650
+ val startIndex = currentIndex + 1
1651
+ val endIndex = minOf(startIndex + count, actualQueue.size)
1652
+
1653
+ return if (startIndex < actualQueue.size) {
1654
+ actualQueue.subList(startIndex, endIndex)
1655
+ } else {
1656
+ emptyList()
1657
+ }
1658
+ }
1659
+
1660
+ /**
1661
+ * Get current track index in playlist
1662
+ */
1663
+ fun getCurrentTrackIndex(): Int {
1664
+ if (android.os.Looper.myLooper() == handler.looper) {
1665
+ return currentTrackIndex
1666
+ }
1667
+
1668
+ val latch = CountDownLatch(1)
1669
+ var result = -1
1670
+
1671
+ handler.post {
1672
+ try {
1673
+ result = currentTrackIndex
1674
+ } finally {
1675
+ latch.countDown()
1676
+ }
1677
+ }
1678
+
1679
+ try {
1680
+ latch.await(5, TimeUnit.SECONDS)
1681
+ } catch (e: InterruptedException) {
1682
+ Thread.currentThread().interrupt()
1683
+ }
1684
+
1685
+ return result
1686
+ }
1687
+
1688
+ /**
1689
+ * Callback interface for tracks needing update
1690
+ */
1691
+ fun interface OnTracksNeedUpdateListener {
1692
+ fun onTracksNeedUpdate(
1693
+ tracks: List<TrackItem>,
1694
+ lookahead: Int,
1695
+ )
1696
+ }
1697
+
1698
+ // Add to class properties
1699
+ private val onTracksNeedUpdateListeners = mutableListOf<WeakReference<OnTracksNeedUpdateListener>>()
1700
+
1701
+ /**
1702
+ * Register listener for when tracks need update
1703
+ */
1704
+ fun addOnTracksNeedUpdateListener(listener: OnTracksNeedUpdateListener) {
1705
+ handler.post {
1706
+ onTracksNeedUpdateListeners.add(WeakReference(listener))
1707
+ }
1708
+ }
1709
+
1710
+ /**
1711
+ * Remove listener
1712
+ */
1713
+ fun removeOnTracksNeedUpdateListener(listener: OnTracksNeedUpdateListener) {
1714
+ handler.post {
1715
+ onTracksNeedUpdateListeners.removeAll { it.get() == listener || it.get() == null }
1716
+ }
1717
+ }
1718
+
1719
+ /**
1720
+ * Notify listeners that tracks need updating
1721
+ * Called internally when moving to next track and upcoming tracks have empty URLs
1722
+ */
1723
+ private fun notifyTracksNeedUpdate(
1724
+ tracks: List<TrackItem>,
1725
+ lookahead: Int,
1726
+ ) {
1727
+ val liveCallbacks =
1728
+ synchronized(onTracksNeedUpdateListeners) {
1729
+ onTracksNeedUpdateListeners.removeAll { it.get() == null }
1730
+ onTracksNeedUpdateListeners.mapNotNull { it.get() }
1731
+ }
1732
+
1733
+ handler.post {
1734
+ for (callback in liveCallbacks) {
1735
+ try {
1736
+ callback.onTracksNeedUpdate(tracks, lookahead)
1737
+ } catch (e: Exception) {
1738
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Error in onTracksNeedUpdate listener: ${e.message}")
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+
1744
+ /**
1745
+ * Check if upcoming tracks need URLs and notify listeners
1746
+ * Call this in onMediaItemTransition or after skipTo operations
1747
+ */
1748
+ private fun checkUpcomingTracksForUrls(lookahead: Int = 5) {
1749
+ val nextTracks = getNextTracksInternal(lookahead)
1750
+ val tracksNeedingUrls = nextTracks.filter { it.url.isEmpty() }
1751
+
1752
+ if (tracksNeedingUrls.isNotEmpty()) {
1753
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ ${tracksNeedingUrls.size} upcoming tracks need URLs")
1754
+ notifyTracksNeedUpdate(tracksNeedingUrls, lookahead)
1755
+ }
1756
+ }
1757
+
1758
+ fun setPlayBackSpeed(speed: Double) {
1759
+ if (android.os.Looper.myLooper() == handler.looper) {
1760
+ setPlayBackSpeedInternal(speed)
1761
+ return
1762
+ }
1763
+ val latch = CountDownLatch(1)
1764
+ handler.post {
1765
+ try {
1766
+ setPlayBackSpeedInternal(speed)
1767
+ } finally {
1768
+ latch.countDown()
1769
+ }
1770
+ }
1771
+ try {
1772
+ latch.await(5, TimeUnit.SECONDS)
1773
+ } catch (e: InterruptedException) {
1774
+ Thread.currentThread().interrupt()
1775
+ }
1776
+ }
1777
+
1778
+ private fun setPlayBackSpeedInternal(speed: Double) {
1779
+ if (::player.isInitialized) {
1780
+ player.setPlaybackSpeed(speed.toFloat())
1781
+ }
1782
+ }
1783
+
1784
+ fun getPlayBackSpeed(): Double {
1785
+ if (android.os.Looper.myLooper() == handler.looper) {
1786
+ return getPlayBackSpeedInternal()
1787
+ }
1788
+ val latch = CountDownLatch(1)
1789
+ var result = 1.0
1790
+ handler.post {
1791
+ try {
1792
+ result = getPlayBackSpeedInternal()
1793
+ } finally {
1794
+ latch.countDown()
1795
+ }
1796
+ }
1797
+ try {
1798
+ latch.await(5, TimeUnit.SECONDS)
1799
+ } catch (e: InterruptedException) {
1800
+ Thread.currentThread().interrupt()
1801
+ }
1802
+ return result
1803
+ }
1804
+
1805
+ private fun getPlayBackSpeedInternal(): Double =
1806
+ if (::player.isInitialized) {
1807
+ player.playbackParameters.speed.toDouble()
1808
+ } else {
1809
+ 1.0
1810
+ }
1470
1811
  }
@@ -1,6 +1,7 @@
1
1
  package com.margelo.nitro.nitroplayer.download
2
2
 
3
3
  import android.content.Context
4
+ import com.margelo.nitro.core.AnyMap
4
5
  import com.margelo.nitro.core.NullType
5
6
  import com.margelo.nitro.nitroplayer.*
6
7
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
@@ -18,6 +19,7 @@ class DownloadDatabase private constructor(
18
19
  ) {
19
20
  companion object {
20
21
  private const val TAG = "DownloadDatabase"
22
+
21
23
  // Legacy SharedPreferences keys (migration only)
22
24
  private const val LEGACY_PREFS_NAME = "NitroPlayerDownloads"
23
25
  private const val LEGACY_KEY_DOWNLOADED_TRACKS = "downloaded_tracks"
@@ -304,10 +306,11 @@ class DownloadDatabase private constructor(
304
306
  playlistJson.put(playlistId, JSONArray(trackIds.toList()))
305
307
  }
306
308
 
307
- val wrapper = JSONObject().apply {
308
- put("downloadedTracks", tracksJson)
309
- put("playlistTracks", playlistJson)
310
- }
309
+ val wrapper =
310
+ JSONObject().apply {
311
+ put("downloadedTracks", tracksJson)
312
+ put("playlistTracks", playlistJson)
313
+ }
311
314
  NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
312
315
  } catch (e: Exception) {
313
316
  e.printStackTrace()
@@ -390,8 +393,14 @@ class DownloadDatabase private constructor(
390
393
  }
391
394
 
392
395
  // Conversion Helpers
393
- private fun trackItemToRecord(track: TrackItem): TrackItemRecord =
394
- TrackItemRecord(
396
+ private fun trackItemToRecord(track: TrackItem): TrackItemRecord {
397
+ val extraPayloadJson =
398
+ track.extraPayload?.let { payload ->
399
+ val extraPayloadMap = payload.toHashMap()
400
+ JSONObject(extraPayloadMap)
401
+ }
402
+
403
+ return TrackItemRecord(
395
404
  id = track.id,
396
405
  title = track.title,
397
406
  artist = track.artist,
@@ -399,7 +408,9 @@ class DownloadDatabase private constructor(
399
408
  duration = track.duration,
400
409
  url = track.url,
401
410
  artwork = track.artwork?.asSecondOrNull(),
411
+ extraPayload = extraPayloadJson,
402
412
  )
413
+ }
403
414
 
404
415
  private fun recordToTrackItem(record: TrackItemRecord): TrackItem {
405
416
  val artworkVariant =
@@ -409,6 +420,21 @@ class DownloadDatabase private constructor(
409
420
  null
410
421
  }
411
422
 
423
+ val extraPayload: AnyMap? =
424
+ record.extraPayload?.let { extraPayloadJson ->
425
+ val map = AnyMap()
426
+ val keyIterator = extraPayloadJson.keys()
427
+ while (keyIterator.hasNext()) {
428
+ val key = keyIterator.next()
429
+ when (val value = extraPayloadJson.get(key)) {
430
+ is String -> map.setString(key, value)
431
+ is Number -> map.setDouble(key, value.toDouble())
432
+ is Boolean -> map.setBoolean(key, value)
433
+ }
434
+ }
435
+ map
436
+ }
437
+
412
438
  return TrackItem(
413
439
  id = record.id,
414
440
  title = record.title,
@@ -417,7 +443,7 @@ class DownloadDatabase private constructor(
417
443
  duration = record.duration,
418
444
  url = record.url,
419
445
  artwork = artworkVariant,
420
- extraPayload = null,
446
+ extraPayload = extraPayload,
421
447
  )
422
448
  }
423
449
 
@@ -440,15 +466,14 @@ class DownloadDatabase private constructor(
440
466
  )
441
467
  }
442
468
 
443
- private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist {
444
- return Playlist(
469
+ private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist =
470
+ Playlist(
445
471
  id = playlist.id,
446
472
  name = playlist.name,
447
473
  description = null,
448
474
  artwork = null,
449
475
  tracks = playlist.tracks.toTypedArray(),
450
476
  )
451
- }
452
477
  }
453
478
 
454
479
  // Internal record classes
@@ -494,6 +519,7 @@ internal data class TrackItemRecord(
494
519
  val duration: Double,
495
520
  val url: String,
496
521
  val artwork: String?,
522
+ val extraPayload: JSONObject?,
497
523
  ) {
498
524
  fun toJson(): JSONObject =
499
525
  JSONObject().apply {
@@ -504,6 +530,7 @@ internal data class TrackItemRecord(
504
530
  put("duration", duration)
505
531
  put("url", url)
506
532
  put("artwork", artwork)
533
+ put("extraPayload", extraPayload)
507
534
  }
508
535
 
509
536
  companion object {
@@ -516,6 +543,12 @@ internal data class TrackItemRecord(
516
543
  duration = json.getDouble("duration"),
517
544
  url = json.getString("url"),
518
545
  artwork = if (json.isNull("artwork")) null else json.getString("artwork"),
546
+ extraPayload =
547
+ if (json.has("extraPayload") && !json.isNull("extraPayload")) {
548
+ json.getJSONObject("extraPayload")
549
+ } else {
550
+ null
551
+ },
519
552
  )
520
553
  }
521
554
  }