浏览代码

Cache artist information

vfsfitvnm 3 年之前
父节点
当前提交
1fb50169ff

+ 141 - 51
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt

@@ -4,13 +4,18 @@ import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.text.BasicText
 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.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.alpha
@@ -28,8 +33,9 @@ import it.vfsfitvnm.route.RouteHandler
 import it.vfsfitvnm.vimusic.Database
 import it.vfsfitvnm.vimusic.Database
 import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
 import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
 import it.vfsfitvnm.vimusic.R
 import it.vfsfitvnm.vimusic.R
+import it.vfsfitvnm.vimusic.models.Artist
 import it.vfsfitvnm.vimusic.models.DetailedSong
 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.TopAppBar
 import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
 import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
 import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
 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.styling.LocalTypography
 import it.vfsfitvnm.vimusic.ui.views.SongItem
 import it.vfsfitvnm.vimusic.ui.views.SongItem
 import it.vfsfitvnm.vimusic.utils.*
 import it.vfsfitvnm.vimusic.utils.*
-import it.vfsfitvnm.youtubemusic.Outcome
 import it.vfsfitvnm.youtubemusic.YouTube
 import it.vfsfitvnm.youtubemusic.YouTube
+import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 import kotlinx.coroutines.Dispatchers
 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
 @ExperimentalAnimationApi
