From 7703bcbd1378fe366e0497de73800b47059b9715 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 24 Feb 2026 18:28:19 -0500 Subject: [PATCH] Refactoring queue --- .../wholphin/data/model/AudioItem.kt | 2 + .../wholphin/services/MusicService.kt | 120 +++++++++++------- .../ui/detail/music/NowPlayingOverlay.kt | 13 +- .../ui/detail/music/NowPlayingPage.kt | 10 +- 4 files changed, 89 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt index 0b170238..1e11aeaf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt @@ -1,9 +1,11 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.runtime.Stable import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID import kotlin.time.Duration +@Stable data class AudioItem( val id: UUID, val albumId: UUID?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index 75f6c964..737ee7fa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -2,7 +2,9 @@ package com.github.damontecres.wholphin.services import android.content.Context import androidx.annotation.OptIn +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -15,6 +17,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.ui.DefaultItemFields +import com.github.damontecres.wholphin.ui.onMain import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.profile.Codec @@ -30,6 +33,7 @@ import org.jellyfin.sdk.api.client.extensions.instantMixApi import org.jellyfin.sdk.api.client.extensions.universalAudioApi import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -95,15 +99,7 @@ class MusicService player.shuffleModeEnabled = shuffled player.play() } - (startIndex..items.lastIndex).forEach { - val item = items.getBlocking(it) - if (item != null && item.type == BaseItemKind.AUDIO) { - val mediaItem = convert(item) - withContext(Dispatchers.Main) { - player.addMediaItem(mediaItem) - } - } - } + addAllToQueue(items) } suspend fun setQueue( @@ -116,6 +112,7 @@ class MusicService .filter { it.type == BaseItemKind.AUDIO } .map(::convert) withContext(Dispatchers.Main) { + updateQueueSize() player.setMediaItems(mediaItems) player.shuffleModeEnabled = shuffled player.play() @@ -124,12 +121,17 @@ class MusicService suspend fun addToQueue( item: BaseItem, - position: Int? = null, + index: Int? = null, ) { if (item.type == BaseItemKind.AUDIO) { val mediaItem = convert(item) withContext(Dispatchers.Main) { - player.addMediaItem(mediaItem) + if (index != null) { + player.addMediaItem(index, mediaItem) + } else { + player.addMediaItem(mediaItem) + } + updateQueueSize() if (player.mediaItemCount == 1) { // Start playing if this was the first time added player.play() @@ -138,6 +140,22 @@ class MusicService } } + suspend fun addAllToQueue(list: BlockingList) { + list.indices + .chunked(25) + .forEach { + val mediaItems = + it.mapNotNull { + list + .getBlocking(it) + ?.takeIf { it.type == BaseItemKind.AUDIO } + ?.let(::convert) + } + onMain { player.addMediaItems(mediaItems) } + } + updateQueueSize() + } + private fun convert(audio: BaseItem): MediaItem { val url = api.universalAudioApi.getUniversalAudioStreamUrl( @@ -164,19 +182,26 @@ class MusicService .setTag(AudioItem.from(audio, imageUrl)) .build() } + + private suspend fun updateQueueSize() { + withContext(Dispatchers.Main) { + _state.update { + it.copy(queueSize = player.mediaItemCount) + } + } + } } @Stable data class MusicServiceState( - val queue: List, + val queueSize: Int, val currentIndex: Int, + val currentItemId: UUID?, val isPlaying: Boolean, ) { companion object { - val EMPTY = MusicServiceState(emptyList(), 0, false) + val EMPTY = MusicServiceState(0, 0, null, false) } - - val currentItemId: UUID? get() = if (isPlaying) queue.getOrNull(currentIndex)?.id else null } /** @@ -190,7 +215,7 @@ private class MusicPlayerListener( Timber.v("MusicPlayerListener init") state.update { it.copy( - queue = PlayerMediaItemList(player, player.mediaItemCount), + queueSize = player.mediaItemCount, currentIndex = player.currentMediaItemIndex, isPlaying = player.isPlaying, ) @@ -211,11 +236,7 @@ private class MusicPlayerListener( reason: Int, ) { Timber.v("MusicPlayerListener onMediaItemTransition") - state.update { - it.copy( - currentIndex = player.currentMediaItemIndex, - ) - } + updateCurrentIndex() } override fun onTimelineChanged( @@ -224,40 +245,41 @@ private class MusicPlayerListener( ) { Timber.v("MusicPlayerListener onTimelineChanged") if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) { - state.update { - it.copy( - queue = PlayerMediaItemList(player, player.mediaItemCount), - currentIndex = player.currentMediaItemIndex, - ) - } + updateCurrentIndex() + } + } + + private fun updateCurrentIndex() { + state.update { + it.copy( + currentIndex = player.currentMediaItemIndex, + currentItemId = player.getMediaItemAt(player.currentMediaItemIndex).mediaId.toUUIDOrNull(), + ) } } } -private class PlayerMediaItemList( +data class PlayerMediaItemList( private val player: Player, - override val size: Int, -) : AbstractList() { - override fun get(index: Int): AudioItem { +) { + val size: Int get() = player.mediaItemCount + + operator fun get(index: Int): AudioItem { // Timber.v("get %s", index) return player.getMediaItemAt(index).localConfiguration?.tag as AudioItem } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is PlayerMediaItemList) return false - if (!super.equals(other)) return false - - if (size != other.size) return false - if (player != other.player) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + size - result = 31 * result + player.hashCode() - return result - } } + +@Composable +fun rememberQueue( + player: Player, + queueSize: Int, +): List = + remember(queueSize) { + object : AbstractList() { + override val size: Int + get() = player.mediaItemCount + + override fun get(index: Int): AudioItem = player.getMediaItemAt(index).localConfiguration?.tag as AudioItem + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt index cd2c61b2..b6a73fa6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingOverlay.kt @@ -23,6 +23,7 @@ import androidx.media3.ui.compose.state.rememberNextButtonState import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import androidx.media3.ui.compose.state.rememberPreviousButtonState import androidx.tv.material3.Text +import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.services.MusicServiceState import com.github.damontecres.wholphin.ui.playback.ControllerViewState import com.github.damontecres.wholphin.ui.playback.PlaybackButtons @@ -37,11 +38,11 @@ import kotlin.time.Duration.Companion.seconds fun NowPlayingOverlay( state: MusicServiceState, player: Player, + current: AudioItem?, + queue: List, controllerViewState: ControllerViewState, modifier: Modifier = Modifier, ) { - val current = state.queue.getOrNull(state.currentIndex) - val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } val playPauseState = rememberPlayPauseButtonState(player) @@ -77,7 +78,7 @@ fun NowPlayingOverlay( PlaybackButtons( player = player, initialFocusRequester = focusRequester, - onControllerInteraction = {}, + onControllerInteraction = { controllerViewState.pulseControls() }, onPlaybackActionClick = {}, showPlay = playPauseState.showPlay, previousEnabled = previousState.isEnabled, @@ -87,21 +88,21 @@ fun NowPlayingOverlay( seekForward = 30.seconds, ) } - if (state.queue.isEmpty()) { + if (queue.isEmpty()) { Text("No items") } else { LazyColumn( contentPadding = PaddingValues(16.dp), modifier = Modifier.fillMaxSize(), ) { - itemsIndexed(state.queue) { index, song -> + itemsIndexed(queue) { index, song -> SongListItem( title = song.title, artist = song.artistNames, indexNumber = index + 1, runtime = song.runtime?.roundSeconds, showArtist = true, - isPlaying = state.currentIndex == index, + isPlaying = current?.id == song.id, onClick = { player.seekTo(index, 0L) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index 0851bc69..81a2b335 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.UserPreferencesService +import com.github.damontecres.wholphin.services.rememberQueue import com.github.damontecres.wholphin.ui.playback.ControllerViewState import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler import com.github.damontecres.wholphin.ui.tryRequestFocus @@ -67,6 +68,9 @@ class NowPlayingViewModel val state get() = musicService.state val player get() = musicService.player + init { + } + fun reportInteraction() { controllerViewState.pulseControls() } @@ -83,7 +87,9 @@ fun NowPlayingPage( ) { val state by viewModel.state.collectAsState() val player = viewModel.player - val current = state.queue.getOrNull(state.currentIndex) + val queue = rememberQueue(player, state.queueSize) + val current = queue.getOrNull(state.currentIndex) + val controllerViewState = viewModel.controllerViewState val preferences by viewModel.userPreferencesService.flow.collectAsState(AppPreferences.getDefaultInstance()) @@ -131,6 +137,8 @@ fun NowPlayingPage( NowPlayingOverlay( state = state, player = player, + current = current, + queue = queue, controllerViewState = controllerViewState, modifier = Modifier.fillMaxSize(), )