Rename youtube-music module to innertube and rewrite it

This commit is contained in:
vfsfitvnm 2022-10-02 15:25:07 +02:00
parent 4bc3671be1
commit 917e194d63
126 changed files with 2210 additions and 2145 deletions

View file

@ -65,6 +65,10 @@ android {
freeCompilerArgs += "-Xcontext-receivers"
jvmTarget = "1.8"
}
packagingOptions {
resources.excludes.add("META-INF/INDEX.LIST")
}
}
kapt {
@ -93,7 +97,7 @@ dependencies {
kapt(libs.room.compiler)
annotationProcessor(libs.room.compiler)
implementation(projects.youtubeMusic)
implementation(projects.innertube)
implementation(projects.kugou)
coreLibraryDesugaring(libs.desugaring)

View file

@ -81,7 +81,10 @@ import it.vfsfitvnm.vimusic.utils.intent
import it.vfsfitvnm.vimusic.utils.listener
import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.playlistPage
import it.vfsfitvnm.youtubemusic.requests.song
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -376,8 +379,8 @@ class MainActivity : ComponentActivity() {
val browseId = "VL$playlistId"
if (playlistId.startsWith("OLAK5uy_")) {
YouTube.playlist(browseId)?.getOrNull()?.let { playlist ->
playlist.songs?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId ->
Innertube.playlistPage(BrowseBody(browseId = browseId))?.getOrNull()?.let { playlist ->
playlist.songsPage?.items?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId ->
albumRoute.ensureGlobal(browseId)
}
}
@ -385,7 +388,7 @@ class MainActivity : ComponentActivity() {
playlistRoute.ensureGlobal(browseId)
}
} ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId ->
YouTube.song(videoId)?.getOrNull()?.let { song ->
Innertube.song(videoId)?.getOrNull()?.let { song ->
withContext(Dispatchers.Main) {
binder?.player?.forcePlay(song.asMediaItem)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val AlbumListSaver = ListSaver.of(AlbumSaver)

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val AlbumResultSaver = resultSaver(AlbumSaver)

View file

@ -27,3 +27,7 @@ object AlbumSaver : Saver<Album, List<Any?>> {
bookmarkedAt = value[7] as Long?,
)
}
val AlbumResultSaver = resultSaver(AlbumSaver)
val AlbumListSaver = listSaver(AlbumSaver)

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val ArtistListSaver = ListSaver.of(ArtistSaver)

View file

@ -23,3 +23,5 @@ object ArtistSaver : Saver<Artist, List<Any?>> {
bookmarkedAt = value[5] as Long?,
)
}
val ArtistListSaver = listSaver(ArtistSaver)

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val DetailedSongListSaver = ListSaver.of(DetailedSongSaver)

View file

@ -29,3 +29,5 @@ object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
artists = (value[7] as List<List<String>>?)?.let(InfoListSaver::restore)
)
}
val DetailedSongListSaver = listSaver(DetailedSongSaver)

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val InfoListSaver = ListSaver.of(InfoSaver)

View file

@ -11,3 +11,5 @@ object InfoSaver : Saver<Info, List<String>> {
return if (value.size == 2) Info(id = value[0], name = value[1]) else null
}
}
val InfoListSaver = listSaver(InfoSaver)

View file

@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeAlbumItemSaver : Saver<Innertube.AlbumItem, List<Any?>> {
override fun SaverScope.save(value: Innertube.AlbumItem): List<Any?> = listOf(
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
value.year,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.AlbumItem(
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
)
}
val InnertubeAlbumItemListSaver = listSaver(InnertubeAlbumItemSaver)

View file

@ -0,0 +1,21 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeArtistItemSaver : Saver<Innertube.ArtistItem, List<Any?>> {
override fun SaverScope.save(value: Innertube.ArtistItem): List<Any?> = listOf(
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
value.subscribersCountText,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = Innertube.ArtistItem(
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
subscribersCountText = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
)
}
val InnertubeArtistItemListSaver = listSaver(InnertubeArtistItemSaver)

View file

@ -0,0 +1,36 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeArtistPageSaver : Saver<Innertube.ArtistPage, List<Any?>> {
override fun SaverScope.save(value: Innertube.ArtistPage) = listOf(
value.name,
value.description,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } },
value.shuffleEndpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
value.radioEndpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
value.songs?.let { with(InnertubeSongItemListSaver) { save(it) } },
value.songsEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
value.albums?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
value.albumsEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
value.singles?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
value.singlesEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ArtistPage(
name = value[0] as String?,
description = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
shuffleEndpoint = (value[3] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore),
radioEndpoint = (value[4] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore),
songs = (value[5] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
songsEndpoint = (value[6] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
albums = (value[7] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
albumsEndpoint = (value[8] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
singles = (value[9] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
singlesEndpoint = (value[10] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
)
}

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeBrowseEndpointSaver : Saver<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
object InnertubeBrowseEndpointSaver : Saver<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
value.browseId,
value.params

View file

@ -0,0 +1,20 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object InnertubeBrowseInfoSaver : Saver<Innertube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
override fun SaverScope.save(value: Innertube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
value.name,
value.endpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = Innertube.Info(
name = value[0] as String?,
endpoint = (value[1] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore)
)
}
val InnertubeBrowseInfoListSaver = listSaver(InnertubeBrowseInfoSaver)

View file

@ -0,0 +1,31 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeSongsPageSaver : Saver<Innertube.ItemsPage<Innertube.SongItem>, List<Any?>> {
override fun SaverScope.save(value: Innertube.ItemsPage<Innertube.SongItem>) = listOf(
value.items?.let {with(InnertubeSongItemListSaver) { save(it) } },
value.continuation
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
items = (value[0] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
continuation = value[1] as String?
)
}
object InnertubeAlbumsPageSaver : Saver<Innertube.ItemsPage<Innertube.AlbumItem>, List<Any?>> {
override fun SaverScope.save(value: Innertube.ItemsPage<Innertube.AlbumItem>) = listOf(
value.items?.let {with(InnertubeAlbumItemListSaver) { save(it) } },
value.continuation
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
items = (value[0] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
continuation = value[1] as String?
)
}

View file

@ -0,0 +1,23 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubePlaylistItemSaver : Saver<Innertube.PlaylistItem, List<Any?>> {
override fun SaverScope.save(value: Innertube.PlaylistItem): List<Any?> = listOf(
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
value.channel?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
value.songCount,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = Innertube.PlaylistItem(
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
channel = (value[1] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
songCount = value[2] as Int?,
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
)
}
val InnertubePlaylistItemListSaver = listSaver(InnertubePlaylistItemSaver)

View file

@ -0,0 +1,26 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage, List<Any?>> {
override fun SaverScope.save(value: Innertube.PlaylistOrAlbumPage): List<Any?> = listOf(
value.title,
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } } ,
value.year,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } ,
value.url,
value.songsPage?.let { with(InnertubeSongsPageSaver) { save(it) } },
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.PlaylistOrAlbumPage(
title = value[0] as String?,
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
url = value[4] as String?,
songsPage = (value[5] as List<Any?>?)?.let(InnertubeSongsPageSaver::restore),
)
}

View file

@ -0,0 +1,22 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeRelatedPageSaver : Saver<Innertube.RelatedPage, List<Any?>> {
override fun SaverScope.save(value: Innertube.RelatedPage): List<Any?> = listOf(
value.songs?.let { with(InnertubeSongItemListSaver) { save(it) } },
value.playlists?.let { with(InnertubePlaylistItemListSaver) { save(it) } },
value.albums?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
value.artists?.let { with(InnertubeArtistItemListSaver) { save(it) } },
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.RelatedPage(
songs = (value[0] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
playlists = (value[1] as List<List<Any?>>?)?.let(InnertubePlaylistItemListSaver::restore),
albums = (value[2] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
artists = (value[3] as List<List<Any?>>?)?.let(InnertubeArtistItemListSaver::restore),
)
}

View file

@ -0,0 +1,26 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeSongItemSaver : Saver<Innertube.SongItem, List<Any?>> {
override fun SaverScope.save(value: Innertube.SongItem): List<Any?> = listOf(
value.info?.let { with(InnertubeWatchInfoSaver) { save(it) } },
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
value.album?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
value.durationText,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.SongItem(
info = (value[0] as List<Any?>?)?.let(InnertubeWatchInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
album = (value[2] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
)
}
val InnertubeSongItemListSaver = listSaver(InnertubeSongItemSaver)

View file

@ -0,0 +1,19 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.Thumbnail
object InnertubeThumbnailSaver : Saver<Thumbnail, List<Any?>> {
override fun SaverScope.save(value: Thumbnail) = listOf(
value.url,
value.width,
value.height
)
override fun restore(value: List<Any?>) = Thumbnail(
url = value[0] as String,
width = value[1] as Int,
height = value[2] as Int?,
)
}

View file

@ -0,0 +1,26 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeVideoItemSaver : Saver<Innertube.VideoItem, List<Any?>> {
override fun SaverScope.save(value: Innertube.VideoItem): List<Any?> = listOf(
value.info?.let { with(InnertubeWatchInfoSaver) { save(it) } },
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
value.viewsText,
value.durationText,
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.VideoItem(
info = (value[0] as List<Any?>?)?.let(InnertubeWatchInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
viewsText = value[2] as String?,
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
)
}
val InnertubeVideoItemListSaver = listSaver(InnertubeVideoItemSaver)

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeWatchEndpointSaver : Saver<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
object InnertubeWatchEndpointSaver : Saver<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
value.params,
value.playlistId,

View file

@ -0,0 +1,18 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object InnertubeWatchInfoSaver : Saver<Innertube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
override fun SaverScope.save(value: Innertube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
value.name,
value.endpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
)
override fun restore(value: List<Any?>) = Innertube.Info(
name = value[0] as String?,
endpoint = (value[1] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore)
)
}

View file

@ -1,23 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
override fun SaverScope.save(value: List<Original>): List<Saveable>
override fun restore(value: List<Saveable>): List<Original>
companion object {
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>): ListSaver<Original, Saveable> {
return object : ListSaver<Original, Saveable> {
override fun restore(value: List<Saveable>): List<Original> {
return value.mapNotNull(saver::restore)
}
override fun SaverScope.save(value: List<Original>): List<Saveable> {
return with(saver) { value.mapNotNull { save(it) } }
}
}
}
}
}

View file

@ -1,13 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
fun <Original, Saveable : Any> nullableSaver(saver: Saver<Original, Saveable>) =
object : Saver<Original?, Saveable> {
override fun SaverScope.save(value: Original?): Saveable? =
value?.let { with(saver) { save(it) } }
override fun restore(value: Saveable): Original? =
saver.restore(value)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver)

View file

@ -4,18 +4,16 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.PlaylistPreview
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any?>> {
override fun SaverScope.save(value: PlaylistPreview): List<Any> {
return listOf(
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any>> {
override fun SaverScope.save(value: PlaylistPreview) = listOf(
with(PlaylistSaver) { save(value.playlist) },
value.songCount,
)
}
override fun restore(value: List<Any?>): PlaylistPreview? {
return if (value.size == 2) PlaylistPreview(
override fun restore(value: List<Any>) = PlaylistPreview(
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
songCount = value[1] as Int,
) else null
}
)
}
val PlaylistPreviewListSaver = listSaver(PlaylistPreviewSaver)

View file

@ -4,13 +4,11 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs?, List<Any>> {
override fun SaverScope.save(value: PlaylistWithSongs?) = value?.let {
listOf(
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs, List<Any>> {
override fun SaverScope.save(value: PlaylistWithSongs) = listOf(
with(PlaylistSaver) { save(value.playlist) },
with(DetailedSongListSaver) { save(value.songs) },
)
}
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any>): PlaylistWithSongs = PlaylistWithSongs(

View file

@ -1,14 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
fun <Original, Saveable : Any> resultSaver(saver: Saver<Original, Saveable>) =
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
override fun restore(value: Pair<Saveable?, Throwable?>) =
value.first?.let(saver::restore)?.let(Result.Companion::success)
?: value.second?.let(Result.Companion::failure)
override fun SaverScope.save(value: Result<Original>?) =
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
}

View file

@ -0,0 +1,37 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
override fun SaverScope.save(value: List<Original>): List<Saveable>
override fun restore(value: List<Saveable>): List<Original>
}
fun <Original, Saveable : Any> resultSaver(saver: Saver<Original, Saveable>) =
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
override fun restore(value: Pair<Saveable?, Throwable?>) =
value.first?.let(saver::restore)?.let(Result.Companion::success)
?: value.second?.let(Result.Companion::failure)
override fun SaverScope.save(value: Result<Original>?) =
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
}
fun <Original, Saveable : Any> listSaver(saver: Saver<Original, Saveable>) =
object : ListSaver<Original, Saveable> {
override fun restore(value: List<Saveable>) =
value.mapNotNull(saver::restore)
override fun SaverScope.save(value: List<Original>) =
with(saver) { value.mapNotNull { save(it) } }
}
fun <Original, Saveable : Any> nullableSaver(saver: Saver<Original, Saveable>) =
object : Saver<Original?, Saveable> {
override fun SaverScope.save(value: Original?): Saveable? =
value?.let { with(saver) { save(it) } }
override fun restore(value: Saveable): Original? =
saver.restore(value)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val SearchQueryListSaver = ListSaver.of(SearchQuerySaver)

View file

@ -15,3 +15,5 @@ object SearchQuerySaver : Saver<SearchQuery, List<Any?>> {
query = value[1] as String
)
}
val SearchQueryListSaver = listSaver(SearchQuerySaver)

View file

@ -1,5 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.autoSaver
val StringListResultSaver = resultSaver(autoSaver<List<String>?>())

View file

@ -1,5 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.autoSaver
val StringResultSaver = resultSaver(autoSaver<String?>())

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver)

View file

@ -1,22 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = listOf(
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } },
value.year,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Album(
info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver)

View file

@ -1,36 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeArtistPageSaver : Saver<YouTube.Artist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Artist): List<Any?> = listOf(
value.name,
value.description,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } },
value.shuffleEndpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
value.radioEndpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
value.songs?.let { with(YouTubeSongListSaver) { save(it) } },
value.songsEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
value.albums?.let { with(YouTubeAlbumListSaver) { save(it) } },
value.albumsEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
value.singles?.let { with(YouTubeAlbumListSaver) { save(it) } },
value.singlesEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Artist(
name = value[0] as String?,
description = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore),
shuffleEndpoint = (value[3] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore),
radioEndpoint = (value[4] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore),
songs = (value[5] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
songsEndpoint = (value[6] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
albums = (value[7] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
albumsEndpoint = (value[8] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
singles = (value[9] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
singlesEndpoint = (value[10] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
)
}

View file

@ -1,20 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.savers.YouTubeThumbnailSaver.save
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
value.subscribersCountText,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
subscribersCountText = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver)

View file

@ -1,18 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
value.name,
value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String?,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver)

View file

@ -1,27 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubePlaylistOrAlbumSaver : Saver<YouTube.PlaylistOrAlbum, List<Any?>> {
override fun SaverScope.save(value: YouTube.PlaylistOrAlbum): List<Any?> = listOf(
value.title,
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } } ,
value.year,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ,
value.songs?.let { with(YouTubeSongListSaver) { save(it) } },
value.url
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.PlaylistOrAlbum(
title = value[0] as String?,
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore),
songs = (value[4] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
url = value[5] as String?,
continuation = null
)
}

View file

@ -1,21 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = listOf(
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
value.channel?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
value.songCount,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Item.Playlist(
info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
songCount = value[2] as Int?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -1,22 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeRelatedSaver : Saver<YouTube.Related, List<Any?>> {
override fun SaverScope.save(value: YouTube.Related): List<Any?> = listOf(
value.songs?.let { with(YouTubeSongListSaver) { save(it) } },
value.playlists?.let { with(YouTubePlaylistListSaver) { save(it) } },
value.albums?.let { with(YouTubeAlbumListSaver) { save(it) } },
value.artists?.let { with(YouTubeArtistListSaver) { save(it) } },
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Related(
songs = (value[0] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
playlists = (value[1] as List<List<Any?>>?)?.let(YouTubePlaylistListSaver::restore),
albums = (value[2] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
artists = (value[3] as List<List<Any?>>?)?.let(YouTubeArtistListSaver::restore),
)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver)

View file

@ -1,24 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = listOf(
value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } },
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } },
value.album?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
value.durationText,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Song(
info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -1,19 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
object YouTubeThumbnailSaver : Saver<ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail, List<Any?>> {
override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf(
value.url,
value.width,
value.height
)
override fun restore(value: List<Any?>) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail(
url = value[0] as String,
width = value[1] as Int,
height = value[2] as Int?,
)
}

View file

@ -1,3 +0,0 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver)

View file

@ -1,24 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = listOf(
value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } },
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } },
value.viewsText,
value.durationText,
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Video(
info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
viewsText = value[2] as String?,
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -1,18 +0,0 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
value.name,
value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String?,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
)
}

View file

@ -92,8 +92,10 @@ import it.vfsfitvnm.vimusic.utils.shouldBePlaying
import it.vfsfitvnm.vimusic.utils.skipSilenceKey
import it.vfsfitvnm.vimusic.utils.timer
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody
import it.vfsfitvnm.youtubemusic.requests.player
import kotlin.math.roundToInt
import kotlin.system.exitProcess
import kotlinx.coroutines.CoroutineScope
@ -642,9 +644,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
else -> {
val urlResult = runBlocking(Dispatchers.IO) {
YouTube.player(videoId)
Innertube.player(PlayerBody(videoId = videoId))
}?.mapCatching { body ->
when (val status = body.playabilityStatus.status) {
when (val status = body.playabilityStatus?.status) {
"OK" -> body.streamingData?.adaptiveFormats?.findLast { format ->
format.itag == 251 || format.itag == 140
}?.let { format ->

View file

@ -61,12 +61,13 @@ import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.albumPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
@ -88,19 +89,21 @@ fun AlbumOverview(
withContext(Dispatchers.IO) {
Database.album(browseId).collect { album ->
if (album?.timestamp == null) {
YouTube.album(browseId)?.onSuccess { youtubeAlbum ->
Innertube.albumPage(BrowseBody(browseId = browseId))?.onSuccess { albumPage ->
Database.upsert(
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" },
shareUrl = youtubeAlbum.url,
title = albumPage.title,
thumbnailUrl = albumPage.thumbnail?.url,
year = albumPage.year,
authorsText = albumPage.authors?.joinToString("") { it.name ?: "" },
shareUrl = albumPage.url,
timestamp = System.currentTimeMillis()
),
youtubeAlbum.songs
?.map(YouTube.Item.Song::asMediaItem)
albumPage
.songsPage
?.items
?.map(Innertube.SongItem::asMediaItem)
?.onEach(Database::insert)
?.mapIndexed { position, mediaItem ->
SongAlbumMap(

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -26,19 +25,19 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Composable
inline fun <T : YouTube.Item> ArtistContent(
inline fun <T : Innertube.Item> ArtistContent(
artist: Artist?,
youtubeArtist: YouTube.Artist?,
youtubeArtistPage: Innertube.ArtistPage?,
isLoading: Boolean,
isError: Boolean,
stateSaver: ListSaver<T, List<Any?>>,
crossinline itemsProvider: suspend (String?) -> Result<Pair<String?, List<T>?>>?,
crossinline itemsPageProvider: suspend (String?) -> Result<Innertube.ItemsPage<T>?>?,
crossinline bookmarkIconContent: @Composable () -> Unit,
crossinline shareIconContent: @Composable () -> Unit,
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
@ -61,18 +60,18 @@ inline fun <T : YouTube.Item> ArtistContent(
val (continuationState, fetch) = produceSaveableRelaunchableOneShotState(
initialValue = null,
stateSaver = autoSaver<String?>(),
youtubeArtist
youtubeArtistPage
) {
if (youtubeArtist == null) return@produceSaveableRelaunchableOneShotState
if (youtubeArtistPage == null) return@produceSaveableRelaunchableOneShotState
println("loading... $value")
isLoadingItems = true
withContext(Dispatchers.IO) {
itemsProvider(value)?.onSuccess { (continuation, newItems) ->
value = continuation
newItems?.let {
items = items.plus(it).distinctBy(YouTube.Item::key)
itemsPageProvider(value)?.onSuccess { itemsPage ->
value = itemsPage?.continuation
itemsPage?.items?.let {
items = items.plus(it).distinctBy(Innertube.Item::key)
}
isErrorItems = false
isLoadingItems = false
@ -105,7 +104,7 @@ inline fun <T : YouTube.Item> ArtistContent(
items(
items = items,
key = YouTube.Item::key,
key = Innertube.Item::key,
itemContent = itemContent
)

View file

@ -57,14 +57,14 @@ import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
@ExperimentalAnimationApi
@Composable
fun ArtistOverview(
artist: Artist?,
youtubeArtist: YouTube.Artist?,
youtubeArtistPage: Innertube.ArtistPage?,
isLoading: Boolean,
isError: Boolean,
onViewAllSongsClick: () -> Unit,
@ -100,7 +100,7 @@ fun ArtistOverview(
when {
artist != null -> {
Header(title = artist.name ?: "Unknown") {
youtubeArtist?.radioEndpoint?.let { radioEndpoint ->
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
SecondaryTextButton(
text = "Start radio",
onClick = {
@ -130,8 +130,8 @@ fun ArtistOverview(
)
when {
youtubeArtist != null -> {
youtubeArtist.songs?.let { songs ->
youtubeArtistPage != null -> {
youtubeArtistPage.songs?.let { songs ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
@ -144,7 +144,7 @@ fun ArtistOverview(
modifier = sectionTextModifier
)
youtubeArtist.songsEndpoint?.let {
youtubeArtistPage.songsEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
@ -174,7 +174,7 @@ fun ArtistOverview(
}
}
youtubeArtist.albums?.let { albums ->
youtubeArtistPage.albums?.let { albums ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
@ -187,7 +187,7 @@ fun ArtistOverview(
modifier = sectionTextModifier
)
youtubeArtist.albumsEndpoint?.let {
youtubeArtistPage.albumsEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
@ -207,7 +207,7 @@ fun ArtistOverview(
) {
items(
items = albums,
key = YouTube.Item.Album::key
key = Innertube.AlbumItem::key
) { album ->
AlternativeAlbumItem(
album = album,
@ -224,7 +224,7 @@ fun ArtistOverview(
}
}
youtubeArtist.singles?.let { singles ->
youtubeArtistPage.singles?.let { singles ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
@ -237,7 +237,7 @@ fun ArtistOverview(
modifier = sectionTextModifier
)
youtubeArtist.singlesEndpoint?.let {
youtubeArtistPage.singlesEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
@ -257,7 +257,7 @@ fun ArtistOverview(
) {
items(
items = singles,
key = YouTube.Item.Album::key
key = Innertube.AlbumItem::key
) { album ->
AlternativeAlbumItem(
album = album,
@ -330,7 +330,7 @@ fun ArtistOverview(
}
}
youtubeArtist?.shuffleEndpoint?.let { shuffleEndpoint ->
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
PrimaryButton(
iconId = R.drawable.shuffle,
onClick = {

View file

@ -26,14 +26,13 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.PartialArtist
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.ArtistSaver
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
import it.vfsfitvnm.vimusic.savers.YouTubeArtistPageSaver
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver
import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResult
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
@ -47,7 +46,12 @@ import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableLazyOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
import it.vfsfitvnm.youtubemusic.requests.artistPage
import it.vfsfitvnm.youtubemusic.requests.itemsPage
import it.vfsfitvnm.youtubemusic.utils.from
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
@ -72,22 +76,22 @@ fun ArtistScreen(browseId: String) {
val youtubeArtist by produceSaveableLazyOneShotState(
initialValue = null,
stateSaver = nullableSaver(YouTubeArtistPageSaver)
stateSaver = nullableSaver(InnertubeArtistPageSaver)
) {
println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!")
isLoading = true
withContext(Dispatchers.IO) {
YouTube.artist(browseId)?.onSuccess { youtubeArtist ->
value = youtubeArtist
Innertube.artistPage(browseId)?.onSuccess { artistPage ->
value = artistPage
query {
Database.upsert(
PartialArtist(
id = browseId,
name = youtubeArtist.name,
thumbnailUrl = youtubeArtist.thumbnail?.url,
info = youtubeArtist.description,
name = artistPage.name,
thumbnailUrl = artistPage.thumbnail?.url,
info = artistPage.description,
timestamp = System.currentTimeMillis()
)
)
@ -136,10 +140,13 @@ fun ArtistScreen(browseId: String) {
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent),
modifier = Modifier
.clickable {
val bookmarkedAt = if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
val bookmarkedAt =
if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
query {
artist?.copy(bookmarkedAt = bookmarkedAt)?.let(Database::update)
artist
?.copy(bookmarkedAt = bookmarkedAt)
?.let(Database::update)
}
}
.padding(all = 4.dp)
@ -194,7 +201,7 @@ fun ArtistScreen(browseId: String) {
when (currentTabIndex) {
0 -> ArtistOverview(
artist = artist,
youtubeArtist = youtubeArtist,
youtubeArtistPage = youtubeArtist,
isLoading = isLoading,
isError = isError,
bookmarkIconContent = bookmarkIconContent,
@ -204,6 +211,7 @@ fun ArtistScreen(browseId: String) {
onViewAllAlbumsClick = { onTabIndexChanged(2) },
onViewAllSinglesClick = { onTabIndexChanged(3) },
)
1 -> {
val binder = LocalPlayerServiceBinder.current
val thumbnailSizeDp = Dimensions.thumbnails.song
@ -211,20 +219,26 @@ fun ArtistScreen(browseId: String) {
ArtistContent(
artist = artist,
youtubeArtist = youtubeArtist,
youtubeArtistPage = youtubeArtist,
isLoading = isLoading,
isError = isError,
stateSaver = YouTubeSongListSaver,
stateSaver = InnertubeSongItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsProvider = { continuation ->
youtubeArtist
itemsPageProvider = { continuation ->
continuation?.let {
Innertube.itemsPage(
body = ContinuationBody(continuation = continuation),
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
)
} ?: youtubeArtist
?.songsEndpoint
?.browseId
?.let { browseId ->
YouTube.items(browseId, continuation, YouTube.Item.Song::from)?.map { result ->
result?.continuation to result?.items
}
Innertube.itemsPage(
body = BrowseBody(browseId = browseId),
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
)
}
},
itemContent = { song ->
@ -243,25 +257,33 @@ fun ArtistScreen(browseId: String) {
}
)
}
2 -> {
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px
ArtistContent(
artist = artist,
youtubeArtist = youtubeArtist,
youtubeArtistPage = youtubeArtist,
isLoading = isLoading,
isError = isError,
stateSaver = YouTubeAlbumListSaver,
stateSaver = InnertubeAlbumItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsProvider = {
youtubeArtist
?.albumsEndpoint
?.let { endpoint ->
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
result?.continuation to result?.items
}
itemsPageProvider = { continuation ->
continuation?.let {
Innertube.itemsPage(
body = ContinuationBody(continuation = continuation),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
)
} ?: youtubeArtist
?.songsEndpoint
?.browseId
?.let { browseId ->
Innertube.itemsPage(
body = BrowseBody(browseId = browseId),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
)
}
},
itemContent = { album ->
@ -282,25 +304,33 @@ fun ArtistScreen(browseId: String) {
}
)
}
3 -> {
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px
ArtistContent(
artist = artist,
youtubeArtist = youtubeArtist,
youtubeArtistPage = youtubeArtist,
isLoading = isLoading,
isError = isError,
stateSaver = YouTubeAlbumListSaver,
stateSaver = InnertubeAlbumItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsProvider = {
youtubeArtist
?.singlesEndpoint
?.let { endpoint ->
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
result?.continuation to result?.items
}
itemsPageProvider = { continuation ->
continuation?.let {
Innertube.itemsPage(
body = ContinuationBody(continuation = continuation),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
)
} ?: youtubeArtist
?.songsEndpoint
?.browseId
?.let { browseId ->
Innertube.itemsPage(
body = BrowseBody(browseId = browseId),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
)
}
},
itemContent = { album ->
@ -321,6 +351,7 @@ fun ArtistScreen(browseId: String) {
}
)
}
4 -> ArtistLocalSongsList(
browseId = browseId,
artist = artist,

View file

@ -34,7 +34,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
import it.vfsfitvnm.vimusic.savers.YouTubeRelatedSaver
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -60,8 +60,10 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
import it.vfsfitvnm.youtubemusic.requests.relatedPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
@ -88,30 +90,13 @@ fun QuickPicks(
.collect { value = it }
}
val relatedResult by produceSaveableOneShotState(
val relatedPageResult by produceSaveableOneShotState(
initialValue = null,
stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)),
stateSaver = resultSaver(nullableSaver(InnertubeRelatedPageSaver)),
trending?.id
) {
trending?.id?.let { trendingVideoId ->
value = YouTube.related(trendingVideoId)?.map { related ->
related?.copy(
albums = related.albums?.map { album ->
album.copy(
authors = trending?.artists?.map { info ->
YouTube.Info(
name = info.name,
endpoint = NavigationEndpoint.Endpoint.Browse(
browseId = info.id,
params = null,
browseEndpointContextSupportedConfigs = null
)
)
}
)
}
)
}
value = Innertube.relatedPage(NextBody(videoId = trendingVideoId))
}
}
@ -140,7 +125,7 @@ fun QuickPicks(
) {
Header(title = "Quick picks")
relatedResult?.getOrNull()?.let { related ->
relatedPageResult?.getOrNull()?.let { related ->
LazyHorizontalGrid(
rows = GridCells.Fixed(4),
modifier = Modifier
@ -171,7 +156,7 @@ fun QuickPicks(
items(
items = related.songs ?: emptyList(),
key = YouTube.Item.Song::key
key = Innertube.SongItem::key
) { song ->
SmallSongItem(
song = song,
@ -204,7 +189,7 @@ fun QuickPicks(
) {
items(
items = related.albums ?: emptyList(),
key = YouTube.Item.Album::key
key = Innertube.AlbumItem::key
) { album ->
AlbumItem(
album = album,
@ -235,7 +220,7 @@ fun QuickPicks(
) {
items(
items = related.artists ?: emptyList(),
key = YouTube.Item.Artist::key,
key = Innertube.ArtistItem::key,
) { artist ->
ArtistItem(
artist = artist,
@ -268,7 +253,7 @@ fun QuickPicks(
) {
items(
items = related.playlists ?: emptyList(),
key = YouTube.Item.Playlist::key,
key = Innertube.PlaylistItem::key,
) { playlist ->
PlaylistItem(
playlist = playlist,
@ -284,7 +269,7 @@ fun QuickPicks(
)
}
}
} ?: relatedResult?.exceptionOrNull()?.let {
} ?: relatedPageResult?.exceptionOrNull()?.let {
BasicText(
text = "An error has occurred",
style = typography.s.secondary.center,

View file

@ -34,6 +34,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -50,7 +51,9 @@ import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.playlistPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
@ -68,7 +71,7 @@ fun LocalPlaylistSongList(
val playlistWithSongs by produceSaveableState(
initialValue = null,
stateSaver = PlaylistWithSongsSaver
stateSaver = nullableSaver(PlaylistWithSongsSaver)
) {
Database
.playlistWithSongs(playlistId)
@ -165,13 +168,16 @@ fun LocalPlaylistSongList(
transaction {
runBlocking(Dispatchers.IO) {
withContext(Dispatchers.IO) {
YouTube.playlist(browseId)?.map { it.next() }
// TODO: fetch all songs!
Innertube.playlistPage(BrowseBody(browseId = browseId))
}
}?.getOrNull()?.let { remotePlaylist ->
Database.clearPlaylist(playlistId)
remotePlaylist.songs
?.map(YouTube.Item.Song::asMediaItem)
remotePlaylist.
songsPage
?.items
?.map(Innertube.SongItem::asMediaItem)
?.onEach(Database::insert)
?.mapIndexed { position, mediaItem ->
SongPlaylistMap(

View file

@ -69,7 +69,9 @@ import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
import it.vfsfitvnm.youtubemusic.requests.lyrics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -134,8 +136,7 @@ fun Lyrics(
duration = duration / 1000
)?.map { it?.value }
} else {
YouTube.next(mediaId, null)
?.map { nextResult -> nextResult.lyrics()?.getOrNull() }
Innertube.lyrics(NextBody(videoId = mediaId))
}?.map { newLyrics ->
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
state = state.copy(isLoading = false)

View file

@ -40,7 +40,9 @@ import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.rememberVolume
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody
import it.vfsfitvnm.youtubemusic.requests.player
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
@ -195,7 +197,7 @@ fun StatsForNerds(
onClick = {
query {
runBlocking(Dispatchers.IO) {
YouTube.player(mediaId)
Innertube.player(PlayerBody(videoId = mediaId))
?.map { response ->
response.streamingData?.adaptiveFormats
?.findLast { format ->

View file

@ -27,7 +27,9 @@ fun PlaylistScreen(browseId: String) {
}
) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
PlaylistSongList(browseId = browseId)
when (currentTabIndex) {
0 -> PlaylistSongList(browseId = browseId)
}
}
}
}

View file

@ -39,7 +39,7 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistOrAlbumSaver
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -58,11 +58,12 @@ import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.playlistPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
@ -76,12 +77,13 @@ fun PlaylistSongList(
val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current
val playlistResult by produceSaveableOneShotState(
val playlistPageResult by produceSaveableOneShotState(
initialValue = null,
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
stateSaver = resultSaver(InnertubePlaylistOrAlbumPageSaver),
) {
value = withContext(Dispatchers.IO) {
YouTube.playlist(browseId)?.map { it.next() }
// TODO: fetch all songs!
Innertube.playlistPage(BrowseBody(browseId = browseId))
}
}
@ -102,7 +104,7 @@ fun PlaylistSongList(
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
playlistResult?.getOrNull()?.let { playlist ->
playlistPageResult?.getOrNull()?.let { playlist ->
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
@ -117,9 +119,9 @@ fun PlaylistSongList(
Header(title = playlist.title ?: "Unknown") {
SecondaryTextButton(
text = "Enqueue",
isEnabled = playlist.songs?.isNotEmpty() == true,
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
onClick = {
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.player?.enqueue(mediaItems)
}
}
@ -147,8 +149,8 @@ fun PlaylistSongList(
)
)
playlist.songs
?.map(YouTube.Item.Song::asMediaItem)
playlist.songsPage?.items
?.map(Innertube.SongItem::asMediaItem)
?.onEach(Database::insert)
?.mapIndexed { index, mediaItem ->
SongPlaylistMap(
@ -196,13 +198,13 @@ fun PlaylistSongList(
}
}
itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song ->
SongItem(
title = song.info?.name,
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" },
durationText = song.durationText,
onClick = {
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
@ -226,15 +228,15 @@ fun PlaylistSongList(
PrimaryButton(
iconId = R.drawable.shuffle,
isEnabled = playlist.songs?.isNotEmpty() == true,
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
onClick = {
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
}
}
)
} ?: playlistResult?.exceptionOrNull()?.let {
} ?: playlistPageResult?.exceptionOrNull()?.let {
Box(
modifier = Modifier
.align(Alignment.Center)

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
@ -41,7 +42,7 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver
import it.vfsfitvnm.vimusic.savers.StringListResultSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@ -51,7 +52,9 @@ import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.SearchSuggestionsBody
import it.vfsfitvnm.youtubemusic.requests.searchSuggestions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -80,11 +83,11 @@ fun OnlineSearch(
val suggestionsResult by produceSaveableOneShotState(
initialValue = null,
stateSaver = StringListResultSaver,
stateSaver = resultSaver(autoSaver<List<String>?>()),
key1 = textFieldValue.text
) {
if (textFieldValue.text.isNotEmpty()) {
value = YouTube.getSearchSuggestions(textFieldValue.text)
value = Innertube.searchSuggestions(SearchSuggestionsBody(input = textFieldValue.text))
}
}

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -23,23 +24,28 @@ import androidx.compose.ui.input.pointer.pointerInput
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.savers.ListSaver
import it.vfsfitvnm.vimusic.savers.StringResultSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
import it.vfsfitvnm.youtubemusic.models.bodies.SearchBody
import it.vfsfitvnm.youtubemusic.requests.searchPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Composable
inline fun <T : YouTube.Item> SearchResult(
inline fun <T : Innertube.Item> SearchResult(
query: String,
filter: String,
stateSaver: ListSaver<T, List<Any?>>,
noinline fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?,
crossinline onSearchAgain: () -> Unit,
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
noinline itemShimmer: @Composable BoxScope.() -> Unit,
@ -52,21 +58,30 @@ inline fun <T : YouTube.Item> SearchResult(
val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
initialValue = null,
stateSaver = StringResultSaver
stateSaver = resultSaver(autoSaver<String?>())
) {
val token = value?.getOrNull()
value = null
value = withContext(Dispatchers.IO) {
YouTube.search(query, filter, token)
}?.map { searchResult ->
@Suppress("UNCHECKED_CAST")
(searchResult.items as List<T>?)?.let {
items = items.plus(it).distinctBy(YouTube.Item::key)
if (token == null) {
Innertube.searchPage(
body = SearchBody(query = query, params = filter),
fromMusicShelfRendererContent = fromMusicShelfRendererContent
)
} else {
Innertube.searchPage(
body = ContinuationBody(continuation = token),
fromMusicShelfRendererContent = fromMusicShelfRendererContent
)
}
}?.map { itemsPage ->
itemsPage?.items?.let {
items = items.plus(it).distinctBy(Innertube.Item::key)
}
searchResult.continuation
itemsPage?.continuation
}
}
@ -94,7 +109,7 @@ inline fun <T : YouTube.Item> SearchResult(
items(
items = items,
key = YouTube.Item::key,
key = Innertube.Item::key,
itemContent = itemContent
)

View file

@ -13,16 +13,15 @@ import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeArtistItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeVideoItemListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.px
@ -40,7 +39,8 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.utils.from
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@ -52,12 +52,6 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
playlistRoute { browseId ->
PlaylistScreen(
browseId = browseId ?: "browseId cannot be null"
)
}
host {
Scaffold(
topIconButtonId = R.drawable.chevron_back,
@ -74,12 +68,12 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
}
) { tabIndex ->
val searchFilter = when (tabIndex) {
0 -> YouTube.Item.Song.Filter
1 -> YouTube.Item.Album.Filter
2 -> YouTube.Item.Artist.Filter
3 -> YouTube.Item.Video.Filter
4 -> YouTube.Item.CommunityPlaylist.Filter
5 -> YouTube.Item.FeaturedPlaylist.Filter
0 -> Innertube.SearchFilter.Song
1 -> Innertube.SearchFilter.Album
2 -> Innertube.SearchFilter.Artist
3 -> Innertube.SearchFilter.Video
4 -> Innertube.SearchFilter.CommunityPlaylist
5 -> Innertube.SearchFilter.FeaturedPlaylist
else -> error("unreachable")
}.value
@ -94,7 +88,8 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain,
stateSaver = YouTubeSongListSaver,
stateSaver = InnertubeSongItemListSaver,
fromMusicShelfRendererContent = Innertube.SongItem.Companion::from,
itemContent = { song ->
SmallSongItem(
song = song,
@ -119,8 +114,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
SearchResult(
query = query,
filter = searchFilter,
stateSaver = YouTubeAlbumListSaver,
stateSaver = InnertubeAlbumItemListSaver,
onSearchAgain = onSearchAgain,
fromMusicShelfRendererContent = Innertube.AlbumItem.Companion::from,
itemContent = { album ->
AlbumItem(
album = album,
@ -148,8 +144,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
SearchResult(
query = query,
filter = searchFilter,
stateSaver = YouTubeArtistListSaver,
stateSaver = InnertubeArtistItemListSaver,
onSearchAgain = onSearchAgain,
fromMusicShelfRendererContent = Innertube.ArtistItem.Companion::from,
itemContent = { artist ->
ArtistItem(
artist = artist,
@ -176,8 +173,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
SearchResult(
query = query,
filter = searchFilter,
stateSaver = YouTubeVideoListSaver,
stateSaver = InnertubeVideoItemListSaver,
onSearchAgain = onSearchAgain,
fromMusicShelfRendererContent = Innertube.VideoItem.Companion::from,
itemContent = { video ->
VideoItem(
video = video,
@ -206,8 +204,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
SearchResult(
query = query,
filter = searchFilter,
stateSaver = YouTubePlaylistListSaver,
stateSaver = InnertubePlaylistItemListSaver,
onSearchAgain = onSearchAgain,
fromMusicShelfRendererContent = Innertube.PlaylistItem.Companion::from,
itemContent = { playlist ->
PlaylistItem(
playlist = playlist,

View file

@ -41,7 +41,7 @@ import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
@Composable
fun SmallSongItemShimmer(
@ -73,7 +73,7 @@ fun SmallSongItemShimmer(
@ExperimentalAnimationApi
@Composable
fun SmallSongItem(
song: YouTube.Item.Song,
song: Innertube.SongItem,
thumbnailSizePx: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
@ -95,7 +95,7 @@ fun SmallSongItem(
@ExperimentalAnimationApi
@Composable
fun VideoItem(
video: YouTube.Item.Video,
video: Innertube.VideoItem,
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
onClick: () -> Unit,
@ -212,7 +212,7 @@ fun VideoItemShimmer(
@Composable
fun PlaylistItem(
playlist: YouTube.Item.Playlist,
playlist: Innertube.PlaylistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
@ -298,7 +298,7 @@ fun PlaylistItemShimmer(
@Composable
fun AlbumItem(
album: YouTube.Item.Album,
album: Innertube.AlbumItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
@ -383,7 +383,7 @@ fun AlbumItemShimmer(
@Composable
fun AlternativeAlbumItem(
album: YouTube.Item.Album,
album: Innertube.AlbumItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
@ -452,7 +452,7 @@ fun AlternativeAlbumItemPlaceholder(
@Composable
fun ArtistItem(
artist: YouTube.Item.Artist,
artist: Innertube.ArtistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,

View file

@ -6,9 +6,9 @@ import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
val YouTube.Item.Song.asMediaItem: MediaItem
val Innertube.SongItem.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(key)
.setUri(key)
@ -32,7 +32,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem
)
.build()
val YouTube.Item.Video.asMediaItem: MediaItem
val Innertube.VideoItem.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(key)
.setUri(key)

View file

@ -1,7 +1,10 @@
package it.vfsfitvnm.vimusic.utils
import androidx.media3.common.MediaItem
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
import it.vfsfitvnm.youtubemusic.requests.nextPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -17,20 +20,30 @@ data class YouTubeRadio(
var mediaItems: List<MediaItem>? = null
nextContinuation = withContext(Dispatchers.IO) {
YouTube.next(
val continuation = nextContinuation
if (continuation == null) {
Innertube.nextPage(
NextBody(
videoId = videoId,
playlistId = playlistId,
params = parameters,
playlistSetVideoId = playlistSetVideoId,
continuation = nextContinuation
)?.getOrNull()?.let { nextResult ->
playlistSetVideoId = playlistSetVideoId
)
)?.map { nextResult ->
playlistId = nextResult.playlistId
parameters = nextResult.params
playlistSetVideoId = nextResult.playlistSetVideoId
mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem)
nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation }
nextResult.itemsPage
}
} else {
Innertube.nextPage(ContinuationBody(continuation = continuation))
}?.getOrNull()?.let { songsPage ->
mediaItems = songsPage.items?.map(Innertube.SongItem::asMediaItem)
songsPage.continuation?.takeUnless { nextContinuation == it }
}
}
return mediaItems ?: emptyList()

View file

@ -0,0 +1,204 @@
package it.vfsfitvnm.youtubemusic
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.BrowserUserAgent
import io.ktor.client.plugins.compression.ContentEncoding
import io.ktor.client.plugins.compression.brotli
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import it.vfsfitvnm.youtubemusic.models.Runs
import it.vfsfitvnm.youtubemusic.models.Thumbnail
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
object Innertube {
val client = HttpClient(OkHttp) {
BrowserUserAgent()
expectSuccess = true
install(ContentNegotiation) {
@OptIn(ExperimentalSerializationApi::class)
json(Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
})
}
install(ContentEncoding) {
brotli()
}
defaultRequest {
url(scheme = "https", host ="music.youtube.com") {
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
headers.append("X-Goog-Api-Key", "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
parameters.append("prettyPrint", "false")
}
}
}
internal const val browse = "/youtubei/v1/browse"
internal const val next = "/youtubei/v1/next"
internal const val player = "/youtubei/v1/player"
internal const val queue = "/youtubei/v1/music/get_queue"
internal const val search = "/youtubei/v1/search"
internal const val searchSuggestions = "/youtubei/v1/music/get_search_suggestions"
internal const val musicResponsiveListItemRendererMask = "musicResponsiveListItemRenderer(flexColumns,fixedColumns,thumbnail,navigationEndpoint)"
internal const val musicTwoRowItemRendererMask = "musicTwoRowItemRenderer(thumbnailRenderer,title,subtitle,navigationEndpoint)"
const val playlistPanelVideoRendererMask = "playlistPanelVideoRenderer(title,navigationEndpoint,longBylineText,shortBylineText,thumbnail,lengthText)"
// contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.musicResponsiveListItemRenderer(flexColumns,fixedColumns,thumbnail)),gridRenderer(continuations,items.musicTwoRowItemRenderer(thumbnailRenderer,title,subtitle,navigationEndpoint)))
internal fun HttpRequestBuilder.mask(value: String = "*") =
header("X-Goog-FieldMask", value)
data class Info<T : NavigationEndpoint.Endpoint>(
val name: String?,
val endpoint: T?
) {
@Suppress("UNCHECKED_CAST")
constructor(run: Runs.Run) : this(
name = run.text,
endpoint = run.navigationEndpoint?.endpoint as T?
)
}
@JvmInline
value class SearchFilter(val value: String) {
companion object {
val Song = SearchFilter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
val Video = SearchFilter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
val Album = SearchFilter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
val Artist = SearchFilter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
val CommunityPlaylist = SearchFilter("EgeKAQQoAEABagoQAxAEEAoQCRAF")
val FeaturedPlaylist = SearchFilter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
}
}
sealed class Item {
abstract val thumbnail: Thumbnail?
abstract val key: String
}
data class SongItem(
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
val durationText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!
companion object
}
data class VideoItem(
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val viewsText: String?,
val durationText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!
val isOfficialMusicVideo: Boolean
get() = info
?.endpoint
?.watchEndpointMusicSupportedConfigs
?.watchEndpointMusicConfig
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
val isUserGeneratedContent: Boolean
get() = info
?.endpoint
?.watchEndpointMusicSupportedConfigs
?.watchEndpointMusicConfig
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
companion object
}
data class AlbumItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val year: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class ArtistItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val subscribersCountText: String?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class PlaylistItem(
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
val songCount: Int?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.browseId!!
companion object
}
data class ArtistPage(
val name: String?,
val description: String?,
val thumbnail: Thumbnail?,
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?,
val songs: List<SongItem>?,
val songsEndpoint: NavigationEndpoint.Endpoint.Browse?,
val albums: List<AlbumItem>?,
val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?,
val singles: List<AlbumItem>?,
val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?,
)
data class PlaylistOrAlbumPage(
val title: String?,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val year: String?,
val thumbnail: Thumbnail?,
val url: String?,
val songsPage: ItemsPage<SongItem>?
)
data class NextPage(
val itemsPage: ItemsPage<SongItem>?,
val playlistId: String?,
val params: String? = null,
val playlistSetVideoId: String? = null
)
data class RelatedPage(
val songs: List<SongItem>? = null,
val playlists: List<PlaylistItem>? = null,
val albums: List<AlbumItem>? = null,
val artists: List<ArtistItem>? = null,
)
data class ItemsPage<T : Item>(
val items: List<T>?,
val continuation: String?
)
}

View file

@ -1,9 +1,7 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class BrowseResponse(
val contents: Contents?,
@ -23,10 +21,10 @@ data class BrowseResponse(
) {
@Serializable
data class MusicDetailHeaderRenderer(
val title: Runs,
val subtitle: Runs,
val secondSubtitle: Runs,
val thumbnail: ThumbnailRenderer,
val title: Runs?,
val subtitle: Runs?,
val secondSubtitle: Runs?,
val thumbnail: ThumbnailRenderer?,
)
@Serializable
@ -35,16 +33,16 @@ data class BrowseResponse(
val playButton: PlayButton?,
val startRadioButton: StartRadioButton?,
val thumbnail: ThumbnailRenderer?,
val title: Runs
val title: Runs?
) {
@Serializable
data class PlayButton(
val buttonRenderer: ButtonRenderer
val buttonRenderer: ButtonRenderer?
)
@Serializable
data class StartRadioButton(
val buttonRenderer: ButtonRenderer
val buttonRenderer: ButtonRenderer?
)
}
}

View file

@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class ButtonRenderer(
val navigationEndpoint: NavigationEndpoint
val navigationEndpoint: NavigationEndpoint?
)

View file

@ -0,0 +1,48 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class Context(
val client: Client,
val thirdParty: ThirdParty? = null,
) {
@Serializable
data class Client(
val clientName: String,
val clientVersion: String,
val visitorData: String?,
val hl: String = "en",
)
@Serializable
data class ThirdParty(
val embedUrl: String,
)
companion object {
val DefaultWeb = Context(
client = Client(
clientName = "WEB_REMIX",
clientVersion = "1.20220328.01.00",
visitorData = "CgtsZG1ySnZiQWtSbyiMjuGSBg%3D%3D"
)
)
val DefaultAndroid = Context(
client = Client(
clientName = "ANDROID",
clientVersion = "16.50",
visitorData = null,
)
)
val DefaultAgeRestrictionBypass = Context(
client = Client(
clientName = "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
clientVersion = "2.0",
visitorData = null,
)
)
}
}

View file

@ -8,10 +8,10 @@ import kotlinx.serialization.json.JsonNames
@Serializable
data class Continuation(
@JsonNames("nextContinuationData", "nextRadioContinuationData")
val nextContinuationData: Data
val nextContinuationData: Data?
) {
@Serializable
data class Data(
val continuation: String
val continuation: String?
)
}

View file

@ -12,12 +12,7 @@ data class ContinuationResponse(
@Serializable
data class ContinuationContents(
@JsonNames("musicPlaylistShelfContinuation")
val musicShelfContinuation: MusicShelfRenderer?
) {
// @Serializable
// data class MusicShelfContinuation(
// val continuations: List<Continuation>?,
// val contents: List<MusicShelfRenderer.Content>
// )
}
val musicShelfContinuation: MusicShelfRenderer?,
val playlistPanelContinuation: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?,
)
}

View file

@ -1,9 +1,7 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class GetQueueResponse(
val queueDatas: List<QueueData>?,

View file

@ -0,0 +1,13 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class GridRenderer(
val items: List<Item>?,
) {
@Serializable
data class Item(
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?
)
}

View file

@ -4,13 +4,12 @@ import kotlinx.serialization.Serializable
@Serializable
data class MusicCarouselShelfRenderer(
val header: Header,
val header: Header?,
val contents: List<Content>,
) {
@Serializable
data class Content(
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
)
@ -23,11 +22,12 @@ data class MusicCarouselShelfRenderer(
@Serializable
data class MusicCarouselShelfBasicHeaderRenderer(
val moreContentButton: MoreContentButton?,
val title: Runs,
val title: Runs?,
val strapline: Runs?,
) {
@Serializable
data class MoreContentButton(
val buttonRenderer: ButtonRenderer
val buttonRenderer: ButtonRenderer?
)
}
}

View file

@ -15,7 +15,7 @@ data class MusicResponsiveListItemRenderer(
@Serializable
data class FlexColumn(
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
) {
@Serializable
data class MusicResponsiveListItemFlexColumnRenderer(

View file

@ -5,34 +5,34 @@ import kotlinx.serialization.Serializable
@Serializable
data class MusicShelfRenderer(
val bottomEndpoint: NavigationEndpoint?,
val contents: List<Content>,
val contents: List<Content>?,
val continuations: List<Continuation>?,
val title: Runs?
) {
@Serializable
data class Content(
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer,
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
) {
val runs: Pair<List<Runs.Run>, List<List<Runs.Run>>>
get() = (musicResponsiveListItemRenderer
.flexColumns
.firstOrNull()
?.flexColumns
?.firstOrNull()
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?: emptyList()) to
(musicResponsiveListItemRenderer
.flexColumns
.lastOrNull()
?.flexColumns
?.lastOrNull()
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.splitBySeparator()
?: emptyList()
)
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
val thumbnail: Thumbnail?
get() = musicResponsiveListItemRenderer
.thumbnail
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails

View file

@ -0,0 +1,11 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class MusicTwoRowItemRenderer(
val navigationEndpoint: NavigationEndpoint?,
val thumbnailRenderer: ThumbnailRenderer?,
val title: Runs?,
val subtitle: Runs?,
)

View file

@ -74,12 +74,12 @@ data class NavigationEndpoint(
@Serializable
data class WatchEndpointMusicSupportedConfigs(
val watchEndpointMusicConfig: WatchEndpointMusicConfig
val watchEndpointMusicConfig: WatchEndpointMusicConfig?
) {
@Serializable
data class WatchEndpointMusicConfig(
val musicVideoType: String
val musicVideoType: String?
)
}
}
@ -87,14 +87,14 @@ data class NavigationEndpoint(
@Serializable
data class WatchPlaylist(
val params: String?,
val playlistId: String,
val playlistId: String?,
) : Endpoint()
@Serializable
data class Browse(
val params: String?,
val browseId: String?,
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
val params: String? = null,
val browseId: String? = null,
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null,
) : Endpoint() {
val type: String?
get() = browseEndpointContextSupportedConfigs

View file

@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class NextResponse(
val contents: Contents,
val continuationContents: MusicQueueRenderer.Content?
val contents: Contents?
) {
@Serializable
data class MusicQueueRenderer(
@ -17,30 +16,18 @@ data class NextResponse(
@Serializable
data class Content(
@JsonNames("playlistPanelContinuation")
val playlistPanelRenderer: PlaylistPanelRenderer
val playlistPanelRenderer: PlaylistPanelRenderer?
) {
@Serializable
data class PlaylistPanelRenderer(
val contents: List<Content>?,
val continuations: List<Continuation>?,
val playlistId: String?
) {
@Serializable
data class Content(
val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,
val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?,
) {
@Serializable
data class PlaylistPanelVideoRenderer(
val title: Runs?,
val longBylineText: Runs?,
val shortBylineText: Runs?,
val lengthText: Runs?,
val navigationEndpoint: NavigationEndpoint,
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail,
val videoId: String,
val playlistSetVideoId: String?,
)
@Serializable
data class AutomixPreviewVideoRenderer(
@ -52,7 +39,7 @@ data class NextResponse(
) {
@Serializable
data class AutomixPlaylistVideoRenderer(
val navigationEndpoint: NavigationEndpoint
val navigationEndpoint: NavigationEndpoint?
)
}
}
@ -63,33 +50,33 @@ data class NextResponse(
@Serializable
data class Contents(
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer?
) {
@Serializable
data class SingleColumnMusicWatchNextResultsRenderer(
val tabbedRenderer: TabbedRenderer
val tabbedRenderer: TabbedRenderer?
) {
@Serializable
data class TabbedRenderer(
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer?
) {
@Serializable
data class WatchNextTabbedResultsRenderer(
val tabs: List<Tab>
val tabs: List<Tab>?
) {
@Serializable
data class Tab(
val tabRenderer: TabRenderer
val tabRenderer: TabRenderer?
) {
@Serializable
data class TabRenderer(
val content: Content?,
val endpoint: NavigationEndpoint?,
val title: String
val title: String?
) {
@Serializable
data class Content(
val musicQueueRenderer: MusicQueueRenderer
val musicQueueRenderer: MusicQueueRenderer?
)
}
}

View file

@ -4,13 +4,13 @@ import kotlinx.serialization.Serializable
@Serializable
data class PlayerResponse(
val playabilityStatus: PlayabilityStatus,
val playabilityStatus: PlayabilityStatus?,
val playerConfig: PlayerConfig?,
val streamingData: StreamingData?,
) {
@Serializable
data class PlayabilityStatus(
val status: String
val status: String?
)
@Serializable
@ -26,8 +26,7 @@ data class PlayerResponse(
@Serializable
data class StreamingData(
val adaptiveFormats: List<AdaptiveFormat>,
val expiresInSeconds: String
val adaptiveFormats: List<AdaptiveFormat>?
) {
@Serializable
data class AdaptiveFormat(

View file

@ -0,0 +1,13 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class PlaylistPanelVideoRenderer(
val title: Runs?,
val longBylineText: Runs?,
val shortBylineText: Runs?,
val lengthText: Runs?,
val navigationEndpoint: NavigationEndpoint?,
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail?,
)

View file

@ -7,7 +7,7 @@ data class Runs(
val runs: List<Run> = listOf()
) {
val text: String
get() = runs.joinToString("") { it.text }
get() = runs.joinToString("") { it.text ?: "" }
fun splitBySeparator(): List<List<Run>> {
return runs.flatMapIndexed { index, run ->
@ -25,7 +25,7 @@ data class Runs(
@Serializable
data class Run(
val text: String,
val text: String?,
val navigationEndpoint: NavigationEndpoint?,
)
}

View file

@ -3,13 +3,12 @@ package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class SearchResponse(
val contents: Contents,
val contents: Contents?,
) {
@Serializable
data class Contents(
val tabbedSearchResultsRenderer: Tabs
val tabbedSearchResultsRenderer: Tabs?
)
}

View file

@ -3,24 +3,24 @@ package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class GetSearchSuggestionsResponse(
data class SearchSuggestionsResponse(
val contents: List<Content>?
) {
@Serializable
data class Content(
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer?
) {
@Serializable
data class SearchSuggestionsSectionRenderer(
val contents: List<Content>
val contents: List<Content>?
) {
@Serializable
data class Content(
val searchSuggestionRenderer: SearchSuggestionRenderer
val searchSuggestionRenderer: SearchSuggestionRenderer?
) {
@Serializable
data class SearchSuggestionRenderer(
val navigationEndpoint: NavigationEndpoint,
val navigationEndpoint: NavigationEndpoint?,
)
}
}

View file

@ -0,0 +1,29 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class SectionListRenderer(
val contents: List<Content>?,
val continuations: List<Continuation>?
) {
@Serializable
data class Content(
@JsonNames("musicImmersiveCarouselShelfRenderer")
val musicCarouselShelfRenderer: MusicCarouselShelfRenderer?,
@JsonNames("musicPlaylistShelfRenderer")
val musicShelfRenderer: MusicShelfRenderer?,
val gridRenderer: GridRenderer?,
val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer?,
) {
@Serializable
data class MusicDescriptionShelfRenderer(
val description: Runs?,
)
}
}

View file

@ -0,0 +1,25 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class Tabs(
val tabs: List<Tab>?
) {
@Serializable
data class Tab(
val tabRenderer: TabRenderer?
) {
@Serializable
data class TabRenderer(
val content: Content?,
val title: String?,
val tabIdentifier: String?,
) {
@Serializable
data class Content(
val sectionListRenderer: SectionListRenderer?,
)
}
}
}

View file

@ -0,0 +1,21 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.Serializable
@Serializable
data class Thumbnail(
val url: String,
val height: Int?,
val width: Int?
) {
val isResizable: Boolean
get() = !url.startsWith("https://i.ytimg.com")
fun size(size: Int): String {
return when {
url.startsWith("https://lh3.googleusercontent.com") -> "$url-w$size-h$size"
url.startsWith("https://yt3.ggpht.com") -> "$url-s$size"
else -> url
}
}
}

View file

@ -0,0 +1,22 @@
package it.vfsfitvnm.youtubemusic.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class ThumbnailRenderer(
@JsonNames("croppedSquareThumbnailRenderer")
val musicThumbnailRenderer: MusicThumbnailRenderer?
) {
@Serializable
data class MusicThumbnailRenderer(
val thumbnail: Thumbnail?
) {
@Serializable
data class Thumbnail(
val thumbnails: List<it.vfsfitvnm.youtubemusic.models.Thumbnail>?
)
}
}

View file

@ -0,0 +1,11 @@
package it.vfsfitvnm.youtubemusic.models.bodies
import it.vfsfitvnm.youtubemusic.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class BrowseBody(
val context: Context = Context.DefaultWeb,
val browseId: String,
val params: String? = null
)

View file

@ -0,0 +1,10 @@
package it.vfsfitvnm.youtubemusic.models.bodies
import it.vfsfitvnm.youtubemusic.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class ContinuationBody(
val context: Context = Context.DefaultWeb,
val continuation: String,
)

View file

@ -0,0 +1,24 @@
package it.vfsfitvnm.youtubemusic.models.bodies
import it.vfsfitvnm.youtubemusic.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class NextBody(
val context: Context = Context.DefaultWeb,
val videoId: String?,
val isAudioOnly: Boolean = true,
val playlistId: String? = null,
val tunerSettingValue: String = "AUTOMIX_SETTING_NORMAL",
val index: Int? = null,
val params: String? = null,
val playlistSetVideoId: String? = null,
val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs = WatchEndpointMusicSupportedConfigs(
musicVideoType = "MUSIC_VIDEO_TYPE_ATV"
)
) {
@Serializable
data class WatchEndpointMusicSupportedConfigs(
val musicVideoType: String
)
}

View file

@ -0,0 +1,11 @@
package it.vfsfitvnm.youtubemusic.models.bodies
import it.vfsfitvnm.youtubemusic.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class PlayerBody(
val context: Context = Context.DefaultAndroid,
val videoId: String,
val playlistId: String? = null
)

View file

@ -0,0 +1,11 @@
package it.vfsfitvnm.youtubemusic.models.bodies
import it.vfsfitvnm.youtubemusic.models.Context
import kotlinx.serialization.Serializable
@Serializable
data class QueueBody(
val context: Context = Context.DefaultWeb,
val videoIds: List<String>? = null,
val playlistId: String? = null,
)

Some files were not shown because too many files have changed in this diff Show more