@@ -51,16 +59,6 @@ fun ArtistScreen(
 ) {
 ) {
     val lazyListState = rememberLazyListState()
     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 albumRoute = rememberPlaylistOrAlbumRoute()
     val artistRoute = rememberArtistRoute()
     val artistRoute = rememberArtistRoute()
 
 
@@ -84,6 +82,26 @@ fun ArtistScreen(
             val colorPalette = LocalColorPalette.current
             val colorPalette = LocalColorPalette.current
             val typography = LocalTypography.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 {
             val (thumbnailSizeDp, thumbnailSizePx) = remember {
                 density.run {
                 density.run {
                     192.dp to 192.dp.roundToPx()
                     192.dp to 192.dp.roundToPx()
@@ -127,18 +145,19 @@ fun ArtistScreen(
                 }
                 }
 
 
                 item {
                 item {
-                    OutcomeItem(
-                        outcome = artist,
-                        onRetry = onLoad,
-                        onLoading = {
-                            Loading()
-                        }
-                    ) { artist ->
+                    artistResult?.getOrNull()?.let { artist ->
                         AsyncImage(
                         AsyncImage(
-                            model = artist.thumbnail?.size(thumbnailSizePx),
+                            model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx),
                             contentDescription = null,
                             contentDescription = null,
                             modifier = Modifier
                             modifier = Modifier
                                 .clip(CircleShape)
                                 .clip(CircleShape)
+                                .clickable {
+                                    query {
+                                        runBlocking {
+                                            Database.artist(browseId).first()?.copy(shufflePlaylistId = null)?.let(Database::update)
+                                        }
+                                    }
+                                }
                                 .size(thumbnailSizeDp)
                                 .size(thumbnailSizeDp)
                         )
                         )
 
 
@@ -160,7 +179,12 @@ fun ArtistScreen(
                                 colorFilter = ColorFilter.tint(colorPalette.text),
                                 colorFilter = ColorFilter.tint(colorPalette.text),
                                 modifier = Modifier
                                 modifier = Modifier
                                     .clickable {
                                     .clickable {
-                                        binder?.playRadio(artist.shuffleEndpoint)
+                                        binder?.playRadio(
+                                            NavigationEndpoint.Endpoint.Watch(
+                                                videoId = artist.shuffleVideoId,
+                                                playlistId = artist.shufflePlaylistId
+                                            )
+                                        )
                                     }
                                     }
                                     .shadow(elevation = 2.dp, shape = CircleShape)
                                     .shadow(elevation = 2.dp, shape = CircleShape)
                                     .background(
                                     .background(
@@ -177,7 +201,12 @@ fun ArtistScreen(
                                 colorFilter = ColorFilter.tint(colorPalette.text),
                                 colorFilter = ColorFilter.tint(colorPalette.text),
                                 modifier = Modifier
                                 modifier = Modifier
                                     .clickable {
                                     .clickable {
-                                        binder?.playRadio(artist.radioEndpoint)
+                                        binder?.playRadio(
+                                            NavigationEndpoint.Endpoint.Watch(
+                                                videoId = artist.radioVideoId,
+                                                playlistId = artist.radioPlaylistId
+                                            )
+                                        )
                                     }
                                     }
                                     .shadow(elevation = 2.dp, shape = CircleShape)
                                     .shadow(elevation = 2.dp, shape = CircleShape)
                                     .background(
                                     .background(
@@ -188,9 +217,18 @@ fun ArtistScreen(
                                     .size(20.dp)
                                     .size(20.dp)
                             )
                             )
                         }
                         }
-
-
-                    }
+                    } ?: artistResult?.exceptionOrNull()?.let { throwable ->
+                        LoadingOrError(
+                            errorMessage = throwable.javaClass.canonicalName,
+                            onRetry = {
+                                query {
+                                    runBlocking {
+                                        Database.artist(browseId).first()?.let(Database::update)
+                                    }
+                                }
+                            }
+                        )
+                    } ?: LoadingOrError()
                 }
                 }
 
 
                 item {
                 item {
@@ -219,7 +257,11 @@ fun ArtistScreen(
                             modifier = Modifier
                             modifier = Modifier
                                 .clickable(enabled = songs.isNotEmpty()) {
                                 .clickable(enabled = songs.isNotEmpty()) {
                                     binder?.stopRadio()
                                     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)
                                 .padding(horizontal = 8.dp, vertical = 8.dp)
                                 .size(20.dp)
                                 .size(20.dp)
@@ -237,7 +279,10 @@ fun ArtistScreen(
                         thumbnailSize = songThumbnailSizePx,
                         thumbnailSize = songThumbnailSizePx,
                         onClick = {
                         onClick = {
                             binder?.stopRadio()
                             binder?.stopRadio()
-                            binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
+                            binder?.player?.forcePlayAtIndex(
+                                songs.map(DetailedSong::asMediaItem),
+                                index
+                            )
                         },
                         },
                         menuContent = {
                         menuContent = {
                             InHistoryMediaItemMenu(song = song)
                             InHistoryMediaItemMenu(song = song)
@@ -245,7 +290,7 @@ fun ArtistScreen(
                     )
                     )
                 }
                 }
 
 
-                artist.valueOrNull?.description?.let { description ->
+                artistResult?.getOrNull()?.info?.let { description ->
                     item {
                     item {
                         Column(
                         Column(
                             modifier = Modifier
                             modifier = Modifier
@@ -272,33 +317,78 @@ fun ArtistScreen(
 }
 }
 
 
 @Composable
 @Composable
-private fun Loading() {
+private fun LoadingOrError(
+    errorMessage: String? = null,
+    onRetry: (() -> Unit)? = null
+) {
+    val typography = LocalTypography.current
     val colorPalette = LocalColorPalette.current
     val colorPalette = LocalColorPalette.current
 
 
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        modifier = Modifier
-            .shimmer()
-    ) {
-        Spacer(
+    Box {
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
             modifier = Modifier
             modifier = Modifier
-                .background(color = colorPalette.darkGray, shape = CircleShape)
-                .size(192.dp)
-        )
-
-        TextPlaceholder(
-            modifier = Modifier
-                .alpha(0.9f)
-                .padding(vertical = 8.dp, horizontal = 16.dp)
-        )
+                .alpha(if (errorMessage == null) 1f else 0f)
+                .shimmer()
+        ) {
+            Spacer(
+                modifier = Modifier
+                    .background(color = colorPalette.darkGray, shape = CircleShape)
+                    .size(192.dp)
+            )
 
 
-        repeat(3) {
             TextPlaceholder(
             TextPlaceholder(
                 modifier = Modifier
                 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)
+                )
+            }
         }
         }
     }
     }
 }
 }
-

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt

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

+ 2 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt

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

+ 21 - 4
youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt

@@ -1,6 +1,7 @@
 package it.vfsfitvnm.youtubemusic
 package it.vfsfitvnm.youtubemusic
 
 
 import io.ktor.client.*
 import io.ktor.client.*
+import io.ktor.client.call.*
 import io.ktor.client.engine.cio.*
 import io.ktor.client.engine.cio.*
 import io.ktor.client.plugins.*
 import io.ktor.client.plugins.*
 import io.ktor.client.plugins.compression.*
 import io.ktor.client.plugins.compression.*
@@ -71,7 +72,7 @@ object YouTube {
     data class NextBody(
     data class NextBody(
         val context: Context,
         val context: Context,
         val isAudioOnly: Boolean,
         val isAudioOnly: Boolean,
-        val videoId: String,
+        val videoId: String?,
         val playlistId: String?,
         val playlistId: String?,
         val tunerSettingValue: String,
         val tunerSettingValue: String,
         val index: Int?,
         val index: Int?,
@@ -532,7 +533,7 @@ object YouTube {
     }
     }
 
 
     suspend fun next(
     suspend fun next(
-        videoId: String,
+        videoId: String?,
         playlistId: String?,
         playlistId: String?,
         index: Int? = null,
         index: Int? = null,
         params: String? = null,
         params: String? = null,
@@ -688,6 +689,22 @@ object YouTube {
         }.bodyCatching()
         }.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(
     open class PlaylistOrAlbum(
         val title: String?,
         val title: String?,
         val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
         val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
@@ -821,8 +838,8 @@ object YouTube {
         val radioEndpoint: NavigationEndpoint.Endpoint.Watch?
         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(
             Artist(
                 name = body
                 name = body
                     .header
                     .header

+ 1 - 1
youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt

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