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.
@@ -625,10 +625,32 @@ class TrackPlayerCore private constructor(
625
625
 
626
626
  fun skipToPrevious() {
627
627
  handler.post {
628
- // If playing temporary track, just seek to beginning (temps not navigable backwards)
629
- if (currentTemporaryType != TemporaryType.NONE) {
630
- println("šŸ”„ TrackPlayerCore: Playing temporary track - seeking to beginning")
628
+ val currentPosition = player.currentPosition // milliseconds
629
+
630
+ if (currentPosition > 2000) {
631
+ // More than 2 seconds in, restart current track
632
+ println("šŸ”„ TrackPlayerCore: Past threshold, restarting current track")
631
633
  player.seekTo(0)
634
+ } else if (currentTemporaryType != TemporaryType.NONE) {
635
+ // Playing temporary track within threshold — remove from its list, go back to original
636
+ println("šŸ”„ TrackPlayerCore: Removing temp track, going back to original")
637
+ val currentMediaItem = player.currentMediaItem
638
+ if (currentMediaItem != null) {
639
+ val trackId = extractTrackId(currentMediaItem.mediaId)
640
+ when (currentTemporaryType) {
641
+ TemporaryType.PLAY_NEXT -> {
642
+ val idx = playNextStack.indexOfFirst { it.id == trackId }
643
+ if (idx >= 0) playNextStack.removeAt(idx)
644
+ }
645
+ TemporaryType.UP_NEXT -> {
646
+ val idx = upNextQueue.indexOfFirst { it.id == trackId }
647
+ if (idx >= 0) upNextQueue.removeAt(idx)
648
+ }
649
+ else -> {}
650
+ }
651
+ }
652
+ currentTemporaryType = TemporaryType.NONE
653
+ playFromIndexInternal(currentTrackIndex)
632
654
  } else if (currentTrackIndex > 0) {
633
655
  // Go to previous track in original playlist
634
656
  println("šŸ”„ TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
@@ -859,50 +881,40 @@ class TrackPlayerCore private constructor(
859
881
  }
860
882
 
861
883
  private fun skipToIndexInternal(index: Int): Boolean {
862
- println("\nšŸŽÆ TrackPlayerCore: SKIP TO INDEX $index")
863
-
864
- if (!::player.isInitialized) {
865
- println(" āŒ Player not initialized")
866
- return false
867
- }
884
+ if (!::player.isInitialized) return false
868
885
 
869
886
  // Get actual queue to validate index and determine position
870
887
  val actualQueue = getActualQueueInternal()
871
888
  val totalQueueSize = actualQueue.size
872
889
 
873
890
  // Validate index
874
- if (index < 0 || index >= totalQueueSize) {
875
- println(" āŒ Invalid index $index, queue size is $totalQueueSize")
876
- return false
877
- }
891
+ if (index < 0 || index >= totalQueueSize) return false
892
+
893
+ // Calculate queue section boundaries using effective sizes
894
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
895
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
896
+ // so the current playing position shifts by 1
897
+ val currentPos = if (currentTemporaryType != TemporaryType.NONE)
898
+ currentTrackIndex + 1 else currentTrackIndex
899
+ val effectivePlayNextSize = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
900
+ maxOf(0, playNextStack.size - 1) else playNextStack.size
901
+ val effectiveUpNextSize = if (currentTemporaryType == TemporaryType.UP_NEXT)
902
+ maxOf(0, upNextQueue.size - 1) else upNextQueue.size
878
903
 
879
- // Calculate queue section boundaries
880
- // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
881
- // Use our internal tracking instead of player.currentMediaItemIndex (which is relative to ExoPlayer's subset queue)
882
- val currentPos = currentTrackIndex
883
904
  val playNextStart = currentPos + 1
884
- val playNextEnd = playNextStart + playNextStack.size
905
+ val playNextEnd = playNextStart + effectivePlayNextSize
885
906
  val upNextStart = playNextEnd
886
- val upNextEnd = upNextStart + upNextQueue.size
907
+ val upNextEnd = upNextStart + effectiveUpNextSize
887
908
  val originalRemainingStart = upNextEnd
888
909
 
889
- println(" Queue structure:")
890
- println(" currentPos: $currentPos")
891
- println(" playNextStart: $playNextStart, playNextEnd: $playNextEnd")
892
- println(" upNextStart: $upNextStart, upNextEnd: $upNextEnd")
893
- println(" originalRemainingStart: $originalRemainingStart")
894
- println(" totalQueueSize: $totalQueueSize")
895
-
896
910
  // Case 1: Target is before current - use playFromIndex on original
897
911
  if (index < currentPos) {
898
- println(" šŸ“ Target is before current, jumping to original playlist index $index")
899
912
  playFromIndexInternal(index)
900
913
  return true
901
914
  }
902
915
 
903
916
  // Case 2: Target is current - seek to beginning
904
917
  if (index == currentPos) {
905
- println(" šŸ“ Target is current track, seeking to beginning")
906
918
  player.seekTo(0)
907
919
  return true
908
920
  }
@@ -910,12 +922,13 @@ class TrackPlayerCore private constructor(
910
922
  // Case 3: Target is in playNext section
911
923
  if (index >= playNextStart && index < playNextEnd) {
912
924
  val playNextIndex = index - playNextStart
913
- println(" šŸ“ Target is in playNext section at position $playNextIndex")
925
+ // Offset by 1 if current is from playNext (index 0 is already playing)
926
+ val actualListIndex = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
927
+ playNextIndex + 1 else playNextIndex
914
928
 
915
929
  // Remove tracks before the target from playNext (they're being skipped)
916
- if (playNextIndex > 0) {
917
- repeat(playNextIndex) { playNextStack.removeAt(0) }
918
- println(" Removed $playNextIndex tracks from playNext stack")
930
+ if (actualListIndex > 0) {
931
+ repeat(actualListIndex) { playNextStack.removeAt(0) }
919
932
  }
920
933
 
921
934
  // Rebuild queue and advance
@@ -927,16 +940,16 @@ class TrackPlayerCore private constructor(
927
940
  // Case 4: Target is in upNext section
928
941
  if (index >= upNextStart && index < upNextEnd) {
929
942
  val upNextIndex = index - upNextStart
930
- println(" šŸ“ Target is in upNext section at position $upNextIndex")
943
+ // Offset by 1 if current is from upNext (index 0 is already playing)
944
+ val actualListIndex = if (currentTemporaryType == TemporaryType.UP_NEXT)
945
+ upNextIndex + 1 else upNextIndex
931
946
 
932
947
  // Clear all playNext tracks (they're being skipped)
933
948
  playNextStack.clear()
934
- println(" Cleared all playNext tracks")
935
949
 
936
950
  // Remove tracks before target from upNext
937
- if (upNextIndex > 0) {
938
- repeat(upNextIndex) { upNextQueue.removeAt(0) }
939
- println(" Removed $upNextIndex tracks from upNext queue")
951
+ if (actualListIndex > 0) {
952
+ repeat(actualListIndex) { upNextQueue.removeAt(0) }
940
953
  }
941
954
 
942
955
  // Rebuild queue and advance
@@ -947,37 +960,21 @@ class TrackPlayerCore private constructor(
947
960
 
948
961
  // Case 5: Target is in remaining original tracks
949
962
  if (index >= originalRemainingStart) {
950
- // Get the target track directly from actualQueue
951
963
  val targetTrack = actualQueue[index]
952
964
 
953
- println(" šŸ“ Case 5: Target is in remaining original tracks")
954
- println(" targetTrack.id: ${targetTrack.id}")
955
- println(" currentTracks.count: ${currentTracks.size}")
956
- println(" currentTracks IDs: ${currentTracks.map { it.id }}")
957
-
958
965
  // Find this track's index in the original playlist
959
966
  val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
960
- if (originalIndex == -1) {
961
- println(" āŒ Could not find track ${targetTrack.id} in original playlist")
962
- println(" Available tracks: ${currentTracks.map { it.id }}")
963
- return false
964
- }
965
-
966
- println(" originalIndex found: $originalIndex")
967
+ if (originalIndex == -1) return false
967
968
 
968
969
  // Clear all temporary tracks (they're being skipped)
969
970
  playNextStack.clear()
970
971
  upNextQueue.clear()
971
972
  currentTemporaryType = TemporaryType.NONE
972
- println(" Cleared all temporary tracks")
973
973
 
974
- // IMPORTANT: Rebuild the ExoPlayer queue without temporary tracks, then seek
975
- // We need to rebuild from the target index, not just seek
976
974
  rebuildQueueAndPlayFromIndex(originalIndex)
977
975
  return true
978
976
  }
979
977
 
980
- println(" āŒ Unexpected case, index $index not handled")
981
978
  return false
982
979
  }
983
980
 
@@ -986,7 +983,6 @@ class TrackPlayerCore private constructor(
986
983
  playNextStack.clear()
987
984
  upNextQueue.clear()
988
985
  currentTemporaryType = TemporaryType.NONE
989
- println(" 🧹 Cleared temporary tracks")
990
986
 
991
987
  rebuildQueueAndPlayFromIndex(index)
992
988
  }
@@ -1104,59 +1100,48 @@ class TrackPlayerCore private constructor(
1104
1100
  private fun rebuildQueueFromCurrentPosition() {
1105
1101
  if (!::player.isInitialized) return
1106
1102
 
1107
- println("\nšŸ”„ TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1108
- println(" currentIndex: ${player.currentMediaItemIndex}")
1109
- println(" currentMediaItem: ${player.currentMediaItem?.mediaId}")
1110
- println(" playNextStack (${playNextStack.size}): ${playNextStack.map { "${it.id}:${it.title}" }}")
1111
- println(" upNextQueue (${upNextQueue.size}): ${upNextQueue.map { "${it.id}:${it.title}" }}")
1112
-
1113
1103
  val currentIndex = player.currentMediaItemIndex
1114
1104
  if (currentIndex < 0) return
1115
1105
 
1116
- // Build new queue order:
1117
- // [playNext stack] + [upNext queue] + [remaining original tracks]
1118
1106
  val newQueueTracks = mutableListOf<TrackItem>()
1119
1107
 
1120
1108
  // Add playNext stack (LIFO - most recently added plays first)
1121
- // Stack is already in correct order since we insert at position 0
1122
- newQueueTracks.addAll(playNextStack)
1109
+ // Skip index 0 if current track is from playNext (it's already playing)
1110
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1111
+ newQueueTracks.addAll(playNextStack.subList(1, playNextStack.size))
1112
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1113
+ newQueueTracks.addAll(playNextStack)
1114
+ }
1123
1115
 
1124
1116
  // Add upNext queue (in order, FIFO)
1125
- newQueueTracks.addAll(upNextQueue)
1117
+ // Skip index 0 if current track is from upNext (it's already playing)
1118
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1119
+ newQueueTracks.addAll(upNextQueue.subList(1, upNextQueue.size))
1120
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1121
+ newQueueTracks.addAll(upNextQueue)
1122
+ }
1126
1123
 
1127
- // Add remaining original tracks
1128
- if (currentIndex + 1 < currentTracks.size) {
1129
- val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1130
- println(" remaining original (${remaining.size}): ${remaining.map { it.id }}")
1124
+ // Add remaining original tracks — use currentTrackIndex (original playlist position)
1125
+ if (currentTrackIndex + 1 < currentTracks.size) {
1126
+ val remaining = currentTracks.subList(currentTrackIndex + 1, currentTracks.size)
1131
1127
  newQueueTracks.addAll(remaining)
1132
1128
  }
1133
1129
 
1134
- println(" New queue total: ${newQueueTracks.size} tracks")
1135
- println(" Queue order: ${newQueueTracks.map { it.id }}")
1136
-
1137
1130
  // Create MediaItems for new tracks
1138
1131
  val playlistId = currentPlaylistId ?: ""
1139
1132
  val newMediaItems =
1140
1133
  newQueueTracks.map { track ->
1141
1134
  val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1142
- println(" Creating MediaItem: mediaId=$mediaId, title=${track.title}")
1143
1135
  track.toMediaItem(mediaId)
1144
1136
  }
1145
1137
 
1146
1138
  // Remove all items after current
1147
- val removedCount = player.mediaItemCount - currentIndex - 1
1148
- println(" Removing $removedCount items after current")
1149
1139
  while (player.mediaItemCount > currentIndex + 1) {
1150
1140
  player.removeMediaItem(currentIndex + 1)
1151
1141
  }
1152
1142
 
1153
1143
  // Add new items
1154
1144
  player.addMediaItems(newMediaItems)
1155
-
1156
- println(" āœ… Queue rebuilt. Player now has ${player.mediaItemCount} items")
1157
- for (i in 0 until player.mediaItemCount) {
1158
- println(" [$i]: ${player.getMediaItemAt(i).mediaId}")
1159
- }
1160
1145
  }
1161
1146
 
1162
1147
  /**
@@ -1393,57 +1378,46 @@ class TrackPlayerCore private constructor(
1393
1378
  }
1394
1379
 
1395
1380
  private fun getActualQueueInternal(): List<TrackItem> {
1396
- println("\nšŸ” TrackPlayerCore: getActualQueueInternal() called")
1397
- println(" playNextStack size: ${playNextStack.size}, tracks: ${playNextStack.map { it.id }}")
1398
- println(" upNextQueue size: ${upNextQueue.size}, tracks: ${upNextQueue.map { it.id }}")
1399
- println(" currentTracks size: ${currentTracks.size}, tracks: ${currentTracks.map { it.id }}")
1400
- println(" currentTrackIndex: $currentTrackIndex")
1401
-
1402
1381
  val queue = mutableListOf<TrackItem>()
1403
1382
 
1404
- if (!::player.isInitialized) {
1405
- println(" āŒ Player not initialized, returning empty")
1406
- return emptyList()
1407
- }
1383
+ if (!::player.isInitialized) return emptyList()
1408
1384
 
1409
- // Use our internal tracking of position in original playlist
1410
1385
  val currentIndex = currentTrackIndex
1411
- println(" Using currentTrackIndex: $currentIndex")
1412
- if (currentIndex < 0) {
1413
- println(" āŒ currentIndex < 0, returning empty")
1414
- return emptyList()
1415
- }
1386
+ if (currentIndex < 0) return emptyList()
1416
1387
 
1417
1388
  // Add tracks before current (original playlist)
1418
- if (currentIndex > 0 && currentIndex <= currentTracks.size) {
1419
- val beforeCurrent = currentTracks.subList(0, currentIndex)
1420
- println(" Adding ${beforeCurrent.size} tracks before current")
1421
- queue.addAll(beforeCurrent)
1389
+ // When a temp track is playing, include the original track at currentTrackIndex
1390
+ // (it already played before the temp track started)
1391
+ val beforeEnd = if (currentTemporaryType != TemporaryType.NONE)
1392
+ minOf(currentIndex + 1, currentTracks.size) else currentIndex
1393
+ if (beforeEnd > 0) {
1394
+ queue.addAll(currentTracks.subList(0, beforeEnd))
1422
1395
  }
1423
1396
 
1424
- // Add current track
1425
- getCurrentTrack()?.let {
1426
- println(" Adding current track: ${it.id}")
1427
- queue.add(it)
1428
- }
1397
+ // Add current track (temp or original)
1398
+ getCurrentTrack()?.let { queue.add(it) }
1429
1399
 
1430
1400
  // Add playNext stack (LIFO - most recently added plays first)
1431
- // Stack is already in correct order since we insert at position 0
1432
- println(" Adding ${playNextStack.size} playNext tracks")
1433
- queue.addAll(playNextStack)
1401
+ // Skip index 0 if current track is from playNext (it's already added as current)
1402
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1403
+ queue.addAll(playNextStack.subList(1, playNextStack.size))
1404
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1405
+ queue.addAll(playNextStack)
1406
+ }
1434
1407
 
1435
1408
  // Add upNext queue (in order, FIFO)
1436
- println(" Adding ${upNextQueue.size} upNext tracks")
1437
- queue.addAll(upNextQueue)
1409
+ // Skip index 0 if current track is from upNext (it's already added as current)
1410
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1411
+ queue.addAll(upNextQueue.subList(1, upNextQueue.size))
1412
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1413
+ queue.addAll(upNextQueue)
1414
+ }
1438
1415
 
1439
1416
  // Add remaining original tracks
1440
1417
  if (currentIndex + 1 < currentTracks.size) {
1441
- val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1442
- println(" Adding ${remaining.size} remaining tracks")
1443
- queue.addAll(remaining)
1418
+ queue.addAll(currentTracks.subList(currentIndex + 1, currentTracks.size))
1444
1419
  }
1445
1420
 
1446
- println(" āœ… Final queue size: ${queue.size}, tracks: ${queue.map { it.id }}")
1447
1421
  return queue
1448
1422
  }
1449
1423
  }
@@ -50,6 +50,7 @@ class DownloadFileManager private constructor(
50
50
  fun createDownloadFile(
51
51
  trackId: String,
52
52
  storageLocation: StorageLocation,
53
+ extension: String = "mp3",
53
54
  ): File {
54
55
  val destinationDir =
55
56
  when (storageLocation) {
@@ -58,7 +59,7 @@ class DownloadFileManager private constructor(
58
59
  }
59
60
 
60
61
  // Create unique filename based on trackId
61
- val fileName = "$trackId.mp3"
62
+ val fileName = "$trackId.$extension"
62
63
  return File(destinationDir, fileName)
63
64
  }
64
65
 
@@ -119,15 +120,17 @@ class DownloadFileManager private constructor(
119
120
 
120
121
  fun getLocalPath(trackId: String): String? {
121
122
  // Check private directory first
122
- val privateFile = File(privateDownloadsDir, "$trackId.mp3")
123
- if (privateFile.exists()) {
124
- return privateFile.absolutePath
123
+ privateDownloadsDir.listFiles()?.forEach { file ->
124
+ if (file.nameWithoutExtension == trackId) {
125
+ return file.absolutePath
126
+ }
125
127
  }
126
128
 
127
129
  // Check public directory
128
- val publicFile = File(publicDownloadsDir, "$trackId.mp3")
129
- if (publicFile.exists()) {
130
- return publicFile.absolutePath
130
+ publicDownloadsDir.listFiles()?.forEach { file ->
131
+ if (file.nameWithoutExtension == trackId) {
132
+ return file.absolutePath
133
+ }
131
134
  }
132
135
 
133
136
  return null
@@ -8,6 +8,7 @@ import androidx.core.app.NotificationCompat
8
8
  import androidx.work.CoroutineWorker
9
9
  import androidx.work.ForegroundInfo
10
10
  import androidx.work.WorkerParameters
11
+ import android.webkit.MimeTypeMap
11
12
  import com.margelo.nitro.nitroplayer.*
12
13
  import kotlinx.coroutines.Dispatchers
13
14
  import kotlinx.coroutines.withContext
@@ -122,16 +123,42 @@ class DownloadWorker(
122
123
  if (responseCode != HttpURLConnection.HTTP_OK) {
123
124
  throw Exception("Server returned HTTP $responseCode")
124
125
  }
126
+ // Determine extension
127
+ var extension = MimeTypeMap.getFileExtensionFromUrl(urlString)
128
+
129
+ // 1. Try Content-Disposition
130
+ if (extension.isNullOrEmpty()) {
131
+ val contentDisposition = connection.getHeaderField("Content-Disposition")
132
+ if (contentDisposition != null) {
133
+ val match = Regex("filename=\"?([^\";]+)\"?").find(contentDisposition)
134
+ if (match != null) {
135
+ val filename = match.groupValues[1]
136
+ extension = MimeTypeMap.getFileExtensionFromUrl(filename)
137
+ }
138
+ }
139
+ }
140
+
141
+ // 2. Try Content-Type
142
+ if (extension.isNullOrEmpty()) {
143
+ val contentType = connection.contentType
144
+ if (contentType != null) {
145
+ val mimeType = contentType.split(";")[0].trim()
146
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
147
+ }
148
+ }
149
+
150
+ val finalExtension = if (extension.isNullOrEmpty()) "mp3" else extension
125
151
 
126
- val totalBytes = connection.contentLengthLong
127
- var bytesDownloaded: Long = 0
128
152
 
129
153
  // Create destination file
130
- val destinationFile = fileManager.createDownloadFile(trackId, storageLocation)
154
+ val destinationFile = fileManager.createDownloadFile(trackId, storageLocation, finalExtension)
131
155
 
132
156
  inputStream = BufferedInputStream(connection.inputStream)
133
157
  outputStream = FileOutputStream(destinationFile)
134
158
 
159
+ val totalBytes = connection.contentLengthLong
160
+ var bytesDownloaded: Long = 0
161
+
135
162
  val buffer = ByteArray(BUFFER_SIZE)
136
163
  var bytesRead: Int
137
164
  var lastProgressUpdate = System.currentTimeMillis()