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"
|
freeCompilerArgs += "-Xcontext-receivers"
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes.add("META-INF/INDEX.LIST")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
|
@ -93,7 +97,7 @@ dependencies {
|
||||||
kapt(libs.room.compiler)
|
kapt(libs.room.compiler)
|
||||||
annotationProcessor(libs.room.compiler)
|
annotationProcessor(libs.room.compiler)
|
||||||
|
|
||||||
implementation(projects.youtubeMusic)
|
implementation(projects.innertube)
|
||||||
implementation(projects.kugou)
|
implementation(projects.kugou)
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
|
|
|
@ -81,7 +81,10 @@ import it.vfsfitvnm.vimusic.utils.intent
|
||||||
import it.vfsfitvnm.vimusic.utils.listener
|
import it.vfsfitvnm.vimusic.utils.listener
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -376,8 +379,8 @@ class MainActivity : ComponentActivity() {
|
||||||
val browseId = "VL$playlistId"
|
val browseId = "VL$playlistId"
|
||||||
|
|
||||||
if (playlistId.startsWith("OLAK5uy_")) {
|
if (playlistId.startsWith("OLAK5uy_")) {
|
||||||
YouTube.playlist(browseId)?.getOrNull()?.let { playlist ->
|
Innertube.playlistPage(BrowseBody(browseId = browseId))?.getOrNull()?.let { playlist ->
|
||||||
playlist.songs?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId ->
|
playlist.songsPage?.items?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId ->
|
||||||
albumRoute.ensureGlobal(browseId)
|
albumRoute.ensureGlobal(browseId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,7 +388,7 @@ class MainActivity : ComponentActivity() {
|
||||||
playlistRoute.ensureGlobal(browseId)
|
playlistRoute.ensureGlobal(browseId)
|
||||||
}
|
}
|
||||||
} ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId ->
|
} ?: (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) {
|
withContext(Dispatchers.Main) {
|
||||||
binder?.player?.forcePlay(song.asMediaItem)
|
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?,
|
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?,
|
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)
|
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
|
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 androidx.compose.runtime.saveable.SaverScope
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
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(
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
|
||||||
value.browseId,
|
value.browseId,
|
||||||
value.params
|
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 androidx.compose.runtime.saveable.SaverScope
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
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(
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
|
||||||
value.params,
|
value.params,
|
||||||
value.playlistId,
|
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 androidx.compose.runtime.saveable.SaverScope
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
|
||||||
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any?>> {
|
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any>> {
|
||||||
override fun SaverScope.save(value: PlaylistPreview): List<Any> {
|
override fun SaverScope.save(value: PlaylistPreview) = listOf(
|
||||||
return listOf(
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
with(PlaylistSaver) { save(value.playlist) },
|
value.songCount,
|
||||||
value.songCount,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun restore(value: List<Any?>): PlaylistPreview? {
|
override fun restore(value: List<Any>) = PlaylistPreview(
|
||||||
return if (value.size == 2) PlaylistPreview(
|
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||||
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
songCount = value[1] as Int,
|
||||||
songCount = value[1] as Int,
|
)
|
||||||
) else null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val PlaylistPreviewListSaver = listSaver(PlaylistPreviewSaver)
|
||||||
|
|
|
@ -4,13 +4,11 @@ import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||||
|
|
||||||
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs?, List<Any>> {
|
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs, List<Any>> {
|
||||||
override fun SaverScope.save(value: PlaylistWithSongs?) = value?.let {
|
override fun SaverScope.save(value: PlaylistWithSongs) = listOf(
|
||||||
listOf(
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
with(PlaylistSaver) { save(value.playlist) },
|
with(DetailedSongListSaver) { save(value.songs) },
|
||||||
with(DetailedSongListSaver) { save(value.songs) },
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun restore(value: List<Any>): PlaylistWithSongs = PlaylistWithSongs(
|
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
|
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.skipSilenceKey
|
||||||
import it.vfsfitvnm.vimusic.utils.timer
|
import it.vfsfitvnm.vimusic.utils.timer
|
||||||
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
|
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.NavigationEndpoint
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.player
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -642,9 +644,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
||||||
else -> {
|
else -> {
|
||||||
val urlResult = runBlocking(Dispatchers.IO) {
|
val urlResult = runBlocking(Dispatchers.IO) {
|
||||||
YouTube.player(videoId)
|
Innertube.player(PlayerBody(videoId = videoId))
|
||||||
}?.mapCatching { body ->
|
}?.mapCatching { body ->
|
||||||
when (val status = body.playabilityStatus.status) {
|
when (val status = body.playabilityStatus?.status) {
|
||||||
"OK" -> body.streamingData?.adaptiveFormats?.findLast { format ->
|
"OK" -> body.streamingData?.adaptiveFormats?.findLast { format ->
|
||||||
format.itag == 251 || format.itag == 140
|
format.itag == 251 || format.itag == 140
|
||||||
}?.let { format ->
|
}?.let { format ->
|
||||||
|
|
|
@ -61,12 +61,13 @@ import it.vfsfitvnm.vimusic.utils.color
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -88,19 +89,21 @@ fun AlbumOverview(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Database.album(browseId).collect { album ->
|
Database.album(browseId).collect { album ->
|
||||||
if (album?.timestamp == null) {
|
if (album?.timestamp == null) {
|
||||||
YouTube.album(browseId)?.onSuccess { youtubeAlbum ->
|
Innertube.albumPage(BrowseBody(browseId = browseId))?.onSuccess { albumPage ->
|
||||||
Database.upsert(
|
Database.upsert(
|
||||||
Album(
|
Album(
|
||||||
id = browseId,
|
id = browseId,
|
||||||
title = youtubeAlbum.title,
|
title = albumPage.title,
|
||||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
thumbnailUrl = albumPage.thumbnail?.url,
|
||||||
year = youtubeAlbum.year,
|
year = albumPage.year,
|
||||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" },
|
authorsText = albumPage.authors?.joinToString("") { it.name ?: "" },
|
||||||
shareUrl = youtubeAlbum.url,
|
shareUrl = albumPage.url,
|
||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
),
|
),
|
||||||
youtubeAlbum.songs
|
albumPage
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
.songsPage
|
||||||
|
?.items
|
||||||
|
?.map(Innertube.SongItem::asMediaItem)
|
||||||
?.onEach(Database::insert)
|
?.onEach(Database::insert)
|
||||||
?.mapIndexed { position, mediaItem ->
|
?.mapIndexed { position, mediaItem ->
|
||||||
SongAlbumMap(
|
SongAlbumMap(
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.center
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <T : YouTube.Item> ArtistContent(
|
inline fun <T : Innertube.Item> ArtistContent(
|
||||||
artist: Artist?,
|
artist: Artist?,
|
||||||
youtubeArtist: YouTube.Artist?,
|
youtubeArtistPage: Innertube.ArtistPage?,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isError: Boolean,
|
isError: Boolean,
|
||||||
stateSaver: ListSaver<T, List<Any?>>,
|
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 bookmarkIconContent: @Composable () -> Unit,
|
||||||
crossinline shareIconContent: @Composable () -> Unit,
|
crossinline shareIconContent: @Composable () -> Unit,
|
||||||
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
||||||
|
@ -61,18 +60,18 @@ inline fun <T : YouTube.Item> ArtistContent(
|
||||||
val (continuationState, fetch) = produceSaveableRelaunchableOneShotState(
|
val (continuationState, fetch) = produceSaveableRelaunchableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = autoSaver<String?>(),
|
stateSaver = autoSaver<String?>(),
|
||||||
youtubeArtist
|
youtubeArtistPage
|
||||||
) {
|
) {
|
||||||
if (youtubeArtist == null) return@produceSaveableRelaunchableOneShotState
|
if (youtubeArtistPage == null) return@produceSaveableRelaunchableOneShotState
|
||||||
|
|
||||||
println("loading... $value")
|
println("loading... $value")
|
||||||
|
|
||||||
isLoadingItems = true
|
isLoadingItems = true
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
itemsProvider(value)?.onSuccess { (continuation, newItems) ->
|
itemsPageProvider(value)?.onSuccess { itemsPage ->
|
||||||
value = continuation
|
value = itemsPage?.continuation
|
||||||
newItems?.let {
|
itemsPage?.items?.let {
|
||||||
items = items.plus(it).distinctBy(YouTube.Item::key)
|
items = items.plus(it).distinctBy(Innertube.Item::key)
|
||||||
}
|
}
|
||||||
isErrorItems = false
|
isErrorItems = false
|
||||||
isLoadingItems = false
|
isLoadingItems = false
|
||||||
|
@ -105,7 +104,7 @@ inline fun <T : YouTube.Item> ArtistContent(
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = items,
|
items = items,
|
||||||
key = YouTube.Item::key,
|
key = Innertube.Item::key,
|
||||||
itemContent = itemContent
|
itemContent = itemContent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -57,14 +57,14 @@ import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
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.NavigationEndpoint
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistOverview(
|
fun ArtistOverview(
|
||||||
artist: Artist?,
|
artist: Artist?,
|
||||||
youtubeArtist: YouTube.Artist?,
|
youtubeArtistPage: Innertube.ArtistPage?,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isError: Boolean,
|
isError: Boolean,
|
||||||
onViewAllSongsClick: () -> Unit,
|
onViewAllSongsClick: () -> Unit,
|
||||||
|
@ -100,7 +100,7 @@ fun ArtistOverview(
|
||||||
when {
|
when {
|
||||||
artist != null -> {
|
artist != null -> {
|
||||||
Header(title = artist.name ?: "Unknown") {
|
Header(title = artist.name ?: "Unknown") {
|
||||||
youtubeArtist?.radioEndpoint?.let { radioEndpoint ->
|
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
|
||||||
SecondaryTextButton(
|
SecondaryTextButton(
|
||||||
text = "Start radio",
|
text = "Start radio",
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -130,8 +130,8 @@ fun ArtistOverview(
|
||||||
)
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
youtubeArtist != null -> {
|
youtubeArtistPage != null -> {
|
||||||
youtubeArtist.songs?.let { songs ->
|
youtubeArtistPage.songs?.let { songs ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
@ -144,7 +144,7 @@ fun ArtistOverview(
|
||||||
modifier = sectionTextModifier
|
modifier = sectionTextModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
youtubeArtist.songsEndpoint?.let {
|
youtubeArtistPage.songsEndpoint?.let {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "View all",
|
text = "View all",
|
||||||
style = typography.xs.secondary,
|
style = typography.xs.secondary,
|
||||||
|
@ -174,7 +174,7 @@ fun ArtistOverview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
youtubeArtist.albums?.let { albums ->
|
youtubeArtistPage.albums?.let { albums ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
@ -187,7 +187,7 @@ fun ArtistOverview(
|
||||||
modifier = sectionTextModifier
|
modifier = sectionTextModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
youtubeArtist.albumsEndpoint?.let {
|
youtubeArtistPage.albumsEndpoint?.let {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "View all",
|
text = "View all",
|
||||||
style = typography.xs.secondary,
|
style = typography.xs.secondary,
|
||||||
|
@ -207,7 +207,7 @@ fun ArtistOverview(
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = albums,
|
items = albums,
|
||||||
key = YouTube.Item.Album::key
|
key = Innertube.AlbumItem::key
|
||||||
) { album ->
|
) { album ->
|
||||||
AlternativeAlbumItem(
|
AlternativeAlbumItem(
|
||||||
album = album,
|
album = album,
|
||||||
|
@ -224,7 +224,7 @@ fun ArtistOverview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
youtubeArtist.singles?.let { singles ->
|
youtubeArtistPage.singles?.let { singles ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
@ -237,7 +237,7 @@ fun ArtistOverview(
|
||||||
modifier = sectionTextModifier
|
modifier = sectionTextModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
youtubeArtist.singlesEndpoint?.let {
|
youtubeArtistPage.singlesEndpoint?.let {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "View all",
|
text = "View all",
|
||||||
style = typography.xs.secondary,
|
style = typography.xs.secondary,
|
||||||
|
@ -257,7 +257,7 @@ fun ArtistOverview(
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = singles,
|
items = singles,
|
||||||
key = YouTube.Item.Album::key
|
key = Innertube.AlbumItem::key
|
||||||
) { album ->
|
) { album ->
|
||||||
AlternativeAlbumItem(
|
AlternativeAlbumItem(
|
||||||
album = album,
|
album = album,
|
||||||
|
@ -330,7 +330,7 @@ fun ArtistOverview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
youtubeArtist?.shuffleEndpoint?.let { shuffleEndpoint ->
|
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
iconId = R.drawable.shuffle,
|
iconId = R.drawable.shuffle,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
@ -26,14 +26,13 @@ import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.PartialArtist
|
import it.vfsfitvnm.vimusic.models.PartialArtist
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.savers.ArtistSaver
|
import it.vfsfitvnm.vimusic.savers.ArtistSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeArtistPageSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
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.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
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.produceSaveableLazyOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
@ -72,22 +76,22 @@ fun ArtistScreen(browseId: String) {
|
||||||
|
|
||||||
val youtubeArtist by produceSaveableLazyOneShotState(
|
val youtubeArtist by produceSaveableLazyOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = nullableSaver(YouTubeArtistPageSaver)
|
stateSaver = nullableSaver(InnertubeArtistPageSaver)
|
||||||
) {
|
) {
|
||||||
println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!")
|
println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!")
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
YouTube.artist(browseId)?.onSuccess { youtubeArtist ->
|
Innertube.artistPage(browseId)?.onSuccess { artistPage ->
|
||||||
value = youtubeArtist
|
value = artistPage
|
||||||
|
|
||||||
query {
|
query {
|
||||||
Database.upsert(
|
Database.upsert(
|
||||||
PartialArtist(
|
PartialArtist(
|
||||||
id = browseId,
|
id = browseId,
|
||||||
name = youtubeArtist.name,
|
name = artistPage.name,
|
||||||
thumbnailUrl = youtubeArtist.thumbnail?.url,
|
thumbnailUrl = artistPage.thumbnail?.url,
|
||||||
info = youtubeArtist.description,
|
info = artistPage.description,
|
||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -136,10 +140,13 @@ fun ArtistScreen(browseId: String) {
|
||||||
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent),
|
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
val bookmarkedAt = if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
|
val bookmarkedAt =
|
||||||
|
if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
|
||||||
|
|
||||||
query {
|
query {
|
||||||
artist?.copy(bookmarkedAt = bookmarkedAt)?.let(Database::update)
|
artist
|
||||||
|
?.copy(bookmarkedAt = bookmarkedAt)
|
||||||
|
?.let(Database::update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
|
@ -194,7 +201,7 @@ fun ArtistScreen(browseId: String) {
|
||||||
when (currentTabIndex) {
|
when (currentTabIndex) {
|
||||||
0 -> ArtistOverview(
|
0 -> ArtistOverview(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
youtubeArtist = youtubeArtist,
|
youtubeArtistPage = youtubeArtist,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isError = isError,
|
isError = isError,
|
||||||
bookmarkIconContent = bookmarkIconContent,
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
|
@ -204,6 +211,7 @@ fun ArtistScreen(browseId: String) {
|
||||||
onViewAllAlbumsClick = { onTabIndexChanged(2) },
|
onViewAllAlbumsClick = { onTabIndexChanged(2) },
|
||||||
onViewAllSinglesClick = { onTabIndexChanged(3) },
|
onViewAllSinglesClick = { onTabIndexChanged(3) },
|
||||||
)
|
)
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
@ -211,20 +219,26 @@ fun ArtistScreen(browseId: String) {
|
||||||
|
|
||||||
ArtistContent(
|
ArtistContent(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
youtubeArtist = youtubeArtist,
|
youtubeArtistPage = youtubeArtist,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isError = isError,
|
isError = isError,
|
||||||
stateSaver = YouTubeSongListSaver,
|
stateSaver = InnertubeSongItemListSaver,
|
||||||
bookmarkIconContent = bookmarkIconContent,
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
shareIconContent = shareIconContent,
|
shareIconContent = shareIconContent,
|
||||||
itemsProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
youtubeArtist
|
continuation?.let {
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = ContinuationBody(continuation = continuation),
|
||||||
|
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
|
||||||
|
)
|
||||||
|
} ?: youtubeArtist
|
||||||
?.songsEndpoint
|
?.songsEndpoint
|
||||||
?.browseId
|
?.browseId
|
||||||
?.let { browseId ->
|
?.let { browseId ->
|
||||||
YouTube.items(browseId, continuation, YouTube.Item.Song::from)?.map { result ->
|
Innertube.itemsPage(
|
||||||
result?.continuation to result?.items
|
body = BrowseBody(browseId = browseId),
|
||||||
}
|
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemContent = { song ->
|
itemContent = { song ->
|
||||||
|
@ -243,25 +257,33 @@ fun ArtistScreen(browseId: String) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ArtistContent(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
youtubeArtist = youtubeArtist,
|
youtubeArtistPage = youtubeArtist,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isError = isError,
|
isError = isError,
|
||||||
stateSaver = YouTubeAlbumListSaver,
|
stateSaver = InnertubeAlbumItemListSaver,
|
||||||
bookmarkIconContent = bookmarkIconContent,
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
shareIconContent = shareIconContent,
|
shareIconContent = shareIconContent,
|
||||||
itemsProvider = {
|
itemsPageProvider = { continuation ->
|
||||||
youtubeArtist
|
continuation?.let {
|
||||||
?.albumsEndpoint
|
Innertube.itemsPage(
|
||||||
?.let { endpoint ->
|
body = ContinuationBody(continuation = continuation),
|
||||||
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
result?.continuation to result?.items
|
)
|
||||||
}
|
} ?: youtubeArtist
|
||||||
|
?.songsEndpoint
|
||||||
|
?.browseId
|
||||||
|
?.let { browseId ->
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = BrowseBody(browseId = browseId),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemContent = { album ->
|
itemContent = { album ->
|
||||||
|
@ -282,25 +304,33 @@ fun ArtistScreen(browseId: String) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
3 -> {
|
3 -> {
|
||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ArtistContent(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
youtubeArtist = youtubeArtist,
|
youtubeArtistPage = youtubeArtist,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isError = isError,
|
isError = isError,
|
||||||
stateSaver = YouTubeAlbumListSaver,
|
stateSaver = InnertubeAlbumItemListSaver,
|
||||||
bookmarkIconContent = bookmarkIconContent,
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
shareIconContent = shareIconContent,
|
shareIconContent = shareIconContent,
|
||||||
itemsProvider = {
|
itemsPageProvider = { continuation ->
|
||||||
youtubeArtist
|
continuation?.let {
|
||||||
?.singlesEndpoint
|
Innertube.itemsPage(
|
||||||
?.let { endpoint ->
|
body = ContinuationBody(continuation = continuation),
|
||||||
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
result?.continuation to result?.items
|
)
|
||||||
}
|
} ?: youtubeArtist
|
||||||
|
?.songsEndpoint
|
||||||
|
?.browseId
|
||||||
|
?.let { browseId ->
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = BrowseBody(browseId = browseId),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemContent = { album ->
|
itemContent = { album ->
|
||||||
|
@ -321,6 +351,7 @@ fun ArtistScreen(browseId: String) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
4 -> ArtistLocalSongsList(
|
4 -> ArtistLocalSongsList(
|
||||||
browseId = browseId,
|
browseId = browseId,
|
||||||
artist = artist,
|
artist = artist,
|
||||||
|
|
|
@ -34,7 +34,7 @@ import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
|
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.nullableSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.resultSaver
|
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
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.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
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.NavigationEndpoint
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.relatedPage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
@ -88,30 +90,13 @@ fun QuickPicks(
|
||||||
.collect { value = it }
|
.collect { value = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
val relatedResult by produceSaveableOneShotState(
|
val relatedPageResult by produceSaveableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)),
|
stateSaver = resultSaver(nullableSaver(InnertubeRelatedPageSaver)),
|
||||||
trending?.id
|
trending?.id
|
||||||
) {
|
) {
|
||||||
trending?.id?.let { trendingVideoId ->
|
trending?.id?.let { trendingVideoId ->
|
||||||
value = YouTube.related(trendingVideoId)?.map { related ->
|
value = Innertube.relatedPage(NextBody(videoId = trendingVideoId))
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +125,7 @@ fun QuickPicks(
|
||||||
) {
|
) {
|
||||||
Header(title = "Quick picks")
|
Header(title = "Quick picks")
|
||||||
|
|
||||||
relatedResult?.getOrNull()?.let { related ->
|
relatedPageResult?.getOrNull()?.let { related ->
|
||||||
LazyHorizontalGrid(
|
LazyHorizontalGrid(
|
||||||
rows = GridCells.Fixed(4),
|
rows = GridCells.Fixed(4),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -171,7 +156,7 @@ fun QuickPicks(
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = related.songs ?: emptyList(),
|
items = related.songs ?: emptyList(),
|
||||||
key = YouTube.Item.Song::key
|
key = Innertube.SongItem::key
|
||||||
) { song ->
|
) { song ->
|
||||||
SmallSongItem(
|
SmallSongItem(
|
||||||
song = song,
|
song = song,
|
||||||
|
@ -204,7 +189,7 @@ fun QuickPicks(
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = related.albums ?: emptyList(),
|
items = related.albums ?: emptyList(),
|
||||||
key = YouTube.Item.Album::key
|
key = Innertube.AlbumItem::key
|
||||||
) { album ->
|
) { album ->
|
||||||
AlbumItem(
|
AlbumItem(
|
||||||
album = album,
|
album = album,
|
||||||
|
@ -235,7 +220,7 @@ fun QuickPicks(
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = related.artists ?: emptyList(),
|
items = related.artists ?: emptyList(),
|
||||||
key = YouTube.Item.Artist::key,
|
key = Innertube.ArtistItem::key,
|
||||||
) { artist ->
|
) { artist ->
|
||||||
ArtistItem(
|
ArtistItem(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
|
@ -268,7 +253,7 @@ fun QuickPicks(
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = related.playlists ?: emptyList(),
|
items = related.playlists ?: emptyList(),
|
||||||
key = YouTube.Item.Playlist::key,
|
key = Innertube.PlaylistItem::key,
|
||||||
) { playlist ->
|
) { playlist ->
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
playlist = playlist,
|
playlist = playlist,
|
||||||
|
@ -284,7 +269,7 @@ fun QuickPicks(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: relatedResult?.exceptionOrNull()?.let {
|
} ?: relatedPageResult?.exceptionOrNull()?.let {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "An error has occurred",
|
text = "An error has occurred",
|
||||||
style = typography.s.secondary.center,
|
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.models.SongPlaylistMap
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
|
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
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.forcePlayAtIndex
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -68,7 +71,7 @@ fun LocalPlaylistSongList(
|
||||||
|
|
||||||
val playlistWithSongs by produceSaveableState(
|
val playlistWithSongs by produceSaveableState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = PlaylistWithSongsSaver
|
stateSaver = nullableSaver(PlaylistWithSongsSaver)
|
||||||
) {
|
) {
|
||||||
Database
|
Database
|
||||||
.playlistWithSongs(playlistId)
|
.playlistWithSongs(playlistId)
|
||||||
|
@ -165,13 +168,16 @@ fun LocalPlaylistSongList(
|
||||||
transaction {
|
transaction {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
YouTube.playlist(browseId)?.map { it.next() }
|
// TODO: fetch all songs!
|
||||||
|
Innertube.playlistPage(BrowseBody(browseId = browseId))
|
||||||
}
|
}
|
||||||
}?.getOrNull()?.let { remotePlaylist ->
|
}?.getOrNull()?.let { remotePlaylist ->
|
||||||
Database.clearPlaylist(playlistId)
|
Database.clearPlaylist(playlistId)
|
||||||
|
|
||||||
remotePlaylist.songs
|
remotePlaylist.
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
songsPage
|
||||||
|
?.items
|
||||||
|
?.map(Innertube.SongItem::asMediaItem)
|
||||||
?.onEach(Database::insert)
|
?.onEach(Database::insert)
|
||||||
?.mapIndexed { position, mediaItem ->
|
?.mapIndexed { position, mediaItem ->
|
||||||
SongPlaylistMap(
|
SongPlaylistMap(
|
||||||
|
|
|
@ -69,7 +69,9 @@ import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
@ -134,8 +136,7 @@ fun Lyrics(
|
||||||
duration = duration / 1000
|
duration = duration / 1000
|
||||||
)?.map { it?.value }
|
)?.map { it?.value }
|
||||||
} else {
|
} else {
|
||||||
YouTube.next(mediaId, null)
|
Innertube.lyrics(NextBody(videoId = mediaId))
|
||||||
?.map { nextResult -> nextResult.lyrics()?.getOrNull() }
|
|
||||||
}?.map { newLyrics ->
|
}?.map { newLyrics ->
|
||||||
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
|
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
|
||||||
state = state.copy(isLoading = false)
|
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.color
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberVolume
|
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 kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
@ -195,7 +197,7 @@ fun StatsForNerds(
|
||||||
onClick = {
|
onClick = {
|
||||||
query {
|
query {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
YouTube.player(mediaId)
|
Innertube.player(PlayerBody(videoId = mediaId))
|
||||||
?.map { response ->
|
?.map { response ->
|
||||||
response.streamingData?.adaptiveFormats
|
response.streamingData?.adaptiveFormats
|
||||||
?.findLast { format ->
|
?.findLast { format ->
|
||||||
|
|
|
@ -27,7 +27,9 @@ fun PlaylistScreen(browseId: String) {
|
||||||
}
|
}
|
||||||
) { currentTabIndex ->
|
) { currentTabIndex ->
|
||||||
saveableStateHolder.SaveableStateProvider(key = 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.R
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistOrAlbumSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.resultSaver
|
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
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.enqueue
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -76,12 +77,13 @@ fun PlaylistSongList(
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val playlistResult by produceSaveableOneShotState(
|
val playlistPageResult by produceSaveableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
|
stateSaver = resultSaver(InnertubePlaylistOrAlbumPageSaver),
|
||||||
) {
|
) {
|
||||||
value = withContext(Dispatchers.IO) {
|
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 songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val songThumbnailSizePx = songThumbnailSizeDp.px
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
|
||||||
playlistResult?.getOrNull()?.let { playlist ->
|
playlistPageResult?.getOrNull()?.let { playlist ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -117,9 +119,9 @@ fun PlaylistSongList(
|
||||||
Header(title = playlist.title ?: "Unknown") {
|
Header(title = playlist.title ?: "Unknown") {
|
||||||
SecondaryTextButton(
|
SecondaryTextButton(
|
||||||
text = "Enqueue",
|
text = "Enqueue",
|
||||||
isEnabled = playlist.songs?.isNotEmpty() == true,
|
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
|
||||||
onClick = {
|
onClick = {
|
||||||
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
|
||||||
binder?.player?.enqueue(mediaItems)
|
binder?.player?.enqueue(mediaItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,8 +149,8 @@ fun PlaylistSongList(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
playlist.songs
|
playlist.songsPage?.items
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
?.map(Innertube.SongItem::asMediaItem)
|
||||||
?.onEach(Database::insert)
|
?.onEach(Database::insert)
|
||||||
?.mapIndexed { index, mediaItem ->
|
?.mapIndexed { index, mediaItem ->
|
||||||
SongPlaylistMap(
|
SongPlaylistMap(
|
||||||
|
@ -196,13 +198,13 @@ fun PlaylistSongList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
|
itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song ->
|
||||||
SongItem(
|
SongItem(
|
||||||
title = song.info?.name,
|
title = song.info?.name,
|
||||||
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" },
|
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" },
|
||||||
durationText = song.durationText,
|
durationText = song.durationText,
|
||||||
onClick = {
|
onClick = {
|
||||||
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
}
|
}
|
||||||
|
@ -226,15 +228,15 @@ fun PlaylistSongList(
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
iconId = R.drawable.shuffle,
|
iconId = R.drawable.shuffle,
|
||||||
isEnabled = playlist.songs?.isNotEmpty() == true,
|
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
|
||||||
onClick = {
|
onClick = {
|
||||||
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
|
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} ?: playlistResult?.exceptionOrNull()?.let {
|
} ?: playlistPageResult?.exceptionOrNull()?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.paint
|
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.models.SearchQuery
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver
|
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.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
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.produceSaveableOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
@ -80,11 +83,11 @@ fun OnlineSearch(
|
||||||
|
|
||||||
val suggestionsResult by produceSaveableOneShotState(
|
val suggestionsResult by produceSaveableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = StringListResultSaver,
|
stateSaver = resultSaver(autoSaver<List<String>?>()),
|
||||||
key1 = textFieldValue.text
|
key1 = textFieldValue.text
|
||||||
) {
|
) {
|
||||||
if (textFieldValue.text.isNotEmpty()) {
|
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.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -23,23 +24,28 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.savers.ListSaver
|
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.components.themed.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <T : YouTube.Item> SearchResult(
|
inline fun <T : Innertube.Item> SearchResult(
|
||||||
query: String,
|
query: String,
|
||||||
filter: String,
|
filter: String,
|
||||||
stateSaver: ListSaver<T, List<Any?>>,
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
|
noinline fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?,
|
||||||
crossinline onSearchAgain: () -> Unit,
|
crossinline onSearchAgain: () -> Unit,
|
||||||
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
||||||
noinline itemShimmer: @Composable BoxScope.() -> Unit,
|
noinline itemShimmer: @Composable BoxScope.() -> Unit,
|
||||||
|
@ -52,21 +58,30 @@ inline fun <T : YouTube.Item> SearchResult(
|
||||||
|
|
||||||
val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
|
val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = StringResultSaver
|
stateSaver = resultSaver(autoSaver<String?>())
|
||||||
) {
|
) {
|
||||||
val token = value?.getOrNull()
|
val token = value?.getOrNull()
|
||||||
|
|
||||||
value = null
|
value = null
|
||||||
|
|
||||||
value = withContext(Dispatchers.IO) {
|
value = withContext(Dispatchers.IO) {
|
||||||
YouTube.search(query, filter, token)
|
if (token == null) {
|
||||||
}?.map { searchResult ->
|
Innertube.searchPage(
|
||||||
@Suppress("UNCHECKED_CAST")
|
body = SearchBody(query = query, params = filter),
|
||||||
(searchResult.items as List<T>?)?.let {
|
fromMusicShelfRendererContent = fromMusicShelfRendererContent
|
||||||
items = items.plus(it).distinctBy(YouTube.Item::key)
|
)
|
||||||
|
} 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 = items,
|
items = items,
|
||||||
key = YouTube.Item::key,
|
key = Innertube.Item::key,
|
||||||
itemContent = itemContent
|
itemContent = itemContent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,15 @@ import androidx.compose.ui.unit.dp
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeArtistItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeVideoItemListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
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.screens.playlistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
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.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey
|
import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.utils.from
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
|
@ -52,12 +52,6 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
globalRoutes()
|
globalRoutes()
|
||||||
|
|
||||||
playlistRoute { browseId ->
|
|
||||||
PlaylistScreen(
|
|
||||||
browseId = browseId ?: "browseId cannot be null"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
host {
|
host {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topIconButtonId = R.drawable.chevron_back,
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
@ -74,12 +68,12 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
}
|
}
|
||||||
) { tabIndex ->
|
) { tabIndex ->
|
||||||
val searchFilter = when (tabIndex) {
|
val searchFilter = when (tabIndex) {
|
||||||
0 -> YouTube.Item.Song.Filter
|
0 -> Innertube.SearchFilter.Song
|
||||||
1 -> YouTube.Item.Album.Filter
|
1 -> Innertube.SearchFilter.Album
|
||||||
2 -> YouTube.Item.Artist.Filter
|
2 -> Innertube.SearchFilter.Artist
|
||||||
3 -> YouTube.Item.Video.Filter
|
3 -> Innertube.SearchFilter.Video
|
||||||
4 -> YouTube.Item.CommunityPlaylist.Filter
|
4 -> Innertube.SearchFilter.CommunityPlaylist
|
||||||
5 -> YouTube.Item.FeaturedPlaylist.Filter
|
5 -> Innertube.SearchFilter.FeaturedPlaylist
|
||||||
else -> error("unreachable")
|
else -> error("unreachable")
|
||||||
}.value
|
}.value
|
||||||
|
|
||||||
|
@ -94,7 +88,8 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
stateSaver = YouTubeSongListSaver,
|
stateSaver = InnertubeSongItemListSaver,
|
||||||
|
fromMusicShelfRendererContent = Innertube.SongItem.Companion::from,
|
||||||
itemContent = { song ->
|
itemContent = { song ->
|
||||||
SmallSongItem(
|
SmallSongItem(
|
||||||
song = song,
|
song = song,
|
||||||
|
@ -119,8 +114,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
SearchResult(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubeAlbumListSaver,
|
stateSaver = InnertubeAlbumItemListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
|
fromMusicShelfRendererContent = Innertube.AlbumItem.Companion::from,
|
||||||
itemContent = { album ->
|
itemContent = { album ->
|
||||||
AlbumItem(
|
AlbumItem(
|
||||||
album = album,
|
album = album,
|
||||||
|
@ -148,8 +144,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
SearchResult(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubeArtistListSaver,
|
stateSaver = InnertubeArtistItemListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
|
fromMusicShelfRendererContent = Innertube.ArtistItem.Companion::from,
|
||||||
itemContent = { artist ->
|
itemContent = { artist ->
|
||||||
ArtistItem(
|
ArtistItem(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
|
@ -176,8 +173,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
SearchResult(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubeVideoListSaver,
|
stateSaver = InnertubeVideoItemListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
|
fromMusicShelfRendererContent = Innertube.VideoItem.Companion::from,
|
||||||
itemContent = { video ->
|
itemContent = { video ->
|
||||||
VideoItem(
|
VideoItem(
|
||||||
video = video,
|
video = video,
|
||||||
|
@ -206,8 +204,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
||||||
SearchResult(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubePlaylistListSaver,
|
stateSaver = InnertubePlaylistItemListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
|
fromMusicShelfRendererContent = Innertube.PlaylistItem.Companion::from,
|
||||||
itemContent = { playlist ->
|
itemContent = { playlist ->
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
playlist = playlist,
|
playlist = playlist,
|
||||||
|
|
|
@ -41,7 +41,7 @@ import it.vfsfitvnm.vimusic.utils.color
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SmallSongItemShimmer(
|
fun SmallSongItemShimmer(
|
||||||
|
@ -73,7 +73,7 @@ fun SmallSongItemShimmer(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun SmallSongItem(
|
fun SmallSongItem(
|
||||||
song: YouTube.Item.Song,
|
song: Innertube.SongItem,
|
||||||
thumbnailSizePx: Int,
|
thumbnailSizePx: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
@ -95,7 +95,7 @@ fun SmallSongItem(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoItem(
|
fun VideoItem(
|
||||||
video: YouTube.Item.Video,
|
video: Innertube.VideoItem,
|
||||||
thumbnailHeightDp: Dp,
|
thumbnailHeightDp: Dp,
|
||||||
thumbnailWidthDp: Dp,
|
thumbnailWidthDp: Dp,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
@ -212,7 +212,7 @@ fun VideoItemShimmer(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistItem(
|
fun PlaylistItem(
|
||||||
playlist: YouTube.Item.Playlist,
|
playlist: Innertube.PlaylistItem,
|
||||||
thumbnailSizePx: Int,
|
thumbnailSizePx: Int,
|
||||||
thumbnailSizeDp: Dp,
|
thumbnailSizeDp: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -298,7 +298,7 @@ fun PlaylistItemShimmer(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumItem(
|
fun AlbumItem(
|
||||||
album: YouTube.Item.Album,
|
album: Innertube.AlbumItem,
|
||||||
thumbnailSizePx: Int,
|
thumbnailSizePx: Int,
|
||||||
thumbnailSizeDp: Dp,
|
thumbnailSizeDp: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -383,7 +383,7 @@ fun AlbumItemShimmer(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlternativeAlbumItem(
|
fun AlternativeAlbumItem(
|
||||||
album: YouTube.Item.Album,
|
album: Innertube.AlbumItem,
|
||||||
thumbnailSizePx: Int,
|
thumbnailSizePx: Int,
|
||||||
thumbnailSizeDp: Dp,
|
thumbnailSizeDp: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -452,7 +452,7 @@ fun AlternativeAlbumItemPlaceholder(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistItem(
|
fun ArtistItem(
|
||||||
artist: YouTube.Item.Artist,
|
artist: Innertube.ArtistItem,
|
||||||
thumbnailSizePx: Int,
|
thumbnailSizePx: Int,
|
||||||
thumbnailSizeDp: Dp,
|
thumbnailSizeDp: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
@ -6,9 +6,9 @@ import androidx.core.os.bundleOf
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
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()
|
get() = MediaItem.Builder()
|
||||||
.setMediaId(key)
|
.setMediaId(key)
|
||||||
.setUri(key)
|
.setUri(key)
|
||||||
|
@ -32,7 +32,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val YouTube.Item.Video.asMediaItem: MediaItem
|
val Innertube.VideoItem.asMediaItem: MediaItem
|
||||||
get() = MediaItem.Builder()
|
get() = MediaItem.Builder()
|
||||||
.setMediaId(key)
|
.setMediaId(key)
|
||||||
.setUri(key)
|
.setUri(key)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package it.vfsfitvnm.vimusic.utils
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ -17,20 +20,30 @@ data class YouTubeRadio(
|
||||||
var mediaItems: List<MediaItem>? = null
|
var mediaItems: List<MediaItem>? = null
|
||||||
|
|
||||||
nextContinuation = withContext(Dispatchers.IO) {
|
nextContinuation = withContext(Dispatchers.IO) {
|
||||||
YouTube.next(
|
val continuation = nextContinuation
|
||||||
videoId = videoId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
params = parameters,
|
|
||||||
playlistSetVideoId = playlistSetVideoId,
|
|
||||||
continuation = nextContinuation
|
|
||||||
)?.getOrNull()?.let { nextResult ->
|
|
||||||
playlistId = nextResult.playlistId
|
|
||||||
parameters = nextResult.params
|
|
||||||
playlistSetVideoId = nextResult.playlistSetVideoId
|
|
||||||
|
|
||||||
mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem)
|
if (continuation == null) {
|
||||||
nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation }
|
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()
|
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
|
package it.vfsfitvnm.youtubemusic.models
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BrowseResponse(
|
data class BrowseResponse(
|
||||||
val contents: Contents?,
|
val contents: Contents?,
|
||||||
|
@ -23,10 +21,10 @@ data class BrowseResponse(
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicDetailHeaderRenderer(
|
data class MusicDetailHeaderRenderer(
|
||||||
val title: Runs,
|
val title: Runs?,
|
||||||
val subtitle: Runs,
|
val subtitle: Runs?,
|
||||||
val secondSubtitle: Runs,
|
val secondSubtitle: Runs?,
|
||||||
val thumbnail: ThumbnailRenderer,
|
val thumbnail: ThumbnailRenderer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -35,16 +33,16 @@ data class BrowseResponse(
|
||||||
val playButton: PlayButton?,
|
val playButton: PlayButton?,
|
||||||
val startRadioButton: StartRadioButton?,
|
val startRadioButton: StartRadioButton?,
|
||||||
val thumbnail: ThumbnailRenderer?,
|
val thumbnail: ThumbnailRenderer?,
|
||||||
val title: Runs
|
val title: Runs?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayButton(
|
data class PlayButton(
|
||||||
val buttonRenderer: ButtonRenderer
|
val buttonRenderer: ButtonRenderer?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StartRadioButton(
|
data class StartRadioButton(
|
||||||
val buttonRenderer: ButtonRenderer
|
val buttonRenderer: ButtonRenderer?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ButtonRenderer(
|
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
|
@Serializable
|
||||||
data class Continuation(
|
data class Continuation(
|
||||||
@JsonNames("nextContinuationData", "nextRadioContinuationData")
|
@JsonNames("nextContinuationData", "nextRadioContinuationData")
|
||||||
val nextContinuationData: Data
|
val nextContinuationData: Data?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
val continuation: String
|
val continuation: String?
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -12,12 +12,7 @@ data class ContinuationResponse(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ContinuationContents(
|
data class ContinuationContents(
|
||||||
@JsonNames("musicPlaylistShelfContinuation")
|
@JsonNames("musicPlaylistShelfContinuation")
|
||||||
val musicShelfContinuation: MusicShelfRenderer?
|
val musicShelfContinuation: MusicShelfRenderer?,
|
||||||
) {
|
val playlistPanelContinuation: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?,
|
||||||
// @Serializable
|
)
|
||||||
// data class MusicShelfContinuation(
|
|
||||||
// val continuations: List<Continuation>?,
|
|
||||||
// val contents: List<MusicShelfRenderer.Content>
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
package it.vfsfitvnm.youtubemusic.models
|
package it.vfsfitvnm.youtubemusic.models
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GetQueueResponse(
|
data class GetQueueResponse(
|
||||||
val queueDatas: List<QueueData>?,
|
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
|
@Serializable
|
||||||
data class MusicCarouselShelfRenderer(
|
data class MusicCarouselShelfRenderer(
|
||||||
val header: Header,
|
val header: Header?,
|
||||||
val contents: List<Content>,
|
val contents: List<Content>,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
|
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
|
||||||
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,
|
|
||||||
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,11 +22,12 @@ data class MusicCarouselShelfRenderer(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicCarouselShelfBasicHeaderRenderer(
|
data class MusicCarouselShelfBasicHeaderRenderer(
|
||||||
val moreContentButton: MoreContentButton?,
|
val moreContentButton: MoreContentButton?,
|
||||||
val title: Runs,
|
val title: Runs?,
|
||||||
|
val strapline: Runs?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MoreContentButton(
|
data class MoreContentButton(
|
||||||
val buttonRenderer: ButtonRenderer
|
val buttonRenderer: ButtonRenderer?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@ data class MusicResponsiveListItemRenderer(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class FlexColumn(
|
data class FlexColumn(
|
||||||
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
|
@JsonNames("musicResponsiveListItemFixedColumnRenderer")
|
||||||
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer
|
val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicResponsiveListItemFlexColumnRenderer(
|
data class MusicResponsiveListItemFlexColumnRenderer(
|
|
@ -5,34 +5,34 @@ import kotlinx.serialization.Serializable
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicShelfRenderer(
|
data class MusicShelfRenderer(
|
||||||
val bottomEndpoint: NavigationEndpoint?,
|
val bottomEndpoint: NavigationEndpoint?,
|
||||||
val contents: List<Content>,
|
val contents: List<Content>?,
|
||||||
val continuations: List<Continuation>?,
|
val continuations: List<Continuation>?,
|
||||||
val title: Runs?
|
val title: Runs?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer,
|
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||||
) {
|
) {
|
||||||
val runs: Pair<List<Runs.Run>, List<List<Runs.Run>>>
|
val runs: Pair<List<Runs.Run>, List<List<Runs.Run>>>
|
||||||
get() = (musicResponsiveListItemRenderer
|
get() = (musicResponsiveListItemRenderer
|
||||||
.flexColumns
|
?.flexColumns
|
||||||
.firstOrNull()
|
?.firstOrNull()
|
||||||
?.musicResponsiveListItemFlexColumnRenderer
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
?.text
|
?.text
|
||||||
?.runs
|
?.runs
|
||||||
?: emptyList()) to
|
?: emptyList()) to
|
||||||
(musicResponsiveListItemRenderer
|
(musicResponsiveListItemRenderer
|
||||||
.flexColumns
|
?.flexColumns
|
||||||
.lastOrNull()
|
?.lastOrNull()
|
||||||
?.musicResponsiveListItemFlexColumnRenderer
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
?.text
|
?.text
|
||||||
?.splitBySeparator()
|
?.splitBySeparator()
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
val thumbnail: Thumbnail?
|
||||||
get() = musicResponsiveListItemRenderer
|
get() = musicResponsiveListItemRenderer
|
||||||
.thumbnail
|
?.thumbnail
|
||||||
?.musicThumbnailRenderer
|
?.musicThumbnailRenderer
|
||||||
?.thumbnail
|
?.thumbnail
|
||||||
?.thumbnails
|
?.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
|
@Serializable
|
||||||
data class WatchEndpointMusicSupportedConfigs(
|
data class WatchEndpointMusicSupportedConfigs(
|
||||||
val watchEndpointMusicConfig: WatchEndpointMusicConfig
|
val watchEndpointMusicConfig: WatchEndpointMusicConfig?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WatchEndpointMusicConfig(
|
data class WatchEndpointMusicConfig(
|
||||||
val musicVideoType: String
|
val musicVideoType: String?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,14 +87,14 @@ data class NavigationEndpoint(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WatchPlaylist(
|
data class WatchPlaylist(
|
||||||
val params: String?,
|
val params: String?,
|
||||||
val playlistId: String,
|
val playlistId: String?,
|
||||||
) : Endpoint()
|
) : Endpoint()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Browse(
|
data class Browse(
|
||||||
val params: String?,
|
val params: String? = null,
|
||||||
val browseId: String?,
|
val browseId: String? = null,
|
||||||
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
|
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null,
|
||||||
) : Endpoint() {
|
) : Endpoint() {
|
||||||
val type: String?
|
val type: String?
|
||||||
get() = browseEndpointContextSupportedConfigs
|
get() = browseEndpointContextSupportedConfigs
|
|
@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonNames
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NextResponse(
|
data class NextResponse(
|
||||||
val contents: Contents,
|
val contents: Contents?
|
||||||
val continuationContents: MusicQueueRenderer.Content?
|
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicQueueRenderer(
|
data class MusicQueueRenderer(
|
||||||
|
@ -17,30 +16,18 @@ data class NextResponse(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
@JsonNames("playlistPanelContinuation")
|
@JsonNames("playlistPanelContinuation")
|
||||||
val playlistPanelRenderer: PlaylistPanelRenderer
|
val playlistPanelRenderer: PlaylistPanelRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlaylistPanelRenderer(
|
data class PlaylistPanelRenderer(
|
||||||
val contents: List<Content>?,
|
val contents: List<Content>?,
|
||||||
val continuations: List<Continuation>?,
|
val continuations: List<Continuation>?,
|
||||||
val playlistId: String?
|
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,
|
val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,
|
||||||
val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?,
|
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
|
@Serializable
|
||||||
data class AutomixPreviewVideoRenderer(
|
data class AutomixPreviewVideoRenderer(
|
||||||
|
@ -52,7 +39,7 @@ data class NextResponse(
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AutomixPlaylistVideoRenderer(
|
data class AutomixPlaylistVideoRenderer(
|
||||||
val navigationEndpoint: NavigationEndpoint
|
val navigationEndpoint: NavigationEndpoint?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,33 +50,33 @@ data class NextResponse(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contents(
|
data class Contents(
|
||||||
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer
|
val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SingleColumnMusicWatchNextResultsRenderer(
|
data class SingleColumnMusicWatchNextResultsRenderer(
|
||||||
val tabbedRenderer: TabbedRenderer
|
val tabbedRenderer: TabbedRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TabbedRenderer(
|
data class TabbedRenderer(
|
||||||
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer
|
val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WatchNextTabbedResultsRenderer(
|
data class WatchNextTabbedResultsRenderer(
|
||||||
val tabs: List<Tab>
|
val tabs: List<Tab>?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Tab(
|
data class Tab(
|
||||||
val tabRenderer: TabRenderer
|
val tabRenderer: TabRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TabRenderer(
|
data class TabRenderer(
|
||||||
val content: Content?,
|
val content: Content?,
|
||||||
val endpoint: NavigationEndpoint?,
|
val endpoint: NavigationEndpoint?,
|
||||||
val title: String
|
val title: String?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val musicQueueRenderer: MusicQueueRenderer
|
val musicQueueRenderer: MusicQueueRenderer?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,13 +4,13 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerResponse(
|
data class PlayerResponse(
|
||||||
val playabilityStatus: PlayabilityStatus,
|
val playabilityStatus: PlayabilityStatus?,
|
||||||
val playerConfig: PlayerConfig?,
|
val playerConfig: PlayerConfig?,
|
||||||
val streamingData: StreamingData?,
|
val streamingData: StreamingData?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayabilityStatus(
|
data class PlayabilityStatus(
|
||||||
val status: String
|
val status: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -26,8 +26,7 @@ data class PlayerResponse(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StreamingData(
|
data class StreamingData(
|
||||||
val adaptiveFormats: List<AdaptiveFormat>,
|
val adaptiveFormats: List<AdaptiveFormat>?
|
||||||
val expiresInSeconds: String
|
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AdaptiveFormat(
|
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 runs: List<Run> = listOf()
|
||||||
) {
|
) {
|
||||||
val text: String
|
val text: String
|
||||||
get() = runs.joinToString("") { it.text }
|
get() = runs.joinToString("") { it.text ?: "" }
|
||||||
|
|
||||||
fun splitBySeparator(): List<List<Run>> {
|
fun splitBySeparator(): List<List<Run>> {
|
||||||
return runs.flatMapIndexed { index, run ->
|
return runs.flatMapIndexed { index, run ->
|
||||||
|
@ -25,7 +25,7 @@ data class Runs(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Run(
|
data class Run(
|
||||||
val text: String,
|
val text: String?,
|
||||||
val navigationEndpoint: NavigationEndpoint?,
|
val navigationEndpoint: NavigationEndpoint?,
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -3,13 +3,12 @@ package it.vfsfitvnm.youtubemusic.models
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SearchResponse(
|
data class SearchResponse(
|
||||||
val contents: Contents,
|
val contents: Contents?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contents(
|
data class Contents(
|
||||||
val tabbedSearchResultsRenderer: Tabs
|
val tabbedSearchResultsRenderer: Tabs?
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -3,24 +3,24 @@ package it.vfsfitvnm.youtubemusic.models
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GetSearchSuggestionsResponse(
|
data class SearchSuggestionsResponse(
|
||||||
val contents: List<Content>?
|
val contents: List<Content>?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer
|
val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SearchSuggestionsSectionRenderer(
|
data class SearchSuggestionsSectionRenderer(
|
||||||
val contents: List<Content>
|
val contents: List<Content>?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
val searchSuggestionRenderer: SearchSuggestionRenderer
|
val searchSuggestionRenderer: SearchSuggestionRenderer?
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SearchSuggestionRenderer(
|
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