Rename youtube-music module to innertube and rewrite it

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

View file

@ -65,6 +65,10 @@ android {
freeCompilerArgs += "-Xcontext-receivers" 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)

View file

@ -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)
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope import 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope import 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,

View file

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

View file

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

View file

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

View file

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

View file

@ -4,18 +4,16 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope import 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)

View file

@ -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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -92,8 +92,10 @@ import it.vfsfitvnm.vimusic.utils.shouldBePlaying
import it.vfsfitvnm.vimusic.utils.skipSilenceKey import it.vfsfitvnm.vimusic.utils.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 ->

View file

@ -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(

View file

@ -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
) )

View file

@ -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 = {

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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)

View file

@ -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 ->

View file

@ -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)
}
} }
} }
} }

View file

@ -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)

View file

@ -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))
} }
} }

View file

@ -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
) )

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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()

View file

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

View file

@ -1,9 +1,7 @@
package it.vfsfitvnm.youtubemusic.models 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?
) )
} }
} }

View file

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

View file

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

View file

@ -8,10 +8,10 @@ import kotlinx.serialization.json.JsonNames
@Serializable @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?
) )
} }

View file

@ -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>
// )
}
} }

View file

@ -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>?,

View file

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

View file

@ -4,13 +4,12 @@ import kotlinx.serialization.Serializable
@Serializable @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?
) )
} }
} }

View file

@ -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(

View file

@ -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

View file

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

View file

@ -74,12 +74,12 @@ data class NavigationEndpoint(
@Serializable @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

View file

@ -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?
) )
} }
} }

View file

@ -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(

View file

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

View file

@ -7,7 +7,7 @@ data class Runs(
val runs: List<Run> = listOf() val 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?,
) )
} }

View file

@ -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?
) )
} }

View file

@ -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?,
) )
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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