Rename youtube-music module to innertube and rewrite it
This commit is contained in:
parent
4bc3671be1
commit
917e194d63
126 changed files with 2210 additions and 2145 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val AlbumListSaver = ListSaver.of(AlbumSaver)
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val AlbumResultSaver = resultSaver(AlbumSaver)
|
|
@ -27,3 +27,7 @@ object AlbumSaver : Saver<Album, List<Any?>> {
|
|||
bookmarkedAt = value[7] as Long?,
|
||||
)
|
||||
}
|
||||
|
||||
val AlbumResultSaver = resultSaver(AlbumSaver)
|
||||
|
||||
val AlbumListSaver = listSaver(AlbumSaver)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val ArtistListSaver = ListSaver.of(ArtistSaver)
|
|
@ -23,3 +23,5 @@ object ArtistSaver : Saver<Artist, List<Any?>> {
|
|||
bookmarkedAt = value[5] as Long?,
|
||||
)
|
||||
}
|
||||
|
||||
val ArtistListSaver = listSaver(ArtistSaver)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val DetailedSongListSaver = ListSaver.of(DetailedSongSaver)
|
|
@ -29,3 +29,5 @@ object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
|||
artists = (value[7] as List<List<String>>?)?.let(InfoListSaver::restore)
|
||||
)
|
||||
}
|
||||
|
||||
val DetailedSongListSaver = listSaver(DetailedSongSaver)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val InfoListSaver = ListSaver.of(InfoSaver)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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?
|
||||
)
|
||||
}
|
|
@ -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)
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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)
|
|
@ -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?,
|
||||
)
|
||||
}
|
|
@ -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)
|
|
@ -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,
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver)
|
|
@ -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(
|
||||
with(PlaylistSaver) { save(value.playlist) },
|
||||
value.songCount,
|
||||
)
|
||||
}
|
||||
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(
|
||||
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||
songCount = value[1] as Int,
|
||||
) else null
|
||||
}
|
||||
override fun restore(value: List<Any>) = PlaylistPreview(
|
||||
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||
songCount = value[1] as Int,
|
||||
)
|
||||
}
|
||||
|
||||
val PlaylistPreviewListSaver = listSaver(PlaylistPreviewSaver)
|
||||
|
|
|
@ -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(
|
||||
with(PlaylistSaver) { save(value.playlist) },
|
||||
with(DetailedSongListSaver) { save(value.songs) },
|
||||
)
|
||||
}
|
||||
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(
|
||||
|
|
|
@ -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()
|
||||
}
|
37
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt
Normal file
37
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt
Normal 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)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val SearchQueryListSaver = ListSaver.of(SearchQuerySaver)
|
|
@ -15,3 +15,5 @@ object SearchQuerySaver : Saver<SearchQuery, List<Any?>> {
|
|||
query = value[1] as String
|
||||
)
|
||||
}
|
||||
|
||||
val SearchQueryListSaver = listSaver(SearchQuerySaver)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
|
||||
val StringListResultSaver = resultSaver(autoSaver<List<String>?>())
|
|
@ -1,5 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
|
||||
val StringResultSaver = resultSaver(autoSaver<String?>())
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver)
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver)
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver)
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver)
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver)
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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?,
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.savers
|
||||
|
||||
val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver)
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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 ->
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -27,7 +27,9 @@ fun PlaylistScreen(browseId: String) {
|
|||
}
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
PlaylistSongList(browseId = browseId)
|
||||
when (currentTabIndex) {
|
||||
0 -> PlaylistSongList(browseId = browseId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
videoId = videoId,
|
||||
playlistId = playlistId,
|
||||
params = parameters,
|
||||
playlistSetVideoId = playlistSetVideoId,
|
||||
continuation = nextContinuation
|
||||
)?.getOrNull()?.let { nextResult ->
|
||||
playlistId = nextResult.playlistId
|
||||
parameters = nextResult.params
|
||||
playlistSetVideoId = nextResult.playlistSetVideoId
|
||||
val continuation = nextContinuation
|
||||
|
||||
mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem)
|
||||
nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation }
|
||||
if (continuation == null) {
|
||||
Innertube.nextPage(
|
||||
NextBody(
|
||||
videoId = videoId,
|
||||
playlistId = playlistId,
|
||||
params = parameters,
|
||||
playlistSetVideoId = playlistSetVideoId
|
||||
)
|
||||
)?.map { nextResult ->
|
||||
playlistId = nextResult.playlistId
|
||||
parameters = nextResult.params
|
||||
playlistSetVideoId = nextResult.playlistSetVideoId
|
||||
|
||||
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()
|
||||
|
|
204
innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt
Normal file
204
innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt
Normal 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?
|
||||
)
|
||||
}
|
|
@ -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?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Serializable
|
||||
data class ButtonRenderer(
|
||||
val navigationEndpoint: NavigationEndpoint
|
||||
val navigationEndpoint: NavigationEndpoint?
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
)
|
||||
}
|
|
@ -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?,
|
||||
)
|
||||
}
|
|
@ -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>?,
|
|
@ -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?
|
||||
)
|
||||
}
|
|
@ -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?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ data class MusicResponsiveListItemRenderer(
|
|||
@Serializable
|
||||
data class FlexColumn(
|
||||
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
|
||||
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer
|
||||
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
|
||||
) {
|
||||
@Serializable
|
||||
data class MusicResponsiveListItemFlexColumnRenderer(
|
|
@ -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
|
|
@ -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?,
|
||||
)
|
|
@ -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
|
|
@ -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?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(
|
|
@ -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?,
|
||||
)
|
|
@ -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?,
|
||||
)
|
||||
}
|
|
@ -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?
|
||||
)
|
||||
}
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
Loading…
Reference in a new issue