|
@@ -20,13 +20,16 @@ import it.vfsfitvnm.youtubemusic.models.BrowseResponse
|
|
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
|
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
|
import it.vfsfitvnm.youtubemusic.models.GetQueueResponse
|
|
import it.vfsfitvnm.youtubemusic.models.GetQueueResponse
|
|
import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse
|
|
import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse
|
|
|
|
+import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
|
|
|
+import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|
import it.vfsfitvnm.youtubemusic.models.NextResponse
|
|
import it.vfsfitvnm.youtubemusic.models.NextResponse
|
|
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
|
|
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
|
|
import it.vfsfitvnm.youtubemusic.models.Runs
|
|
import it.vfsfitvnm.youtubemusic.models.Runs
|
|
import it.vfsfitvnm.youtubemusic.models.SearchResponse
|
|
import it.vfsfitvnm.youtubemusic.models.SearchResponse
|
|
|
|
+import it.vfsfitvnm.youtubemusic.models.SectionListRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
|
|
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
|
|
import kotlinx.serialization.ExperimentalSerializationApi
|
|
import kotlinx.serialization.ExperimentalSerializationApi
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.Serializable
|
|
@@ -36,7 +39,7 @@ import kotlinx.serialization.json.Json
|
|
object YouTube {
|
|
object YouTube {
|
|
private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
|
private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
|
|
|
|
|
- val client = HttpClient(OkHttp) {
|
|
|
|
|
|
+ private val client = HttpClient(OkHttp) {
|
|
BrowserUserAgent()
|
|
BrowserUserAgent()
|
|
|
|
|
|
expectSuccess = true
|
|
expectSuccess = true
|
|
@@ -162,37 +165,34 @@ object YouTube {
|
|
}
|
|
}
|
|
|
|
|
|
data class Info<T : NavigationEndpoint.Endpoint>(
|
|
data class Info<T : NavigationEndpoint.Endpoint>(
|
|
- val name: String,
|
|
|
|
|
|
+ val name: String?,
|
|
val endpoint: T?
|
|
val endpoint: T?
|
|
) {
|
|
) {
|
|
- companion object {
|
|
|
|
- inline fun <reified T : NavigationEndpoint.Endpoint> from(run: Runs.Run): Info<T> {
|
|
|
|
- return Info(
|
|
|
|
- name = run.text,
|
|
|
|
- endpoint = run.navigationEndpoint?.endpoint as T?
|
|
|
|
- )
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ @Suppress("UNCHECKED_CAST")
|
|
|
|
+ constructor(run: Runs.Run) : this(
|
|
|
|
+ name = run.text,
|
|
|
|
+ endpoint = run.navigationEndpoint?.endpoint as T?
|
|
|
|
+ )
|
|
}
|
|
}
|
|
|
|
|
|
sealed class Item {
|
|
sealed class Item {
|
|
abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
- abstract val key: String?
|
|
|
|
|
|
+ abstract val key: String
|
|
|
|
|
|
data class Song(
|
|
data class Song(
|
|
- val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
|
|
|
|
|
+ val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val durationText: String?,
|
|
val durationText: String?,
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
) : Item() {
|
|
) : Item() {
|
|
- override val key: String?
|
|
|
|
- get() = info.endpoint?.videoId
|
|
|
|
|
|
+ override val key: String
|
|
|
|
+ get() = info!!.endpoint!!.videoId!!
|
|
|
|
|
|
- companion object : FromMusicShelfRendererContent<Song> {
|
|
|
|
|
|
+ companion object {
|
|
val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
|
|
val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
|
|
|
|
|
|
- override fun from(content: MusicShelfRenderer.Content): Song {
|
|
|
|
|
|
+ fun from(content: MusicShelfRenderer.Content): Song? {
|
|
val (mainRuns, otherRuns) = content.runs
|
|
val (mainRuns, otherRuns) = content.runs
|
|
|
|
|
|
// Possible configurations:
|
|
// Possible configurations:
|
|
@@ -210,21 +210,22 @@ object YouTube {
|
|
?.browseEndpoint
|
|
?.browseEndpoint
|
|
?.type == "MUSIC_PAGE_TYPE_ALBUM"
|
|
?.type == "MUSIC_PAGE_TYPE_ALBUM"
|
|
}
|
|
}
|
|
- ?.let(Info.Companion::from)
|
|
|
|
|
|
+ ?.let(::Info)
|
|
|
|
|
|
return Song(
|
|
return Song(
|
|
- info = Info.from(mainRuns.first()),
|
|
|
|
|
|
+ info = mainRuns
|
|
|
|
+ .firstOrNull()
|
|
|
|
+ ?.let(::Info),
|
|
authors = otherRuns
|
|
authors = otherRuns
|
|
.getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
|
|
.getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
|
|
- ?.map(Info.Companion::from)
|
|
|
|
- ?: emptyList(),
|
|
|
|
|
|
+ ?.map(::Info),
|
|
album = album,
|
|
album = album,
|
|
durationText = otherRuns
|
|
durationText = otherRuns
|
|
.lastOrNull()
|
|
.lastOrNull()
|
|
?.firstOrNull()?.text,
|
|
?.firstOrNull()?.text,
|
|
thumbnail = content
|
|
thumbnail = content
|
|
.thumbnail
|
|
.thumbnail
|
|
- )
|
|
|
|
|
|
+ ).takeIf { it.info?.endpoint?.videoId != null }
|
|
}
|
|
}
|
|
|
|
|
|
fun from(renderer: MusicResponsiveListItemRenderer): Song? {
|
|
fun from(renderer: MusicResponsiveListItemRenderer): Song? {
|
|
@@ -236,15 +237,15 @@ object YouTube {
|
|
?.text
|
|
?.text
|
|
?.runs
|
|
?.runs
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
- ?.let { Info.from(it) } ?: return null,
|
|
|
|
|
|
+ ?.let(::Info),
|
|
authors = renderer
|
|
authors = renderer
|
|
.flexColumns
|
|
.flexColumns
|
|
.getOrNull(1)
|
|
.getOrNull(1)
|
|
?.musicResponsiveListItemFlexColumnRenderer
|
|
?.musicResponsiveListItemFlexColumnRenderer
|
|
?.text
|
|
?.text
|
|
?.runs
|
|
?.runs
|
|
- ?.map { Info.from<NavigationEndpoint.Endpoint.Browse>(it) }
|
|
|
|
- ?.takeIf { it.isNotEmpty() },
|
|
|
|
|
|
+ ?.map<Runs.Run, Info<NavigationEndpoint.Endpoint.Browse>>(::Info)
|
|
|
|
+ ?.takeIf(List<Any>::isNotEmpty),
|
|
durationText = renderer
|
|
durationText = renderer
|
|
.fixedColumns
|
|
.fixedColumns
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
@@ -260,53 +261,55 @@ object YouTube {
|
|
?.text
|
|
?.text
|
|
?.runs
|
|
?.runs
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
- ?.let { Info.from(it) },
|
|
|
|
|
|
+ ?.let(::Info),
|
|
thumbnail = renderer
|
|
thumbnail = renderer
|
|
.thumbnail
|
|
.thumbnail
|
|
?.musicThumbnailRenderer
|
|
?.musicThumbnailRenderer
|
|
?.thumbnail
|
|
?.thumbnail
|
|
?.thumbnails
|
|
?.thumbnails
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
- )
|
|
|
|
|
|
+ ).takeIf { it.info?.endpoint?.videoId != null }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
data class Video(
|
|
data class Video(
|
|
- val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
|
|
|
|
|
+ val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val viewsText: String?,
|
|
val viewsText: String?,
|
|
val durationText: String?,
|
|
val durationText: String?,
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
) : Item() {
|
|
) : Item() {
|
|
- override val key: String?
|
|
|
|
- get() = info.endpoint?.videoId
|
|
|
|
|
|
+ override val key: String
|
|
|
|
+ get() = info!!.endpoint!!.videoId!!
|
|
|
|
|
|
val isOfficialMusicVideo: Boolean
|
|
val isOfficialMusicVideo: Boolean
|
|
get() = info
|
|
get() = info
|
|
- .endpoint
|
|
|
|
|
|
+ ?.endpoint
|
|
?.watchEndpointMusicSupportedConfigs
|
|
?.watchEndpointMusicSupportedConfigs
|
|
?.watchEndpointMusicConfig
|
|
?.watchEndpointMusicConfig
|
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
|
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
|
|
|
|
|
|
val isUserGeneratedContent: Boolean
|
|
val isUserGeneratedContent: Boolean
|
|
get() = info
|
|
get() = info
|
|
- .endpoint
|
|
|
|
|
|
+ ?.endpoint
|
|
?.watchEndpointMusicSupportedConfigs
|
|
?.watchEndpointMusicSupportedConfigs
|
|
?.watchEndpointMusicConfig
|
|
?.watchEndpointMusicConfig
|
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
|
|
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
|
|
|
|
|
|
- companion object : FromMusicShelfRendererContent<Video> {
|
|
|
|
|
|
+ companion object {
|
|
val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
|
|
val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
|
|
|
|
|
|
- override fun from(content: MusicShelfRenderer.Content): Video {
|
|
|
|
|
|
+ fun from(content: MusicShelfRenderer.Content): Video? {
|
|
val (mainRuns, otherRuns) = content.runs
|
|
val (mainRuns, otherRuns) = content.runs
|
|
|
|
|
|
return Video(
|
|
return Video(
|
|
- info = Info.from(mainRuns.first()),
|
|
|
|
|
|
+ info = mainRuns
|
|
|
|
+ .firstOrNull()
|
|
|
|
+ ?.let(::Info),
|
|
authors = otherRuns
|
|
authors = otherRuns
|
|
.getOrNull(otherRuns.lastIndex - 2)
|
|
.getOrNull(otherRuns.lastIndex - 2)
|
|
- ?.map(Info.Companion::from),
|
|
|
|
|
|
+ ?.map(::Info),
|
|
viewsText = otherRuns
|
|
viewsText = otherRuns
|
|
.getOrNull(otherRuns.lastIndex - 1)
|
|
.getOrNull(otherRuns.lastIndex - 1)
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
@@ -317,31 +320,31 @@ object YouTube {
|
|
?.text,
|
|
?.text,
|
|
thumbnail = content
|
|
thumbnail = content
|
|
.thumbnail
|
|
.thumbnail
|
|
- )
|
|
|
|
|
|
+ ).takeIf { it.info?.endpoint?.videoId != null }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
data class Album(
|
|
data class Album(
|
|
- val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
|
|
|
|
|
+ val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
val year: String?,
|
|
val year: String?,
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
) : Item() {
|
|
) : Item() {
|
|
- override val key: String?
|
|
|
|
- get() = info.endpoint?.browseId
|
|
|
|
|
|
+ override val key: String
|
|
|
|
+ get() = info!!.endpoint!!.browseId!!
|
|
|
|
|
|
- companion object : FromMusicShelfRendererContent<Album> {
|
|
|
|
|
|
+ companion object {
|
|
val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
|
|
val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
|
|
|
|
|
|
- override fun from(content: MusicShelfRenderer.Content): Album {
|
|
|
|
|
|
+ fun from(content: MusicShelfRenderer.Content): Album? {
|
|
val (mainRuns, otherRuns) = content.runs
|
|
val (mainRuns, otherRuns) = content.runs
|
|
|
|
|
|
return Album(
|
|
return Album(
|
|
info = Info(
|
|
info = Info(
|
|
name = mainRuns
|
|
name = mainRuns
|
|
- .first()
|
|
|
|
- .text,
|
|
|
|
|
|
+ .firstOrNull()
|
|
|
|
+ ?.text,
|
|
endpoint = content
|
|
endpoint = content
|
|
.musicResponsiveListItemRenderer
|
|
.musicResponsiveListItemRenderer
|
|
.navigationEndpoint
|
|
.navigationEndpoint
|
|
@@ -349,37 +352,59 @@ object YouTube {
|
|
),
|
|
),
|
|
authors = otherRuns
|
|
authors = otherRuns
|
|
.getOrNull(otherRuns.lastIndex - 1)
|
|
.getOrNull(otherRuns.lastIndex - 1)
|
|
- ?.map(Info.Companion::from),
|
|
|
|
|
|
+ ?.map(::Info),
|
|
year = otherRuns
|
|
year = otherRuns
|
|
.getOrNull(otherRuns.lastIndex)
|
|
.getOrNull(otherRuns.lastIndex)
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
?.text,
|
|
?.text,
|
|
thumbnail = content
|
|
thumbnail = content
|
|
.thumbnail
|
|
.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(
|
|
data class Artist(
|
|
- val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
|
|
|
|
|
+ val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val subscribersCountText: String?,
|
|
val subscribersCountText: String?,
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
) : Item() {
|
|
) : Item() {
|
|
- override val key: String?
|
|
|
|
- get() = info.endpoint?.browseId
|
|
|
|
|
|
+ override val key: String
|
|
|
|
+ get() = info!!.endpoint!!.browseId!!
|
|
|
|
|
|
- companion object : FromMusicShelfRendererContent<Artist> {
|
|
|
|
|
|
+ companion object {
|
|
val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
|
|
val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
|
|
|
|
|
|
- override fun from(content: MusicShelfRenderer.Content): Artist {
|
|
|
|
|
|
+ fun from(content: MusicShelfRenderer.Content): Artist? {
|
|
val (mainRuns, otherRuns) = content.runs
|
|
val (mainRuns, otherRuns) = content.runs
|
|
|
|
|
|
return Artist(
|
|
return Artist(
|
|
info = Info(
|
|
info = Info(
|
|
name = mainRuns
|
|
name = mainRuns
|
|
- .first()
|
|
|
|
- .text,
|
|
|
|
|
|
+ .firstOrNull()
|
|
|
|
+ ?.text,
|
|
endpoint = content
|
|
endpoint = content
|
|
.musicResponsiveListItemRenderer
|
|
.musicResponsiveListItemRenderer
|
|
.navigationEndpoint
|
|
.navigationEndpoint
|
|
@@ -391,22 +416,43 @@ object YouTube {
|
|
?.text,
|
|
?.text,
|
|
thumbnail = content
|
|
thumbnail = content
|
|
.thumbnail
|
|
.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(
|
|
data class Playlist(
|
|
- val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
|
|
|
|
|
+ val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
val songCount: Int?,
|
|
val songCount: Int?,
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
|
) : Item() {
|
|
) : Item() {
|
|
- override val key: String?
|
|
|
|
- get() = info.endpoint?.browseId
|
|
|
|
|
|
+ override val key: String
|
|
|
|
+ get() = info!!.endpoint!!.browseId!!
|
|
|
|
|
|
- companion object : FromMusicShelfRendererContent<Playlist> {
|
|
|
|
- override fun from(content: MusicShelfRenderer.Content): Playlist {
|
|
|
|
|
|
+ companion object {
|
|
|
|
+ fun from(content: MusicShelfRenderer.Content): Playlist? {
|
|
val (mainRuns, otherRuns) = content.runs
|
|
val (mainRuns, otherRuns) = content.runs
|
|
|
|
|
|
return Playlist(
|
|
return Playlist(
|
|
@@ -422,7 +468,7 @@ object YouTube {
|
|
channel = otherRuns
|
|
channel = otherRuns
|
|
.firstOrNull()
|
|
.firstOrNull()
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
- ?.let { Info.from(it) },
|
|
|
|
|
|
+ ?.let(::Info),
|
|
songCount = otherRuns
|
|
songCount = otherRuns
|
|
.lastOrNull()
|
|
.lastOrNull()
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
@@ -432,7 +478,36 @@ object YouTube {
|
|
?.toIntOrNull(),
|
|
?.toIntOrNull(),
|
|
thumbnail = content
|
|
thumbnail = content
|
|
.thumbnail
|
|
.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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -445,15 +520,11 @@ object YouTube {
|
|
val Filter = Filter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
|
|
val Filter = Filter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
|
|
}
|
|
}
|
|
|
|
|
|
- interface FromMusicShelfRendererContent<out T : Item> {
|
|
|
|
- fun from(content: MusicShelfRenderer.Content): T
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
@JvmInline
|
|
@JvmInline
|
|
value class Filter(val value: String)
|
|
value class Filter(val value: String)
|
|
}
|
|
}
|
|
|
|
|
|
- class SearchResult(val items: List<Item>, val continuation: String?)
|
|
|
|
|
|
+ class SearchResult(val items: List<Item>?, val continuation: String?)
|
|
|
|
|
|
suspend fun search(
|
|
suspend fun search(
|
|
query: String,
|
|
query: String,
|
|
@@ -495,7 +566,7 @@ object YouTube {
|
|
SearchResult(
|
|
SearchResult(
|
|
items = musicShelfRenderer
|
|
items = musicShelfRenderer
|
|
?.contents
|
|
?.contents
|
|
- ?.map(
|
|
|
|
|
|
+ ?.mapNotNull(
|
|
when (filter) {
|
|
when (filter) {
|
|
Item.Song.Filter.value -> Item.Song.Companion::from
|
|
Item.Song.Filter.value -> Item.Song.Companion::from
|
|
Item.Album.Filter.value -> Item.Album.Companion::from
|
|
Item.Album.Filter.value -> Item.Album.Companion::from
|
|
@@ -505,7 +576,7 @@ object YouTube {
|
|
Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from
|
|
Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from
|
|
else -> error("Unknown filter: $filter")
|
|
else -> error("Unknown filter: $filter")
|
|
}
|
|
}
|
|
- ) ?: emptyList(),
|
|
|
|
|
|
+ ),
|
|
continuation = musicShelfRenderer
|
|
continuation = musicShelfRenderer
|
|
?.continuations
|
|
?.continuations
|
|
?.firstOrNull()
|
|
?.firstOrNull()
|
|
@@ -623,7 +694,7 @@ object YouTube {
|
|
info = Info(
|
|
info = Info(
|
|
name = renderer
|
|
name = renderer
|
|
.title
|
|
.title
|
|
- ?.text ?: return@let null,
|
|
|
|
|
|
+ ?.text,
|
|
endpoint = renderer
|
|
endpoint = renderer
|
|
.navigationEndpoint
|
|
.navigationEndpoint
|
|
.watchEndpoint
|
|
.watchEndpoint
|
|
@@ -632,14 +703,13 @@ object YouTube {
|
|
.longBylineText
|
|
.longBylineText
|
|
?.splitBySeparator()
|
|
?.splitBySeparator()
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
- ?.map { Info.from(it) }
|
|
|
|
- ?: emptyList(),
|
|
|
|
|
|
+ ?.map(::Info),
|
|
album = renderer
|
|
album = renderer
|
|
.longBylineText
|
|
.longBylineText
|
|
?.splitBySeparator()
|
|
?.splitBySeparator()
|
|
?.getOrNull(1)
|
|
?.getOrNull(1)
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
- ?.let { Info.from(it) },
|
|
|
|
|
|
+ ?.let(::Info),
|
|
thumbnail = renderer
|
|
thumbnail = renderer
|
|
.thumbnail
|
|
.thumbnail
|
|
.thumbnails
|
|
.thumbnails
|
|
@@ -647,7 +717,7 @@ object YouTube {
|
|
durationText = renderer
|
|
durationText = renderer
|
|
.lengthText
|
|
.lengthText
|
|
?.text
|
|
?.text
|
|
- )
|
|
|
|
|
|
+ ).takeIf { it.info?.endpoint?.videoId != null }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}.recoverIfCancelled()
|
|
}.recoverIfCancelled()
|
|
@@ -663,16 +733,6 @@ object YouTube {
|
|
)?.map { it?.firstOrNull() }
|
|
)?.map { it?.firstOrNull() }
|
|
}
|
|
}
|
|
|
|
|
|
- suspend fun queue(playlistId: String): Result<List<Item.Song>?>? {
|
|
|
|
- return getQueue(
|
|
|
|
- GetQueueBody(
|
|
|
|
- context = Context.DefaultWeb,
|
|
|
|
- videoIds = null,
|
|
|
|
- playlistId = playlistId
|
|
|
|
- )
|
|
|
|
- )
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
suspend fun next(
|
|
suspend fun next(
|
|
videoId: String?,
|
|
videoId: String?,
|
|
playlistId: String?,
|
|
playlistId: String?,
|
|
@@ -759,7 +819,7 @@ object YouTube {
|
|
info = Info(
|
|
info = Info(
|
|
name = renderer
|
|
name = renderer
|
|
.title
|
|
.title
|
|
- ?.text ?: return@mapNotNull null,
|
|
|
|
|
|
+ ?.text,
|
|
endpoint = renderer
|
|
endpoint = renderer
|
|
.navigationEndpoint
|
|
.navigationEndpoint
|
|
.watchEndpoint
|
|
.watchEndpoint
|
|
@@ -768,14 +828,13 @@ object YouTube {
|
|
.longBylineText
|
|
.longBylineText
|
|
?.splitBySeparator()
|
|
?.splitBySeparator()
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
- ?.map { run -> Info.from(run) }
|
|
|
|
- ?: emptyList(),
|
|
|
|
|
|
+ ?.map(::Info),
|
|
album = renderer
|
|
album = renderer
|
|
.longBylineText
|
|
.longBylineText
|
|
?.splitBySeparator()
|
|
?.splitBySeparator()
|
|
?.getOrNull(1)
|
|
?.getOrNull(1)
|
|
?.getOrNull(0)
|
|
?.getOrNull(0)
|
|
- ?.let { run -> Info.from(run) },
|
|
|
|
|
|
+ ?.let(::Info),
|
|
thumbnail = renderer
|
|
thumbnail = renderer
|
|
.thumbnail
|
|
.thumbnail
|
|
.thumbnails
|
|
.thumbnails
|
|
@@ -783,24 +842,14 @@ object YouTube {
|
|
durationText = renderer
|
|
durationText = renderer
|
|
.lengthText
|
|
.lengthText
|
|
?.text
|
|
?.text
|
|
- )
|
|
|
|
|
|
+ ).takeIf { it.info?.endpoint?.videoId != null }
|
|
},
|
|
},
|
|
- lyrics = NextResult.Lyrics(
|
|
|
|
- browseId = tabs
|
|
|
|
- .getOrNull(1)
|
|
|
|
- ?.tabRenderer
|
|
|
|
- ?.endpoint
|
|
|
|
- ?.browseEndpoint
|
|
|
|
- ?.browseId
|
|
|
|
- ),
|
|
|
|
- related = NextResult.Related(
|
|
|
|
- browseId = tabs
|
|
|
|
- .getOrNull(2)
|
|
|
|
- ?.tabRenderer
|
|
|
|
- ?.endpoint
|
|
|
|
- ?.browseEndpoint
|
|
|
|
- ?.browseId
|
|
|
|
- )
|
|
|
|
|
|
+ lyricsBrowseId = tabs
|
|
|
|
+ .getOrNull(1)
|
|
|
|
+ ?.tabRenderer
|
|
|
|
+ ?.endpoint
|
|
|
|
+ ?.browseEndpoint
|
|
|
|
+ ?.browseId,
|
|
)
|
|
)
|
|
}.recoverIfCancelled()
|
|
}.recoverIfCancelled()
|
|
}
|
|
}
|
|
@@ -811,32 +860,23 @@ object YouTube {
|
|
val params: String? = null,
|
|
val params: String? = null,
|
|
val playlistSetVideoId: String? = null,
|
|
val playlistSetVideoId: String? = null,
|
|
val items: List<Item.Song>?,
|
|
val items: List<Item.Song>?,
|
|
- val lyrics: Lyrics?,
|
|
|
|
- val related: Related?,
|
|
|
|
|
|
+ val lyricsBrowseId: String?
|
|
) {
|
|
) {
|
|
- class Lyrics(
|
|
|
|
- val browseId: String?,
|
|
|
|
- ) {
|
|
|
|
- suspend fun text(): Result<String?>? {
|
|
|
|
- return if (browseId == null) {
|
|
|
|
- Result.success(null)
|
|
|
|
- } else {
|
|
|
|
- browse(browseId)?.map { body ->
|
|
|
|
- body.contents
|
|
|
|
- .sectionListRenderer
|
|
|
|
- ?.contents
|
|
|
|
- ?.first()
|
|
|
|
- ?.musicDescriptionShelfRenderer
|
|
|
|
- ?.description
|
|
|
|
- ?.text
|
|
|
|
- }
|
|
|
|
|
|
+ suspend fun lyrics(): Result<String?>? {
|
|
|
|
+ return if (lyricsBrowseId == null) {
|
|
|
|
+ Result.success(null)
|
|
|
|
+ } else {
|
|
|
|
+ browse(lyricsBrowseId)?.map { body ->
|
|
|
|
+ body.contents
|
|
|
|
+ .sectionListRenderer
|
|
|
|
+ ?.contents
|
|
|
|
+ ?.first()
|
|
|
|
+ ?.musicDescriptionShelfRenderer
|
|
|
|
+ ?.description
|
|
|
|
+ ?.text
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
- class Related(
|
|
|
|
- val browseId: String?,
|
|
|
|
- )
|
|
|
|
}
|
|
}
|
|
|
|
|
|
suspend fun browse(browseId: String): Result<BrowseResponse>? {
|
|
suspend fun browse(browseId: String): Result<BrowseResponse>? {
|
|
@@ -875,12 +915,14 @@ object YouTube {
|
|
parameter("continuation", continuation)
|
|
parameter("continuation", continuation)
|
|
}.body<ContinuationResponse>().let { continuationResponse ->
|
|
}.body<ContinuationResponse>().let { continuationResponse ->
|
|
copy(
|
|
copy(
|
|
- songs = songs?.plus(continuationResponse
|
|
|
|
- .continuationContents
|
|
|
|
- .musicShelfContinuation
|
|
|
|
- ?.contents
|
|
|
|
- ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
|
|
|
- ?.mapNotNull(Item.Song.Companion::from) ?: emptyList()),
|
|
|
|
|
|
+ songs = songs?.plus(
|
|
|
|
+ continuationResponse
|
|
|
|
+ .continuationContents
|
|
|
|
+ .musicShelfContinuation
|
|
|
|
+ ?.contents
|
|
|
|
+ ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
|
|
|
+ ?.mapNotNull(Item.Song.Companion::from) ?: emptyList()
|
|
|
|
+ ),
|
|
continuation = continuationResponse
|
|
continuation = continuationResponse
|
|
.continuationContents
|
|
.continuationContents
|
|
.musicShelfContinuation
|
|
.musicShelfContinuation
|
|
@@ -897,7 +939,7 @@ object YouTube {
|
|
|
|
|
|
suspend fun album(browseId: String): Result<PlaylistOrAlbum>? {
|
|
suspend fun album(browseId: String): Result<PlaylistOrAlbum>? {
|
|
return playlistOrAlbum(browseId)?.map { album ->
|
|
return playlistOrAlbum(browseId)?.map { album ->
|
|
- album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
|
|
|
|
|
|
+ album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
|
|
playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
|
|
playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
|
|
album.copy(songs = playlist.songs)
|
|
album.copy(songs = playlist.songs)
|
|
}
|
|
}
|
|
@@ -950,7 +992,7 @@ object YouTube {
|
|
?.subtitle
|
|
?.subtitle
|
|
?.splitBySeparator()
|
|
?.splitBySeparator()
|
|
?.getOrNull(1)
|
|
?.getOrNull(1)
|
|
- ?.map { Info.from(it) },
|
|
|
|
|
|
+ ?.map(::Info),
|
|
year = body
|
|
year = body
|
|
.header
|
|
.header
|
|
?.musicDetailHeaderRenderer
|
|
?.musicDetailHeaderRenderer
|
|
@@ -972,9 +1014,7 @@ object YouTube {
|
|
?.musicShelfRenderer
|
|
?.musicShelfRenderer
|
|
?.contents
|
|
?.contents
|
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
|
- ?.mapNotNull(Item.Song.Companion::from)
|
|
|
|
-// ?.filter { it.info.endpoint != null }
|
|
|
|
- ,
|
|
|
|
|
|
+ ?.mapNotNull(Item.Song.Companion::from),
|
|
url = body
|
|
url = body
|
|
.microformat
|
|
.microformat
|
|
?.microformatDataRenderer
|
|
?.microformatDataRenderer
|
|
@@ -999,7 +1039,7 @@ object YouTube {
|
|
}
|
|
}
|
|
|
|
|
|
data class Artist(
|
|
data class Artist(
|
|
- val name: String,
|
|
|
|
|
|
+ val name: String?,
|
|
val description: String?,
|
|
val description: String?,
|
|
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
|
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
|
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
|
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
|
@@ -1013,7 +1053,7 @@ object YouTube {
|
|
.header
|
|
.header
|
|
?.musicImmersiveHeaderRenderer
|
|
?.musicImmersiveHeaderRenderer
|
|
?.title
|
|
?.title
|
|
- ?.text ?: "Unknown",
|
|
|
|
|
|
+ ?.text,
|
|
description = body
|
|
description = body
|
|
.header
|
|
.header
|
|
?.musicImmersiveHeaderRenderer
|
|
?.musicImmersiveHeaderRenderer
|
|
@@ -1045,4 +1085,100 @@ object YouTube {
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ data class Related(
|
|
|
|
+ val songs: List<Item.Song>? = null,
|
|
|
|
+ val playlists: List<Item.Playlist>? = null,
|
|
|
|
+ val albums: List<Item.Album>? = null,
|
|
|
|
+ val artists: List<Item.Artist>? = null,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ suspend fun related(videoId: String): Result<Related?>? {
|
|
|
|
+ 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<NextResponse>()
|
|
|
|
+
|
|
|
|
+ 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()
|
|
|
|
+ }
|
|
}
|
|
}
|