diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt index aab6f5c..97b1284 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt @@ -17,17 +17,16 @@ import androidx.compose.ui.unit.dp import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.Message -import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.Outcome + @Composable fun LyricsView( - lyricsOutcome: Outcome, + lyrics: String?, onInitialize: () -> Unit, onSearchOnline: () -> Unit, onLyricsUpdate: (String) -> Unit, @@ -42,7 +41,7 @@ fun LyricsView( if (isEditingLyrics) { TextFieldDialog( hintText = "Enter the lyrics", - initialTextInput = lyricsOutcome.valueOrNull ?: "", + initialTextInput = lyrics ?: "", singleLine = false, maxLines = 10, isTextInputValid = { true }, @@ -53,26 +52,7 @@ fun LyricsView( ) } - OutcomeItem( - outcome = lyricsOutcome, - onInitialize = onInitialize, - onLoading = { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .shimmer() - ) { - repeat(16) { index -> - TextPlaceholder( - modifier = Modifier - .alpha(1f - index * 0.05f) - ) - } - } - } - ) { lyrics -> + if (lyrics != null ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -125,5 +105,22 @@ fun LyricsView( ) } } + } else { + SideEffect(onInitialize) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .shimmer() + ) { + repeat(16) { index -> + TextPlaceholder( + modifier = Modifier + .alpha(1f - index * 0.05f) + ) + } + } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index 09cd6a3..44f2b67 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -34,11 +34,11 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography -import it.vfsfitvnm.vimusic.utils.* -import it.vfsfitvnm.youtubemusic.Outcome +import it.vfsfitvnm.vimusic.utils.PlayerState +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.color +import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.youtubemusic.YouTube -import it.vfsfitvnm.youtubemusic.isEvaluable -import it.vfsfitvnm.youtubemusic.toNotNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -62,8 +62,8 @@ fun PlayerBottomSheet( var route by rememberRoute() - var nextOutcome by remember(playerState?.mediaItem?.mediaId) { - mutableStateOf>(Outcome.Initial) + var nextResult by remember(playerState?.mediaItem?.mediaId) { + mutableStateOf?>(null) } BottomSheet( @@ -150,8 +150,8 @@ fun PlayerBottomSheet( } } ) { - var lyricsOutcome by remember(song) { - mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial) + var lyricsResult by remember(song) { + mutableStateOf(song?.lyrics?.let { Result.success(it) }) } RouteHandler( @@ -181,21 +181,16 @@ fun PlayerBottomSheet( val context = LocalContext.current LyricsView( - lyricsOutcome = lyricsOutcome, + lyrics = lyricsResult?.getOrNull(), nestedScrollConnectionProvider = layoutState::nestedScrollConnection, onInitialize = { coroutineScope.launch(Dispatchers.Main) { - lyricsOutcome = Outcome.Loading - val mediaItem = player?.currentMediaItem!! - if (nextOutcome.isEvaluable) { - nextOutcome = Outcome.Loading - - + if (nextResult == null) { val mediaItemIndex = player.currentMediaItemIndex - nextOutcome = withContext(Dispatchers.IO) { + nextResult = withContext(Dispatchers.IO) { YouTube.next( mediaItem.mediaId, mediaItem.mediaMetadata.extras?.getString("playlistId"), @@ -204,11 +199,9 @@ fun PlayerBottomSheet( } } - lyricsOutcome = nextOutcome.flatMap { - it.lyrics?.text().toNotNull() - }.map { lyrics -> - lyrics ?: "" - }.map { lyrics -> + lyricsResult = nextResult?.map { nextResult -> + nextResult.lyrics?.text()?.getOrNull() ?: "" + }?.map { lyrics -> query { song?.let { Database.update(song.copy(lyrics = lyrics)) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt similarity index 76% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt index 771d1e2..6e0cec7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubePlayer.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt @@ -5,6 +5,7 @@ import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext + data class YouTubeRadio( private val videoId: String? = null, private val playlistId: String? = null, @@ -23,11 +24,11 @@ data class YouTubeRadio( params = parameters, playlistSetVideoId = playlistSetVideoId, continuation = nextContinuation - ) - }.map { nextResult -> - mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem) - nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation } - }.recoverWith(nextContinuation).valueOrNull + )?.getOrNull()?.let { nextResult -> + mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem) + nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation } + } + } return mediaItems ?: emptyList() } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Extensions.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Extensions.kt index e1e8753..d558d12 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Extensions.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Extensions.kt @@ -8,6 +8,13 @@ import io.ktor.util.network.* import io.ktor.utils.io.* +fun Result.recoverIfCancelled(): Result? { + return when (exceptionOrNull()) { + is CancellationException -> null + else -> this + } +} + suspend inline fun Outcome.bodyCatching(): Outcome { return when (this) { is Outcome.Success -> value.bodyCatching() @@ -39,4 +46,4 @@ suspend inline fun HttpResponse.bodyCatching(): Outcome { }.getOrElse { throwable -> Outcome.Error.Unhandled(throwable) } -} \ No newline at end of file +} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 3435004..c9990e0 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -448,7 +448,7 @@ object YouTube { .searchEndpoint ?.query } - } + } } } @@ -539,109 +539,109 @@ object YouTube { params: String? = null, playlistSetVideoId: String? = null, continuation: String? = null, - ): Outcome { - return client.postCatching("/youtubei/v1/next") { - contentType(ContentType.Application.Json) - setBody( - NextBody( - context = Context.DefaultWeb, - videoId = videoId, - playlistId = playlistId, - isAudioOnly = true, - tunerSettingValue = "AUTOMIX_SETTING_NORMAL", - watchEndpointMusicSupportedConfigs = NextBody.WatchEndpointMusicSupportedConfigs( - musicVideoType = "MUSIC_VIDEO_TYPE_ATV" - ), - index = index, - playlistSetVideoId = playlistSetVideoId, - params = params, - continuation = continuation - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - } - .bodyCatching() - .map { body -> - val tabs = body - .contents - .singleColumnMusicWatchNextResultsRenderer - .tabbedRenderer - .watchNextTabbedResultsRenderer - .tabs - - NextResult( - continuation = (tabs - .getOrNull(0) - ?.tabRenderer - ?.content - ?.musicQueueRenderer - ?.content - ?: body.continuationContents) - ?.playlistPanelRenderer - ?.continuations - ?.getOrNull(0) - ?.nextRadioContinuationData - ?.continuation, - items = (tabs - .getOrNull(0) - ?.tabRenderer - ?.content - ?.musicQueueRenderer - ?.content - ?: body.continuationContents) - ?.playlistPanelRenderer - ?.contents - ?.mapNotNull { it.playlistPanelVideoRenderer } - ?.mapNotNull { renderer -> - Item.Song( - info = Info( - name = renderer - .title - ?.text ?: return@mapNotNull null, - endpoint = renderer - .navigationEndpoint - .watchEndpoint - ), - authors = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(0) - ?.map { run -> Info.from(run) } - ?: emptyList(), - album = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(1) - ?.getOrNull(0) - ?.let { run -> Info.from(run) }, - thumbnail = renderer - .thumbnail - .thumbnails - .firstOrNull(), - durationText = renderer - .lengthText - ?.text - ) - }, - lyrics = NextResult.Lyrics( - browseId = tabs - .getOrNull(1) - ?.tabRenderer - ?.endpoint - ?.browseEndpoint - ?.browseId - ), - related = NextResult.Related( - browseId = tabs - .getOrNull(2) - ?.tabRenderer - ?.endpoint - ?.browseEndpoint - ?.browseId + ): Result? { + return runCatching { + val body = client.post("/youtubei/v1/next") { + contentType(ContentType.Application.Json) + setBody( + NextBody( + context = Context.DefaultWeb, + videoId = videoId, + playlistId = playlistId, + isAudioOnly = true, + tunerSettingValue = "AUTOMIX_SETTING_NORMAL", + watchEndpointMusicSupportedConfigs = NextBody.WatchEndpointMusicSupportedConfigs( + musicVideoType = "MUSIC_VIDEO_TYPE_ATV" + ), + index = index, + playlistSetVideoId = playlistSetVideoId, + params = params, + continuation = continuation ) ) - } + parameter("key", Key) + parameter("prettyPrint", false) + }.body() + + val tabs = body + .contents + .singleColumnMusicWatchNextResultsRenderer + .tabbedRenderer + .watchNextTabbedResultsRenderer + .tabs + + NextResult( + continuation = (tabs + .getOrNull(0) + ?.tabRenderer + ?.content + ?.musicQueueRenderer + ?.content + ?: body.continuationContents) + ?.playlistPanelRenderer + ?.continuations + ?.getOrNull(0) + ?.nextRadioContinuationData + ?.continuation, + items = (tabs + .getOrNull(0) + ?.tabRenderer + ?.content + ?.musicQueueRenderer + ?.content + ?: body.continuationContents) + ?.playlistPanelRenderer + ?.contents + ?.mapNotNull { it.playlistPanelVideoRenderer } + ?.mapNotNull { renderer -> + Item.Song( + info = Info( + name = renderer + .title + ?.text ?: return@mapNotNull null, + endpoint = renderer + .navigationEndpoint + .watchEndpoint + ), + authors = renderer + .longBylineText + ?.splitBySeparator() + ?.getOrNull(0) + ?.map { run -> Info.from(run) } + ?: emptyList(), + album = renderer + .longBylineText + ?.splitBySeparator() + ?.getOrNull(1) + ?.getOrNull(0) + ?.let { run -> Info.from(run) }, + thumbnail = renderer + .thumbnail + .thumbnails + .firstOrNull(), + durationText = renderer + .lengthText + ?.text + ) + }, + lyrics = NextResult.Lyrics( + browseId = tabs + .getOrNull(1) + ?.tabRenderer + ?.endpoint + ?.browseEndpoint + ?.browseId + ), + related = NextResult.Related( + browseId = tabs + .getOrNull(2) + ?.tabRenderer + ?.endpoint + ?.browseEndpoint + ?.browseId + ) + ) + }.recoverIfCancelled() } data class NextResult( @@ -653,11 +653,11 @@ object YouTube { class Lyrics( val browseId: String?, ) { - suspend fun text(): Outcome { + suspend fun text(): Result { return if (browseId == null) { - Outcome.Success(null) + Result.success(null) } else { - browse(browseId).map { body -> + browse2(browseId).map { body -> body.contents .sectionListRenderer ?.contents @@ -690,7 +690,7 @@ object YouTube { } suspend fun browse2(browseId: String): Result { - return runCatching { + return runCatching { client.post("/youtubei/v1/browse") { contentType(ContentType.Application.Json) setBody(