Cache artist information

This commit is contained in:
vfsfitvnm 2022-06-29 20:15:43 +02:00
parent 309a1a4237
commit 1fb50169ff
5 changed files with 166 additions and 59 deletions

View file

@ -4,13 +4,18 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -28,8 +33,9 @@ import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
@ -37,11 +43,13 @@ 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.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
@ExperimentalAnimationApi
@ -51,16 +59,6 @@ fun ArtistScreen(
) {
val lazyListState = rememberLazyListState()
var artist by remember {
mutableStateOf<Outcome<YouTube.Artist>>(Outcome.Loading)
}
val onLoad = relaunchableEffect(Unit) {
artist = withContext(Dispatchers.IO) {
YouTube.artist(browseId)
}
}
val albumRoute = rememberPlaylistOrAlbumRoute()
val artistRoute = rememberArtistRoute()
@ -84,6 +82,26 @@ fun ArtistScreen(
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
val artistResult by remember(browseId) {
Database.artist(browseId).map { artist ->
artist?.takeIf {
artist.shufflePlaylistId != null
}?.let(Result.Companion::success) ?: YouTube.artist(browseId)
.map { youtubeArtist ->
Artist(
id = browseId,
name = youtubeArtist.name,
thumbnailUrl = youtubeArtist.thumbnail?.url,
info = youtubeArtist.description,
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
shufflePlaylistId = youtubeArtist.shuffleEndpoint?.playlistId,
radioVideoId = youtubeArtist.radioEndpoint?.videoId,
radioPlaylistId = youtubeArtist.radioEndpoint?.playlistId,
).also(Database::update)
}
}.distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO)
val (thumbnailSizeDp, thumbnailSizePx) = remember {
density.run {
192.dp to 192.dp.roundToPx()
@ -127,18 +145,19 @@ fun ArtistScreen(
}
item {
OutcomeItem(
outcome = artist,
onRetry = onLoad,
onLoading = {
Loading()
}
) { artist ->
artistResult?.getOrNull()?.let { artist ->
AsyncImage(
model = artist.thumbnail?.size(thumbnailSizePx),
model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.clickable {
query {
runBlocking {
Database.artist(browseId).first()?.copy(shufflePlaylistId = null)?.let(Database::update)
}
}
}
.size(thumbnailSizeDp)
)
@ -160,7 +179,12 @@ fun ArtistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.playRadio(artist.shuffleEndpoint)
binder?.playRadio(
NavigationEndpoint.Endpoint.Watch(
videoId = artist.shuffleVideoId,
playlistId = artist.shufflePlaylistId
)
)
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@ -177,7 +201,12 @@ fun ArtistScreen(
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.playRadio(artist.radioEndpoint)
binder?.playRadio(
NavigationEndpoint.Endpoint.Watch(
videoId = artist.radioVideoId,
playlistId = artist.radioPlaylistId
)
)
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@ -188,9 +217,18 @@ fun ArtistScreen(
.size(20.dp)
)
}
}
} ?: artistResult?.exceptionOrNull()?.let { throwable ->
LoadingOrError(
errorMessage = throwable.javaClass.canonicalName,
onRetry = {
query {
runBlocking {
Database.artist(browseId).first()?.let(Database::update)
}
}
}
)
} ?: LoadingOrError()
}
item {
@ -219,7 +257,11 @@ fun ArtistScreen(
modifier = Modifier
.clickable(enabled = songs.isNotEmpty()) {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(songs.shuffled().map(DetailedSong::asMediaItem))
binder?.player?.forcePlayFromBeginning(
songs
.shuffled()
.map(DetailedSong::asMediaItem)
)
}
.padding(horizontal = 8.dp, vertical = 8.dp)
.size(20.dp)
@ -237,7 +279,10 @@ fun ArtistScreen(
thumbnailSize = songThumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
},
menuContent = {
InHistoryMediaItemMenu(song = song)
@ -245,7 +290,7 @@ fun ArtistScreen(
)
}
artist.valueOrNull?.description?.let { description ->
artistResult?.getOrNull()?.info?.let { description ->
item {
Column(
modifier = Modifier
@ -272,33 +317,78 @@ fun ArtistScreen(
}
@Composable
private fun Loading() {
private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
val typography = LocalTypography.current
val colorPalette = LocalColorPalette.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.shimmer()
) {
Spacer(
Box {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(color = colorPalette.darkGray, shape = CircleShape)
.size(192.dp)
)
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = CircleShape)
.size(192.dp)
)
TextPlaceholder(
modifier = Modifier
.alpha(0.9f)
.padding(vertical = 8.dp, horizontal = 16.dp)
)
repeat(3) {
TextPlaceholder(
modifier = Modifier
.alpha(0.8f)
.padding(horizontal = 16.dp)
.alpha(0.9f)
.padding(vertical = 8.dp, horizontal = 16.dp)
)
repeat(3) {
TextPlaceholder(
modifier = Modifier
.alpha(0.8f)
.padding(horizontal = 16.dp)
)
}
}
errorMessage?.let {
Column(
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 16.dp, vertical = 16.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
enabled = onRetry != null,
onClick = onRetry ?: {}
)
.background(colorPalette.lightBackground)
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
Image(
painter = painterResource(R.drawable.alert_circle),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.red),
modifier = Modifier
.padding(bottom = 16.dp)
.size(24.dp)
)
BasicText(
text = onRetry?.let { "Tap to retry" } ?: "Error",
style = typography.xxs.semiBold,
modifier = Modifier
.padding(horizontal = 16.dp)
)
BasicText(
text = "An error has occurred:\n$errorMessage",
style = typography.xxs.secondary,
modifier = Modifier
.padding(horizontal = 16.dp)
)
}
}
}
}

View file

@ -24,7 +24,7 @@ data class YouTubeRadio(
nextContinuation = withContext(Dispatchers.IO) {
YouTube.next(
videoId = videoId ?: error("This should not happen"),
videoId = videoId,
playlistId = playlistId,
params = parameters,
playlistSetVideoId = playlistSetVideoId,

View file

@ -75,7 +75,7 @@ fun Database.insert(mediaItem: MediaItem): Song {
val YouTube.Item.Song.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(info.endpoint!!.videoId)
.setMediaId(info.endpoint!!.videoId!!)
.setUri(info.endpoint!!.videoId)
.setCustomCacheKey(info.endpoint!!.videoId)
.setMediaMetadata(
@ -99,7 +99,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem
val YouTube.Item.Video.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(info.endpoint!!.videoId)
.setMediaId(info.endpoint!!.videoId!!)
.setUri(info.endpoint!!.videoId)
.setCustomCacheKey(info.endpoint!!.videoId)
.setMediaMetadata(

View file

@ -1,6 +1,7 @@
package it.vfsfitvnm.youtubemusic
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.compression.*
@ -71,7 +72,7 @@ object YouTube {
data class NextBody(
val context: Context,
val isAudioOnly: Boolean,
val videoId: String,
val videoId: String?,
val playlistId: String?,
val tunerSettingValue: String,
val index: Int?,
@ -532,7 +533,7 @@ object YouTube {
}
suspend fun next(
videoId: String,
videoId: String?,
playlistId: String?,
index: Int? = null,
params: String? = null,
@ -688,6 +689,22 @@ object YouTube {
}.bodyCatching()
}
suspend fun browse2(browseId: String): Result<BrowseResponse> {
return runCatching {
client.post("/youtubei/v1/browse") {
contentType(ContentType.Application.Json)
setBody(
BrowseBody(
browseId = browseId,
context = Context.DefaultWeb
)
)
parameter("key", Key)
parameter("prettyPrint", false)
}.body()
}
}
open class PlaylistOrAlbum(
val title: String?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
@ -821,8 +838,8 @@ object YouTube {
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?
)
suspend fun artist(browseId: String): Outcome<Artist> {
return browse(browseId).map { body ->
suspend fun artist(browseId: String): Result<Artist> {
return browse2(browseId).map { body ->
Artist(
name = body
.header

View file

@ -62,7 +62,7 @@ data class NavigationEndpoint(
data class Watch(
val params: String? = null,
val playlistId: String? = null,
val videoId: String,
val videoId: String? = null,
val index: Int? = null,
val playlistSetVideoId: String? = null,
val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs? = null,