Cache artist information
This commit is contained in:
parent
309a1a4237
commit
1fb50169ff
5 changed files with 166 additions and 59 deletions
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue