From 917e194d634c5f789734879d10f03969ecc9ac8b Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sun, 2 Oct 2022 15:25:07 +0200 Subject: [PATCH] Rename youtube-music module to innertube and rewrite it --- app/build.gradle.kts | 6 +- .../it/vfsfitvnm/vimusic/MainActivity.kt | 11 +- .../vimusic/savers/AlbumListSaver.kt | 3 - .../vimusic/savers/AlbumResultSaver.kt | 3 - .../it/vfsfitvnm/vimusic/savers/AlbumSaver.kt | 4 + .../vimusic/savers/ArtistListSaver.kt | 3 - .../vfsfitvnm/vimusic/savers/ArtistSaver.kt | 2 + .../vimusic/savers/DetailedSongListSaver.kt | 3 - .../vimusic/savers/DetailedSongSaver.kt | 2 + .../vfsfitvnm/vimusic/savers/InfoListSaver.kt | 3 - .../it/vfsfitvnm/vimusic/savers/InfoSaver.kt | 2 + .../vimusic/savers/InnertubeAlbumItemSaver.kt | 24 + .../savers/InnertubeArtistItemSaver.kt | 21 + .../savers/InnertubeArtistPageSaver.kt | 36 + ...ver.kt => InnertubeBrowseEndpointSaver.kt} | 2 +- .../savers/InnertubeBrowseInfoSaver.kt | 20 + .../vimusic/savers/InnertubeItemsPageSaver.kt | 31 + .../savers/InnertubePlaylistItemSaver.kt | 23 + .../InnertubePlaylistOrAlbumPageSaver.kt | 26 + .../savers/InnertubeRelatedPageSaver.kt | 22 + .../vimusic/savers/InnertubeSongItemSaver.kt | 26 + .../vimusic/savers/InnertubeThumbnailSaver.kt | 19 + .../vimusic/savers/InnertubeVideoItemSaver.kt | 26 + ...aver.kt => InnertubeWatchEndpointSaver.kt} | 2 +- .../vimusic/savers/InnertubeWatchInfoSaver.kt | 18 + .../it/vfsfitvnm/vimusic/savers/ListSaver.kt | 23 - .../vfsfitvnm/vimusic/savers/NullableSaver.kt | 13 - .../savers/PlaylistPreviewListSaver.kt | 3 - .../vimusic/savers/PlaylistPreviewSaver.kt | 24 +- .../vimusic/savers/PlaylistWithSongsSaver.kt | 12 +- .../vfsfitvnm/vimusic/savers/ResultSaver.kt | 14 - .../it/vfsfitvnm/vimusic/savers/Savers.kt | 37 + .../vimusic/savers/SearchQueryListSaver.kt | 3 - .../vimusic/savers/SearchQuerySaver.kt | 2 + .../vimusic/savers/StringListResultSaver.kt | 5 - .../vimusic/savers/StringResultSaver.kt | 5 - .../vimusic/savers/YouTubeAlbumListSaver.kt | 3 - .../vimusic/savers/YouTubeAlbumSaver.kt | 22 - .../vimusic/savers/YouTubeArtistListSaver.kt | 3 - .../vimusic/savers/YouTubeArtistPageSaver.kt | 36 - .../vimusic/savers/YouTubeArtistSaver.kt | 20 - .../savers/YouTubeBrowseInfoListSaver.kt | 3 - .../vimusic/savers/YouTubeBrowseInfoSaver.kt | 18 - .../savers/YouTubePlaylistListSaver.kt | 3 - .../savers/YouTubePlaylistOrAlbumSaver.kt | 27 - .../vimusic/savers/YouTubePlaylistSaver.kt | 21 - .../vimusic/savers/YouTubeRelatedSaver.kt | 22 - .../vimusic/savers/YouTubeSongListSaver.kt | 3 - .../vimusic/savers/YouTubeSongSaver.kt | 24 - .../vimusic/savers/YouTubeThumbnailSaver.kt | 19 - .../vimusic/savers/YouTubeVideoListSaver.kt | 3 - .../vimusic/savers/YouTubeVideoSaver.kt | 24 - .../vimusic/savers/YouTubeWatchInfoSaver.kt | 18 - .../vimusic/service/PlayerService.kt | 8 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 23 +- .../ui/screens/artist/ArtistContent.kt | 23 +- .../ui/screens/artist/ArtistOverview.kt | 26 +- .../vimusic/ui/screens/artist/ArtistScreen.kt | 109 +- .../vimusic/ui/screens/home/QuickPicks.kt | 41 +- .../localplaylist/LocalPlaylistSongList.kt | 16 +- .../vimusic/ui/screens/player/Lyrics.kt | 7 +- .../ui/screens/player/StatsForNerds.kt | 6 +- .../ui/screens/playlist/PlaylistScreen.kt | 4 +- .../ui/screens/playlist/PlaylistSongList.kt | 34 +- .../vimusic/ui/screens/search/OnlineSearch.kt | 11 +- .../ui/screens/searchresult/SearchResult.kt | 37 +- .../searchresult/SearchResultScreen.kt | 47 +- .../vimusic/ui/views/YouTubeItems.kt | 14 +- .../it/vfsfitvnm/vimusic/utils/Utils.kt | 6 +- .../vfsfitvnm/vimusic/utils/YoutubeRadio.kt | 39 +- {youtube-music => innertube}/.gitignore | 0 {youtube-music => innertube}/build.gradle.kts | 0 .../it/vfsfitvnm/youtubemusic/Innertube.kt | 204 +++ .../youtubemusic/models/BrowseResponse.kt | 16 +- .../youtubemusic/models/ButtonRenderer.kt | 2 +- .../vfsfitvnm/youtubemusic/models/Context.kt | 48 + .../youtubemusic/models/Continuation.kt | 4 +- .../models/ContinuationResponse.kt | 11 +- .../youtubemusic/models/GetQueueResponse.kt | 2 - .../youtubemusic/models/GridRenderer.kt | 13 + .../models/MusicCarouselShelfRenderer.kt | 8 +- .../models/MusicResponsiveListItemRenderer.kt | 2 +- .../youtubemusic/models/MusicShelfRenderer.kt | 16 +- .../models/MusicTwoRowItemRenderer.kt | 11 + .../youtubemusic/models/NavigationEndpoint.kt | 12 +- .../youtubemusic/models/NextResponse.kt | 33 +- .../youtubemusic/models/PlayerResponse.kt | 7 +- .../models/PlaylistPanelVideoRenderer.kt | 13 + .../it/vfsfitvnm/youtubemusic/models/Runs.kt | 4 +- .../youtubemusic/models/SearchResponse.kt | 5 +- .../models/SearchSuggestionsResponse.kt | 10 +- .../models/SectionListRenderer.kt | 29 + .../it/vfsfitvnm/youtubemusic/models/Tabs.kt | 25 + .../youtubemusic/models/Thumbnail.kt | 21 + .../youtubemusic/models/ThumbnailRenderer.kt | 22 + .../youtubemusic/models/bodies/BrowseBody.kt | 11 + .../models/bodies/ContinuationBody.kt | 10 + .../youtubemusic/models/bodies/NextBody.kt | 24 + .../youtubemusic/models/bodies/PlayerBody.kt | 11 + .../youtubemusic/models/bodies/QueueBody.kt | 11 + .../youtubemusic/models/bodies/SearchBody.kt | 11 + .../models/bodies/SearchSuggestionsBody.kt | 10 + .../youtubemusic/requests/AlbumPage.kt | 36 + .../youtubemusic/requests/ArtistPage.kt | 102 ++ .../youtubemusic/requests/ItemsPage.kt | 97 ++ .../vfsfitvnm/youtubemusic/requests/Lyrics.kt | 44 + .../youtubemusic/requests/NextPage.kt | 90 ++ .../vfsfitvnm/youtubemusic/requests/Player.kt | 59 + .../youtubemusic/requests/PlaylistPage.kt | 90 ++ .../vfsfitvnm/youtubemusic/requests/Queue.kt | 29 + .../youtubemusic/requests/RelatedPage.kt | 71 + .../youtubemusic/requests/SearchPage.kt | 62 + .../requests/SearchSuggestions.kt | 29 + .../FromMusicResponsiveListItemRenderer.kt | 49 + .../utils/FromMusicShelfRendererContent.kt | 140 ++ .../utils/FromMusicTwoRowItemRenderer.kt | 76 + .../utils/FromPlaylistPanelVideoRenderer.kt | 35 + .../it/vfsfitvnm/youtubemusic/utils/Utils.kt | 44 + .../src/test/kotlin/Test.kt | 0 settings.gradle.kts | 2 +- .../it/vfsfitvnm/youtubemusic/Result.kt | 10 - .../it/vfsfitvnm/youtubemusic/YouTube.kt | 1350 ----------------- .../models/MusicNavigationButtonRenderer.kt | 21 - .../models/MusicTwoRowItemRenderer.kt | 13 - .../it/vfsfitvnm/youtubemusic/models/Tabs.kt | 61 - .../youtubemusic/models/ThumbnailRenderer.kt | 40 - 126 files changed, 2210 insertions(+), 2145 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeAlbumItemSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistItemSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistPageSaver.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/{YouTubeBrowseEndpointSaver.kt => InnertubeBrowseEndpointSaver.kt} (85%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseInfoSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistItemSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistOrAlbumPageSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeRelatedPageSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeSongItemSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeThumbnailSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeVideoItemSaver.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/{YouTubeWatchEndpointSaver.kt => InnertubeWatchEndpointSaver.kt} (89%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchInfoSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistPageSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt rename {youtube-music => innertube}/.gitignore (100%) rename {youtube-music => innertube}/build.gradle.kts (100%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt (77%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt (72%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Context.kt rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt (84%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt (62%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt (75%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GridRenderer.kt rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt (83%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt (95%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt (78%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt (96%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt (72%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt (86%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlaylistPanelVideoRenderer.kt rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt (90%) rename {youtube-music => innertube}/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt (66%) rename youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetSearchSuggestionsResponse.kt => innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchSuggestionsResponse.kt (78%) create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SectionListRenderer.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Thumbnail.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/BrowseBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/ContinuationBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/NextBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/PlayerBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/QueueBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchSuggestionsBody.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/AlbumPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ArtistPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ItemsPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Lyrics.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/NextPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Player.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/PlaylistPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Queue.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/RelatedPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchPage.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchSuggestions.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicResponsiveListItemRenderer.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicShelfRendererContent.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicTwoRowItemRenderer.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromPlaylistPanelVideoRenderer.kt create mode 100644 innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt rename {youtube-music => innertube}/src/test/kotlin/Test.kt (100%) delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Result.kt delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicNavigationButtonRenderer.kt delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt delete mode 100644 youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5687011..ee3a552 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,6 +65,10 @@ android { freeCompilerArgs += "-Xcontext-receivers" jvmTarget = "1.8" } + + packagingOptions { + resources.excludes.add("META-INF/INDEX.LIST") + } } kapt { @@ -93,7 +97,7 @@ dependencies { kapt(libs.room.compiler) annotationProcessor(libs.room.compiler) - implementation(projects.youtubeMusic) + implementation(projects.innertube) implementation(projects.kugou) coreLibraryDesugaring(libs.desugaring) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 745003d..32b0563 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -81,7 +81,10 @@ import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.listener import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.requests.playlistPage +import it.vfsfitvnm.youtubemusic.requests.song import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -376,8 +379,8 @@ class MainActivity : ComponentActivity() { val browseId = "VL$playlistId" if (playlistId.startsWith("OLAK5uy_")) { - YouTube.playlist(browseId)?.getOrNull()?.let { playlist -> - playlist.songs?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId -> + Innertube.playlistPage(BrowseBody(browseId = browseId))?.getOrNull()?.let { playlist -> + playlist.songsPage?.items?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId -> albumRoute.ensureGlobal(browseId) } } @@ -385,7 +388,7 @@ class MainActivity : ComponentActivity() { playlistRoute.ensureGlobal(browseId) } } ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId -> - YouTube.song(videoId)?.getOrNull()?.let { song -> + Innertube.song(videoId)?.getOrNull()?.let { song -> withContext(Dispatchers.Main) { binder?.player?.forcePlay(song.asMediaItem) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt deleted file mode 100644 index b0c86d0..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val AlbumListSaver = ListSaver.of(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt deleted file mode 100644 index b3cf4b3..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val AlbumResultSaver = resultSaver(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt index 2f88b36..7825d4b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt @@ -27,3 +27,7 @@ object AlbumSaver : Saver> { bookmarkedAt = value[7] as Long?, ) } + +val AlbumResultSaver = resultSaver(AlbumSaver) + +val AlbumListSaver = listSaver(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt deleted file mode 100644 index 125d725..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val ArtistListSaver = ListSaver.of(ArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt index 8247c8e..4112741 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt @@ -23,3 +23,5 @@ object ArtistSaver : Saver> { bookmarkedAt = value[5] as Long?, ) } + +val ArtistListSaver = listSaver(ArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt deleted file mode 100644 index cdca8e8..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val DetailedSongListSaver = ListSaver.of(DetailedSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt index 1f50148..e6dd4a9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -29,3 +29,5 @@ object DetailedSongSaver : Saver> { artists = (value[7] as List>?)?.let(InfoListSaver::restore) ) } + +val DetailedSongListSaver = listSaver(DetailedSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt deleted file mode 100644 index 8c3347f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val InfoListSaver = ListSaver.of(InfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt index 9eae23b..d8422ea 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt @@ -11,3 +11,5 @@ object InfoSaver : Saver> { return if (value.size == 2) Info(id = value[0], name = value[1]) else null } } + +val InfoListSaver = listSaver(InfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeAlbumItemSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeAlbumItemSaver.kt new file mode 100644 index 0000000..78e1854 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeAlbumItemSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.AlbumItem): List = 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) = Innertube.AlbumItem( + info = (value[0] as List?)?.let(InnertubeBrowseInfoSaver::restore), + authors = (value[1] as List>?)?.let(InnertubeBrowseInfoListSaver::restore), + year = value[2] as String?, + thumbnail = (value[3] as List?)?.let(InnertubeThumbnailSaver::restore) + ) +} + +val InnertubeAlbumItemListSaver = listSaver(InnertubeAlbumItemSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistItemSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistItemSaver.kt new file mode 100644 index 0000000..fd94ebe --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistItemSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.ArtistItem): List = listOf( + value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } }, + value.subscribersCountText, + value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } + ) + + override fun restore(value: List) = Innertube.ArtistItem( + info = (value[0] as List?)?.let(InnertubeBrowseInfoSaver::restore), + subscribersCountText = value[1] as String?, + thumbnail = (value[2] as List?)?.let(InnertubeThumbnailSaver::restore) + ) +} + +val InnertubeArtistItemListSaver = listSaver(InnertubeArtistItemSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistPageSaver.kt new file mode 100644 index 0000000..5a86315 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeArtistPageSaver.kt @@ -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> { + 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) = Innertube.ArtistPage( + name = value[0] as String?, + description = value[1] as String?, + thumbnail = (value[2] as List?)?.let(InnertubeThumbnailSaver::restore), + shuffleEndpoint = (value[3] as List?)?.let(InnertubeWatchEndpointSaver::restore), + radioEndpoint = (value[4] as List?)?.let(InnertubeWatchEndpointSaver::restore), + songs = (value[5] as List>?)?.let(InnertubeSongItemListSaver::restore), + songsEndpoint = (value[6] as List?)?.let(InnertubeBrowseEndpointSaver::restore), + albums = (value[7] as List>?)?.let(InnertubeAlbumItemListSaver::restore), + albumsEndpoint = (value[8] as List?)?.let(InnertubeBrowseEndpointSaver::restore), + singles = (value[9] as List>?)?.let(InnertubeAlbumItemListSaver::restore), + singlesEndpoint = (value[10] as List?)?.let(InnertubeBrowseEndpointSaver::restore), + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseEndpointSaver.kt similarity index 85% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseEndpointSaver.kt index 30aa186..2ccd732 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseEndpointSaver.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint -object YouTubeBrowseEndpointSaver : Saver> { +object InnertubeBrowseEndpointSaver : Saver> { override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf( value.browseId, value.params diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseInfoSaver.kt new file mode 100644 index 0000000..9a0e876 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeBrowseInfoSaver.kt @@ -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, List> { + override fun SaverScope.save(value: Innertube.Info) = listOf( + value.name, + value.endpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } } + ) + + override fun restore(value: List) = Innertube.Info( + name = value[0] as String?, + endpoint = (value[1] as List?)?.let(InnertubeBrowseEndpointSaver::restore) + ) +} + +val InnertubeBrowseInfoListSaver = listSaver(InnertubeBrowseInfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt new file mode 100644 index 0000000..8378710 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt @@ -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, List> { + override fun SaverScope.save(value: Innertube.ItemsPage) = listOf( + value.items?.let {with(InnertubeSongItemListSaver) { save(it) } }, + value.continuation + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = Innertube.ItemsPage( + items = (value[0] as List>?)?.let(InnertubeSongItemListSaver::restore), + continuation = value[1] as String? + ) +} + +object InnertubeAlbumsPageSaver : Saver, List> { + override fun SaverScope.save(value: Innertube.ItemsPage) = listOf( + value.items?.let {with(InnertubeAlbumItemListSaver) { save(it) } }, + value.continuation + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = Innertube.ItemsPage( + items = (value[0] as List>?)?.let(InnertubeAlbumItemListSaver::restore), + continuation = value[1] as String? + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistItemSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistItemSaver.kt new file mode 100644 index 0000000..3f82dfc --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistItemSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.PlaylistItem): List = 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) = Innertube.PlaylistItem( + info = (value[0] as List?)?.let(InnertubeBrowseInfoSaver::restore), + channel = (value[1] as List?)?.let(InnertubeBrowseInfoSaver::restore), + songCount = value[2] as Int?, + thumbnail = (value[3] as List?)?.let(InnertubeThumbnailSaver::restore) + ) +} + +val InnertubePlaylistItemListSaver = listSaver(InnertubePlaylistItemSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistOrAlbumPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistOrAlbumPageSaver.kt new file mode 100644 index 0000000..3abd5cd --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubePlaylistOrAlbumPageSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.PlaylistOrAlbumPage): List = 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) = Innertube.PlaylistOrAlbumPage( + title = value[0] as String?, + authors = (value[1] as List>?)?.let(InnertubeBrowseInfoListSaver::restore), + year = value[2] as String?, + thumbnail = (value[3] as List?)?.let(InnertubeThumbnailSaver::restore), + url = value[4] as String?, + songsPage = (value[5] as List?)?.let(InnertubeSongsPageSaver::restore), + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeRelatedPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeRelatedPageSaver.kt new file mode 100644 index 0000000..46f9f16 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeRelatedPageSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.RelatedPage): List = 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) = Innertube.RelatedPage( + songs = (value[0] as List>?)?.let(InnertubeSongItemListSaver::restore), + playlists = (value[1] as List>?)?.let(InnertubePlaylistItemListSaver::restore), + albums = (value[2] as List>?)?.let(InnertubeAlbumItemListSaver::restore), + artists = (value[3] as List>?)?.let(InnertubeArtistItemListSaver::restore), + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeSongItemSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeSongItemSaver.kt new file mode 100644 index 0000000..0a696b9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeSongItemSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.SongItem): List = 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) = Innertube.SongItem( + info = (value[0] as List?)?.let(InnertubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(InnertubeBrowseInfoListSaver::restore), + album = (value[2] as List?)?.let(InnertubeBrowseInfoSaver::restore), + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(InnertubeThumbnailSaver::restore) + ) +} + +val InnertubeSongItemListSaver = listSaver(InnertubeSongItemSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeThumbnailSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeThumbnailSaver.kt new file mode 100644 index 0000000..2aa3b64 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeThumbnailSaver.kt @@ -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> { + override fun SaverScope.save(value: Thumbnail) = listOf( + value.url, + value.width, + value.height + ) + + override fun restore(value: List) = Thumbnail( + url = value[0] as String, + width = value[1] as Int, + height = value[2] as Int?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeVideoItemSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeVideoItemSaver.kt new file mode 100644 index 0000000..2c8f245 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeVideoItemSaver.kt @@ -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> { + override fun SaverScope.save(value: Innertube.VideoItem): List = 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) = Innertube.VideoItem( + info = (value[0] as List?)?.let(InnertubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(InnertubeBrowseInfoListSaver::restore), + viewsText = value[2] as String?, + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(InnertubeThumbnailSaver::restore) + ) +} + +val InnertubeVideoItemListSaver = listSaver(InnertubeVideoItemSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchEndpointSaver.kt similarity index 89% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchEndpointSaver.kt index 5548bcf..69feee1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchEndpointSaver.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint -object YouTubeWatchEndpointSaver : Saver> { +object InnertubeWatchEndpointSaver : Saver> { override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf( value.params, value.playlistId, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchInfoSaver.kt new file mode 100644 index 0000000..1090e0b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeWatchInfoSaver.kt @@ -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, List> { + override fun SaverScope.save(value: Innertube.Info) = listOf( + value.name, + value.endpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } }, + ) + + override fun restore(value: List) = Innertube.Info( + name = value[0] as String?, + endpoint = (value[1] as List?)?.let(InnertubeWatchEndpointSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt deleted file mode 100644 index 62e8621..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt +++ /dev/null @@ -1,23 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.SaverScope - -interface ListSaver : Saver, List> { - override fun SaverScope.save(value: List): List - override fun restore(value: List): List - - companion object { - fun of(saver: Saver): ListSaver { - return object : ListSaver { - override fun restore(value: List): List { - return value.mapNotNull(saver::restore) - } - - override fun SaverScope.save(value: List): List { - return with(saver) { value.mapNotNull { save(it) } } - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt deleted file mode 100644 index 4f56b9b..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt +++ /dev/null @@ -1,13 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.SaverScope - -fun nullableSaver(saver: Saver) = - object : Saver { - override fun SaverScope.save(value: Original?): Saveable? = - value?.let { with(saver) { save(it) } } - - override fun restore(value: Saveable): Original? = - saver.restore(value) - } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt deleted file mode 100644 index 6d726e3..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt index 5641d4f..56d3fe1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt @@ -4,18 +4,16 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import it.vfsfitvnm.vimusic.models.PlaylistPreview -object PlaylistPreviewSaver : Saver> { - override fun SaverScope.save(value: PlaylistPreview): List { - return listOf( - with(PlaylistSaver) { save(value.playlist) }, - value.songCount, - ) - } +object PlaylistPreviewSaver : Saver> { + override fun SaverScope.save(value: PlaylistPreview) = listOf( + with(PlaylistSaver) { save(value.playlist) }, + value.songCount, + ) - override fun restore(value: List): PlaylistPreview? { - return if (value.size == 2) PlaylistPreview( - playlist = PlaylistSaver.restore(value[0] as List), - songCount = value[1] as Int, - ) else null - } + override fun restore(value: List) = PlaylistPreview( + playlist = PlaylistSaver.restore(value[0] as List), + songCount = value[1] as Int, + ) } + +val PlaylistPreviewListSaver = listSaver(PlaylistPreviewSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt index c6b4a9d..fe73abd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt @@ -4,13 +4,11 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import it.vfsfitvnm.vimusic.models.PlaylistWithSongs -object PlaylistWithSongsSaver : Saver> { - override fun SaverScope.save(value: PlaylistWithSongs?) = value?.let { - listOf( - with(PlaylistSaver) { save(value.playlist) }, - with(DetailedSongListSaver) { save(value.songs) }, - ) - } +object PlaylistWithSongsSaver : Saver> { + override fun SaverScope.save(value: PlaylistWithSongs) = listOf( + with(PlaylistSaver) { save(value.playlist) }, + with(DetailedSongListSaver) { save(value.songs) }, + ) @Suppress("UNCHECKED_CAST") override fun restore(value: List): PlaylistWithSongs = PlaylistWithSongs( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt deleted file mode 100644 index 827f7eb..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt +++ /dev/null @@ -1,14 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.SaverScope - -fun resultSaver(saver: Saver) = - object : Saver?, Pair> { - override fun restore(value: Pair) = - value.first?.let(saver::restore)?.let(Result.Companion::success) - ?: value.second?.let(Result.Companion::failure) - - override fun SaverScope.save(value: Result?) = - with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() - } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt new file mode 100644 index 0000000..09e545f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt @@ -0,0 +1,37 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +interface ListSaver : Saver, List> { + override fun SaverScope.save(value: List): List + override fun restore(value: List): List +} + +fun resultSaver(saver: Saver) = + object : Saver?, Pair> { + override fun restore(value: Pair) = + value.first?.let(saver::restore)?.let(Result.Companion::success) + ?: value.second?.let(Result.Companion::failure) + + override fun SaverScope.save(value: Result?) = + with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() + } + +fun listSaver(saver: Saver) = + object : ListSaver { + override fun restore(value: List) = + value.mapNotNull(saver::restore) + + override fun SaverScope.save(value: List) = + with(saver) { value.mapNotNull { save(it) } } + } + +fun nullableSaver(saver: Saver) = + object : Saver { + override fun SaverScope.save(value: Original?): Saveable? = + value?.let { with(saver) { save(it) } } + + override fun restore(value: Saveable): Original? = + saver.restore(value) + } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt deleted file mode 100644 index 9e580f5..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val SearchQueryListSaver = ListSaver.of(SearchQuerySaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt index 57df500..f5a46cf 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt @@ -15,3 +15,5 @@ object SearchQuerySaver : Saver> { query = value[1] as String ) } + +val SearchQueryListSaver = listSaver(SearchQuerySaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt deleted file mode 100644 index 37c0c69..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt +++ /dev/null @@ -1,5 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.autoSaver - -val StringListResultSaver = resultSaver(autoSaver?>()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt deleted file mode 100644 index 1db4d43..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt +++ /dev/null @@ -1,5 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.autoSaver - -val StringResultSaver = resultSaver(autoSaver()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt deleted file mode 100644 index b7c6f0a..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt deleted file mode 100644 index 749db84..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Item.Album): List = 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) = YouTube.Item.Album( - info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), - authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), - year = value[2] as String?, - thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt deleted file mode 100644 index 07d70fc..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistPageSaver.kt deleted file mode 100644 index ad9c8cf..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistPageSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Artist): List = 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) = YouTube.Artist( - name = value[0] as String?, - description = value[1] as String?, - thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore), - shuffleEndpoint = (value[3] as List?)?.let(YouTubeWatchEndpointSaver::restore), - radioEndpoint = (value[4] as List?)?.let(YouTubeWatchEndpointSaver::restore), - songs = (value[5] as List>?)?.let(YouTubeSongListSaver::restore), - songsEndpoint = (value[6] as List?)?.let(YouTubeBrowseEndpointSaver::restore), - albums = (value[7] as List>?)?.let(YouTubeAlbumListSaver::restore), - albumsEndpoint = (value[8] as List?)?.let(YouTubeBrowseEndpointSaver::restore), - singles = (value[9] as List>?)?.let(YouTubeAlbumListSaver::restore), - singlesEndpoint = (value[10] as List?)?.let(YouTubeBrowseEndpointSaver::restore), - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt deleted file mode 100644 index 2ebc020..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Item.Artist): List = listOf( - value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, - value.subscribersCountText, - value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } - ) - - override fun restore(value: List) = YouTube.Item.Artist( - info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), - subscribersCountText = value[1] as String?, - thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt deleted file mode 100644 index 6d700e9..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt deleted file mode 100644 index 0e2bb9d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt +++ /dev/null @@ -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, List> { - override fun SaverScope.save(value: YouTube.Info) = listOf( - value.name, - value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } } - ) - - override fun restore(value: List) = YouTube.Info( - name = value[0] as String?, - endpoint = (value[1] as List?)?.let(YouTubeBrowseEndpointSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt deleted file mode 100644 index 7fe7d81..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt deleted file mode 100644 index 2601e8b..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.PlaylistOrAlbum): List = 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) = YouTube.PlaylistOrAlbum( - title = value[0] as String?, - authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), - year = value[2] as String?, - thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore), - songs = (value[4] as List>?)?.let(YouTubeSongListSaver::restore), - url = value[5] as String?, - continuation = null - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt deleted file mode 100644 index 599e6f2..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt +++ /dev/null @@ -1,21 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.SaverScope -import it.vfsfitvnm.youtubemusic.YouTube - -object YouTubePlaylistSaver : Saver> { - override fun SaverScope.save(value: YouTube.Item.Playlist): List = 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) = YouTube.Item.Playlist( - info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), - channel = (value[1] as List?)?.let(YouTubeBrowseInfoSaver::restore), - songCount = value[2] as Int?, - thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt deleted file mode 100644 index 6024b90..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Related): List = 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) = YouTube.Related( - songs = (value[0] as List>?)?.let(YouTubeSongListSaver::restore), - playlists = (value[1] as List>?)?.let(YouTubePlaylistListSaver::restore), - albums = (value[2] as List>?)?.let(YouTubeAlbumListSaver::restore), - artists = (value[3] as List>?)?.let(YouTubeArtistListSaver::restore), - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt deleted file mode 100644 index 8dade3e..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt deleted file mode 100644 index 848efc0..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Item.Song): List = 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) = YouTube.Item.Song( - info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), - authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), - album = (value[2] as List?)?.let(YouTubeBrowseInfoSaver::restore), - durationText = value[3] as String?, - thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt deleted file mode 100644 index d5664a4..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf( - value.url, - value.width, - value.height - ) - - override fun restore(value: List) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail( - url = value[0] as String, - width = value[1] as Int, - height = value[2] as Int?, - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt deleted file mode 100644 index 2e05f70..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt +++ /dev/null @@ -1,3 +0,0 @@ -package it.vfsfitvnm.vimusic.savers - -val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt deleted file mode 100644 index 1150385..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt +++ /dev/null @@ -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> { - override fun SaverScope.save(value: YouTube.Item.Video): List = 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) = YouTube.Item.Video( - info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), - authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), - viewsText = value[2] as String?, - durationText = value[3] as String?, - thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt deleted file mode 100644 index 11c09f2..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt +++ /dev/null @@ -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, List> { - override fun SaverScope.save(value: YouTube.Info) = listOf( - value.name, - value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } }, - ) - - override fun restore(value: List) = YouTube.Info( - name = value[0] as String?, - endpoint = (value[1] as List?)?.let(YouTubeWatchEndpointSaver::restore) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt index b2d1996..3a444fc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt @@ -92,8 +92,10 @@ import it.vfsfitvnm.vimusic.utils.shouldBePlaying import it.vfsfitvnm.vimusic.utils.skipSilenceKey import it.vfsfitvnm.vimusic.utils.timer import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody +import it.vfsfitvnm.youtubemusic.requests.player import kotlin.math.roundToInt import kotlin.system.exitProcess import kotlinx.coroutines.CoroutineScope @@ -642,9 +644,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second) else -> { val urlResult = runBlocking(Dispatchers.IO) { - YouTube.player(videoId) + Innertube.player(PlayerBody(videoId = videoId)) }?.mapCatching { body -> - when (val status = body.playabilityStatus.status) { + when (val status = body.playabilityStatus?.status) { "OK" -> body.streamingData?.adaptiveFormats?.findLast { format -> format.itag == 251 || format.itag == 140 }?.let { format -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index ce7f5e8..bb2c190 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -61,12 +61,13 @@ import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.requests.albumPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext @@ -88,19 +89,21 @@ fun AlbumOverview( withContext(Dispatchers.IO) { Database.album(browseId).collect { album -> if (album?.timestamp == null) { - YouTube.album(browseId)?.onSuccess { youtubeAlbum -> + Innertube.albumPage(BrowseBody(browseId = browseId))?.onSuccess { albumPage -> Database.upsert( Album( id = browseId, - title = youtubeAlbum.title, - thumbnailUrl = youtubeAlbum.thumbnail?.url, - year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" }, - shareUrl = youtubeAlbum.url, + title = albumPage.title, + thumbnailUrl = albumPage.thumbnail?.url, + year = albumPage.year, + authorsText = albumPage.authors?.joinToString("") { it.name ?: "" }, + shareUrl = albumPage.url, timestamp = System.currentTimeMillis() ), - youtubeAlbum.songs - ?.map(YouTube.Item.Song::asMediaItem) + albumPage + .songsPage + ?.items + ?.map(Innertube.SongItem::asMediaItem) ?.onEach(Database::insert) ?.mapIndexed { position, mediaItem -> SongAlbumMap( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt index 4383bd2..79170c5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,19 +25,19 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable -inline fun ArtistContent( +inline fun ArtistContent( artist: Artist?, - youtubeArtist: YouTube.Artist?, + youtubeArtistPage: Innertube.ArtistPage?, isLoading: Boolean, isError: Boolean, stateSaver: ListSaver>, - crossinline itemsProvider: suspend (String?) -> Result?>>?, + crossinline itemsPageProvider: suspend (String?) -> Result?>?, crossinline bookmarkIconContent: @Composable () -> Unit, crossinline shareIconContent: @Composable () -> Unit, crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, @@ -61,18 +60,18 @@ inline fun ArtistContent( val (continuationState, fetch) = produceSaveableRelaunchableOneShotState( initialValue = null, stateSaver = autoSaver(), - youtubeArtist + youtubeArtistPage ) { - if (youtubeArtist == null) return@produceSaveableRelaunchableOneShotState + if (youtubeArtistPage == null) return@produceSaveableRelaunchableOneShotState println("loading... $value") isLoadingItems = true withContext(Dispatchers.IO) { - itemsProvider(value)?.onSuccess { (continuation, newItems) -> - value = continuation - newItems?.let { - items = items.plus(it).distinctBy(YouTube.Item::key) + itemsPageProvider(value)?.onSuccess { itemsPage -> + value = itemsPage?.continuation + itemsPage?.items?.let { + items = items.plus(it).distinctBy(Innertube.Item::key) } isErrorItems = false isLoadingItems = false @@ -105,7 +104,7 @@ inline fun ArtistContent( items( items = items, - key = YouTube.Item::key, + key = Innertube.Item::key, itemContent = itemContent ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index fdea1c9..3ed1e4a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -57,14 +57,14 @@ import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @ExperimentalAnimationApi @Composable fun ArtistOverview( artist: Artist?, - youtubeArtist: YouTube.Artist?, + youtubeArtistPage: Innertube.ArtistPage?, isLoading: Boolean, isError: Boolean, onViewAllSongsClick: () -> Unit, @@ -100,7 +100,7 @@ fun ArtistOverview( when { artist != null -> { Header(title = artist.name ?: "Unknown") { - youtubeArtist?.radioEndpoint?.let { radioEndpoint -> + youtubeArtistPage?.radioEndpoint?.let { radioEndpoint -> SecondaryTextButton( text = "Start radio", onClick = { @@ -130,8 +130,8 @@ fun ArtistOverview( ) when { - youtubeArtist != null -> { - youtubeArtist.songs?.let { songs -> + youtubeArtistPage != null -> { + youtubeArtistPage.songs?.let { songs -> Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.SpaceBetween, @@ -144,7 +144,7 @@ fun ArtistOverview( modifier = sectionTextModifier ) - youtubeArtist.songsEndpoint?.let { + youtubeArtistPage.songsEndpoint?.let { BasicText( text = "View all", style = typography.xs.secondary, @@ -174,7 +174,7 @@ fun ArtistOverview( } } - youtubeArtist.albums?.let { albums -> + youtubeArtistPage.albums?.let { albums -> Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.SpaceBetween, @@ -187,7 +187,7 @@ fun ArtistOverview( modifier = sectionTextModifier ) - youtubeArtist.albumsEndpoint?.let { + youtubeArtistPage.albumsEndpoint?.let { BasicText( text = "View all", style = typography.xs.secondary, @@ -207,7 +207,7 @@ fun ArtistOverview( ) { items( items = albums, - key = YouTube.Item.Album::key + key = Innertube.AlbumItem::key ) { album -> AlternativeAlbumItem( album = album, @@ -224,7 +224,7 @@ fun ArtistOverview( } } - youtubeArtist.singles?.let { singles -> + youtubeArtistPage.singles?.let { singles -> Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.SpaceBetween, @@ -237,7 +237,7 @@ fun ArtistOverview( modifier = sectionTextModifier ) - youtubeArtist.singlesEndpoint?.let { + youtubeArtistPage.singlesEndpoint?.let { BasicText( text = "View all", style = typography.xs.secondary, @@ -257,7 +257,7 @@ fun ArtistOverview( ) { items( items = singles, - key = YouTube.Item.Album::key + key = Innertube.AlbumItem::key ) { album -> AlternativeAlbumItem( album = album, @@ -330,7 +330,7 @@ fun ArtistOverview( } } - youtubeArtist?.shuffleEndpoint?.let { shuffleEndpoint -> + youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint -> PrimaryButton( iconId = R.drawable.shuffle, onClick = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index cb20a28..41377c4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -26,14 +26,13 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.PartialArtist import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.ArtistSaver -import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver -import it.vfsfitvnm.vimusic.savers.YouTubeArtistPageSaver -import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver +import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver import it.vfsfitvnm.vimusic.savers.nullableSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResult import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -47,7 +46,12 @@ import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.produceSaveableLazyOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.requests.artistPage +import it.vfsfitvnm.youtubemusic.requests.itemsPage +import it.vfsfitvnm.youtubemusic.utils.from import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn @@ -72,22 +76,22 @@ fun ArtistScreen(browseId: String) { val youtubeArtist by produceSaveableLazyOneShotState( initialValue = null, - stateSaver = nullableSaver(YouTubeArtistPageSaver) + stateSaver = nullableSaver(InnertubeArtistPageSaver) ) { println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!") isLoading = true withContext(Dispatchers.IO) { - YouTube.artist(browseId)?.onSuccess { youtubeArtist -> - value = youtubeArtist + Innertube.artistPage(browseId)?.onSuccess { artistPage -> + value = artistPage query { Database.upsert( PartialArtist( id = browseId, - name = youtubeArtist.name, - thumbnailUrl = youtubeArtist.thumbnail?.url, - info = youtubeArtist.description, + name = artistPage.name, + thumbnailUrl = artistPage.thumbnail?.url, + info = artistPage.description, timestamp = System.currentTimeMillis() ) ) @@ -136,10 +140,13 @@ fun ArtistScreen(browseId: String) { colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent), modifier = Modifier .clickable { - val bookmarkedAt = if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null + val bookmarkedAt = + if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null query { - artist?.copy(bookmarkedAt = bookmarkedAt)?.let(Database::update) + artist + ?.copy(bookmarkedAt = bookmarkedAt) + ?.let(Database::update) } } .padding(all = 4.dp) @@ -194,7 +201,7 @@ fun ArtistScreen(browseId: String) { when (currentTabIndex) { 0 -> ArtistOverview( artist = artist, - youtubeArtist = youtubeArtist, + youtubeArtistPage = youtubeArtist, isLoading = isLoading, isError = isError, bookmarkIconContent = bookmarkIconContent, @@ -204,6 +211,7 @@ fun ArtistScreen(browseId: String) { onViewAllAlbumsClick = { onTabIndexChanged(2) }, onViewAllSinglesClick = { onTabIndexChanged(3) }, ) + 1 -> { val binder = LocalPlayerServiceBinder.current val thumbnailSizeDp = Dimensions.thumbnails.song @@ -211,20 +219,26 @@ fun ArtistScreen(browseId: String) { ArtistContent( artist = artist, - youtubeArtist = youtubeArtist, + youtubeArtistPage = youtubeArtist, isLoading = isLoading, isError = isError, - stateSaver = YouTubeSongListSaver, + stateSaver = InnertubeSongItemListSaver, bookmarkIconContent = bookmarkIconContent, shareIconContent = shareIconContent, - itemsProvider = { continuation -> - youtubeArtist + itemsPageProvider = { continuation -> + continuation?.let { + Innertube.itemsPage( + body = ContinuationBody(continuation = continuation), + fromMusicResponsiveListItemRenderer = Innertube.SongItem::from, + ) + } ?: youtubeArtist ?.songsEndpoint ?.browseId ?.let { browseId -> - YouTube.items(browseId, continuation, YouTube.Item.Song::from)?.map { result -> - result?.continuation to result?.items - } + Innertube.itemsPage( + body = BrowseBody(browseId = browseId), + fromMusicResponsiveListItemRenderer = Innertube.SongItem::from, + ) } }, itemContent = { song -> @@ -243,25 +257,33 @@ fun ArtistScreen(browseId: String) { } ) } + 2 -> { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px ArtistContent( artist = artist, - youtubeArtist = youtubeArtist, + youtubeArtistPage = youtubeArtist, isLoading = isLoading, isError = isError, - stateSaver = YouTubeAlbumListSaver, + stateSaver = InnertubeAlbumItemListSaver, bookmarkIconContent = bookmarkIconContent, shareIconContent = shareIconContent, - itemsProvider = { - youtubeArtist - ?.albumsEndpoint - ?.let { endpoint -> - YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result -> - result?.continuation to result?.items - } + itemsPageProvider = { continuation -> + continuation?.let { + Innertube.itemsPage( + body = ContinuationBody(continuation = continuation), + fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, + ) + } ?: youtubeArtist + ?.songsEndpoint + ?.browseId + ?.let { browseId -> + Innertube.itemsPage( + body = BrowseBody(browseId = browseId), + fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, + ) } }, itemContent = { album -> @@ -282,25 +304,33 @@ fun ArtistScreen(browseId: String) { } ) } + 3 -> { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px ArtistContent( artist = artist, - youtubeArtist = youtubeArtist, + youtubeArtistPage = youtubeArtist, isLoading = isLoading, isError = isError, - stateSaver = YouTubeAlbumListSaver, + stateSaver = InnertubeAlbumItemListSaver, bookmarkIconContent = bookmarkIconContent, shareIconContent = shareIconContent, - itemsProvider = { - youtubeArtist - ?.singlesEndpoint - ?.let { endpoint -> - YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result -> - result?.continuation to result?.items - } + itemsPageProvider = { continuation -> + continuation?.let { + Innertube.itemsPage( + body = ContinuationBody(continuation = continuation), + fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, + ) + } ?: youtubeArtist + ?.songsEndpoint + ?.browseId + ?.let { browseId -> + Innertube.itemsPage( + body = BrowseBody(browseId = browseId), + fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, + ) } }, itemContent = { album -> @@ -321,6 +351,7 @@ fun ArtistScreen(browseId: String) { } ) } + 4 -> ArtistLocalSongsList( browseId = browseId, artist = artist, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt index 1a4289c..b1c783d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt @@ -34,7 +34,7 @@ import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.savers.DetailedSongSaver -import it.vfsfitvnm.vimusic.savers.YouTubeRelatedSaver +import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver import it.vfsfitvnm.vimusic.savers.nullableSaver import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header @@ -60,8 +60,10 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.requests.relatedPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull @@ -88,30 +90,13 @@ fun QuickPicks( .collect { value = it } } - val relatedResult by produceSaveableOneShotState( + val relatedPageResult by produceSaveableOneShotState( initialValue = null, - stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)), + stateSaver = resultSaver(nullableSaver(InnertubeRelatedPageSaver)), trending?.id ) { trending?.id?.let { trendingVideoId -> - value = YouTube.related(trendingVideoId)?.map { related -> - related?.copy( - albums = related.albums?.map { album -> - album.copy( - authors = trending?.artists?.map { info -> - YouTube.Info( - name = info.name, - endpoint = NavigationEndpoint.Endpoint.Browse( - browseId = info.id, - params = null, - browseEndpointContextSupportedConfigs = null - ) - ) - } - ) - } - ) - } + value = Innertube.relatedPage(NextBody(videoId = trendingVideoId)) } } @@ -140,7 +125,7 @@ fun QuickPicks( ) { Header(title = "Quick picks") - relatedResult?.getOrNull()?.let { related -> + relatedPageResult?.getOrNull()?.let { related -> LazyHorizontalGrid( rows = GridCells.Fixed(4), modifier = Modifier @@ -171,7 +156,7 @@ fun QuickPicks( items( items = related.songs ?: emptyList(), - key = YouTube.Item.Song::key + key = Innertube.SongItem::key ) { song -> SmallSongItem( song = song, @@ -204,7 +189,7 @@ fun QuickPicks( ) { items( items = related.albums ?: emptyList(), - key = YouTube.Item.Album::key + key = Innertube.AlbumItem::key ) { album -> AlbumItem( album = album, @@ -235,7 +220,7 @@ fun QuickPicks( ) { items( items = related.artists ?: emptyList(), - key = YouTube.Item.Artist::key, + key = Innertube.ArtistItem::key, ) { artist -> ArtistItem( artist = artist, @@ -268,7 +253,7 @@ fun QuickPicks( ) { items( items = related.playlists ?: emptyList(), - key = YouTube.Item.Playlist::key, + key = Innertube.PlaylistItem::key, ) { playlist -> PlaylistItem( playlist = playlist, @@ -284,7 +269,7 @@ fun QuickPicks( ) } } - } ?: relatedResult?.exceptionOrNull()?.let { + } ?: relatedPageResult?.exceptionOrNull()?.let { BasicText( text = "An error has occurred", style = typography.s.secondary.center, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index 4ecd231..f114983 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -34,6 +34,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver +import it.vfsfitvnm.vimusic.savers.nullableSaver import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.Header @@ -50,7 +51,9 @@ import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.produceSaveableState -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.requests.playlistPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.runBlocking @@ -68,7 +71,7 @@ fun LocalPlaylistSongList( val playlistWithSongs by produceSaveableState( initialValue = null, - stateSaver = PlaylistWithSongsSaver + stateSaver = nullableSaver(PlaylistWithSongsSaver) ) { Database .playlistWithSongs(playlistId) @@ -165,13 +168,16 @@ fun LocalPlaylistSongList( transaction { runBlocking(Dispatchers.IO) { withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { it.next() } + // TODO: fetch all songs! + Innertube.playlistPage(BrowseBody(browseId = browseId)) } }?.getOrNull()?.let { remotePlaylist -> Database.clearPlaylist(playlistId) - remotePlaylist.songs - ?.map(YouTube.Item.Song::asMediaItem) + remotePlaylist. + songsPage + ?.items + ?.map(Innertube.SongItem::asMediaItem) ?.onEach(Database::insert) ?.mapIndexed { position, mediaItem -> SongPlaylistMap( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index 6267680..45b7dbe 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -69,7 +69,9 @@ import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.relaunchableEffect import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.verticalFadingEdge -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.requests.lyrics import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -134,8 +136,7 @@ fun Lyrics( duration = duration / 1000 )?.map { it?.value } } else { - YouTube.next(mediaId, null) - ?.map { nextResult -> nextResult.lyrics()?.getOrNull() } + Innertube.lyrics(NextBody(videoId = mediaId)) }?.map { newLyrics -> onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "") state = state.copy(isLoading = false) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt index 3984ac9..1119cc3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt @@ -40,7 +40,9 @@ import it.vfsfitvnm.vimusic.ui.styling.overlay import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.rememberVolume -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody +import it.vfsfitvnm.youtubemusic.requests.player import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged @@ -195,7 +197,7 @@ fun StatsForNerds( onClick = { query { runBlocking(Dispatchers.IO) { - YouTube.player(mediaId) + Innertube.player(PlayerBody(videoId = mediaId)) ?.map { response -> response.streamingData?.adaptiveFormats ?.findLast { format -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt index 4f8720d..94aaedc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt @@ -27,7 +27,9 @@ fun PlaylistScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - PlaylistSongList(browseId = browseId) + when (currentTabIndex) { + 0 -> PlaylistSongList(browseId = browseId) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 2ecc49b..dd71efc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -39,7 +39,7 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.savers.YouTubePlaylistOrAlbumSaver +import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.ui.components.themed.Header @@ -58,11 +58,12 @@ import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.requests.playlistPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext @@ -76,12 +77,13 @@ fun PlaylistSongList( val binder = LocalPlayerServiceBinder.current val context = LocalContext.current - val playlistResult by produceSaveableOneShotState( + val playlistPageResult by produceSaveableOneShotState( initialValue = null, - stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver), + stateSaver = resultSaver(InnertubePlaylistOrAlbumPageSaver), ) { value = withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { it.next() } + // TODO: fetch all songs! + Innertube.playlistPage(BrowseBody(browseId = browseId)) } } @@ -102,7 +104,7 @@ fun PlaylistSongList( val songThumbnailSizeDp = Dimensions.thumbnails.song val songThumbnailSizePx = songThumbnailSizeDp.px - playlistResult?.getOrNull()?.let { playlist -> + playlistPageResult?.getOrNull()?.let { playlist -> LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -117,9 +119,9 @@ fun PlaylistSongList( Header(title = playlist.title ?: "Unknown") { SecondaryTextButton( text = "Enqueue", - isEnabled = playlist.songs?.isNotEmpty() == true, + isEnabled = playlist.songsPage?.items?.isNotEmpty() == true, onClick = { - playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> binder?.player?.enqueue(mediaItems) } } @@ -147,8 +149,8 @@ fun PlaylistSongList( ) ) - playlist.songs - ?.map(YouTube.Item.Song::asMediaItem) + playlist.songsPage?.items + ?.map(Innertube.SongItem::asMediaItem) ?.onEach(Database::insert) ?.mapIndexed { index, mediaItem -> SongPlaylistMap( @@ -196,13 +198,13 @@ fun PlaylistSongList( } } - itemsIndexed(items = playlist.songs ?: emptyList()) { index, song -> + itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song -> SongItem( title = song.info?.name, authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" }, durationText = song.durationText, onClick = { - playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> binder?.stopRadio() binder?.player?.forcePlayAtIndex(mediaItems, index) } @@ -226,15 +228,15 @@ fun PlaylistSongList( PrimaryButton( iconId = R.drawable.shuffle, - isEnabled = playlist.songs?.isNotEmpty() == true, + isEnabled = playlist.songsPage?.items?.isNotEmpty() == true, onClick = { - playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> binder?.stopRadio() binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) } } ) - } ?: playlistResult?.exceptionOrNull()?.let { + } ?: playlistPageResult?.exceptionOrNull()?.let { Box( modifier = Modifier .align(Alignment.Center) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index fe41c28..a886ed7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.autoSaver import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint @@ -41,7 +42,7 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver -import it.vfsfitvnm.vimusic.savers.StringListResultSaver +import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -51,7 +52,9 @@ import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.SearchSuggestionsBody +import it.vfsfitvnm.youtubemusic.requests.searchSuggestions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -80,11 +83,11 @@ fun OnlineSearch( val suggestionsResult by produceSaveableOneShotState( initialValue = null, - stateSaver = StringListResultSaver, + stateSaver = resultSaver(autoSaver?>()), key1 = textFieldValue.text ) { if (textFieldValue.text.isNotEmpty()) { - value = YouTube.getSearchSuggestions(textFieldValue.text) + value = Innertube.searchSuggestions(SearchSuggestionsBody(input = textFieldValue.text)) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index 38c4aa2..62b8fb7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -23,23 +24,28 @@ import androidx.compose.ui.input.pointer.pointerInput import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.savers.ListSaver -import it.vfsfitvnm.vimusic.savers.StringResultSaver +import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.models.bodies.SearchBody +import it.vfsfitvnm.youtubemusic.requests.searchPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable -inline fun SearchResult( +inline fun SearchResult( query: String, filter: String, stateSaver: ListSaver>, + noinline fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?, crossinline onSearchAgain: () -> Unit, crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, noinline itemShimmer: @Composable BoxScope.() -> Unit, @@ -52,21 +58,30 @@ inline fun SearchResult( val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState( initialValue = null, - stateSaver = StringResultSaver + stateSaver = resultSaver(autoSaver()) ) { val token = value?.getOrNull() value = null value = withContext(Dispatchers.IO) { - YouTube.search(query, filter, token) - }?.map { searchResult -> - @Suppress("UNCHECKED_CAST") - (searchResult.items as List?)?.let { - items = items.plus(it).distinctBy(YouTube.Item::key) + if (token == null) { + Innertube.searchPage( + body = SearchBody(query = query, params = filter), + fromMusicShelfRendererContent = fromMusicShelfRendererContent + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = token), + fromMusicShelfRendererContent = fromMusicShelfRendererContent + ) + } + }?.map { itemsPage -> + itemsPage?.items?.let { + items = items.plus(it).distinctBy(Innertube.Item::key) } - searchResult.continuation + itemsPage?.continuation } } @@ -94,7 +109,7 @@ inline fun SearchResult( items( items = items, - key = YouTube.Item::key, + key = Innertube.Item::key, itemContent = itemContent ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index 1a75ddb..1f282e8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -13,16 +13,15 @@ import androidx.compose.ui.unit.dp import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver -import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver -import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver -import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver -import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeArtistItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeVideoItemListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.px @@ -40,7 +39,8 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.utils.from @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -52,12 +52,6 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { RouteHandler(listenToGlobalEmitter = true) { globalRoutes() - playlistRoute { browseId -> - PlaylistScreen( - browseId = browseId ?: "browseId cannot be null" - ) - } - host { Scaffold( topIconButtonId = R.drawable.chevron_back, @@ -74,12 +68,12 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { } ) { tabIndex -> val searchFilter = when (tabIndex) { - 0 -> YouTube.Item.Song.Filter - 1 -> YouTube.Item.Album.Filter - 2 -> YouTube.Item.Artist.Filter - 3 -> YouTube.Item.Video.Filter - 4 -> YouTube.Item.CommunityPlaylist.Filter - 5 -> YouTube.Item.FeaturedPlaylist.Filter + 0 -> Innertube.SearchFilter.Song + 1 -> Innertube.SearchFilter.Album + 2 -> Innertube.SearchFilter.Artist + 3 -> Innertube.SearchFilter.Video + 4 -> Innertube.SearchFilter.CommunityPlaylist + 5 -> Innertube.SearchFilter.FeaturedPlaylist else -> error("unreachable") }.value @@ -94,7 +88,8 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { query = query, filter = searchFilter, onSearchAgain = onSearchAgain, - stateSaver = YouTubeSongListSaver, + stateSaver = InnertubeSongItemListSaver, + fromMusicShelfRendererContent = Innertube.SongItem.Companion::from, itemContent = { song -> SmallSongItem( song = song, @@ -119,8 +114,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { SearchResult( query = query, filter = searchFilter, - stateSaver = YouTubeAlbumListSaver, + stateSaver = InnertubeAlbumItemListSaver, onSearchAgain = onSearchAgain, + fromMusicShelfRendererContent = Innertube.AlbumItem.Companion::from, itemContent = { album -> AlbumItem( album = album, @@ -148,8 +144,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { SearchResult( query = query, filter = searchFilter, - stateSaver = YouTubeArtistListSaver, + stateSaver = InnertubeArtistItemListSaver, onSearchAgain = onSearchAgain, + fromMusicShelfRendererContent = Innertube.ArtistItem.Companion::from, itemContent = { artist -> ArtistItem( artist = artist, @@ -176,8 +173,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { SearchResult( query = query, filter = searchFilter, - stateSaver = YouTubeVideoListSaver, + stateSaver = InnertubeVideoItemListSaver, onSearchAgain = onSearchAgain, + fromMusicShelfRendererContent = Innertube.VideoItem.Companion::from, itemContent = { video -> VideoItem( video = video, @@ -206,8 +204,9 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { SearchResult( query = query, filter = searchFilter, - stateSaver = YouTubePlaylistListSaver, + stateSaver = InnertubePlaylistItemListSaver, onSearchAgain = onSearchAgain, + fromMusicShelfRendererContent = Innertube.PlaylistItem.Companion::from, itemContent = { playlist -> PlaylistItem( playlist = playlist, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index 0e9362c..12ffdd7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -41,7 +41,7 @@ import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube @Composable fun SmallSongItemShimmer( @@ -73,7 +73,7 @@ fun SmallSongItemShimmer( @ExperimentalAnimationApi @Composable fun SmallSongItem( - song: YouTube.Item.Song, + song: Innertube.SongItem, thumbnailSizePx: Int, onClick: () -> Unit, modifier: Modifier = Modifier @@ -95,7 +95,7 @@ fun SmallSongItem( @ExperimentalAnimationApi @Composable fun VideoItem( - video: YouTube.Item.Video, + video: Innertube.VideoItem, thumbnailHeightDp: Dp, thumbnailWidthDp: Dp, onClick: () -> Unit, @@ -212,7 +212,7 @@ fun VideoItemShimmer( @Composable fun PlaylistItem( - playlist: YouTube.Item.Playlist, + playlist: Innertube.PlaylistItem, thumbnailSizePx: Int, thumbnailSizeDp: Dp, modifier: Modifier = Modifier, @@ -298,7 +298,7 @@ fun PlaylistItemShimmer( @Composable fun AlbumItem( - album: YouTube.Item.Album, + album: Innertube.AlbumItem, thumbnailSizePx: Int, thumbnailSizeDp: Dp, modifier: Modifier = Modifier, @@ -383,7 +383,7 @@ fun AlbumItemShimmer( @Composable fun AlternativeAlbumItem( - album: YouTube.Item.Album, + album: Innertube.AlbumItem, thumbnailSizePx: Int, thumbnailSizeDp: Dp, modifier: Modifier = Modifier, @@ -452,7 +452,7 @@ fun AlternativeAlbumItemPlaceholder( @Composable fun ArtistItem( - artist: YouTube.Item.Artist, + artist: Innertube.ArtistItem, thumbnailSizePx: Int, thumbnailSizeDp: Dp, modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 8b83ac4..d925205 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -6,9 +6,9 @@ import androidx.core.os.bundleOf import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import it.vfsfitvnm.vimusic.models.DetailedSong -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube -val YouTube.Item.Song.asMediaItem: MediaItem +val Innertube.SongItem.asMediaItem: MediaItem get() = MediaItem.Builder() .setMediaId(key) .setUri(key) @@ -32,7 +32,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem ) .build() -val YouTube.Item.Video.asMediaItem: MediaItem +val Innertube.VideoItem.asMediaItem: MediaItem get() = MediaItem.Builder() .setMediaId(key) .setUri(key) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt index 8a0ce11..a83399e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/YoutubeRadio.kt @@ -1,7 +1,10 @@ package it.vfsfitvnm.vimusic.utils import androidx.media3.common.MediaItem -import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.requests.nextPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -17,20 +20,30 @@ data class YouTubeRadio( var mediaItems: List? = null nextContinuation = withContext(Dispatchers.IO) { - YouTube.next( - videoId = videoId, - playlistId = playlistId, - params = parameters, - playlistSetVideoId = playlistSetVideoId, - continuation = nextContinuation - )?.getOrNull()?.let { nextResult -> - playlistId = nextResult.playlistId - parameters = nextResult.params - playlistSetVideoId = nextResult.playlistSetVideoId + val continuation = nextContinuation - mediaItems = nextResult.items?.map(YouTube.Item.Song::asMediaItem) - nextResult.continuation?.takeUnless { nextContinuation == nextResult.continuation } + if (continuation == null) { + Innertube.nextPage( + NextBody( + videoId = videoId, + playlistId = playlistId, + params = parameters, + playlistSetVideoId = playlistSetVideoId + ) + )?.map { nextResult -> + playlistId = nextResult.playlistId + parameters = nextResult.params + playlistSetVideoId = nextResult.playlistSetVideoId + + nextResult.itemsPage + } + } else { + Innertube.nextPage(ContinuationBody(continuation = continuation)) + }?.getOrNull()?.let { songsPage -> + mediaItems = songsPage.items?.map(Innertube.SongItem::asMediaItem) + songsPage.continuation?.takeUnless { nextContinuation == it } } + } return mediaItems ?: emptyList() diff --git a/youtube-music/.gitignore b/innertube/.gitignore similarity index 100% rename from youtube-music/.gitignore rename to innertube/.gitignore diff --git a/youtube-music/build.gradle.kts b/innertube/build.gradle.kts similarity index 100% rename from youtube-music/build.gradle.kts rename to innertube/build.gradle.kts diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt new file mode 100644 index 0000000..a0a5a42 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/Innertube.kt @@ -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( + 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?, + val authors: List>?, + val album: Info?, + val durationText: String?, + override val thumbnail: Thumbnail? + ) : Item() { + override val key get() = info!!.endpoint!!.videoId!! + + companion object + } + + data class VideoItem( + val info: Info?, + val authors: List>?, + 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?, + val authors: List>?, + val year: String?, + override val thumbnail: Thumbnail? + ) : Item() { + override val key get() = info!!.endpoint!!.browseId!! + + companion object + } + + data class ArtistItem( + val info: Info?, + val subscribersCountText: String?, + override val thumbnail: Thumbnail? + ) : Item() { + override val key get() = info!!.endpoint!!.browseId!! + + companion object + } + + data class PlaylistItem( + val info: Info?, + val channel: Info?, + 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?, + val songsEndpoint: NavigationEndpoint.Endpoint.Browse?, + val albums: List?, + val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?, + val singles: List?, + val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?, + ) + + data class PlaylistOrAlbumPage( + val title: String?, + val authors: List>?, + val year: String?, + val thumbnail: Thumbnail?, + val url: String?, + val songsPage: ItemsPage? + ) + + data class NextPage( + val itemsPage: ItemsPage?, + val playlistId: String?, + val params: String? = null, + val playlistSetVideoId: String? = null + ) + + data class RelatedPage( + val songs: List? = null, + val playlists: List? = null, + val albums: List? = null, + val artists: List? = null, + ) + + data class ItemsPage( + val items: List?, + val continuation: String? + ) +} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt similarity index 77% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt index 40779d1..3c4ada8 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/BrowseResponse.kt @@ -1,9 +1,7 @@ package it.vfsfitvnm.youtubemusic.models -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -@OptIn(ExperimentalSerializationApi::class) @Serializable data class BrowseResponse( val contents: Contents?, @@ -23,10 +21,10 @@ data class BrowseResponse( ) { @Serializable data class MusicDetailHeaderRenderer( - val title: Runs, - val subtitle: Runs, - val secondSubtitle: Runs, - val thumbnail: ThumbnailRenderer, + val title: Runs?, + val subtitle: Runs?, + val secondSubtitle: Runs?, + val thumbnail: ThumbnailRenderer?, ) @Serializable @@ -35,16 +33,16 @@ data class BrowseResponse( val playButton: PlayButton?, val startRadioButton: StartRadioButton?, val thumbnail: ThumbnailRenderer?, - val title: Runs + val title: Runs? ) { @Serializable data class PlayButton( - val buttonRenderer: ButtonRenderer + val buttonRenderer: ButtonRenderer? ) @Serializable data class StartRadioButton( - val buttonRenderer: ButtonRenderer + val buttonRenderer: ButtonRenderer? ) } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt similarity index 72% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt index d511804..495cf83 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ButtonRenderer.kt @@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable @Serializable data class ButtonRenderer( - val navigationEndpoint: NavigationEndpoint + val navigationEndpoint: NavigationEndpoint? ) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Context.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Context.kt new file mode 100644 index 0000000..92af0e8 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Context.kt @@ -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, + ) + ) + } +} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt similarity index 84% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt index 7e6092f..5dbc6f8 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Continuation.kt @@ -8,10 +8,10 @@ import kotlinx.serialization.json.JsonNames @Serializable data class Continuation( @JsonNames("nextContinuationData", "nextRadioContinuationData") - val nextContinuationData: Data + val nextContinuationData: Data? ) { @Serializable data class Data( - val continuation: String + val continuation: String? ) } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt similarity index 62% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt index c880434..f5df6c1 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ContinuationResponse.kt @@ -12,12 +12,7 @@ data class ContinuationResponse( @Serializable data class ContinuationContents( @JsonNames("musicPlaylistShelfContinuation") - val musicShelfContinuation: MusicShelfRenderer? - ) { -// @Serializable -// data class MusicShelfContinuation( -// val continuations: List?, -// val contents: List -// ) - } + val musicShelfContinuation: MusicShelfRenderer?, + val playlistPanelContinuation: NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?, + ) } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt similarity index 75% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt index b9a7a44..3c619ba 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetQueueResponse.kt @@ -1,9 +1,7 @@ package it.vfsfitvnm.youtubemusic.models -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -@OptIn(ExperimentalSerializationApi::class) @Serializable data class GetQueueResponse( val queueDatas: List?, diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GridRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GridRenderer.kt new file mode 100644 index 0000000..fce0477 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GridRenderer.kt @@ -0,0 +1,13 @@ +package it.vfsfitvnm.youtubemusic.models + +import kotlinx.serialization.Serializable + +@Serializable +data class GridRenderer( + val items: List?, +) { + @Serializable + data class Item( + val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? + ) +} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt similarity index 83% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt index 121b825..ad51a67 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt @@ -4,13 +4,12 @@ import kotlinx.serialization.Serializable @Serializable data class MusicCarouselShelfRenderer( - val header: Header, + val header: Header?, val contents: List, ) { @Serializable data class Content( val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?, - val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?, val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?, ) @@ -23,11 +22,12 @@ data class MusicCarouselShelfRenderer( @Serializable data class MusicCarouselShelfBasicHeaderRenderer( val moreContentButton: MoreContentButton?, - val title: Runs, + val title: Runs?, + val strapline: Runs?, ) { @Serializable data class MoreContentButton( - val buttonRenderer: ButtonRenderer + val buttonRenderer: ButtonRenderer? ) } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt similarity index 95% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt index 66016bc..340f5ba 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicResponsiveListItemRenderer.kt @@ -15,7 +15,7 @@ data class MusicResponsiveListItemRenderer( @Serializable data class FlexColumn( @JsonNames("musicResponsiveListItemFixedColumnRenderer") - val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer + val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer? ) { @Serializable data class MusicResponsiveListItemFlexColumnRenderer( diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt similarity index 78% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt index ea6dca6..25699d1 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicShelfRenderer.kt @@ -5,34 +5,34 @@ import kotlinx.serialization.Serializable @Serializable data class MusicShelfRenderer( val bottomEndpoint: NavigationEndpoint?, - val contents: List, + val contents: List?, val continuations: List?, val title: Runs? ) { @Serializable data class Content( - val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer, + val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?, ) { val runs: Pair, List>> get() = (musicResponsiveListItemRenderer - .flexColumns - .firstOrNull() + ?.flexColumns + ?.firstOrNull() ?.musicResponsiveListItemFlexColumnRenderer ?.text ?.runs ?: emptyList()) to (musicResponsiveListItemRenderer - .flexColumns - .lastOrNull() + ?.flexColumns + ?.lastOrNull() ?.musicResponsiveListItemFlexColumnRenderer ?.text ?.splitBySeparator() ?: emptyList() ) - val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? + val thumbnail: Thumbnail? get() = musicResponsiveListItemRenderer - .thumbnail + ?.thumbnail ?.musicThumbnailRenderer ?.thumbnail ?.thumbnails diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt new file mode 100644 index 0000000..6baee0d --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt @@ -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?, +) diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt similarity index 96% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt index da97a1f..5352669 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt @@ -74,12 +74,12 @@ data class NavigationEndpoint( @Serializable data class WatchEndpointMusicSupportedConfigs( - val watchEndpointMusicConfig: WatchEndpointMusicConfig + val watchEndpointMusicConfig: WatchEndpointMusicConfig? ) { @Serializable data class WatchEndpointMusicConfig( - val musicVideoType: String + val musicVideoType: String? ) } } @@ -87,14 +87,14 @@ data class NavigationEndpoint( @Serializable data class WatchPlaylist( val params: String?, - val playlistId: String, + val playlistId: String?, ) : Endpoint() @Serializable data class Browse( - val params: String?, - val browseId: String?, - val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?, + val params: String? = null, + val browseId: String? = null, + val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null, ) : Endpoint() { val type: String? get() = browseEndpointContextSupportedConfigs diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt similarity index 72% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt index 2dcdb28..7676c89 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NextResponse.kt @@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonNames @OptIn(ExperimentalSerializationApi::class) @Serializable data class NextResponse( - val contents: Contents, - val continuationContents: MusicQueueRenderer.Content? + val contents: Contents? ) { @Serializable data class MusicQueueRenderer( @@ -17,30 +16,18 @@ data class NextResponse( @Serializable data class Content( @JsonNames("playlistPanelContinuation") - val playlistPanelRenderer: PlaylistPanelRenderer + val playlistPanelRenderer: PlaylistPanelRenderer? ) { @Serializable data class PlaylistPanelRenderer( val contents: List?, val continuations: List?, - val playlistId: String? ) { @Serializable data class Content( val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?, val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?, ) { - @Serializable - data class PlaylistPanelVideoRenderer( - val title: Runs?, - val longBylineText: Runs?, - val shortBylineText: Runs?, - val lengthText: Runs?, - val navigationEndpoint: NavigationEndpoint, - val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail, - val videoId: String, - val playlistSetVideoId: String?, - ) @Serializable data class AutomixPreviewVideoRenderer( @@ -52,7 +39,7 @@ data class NextResponse( ) { @Serializable data class AutomixPlaylistVideoRenderer( - val navigationEndpoint: NavigationEndpoint + val navigationEndpoint: NavigationEndpoint? ) } } @@ -63,33 +50,33 @@ data class NextResponse( @Serializable data class Contents( - val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer + val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer? ) { @Serializable data class SingleColumnMusicWatchNextResultsRenderer( - val tabbedRenderer: TabbedRenderer + val tabbedRenderer: TabbedRenderer? ) { @Serializable data class TabbedRenderer( - val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer + val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer? ) { @Serializable data class WatchNextTabbedResultsRenderer( - val tabs: List + val tabs: List? ) { @Serializable data class Tab( - val tabRenderer: TabRenderer + val tabRenderer: TabRenderer? ) { @Serializable data class TabRenderer( val content: Content?, val endpoint: NavigationEndpoint?, - val title: String + val title: String? ) { @Serializable data class Content( - val musicQueueRenderer: MusicQueueRenderer + val musicQueueRenderer: MusicQueueRenderer? ) } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt similarity index 86% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt index 2197f10..18ffe4f 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlayerResponse.kt @@ -4,13 +4,13 @@ import kotlinx.serialization.Serializable @Serializable data class PlayerResponse( - val playabilityStatus: PlayabilityStatus, + val playabilityStatus: PlayabilityStatus?, val playerConfig: PlayerConfig?, val streamingData: StreamingData?, ) { @Serializable data class PlayabilityStatus( - val status: String + val status: String? ) @Serializable @@ -26,8 +26,7 @@ data class PlayerResponse( @Serializable data class StreamingData( - val adaptiveFormats: List, - val expiresInSeconds: String + val adaptiveFormats: List? ) { @Serializable data class AdaptiveFormat( diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlaylistPanelVideoRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlaylistPanelVideoRenderer.kt new file mode 100644 index 0000000..f24ba6d --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/PlaylistPanelVideoRenderer.kt @@ -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?, +) diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt similarity index 90% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt index 000dcb5..94825a8 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Runs.kt @@ -7,7 +7,7 @@ data class Runs( val runs: List = listOf() ) { val text: String - get() = runs.joinToString("") { it.text } + get() = runs.joinToString("") { it.text ?: "" } fun splitBySeparator(): List> { return runs.flatMapIndexed { index, run -> @@ -25,7 +25,7 @@ data class Runs( @Serializable data class Run( - val text: String, + val text: String?, val navigationEndpoint: NavigationEndpoint?, ) } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt similarity index 66% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt index fbd4883..5d6936b 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchResponse.kt @@ -3,13 +3,12 @@ package it.vfsfitvnm.youtubemusic.models import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -@OptIn(ExperimentalSerializationApi::class) @Serializable data class SearchResponse( - val contents: Contents, + val contents: Contents?, ) { @Serializable data class Contents( - val tabbedSearchResultsRenderer: Tabs + val tabbedSearchResultsRenderer: Tabs? ) } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetSearchSuggestionsResponse.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchSuggestionsResponse.kt similarity index 78% rename from youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetSearchSuggestionsResponse.kt rename to innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchSuggestionsResponse.kt index 6741633..f6dc30a 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/GetSearchSuggestionsResponse.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SearchSuggestionsResponse.kt @@ -3,24 +3,24 @@ package it.vfsfitvnm.youtubemusic.models import kotlinx.serialization.Serializable @Serializable -data class GetSearchSuggestionsResponse( +data class SearchSuggestionsResponse( val contents: List? ) { @Serializable data class Content( - val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer + val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer? ) { @Serializable data class SearchSuggestionsSectionRenderer( - val contents: List + val contents: List? ) { @Serializable data class Content( - val searchSuggestionRenderer: SearchSuggestionRenderer + val searchSuggestionRenderer: SearchSuggestionRenderer? ) { @Serializable data class SearchSuggestionRenderer( - val navigationEndpoint: NavigationEndpoint, + val navigationEndpoint: NavigationEndpoint?, ) } } diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SectionListRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SectionListRenderer.kt new file mode 100644 index 0000000..7b2bbfb --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/SectionListRenderer.kt @@ -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?, + val continuations: List? +) { + @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?, + ) + } + +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt new file mode 100644 index 0000000..e6c5de5 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt @@ -0,0 +1,25 @@ +package it.vfsfitvnm.youtubemusic.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Tabs( + val tabs: List? +) { + @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?, + ) + } + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Thumbnail.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Thumbnail.kt new file mode 100644 index 0000000..2705b99 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Thumbnail.kt @@ -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 + } + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt new file mode 100644 index 0000000..f1efcf5 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt @@ -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? + ) + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/BrowseBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/BrowseBody.kt new file mode 100644 index 0000000..ea910da --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/BrowseBody.kt @@ -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 +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/ContinuationBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/ContinuationBody.kt new file mode 100644 index 0000000..ff7b95b --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/ContinuationBody.kt @@ -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, +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/NextBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/NextBody.kt new file mode 100644 index 0000000..f1face1 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/NextBody.kt @@ -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 + ) +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/PlayerBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/PlayerBody.kt new file mode 100644 index 0000000..6f9b11f --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/PlayerBody.kt @@ -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 +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/QueueBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/QueueBody.kt new file mode 100644 index 0000000..960dfbc --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/QueueBody.kt @@ -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? = null, + val playlistId: String? = null, +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchBody.kt new file mode 100644 index 0000000..af4bb59 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchBody.kt @@ -0,0 +1,11 @@ +package it.vfsfitvnm.youtubemusic.models.bodies + +import it.vfsfitvnm.youtubemusic.models.Context +import kotlinx.serialization.Serializable + +@Serializable +data class SearchBody( + val context: Context = Context.DefaultWeb, + val query: String, + val params: String +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchSuggestionsBody.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchSuggestionsBody.kt new file mode 100644 index 0000000..3a7a32b --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/bodies/SearchSuggestionsBody.kt @@ -0,0 +1,10 @@ +package it.vfsfitvnm.youtubemusic.models.bodies + +import it.vfsfitvnm.youtubemusic.models.Context +import kotlinx.serialization.Serializable + +@Serializable +data class SearchSuggestionsBody( + val context: Context = Context.DefaultWeb, + val input: String +) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/AlbumPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/AlbumPage.kt new file mode 100644 index 0000000..0e610f6 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/AlbumPage.kt @@ -0,0 +1,36 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.http.Url +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody + +suspend fun Innertube.albumPage(body: BrowseBody): Result? { + return playlistPage(body)?.map { album -> + album.url?.let { Url(it).parameters["list"] }?.let { playlistId -> + playlistPage(BrowseBody(browseId = "VL$playlistId"))?.getOrNull()?.let { playlist -> + album.copy(songsPage = playlist.songsPage) + } + } ?: album + }?.map { album -> + val albumInfo = Innertube.Info( + name = album.title, + endpoint = NavigationEndpoint.Endpoint.Browse( + browseId = body.browseId, + params = body.params + ) + ) + + album.copy( + songsPage = album.songsPage?.copy( + items = album.songsPage.items?.map { song -> + song.copy( + authors = song.authors ?: album.authors, + album = albumInfo, + thumbnail = album.thumbnail + ) + } + ) + ) + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ArtistPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ArtistPage.kt new file mode 100644 index 0000000..392346a --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ArtistPage.kt @@ -0,0 +1,102 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.BrowseResponse +import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.SectionListRenderer +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.utils.findSectionByTitle +import it.vfsfitvnm.youtubemusic.utils.from +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.artistPage(browseId: String): Result? = runCatchingNonCancellable { + val response = client.post(browse) { + setBody(BrowseBody(browseId = browseId)) + mask("contents,header") + }.body() + + fun findSectionByTitle(text: String): SectionListRenderer.Content? { + return response + .contents + ?.singleColumnBrowseResultsRenderer + ?.tabs + ?.get(0) + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.findSectionByTitle(text) + } + + val songsSection = findSectionByTitle("Songs")?.musicShelfRenderer + val albumsSection = findSectionByTitle("Albums")?.musicCarouselShelfRenderer + val singlesSection = findSectionByTitle("Singles")?.musicCarouselShelfRenderer + + Innertube.ArtistPage( + name = response + .header + ?.musicImmersiveHeaderRenderer + ?.title + ?.text, + description = response + .header + ?.musicImmersiveHeaderRenderer + ?.description + ?.text + ?.substringBeforeLast("\n\nFrom Wikipedia"), + thumbnail = response + .header + ?.musicImmersiveHeaderRenderer + ?.thumbnail + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.getOrNull(0), + shuffleEndpoint = response + .header + ?.musicImmersiveHeaderRenderer + ?.playButton + ?.buttonRenderer + ?.navigationEndpoint + ?.watchEndpoint, + radioEndpoint = response + .header + ?.musicImmersiveHeaderRenderer + ?.startRadioButton + ?.buttonRenderer + ?.navigationEndpoint + ?.watchEndpoint, + songs = songsSection + ?.contents + ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) + ?.mapNotNull(Innertube.SongItem::from), + songsEndpoint = songsSection + ?.bottomEndpoint + ?.browseEndpoint, + albums = albumsSection + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) + ?.mapNotNull(Innertube.AlbumItem::from), + albumsEndpoint = albumsSection + ?.header + ?.musicCarouselShelfBasicHeaderRenderer + ?.moreContentButton + ?.buttonRenderer + ?.navigationEndpoint + ?.browseEndpoint, + singles = singlesSection + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) + ?.mapNotNull(Innertube.AlbumItem::from), + singlesEndpoint = singlesSection + ?.header + ?.musicCarouselShelfBasicHeaderRenderer + ?.moreContentButton + ?.buttonRenderer + ?.navigationEndpoint + ?.browseEndpoint, + ) +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ItemsPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ItemsPage.kt new file mode 100644 index 0000000..840aac0 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/ItemsPage.kt @@ -0,0 +1,97 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.BrowseResponse +import it.vfsfitvnm.youtubemusic.models.ContinuationResponse +import it.vfsfitvnm.youtubemusic.models.GridRenderer +import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.itemsPage( + body: BrowseBody, + fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null }, + fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null }, +) = runCatchingNonCancellable { + val response = client.post(browse) { + setBody(body) +// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))") + }.body() + + val sectionListRendererContent = response + .contents + ?.singleColumnBrowseResultsRenderer + ?.tabs + ?.firstOrNull() + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + + itemsPageFromMusicShelRendererOrGridRenderer( + musicShelfRenderer = sectionListRendererContent + ?.musicShelfRenderer, + gridRenderer = sectionListRendererContent + ?.gridRenderer, + fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer, + fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer, + ) +} + +suspend fun Innertube.itemsPage( + body: ContinuationBody, + fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T? = { null }, + fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T? = { null }, +) = runCatchingNonCancellable { + val response = client.post(browse) { + setBody(body) +// mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),gridRenderer(continuations,items.$musicTwoRowItemRendererMask))") + }.body() + + itemsPageFromMusicShelRendererOrGridRenderer( + musicShelfRenderer = response + .continuationContents + ?.musicShelfContinuation, + gridRenderer = null, + fromMusicResponsiveListItemRenderer = fromMusicResponsiveListItemRenderer, + fromMusicTwoRowItemRenderer = fromMusicTwoRowItemRenderer, + ) +} + +private fun itemsPageFromMusicShelRendererOrGridRenderer( + musicShelfRenderer: MusicShelfRenderer?, + gridRenderer: GridRenderer?, + fromMusicResponsiveListItemRenderer: (MusicResponsiveListItemRenderer) -> T?, + fromMusicTwoRowItemRenderer: (MusicTwoRowItemRenderer) -> T?, +): Innertube.ItemsPage? { + return if (musicShelfRenderer != null) { + Innertube.ItemsPage( + continuation = musicShelfRenderer + .continuations + ?.firstOrNull() + ?.nextContinuationData + ?.continuation, + items = musicShelfRenderer + .contents + ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) + ?.mapNotNull(fromMusicResponsiveListItemRenderer) + ) + } else if (gridRenderer != null) { + Innertube.ItemsPage( + continuation = null, + items = gridRenderer + .items + ?.mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer) + ?.mapNotNull(fromMusicTwoRowItemRenderer) + ) + } else { + null + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Lyrics.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Lyrics.kt new file mode 100644 index 0000000..a7222e9 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Lyrics.kt @@ -0,0 +1,44 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.BrowseResponse +import it.vfsfitvnm.youtubemusic.models.NextResponse +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.lyrics(body: NextBody): Result? = runCatchingNonCancellable { + val nextResponse = client.post(next) { + setBody(body) + mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)") + }.body() + + val browseId = nextResponse + .contents + ?.singleColumnMusicWatchNextResultsRenderer + ?.tabbedRenderer + ?.watchNextTabbedResultsRenderer + ?.tabs + ?.getOrNull(1) + ?.tabRenderer + ?.endpoint + ?.browseEndpoint + ?.browseId + ?: return@runCatchingNonCancellable null + + val response = client.post(browse) { + setBody(BrowseBody(browseId = browseId)) + mask("contents.sectionListRenderer.contents.musicDescriptionShelfRenderer.description") + }.body() + + response.contents + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + ?.musicDescriptionShelfRenderer + ?.description + ?.text +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/NextPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/NextPage.kt new file mode 100644 index 0000000..38fed8a --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/NextPage.kt @@ -0,0 +1,90 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.ContinuationResponse +import it.vfsfitvnm.youtubemusic.models.NextResponse +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.utils.from +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + + + +suspend fun Innertube.nextPage(body: NextBody): Result? = + runCatchingNonCancellable { + val response = client.post(next) { + setBody(body) + mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer(continuations,contents(automixPreviewVideoRenderer,$playlistPanelVideoRendererMask))") + }.body() + + val tabs = response + .contents + ?.singleColumnMusicWatchNextResultsRenderer + ?.tabbedRenderer + ?.watchNextTabbedResultsRenderer + ?.tabs + + val playlistPanelRenderer = tabs + ?.getOrNull(0) + ?.tabRenderer + ?.content + ?.musicQueueRenderer + ?.content + ?.playlistPanelRenderer + + if (body.playlistId == null) { + val endpoint = playlistPanelRenderer + ?.contents + ?.lastOrNull() + ?.automixPreviewVideoRenderer + ?.content + ?.automixPlaylistVideoRenderer + ?.navigationEndpoint + ?.watchPlaylistEndpoint + + if (endpoint != null) { + return nextPage( + body.copy( + playlistId = endpoint.playlistId, + params = endpoint.params + ) + ) + } + } + + Innertube.NextPage( + playlistId = body.playlistId, + playlistSetVideoId = body.playlistSetVideoId, + params = body.params, + itemsPage = playlistPanelRenderer + ?.toSongsPage() + ) + } + +suspend fun Innertube.nextPage(body: ContinuationBody) = runCatchingNonCancellable { + val response = client.post(next) { + setBody(body) + mask("continuationContents.playlistPanelContinuation(continuations,contents.$playlistPanelVideoRendererMask)") + }.body() + + response + .continuationContents + ?.playlistPanelContinuation + ?.toSongsPage() +} + +private fun NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer?.toSongsPage() = + Innertube.ItemsPage( + items = this + ?.contents + ?.mapNotNull(NextResponse.MusicQueueRenderer.Content.PlaylistPanelRenderer.Content::playlistPanelVideoRenderer) + ?.mapNotNull(Innertube.SongItem::from), + continuation = this + ?.continuations + ?.firstOrNull() + ?.nextContinuationData + ?.continuation + ) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Player.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Player.kt new file mode 100644 index 0000000..c2554fd --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Player.kt @@ -0,0 +1,59 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.Context +import it.vfsfitvnm.youtubemusic.models.PlayerResponse +import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable +import kotlinx.serialization.Serializable + +suspend fun Innertube.player(body: PlayerBody) = runCatchingNonCancellable { + val response = client.post(player) { + setBody(body) + mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats") + }.body() + + if (response.playabilityStatus?.status == "OK") { + response + } else { + @Serializable + data class AudioStream( + val url: String, + val bitrate: Long + ) + + @Serializable + data class PipedResponse( + val audioStreams: List + ) + + val safePlayerResponse = client.post(player) { + setBody(body.copy(context = Context.DefaultAgeRestrictionBypass)) + mask("playabilityStatus.status,playerConfig.audioConfig,streamingData.adaptiveFormats") + }.body() + + if (safePlayerResponse.playabilityStatus?.status != "OK") { + return@runCatchingNonCancellable response + } + + val audioStreams = client.get("https://watchapi.whatever.social/streams/${body.videoId}") { + contentType(ContentType.Application.Json) + }.body().audioStreams + + safePlayerResponse.copy( + streamingData = safePlayerResponse.streamingData?.copy( + adaptiveFormats = safePlayerResponse.streamingData.adaptiveFormats?.map { adaptiveFormat -> + adaptiveFormat.copy( + url = audioStreams.find { it.bitrate == adaptiveFormat.bitrate }?.url + ) + } + ) + ) + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/PlaylistPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/PlaylistPage.kt new file mode 100644 index 0000000..7f796d5 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/PlaylistPage.kt @@ -0,0 +1,90 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.BrowseResponse +import it.vfsfitvnm.youtubemusic.models.ContinuationResponse +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.utils.from +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable { + val response = client.post(browse) { + setBody(body) + mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents.musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),header.musicDetailHeaderRenderer(title,subtitle,thumbnail),microformat") + }.body() + + val musicDetailHeaderRenderer = response + .header + ?.musicDetailHeaderRenderer + + val musicShelfRenderer = response + .contents + ?.singleColumnBrowseResultsRenderer + ?.tabs + ?.firstOrNull() + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.contents + ?.firstOrNull() + ?.musicShelfRenderer + + Innertube.PlaylistOrAlbumPage( + title = musicDetailHeaderRenderer + ?.title + ?.text, + thumbnail = musicDetailHeaderRenderer + ?.thumbnail + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull(), + authors = musicDetailHeaderRenderer + ?.subtitle + ?.splitBySeparator() + ?.getOrNull(1) + ?.map(Innertube::Info), + year = musicDetailHeaderRenderer + ?.subtitle + ?.splitBySeparator() + ?.getOrNull(2) + ?.firstOrNull() + ?.text, + url = response + .microformat + ?.microformatDataRenderer + ?.urlCanonical, + songsPage = musicShelfRenderer + ?.toSongsPage() + ) +} + +suspend fun Innertube.playlistPage(body: ContinuationBody) = runCatchingNonCancellable { + val response = client.post(browse) { + setBody(body) + mask("continuationContents.musicPlaylistShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)") + }.body() + + response + .continuationContents + ?.musicShelfContinuation + ?.toSongsPage() +} + +private fun MusicShelfRenderer?.toSongsPage() = + Innertube.ItemsPage( + items = this + ?.contents + ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) + ?.mapNotNull(Innertube.SongItem::from), + continuation = this + ?.continuations + ?.firstOrNull() + ?.nextContinuationData + ?.continuation + ) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Queue.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Queue.kt new file mode 100644 index 0000000..06c9f19 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/Queue.kt @@ -0,0 +1,29 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.GetQueueResponse +import it.vfsfitvnm.youtubemusic.models.bodies.QueueBody +import it.vfsfitvnm.youtubemusic.utils.from +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.queue(body: QueueBody) = runCatchingNonCancellable { + val response = client.post(queue) { + setBody(body) + mask("queueDatas.content.$playlistPanelVideoRendererMask") + }.body() + + response + .queueDatas + ?.mapNotNull { queueData -> + queueData + .content + ?.playlistPanelVideoRenderer + ?.let(Innertube.SongItem::from) + } +} + +suspend fun Innertube.song(videoId: String): Result? = + queue(QueueBody(videoIds = listOf(videoId)))?.map { it?.firstOrNull() } diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/RelatedPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/RelatedPage.kt new file mode 100644 index 0000000..c2ad8ce --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/RelatedPage.kt @@ -0,0 +1,71 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.BrowseResponse +import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer +import it.vfsfitvnm.youtubemusic.models.NextResponse +import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody +import it.vfsfitvnm.youtubemusic.models.bodies.NextBody +import it.vfsfitvnm.youtubemusic.utils.findSectionByStrapline +import it.vfsfitvnm.youtubemusic.utils.findSectionByTitle +import it.vfsfitvnm.youtubemusic.utils.from +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.relatedPage(body: NextBody) = runCatchingNonCancellable { + val nextResponse = client.post(next) { + setBody(body) + mask("contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs.tabRenderer(endpoint,title)") + }.body() + + val browseId = nextResponse + .contents + ?.singleColumnMusicWatchNextResultsRenderer + ?.tabbedRenderer + ?.watchNextTabbedResultsRenderer + ?.tabs + ?.getOrNull(2) + ?.tabRenderer + ?.endpoint + ?.browseEndpoint + ?.browseId + ?: return@runCatchingNonCancellable null + + val response = client.post(browse) { + setBody(BrowseBody(browseId = browseId)) + mask("contents.sectionListRenderer.contents.musicCarouselShelfRenderer(header.musicCarouselShelfBasicHeaderRenderer(title,strapline),contents($musicResponsiveListItemRendererMask,$musicTwoRowItemRendererMask))") + }.body() + + val sectionListRenderer = response + .contents + ?.sectionListRenderer + + Innertube.RelatedPage( + songs = sectionListRenderer + ?.findSectionByTitle("You might also like") + ?.musicCarouselShelfRenderer + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicResponsiveListItemRenderer) + ?.mapNotNull(Innertube.SongItem::from), + playlists = sectionListRenderer + ?.findSectionByTitle("Recommended playlists") + ?.musicCarouselShelfRenderer + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) + ?.mapNotNull(Innertube.PlaylistItem::from), + albums = sectionListRenderer + ?.findSectionByStrapline("MORE FROM") + ?.musicCarouselShelfRenderer + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) + ?.mapNotNull(Innertube.AlbumItem::from), + artists = sectionListRenderer + ?.findSectionByTitle("Similar artists") + ?.musicCarouselShelfRenderer + ?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) + ?.mapNotNull(Innertube.ArtistItem::from), + ) +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchPage.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchPage.kt new file mode 100644 index 0000000..4d9218e --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchPage.kt @@ -0,0 +1,62 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.ContinuationResponse +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.SearchResponse +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.models.bodies.SearchBody +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.searchPage( + body: SearchBody, + fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T? +) = runCatchingNonCancellable { + val response = client.post(search) { + setBody(body) + mask("contents.tabbedSearchResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents.musicShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask)") + }.body() + + response + .contents + ?.tabbedSearchResultsRenderer + ?.tabs + ?.firstOrNull() + ?.tabRenderer + ?.content + ?.sectionListRenderer + ?.contents + ?.lastOrNull() + ?.musicShelfRenderer + ?.toItemsPage(fromMusicShelfRendererContent) +} + +suspend fun Innertube.searchPage( + body: ContinuationBody, + fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T? +) = runCatchingNonCancellable { + val response = client.post(search) { + setBody(body) + mask("continuationContents.musicShelfContinuation(continuations,contents.$musicResponsiveListItemRendererMask)") + }.body() + + response + .continuationContents + ?.musicShelfContinuation + ?.toItemsPage(fromMusicShelfRendererContent) +} + +private fun MusicShelfRenderer?.toItemsPage(mapper: (MusicShelfRenderer.Content) -> T?) = + Innertube.ItemsPage( + items = this + ?.contents + ?.mapNotNull(mapper), + continuation = this + ?.continuations + ?.firstOrNull() + ?.nextContinuationData + ?.continuation + ) diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchSuggestions.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchSuggestions.kt new file mode 100644 index 0000000..21a18bd --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/requests/SearchSuggestions.kt @@ -0,0 +1,29 @@ +package it.vfsfitvnm.youtubemusic.requests + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.SearchSuggestionsResponse +import it.vfsfitvnm.youtubemusic.models.bodies.SearchSuggestionsBody +import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable + +suspend fun Innertube.searchSuggestions(body: SearchSuggestionsBody) = runCatchingNonCancellable { + val response = client.post(searchSuggestions) { + setBody(body) + mask("contents.searchSuggestionsSectionRenderer.contents.searchSuggestionRenderer.navigationEndpoint.searchEndpoint.query") + }.body() + + response + .contents + ?.firstOrNull() + ?.searchSuggestionsSectionRenderer + ?.contents + ?.mapNotNull { content -> + content + .searchSuggestionRenderer + ?.navigationEndpoint + ?.searchEndpoint + ?.query + } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicResponsiveListItemRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicResponsiveListItemRenderer.kt new file mode 100644 index 0000000..51a3ca2 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicResponsiveListItemRenderer.kt @@ -0,0 +1,49 @@ +package it.vfsfitvnm.youtubemusic.utils + +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import it.vfsfitvnm.youtubemusic.models.Runs + +fun Innertube.SongItem.Companion.from(renderer: MusicResponsiveListItemRenderer): Innertube.SongItem? { + return Innertube.SongItem( + info = renderer + .flexColumns + .getOrNull(0) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.getOrNull(0) + ?.let(Innertube::Info), + authors = renderer + .flexColumns + .getOrNull(1) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.map>(Innertube::Info) + ?.takeIf(List::isNotEmpty), + durationText = renderer + .fixedColumns + ?.getOrNull(0) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.getOrNull(0) + ?.text, + album = renderer + .flexColumns + .getOrNull(2) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.firstOrNull() + ?.let(Innertube::Info), + thumbnail = renderer + .thumbnail + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + ).takeIf { it.info?.endpoint?.videoId != null } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicShelfRendererContent.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicShelfRendererContent.kt new file mode 100644 index 0000000..ea4c69b --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicShelfRendererContent.kt @@ -0,0 +1,140 @@ +package it.vfsfitvnm.youtubemusic.utils + +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +fun Innertube.SongItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.SongItem? { + val (mainRuns, otherRuns) = content.runs + + // Possible configurations: + // "song" • author(s) • album • duration + // "song" • author(s) • duration + // author(s) • album • duration + // author(s) • duration + + val album: Innertube.Info? = otherRuns + .getOrNull(otherRuns.lastIndex - 1) + ?.firstOrNull() + ?.takeIf { run -> + run + .navigationEndpoint + ?.browseEndpoint + ?.type == "MUSIC_PAGE_TYPE_ALBUM" + } + ?.let(Innertube::Info) + + return Innertube.SongItem( + info = mainRuns + .firstOrNull() + ?.let(Innertube::Info), + authors = otherRuns + .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2) + ?.map(Innertube::Info), + album = album, + durationText = otherRuns + .lastOrNull() + ?.firstOrNull()?.text, + thumbnail = content + .thumbnail + ).takeIf { it.info?.endpoint?.videoId != null } +} + +fun Innertube.VideoItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.VideoItem? { + val (mainRuns, otherRuns) = content.runs + + return Innertube.VideoItem( + info = mainRuns + .firstOrNull() + ?.let(Innertube::Info), + authors = otherRuns + .getOrNull(otherRuns.lastIndex - 2) + ?.map(Innertube::Info), + viewsText = otherRuns + .getOrNull(otherRuns.lastIndex - 1) + ?.firstOrNull() + ?.text, + durationText = otherRuns + .getOrNull(otherRuns.lastIndex) + ?.firstOrNull() + ?.text, + thumbnail = content + .thumbnail + ).takeIf { it.info?.endpoint?.videoId != null } +} + +fun Innertube.AlbumItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.AlbumItem? { + val (mainRuns, otherRuns) = content.runs + + return Innertube.AlbumItem( + info = Innertube.Info( + name = mainRuns + .firstOrNull() + ?.text, + endpoint = content + .musicResponsiveListItemRenderer + ?.navigationEndpoint + ?.browseEndpoint + ), + authors = otherRuns + .getOrNull(otherRuns.lastIndex - 1) + ?.map(Innertube::Info), + year = otherRuns + .getOrNull(otherRuns.lastIndex) + ?.firstOrNull() + ?.text, + thumbnail = content + .thumbnail + ).takeIf { it.info?.endpoint?.browseId != null } +} + +fun Innertube.ArtistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.ArtistItem? { + val (mainRuns, otherRuns) = content.runs + + return Innertube.ArtistItem( + info = Innertube.Info( + name = mainRuns + .firstOrNull() + ?.text, + endpoint = content + .musicResponsiveListItemRenderer + ?.navigationEndpoint + ?.browseEndpoint + ), + subscribersCountText = otherRuns + .lastOrNull() + ?.last() + ?.text, + thumbnail = content + .thumbnail + ).takeIf { it.info?.endpoint?.browseId != null } +} + +fun Innertube.PlaylistItem.Companion.from(content: MusicShelfRenderer.Content): Innertube.PlaylistItem? { + val (mainRuns, otherRuns) = content.runs + + return Innertube.PlaylistItem( + info = Innertube.Info( + name = mainRuns + .firstOrNull() + ?.text, + endpoint = content + .musicResponsiveListItemRenderer + ?.navigationEndpoint + ?.browseEndpoint + ), + channel = otherRuns + .firstOrNull() + ?.firstOrNull() + ?.let(Innertube::Info), + songCount = otherRuns + .lastOrNull() + ?.firstOrNull() + ?.text + ?.split(' ') + ?.firstOrNull() + ?.toIntOrNull(), + thumbnail = content + .thumbnail + ).takeIf { it.info?.endpoint?.browseId != null } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicTwoRowItemRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicTwoRowItemRenderer.kt new file mode 100644 index 0000000..f693427 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromMusicTwoRowItemRenderer.kt @@ -0,0 +1,76 @@ +package it.vfsfitvnm.youtubemusic.utils + +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer + +fun Innertube.AlbumItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.AlbumItem? { + return Innertube.AlbumItem( + info = renderer + .title + ?.runs + ?.firstOrNull() + ?.let(Innertube::Info), + authors = null, + year = renderer + .subtitle + ?.runs + ?.lastOrNull() + ?.text, + thumbnail = renderer + .thumbnailRenderer + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + ).takeIf { it.info?.endpoint?.browseId != null } +} + +fun Innertube.ArtistItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.ArtistItem? { + return Innertube.ArtistItem( + info = renderer + .title + ?.runs + ?.firstOrNull() + ?.let(Innertube::Info), + subscribersCountText = renderer + .subtitle + ?.runs + ?.firstOrNull() + ?.text, + thumbnail = renderer + .thumbnailRenderer + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + ).takeIf { it.info?.endpoint?.browseId != null } +} + +fun Innertube.PlaylistItem.Companion.from(renderer: MusicTwoRowItemRenderer): Innertube.PlaylistItem? { + return Innertube.PlaylistItem( + info = renderer + .title + ?.runs + ?.firstOrNull() + ?.let(Innertube::Info), + channel = renderer + .subtitle + ?.runs + ?.getOrNull(2) + ?.let(Innertube::Info), + songCount = renderer + .subtitle + ?.runs + ?.getOrNull(4) + ?.text + ?.split(' ') + ?.firstOrNull() + ?.toIntOrNull(), + thumbnail = renderer + .thumbnailRenderer + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + ).takeIf { it.info?.endpoint?.browseId != null } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromPlaylistPanelVideoRenderer.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromPlaylistPanelVideoRenderer.kt new file mode 100644 index 0000000..d180dfe --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/FromPlaylistPanelVideoRenderer.kt @@ -0,0 +1,35 @@ +package it.vfsfitvnm.youtubemusic.utils + +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.PlaylistPanelVideoRenderer + +fun Innertube.SongItem.Companion.from(renderer: PlaylistPanelVideoRenderer): Innertube.SongItem? { + return Innertube.SongItem( + info = Innertube.Info( + name = renderer + .title + ?.text, + endpoint = renderer + .navigationEndpoint + ?.watchEndpoint + ), + authors = renderer + .longBylineText + ?.splitBySeparator() + ?.getOrNull(0) + ?.map(Innertube::Info), + album = renderer + .longBylineText + ?.splitBySeparator() + ?.getOrNull(1) + ?.getOrNull(0) + ?.let(Innertube::Info), + thumbnail = renderer + .thumbnail + ?.thumbnails + ?.getOrNull(0), + durationText = renderer + .lengthText + ?.text + ).takeIf { it.info?.endpoint?.videoId != null } +} diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt new file mode 100644 index 0000000..ac128e1 --- /dev/null +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt @@ -0,0 +1,44 @@ +package it.vfsfitvnm.youtubemusic.utils + +import io.ktor.utils.io.CancellationException +import it.vfsfitvnm.youtubemusic.models.SectionListRenderer + +internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? { + return contents?.find { content -> + val title = content + .musicCarouselShelfRenderer + ?.header + ?.musicCarouselShelfBasicHeaderRenderer + ?.title + ?: content + .musicShelfRenderer + ?.title + + title + ?.runs + ?.firstOrNull() + ?.text == text + } +} + +internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? { + return contents?.find { content -> + content + .musicCarouselShelfRenderer + ?.header + ?.musicCarouselShelfBasicHeaderRenderer + ?.strapline + ?.runs + ?.firstOrNull() + ?.text == text + } +} + +internal inline fun runCatchingNonCancellable(block: () -> R): Result? { + return Result.success(block()) +// val result = runCatching(block) +// return when (val ex = result.exceptionOrNull()) { +// is CancellationException -> null +// else -> result +// } +} diff --git a/youtube-music/src/test/kotlin/Test.kt b/innertube/src/test/kotlin/Test.kt similarity index 100% rename from youtube-music/src/test/kotlin/Test.kt rename to innertube/src/test/kotlin/Test.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 176589f..0d266d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,6 +62,6 @@ rootProject.name = "ViMusic" include(":app") include(":compose-routing") include(":compose-reordering") -include(":youtube-music") +include(":innertube") include(":ktor-client-brotli") include(":kugou") diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Result.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Result.kt deleted file mode 100644 index 8524ce7..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/Result.kt +++ /dev/null @@ -1,10 +0,0 @@ -package it.vfsfitvnm.youtubemusic - -import io.ktor.utils.io.CancellationException - -internal fun Result.recoverIfCancelled(): Result? { - return when (exceptionOrNull()) { - is CancellationException -> null - else -> this - } -} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt deleted file mode 100644 index c72c669..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ /dev/null @@ -1,1350 +0,0 @@ -package it.vfsfitvnm.youtubemusic - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -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.get -import io.ktor.client.request.parameter -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType -import io.ktor.http.Url -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import it.vfsfitvnm.youtubemusic.models.BrowseResponse -import it.vfsfitvnm.youtubemusic.models.ContinuationResponse -import it.vfsfitvnm.youtubemusic.models.GetQueueResponse -import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse -import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer -import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer -import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer -import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer -import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint -import it.vfsfitvnm.youtubemusic.models.NextResponse -import it.vfsfitvnm.youtubemusic.models.PlayerResponse -import it.vfsfitvnm.youtubemusic.models.Runs -import it.vfsfitvnm.youtubemusic.models.SearchResponse -import it.vfsfitvnm.youtubemusic.models.SectionListRenderer -import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json - -@OptIn(ExperimentalSerializationApi::class) -object YouTube { - private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - - private val client = HttpClient(OkHttp) { - BrowserUserAgent() - - expectSuccess = true - - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = true - }) - } - - install(ContentEncoding) { - brotli() - } - - defaultRequest { - url("https://music.youtube.com") - } - } - - @Serializable - data class EmptyBody( - val context: Context, - ) - - @Serializable - data class BrowseBody( - val context: Context, - val browseId: String, - val params: String? = null, - ) - - @Serializable - data class SearchBody( - val context: Context, - val query: String, - val params: String - ) - - @Serializable - data class PlayerBody( - val context: Context, - val videoId: String, - val playlistId: String? - ) - - @Serializable - data class GetQueueBody( - val context: Context, - val videoIds: List?, - val playlistId: String?, - ) - - @Serializable - data class NextBody( - val context: Context, - val isAudioOnly: Boolean, - val videoId: String?, - val playlistId: String?, - val tunerSettingValue: String, - val index: Int?, - val params: String?, - val playlistSetVideoId: String?, - val continuation: String?, - val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs - ) { - @Serializable - data class WatchEndpointMusicSupportedConfigs( - val musicVideoType: String - ) - } - - @Serializable - data class GetSearchSuggestionsBody( - val context: Context, - val input: String - ) - - @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 gl: String = "US", - 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, - ) - ) - } - } - - data class Info( - val name: String?, - val endpoint: T? - ) { - @Suppress("UNCHECKED_CAST") - constructor(run: Runs.Run) : this( - name = run.text, - endpoint = run.navigationEndpoint?.endpoint as T? - ) - } - - sealed class Item { - abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - abstract val key: String - - data class Song( - val info: Info?, - val authors: List>?, - val album: Info?, - val durationText: String?, - override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - ) : Item() { - override val key: String - get() = info!!.endpoint!!.videoId!! - - companion object { - val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D") - - fun from(content: MusicShelfRenderer.Content): Song? { - val (mainRuns, otherRuns) = content.runs - - // Possible configurations: - // "song" • author(s) • album • duration - // "song" • author(s) • duration - // author(s) • album • duration - // author(s) • duration - - val album: Info? = otherRuns - .getOrNull(otherRuns.lastIndex - 1) - ?.firstOrNull() - ?.takeIf { run -> - run - .navigationEndpoint - ?.browseEndpoint - ?.type == "MUSIC_PAGE_TYPE_ALBUM" - } - ?.let(::Info) - - return Song( - info = mainRuns - .firstOrNull() - ?.let(::Info), - authors = otherRuns - .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2) - ?.map(::Info), - album = album, - durationText = otherRuns - .lastOrNull() - ?.firstOrNull()?.text, - thumbnail = content - .thumbnail - ).takeIf { it.info?.endpoint?.videoId != null } - } - - fun from(renderer: MusicResponsiveListItemRenderer): Song? { - return Song( - info = renderer - .flexColumns - .getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.let(::Info), - authors = renderer - .flexColumns - .getOrNull(1) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.map>(::Info) - ?.takeIf(List::isNotEmpty), - durationText = renderer - .fixedColumns - ?.getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.text, - album = renderer - .flexColumns - .getOrNull(2) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.firstOrNull() - ?.let(::Info), - thumbnail = renderer - .thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.firstOrNull() - ).takeIf { it.info?.endpoint?.videoId != null } - } - } - } - - data class Video( - val info: Info?, - val authors: List>?, - val viewsText: String?, - val durationText: String?, - override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - ) : Item() { - override val key: String - 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 { - val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D") - - fun from(content: MusicShelfRenderer.Content): Video? { - val (mainRuns, otherRuns) = content.runs - - return Video( - info = mainRuns - .firstOrNull() - ?.let(::Info), - authors = otherRuns - .getOrNull(otherRuns.lastIndex - 2) - ?.map(::Info), - viewsText = otherRuns - .getOrNull(otherRuns.lastIndex - 1) - ?.firstOrNull() - ?.text, - durationText = otherRuns - .getOrNull(otherRuns.lastIndex) - ?.firstOrNull() - ?.text, - thumbnail = content - .thumbnail - ).takeIf { it.info?.endpoint?.videoId != null } - } - } - } - - data class Album( - val info: Info?, - val authors: List>?, - val year: String?, - override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - ) : Item() { - override val key: String - get() = info!!.endpoint!!.browseId!! - - companion object { - val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D") - - fun from(content: MusicShelfRenderer.Content): Album? { - val (mainRuns, otherRuns) = content.runs - - return Album( - info = Info( - name = mainRuns - .firstOrNull() - ?.text, - endpoint = content - .musicResponsiveListItemRenderer - .navigationEndpoint - ?.browseEndpoint - ), - authors = otherRuns - .getOrNull(otherRuns.lastIndex - 1) - ?.map(::Info), - year = otherRuns - .getOrNull(otherRuns.lastIndex) - ?.firstOrNull() - ?.text, - thumbnail = content - .thumbnail - ).takeIf { it.info?.endpoint?.browseId != null } - } - - fun from(renderer: MusicTwoRowItemRenderer): Album? { - return Album( - info = renderer - .title - .runs - .firstOrNull() - ?.let(::Info), - authors = null, - year = renderer - .subtitle - .runs - .lastOrNull() - ?.text, - thumbnail = renderer - .thumbnailRenderer - .musicThumbnailRenderer - .thumbnail - .thumbnails - .firstOrNull() - ).takeIf { it.info?.endpoint?.browseId != null } - } - } - } - - data class Artist( - val info: Info?, - val subscribersCountText: String?, - override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - ) : Item() { - override val key: String - get() = info!!.endpoint!!.browseId!! - - companion object { - val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D") - - fun from(content: MusicShelfRenderer.Content): Artist? { - val (mainRuns, otherRuns) = content.runs - - return Artist( - info = Info( - name = mainRuns - .firstOrNull() - ?.text, - endpoint = content - .musicResponsiveListItemRenderer - .navigationEndpoint - ?.browseEndpoint - ), - subscribersCountText = otherRuns - .lastOrNull() - ?.last() - ?.text, - thumbnail = content - .thumbnail - ).takeIf { it.info?.endpoint?.browseId != null } - } - - fun from(renderer: MusicTwoRowItemRenderer): Artist? { - return Artist( - info = renderer - .title - .runs - .firstOrNull() - ?.let(::Info), - subscribersCountText = renderer - .subtitle - .runs - .firstOrNull() - ?.text, - thumbnail = renderer - .thumbnailRenderer - .musicThumbnailRenderer - .thumbnail - .thumbnails - .firstOrNull() - ).takeIf { it.info?.endpoint?.browseId != null } - } - } - } - - data class Playlist( - val info: Info?, - val channel: Info?, - val songCount: Int?, - override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - ) : Item() { - override val key: String - get() = info!!.endpoint!!.browseId!! - - companion object { - fun from(content: MusicShelfRenderer.Content): Playlist? { - val (mainRuns, otherRuns) = content.runs - - return Playlist( - info = Info( - name = mainRuns - .firstOrNull() - ?.text ?: "?", - endpoint = content - .musicResponsiveListItemRenderer - .navigationEndpoint - ?.browseEndpoint - ), - channel = otherRuns - .firstOrNull() - ?.firstOrNull() - ?.let(::Info), - songCount = otherRuns - .lastOrNull() - ?.firstOrNull() - ?.text - ?.split(' ') - ?.firstOrNull() - ?.toIntOrNull(), - thumbnail = content - .thumbnail - ).takeIf { it.info?.endpoint?.browseId != null } - } - - fun from(renderer: MusicTwoRowItemRenderer): Playlist? { - return Playlist( - info = renderer - .title - .runs - .firstOrNull() - ?.let(::Info), - channel = renderer - .subtitle - .runs - .getOrNull(2) - ?.let(::Info), - songCount = renderer - .subtitle - .runs - .getOrNull(4) - ?.text - ?.split(' ') - ?.firstOrNull() - ?.toIntOrNull(), - thumbnail = renderer - .thumbnailRenderer - .musicThumbnailRenderer - .thumbnail - .thumbnails - .firstOrNull() - ).takeIf { it.info?.endpoint?.browseId != null } - } - } - } - - object CommunityPlaylist { - val Filter = Filter("EgeKAQQoAEABagoQAxAEEAoQCRAF") - } - - object FeaturedPlaylist { - val Filter = Filter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D") - } - - @JvmInline - value class Filter(val value: String) - } - - class SearchResult(val items: List?, val continuation: String?) - - suspend fun search( - query: String, - filter: String, - continuation: String? - ): Result? { - return runCatching { - val musicShelfRenderer = client.post("/youtubei/v1/search") { - contentType(ContentType.Application.Json) - setBody( - SearchBody( - context = Context.DefaultWeb, - query = query, - params = filter - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - parameter("continuation", continuation) - }.let { response -> - if (continuation == null) { - response.body() - .contents - .tabbedSearchResultsRenderer - .tabs - .firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.lastOrNull() - ?.musicShelfRenderer - } else { - response.body() - .continuationContents - ?.musicShelfContinuation - } - } - SearchResult( - items = musicShelfRenderer - ?.contents - ?.mapNotNull( - when (filter) { - Item.Song.Filter.value -> Item.Song.Companion::from - Item.Album.Filter.value -> Item.Album.Companion::from - Item.Artist.Filter.value -> Item.Artist.Companion::from - Item.Video.Filter.value -> Item.Video.Companion::from - Item.CommunityPlaylist.Filter.value -> Item.Playlist.Companion::from - Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from - else -> error("Unknown filter: $filter") - } - ), - continuation = musicShelfRenderer - ?.continuations - ?.firstOrNull() - ?.nextContinuationData - ?.continuation - ) - }.recoverIfCancelled() - } - - suspend fun getSearchSuggestions(input: String): Result?>? { - return runCatching { - val body = client.post("/youtubei/v1/music/get_search_suggestions") { - contentType(ContentType.Application.Json) - setBody( - GetSearchSuggestionsBody( - context = Context.DefaultWeb, - input = input - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - body - .contents - ?.flatMap { content -> - content - .searchSuggestionsSectionRenderer - .contents.mapNotNull { - it - .searchSuggestionRenderer - .navigationEndpoint - .searchEndpoint - ?.query - } - } - }.recoverIfCancelled() - } - - suspend fun player(videoId: String, playlistId: String? = null): Result? { - return runCatching { - val playerResponse = client.post("/youtubei/v1/player") { - contentType(ContentType.Application.Json) - setBody( - PlayerBody( - context = Context.DefaultAndroid, - videoId = videoId, - playlistId = playlistId, - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - if (playerResponse.playabilityStatus.status == "OK") { - playerResponse - } else { - @Serializable - data class AudioStream( - val url: String, - val bitrate: Long - ) - - @Serializable - data class PipedResponse( - val audioStreams: List - ) - - val safePlayerResponse = client.post("/youtubei/v1/player") { - contentType(ContentType.Application.Json) - setBody( - PlayerBody( - context = Context.DefaultAgeRestrictionBypass, - videoId = videoId, - playlistId = playlistId, - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - if (safePlayerResponse.playabilityStatus.status != "OK") { - return@runCatching playerResponse - } - - val audioStreams = client.get("https://watchapi.whatever.social/streams/$videoId") { - contentType(ContentType.Application.Json) - }.body().audioStreams - - safePlayerResponse.copy( - streamingData = safePlayerResponse.streamingData?.copy( - adaptiveFormats = safePlayerResponse.streamingData.adaptiveFormats.map { adaptiveFormat -> - adaptiveFormat.copy( - url = audioStreams.find { it.bitrate == adaptiveFormat.bitrate }?.url - ) - } - ) - ) - } - }.recoverIfCancelled() - } - - private suspend fun getQueue(body: GetQueueBody): Result?>? { - return runCatching { - val response = client.post("/youtubei/v1/music/get_queue") { - contentType(ContentType.Application.Json) - setBody(body) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - response.queueDatas?.mapNotNull { queueData -> - queueData.content?.playlistPanelVideoRenderer?.let { renderer -> - Item.Song( - info = Info( - name = renderer - .title - ?.text, - endpoint = renderer - .navigationEndpoint - .watchEndpoint - ), - authors = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(0) - ?.map(::Info), - album = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(1) - ?.getOrNull(0) - ?.let(::Info), - thumbnail = renderer - .thumbnail - .thumbnails - .getOrNull(0), - durationText = renderer - .lengthText - ?.text - ).takeIf { it.info?.endpoint?.videoId != null } - } - } - }.recoverIfCancelled() - } - - suspend fun song(videoId: String): Result? { - return getQueue( - GetQueueBody( - context = Context.DefaultWeb, - videoIds = listOf(videoId), - playlistId = null - ) - )?.map { it?.firstOrNull() } - } - - suspend fun next( - videoId: String?, - playlistId: String?, - index: Int? = null, - params: String? = null, - playlistSetVideoId: String? = null, - continuation: String? = null, - ): Result? { - return runCatching { - val body = client.post("/youtubei/v1/next") { - contentType(ContentType.Application.Json) - setBody( - NextBody( - context = Context.DefaultWeb, - videoId = videoId, - playlistId = playlistId, - isAudioOnly = true, - tunerSettingValue = "AUTOMIX_SETTING_NORMAL", - watchEndpointMusicSupportedConfigs = NextBody.WatchEndpointMusicSupportedConfigs( - musicVideoType = "MUSIC_VIDEO_TYPE_ATV" - ), - index = index, - playlistSetVideoId = playlistSetVideoId, - params = params, - continuation = continuation - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - val tabs = body - .contents - .singleColumnMusicWatchNextResultsRenderer - .tabbedRenderer - .watchNextTabbedResultsRenderer - .tabs - - NextResult( - playlistId = playlistId, - playlistSetVideoId = playlistSetVideoId, - params = params, - continuation = (tabs - .getOrNull(0) - ?.tabRenderer - ?.content - ?.musicQueueRenderer - ?.content - ?: body.continuationContents) - ?.playlistPanelRenderer - ?.continuations - ?.getOrNull(0) - ?.nextContinuationData - ?.continuation, - items = (tabs - .getOrNull(0) - ?.tabRenderer - ?.content - ?.musicQueueRenderer - ?.content - ?: body.continuationContents) - ?.playlistPanelRenderer - ?.contents - ?.also { - // TODO: we should parse the MusicResponsiveListItemRenderer menu so we can - // avoid an extra network request - it.lastOrNull() - ?.automixPreviewVideoRenderer - ?.content - ?.automixPlaylistVideoRenderer - ?.navigationEndpoint - ?.watchPlaylistEndpoint - ?.let { endpoint -> - return next( - videoId = videoId, - playlistId = endpoint.playlistId, - params = endpoint.params - ) - } - } - ?.mapNotNull { it.playlistPanelVideoRenderer } - ?.mapNotNull { renderer -> - Item.Song( - info = Info( - name = renderer - .title - ?.text, - endpoint = renderer - .navigationEndpoint - .watchEndpoint - ), - authors = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(0) - ?.map(::Info), - album = renderer - .longBylineText - ?.splitBySeparator() - ?.getOrNull(1) - ?.getOrNull(0) - ?.let(::Info), - thumbnail = renderer - .thumbnail - .thumbnails - .firstOrNull(), - durationText = renderer - .lengthText - ?.text - ).takeIf { it.info?.endpoint?.videoId != null } - }, - lyricsBrowseId = tabs - .getOrNull(1) - ?.tabRenderer - ?.endpoint - ?.browseEndpoint - ?.browseId, - ) - }.recoverIfCancelled() - } - - data class NextResult( - val continuation: String?, - val playlistId: String?, - val params: String? = null, - val playlistSetVideoId: String? = null, - val items: List?, - val lyricsBrowseId: String? - ) { - suspend fun lyrics(): Result? { - return if (lyricsBrowseId == null) { - Result.success(null) - } else { - browse(lyricsBrowseId)?.map { body -> - body.contents - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicDescriptionShelfRenderer - ?.description - ?.text - } - } - } - } - - suspend fun browse(browseId: String): Result? { - return runCatching { - client.post("/youtubei/v1/browse") { - contentType(ContentType.Application.Json) - setBody( - BrowseBody( - browseId = browseId, - context = Context.DefaultWeb - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - }.recoverIfCancelled() - } - - data class ItemsResult( - val items: List?, - val continuation: String? - ) - - suspend fun items( - browseId: String, - continuation: String?, - block: (MusicResponsiveListItemRenderer) -> T? - ): Result?>? { - return runCatching { - val response = client.post("/youtubei/v1/browse") { - contentType(ContentType.Application.Json) - setBody( - BrowseBody( - browseId = browseId, - context = Context.DefaultWeb - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - parameter("continuation", continuation) - } - - if (continuation == null) { - response - .body() - .contents - ?.singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicShelfRenderer - } else { - response - .body() - .continuationContents - ?.musicShelfContinuation - }?.let { musicShelfRenderer -> - ItemsResult( - items = musicShelfRenderer - .contents - .mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - .mapNotNull(block), - continuation = musicShelfRenderer - .continuations - ?.firstOrNull() - ?.nextContinuationData - ?.continuation - ) - } - }.recoverIfCancelled() - } - - suspend fun items2( - browseId: String, - params: String?, - block: (MusicTwoRowItemRenderer) -> T? - ): Result?>? { - return runCatching { - client.post("/youtubei/v1/browse") { - contentType(ContentType.Application.Json) - setBody( - BrowseBody( - browseId = browseId, - context = Context.DefaultWeb, - params = params - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - } - .body() - .contents - ?.singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.gridRenderer - ?.let { gridRenderer -> - ItemsResult( - items = gridRenderer - .items - ?.mapNotNull(SectionListRenderer.Content.GridRenderer.Item::musicTwoRowItemRenderer) - ?.mapNotNull(block), - continuation = null - ) - } - }.recoverIfCancelled() - } - - data class PlaylistOrAlbum( - val title: String?, - val authors: List>?, - val year: String?, - val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?, - val songs: List?, - val url: String?, - val continuation: String?, - ) { - suspend fun next(): PlaylistOrAlbum { - return continuation?.let { - runCatching { - client.post("/youtubei/v1/browse") { - contentType(ContentType.Application.Json) - setBody(EmptyBody(context = Context.DefaultWeb)) - parameter("key", Key) - parameter("prettyPrint", false) - parameter("continuation", continuation) - }.body().let { continuationResponse -> - copy( - songs = songs?.plus( - continuationResponse - .continuationContents - ?.musicShelfContinuation - ?.contents - ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull(Item.Song.Companion::from) ?: emptyList() - ), - continuation = continuationResponse - .continuationContents - ?.musicShelfContinuation - ?.continuations - ?.firstOrNull() - ?.nextContinuationData - ?.continuation - ).next() - } - }.recoverIfCancelled()?.getOrNull() - } ?: this - } - } - - suspend fun album(browseId: String): Result? { - return playlistOrAlbum(browseId)?.map { album -> - album.url?.let { Url(it).parameters["list"] }?.let { playlistId -> - playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist -> - album.copy(songs = playlist.songs) - } - } ?: album - }?.map { album -> - val albumInfo = Info( - name = album.title ?: "", - endpoint = NavigationEndpoint.Endpoint.Browse( - browseId = browseId, - params = null, - browseEndpointContextSupportedConfigs = null - ) - ) - - album.copy( - songs = album.songs?.map { song -> - song.copy( - authors = song.authors ?: album.authors, - album = albumInfo, - thumbnail = album.thumbnail - ) - } - ) - } - } - - suspend fun playlist(browseId: String): Result? { - return playlistOrAlbum(browseId) - } - - private suspend fun playlistOrAlbum(browseId: String): Result? { - return browse(browseId)?.map { body -> - PlaylistOrAlbum( - title = body - .header - ?.musicDetailHeaderRenderer - ?.title - ?.text, - thumbnail = body - .header - ?.musicDetailHeaderRenderer - ?.thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.firstOrNull(), - authors = body - .header - ?.musicDetailHeaderRenderer - ?.subtitle - ?.splitBySeparator() - ?.getOrNull(1) - ?.map(::Info), - year = body - .header - ?.musicDetailHeaderRenderer - ?.subtitle - ?.splitBySeparator() - ?.getOrNull(2) - ?.firstOrNull() - ?.text, - songs = body - .contents - ?.singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicShelfRenderer - ?.contents - ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull(Item.Song.Companion::from), - url = body - .microformat - ?.microformatDataRenderer - ?.urlCanonical, - continuation = body - .contents - ?.singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicShelfRenderer - ?.continuations - ?.firstOrNull() - ?.nextContinuationData - ?.continuation - ) - } - } - - data class Artist( - val name: String?, - val description: String?, - val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?, - val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?, - val radioEndpoint: NavigationEndpoint.Endpoint.Watch?, - val songs: List?, - val songsEndpoint: NavigationEndpoint.Endpoint.Browse?, - val albums: List?, - val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?, - val singles: List?, - val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?, - ) - - suspend fun artist(browseId: String): Result? { - return browse(browseId)?.map { response -> - fun findSectionByTitle(text: String): SectionListRenderer.Content? { - return response - .contents - ?.singleColumnBrowseResultsRenderer - ?.tabs - ?.get(0) - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.find { content -> - val title = content - .musicCarouselShelfRenderer - ?.header - ?.musicCarouselShelfBasicHeaderRenderer - ?.title - ?: content - .musicShelfRenderer - ?.title - - title - ?.runs - ?.firstOrNull() - ?.text == text - } - } - - val songsSection = findSectionByTitle("Songs")?.musicShelfRenderer - val albumsSection = findSectionByTitle("Albums")?.musicCarouselShelfRenderer - val singlesSection = findSectionByTitle("Singles")?.musicCarouselShelfRenderer - - Artist( - name = response - .header - ?.musicImmersiveHeaderRenderer - ?.title - ?.text, - description = response - .header - ?.musicImmersiveHeaderRenderer - ?.description - ?.text - ?.substringBeforeLast("\n\nFrom Wikipedia"), - thumbnail = response - .header - ?.musicImmersiveHeaderRenderer - ?.thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.getOrNull(0), - shuffleEndpoint = response - .header - ?.musicImmersiveHeaderRenderer - ?.playButton - ?.buttonRenderer - ?.navigationEndpoint - ?.watchEndpoint, - radioEndpoint = response - .header - ?.musicImmersiveHeaderRenderer - ?.startRadioButton - ?.buttonRenderer - ?.navigationEndpoint - ?.watchEndpoint, - songs = songsSection - ?.contents - ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull(Item.Song::from), - songsEndpoint = songsSection - ?.bottomEndpoint - ?.browseEndpoint, - albums = albumsSection - ?.contents - ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) - ?.mapNotNull(Item.Album::from), - albumsEndpoint = albumsSection - ?.header - ?.musicCarouselShelfBasicHeaderRenderer - ?.moreContentButton - ?.buttonRenderer - ?.navigationEndpoint - ?.browseEndpoint, - singles = singlesSection - ?.contents - ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) - ?.mapNotNull(Item.Album::from), - singlesEndpoint = singlesSection - ?.header - ?.musicCarouselShelfBasicHeaderRenderer - ?.moreContentButton - ?.buttonRenderer - ?.navigationEndpoint - ?.browseEndpoint, - ) - } - } - - data class Related( - val songs: List? = null, - val playlists: List? = null, - val albums: List? = null, - val artists: List? = null, - ) - - suspend fun related(videoId: String): Result? { - return runCatching { - val body = client.post("/youtubei/v1/next") { - contentType(ContentType.Application.Json) - setBody( - NextBody( - context = Context.DefaultWeb, - videoId = videoId, - playlistId = null, - isAudioOnly = true, - tunerSettingValue = "AUTOMIX_SETTING_NORMAL", - watchEndpointMusicSupportedConfigs = NextBody.WatchEndpointMusicSupportedConfigs( - musicVideoType = "MUSIC_VIDEO_TYPE_ATV" - ), - index = 0, - playlistSetVideoId = null, - params = null, - continuation = null - ) - ) - parameter("key", Key) - parameter("prettyPrint", false) - }.body() - - body - .contents - .singleColumnMusicWatchNextResultsRenderer - .tabbedRenderer - .watchNextTabbedResultsRenderer - .tabs - .getOrNull(2) - ?.tabRenderer - ?.endpoint - ?.browseEndpoint - ?.browseId - ?.let { browseId -> - browse(browseId)?.getOrThrow()?.let { browseResponse -> - browseResponse - .contents - ?.sectionListRenderer - ?.contents - ?.mapNotNull(SectionListRenderer.Content::musicCarouselShelfRenderer) - ?.map(MusicCarouselShelfRenderer::contents) - } - }?.let { contents -> - Related( - songs = contents.find { items -> - items.firstOrNull()?.musicResponsiveListItemRenderer != null - }?.mapNotNull { content -> - Item.Song.from(content.musicResponsiveListItemRenderer!!) - }, - playlists = contents.find { items -> - items.firstOrNull() - ?.musicTwoRowItemRenderer - ?.navigationEndpoint - ?.browseEndpoint - ?.browseEndpointContextSupportedConfigs - ?.browseEndpointContextMusicConfig - ?.pageType == "MUSIC_PAGE_TYPE_PLAYLIST" - } - ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) - ?.mapNotNull(Item.Playlist.Companion::from), - albums = contents.find { items -> - items.firstOrNull() - ?.musicTwoRowItemRenderer - ?.navigationEndpoint - ?.browseEndpoint - ?.browseEndpointContextSupportedConfigs - ?.browseEndpointContextMusicConfig - ?.pageType == "MUSIC_PAGE_TYPE_ALBUM" - } - ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) - ?.mapNotNull(Item.Album.Companion::from), - artists = contents.find { items -> - items.firstOrNull() - ?.musicTwoRowItemRenderer - ?.navigationEndpoint - ?.browseEndpoint - ?.browseEndpointContextSupportedConfigs - ?.browseEndpointContextMusicConfig - ?.pageType == "MUSIC_PAGE_TYPE_ARTIST" - } - ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer) - ?.mapNotNull(Item.Artist.Companion::from), - ) - } - }.recoverIfCancelled() - } -} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicNavigationButtonRenderer.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicNavigationButtonRenderer.kt deleted file mode 100644 index 148b3d6..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicNavigationButtonRenderer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package it.vfsfitvnm.youtubemusic.models - -import kotlinx.serialization.Serializable - -@Serializable -data class MusicNavigationButtonRenderer( - val buttonText: Runs, - val solid: Solid?, - val clickCommand: ClickCommand, -) { - @Serializable - data class Solid( - val leftStripeColor: Long - ) - - @Serializable - data class ClickCommand( - val clickTrackingParams: String, - val browseEndpoint: NavigationEndpoint.Endpoint.Browse - ) -} \ No newline at end of file diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt deleted file mode 100644 index f07179b..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicTwoRowItemRenderer.kt +++ /dev/null @@ -1,13 +0,0 @@ -package it.vfsfitvnm.youtubemusic.models - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable - -@OptIn(ExperimentalSerializationApi::class) -@Serializable -data class MusicTwoRowItemRenderer( - val navigationEndpoint: NavigationEndpoint, - val thumbnailRenderer: ThumbnailRenderer, - val title: Runs, - val subtitle: Runs, -) diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt deleted file mode 100644 index 17ae0b5..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/Tabs.kt +++ /dev/null @@ -1,61 +0,0 @@ -package it.vfsfitvnm.youtubemusic.models - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames - -@OptIn(ExperimentalSerializationApi::class) -@Serializable -data class Tabs( - val tabs: List -) { - @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, - ) - } - } -} - -@OptIn(ExperimentalSerializationApi::class) -@Serializable -data class SectionListRenderer( - val contents: List, - val continuations: List? -) { - @Serializable - data class Content( - @JsonNames("musicImmersiveCarouselShelfRenderer") - val musicCarouselShelfRenderer: MusicCarouselShelfRenderer?, - @JsonNames("musicPlaylistShelfRenderer") - val musicShelfRenderer: MusicShelfRenderer?, - val gridRenderer: GridRenderer?, - val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer?, - ) { - @Serializable - data class GridRenderer( - val items: List?, - ) { - @Serializable - data class Item( - val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?, - val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? - ) - } - - @Serializable - data class MusicDescriptionShelfRenderer( - val description: Runs, - ) - } -} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt deleted file mode 100644 index 77e8135..0000000 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/ThumbnailRenderer.kt +++ /dev/null @@ -1,40 +0,0 @@ -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 - ) { - @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 - } - } - } - } - } -}