Rework YouTube Radio
This commit is contained in:
parent
f39276875d
commit
a0e42473e6
10 changed files with 222 additions and 219 deletions
|
@ -32,29 +32,33 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener
|
|||
import androidx.media3.exoplayer.analytics.PlaybackStats
|
||||
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.MediaNotification
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaNotification.ActionFactory
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.MainActivity
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.utils.RingBuffer
|
||||
import it.vfsfitvnm.vimusic.utils.YoutubePlayer
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||
import it.vfsfitvnm.vimusic.utils.insert
|
||||
import it.vfsfitvnm.youtubemusic.Outcome
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY)
|
||||
val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.EMPTY)
|
||||
val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY)
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalFoundationApi
|
||||
class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
|
||||
MediaNotification.Provider,
|
||||
PlaybackStatsListener.Callback, Player.Listener,YoutubePlayer.Radio.Listener {
|
||||
MediaSession.SessionCallback,
|
||||
PlaybackStatsListener.Callback, Player.Listener {
|
||||
|
||||
companion object {
|
||||
private const val NotificationId = 1001
|
||||
|
@ -74,6 +78,8 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
|
|||
private var lastArtworkUri: Uri? = null
|
||||
private var lastBitmap: Bitmap? = null
|
||||
|
||||
private var radio: YoutubePlayer.Radio? = null
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
|
||||
|
||||
override fun onCreate() {
|
||||
|
@ -101,11 +107,11 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
|
|||
|
||||
mediaSession = MediaSession.Builder(this, player)
|
||||
.withSessionActivity()
|
||||
.setSessionCallback(this)
|
||||
.setMediaItemFiller(this)
|
||||
.build()
|
||||
|
||||
player.addListener(this)
|
||||
YoutubePlayer.Radio.listener = this
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -119,6 +125,49 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
|
|||
return mediaSession
|
||||
}
|
||||
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val sessionCommands = SessionCommands.Builder()
|
||||
.add(StartRadioCommand)
|
||||
.add(StartArtistRadioCommand)
|
||||
.add(StopRadioCommand)
|
||||
.build()
|
||||
val playerCommands = Player.Commands.Builder().addAllCommands().build()
|
||||
return MediaSession.ConnectionResult.accept(sessionCommands,playerCommands)
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand) {
|
||||
StartRadioCommand, StartArtistRadioCommand -> {
|
||||
radio = null
|
||||
YoutubePlayer.Radio(
|
||||
videoId = args.getString("videoId"),
|
||||
playlistId = args.getString("playlistId"),
|
||||
playlistSetVideoId = args.getString("playlistSetVideoId"),
|
||||
parameters = args.getString("params"),
|
||||
).let {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
when (customCommand) {
|
||||
StartRadioCommand -> mediaSession.player.addMediaItems(it.process().drop(1))
|
||||
StartArtistRadioCommand -> mediaSession.player.forcePlayFromBeginning(it.process())
|
||||
}
|
||||
radio = it
|
||||
}
|
||||
}
|
||||
}
|
||||
StopRadioCommand -> radio = null
|
||||
}
|
||||
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
|
||||
override fun onPlaybackStatsReady(
|
||||
eventTime: AnalyticsListener.EventTime,
|
||||
playbackStats: PlaybackStats
|
||||
|
@ -132,18 +181,12 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
|
|||
}
|
||||
}
|
||||
|
||||
override fun process(play: Boolean) {
|
||||
if (YoutubePlayer.Radio.isActive) {
|
||||
coroutineScope.launch {
|
||||
YoutubePlayer.Radio.process(mediaSession.player, play = play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (YoutubePlayer.Radio.isActive) {
|
||||
coroutineScope.launch {
|
||||
YoutubePlayer.Radio.process(mediaSession.player)
|
||||
radio?.let { radio ->
|
||||
if (mediaSession.player.mediaItemCount - mediaSession.player.currentMediaItemIndex <= 3) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
mediaSession.player.addMediaItems(radio.process())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.with
|
||||
|
@ -13,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
|
@ -23,6 +25,8 @@ import it.vfsfitvnm.vimusic.internal
|
|||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||
import it.vfsfitvnm.vimusic.services.StartRadioCommand
|
||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
|
||||
import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute
|
||||
|
@ -145,13 +149,19 @@ fun NonQueuedMediaItemMenu(
|
|||
mediaItem = mediaItem,
|
||||
onDismiss = onDismiss,
|
||||
onStartRadio = {
|
||||
val playlistId = mediaItem.mediaMetadata.extras?.getString("playlistId")
|
||||
YoutubePlayer.Radio.setup(playlistId = playlistId)
|
||||
player?.mediaController?.forcePlay(mediaItem)
|
||||
player?.mediaController?.run {
|
||||
forcePlay(mediaItem)
|
||||
sendCustomCommand(StartRadioCommand, bundleOf(
|
||||
"videoId" to mediaItem.mediaId,
|
||||
"playlistId" to mediaItem.mediaMetadata.extras?.getString("playlistId")
|
||||
))
|
||||
}
|
||||
},
|
||||
onPlaySingle = {
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlay(mediaItem)
|
||||
player?.mediaController?.run {
|
||||
sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
forcePlay(mediaItem)
|
||||
}
|
||||
},
|
||||
onPlayNext = if (player?.playbackState == Player.STATE_READY) ({
|
||||
player.mediaController.addNext(mediaItem)
|
||||
|
|
|
@ -21,6 +21,7 @@ import coil.compose.AsyncImage
|
|||
import com.valentinilk.shimmer.shimmer
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.services.StartArtistRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.ExpandableText
|
||||
import it.vfsfitvnm.vimusic.ui.components.Message
|
||||
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
||||
|
@ -69,6 +70,7 @@ fun ArtistScreen(
|
|||
}
|
||||
|
||||
host {
|
||||
val player = LocalYoutubePlayer.current
|
||||
val density = LocalDensity.current
|
||||
val colorPalette = LocalColorPalette.current
|
||||
val typography = LocalTypography.current
|
||||
|
@ -137,8 +139,7 @@ fun ArtistScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
artist.shuffleEndpoint?.let(YoutubePlayer.Radio::setup)
|
||||
player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.shuffleEndpoint.asBundle)
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(color = colorPalette.elevatedBackground, shape = CircleShape)
|
||||
|
@ -152,8 +153,7 @@ fun ArtistScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
artist.radioEndpoint?.let(YoutubePlayer.Radio::setup)
|
||||
player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.radioEndpoint.asBundle)
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(color = colorPalette.elevatedBackground, shape = CircleShape)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -36,6 +37,7 @@ import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||
|
@ -331,11 +333,10 @@ fun HomeScreen(
|
|||
enabled = songCollection.isNotEmpty(),
|
||||
onClick = {
|
||||
menuState.hide()
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayFromBeginning(
|
||||
songCollection
|
||||
.map(SongWithInfo::asMediaItem)
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayFromBeginning(songCollection.map(SongWithInfo::asMediaItem))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -345,12 +346,10 @@ fun HomeScreen(
|
|||
enabled = songCollection.isNotEmpty(),
|
||||
onClick = {
|
||||
menuState.hide()
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayFromBeginning(
|
||||
songCollection
|
||||
.shuffled()
|
||||
.map(SongWithInfo::asMediaItem)
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -385,11 +384,10 @@ fun HomeScreen(
|
|||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
onClick = {
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayAtIndex(
|
||||
songCollection.map(SongWithInfo::asMediaItem),
|
||||
index
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index)
|
||||
}
|
||||
},
|
||||
menuContent = {
|
||||
when (preferences.homePageSongCollection) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -26,6 +27,7 @@ import it.vfsfitvnm.vimusic.R
|
|||
import it.vfsfitvnm.vimusic.internal
|
||||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.Error
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.Message
|
||||
|
@ -238,13 +240,10 @@ fun IntentUriScreen(uri: Uri) {
|
|||
song = item,
|
||||
thumbnailSizePx = density.run { 54.dp.roundToPx() },
|
||||
onClick = {
|
||||
YoutubePlayer.Radio.reset()
|
||||
|
||||
player?.mediaController?.forcePlayAtIndex(
|
||||
currentItems.value.map(
|
||||
YouTube.Item.Song::asMediaItem
|
||||
), index
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -30,6 +31,7 @@ import it.vfsfitvnm.vimusic.R
|
|||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||
import it.vfsfitvnm.vimusic.models.SongWithInfo
|
||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||
|
@ -232,12 +234,10 @@ fun LocalPlaylistScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayFromBeginning(
|
||||
playlistWithSongs.songs
|
||||
.map(SongWithInfo::asMediaItem)
|
||||
.shuffled()
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled())
|
||||
}
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(
|
||||
|
@ -254,12 +254,10 @@ fun LocalPlaylistScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayFromBeginning(
|
||||
playlistWithSongs.songs.map(
|
||||
SongWithInfo::asMediaItem
|
||||
)
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem))
|
||||
}
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(
|
||||
|
@ -282,12 +280,10 @@ fun LocalPlaylistScreen(
|
|||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
onClick = {
|
||||
YoutubePlayer.Radio.reset()
|
||||
player?.mediaController?.forcePlayAtIndex(
|
||||
playlistWithSongs.songs.map(
|
||||
SongWithInfo::asMediaItem
|
||||
), index
|
||||
)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
it.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index)
|
||||
}
|
||||
},
|
||||
menuContent = {
|
||||
InPlaylistMediaItemMenu(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -22,21 +23,22 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.media3.common.Player
|
||||
import coil.compose.AsyncImage
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.internal
|
||||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.SongInPlaylist
|
||||
import it.vfsfitvnm.vimusic.services.StopRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
|
||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||
import it.vfsfitvnm.vimusic.utils.*
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||
import it.vfsfitvnm.youtubemusic.Outcome
|
||||
import it.vfsfitvnm.youtubemusic.YouTube
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -276,14 +278,16 @@ fun PlaylistOrAlbumScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
playlistOrAlbum.items
|
||||
?.shuffled()
|
||||
?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
player?.mediaController?.forcePlayFromBeginning(mediaItems)
|
||||
}
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
playlistOrAlbum.items
|
||||
?.shuffled()
|
||||
?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
it.forcePlayFromBeginning(mediaItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(
|
||||
|
@ -300,12 +304,13 @@ fun PlaylistOrAlbumScreen(
|
|||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
YoutubePlayer.Radio.reset()
|
||||
|
||||
playlistOrAlbum.items?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
player?.mediaController?.forcePlayFromBeginning(mediaItems)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
playlistOrAlbum.items?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
it.forcePlayFromBeginning(mediaItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
|
@ -326,12 +331,13 @@ fun PlaylistOrAlbumScreen(
|
|||
authors = (song.authors ?: playlistOrAlbum.authors)?.joinToString("") { it.name },
|
||||
durationText = song.durationText,
|
||||
onClick = {
|
||||
YoutubePlayer.Radio.reset()
|
||||
|
||||
playlistOrAlbum.items?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
player?.mediaController?.forcePlayAtIndex(mediaItems, index)
|
||||
player?.mediaController?.let {
|
||||
it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY)
|
||||
playlistOrAlbum.items?.mapNotNull { song ->
|
||||
song.toMediaItem(browseId, playlistOrAlbum)
|
||||
}?.let { mediaItems ->
|
||||
it.forcePlayAtIndex(mediaItems, index)
|
||||
}
|
||||
}
|
||||
},
|
||||
startContent = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -25,6 +26,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import coil.compose.AsyncImage
|
||||
import com.valentinilk.shimmer.Shimmer
|
||||
import com.valentinilk.shimmer.ShimmerBounds
|
||||
|
@ -33,6 +35,7 @@ import com.valentinilk.shimmer.shimmer
|
|||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.services.StartRadioCommand
|
||||
import it.vfsfitvnm.vimusic.ui.components.*
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||
|
@ -42,6 +45,7 @@ import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|||
import it.vfsfitvnm.vimusic.utils.*
|
||||
import it.vfsfitvnm.youtubemusic.Outcome
|
||||
import it.vfsfitvnm.youtubemusic.YouTube
|
||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -214,17 +218,13 @@ fun SearchResultScreen(
|
|||
is YouTube.Item.Album -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
||||
is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId)
|
||||
is YouTube.Item.Playlist -> playlistOrAlbumRoute(item.info.endpoint!!.browseId)
|
||||
is YouTube.Item.Song -> {
|
||||
player?.mediaController?.forcePlay(item.asMediaItem)
|
||||
item.info.endpoint?.let {
|
||||
YoutubePlayer.Radio.setup(it, false)
|
||||
}
|
||||
is YouTube.Item.Song -> player?.mediaController?.let {
|
||||
it.forcePlay(item.asMediaItem)
|
||||
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
|
||||
}
|
||||
is YouTube.Item.Video -> {
|
||||
player?.mediaController?.forcePlay(item.asMediaItem)
|
||||
item.info.endpoint?.let {
|
||||
YoutubePlayer.Radio.setup(it, false)
|
||||
}
|
||||
is YouTube.Item.Video -> player?.mediaController?.let {
|
||||
it.forcePlay(item.asMediaItem)
|
||||
it.sendCustomCommand(StartRadioCommand, item.info.endpoint.asBundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -572,4 +572,14 @@ fun SmallArtistItem(
|
|||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val NavigationEndpoint.Endpoint.Watch?.asBundle: Bundle
|
||||
get() = this?.let {
|
||||
bundleOf(
|
||||
"videoId" to videoId,
|
||||
"playlistId" to playlistId,
|
||||
"playlistSetVideoId" to playlistSetVideoId,
|
||||
"params" to params,
|
||||
)
|
||||
} ?: Bundle.EMPTY
|
|
@ -157,50 +157,50 @@ fun CurrentPlaylistView(
|
|||
)
|
||||
}
|
||||
|
||||
if (YoutubePlayer.Radio.isActive && player != null) {
|
||||
when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) {
|
||||
is Outcome.Loading, is Outcome.Success<*> -> {
|
||||
if (nextContinuation is Outcome.Success<*>) {
|
||||
item {
|
||||
SideEffect {
|
||||
coroutineScope.launch {
|
||||
YoutubePlayer.Radio.process(
|
||||
player.mediaController,
|
||||
force = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(count = 3, key = { it }) { index ->
|
||||
SmallSongItemShimmer(
|
||||
shimmer = shimmer,
|
||||
thumbnailSizeDp = 54.dp,
|
||||
modifier = Modifier
|
||||
.alpha(1f - index * 0.125f)
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
is Outcome.Error -> item {
|
||||
Error(
|
||||
error = nextContinuation
|
||||
)
|
||||
}
|
||||
is Outcome.Recovered<*> -> item {
|
||||
Error(
|
||||
error = nextContinuation.error,
|
||||
onRetry = {
|
||||
coroutineScope.launch {
|
||||
YoutubePlayer.Radio.process(player.mediaController, force = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
// if (YoutubePlayer.Radio.isActive && player != null) {
|
||||
// when (val nextContinuation = YoutubePlayer.Radio.nextContinuation) {
|
||||
// is Outcome.Loading, is Outcome.Success<*> -> {
|
||||
// if (nextContinuation is Outcome.Success<*>) {
|
||||
// item {
|
||||
// SideEffect {
|
||||
// coroutineScope.launch {
|
||||
// YoutubePlayer.Radio.process(
|
||||
// player.mediaController,
|
||||
// force = true
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// items(count = 3, key = { it }) { index ->
|
||||
// SmallSongItemShimmer(
|
||||
// shimmer = shimmer,
|
||||
// thumbnailSizeDp = 54.dp,
|
||||
// modifier = Modifier
|
||||
// .alpha(1f - index * 0.125f)
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// is Outcome.Error -> item {
|
||||
// Error(
|
||||
// error = nextContinuation
|
||||
// )
|
||||
// }
|
||||
// is Outcome.Recovered<*> -> item {
|
||||
// Error(
|
||||
// error = nextContinuation.error,
|
||||
// onRetry = {
|
||||
// coroutineScope.launch {
|
||||
// YoutubePlayer.Radio.process(player.mediaController, force = true)
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// else -> {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,106 +1,47 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.session.MediaController
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import it.vfsfitvnm.youtubemusic.Outcome
|
||||
import it.vfsfitvnm.youtubemusic.YouTube
|
||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class YoutubePlayer(mediaController: MediaController) : PlayerState(mediaController) {
|
||||
object Radio {
|
||||
var isActive by mutableStateOf(false)
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private var videoId: String? = null
|
||||
private var playlistId: String? = null
|
||||
private var playlistSetVideoId: String? = null
|
||||
private var parameters: String? = null
|
||||
|
||||
data class Radio(
|
||||
private val videoId: String? = null,
|
||||
private val playlistId: String? = null,
|
||||
private val playlistSetVideoId: String? = null,
|
||||
private val parameters: String? = null
|
||||
) {
|
||||
var nextContinuation by mutableStateOf<Outcome<String?>>(Outcome.Initial)
|
||||
|
||||
fun setup(videoId: String? = null, playlistId: String? = null, playlistSetVideoId: String? = null, parameters: String? = null) {
|
||||
this.videoId = videoId
|
||||
this.playlistId = playlistId
|
||||
this.playlistSetVideoId = playlistSetVideoId
|
||||
this.parameters = parameters
|
||||
|
||||
isActive = true
|
||||
nextContinuation = Outcome.Initial
|
||||
}
|
||||
|
||||
fun setup(watchEndpoint: NavigationEndpoint.Endpoint.Watch?, play: Boolean = true) {
|
||||
setup(
|
||||
videoId = watchEndpoint?.videoId,
|
||||
playlistId = watchEndpoint?.playlistId,
|
||||
parameters = watchEndpoint?.params,
|
||||
playlistSetVideoId = watchEndpoint?.playlistSetVideoId
|
||||
)
|
||||
|
||||
listener?.process(play)
|
||||
}
|
||||
|
||||
suspend fun process(player: Player, force: Boolean = false, play: Boolean = false) {
|
||||
if (!isActive) return
|
||||
|
||||
if (!force && !play) {
|
||||
val isFirstSong = withContext(Dispatchers.Main) {
|
||||
player.mediaItemCount == 0 || (player.currentMediaItemIndex == 0 && player.mediaItemCount == 1)
|
||||
}
|
||||
val isNearEndSong = withContext(Dispatchers.Main) {
|
||||
player.mediaItemCount - player.currentMediaItemIndex <= 3
|
||||
}
|
||||
|
||||
if (!isFirstSong && !isNearEndSong) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun process(): List<MediaItem> {
|
||||
println("process: ${nextContinuation.valueOrNull}")
|
||||
val token = nextContinuation.valueOrNull
|
||||
|
||||
nextContinuation = Outcome.Loading
|
||||
|
||||
var mediaItems: List<MediaItem>? = null
|
||||
|
||||
nextContinuation = withContext(Dispatchers.IO) {
|
||||
YouTube.next(
|
||||
videoId = videoId ?: withContext(Dispatchers.Main) {
|
||||
player.lastMediaItem?.mediaId ?: error("This should not happen")
|
||||
},
|
||||
videoId = videoId ?: error("This should not happen"),
|
||||
playlistId = playlistId,
|
||||
params = parameters,
|
||||
playlistSetVideoId = playlistSetVideoId,
|
||||
continuation = token
|
||||
)
|
||||
}.map { nextResult ->
|
||||
nextResult.items?.map(it.vfsfitvnm.youtubemusic.YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (play) {
|
||||
player.forcePlayFromBeginning(mediaItems)
|
||||
} else {
|
||||
player.addMediaItems(mediaItems.drop(if (token == null) 1 else 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem)
|
||||
|
||||
nextResult.continuation?.takeUnless { token == nextResult.continuation }
|
||||
}.recoverWith(token)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
videoId = null
|
||||
playlistId = null
|
||||
playlistSetVideoId = null
|
||||
parameters = null
|
||||
isActive = false
|
||||
nextContinuation = Outcome.Initial
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun process(play: Boolean)
|
||||
return mediaItems ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue