Remove Outcome class

This commit is contained in:
vfsfitvnm
2022-07-01 20:19:05 +02:00
parent 17cf2454c7
commit fc9b023174
10 changed files with 172 additions and 423 deletions

View File

@@ -51,7 +51,7 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.QueuedMediaItem import it.vfsfitvnm.vimusic.models.QueuedMediaItem
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -427,9 +427,9 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second) ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second) ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
else -> { else -> {
val url = runBlocking(Dispatchers.IO) { val urlResult = runBlocking(Dispatchers.IO) {
it.vfsfitvnm.youtubemusic.YouTube.player(videoId) YouTube.player(videoId)
}.flatMap { body -> }?.mapCatching { body ->
val loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat() val loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat()
songPendingLoudnessDb[videoId] = loudnessDb songPendingLoudnessDb[videoId] = loudnessDb
@@ -462,42 +462,25 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
} }
} }
Outcome.Success(format.url) format.url
} ?: Outcome.Error.Unhandled( } ?: throw PlaybackException(
PlaybackException(
"Couldn't find a playable audio format", "Couldn't find a playable audio format",
null, null,
PlaybackException.ERROR_CODE_REMOTE_ERROR PlaybackException.ERROR_CODE_REMOTE_ERROR
) )
) else -> throw PlaybackException(
else -> Outcome.Error.Unhandled(
PlaybackException(
status, status,
null, null,
PlaybackException.ERROR_CODE_REMOTE_ERROR PlaybackException.ERROR_CODE_REMOTE_ERROR
) )
)
} }
} }
when (url) { urlResult?.getOrThrow()?.let { url ->
is Outcome.Success -> { ringBuffer.append(videoId to url.toUri())
ringBuffer.append(videoId to url.value.toUri()) dataSpec.withUri(url.toUri())
dataSpec.withUri(url.value.toUri())
.subrange(dataSpec.uriPositionOffset, chunkLength) .subrange(dataSpec.uriPositionOffset, chunkLength)
} } ?: throw PlaybackException(null, null, PlaybackException.ERROR_CODE_REMOTE_ERROR)
is Outcome.Error.Network -> throw PlaybackException(
"Couldn't reach the internet",
null,
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
is Outcome.Error.Unhandled -> throw url.throwable
else -> throw PlaybackException(
"Unexpected error",
null,
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
}
} }
} }
} }

View File

@@ -1,127 +0,0 @@
package it.vfsfitvnm.vimusic.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.utils.italic
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.Outcome
@Composable
fun <T> OutcomeItem(
outcome: Outcome<T>,
onInitialize: (() -> Unit)? = null,
onRetry: (() -> Unit)? = onInitialize,
onUninitialized: @Composable () -> Unit = {
onInitialize?.let {
SideEffect(it)
}
},
onLoading: @Composable () -> Unit = {},
onError: @Composable (Outcome.Error) -> Unit = {
Error(
error = it,
onRetry = onRetry,
)
},
onSuccess: @Composable (T) -> Unit
) {
when (outcome) {
is Outcome.Initial -> onUninitialized()
is Outcome.Loading -> onLoading()
is Outcome.Error -> onError(outcome)
is Outcome.Recovered -> onError(outcome.error)
is Outcome.Success -> onSuccess(outcome.value)
}
}
@Composable
fun Error(
error: Outcome.Error,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null
) {
Column(
verticalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Image(
painter = painterResource(R.drawable.alert_circle),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFC5F5F)),
modifier = Modifier
.padding(horizontal = 16.dp)
.size(48.dp)
)
BasicText(
text = when (error) {
is Outcome.Error.Network -> "Couldn't reach the Internet"
is Outcome.Error.Unhandled -> (error.throwable.message ?: error.throwable.toString())
},
style = LocalTypography.current.xxs.medium.secondary,
)
onRetry?.let { retry ->
BasicText(
text = "Retry",
style = LocalTypography.current.xxs.medium,
modifier = Modifier
.clickable(onClick = retry)
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp)
)
}
}
}
@Composable
fun Message(
text: String,
modifier: Modifier = Modifier,
@DrawableRes icon: Int = R.drawable.alert_circle
) {
Column(
verticalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Image(
painter = painterResource(icon),
contentDescription = null,
colorFilter = ColorFilter.tint(LocalColorPalette.current.darkGray),
modifier = Modifier
.padding(horizontal = 16.dp)
.size(36.dp)
)
BasicText(
text = text,
style = LocalTypography.current.xs.medium.secondary.italic,
)
}
}

