Skip to content

Commit

Permalink
Merge pull request #264 from 'Millencolin/fix-start-radio-from-queue'
Browse files Browse the repository at this point in the history
* Fix for starting song radio from queue creates a queue based on the wrong song 
* Adds seamless playback queue switching if it's possible
* Adds back song continuation/auto load


Fixes #263
  • Loading branch information
mikooomich authored Feb 2, 2025
2 parents 90fe134 + 8f02383 commit 82d2081
Show file tree
Hide file tree
Showing 15 changed files with 130 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ val PersistentQueueKey = booleanPreferencesKey("persistentQueue")
val SkipSilenceKey = booleanPreferencesKey("skipSilence")
val SkipOnErrorKey = booleanPreferencesKey("skipOnError")
val AudioNormalizationKey = booleanPreferencesKey("audioNormalization")
val AutoLoadMoreKey = booleanPreferencesKey("autoLoadMore")
val KeepAliveKey = booleanPreferencesKey("keepAlive")
val StopMusicOnTaskClearKey = booleanPreferencesKey("stopMusicOnTaskClear")

Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ interface DatabaseDao : SongsDao, AlbumsDao, ArtistsDao, PlaylistsDao, QueueDao
title = mq.title,
shuffled = mq.shuffled,
queuePos = mq.queuePos,
index = mq.index
index = mq.index,
playlistId = mq.playlistId
)
)

Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/dd3boh/outertune/db/daos/QueueDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ interface QueueDao {
unShuffled = unshuffledSongs.map { it.toMediaMetadata() }.toMutableList(),
shuffled = queue.shuffled,
queuePos = queue.queuePos,
index = queue.index
index = queue.index,
playlistId = queue.playlistId
)
)
}
Expand Down
104 changes: 61 additions & 43 deletions app/src/main/java/com/dd3boh/outertune/models/QueueBoard.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.dd3boh.outertune.models

