react-native-nitro-player 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -280,8 +280,7 @@ class TrackPlayerCore: NSObject {
280
280
  print("\nšŸ TrackPlayerCore: Track finished playing")
281
281
 
282
282
  guard let finishedItem = notification.object as? AVPlayerItem else {
283
- print("āš ļø Cannot identify finished item")
284
- skipToNext()
283
+ // Don't call skipToNext — AVQueuePlayer with actionAtItemEnd = .advance already auto-advances
285
284
  return
286
285
  }
287
286
 
@@ -352,7 +351,9 @@ class TrackPlayerCore: NSObject {
352
351
  print("šŸ” TrackPlayerCore: Repeat mode is OFF")
353
352
  }
354
353
 
355
- // Track ended naturally
354
+ // Track ended naturally — notify with .end reason
355
+ // AVQueuePlayer with actionAtItemEnd = .advance auto-advances to next item
356
+ // The KVO observer (currentItemDidChange) will handle the track change notification
356
357
  notifyTrackChange(
357
358
  getCurrentTrack()
358
359
  ?? TrackItem(
@@ -365,9 +366,6 @@ class TrackPlayerCore: NSObject {
365
366
  artwork: nil,
366
367
  extraPayload: nil
367
368
  ), .end)
368
-
369
- // Try to play next track
370
- skipToNext()
371
369
  }
372
370
 
373
371
  @objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
@@ -642,12 +640,21 @@ class TrackPlayerCore: NSObject {
642
640
  func updatePlaylist(playlistId: String) {
643
641
  DispatchQueue.main.async { [weak self] in
644
642
  guard let self = self else { return }
645
- if self.currentPlaylistId == playlistId {
643
+ guard self.currentPlaylistId == playlistId,
646
644
  let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
647
- if let playlist = playlist {
648
- self.updatePlayerQueue(tracks: playlist.tracks)
649
- }
645
+ else { return }
646
+
647
+ // If nothing is playing yet, do a full load
648
+ guard let player = self.player, player.currentItem != nil else {
649
+ self.updatePlayerQueue(tracks: playlist.tracks)
650
+ return
650
651
  }
652
+
653
+ // Update tracks list without interrupting playback
654
+ self.currentTracks = playlist.tracks
655
+
656
+ // Rebuild only the items after the currently playing item
657
+ self.rebuildAVQueueFromCurrentPosition()
651
658
  }
652
659
  }
653
660
 
@@ -1134,21 +1141,34 @@ class TrackPlayerCore: NSObject {
1134
1141
  var queue: [TrackItem] = []
1135
1142
 
1136
1143
  // Add tracks before current (original playlist)
1137
- if currentTrackIndex > 0 {
1138
- queue.append(contentsOf: Array(currentTracks[0..<currentTrackIndex]))
1144
+ // When a temp track is playing, include the original track at currentTrackIndex
1145
+ // (it already played before the temp track started)
1146
+ let beforeEnd = currentTemporaryType != .none
1147
+ ? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
1148
+ if beforeEnd > 0 {
1149
+ queue.append(contentsOf: Array(currentTracks[0..<beforeEnd]))
1139
1150
  }
1140
1151
 
1141
- // Add current track
1152
+ // Add current track (temp or original)
1142
1153
  if let current = getCurrentTrack() {
1143
1154
  queue.append(current)
1144
1155
  }
1145
1156
 
1146
1157
  // Add playNext stack (LIFO - most recently added plays first)
1147
- // Stack is already in correct order since we insert at position 0
1148
- queue.append(contentsOf: playNextStack)
1158
+ // Skip index 0 if current track is from playNext (it's already added as current)
1159
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1160
+ queue.append(contentsOf: Array(playNextStack.dropFirst()))
1161
+ } else if currentTemporaryType != .playNext {
1162
+ queue.append(contentsOf: playNextStack)
1163
+ }
1149
1164
 
1150
1165
  // Add upNext queue (in order, FIFO)
1151
- queue.append(contentsOf: upNextQueue)
1166
+ // Skip index 0 if current track is from upNext (it's already added as current)
1167
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1168
+ queue.append(contentsOf: Array(upNextQueue.dropFirst()))
1169
+ } else if currentTemporaryType != .upNext {
1170
+ queue.append(contentsOf: upNextQueue)
1171
+ }
1152
1172
 
1153
1173
  // Add remaining original tracks
1154
1174
  if currentTrackIndex + 1 < currentTracks.count {
@@ -1314,39 +1334,23 @@ class TrackPlayerCore: NSObject {
1314
1334
  private func skipToNextInternal() {
1315
1335
  guard let queuePlayer = self.player else { return }
1316
1336
 
1317
- print("\nā­ļø TrackPlayerCore: SKIP TO NEXT")
1318
- print(" BEFORE:")
1319
- print(" currentTrackIndex: \(self.currentTrackIndex)")
1320
- print(" Total tracks in currentTracks: \(self.currentTracks.count)")
1321
- print(" Items in player queue: \(queuePlayer.items().count)")
1322
-
1323
- if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
1324
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1325
- print(" Currently playing: \(track.title) (ID: \(track.id))")
1337
+ // Remove current temp track from its list before advancing
1338
+ if let trackId = queuePlayer.currentItem?.trackId {
1339
+ if currentTemporaryType == .playNext {
1340
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1341
+ playNextStack.remove(at: idx)
1342
+ }
1343
+ } else if currentTemporaryType == .upNext {
1344
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1345
+ upNextQueue.remove(at: idx)
1346
+ }
1326
1347
  }
1327
1348
  }
1328
1349
 
1329
- // Check if there are more items in the queue
1330
- if self.currentTrackIndex + 1 < self.currentTracks.count {
1331
- print(" šŸ”„ Calling advanceToNextItem()...")
1350
+ // Check if there are more items in the player queue
1351
+ if queuePlayer.items().count > 1 {
1332
1352
  queuePlayer.advanceToNextItem()
1333
-
1334
- // NOTE: Don't manually update currentTrackIndex here!
1335
- // The KVO observer (currentItemDidChange) will update it automatically
1336
-
1337
- print(" AFTER advanceToNextItem():")
1338
- print(" Items in player queue: \(queuePlayer.items().count)")
1339
-
1340
- if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
1341
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1342
- print(" New current item: \(track.title) (ID: \(track.id))")
1343
- }
1344
- }
1345
-
1346
- print(" ā³ Waiting for KVO observer to update index...")
1347
1353
  } else {
1348
- print(" āš ļø No more tracks in playlist")
1349
- // At end of playlist - stop or loop
1350
1354
  queuePlayer.pause()
1351
1355
  self.notifyPlaybackStateChange(.stopped, .end)
1352
1356
  }
@@ -1365,29 +1369,31 @@ class TrackPlayerCore: NSObject {
1365
1369
  private func skipToPreviousInternal() {
1366
1370
  guard let queuePlayer = self.player else { return }
1367
1371
 
1368
- print("\nā®ļø TrackPlayerCore: SKIP TO PREVIOUS")
1369
- print(" Current index: \(self.currentTrackIndex)")
1370
- print(" Temporary type: \(self.currentTemporaryType)")
1371
- print(" Current time: \(queuePlayer.currentTime().seconds)s")
1372
-
1373
1372
  let currentTime = queuePlayer.currentTime()
1374
1373
  if currentTime.seconds > Constants.skipToPreviousThreshold {
1375
1374
  // If more than threshold seconds in, restart current track
1376
- print(
1377
- " šŸ”„ More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
1378
1375
  queuePlayer.seek(to: .zero)
1379
1376
  } else if self.currentTemporaryType != .none {
1380
- // Playing temporary track - just restart it (temps are not navigable backwards)
1381
- print(" šŸ”„ Playing temporary track - restarting it (temps not navigable backwards)")
1382
- queuePlayer.seek(to: .zero)
1377
+ // Playing temporary track — remove from its list, then restart
1378
+ if let trackId = queuePlayer.currentItem?.trackId {
1379
+ if currentTemporaryType == .playNext {
1380
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1381
+ playNextStack.remove(at: idx)
1382
+ }
1383
+ } else if currentTemporaryType == .upNext {
1384
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1385
+ upNextQueue.remove(at: idx)
1386
+ }
1387
+ }
1388
+ }
1389
+ // Go to current original track position (skip back from temp)
1390
+ self.playFromIndex(index: self.currentTrackIndex)
1383
1391
  } else if self.currentTrackIndex > 0 {
1384
1392
  // Go to previous track in original playlist
1385
1393
  let previousIndex = self.currentTrackIndex - 1
1386
- print(" ā®ļø Going to previous track at index \(previousIndex)")
1387
1394
  self.playFromIndex(index: previousIndex)
1388
1395
  } else {
1389
1396
  // Already at first track, restart it
1390
- print(" šŸ”„ Already at first track, restarting it")
1391
1397
  queuePlayer.seek(to: .zero)
1392
1398
  }
1393
1399
  }
@@ -1577,44 +1583,38 @@ class TrackPlayerCore: NSObject {
1577
1583
  }
1578
1584
 
1579
1585
  private func skipToIndexInternal(index: Int) -> Bool {
1580
- print("\nšŸŽÆ TrackPlayerCore: SKIP TO INDEX \(index)")
1581
-
1582
1586
  // Get actual queue to validate index and determine position
1583
1587
  let actualQueue = getActualQueueInternal()
1584
1588
  let totalQueueSize = actualQueue.count
1585
1589
 
1586
1590
  // Validate index
1587
- guard index >= 0 && index < totalQueueSize else {
1588
- print(" āŒ Invalid index \(index), queue size is \(totalQueueSize)")
1589
- return false
1590
- }
1591
+ guard index >= 0 && index < totalQueueSize else { return false }
1592
+
1593
+ // Calculate queue section boundaries using effective sizes
1594
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
1595
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
1596
+ // so the current playing position shifts by 1
1597
+ let currentPos = currentTemporaryType != .none
1598
+ ? currentTrackIndex + 1 : currentTrackIndex
1599
+ let effectivePlayNextSize = currentTemporaryType == .playNext
1600
+ ? max(0, playNextStack.count - 1) : playNextStack.count
1601
+ let effectiveUpNextSize = currentTemporaryType == .upNext
1602
+ ? max(0, upNextQueue.count - 1) : upNextQueue.count
1591
1603
 
1592
- // Calculate queue section boundaries
1593
- // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
1594
- let currentPos = currentTrackIndex
1595
1604
  let playNextStart = currentPos + 1
1596
- let playNextEnd = playNextStart + playNextStack.count
1605
+ let playNextEnd = playNextStart + effectivePlayNextSize
1597
1606
  let upNextStart = playNextEnd
1598
- let upNextEnd = upNextStart + upNextQueue.count
1607
+ let upNextEnd = upNextStart + effectiveUpNextSize
1599
1608
  let originalRemainingStart = upNextEnd
1600
1609
 
1601
- print(" Queue structure:")
1602
- print(" currentPos: \(currentPos)")
1603
- print(" playNextStart: \(playNextStart), playNextEnd: \(playNextEnd)")
1604
- print(" upNextStart: \(upNextStart), upNextEnd: \(upNextEnd)")
1605
- print(" originalRemainingStart: \(originalRemainingStart)")
1606
- print(" totalQueueSize: \(totalQueueSize)")
1607
-
1608
1610
  // Case 1: Target is before current - use playFromIndex on original
1609
1611
  if index < currentPos {
1610
- print(" šŸ“ Target is before current, jumping to original playlist index \(index)")
1611
1612
  playFromIndexInternal(index: index)
1612
1613
  return true
1613
1614
  }
1614
1615
 
1615
1616
  // Case 2: Target is current - seek to beginning
1616
1617
  if index == currentPos {
1617
- print(" šŸ“ Target is current track, seeking to beginning")
1618
1618
  player?.seek(to: .zero)
1619
1619
  return true
1620
1620
  }
@@ -1622,12 +1622,13 @@ class TrackPlayerCore: NSObject {
1622
1622
  // Case 3: Target is in playNext section
1623
1623
  if index >= playNextStart && index < playNextEnd {
1624
1624
  let playNextIndex = index - playNextStart
1625
- print(" šŸ“ Target is in playNext section at position \(playNextIndex)")
1625
+ // Offset by 1 if current is from playNext (index 0 is already playing)
1626
+ let actualListIndex = currentTemporaryType == .playNext
1627
+ ? playNextIndex + 1 : playNextIndex
1626
1628
 
1627
1629
  // Remove tracks before the target from playNext (they're being skipped)
1628
- if playNextIndex > 0 {
1629
- playNextStack.removeFirst(playNextIndex)
1630
- print(" Removed \(playNextIndex) tracks from playNext stack")
1630
+ if actualListIndex > 0 {
1631
+ playNextStack.removeFirst(actualListIndex)
1631
1632
  }
1632
1633
 
1633
1634
  // Rebuild queue and advance
@@ -1639,16 +1640,16 @@ class TrackPlayerCore: NSObject {
1639
1640
  // Case 4: Target is in upNext section
1640
1641
  if index >= upNextStart && index < upNextEnd {
1641
1642
  let upNextIndex = index - upNextStart
1642
- print(" šŸ“ Target is in upNext section at position \(upNextIndex)")
1643
+ // Offset by 1 if current is from upNext (index 0 is already playing)
1644
+ let actualListIndex = currentTemporaryType == .upNext
1645
+ ? upNextIndex + 1 : upNextIndex
1643
1646
 
1644
1647
  // Clear all playNext tracks (they're being skipped)
1645
1648
  playNextStack.removeAll()
1646
- print(" Cleared all playNext tracks")
1647
1649
 
1648
1650
  // Remove tracks before target from upNext
1649
- if upNextIndex > 0 {
1650
- upNextQueue.removeFirst(upNextIndex)
1651
- print(" Removed \(upNextIndex) tracks from upNext queue")
1651
+ if actualListIndex > 0 {
1652
+ upNextQueue.removeFirst(actualListIndex)
1652
1653
  }
1653
1654
 
1654
1655
  // Rebuild queue and advance
@@ -1659,35 +1660,21 @@ class TrackPlayerCore: NSObject {
1659
1660
 
1660
1661
  // Case 5: Target is in remaining original tracks
1661
1662
  if index >= originalRemainingStart {
1662
- // Get the target track directly from actualQueue
1663
1663
  let targetTrack = actualQueue[index]
1664
1664
 
1665
- print(" šŸ“ Case 5: Target is in remaining original tracks")
1666
- print(" targetTrack.id: \(targetTrack.id)")
1667
- print(" currentTracks.count: \(currentTracks.count)")
1668
- print(" currentTracks IDs: \(currentTracks.map { $0.id })")
1669
-
1670
1665
  // Find this track's index in the original playlist
1671
1666
  guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
1672
- print(" āŒ Could not find track \(targetTrack.id) in original playlist")
1673
- print(" Available tracks: \(currentTracks.map { $0.id })")
1674
1667
  return false
1675
1668
  }
1676
1669
 
1677
- print(" originalIndex found: \(originalIndex)")
1678
-
1679
1670
  // Clear all temporary tracks (they're being skipped)
1680
1671
  playNextStack.removeAll()
1681
1672
  upNextQueue.removeAll()
1682
1673
  currentTemporaryType = .none
1683
- print(" Cleared all temporary tracks")
1684
1674
 
1685
- // Play from the original playlist index
1686
- let success = playFromIndexInternalWithResult(index: originalIndex)
1687
- return success
1675
+ return playFromIndexInternalWithResult(index: originalIndex)
1688
1676
  }
1689
1677
 
1690
- print(" āŒ Unexpected case, index \(index) not handled")
1691
1678
  return false
1692
1679
  }
1693
1680
 
@@ -1798,6 +1785,7 @@ class TrackPlayerCore: NSObject {
1798
1785
  if self.player?.currentItem != nil {
1799
1786
  self.rebuildAVQueueFromCurrentPosition()
1800
1787
  }
1788
+ mediaSessionManager?.onQueueChanged()
1801
1789
  }
1802
1790
 
1803
1791
  /**
@@ -1827,6 +1815,7 @@ class TrackPlayerCore: NSObject {
1827
1815
  if self.player?.currentItem != nil {
1828
1816
  self.rebuildAVQueueFromCurrentPosition()
1829
1817
  }
1818
+ mediaSessionManager?.onQueueChanged()
1830
1819
  }
1831
1820
 
1832
1821
  /**
@@ -1836,24 +1825,26 @@ class TrackPlayerCore: NSObject {
1836
1825
  private func rebuildAVQueueFromCurrentPosition() {
1837
1826
  guard let player = self.player else { return }
1838
1827
 
1839
- print("\nšŸ”„ TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1840
- print(" playNext stack: \(playNextStack.count) tracks")
1841
- print(" upNext queue: \(upNextQueue.count) tracks")
1842
-
1843
- // Don't interrupt currently playing item
1844
1828
  let currentItem = player.currentItem
1845
1829
  let playingItems = player.items()
1846
1830
 
1847
- // Build new queue order:
1848
- // [playNext stack] + [upNext queue] + [remaining original tracks]
1849
1831
  var newQueueTracks: [TrackItem] = []
1850
1832
 
1851
1833
  // Add playNext stack (LIFO - most recently added plays first)
1852
- // Stack is already in correct order since we insert at position 0
1853
- newQueueTracks.append(contentsOf: playNextStack)
1834
+ // Skip index 0 if current track is from playNext (it's already playing)
1835
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1836
+ newQueueTracks.append(contentsOf: Array(playNextStack.dropFirst()))
1837
+ } else if currentTemporaryType != .playNext {
1838
+ newQueueTracks.append(contentsOf: playNextStack)
1839
+ }
1854
1840
 
1855
1841
  // Add upNext queue (in order, FIFO)
1856
- newQueueTracks.append(contentsOf: upNextQueue)
1842
+ // Skip index 0 if current track is from upNext (it's already playing)
1843
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1844
+ newQueueTracks.append(contentsOf: Array(upNextQueue.dropFirst()))
1845
+ } else if currentTemporaryType != .upNext {
1846
+ newQueueTracks.append(contentsOf: upNextQueue)
1847
+ }
1857
1848
 
1858
1849
  // Add remaining original tracks
1859
1850
  if currentTrackIndex + 1 < currentTracks.count {
@@ -1861,8 +1852,6 @@ class TrackPlayerCore: NSObject {
1861
1852
  newQueueTracks.append(contentsOf: remainingOriginal)
1862
1853
  }
1863
1854
 
1864
- print(" New queue: \(newQueueTracks.count) tracks total")
1865
-
1866
1855
  // Remove all items from player EXCEPT the currently playing one
1867
1856
  for item in playingItems where item != currentItem {
1868
1857
  player.remove(item)
@@ -1877,7 +1866,6 @@ class TrackPlayerCore: NSObject {
1877
1866
  }
1878
1867
  }
1879
1868
 
1880
- print(" āœ… Queue rebuilt successfully")
1881
1869
  }
1882
1870
 
1883
1871
  /**
@@ -56,20 +56,28 @@ final class DownloadFileManager {
56
56
 
57
57
  func saveDownloadedFile(
58
58
  from temporaryLocation: URL, trackId: String, storageLocation: StorageLocation,
59
- originalURL: String? = nil
59
+ originalURL: String? = nil,
60
+ suggestedFilename: String? = nil
60
61
  ) -> String? {
61
62
  print("šŸŽÆ DownloadFileManager: saveDownloadedFile called for trackId=\(trackId)")
62
63
  print(" From: \(temporaryLocation.path)")
63
64
  print(" Original URL: \(originalURL ?? "nil")")
65
+ print(" Suggested Filename: \(suggestedFilename ?? "nil")")
64
66
 
65
67
  let destinationDirectory =
66
68
  storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
67
69
  print(" Destination directory: \(destinationDirectory.path)")
68
70
 
69
- // Determine file extension from the original URL, not the temp file
70
- // The temp file has .tmp extension which AVPlayer cannot play
71
+ // Determine file extension
71
72
  var fileExtension = "mp3" // Default fallback
72
- if let originalURL = originalURL, let url = URL(string: originalURL) {
73
+
74
+ if let suggestedFilename = suggestedFilename, !suggestedFilename.isEmpty {
75
+ let url = URL(fileURLWithPath: suggestedFilename)
76
+ let pathExtension = url.pathExtension.lowercased()
77
+ if !pathExtension.isEmpty {
78
+ fileExtension = pathExtension
79
+ }
80
+ } else if let originalURL = originalURL, let url = URL(string: originalURL) {
73
81
  let pathExtension = url.pathExtension.lowercased()
74
82
  if !pathExtension.isEmpty {
75
83
  fileExtension = pathExtension
@@ -681,11 +681,16 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
681
681
  let (storageLocation, originalURL) = queue.sync {
682
682
  (self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
683
683
  }
684
+
685
+ // Get suggested filename from response
686
+ let suggestedFilename = downloadTask.response?.suggestedFilename
687
+
684
688
  let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
685
689
  from: location,
686
690
  trackId: trackId,
687
691
  storageLocation: storageLocation,
688
- originalURL: originalURL
692
+ originalURL: originalURL,
693
+ suggestedFilename: suggestedFilename
689
694
  )
690
695
 
691
696
  // Now handle the rest asynchronously