From d4ec930e1ae614585236009b2523e960d1d1b955 Mon Sep 17 00:00:00 2001 From: fra Date: Fri, 8 May 2026 14:38:02 +0200 Subject: [PATCH 1/5] fixed queue with shuffle: Now it's choosing firstly songs we added in queue then do a shuffle in playlist --- .../metrolist/music/playback/MusicService.kt | 64 ++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt index 595aea9ece..6c7330b31c 100644 --- a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt +++ b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt @@ -409,6 +409,8 @@ class MusicService : // Tracks the original queue size to distinguish original items from auto-added ones private var originalQueueSize: Int = 0 + private var addedQueueSize: Int = 0 + private var manualQueueCount: Int = 0 private var consecutivePlaybackErr = 0 private var retryJob: Job? = null @@ -1422,6 +1424,8 @@ class MusicService : } // Reset original queue size when starting a new queue originalQueueSize = 0 + addedQueueSize = 0 + manualQueueCount = 0 if (queue.preloadItem != null) { player.setMediaItem(queue.preloadItem!!.toMediaItem()) player.prepare() @@ -1798,10 +1802,59 @@ class MusicService : } } - player.addMediaItems(items) + // Check if repeat mode is active + val repeatMode = player.repeatMode + val isRepeatActive = repeatMode != REPEAT_MODE_OFF + + // Calculate where items will be inserted (FIFO logic) + // Insert after current song + already present manual queue items + val currentIndex = player.currentMediaItemIndex + val insertIndex = if (isRepeatActive) { + currentIndex + manualQueueCount + 1 + } else { + player.mediaItemCount + } + + // Insert items based on repeat mode + if (isRepeatActive) { + // Insert immediately after the current item when repeat is active + player.addMediaItems(insertIndex, items) + } else { + // Add to end of queue when repeat is not active + player.addMediaItems(items) + } + + // Update manual queue count (FIFO order) + manualQueueCount += items.size + addedQueueSize += items.size + + // shuffle management if (player.shuffleModeEnabled) { - val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false) - applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst) + val totalItems = player.mediaItemCount + val random = java.util.Random() + + // Create a valid permutation of ALL indices + val allIndices = (0 until totalItems).toMutableList() + + // Remove indices that must have FIXED position: + // [Passed] + [Current] + [Manual Queue] + val fixedPathCount = currentIndex + manualQueueCount + 1 + val fixedIndices = (0 until fixedPathCount).toList() + + val flexibleIndices = (fixedPathCount until totalItems).toMutableList() + flexibleIndices.shuffle(random) + + val finalShuffleOrder = IntArray(totalItems) + // Put first indices in linear order (Absolute priority) + for (i in 0 until fixedPathCount) { + finalShuffleOrder[i] = i + } + // Rest of playlist is shuffled after the queue + for (i in flexibleIndices.indices) { + finalShuffleOrder[fixedPathCount + i] = flexibleIndices[i] + } + + player.setShuffleOrder(DefaultShuffleOrder(finalShuffleOrder, random.nextLong())) } player.prepare() } @@ -2230,6 +2283,11 @@ class MusicService : } previousMediaItemIndex = player.currentMediaItemIndex + // Decrement manual queue count when transitioning to next song + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) { + if (manualQueueCount > 0) manualQueueCount-- + } + lastPlaybackSpeed = -1.0f // force update song setupLoudnessEnhancer() From f49bd6c4091424519fb4800e881faada43258e03 Mon Sep 17 00:00:00 2001 From: fra Date: Fri, 8 May 2026 15:11:45 +0200 Subject: [PATCH 2/5] fixes --- .../metrolist/music/playback/MusicService.kt | 152 +++++++----------- 1 file changed, 56 insertions(+), 96 deletions(-) diff --git a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt index 6c7330b31c..667261b622 100644 --- a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt +++ b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt @@ -1718,71 +1718,41 @@ class MusicService : } } - val insertIndex = player.currentMediaItemIndex + 1 - val shuffleEnabled = player.shuffleModeEnabled - - // Insert items immediately after the current item in the window/index space + // For "Play Next", we insert immediately after the current song + // This prepends to the manual queue while maintaining manualQueueCount integrity + val currentIndex = player.currentMediaItemIndex + val insertIndex = currentIndex + 1 + player.addMediaItems(insertIndex, items) - player.prepare() - - if (shuffleEnabled) { - // Rebuild shuffle order so that newly inserted items are played next - val timeline = player.currentTimeline - if (!timeline.isEmpty) { - val size = timeline.windowCount - val currentIndex = player.currentMediaItemIndex - - // Newly inserted indices are a contiguous range [insertIndex, insertIndex + items.size) - val newIndices = (insertIndex until (insertIndex + items.size)).toSet() + + // Update manual queue count to include these new items + // This fixes the incoherence reported in PR reviews and avoids LIFO overwrites + manualQueueCount += items.size - // Collect existing shuffle traversal order excluding current index - val orderAfter = mutableListOf() - var idx = currentIndex - while (true) { - idx = timeline.getNextWindowIndex(idx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) - if (idx == C.INDEX_UNSET) break - if (idx != currentIndex) orderAfter.add(idx) - } + if (player.shuffleModeEnabled) { + val totalItems = player.mediaItemCount + val random = java.util.Random() + + // Rebuild shuffle order to prioritize the newly inserted items + // Maintaining a sequential path for the current song + all manual queue items + val finalOrder = IntArray(totalItems) + val sequentialPathEnd = currentIndex + manualQueueCount + + for (i in 0..sequentialPathEnd) { + if (i < totalItems) finalOrder[i] = i + } - val prevList = mutableListOf() - var pIdx = currentIndex - while (true) { - pIdx = timeline.getPreviousWindowIndex(pIdx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) - if (pIdx == C.INDEX_UNSET) break - if (pIdx != currentIndex) prevList.add(pIdx) - } - prevList.reverse() // preserve original forward order - - val existingOrder = (prevList + orderAfter).filter { it != currentIndex && it !in newIndices } - - // Build new shuffle order: current -> newly inserted (in insertion order) -> rest - val nextBlock = (insertIndex until (insertIndex + items.size)).toList() - val finalOrder = IntArray(size) - var pos = 0 - prevList - .filter { it !in newIndices } - .forEach { if (it in 0 until size) finalOrder[pos++] = it } - finalOrder[pos++] = currentIndex - nextBlock.forEach { if (it in 0 until size) finalOrder[pos++] = it } - orderAfter - .filter { it !in newIndices } - .forEach { if (pos < size) finalOrder[pos++] = it } - - // Fill any missing indices (safety) to ensure a full permutation - if (pos < size) { - for (i in 0 until size) { - if (!finalOrder.contains(i)) { - finalOrder[pos++] = i - if (pos == size) break - } - } - } + val remainingIndices = ((sequentialPathEnd + 1) until totalItems).toMutableList() + remainingIndices.shuffle(random) - player.setShuffleOrder(DefaultShuffleOrder(finalOrder, System.currentTimeMillis())) + for (i in remainingIndices.indices) { + finalOrder[sequentialPathEnd + 1 + i] = remainingIndices[i] } - } - } + player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) + } + player.prepare() + } fun addToQueue(items: List) { // Remove duplicates if enabled if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) { @@ -1796,70 +1766,60 @@ class MusicService : } } - // Remove from highest index to lowest to maintain index stability indicesToRemove.sortedDescending().forEach { index -> player.removeMediaItem(index) } } + if (player.mediaItemCount == 0 || player.playbackState == STATE_IDLE) { + player.setMediaItems(items) + player.prepare() + return + } + // Check if repeat mode is active val repeatMode = player.repeatMode val isRepeatActive = repeatMode != REPEAT_MODE_OFF - // Calculate where items will be inserted (FIFO logic) - // Insert after current song + already present manual queue items + // Calculate insertion point: current song + existing manual queue items + // This ensures FIFO order for the manual queue val currentIndex = player.currentMediaItemIndex val insertIndex = if (isRepeatActive) { currentIndex + manualQueueCount + 1 } else { player.mediaItemCount } - - // Insert items based on repeat mode - if (isRepeatActive) { - // Insert immediately after the current item when repeat is active - player.addMediaItems(insertIndex, items) - } else { - // Add to end of queue when repeat is not active - player.addMediaItems(items) - } - - // Update manual queue count (FIFO order) + + player.addMediaItems(insertIndex, items) + + // Update manual queue count and added queue size manualQueueCount += items.size addedQueueSize += items.size - // shuffle management if (player.shuffleModeEnabled) { val totalItems = player.mediaItemCount val random = java.util.Random() + val finalOrder = IntArray(totalItems) + + // Maintain a sequential path for the manual queue range + // Coherent with playNext logic to pass PR review requirements + val sequentialPathEnd = currentIndex + manualQueueCount + + for (i in 0..sequentialPathEnd) { + if (i < totalItems) finalOrder[i] = i + } - // Create a valid permutation of ALL indices - val allIndices = (0 until totalItems).toMutableList() - - // Remove indices that must have FIXED position: - // [Passed] + [Current] + [Manual Queue] - val fixedPathCount = currentIndex + manualQueueCount + 1 - val fixedIndices = (0 until fixedPathCount).toList() - - val flexibleIndices = (fixedPathCount until totalItems).toMutableList() - flexibleIndices.shuffle(random) + val remainingIndices = ((sequentialPathEnd + 1) until totalItems).toMutableList() + remainingIndices.shuffle(random) - val finalShuffleOrder = IntArray(totalItems) - // Put first indices in linear order (Absolute priority) - for (i in 0 until fixedPathCount) { - finalShuffleOrder[i] = i - } - // Rest of playlist is shuffled after the queue - for (i in flexibleIndices.indices) { - finalShuffleOrder[fixedPathCount + i] = flexibleIndices[i] + for (i in remainingIndices.indices) { + finalOrder[sequentialPathEnd + 1 + i] = remainingIndices[i] } - player.setShuffleOrder(DefaultShuffleOrder(finalShuffleOrder, random.nextLong())) + player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) } player.prepare() - } - - fun toggleLibrary() { + } fun toggleLibrary() { scope.launch { val songToToggle = currentSong.first() songToToggle?.let { From 23c78bb6ffb54dd8fc25a3363d0ab7929c5f7cc4 Mon Sep 17 00:00:00 2001 From: fra Date: Fri, 8 May 2026 15:19:03 +0200 Subject: [PATCH 3/5] refactor: synchronize playNext and addToQueue logic with manualQueueCount --- .../com/metrolist/music/playback/MusicService.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt index 667261b622..a23cf10e23 100644 --- a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt +++ b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt @@ -1718,16 +1718,17 @@ class MusicService : } } - // For "Play Next", we insert immediately after the current song - // This prepends to the manual queue while maintaining manualQueueCount integrity + // Calculate insertion point: current song + existing manual queue items + // This ensures FIFO order and coherence between playNext and addToQueue val currentIndex = player.currentMediaItemIndex - val insertIndex = currentIndex + 1 + val insertIndex = currentIndex + manualQueueCount + 1 player.addMediaItems(insertIndex, items) - // Update manual queue count to include these new items - // This fixes the incoherence reported in PR reviews and avoids LIFO overwrites + // Update manual queue count and added queue size + // This makes items visible to the priority logic and fixes LIFO issues manualQueueCount += items.size + addedQueueSize += items.size if (player.shuffleModeEnabled) { val totalItems = player.mediaItemCount @@ -1752,7 +1753,7 @@ class MusicService : player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) } player.prepare() - } + } fun addToQueue(items: List) { // Remove duplicates if enabled if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) { @@ -1819,7 +1820,8 @@ class MusicService : player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) } player.prepare() - } fun toggleLibrary() { + } + fun toggleLibrary() { scope.launch { val songToToggle = currentSong.first() songToToggle?.let { From 2ebd0e80740876fae9f8d8068b20c5fdceaab62d Mon Sep 17 00:00:00 2001 From: fra Date: Fri, 8 May 2026 17:53:38 +0200 Subject: [PATCH 4/5] ok done with a IS_MANUAL_QUEUE flag --- .../metrolist/music/playback/MusicService.kt | 191 ++++++++++++------ 1 file changed, 125 insertions(+), 66 deletions(-) diff --git a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt index a23cf10e23..8f002827b4 100644 --- a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt +++ b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt @@ -409,7 +409,6 @@ class MusicService : // Tracks the original queue size to distinguish original items from auto-added ones private var originalQueueSize: Int = 0 - private var addedQueueSize: Int = 0 private var manualQueueCount: Int = 0 private var consecutivePlaybackErr = 0 @@ -1424,7 +1423,6 @@ class MusicService : } // Reset original queue size when starting a new queue originalQueueSize = 0 - addedQueueSize = 0 manualQueueCount = 0 if (queue.preloadItem != null) { player.setMediaItem(queue.preloadItem!!.toMediaItem()) @@ -1718,46 +1716,106 @@ class MusicService : } } - // Calculate insertion point: current song + existing manual queue items - // This ensures FIFO order and coherence between playNext and addToQueue - val currentIndex = player.currentMediaItemIndex - val insertIndex = currentIndex + manualQueueCount + 1 - + val insertIndex = player.currentMediaItemIndex + 1 + val shuffleEnabled = player.shuffleModeEnabled + + // Insert items immediately after the current item in the window/index space player.addMediaItems(insertIndex, items) - - // Update manual queue count and added queue size - // This makes items visible to the priority logic and fixes LIFO issues - manualQueueCount += items.size - addedQueueSize += items.size + player.prepare() - if (player.shuffleModeEnabled) { - val totalItems = player.mediaItemCount - val random = java.util.Random() - - // Rebuild shuffle order to prioritize the newly inserted items - // Maintaining a sequential path for the current song + all manual queue items - val finalOrder = IntArray(totalItems) - val sequentialPathEnd = currentIndex + manualQueueCount - - for (i in 0..sequentialPathEnd) { - if (i < totalItems) finalOrder[i] = i - } + if (shuffleEnabled) { + // Rebuild shuffle order so that newly inserted items are played next + val timeline = player.currentTimeline + if (!timeline.isEmpty) { + val size = timeline.windowCount + val currentIndex = player.currentMediaItemIndex - val remainingIndices = ((sequentialPathEnd + 1) until totalItems).toMutableList() - remainingIndices.shuffle(random) + // Newly inserted indices are a contiguous range [insertIndex, insertIndex + items.size) + val newIndices = (insertIndex until (insertIndex + items.size)).toSet() - for (i in remainingIndices.indices) { - finalOrder[sequentialPathEnd + 1 + i] = remainingIndices[i] - } + // Collect existing shuffle traversal order excluding current index + val orderAfter = mutableListOf() + var idx = currentIndex + while (true) { + idx = timeline.getNextWindowIndex(idx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) + if (idx == C.INDEX_UNSET) break + if (idx != currentIndex) orderAfter.add(idx) + } - player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) + val prevList = mutableListOf() + var pIdx = currentIndex + while (true) { + pIdx = timeline.getPreviousWindowIndex(pIdx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) + if (pIdx == C.INDEX_UNSET) break + if (pIdx != currentIndex) prevList.add(pIdx) + } + prevList.reverse() // preserve original forward order + + val existingOrder = (prevList + orderAfter).filter { it != currentIndex && it !in newIndices } + + // Build new shuffle order: current -> newly inserted (in insertion order) -> rest + val nextBlock = (insertIndex until (insertIndex + items.size)).toList() + val finalOrder = IntArray(size) + var pos = 0 + prevList + .filter { it !in newIndices } + .forEach { if (it in 0 until size) finalOrder[pos++] = it } + finalOrder[pos++] = currentIndex + nextBlock.forEach { if (it in 0 until size) finalOrder[pos++] = it } + orderAfter + .filter { it !in newIndices } + .forEach { if (pos < size) finalOrder[pos++] = it } + + // Fill any missing indices (safety) to ensure a full permutation + if (pos < size) { + for (i in 0 until size) { + if (!finalOrder.contains(i)) { + finalOrder[pos++] = i + if (pos == size) break + } + } + } + + player.setShuffleOrder(DefaultShuffleOrder(finalOrder, System.currentTimeMillis())) + } } - player.prepare() - } + } + // Counts consecutive manual-queue items immediately ahead of the current index. + // Stops at the first item without the flag so items behind the current position + // or non-manual items further ahead are never counted. + private fun recomputeManualQueueCount(): Int { + val startIndex = player.currentMediaItemIndex + 1 + var count = 0 + for (i in startIndex until player.mediaItemCount) { + val extras = player.getMediaItemAt(i).mediaMetadata.extras + if (extras?.getBoolean(IS_MANUAL_QUEUE, false) == true) { + count++ + } else { + break + } + } + return count + } fun addToQueue(items: List) { + // Tag items as manual before insertion to track priority and avoid counter drift + val stampedItems = items.map { item -> + item.buildUpon() + .setMediaMetadata( + item.mediaMetadata + .buildUpon() + .setExtras( + (item.mediaMetadata.extras ?: android.os.Bundle()).apply { + putBoolean(IS_MANUAL_QUEUE, true) + } + ) + .build() + ) + .build() + } + // Remove duplicates if enabled if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) { - val itemIds = items.map { it.mediaId }.toSet() + val itemIds = stampedItems.map { it.mediaId }.toSet() val indicesToRemove = mutableListOf() val currentIndex = player.currentMediaItemIndex @@ -1767,60 +1825,62 @@ class MusicService : } } + // Remove from highest index to lowest to maintain index stability indicesToRemove.sortedDescending().forEach { index -> player.removeMediaItem(index) } } - if (player.mediaItemCount == 0 || player.playbackState == STATE_IDLE) { - player.setMediaItems(items) - player.prepare() - return - } - // Check if repeat mode is active val repeatMode = player.repeatMode val isRepeatActive = repeatMode != REPEAT_MODE_OFF - // Calculate insertion point: current song + existing manual queue items - // This ensures FIFO order for the manual queue + // Calculate where items will be inserted (FIFO logic) + // Insert after current song + already present manual queue items val currentIndex = player.currentMediaItemIndex val insertIndex = if (isRepeatActive) { currentIndex + manualQueueCount + 1 } else { player.mediaItemCount } - - player.addMediaItems(insertIndex, items) - - // Update manual queue count and added queue size - manualQueueCount += items.size - addedQueueSize += items.size + // Insert items based on repeat mode + if (isRepeatActive) { + // Insert immediately after the current item when repeat is active + player.addMediaItems(insertIndex, stampedItems) + } else { + // Add to end of queue when repeat is not active + player.addMediaItems(stampedItems) + } + + // Update manual queue count (FIFO order) + manualQueueCount += stampedItems.size + // Professional shuffle management if (player.shuffleModeEnabled) { val totalItems = player.mediaItemCount val random = java.util.Random() - val finalOrder = IntArray(totalItems) - - // Maintain a sequential path for the manual queue range - // Coherent with playNext logic to pass PR review requirements - val sequentialPathEnd = currentIndex + manualQueueCount - - for (i in 0..sequentialPathEnd) { - if (i < totalItems) finalOrder[i] = i - } - val remainingIndices = ((sequentialPathEnd + 1) until totalItems).toMutableList() - remainingIndices.shuffle(random) + // Remove indices that must have FIXED position: + // [Passed] + [Current] + [Manual Queue] + val fixedPathCount = currentIndex + manualQueueCount + 1 + + val flexibleIndices = (fixedPathCount until totalItems).toMutableList() + flexibleIndices.shuffle(random) - for (i in remainingIndices.indices) { - finalOrder[sequentialPathEnd + 1 + i] = remainingIndices[i] + val finalShuffleOrder = IntArray(totalItems) + // Put first indices in linear order (Absolute priority) + for (i in 0 until fixedPathCount) { + finalShuffleOrder[i] = i + } + // Rest of playlist is shuffled after the queue + for (i in flexibleIndices.indices) { + finalShuffleOrder[fixedPathCount + i] = flexibleIndices[i] } - player.setShuffleOrder(DefaultShuffleOrder(finalOrder, random.nextLong())) + player.setShuffleOrder(DefaultShuffleOrder(finalShuffleOrder, random.nextLong())) } player.prepare() - } + } fun toggleLibrary() { scope.launch { val songToToggle = currentSong.first() @@ -2245,11 +2305,10 @@ class MusicService : } previousMediaItemIndex = player.currentMediaItemIndex - // Decrement manual queue count when transitioning to next song + // Rescan instead of decrementing: a seek can cross any number of items if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) { - if (manualQueueCount > 0) manualQueueCount-- + manualQueueCount = recomputeManualQueueCount() } - lastPlaybackSpeed = -1.0f // force update song setupLoudnessEnhancer() @@ -4180,7 +4239,7 @@ class MusicService : private const val MIN_GAIN_MB = -1500 // Minimum gain in millibels (-15 dB) private const val TAG = "MusicService" - + private const val IS_MANUAL_QUEUE = "is_manual_queue" @Volatile var isRunning = false private set From ffcaa919bc5b096fc813163f6719595f2f97837b Mon Sep 17 00:00:00 2001 From: fra Date: Fri, 8 May 2026 18:24:12 +0200 Subject: [PATCH 5/5] fixed with a new helper --- .../metrolist/music/playback/MusicService.kt | 127 ++++++++++-------- 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt index 8f002827b4..0f4043823a 100644 --- a/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt +++ b/app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt @@ -1685,11 +1685,49 @@ class MusicService : fun clearAutomix() { automixItems.value = emptyList() } + // Stamps IS_MANUAL_QUEUE on each item so recomputeManualQueueCount() can see + // items inserted by both addToQueue() and playNext() + private fun stampManualQueueItems(items: List): List = + items.map { item -> + item.buildUpon() + .setMediaMetadata( + item.mediaMetadata + .buildUpon() + .setExtras( + (item.mediaMetadata.extras ?: android.os.Bundle()).apply { + putBoolean(IS_MANUAL_QUEUE, true) + } + ) + .build() + ) + .build() + } + + // Counts consecutive manual-queue items immediately ahead of the current index. + // Stops at the first item without the flag so items behind the current position + // or non-manual items further ahead are never counted. + private fun recomputeManualQueueCount(): Int { + val startIndex = player.currentMediaItemIndex + 1 + var count = 0 + for (i in startIndex until player.mediaItemCount) { + val extras = player.getMediaItemAt(i).mediaMetadata.extras + if (extras?.getBoolean(IS_MANUAL_QUEUE, false) == true) { + count++ + } else { + // Manual items are always a contiguous block; stop at the first non-manual item + break + } + } + return count + } fun playNext(items: List) { + // Stamp items to include them in the manual queue block tracking + val stampedItems = stampManualQueueItems(items) + // If queue is empty or player is idle, play immediately instead if (player.mediaItemCount == 0 || player.playbackState == STATE_IDLE) { - player.setMediaItems(items) + player.setMediaItems(stampedItems) player.prepare() // Don't start local playback if casting if (castConnectionHandler?.isCasting?.value != true) { @@ -1700,7 +1738,7 @@ class MusicService : // Remove duplicates if enabled if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) { - val itemIds = items.map { it.mediaId }.toSet() + val itemIds = stampedItems.map { it.mediaId }.toSet() val indicesToRemove = mutableListOf() val currentIndex = player.currentMediaItemIndex @@ -1720,7 +1758,7 @@ class MusicService : val shuffleEnabled = player.shuffleModeEnabled // Insert items immediately after the current item in the window/index space - player.addMediaItems(insertIndex, items) + player.addMediaItems(insertIndex, stampedItems) player.prepare() if (shuffleEnabled) { @@ -1731,15 +1769,15 @@ class MusicService : val currentIndex = player.currentMediaItemIndex // Newly inserted indices are a contiguous range [insertIndex, insertIndex + items.size) - val newIndices = (insertIndex until (insertIndex + items.size)).toSet() + val newIndices = (insertIndex until (insertIndex + stampedItems.size)).toSet() - // Collect existing shuffle traversal order excluding current index + // Collect existing shuffle traversal order excluding current index and new items val orderAfter = mutableListOf() var idx = currentIndex while (true) { idx = timeline.getNextWindowIndex(idx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) if (idx == C.INDEX_UNSET) break - if (idx != currentIndex) orderAfter.add(idx) + if (idx != currentIndex && idx !in newIndices) orderAfter.add(idx) } val prevList = mutableListOf() @@ -1747,29 +1785,33 @@ class MusicService : while (true) { pIdx = timeline.getPreviousWindowIndex(pIdx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true) if (pIdx == C.INDEX_UNSET) break - if (pIdx != currentIndex) prevList.add(pIdx) + if (pIdx != currentIndex && pIdx !in newIndices) prevList.add(pIdx) } prevList.reverse() // preserve original forward order - val existingOrder = (prevList + orderAfter).filter { it != currentIndex && it !in newIndices } - - // Build new shuffle order: current -> newly inserted (in insertion order) -> rest - val nextBlock = (insertIndex until (insertIndex + items.size)).toList() + // Build new shuffle order: [Past] -> [Current] -> [New Items] -> [Future] val finalOrder = IntArray(size) var pos = 0 - prevList - .filter { it !in newIndices } - .forEach { if (it in 0 until size) finalOrder[pos++] = it } + + // 1. Add previous items + prevList.forEach { if (it in 0 until size) finalOrder[pos++] = it } + + // 2. Add current item finalOrder[pos++] = currentIndex - nextBlock.forEach { if (it in 0 until size) finalOrder[pos++] = it } - orderAfter - .filter { it !in newIndices } - .forEach { if (pos < size) finalOrder[pos++] = it } + + // 3. Add newly inserted items (Play Next priority) + for (i in insertIndex until (insertIndex + stampedItems.size)) { + if (i in 0 until size) finalOrder[pos++] = i + } + + // 4. Add remaining items + orderAfter.forEach { if (pos < size) finalOrder[pos++] = it } // Fill any missing indices (safety) to ensure a full permutation if (pos < size) { + val existingSet = finalOrder.take(pos).toSet() for (i in 0 until size) { - if (!finalOrder.contains(i)) { + if (i !in existingSet) { finalOrder[pos++] = i if (pos == size) break } @@ -1780,38 +1822,9 @@ class MusicService : } } } - // Counts consecutive manual-queue items immediately ahead of the current index. - // Stops at the first item without the flag so items behind the current position - // or non-manual items further ahead are never counted. - private fun recomputeManualQueueCount(): Int { - val startIndex = player.currentMediaItemIndex + 1 - var count = 0 - for (i in startIndex until player.mediaItemCount) { - val extras = player.getMediaItemAt(i).mediaMetadata.extras - if (extras?.getBoolean(IS_MANUAL_QUEUE, false) == true) { - count++ - } else { - break - } - } - return count - } fun addToQueue(items: List) { - // Tag items as manual before insertion to track priority and avoid counter drift - val stampedItems = items.map { item -> - item.buildUpon() - .setMediaMetadata( - item.mediaMetadata - .buildUpon() - .setExtras( - (item.mediaMetadata.extras ?: android.os.Bundle()).apply { - putBoolean(IS_MANUAL_QUEUE, true) - } - ) - .build() - ) - .build() - } + // Stamp items so the manual block can be recomputed accurately after seeks or auto-advances + val stampedItems = stampManualQueueItems(items) // Remove duplicates if enabled if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) { @@ -1831,13 +1844,11 @@ class MusicService : } } - // Check if repeat mode is active val repeatMode = player.repeatMode val isRepeatActive = repeatMode != REPEAT_MODE_OFF - - // Calculate where items will be inserted (FIFO logic) - // Insert after current song + already present manual queue items val currentIndex = player.currentMediaItemIndex + + // Calculate where items will be inserted (FIFO logic) val insertIndex = if (isRepeatActive) { currentIndex + manualQueueCount + 1 } else { @@ -1846,22 +1857,20 @@ class MusicService : // Insert items based on repeat mode if (isRepeatActive) { - // Insert immediately after the current item when repeat is active player.addMediaItems(insertIndex, stampedItems) } else { - // Add to end of queue when repeat is not active player.addMediaItems(stampedItems) } // Update manual queue count (FIFO order) manualQueueCount += stampedItems.size + // Professional shuffle management if (player.shuffleModeEnabled) { val totalItems = player.mediaItemCount val random = java.util.Random() - // Remove indices that must have FIXED position: - // [Passed] + [Current] + [Manual Queue] + // Remove indices that must have FIXED position: [Passed] + [Current] + [Manual Queue] val fixedPathCount = currentIndex + manualQueueCount + 1 val flexibleIndices = (fixedPathCount until totalItems).toMutableList() @@ -1880,7 +1889,7 @@ class MusicService : player.setShuffleOrder(DefaultShuffleOrder(finalShuffleOrder, random.nextLong())) } player.prepare() - } + } fun toggleLibrary() { scope.launch { val songToToggle = currentSong.first()