import androidx.media3.common.C
import androidx.media3.common.MediaItem
import com.dd3boh.outertune.constants.PersistentQueueKey
import com.dd3boh.outertune.db.entities.QueueEntity
import com.dd3boh.outertune.extensions.metadata
import com.dd3boh.outertune.extensions.currentMetadata
import com.dd3boh.outertune.extensions.move
import com.dd3boh.outertune.extensions.toMediaItem
import com.dd3boh.outertune.playback.MusicService
Expand Down Expand Up @@ -33,7 +32,7 @@ var isShuffleEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
* @param title Queue title (and UID)
* @param queue List of media items
*/
class MultiQueueObject(
data class MultiQueueObject(
val id: Long,
val title: String,
/**
Expand All @@ -47,7 +46,11 @@ class MultiQueueObject(
var shuffled: Boolean = false,
var queuePos: Int = -1, // position of current song
var index: Int, // order of queue
val playlistId: String? = null,
/**
* Song id to start watch endpoint
* TODO: change this in database too
*/
var playlistId: String? = null,
) {

/**
Expand Down Expand Up @@ -169,6 +172,7 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
forceInsert: Boolean = false,
replace: Boolean = false,
delta: Boolean = true,
isRadio: Boolean = false,
startIndex: Int = 0
): Boolean {
if (QUEUE_DEBUG)
Expand Down Expand Up @@ -296,17 +300,20 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
}
} else {
// add entirely new queue
// Precondition(s): radio queues never include local songs
if (masterQueues.size > MAX_QUEUES) {
deleteQueue(masterQueues.first(), player)
}
val q = ArrayList(mediaList.filterNotNull())
val newQueue = MultiQueueObject(
QueueEntity.generateQueueId(),
title,
ArrayList(mediaList.filterNotNull()),
q,
ArrayList(mediaList.filterNotNull()),
false,
startIndex,
masterQueues.size
masterQueues.size,
if (isRadio) q.lastOrNull()?.id else null
)
masterQueues.add(newQueue)
if (player.dataStore.get(PersistentQueueKey, true)) {
Expand All @@ -326,16 +333,17 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
forceInsert: Boolean = false,
replace: Boolean = false,
delta: Boolean = true,
startIndex: Int = 0
) = addQueue(title, mediaList, playerConnection.service, forceInsert, replace, delta, startIndex)
startIndex: Int = 0,
isRadio: Boolean = false,
) = addQueue(title, mediaList, playerConnection.service, forceInsert, replace, delta, isRadio, startIndex)


/**
* Add songs to end of CURRENT QUEUE & update it in the player
*/
fun enqueueEnd(mediaList: List<MediaMetadata>, player: MusicService) {
fun enqueueEnd(mediaList: List<MediaMetadata>, player: MusicService, isRadio: Boolean = false) {
getCurrentQueue()?.let {
addSongsToQueue(it, Int.MAX_VALUE, mediaList, player)
addSongsToQueue(it, Int.MAX_VALUE, mediaList, player, isRadio = isRadio)
}
}

Expand All @@ -347,27 +355,30 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
pos: Int,
mediaList: List<MediaMetadata>,
player: MusicService,
saveToDb: Boolean = true
saveToDb: Boolean = true,
isRadio: Boolean = false
) {
val listPos = if (pos < 0) {
0
} else if (pos > q.queue.size) {
q.queue.size - 1
q.queue.size
} else {
pos
}

// Add to current queue at the position. For the other queue, just add to end
if (q.shuffled) {
q.queue.addAll(listPos, mediaList)
q.unShuffled.addAll(q.queue.size - 1, mediaList)
q.unShuffled.addAll(q.queue.size, mediaList)
} else {
q.queue.addAll(q.queue.size - 1, mediaList)
q.queue.addAll(q.queue.size, mediaList)
q.unShuffled.addAll(listPos, mediaList)
}

// copy so ui doesnt crash
player.player.replaceMediaItems(listPos, pos, mediaList.map { it.toMediaItem() })
setCurrQueue(q, player)
if (isRadio) {
q.playlistId = mediaList.lastOrNull()?.id
}

if (saveToDb && player.dataStore.get(PersistentQueueKey, true)) {
CoroutineScope(Dispatchers.IO).launch {
Expand Down Expand Up @@ -706,14 +717,45 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
masterIndex = masterQueues.indexOf(item)

// if requested to get shuffled queue
val mediaItems: MutableList<MediaMetadata>?
if (item.shuffled) {
player.player.setMediaItems(item.queue.map { it.toMediaItem() })
mediaItems = item.queue
} else {
player.player.setMediaItems(item.unShuffled.map { it.toMediaItem() })
mediaItems = item.unShuffled
}

/**
* current playing == jump target, do seamlessly
*/
val seamlessSupported = player.player.currentMetadata?.id == mediaItems[queuePos].id
if (seamlessSupported) {
if (queuePos == 0) {
val playerIndex = player.player.currentMediaItemIndex
val playerItemCount = player.player.mediaItemCount
// player.player.replaceMediaItems seems to stop playback so we
// remove all songs except the currently playing one and then add the list of new items
if (playerIndex < playerItemCount - 1) {
player.player.removeMediaItems(playerIndex + 1, playerItemCount)
}
if (playerIndex > 0) {
player.player.removeMediaItems(0, playerIndex)
}
// add all songs except the first one since it is already present and playing
player.player.addMediaItems(mediaItems.drop(1).map { it.toMediaItem() })
} else {
// replace items up to current playing, then replace items after current
player.player.replaceMediaItems(0, queuePos,
mediaItems.subList(0, queuePos).map { it.toMediaItem() })
player.player.replaceMediaItems(queuePos + 1, Int.MAX_VALUE,
mediaItems.subList(queuePos + 1, mediaItems.size).map { it.toMediaItem() })
}
} else {
player.player.setMediaItems(mediaItems.map { it.toMediaItem() })
}

isShuffleEnabled.value = item.shuffled

if (autoSeek) {
if (autoSeek && !seamlessSupported) {
player.player.seekTo(queuePos, C.TIME_UNSET)
}

Expand All @@ -739,30 +781,6 @@ class QueueBoard(queues: MutableList<MultiQueueObject> = ArrayList()) {
}
}

/**
* Update the current position index of the current queue to the index of the FIRST media item match
*
* @param mediaItem
*/
fun setCurrQueuePosIndex(mediaItem: MediaItem?, player: MusicService) {
val currentQueue = getCurrentQueue()
if (mediaItem == null || currentQueue == null) {
return
}

if (currentQueue.shuffled) {
currentQueue.queuePos = currentQueue.queue.indexOf(mediaItem.metadata)
} else {
currentQueue.queuePos = currentQueue.unShuffled.indexOf(mediaItem.metadata)
}

if (player.dataStore.get(PersistentQueueKey, true)) {
CoroutineScope(Dispatchers.IO).launch {
player.database.updateQueue(currentQueue)
}
}
}

companion object {
val mutex = Mutex()

Expand Down
61 changes: 40 additions & 21 deletions app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import com.dd3boh.outertune.constants.AudioNormalizationKey
import com.dd3boh.outertune.constants.AudioOffload
import com.dd3boh.outertune.constants.AudioQuality
import com.dd3boh.outertune.constants.AudioQualityKey
import com.dd3boh.outertune.constants.AutoLoadMoreKey
import com.dd3boh.outertune.constants.KeepAliveKey
import com.dd3boh.outertune.constants.LastPosKey
import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleLike
Expand Down Expand Up @@ -91,6 +92,7 @@ import com.dd3boh.outertune.extensions.metadata
import com.dd3boh.outertune.extensions.setOffloadEnabled
import com.dd3boh.outertune.extensions.toMediaItem
import com.dd3boh.outertune.lyrics.LyricsHelper
import com.dd3boh.outertune.models.MediaMetadata
import com.dd3boh.outertune.models.QueueBoard
import com.dd3boh.outertune.models.isShuffleEnabled
import com.dd3boh.outertune.models.toMediaMetadata
Expand Down Expand Up @@ -137,6 +139,7 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.time.LocalDateTime
import javax.inject.Inject
import kotlin.collections.map
import kotlin.math.min
import kotlin.math.pow

Expand Down Expand Up @@ -279,6 +282,22 @@ class MusicService : MediaLibraryService(),
player.play()
}

// Auto load more songs
val q = queueBoard.getCurrentQueue()
val songId = q?.playlistId
if (dataStore.get(AutoLoadMoreKey, true) &&
reason != Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT &&
player.mediaItemCount - player.currentMediaItemIndex <= 5 &&
songId != null // aka "hasNext"
) {
scope.launch(SilentHandler) {
val mediaItems = YouTubeQueue(WatchEndpoint(songId)).nextPage()
if (player.playbackState != STATE_IDLE) {
queueBoard.enqueueEnd(mediaItems.drop(1), this@MusicService, isRadio = true)
}
}
}

// this absolute eye sore detects if we loop back to the beginning of queue, when shuffle AND repeat all
// no, when repeat mode is on, player does not "STATE_ENDED"
if (player.currentMediaItemIndex == 0 && lastMediaItemIndex == player.mediaItemCount - 1 &&
Expand All @@ -292,7 +311,7 @@ class MusicService : MediaLibraryService(),
updateNotification() // also updates when queue changes

queueBoard.setCurrQueuePosIndex(player.currentMediaItemIndex, this@MusicService)
queueTitle = queueBoard.getCurrentQueue()?.title
queueTitle = q?.title
}
})
sleepTimer = SleepTimer(scope, this)
Expand Down Expand Up @@ -568,7 +587,13 @@ class MusicService : MediaLibraryService(),
* @param title Title override for the queue. If this value us unspecified, this method takes the value from queue.
* If both are unspecified, the title will default to "Queue".
*/
fun playQueue(queue: Queue, playWhenReady: Boolean = true, replace: Boolean = false, title: String? = null) {
fun playQueue(
queue: Queue,
playWhenReady: Boolean = true,
replace: Boolean = false,
isRadio: Boolean = false,
title: String? = null
) {
if (!queueBoard.initialized) {
initQueue()
queueBoard.initialized = true
Expand All @@ -581,17 +606,26 @@ class MusicService : MediaLibraryService(),
if (queueTitle == null && initialStatus.title != null) { // do not find a title if an override is provided
queueTitle = initialStatus.title
}
val items = ArrayList<MediaMetadata>()
val preloadItem = queue.preloadItem

// print out queue
// println("-----------------------------")
// initialStatus.items.map { println(it.title) }
if (initialStatus.items.isEmpty()) return@launch
if (preloadItem != null) {
items.add(preloadItem)
items.addAll(initialStatus.items.subList(1, initialStatus.items.size))
} else {
items.addAll(initialStatus.items)
}
queueBoard.addQueue(
queueTitle?: "Queue",
initialStatus.items,
items,
player = this@MusicService,
startIndex = if (initialStatus.mediaItemIndex > 0) initialStatus.mediaItemIndex else 0,
replace = replace
replace = replace,
isRadio = isRadio
)
queueBoard.setCurrQueue(this@MusicService)

Expand All @@ -600,30 +634,14 @@ class MusicService : MediaLibraryService(),
}
}

fun startRadioSeamlessly() {
val currentMediaMetadata = player.currentMetadata ?: return
if (player.currentMediaItemIndex > 0) player.removeMediaItems(0, player.currentMediaItemIndex)
if (player.currentMediaItemIndex < player.mediaItemCount - 1) player.removeMediaItems(player.currentMediaItemIndex + 1, player.mediaItemCount)
scope.launch(SilentHandler) {
val radioQueue = YouTubeQueue(endpoint = WatchEndpoint(videoId = currentMediaMetadata.id))
val initialStatus = radioQueue.getInitialStatus()
if (initialStatus.title != null) {
queueTitle = initialStatus.title
}
player.addMediaItems(initialStatus.items.drop(1).map { it.toMediaItem() })
}
}

/**
* Add items to queue, right after current playing item
*/
fun enqueueNext(items: List<MediaItem>) {
println("wtf "+ "ENQUEUNE NEXT CALLED " + queueBoard.initialized)
if (!queueBoard.initialized) {

// when enqueuing next when player isn't active, play as a new song
if (items.isNotEmpty()) {
println("wtf "+ "PLAYING FROMe")
CoroutineScope(Dispatchers.Main).launch {
playQueue(
ListQueue(
Expand Down Expand Up @@ -668,7 +686,8 @@ class MusicService : MediaLibraryService(),
}

fun toggleStartRadio() {
startRadioSeamlessly()
val mediaMetadata = player.currentMetadata ?: return
playQueue(YouTubeQueue.radio(mediaMetadata), isRadio = true)
}

private fun openAudioEffectSession() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ class PlayerConnection(
repeatMode.value = player.repeatMode
}

fun playQueue(queue: Queue, replace: Boolean = true, title: String? = null) {
service.playQueue(queue, replace = replace, title = title)
fun playQueue(queue: Queue, replace: Boolean = true, isRadio: Boolean = false, title: String? = null) {
service.playQueue(queue, replace = replace, title = title, isRadio = isRadio)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ fun PlayerMenu(
icon = Icons.Rounded.Radio,
title = R.string.start_radio
) {
playerConnection.playQueue(YouTubeQueue.radio(mediaMetadata))
playerConnection.playQueue(YouTubeQueue.radio(mediaMetadata), isRadio = true)
onDismiss()
}
GridMenuItem(
Expand Down
Loading

0 comments on commit 82d2081

Please sign in to comment.