View File

@@ -17,8 +17,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.valentinilk.shimmer.ShimmerBounds
import com.valentinilk.shimmer.rememberShimmer
import it.vfsfitvnm.route.RouteHandler 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
@@ -26,19 +24,15 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.Error
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.Message
import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.Menu import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.components.themed.MenuCloseButton
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.toNullable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -69,19 +63,21 @@ fun IntentUriScreen(uri: Uri) {
val density = LocalDensity.current val density = LocalDensity.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
var items by remember(uri) { var itemsResult by remember(uri) {
mutableStateOf<Outcome<List<YouTube.Item.Song>>>(Outcome.Loading) mutableStateOf<Result<List<YouTube.Item.Song>>?>(null)
} }
val onLoad = relaunchableEffect(uri) { val onLoad = relaunchableEffect(uri) {
items = withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
uri.getQueryParameter("list")?.let { playlistId -> itemsResult = uri.getQueryParameter("list")?.let { playlistId ->
YouTube.queue(playlistId).toNullable()?.map { songList -> YouTube.queue(playlistId)?.map { songList ->
songList songList ?: emptyList()
} }
} ?: uri.getQueryParameter("v")?.let { videoId -> } ?: uri.getQueryParameter("v")?.let { videoId ->
YouTube.song(videoId).toNullable()?.map { listOf(it) } YouTube.song(videoId)?.map { song ->
} ?: Outcome.Error.Unhandled(Error("Missing URL parameters")) song?.let { listOf(song) } ?: emptyList()
}
} ?: Result.failure(Error("Missing URL parameters"))
} }
} }
@@ -101,7 +97,8 @@ fun IntentUriScreen(uri: Uri) {
transaction { transaction {
val playlistId = Database.insert(Playlist(name = text)) val playlistId = Database.insert(Playlist(name = text))
items.valueOrNull itemsResult
?.getOrNull()
?.map(YouTube.Item.Song::asMediaItem) ?.map(YouTube.Item.Song::asMediaItem)
?.forEachIndexed { index, mediaItem -> ?.forEachIndexed { index, mediaItem ->
Database.insert(mediaItem) Database.insert(mediaItem)
@@ -159,7 +156,8 @@ fun IntentUriScreen(uri: Uri) {
onClick = { onClick = {
menuState.hide() menuState.hide()
items.valueOrNull itemsResult
?.getOrNull()
?.map(YouTube.Item.Song::asMediaItem) ?.map(YouTube.Item.Song::asMediaItem)
?.let { mediaItems -> ?.let { mediaItems ->
binder?.player?.enqueue( binder?.player?.enqueue(
@@ -185,24 +183,55 @@ fun IntentUriScreen(uri: Uri) {
} }
} }
when (val currentItems = items) {
is Outcome.Error -> item { itemsResult?.getOrNull()?.let { items ->
Error( if (items.isEmpty()) {
error = currentItems, item {
onRetry = onLoad, TextCard(icon = R.drawable.sad) {
modifier = Modifier Title(text = "No songs found")
.padding(vertical = 16.dp) Text(text = "Please try a different query or category.")
}
}
} else {
itemsIndexed(
items = items,
contentType = { _, item -> item }
) { index, item ->
SmallSongItem(
song = item,
thumbnailSizePx = density.run { 54.dp.roundToPx() },
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(items.map(YouTube.Item.Song::asMediaItem), index)
}
) )
} }
is Outcome.Recovered -> item { }
Error( } ?: itemsResult?.exceptionOrNull()?.let { throwable ->
error = currentItems.error, item {
onRetry = onLoad, LoadingOrError(
modifier = Modifier errorMessage = throwable.javaClass.canonicalName,
.padding(vertical = 16.dp) onRetry = onLoad
) )
} }
is Outcome.Loading, is Outcome.Initial -> items(count = 5) { index -> } ?: item {
LoadingOrError()
}
}
}
}
}
@Composable
private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
LoadingOrError(
errorMessage = errorMessage,
onRetry = onRetry
) {
repeat(5) { index ->
SmallSongItemShimmer( SmallSongItemShimmer(
thumbnailSizeDp = 54.dp, thumbnailSizeDp = 54.dp,
modifier = Modifier modifier = Modifier
@@ -211,33 +240,5 @@ fun IntentUriScreen(uri: Uri) {
.padding(vertical = 4.dp, horizontal = 16.dp) .padding(vertical = 4.dp, horizontal = 16.dp)
) )
} }
is Outcome.Success -> {
if (currentItems.value.isEmpty()) {
item {
Message(
text = "No songs were found",
modifier = Modifier
)
}
} else {
itemsIndexed(
items = currentItems.value,
contentType = { _, item -> item }
) { index, item ->
SmallSongItem(
song = item,
thumbnailSizePx = density.run { 54.dp.roundToPx() },
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
}
)
}
}
}
else -> {}
}
}
}
} }
} }

View File

@@ -250,9 +250,7 @@ fun SearchResultScreen(
} ?: continuationResult?.let { } ?: continuationResult?.let {
if (items.isEmpty()) { if (items.isEmpty()) {
item { item {
TextCard( TextCard(icon = R.drawable.sad) {
icon = R.drawable.sad
) {
Title(text = "No results found") Title(text = "No results found")
Text(text = "Please try a different query or category.") Text(text = "Please try a different query or category.")
} }

View File

@@ -16,7 +16,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.Message import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
@@ -64,10 +64,12 @@ fun LyricsView(
.padding(horizontal = 48.dp) .padding(horizontal = 48.dp)
) { ) {
if (lyrics.isEmpty()) { if (lyrics.isEmpty()) {
Message( TextCard(
text = "Lyrics not available", icon = R.drawable.sad
icon = R.drawable.text, ) {
) Title(text = "No results found")
Text(text = "Please try a different query or category.")
}
} else { } else {
BasicText( BasicText(
text = lyrics, text = lyrics,

View File

@@ -43,12 +43,12 @@ import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.*
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette 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.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.PlayerResponse import it.vfsfitvnm.youtubemusic.models.PlayerResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -416,7 +416,7 @@ fun PlayerView(
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
YouTube YouTube
.player(song.id) .player(song.id)
.map { body -> ?.map { body ->
Database.update( Database.update(
song.copy( song.copy(
loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(), loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
@@ -450,14 +450,14 @@ fun PlayerView(
.padding(horizontal = 32.dp) .padding(horizontal = 32.dp)
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
Error( LoadingOrError(
error = Outcome.Error.Unhandled(playerState.error!!), errorMessage = playerState.error?.javaClass?.canonicalName,
onRetry = { onRetry = {
player?.playWhenReady = true player?.playWhenReady = true
player?.prepare() player?.prepare()
playerState.error = null playerState.error = null
} }
) ) {}
} }
} }

View File

@@ -1,49 +0,0 @@
package it.vfsfitvnm.youtubemusic
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.network.*
import io.ktor.utils.io.*
fun <T> Result<T>.recoverIfCancelled(): Result<T>? {
return when (exceptionOrNull()) {
is CancellationException -> null
else -> this
}
}
suspend inline fun <reified T> Outcome<HttpResponse>.bodyCatching(): Outcome<T> {
return when (this) {
is Outcome.Success -> value.bodyCatching()
is Outcome.Recovered -> value.bodyCatching()
is Outcome.Initial -> this
is Outcome.Loading -> this
is Outcome.Error -> this
}
}
suspend inline fun HttpClient.postCatching(
urlString: String,
block: HttpRequestBuilder.() -> Unit = {}
): Outcome<HttpResponse> {
return runCatching {
Outcome.Success(post(urlString, block))
}.getOrElse { throwable ->
when (throwable) {
is CancellationException -> Outcome.Loading
is UnresolvedAddressException -> Outcome.Error.Network
else -> Outcome.Error.Unhandled(throwable)
}
}
}
suspend inline fun <reified T> HttpResponse.bodyCatching(): Outcome<T> {
return runCatching {
Outcome.Success(body<T>())
}.getOrElse { throwable ->
Outcome.Error.Unhandled(throwable)
}
}

View File

@@ -1,72 +0,0 @@
package it.vfsfitvnm.youtubemusic
sealed class Outcome<out T> {
val valueOrNull: T?
get() = when (this) {
is Success -> value
is Recovered -> value
else -> null
}
fun recoverWith(value: @UnsafeVariance T): Outcome<T> {
return when (this) {
is Error -> Recovered(value, this)
else -> this
}
}
inline fun <R> map(block: (T) -> R): Outcome<R> {
return when (this) {
is Success -> Success(block(value))
is Recovered -> Success(block(value))
is Initial -> this
is Loading -> this
is Error -> this
}
}
inline fun <R> flatMap(block: (T) -> Outcome<R>): Outcome<R> {
return when (this) {
is Success -> block(value)
is Recovered -> block(value)
is Initial -> this
is Loading -> this
is Error -> this
}
}
object Initial : Outcome<Nothing>()
object Loading : Outcome<Nothing>()
sealed class Error : Outcome<Nothing>() {
object Network : Error()
class Unhandled(val throwable: Throwable) : Error()
}
class Recovered<T>(val value: T, val error: Error) : Outcome<T>()
class Success<T>(val value: T) : Outcome<T>()
}
fun <T> Outcome<T>?.toNotNull(): Outcome<T?> {
return when (this) {
null -> Outcome.Success(null)
else -> this
}
}
fun <T> Outcome<T?>.toNullable(error: Outcome.Error? = null): Outcome<T>? {
return when (this) {
is Outcome.Success -> value?.let { Outcome.Success(it) } ?: error
is Outcome.Recovered -> value?.let { Outcome.Success(it) } ?: error
is Outcome.Initial -> this
is Outcome.Loading -> this
is Outcome.Error -> this
}
}
val Outcome<*>.isEvaluable: Boolean
get() = this !is Outcome.Success && this !is Outcome.Loading

View File

@@ -0,0 +1,11 @@
package it.vfsfitvnm.youtubemusic
import io.ktor.utils.io.*
internal fun <T> Result<T>.recoverIfCancelled(): Result<T>? {
return when (exceptionOrNull()) {
is CancellationException -> null
else -> this
}
}

View File

@@ -450,8 +450,9 @@ object YouTube {
}.recoverIfCancelled() }.recoverIfCancelled()
} }
suspend fun player(videoId: String, playlistId: String? = null): Outcome<PlayerResponse> { suspend fun player(videoId: String, playlistId: String? = null): Result<PlayerResponse>? {
return client.postCatching("/youtubei/v1/player") { return runCatching {
client.post("/youtubei/v1/player") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody( setBody(
PlayerBody( PlayerBody(
@@ -462,18 +463,19 @@ object YouTube {
) )
parameter("key", Key) parameter("key", Key)
parameter("prettyPrint", false) parameter("prettyPrint", false)
}.bodyCatching() }.body<PlayerResponse>()
}.recoverIfCancelled()
} }
private suspend fun getQueue(body: GetQueueBody): Outcome<List<Item.Song>?> { private suspend fun getQueue(body: GetQueueBody): Result<List<Item.Song>?>? {
return client.postCatching("/youtubei/v1/music/get_queue") { return runCatching {
val body = client.post("/youtubei/v1/music/get_queue") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody(body) setBody(body)
parameter("key", Key) parameter("key", Key)
parameter("prettyPrint", false) parameter("prettyPrint", false)
} }.body<GetQueueResponse>()
.bodyCatching<GetQueueResponse>()
.map { body ->
body.queueDatas?.mapNotNull { queueData -> body.queueDatas?.mapNotNull { queueData ->
queueData.content?.playlistPanelVideoRenderer?.let { renderer -> queueData.content?.playlistPanelVideoRenderer?.let { renderer ->
Item.Song( Item.Song(
@@ -507,20 +509,20 @@ object YouTube {
) )
} }
} }
} }.recoverIfCancelled()
} }
suspend fun song(videoId: String): Outcome<Item.Song?> { suspend fun song(videoId: String): Result<Item.Song?>? {
return getQueue( return getQueue(
GetQueueBody( GetQueueBody(
context = Context.DefaultWeb, context = Context.DefaultWeb,
videoIds = listOf(videoId), videoIds = listOf(videoId),
playlistId = null playlistId = null
) )
).map { it?.firstOrNull() } )?.map { it?.firstOrNull() }
} }
suspend fun queue(playlistId: String): Outcome<List<Item.Song>?> { suspend fun queue(playlistId: String): Result<List<Item.Song>?>? {
return getQueue( return getQueue(
GetQueueBody( GetQueueBody(
context = Context.DefaultWeb, context = Context.DefaultWeb,
@@ -674,7 +676,7 @@ object YouTube {
} }
suspend fun browse(browseId: String): Result<BrowseResponse>? { suspend fun browse(browseId: String): Result<BrowseResponse>? {
return runCatching<YouTube, BrowseResponse> { return runCatching {
client.post("/youtubei/v1/browse") { client.post("/youtubei/v1/browse") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody( setBody(
@@ -685,7 +687,7 @@ object YouTube {
) )
parameter("key", Key) parameter("key", Key)
parameter("prettyPrint", false) parameter("prettyPrint", false)
}.body() }.body<BrowseResponse>()
}.recoverIfCancelled() }.recoverIfCancelled()
} }