Skip to content

Commit

Permalink
Albums: sync both yt and uploaded
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcarter11 committed Oct 30, 2024
1 parent 6d15c12 commit b0099a1
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 64 deletions.
70 changes: 15 additions & 55 deletions innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.zionhuang.innertube.models.YouTubeClient.Companion.WEB
import com.zionhuang.innertube.models.YouTubeClient.Companion.WEB_REMIX
import com.zionhuang.innertube.models.YouTubeLocale
import com.zionhuang.innertube.models.getContinuation
import com.zionhuang.innertube.models.oddElements
import com.zionhuang.innertube.models.response.AccountMenuResponse
import com.zionhuang.innertube.models.response.BrowseResponse
import com.zionhuang.innertube.models.response.CreatePlaylistResponse
Expand All @@ -30,7 +29,6 @@ import com.zionhuang.innertube.models.response.NextResponse
import com.zionhuang.innertube.models.response.PipedResponse
import com.zionhuang.innertube.models.response.PlayerResponse
import com.zionhuang.innertube.models.response.SearchResponse
import com.zionhuang.innertube.models.splitBySeparator
import com.zionhuang.innertube.pages.AlbumPage
import com.zionhuang.innertube.pages.ArtistItemsContinuationPage
import com.zionhuang.innertube.pages.ArtistItemsPage
Expand All @@ -52,6 +50,7 @@ import com.zionhuang.innertube.pages.SearchResult
import com.zionhuang.innertube.pages.SearchSuggestionPage
import com.zionhuang.innertube.pages.SearchSummary
import com.zionhuang.innertube.pages.SearchSummaryPage
import com.zionhuang.innertube.utils.isPrivateId
import io.ktor.client.call.body
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -160,58 +159,18 @@ object YouTube {
}

suspend fun album(browseId: String): Result<AlbumPage> = runCatching {
val response = innerTube.browse(WEB_REMIX, browseId).body<BrowseResponse>()

if (response.header != null) albumOld(browseId, response)
else albumNew(browseId, response)
}

private suspend fun albumOld(browseId: String, response: BrowseResponse): AlbumPage {
val playlistId = response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')!!

val artists = response.header?.musicDetailHeaderRenderer?.subtitle?.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
}!!
val response = innerTube.browse(WEB_REMIX, browseId, setLogin = isPrivateId(browseId)).body<BrowseResponse>()

return AlbumPage(
album = AlbumItem(
browseId = browseId,
playlistId = playlistId,
title = response.header.musicDetailHeaderRenderer.title.runs?.firstOrNull()?.text!!,
artists = artists,
year = response.header.musicDetailHeaderRenderer.subtitle.runs.lastOrNull()?.text?.toIntOrNull(),
thumbnail = response.header.musicDetailHeaderRenderer.thumbnail.croppedSquareThumbnailRenderer?.getThumbnailUrl()!!
),
songs = albumSongs(playlistId).getOrNull() ?: emptyList()
)
}

private suspend fun albumNew(browseId: String, response: BrowseResponse): AlbumPage {
val header = response.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer
?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer
val playlistId = response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')!!

val artists = header?.straplineTextOne?.runs?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
}!!

return AlbumPage(
album = AlbumItem(
browseId = browseId,
playlistId = playlistId,
title = header.title.runs?.firstOrNull()?.text!!,
artists = artists,
year = header.subtitle.runs?.lastOrNull()?.text?.toIntOrNull(),
thumbnail = response.background?.musicThumbnailRenderer?.getThumbnailUrl()!!
),
songs = albumSongs(playlistId).getOrNull() ?: emptyList()
val album = AlbumItem(
browseId = browseId,
playlistId = AlbumPage.getPlaylistId(response)!!,
title = AlbumPage.getTitle(response)!!,
artists = AlbumPage.getArtists(response),
year = AlbumPage.getYear(response),
thumbnail = AlbumPage.getThumbnail(response)!!
)
val songs = AlbumPage.getSongs(response, album)
AlbumPage(album, songs)
}

suspend fun albumSongs(playlistId: String): Result<List<SongItem>> = runCatching {
Expand All @@ -224,9 +183,10 @@ object YouTube {
response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer
?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.contents

contents?.mapNotNull {
AlbumPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer)
}!!
val songs = contents?.mapNotNull {
AlbumPage.getSong(it.musicResponsiveListItemRenderer)
}
songs!!
}

suspend fun artist(browseId: String): Result<ArtistPage> = runCatching {
Expand Down
103 changes: 94 additions & 9 deletions innertube/src/main/java/com/zionhuang/innertube/pages/AlbumPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,127 @@ package com.zionhuang.innertube.pages
import com.zionhuang.innertube.models.Album
import com.zionhuang.innertube.models.AlbumItem
import com.zionhuang.innertube.models.Artist
import com.zionhuang.innertube.models.MusicResponsiveHeaderRenderer
import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer
import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer.FlexColumn
import com.zionhuang.innertube.models.Run
import com.zionhuang.innertube.models.SongItem
import com.zionhuang.innertube.models.oddElements
import com.zionhuang.innertube.models.response.BrowseResponse
import com.zionhuang.innertube.models.splitBySeparator
import com.zionhuang.innertube.utils.parseTime

data class AlbumPage(
val album: AlbumItem,
val songs: List<SongItem>,
) {
companion object {
fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {
fun getPlaylistId(response: BrowseResponse): String? {
var playlistId = response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')
if (playlistId == null)
{
playlistId = response.header?.musicDetailHeaderRenderer?.menu?.menuRenderer?.topLevelButtons?.firstOrNull()
?.buttonRenderer?.navigationEndpoint?.watchPlaylistEndpoint?.playlistId
}
return playlistId
}

fun getTitle(response: BrowseResponse): String? {
val title = getHeader(response)?.title ?: response.header?.musicDetailHeaderRenderer?.title
return title?.runs?.firstOrNull()?.text
}

fun getYear(response: BrowseResponse): Int? {
val title = getHeader(response)?.subtitle ?: response.header?.musicDetailHeaderRenderer?.subtitle
return title?.runs?.lastOrNull()?.text?.toIntOrNull()
}

fun getThumbnail(response: BrowseResponse): String? {
return response.background?.musicThumbnailRenderer?.getThumbnailUrl() ?: response.header?.musicDetailHeaderRenderer?.thumbnail
?.croppedSquareThumbnailRenderer?.getThumbnailUrl()
}

fun getArtists(response: BrowseResponse): List<Artist> {
val artists = getHeader(response)?.straplineTextOne?.runs?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
} ?: response.header?.musicDetailHeaderRenderer?.subtitle?.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
} ?: emptyList()

return artists
}

private fun getHeader(response: BrowseResponse): MusicResponsiveHeaderRenderer? {
val tabs = response.contents?.singleColumnBrowseResultsRenderer?.tabs
?: response.contents?.twoColumnBrowseResultsRenderer?.tabs
val section =
tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()
val header = section?.musicResponsiveHeaderRenderer
return header
}

fun getSongs(response: BrowseResponse, album: AlbumItem): List<SongItem> {
val tabs = response.contents?.singleColumnBrowseResultsRenderer?.tabs ?: response.contents?.twoColumnBrowseResultsRenderer?.tabs
val shelfRenderer = tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer ?:
response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer

val songs = shelfRenderer?.contents?.mapNotNull {
getSong(it.musicResponsiveListItemRenderer, album)
}
return songs ?: emptyList()
}

fun getSong(renderer: MusicResponsiveListItemRenderer, album: AlbumItem? = null): SongItem? {
return SongItem(
id = renderer.playlistItemData?.videoId ?: return null,
title = renderer.flexColumns.firstOrNull()
?.musicResponsiveListItemFlexColumnRenderer?.text?.runs
?.firstOrNull()?.text ?: return null,
artists = renderer.flexColumns.getOrNull(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.oddElements()?.map {
title = extractRuns(renderer.flexColumns, "MUSIC_VIDEO").firstOrNull()?.text ?: return null,
artists = extractRuns(renderer.flexColumns, "MUSIC_PAGE_TYPE_ARTIST").map{
Artist(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId
)
} ?: return null,
album = renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {
},
album = album?.let {
Album(it.title, it.browseId)
} ?: renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {
Album(
name = it.text,
id = it.navigationEndpoint?.browseEndpoint?.browseId!!
)
} ?: return null,
}!!,
duration = renderer.fixedColumns?.firstOrNull()
?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()
?.text?.parseTime() ?: return null,
thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,
thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: album?.thumbnail!!,
explicit = renderer.badges?.find {
it.musicInlineBadgeRenderer?.icon?.iconType == "MUSIC_EXPLICIT_BADGE"
} != null
)
}

private fun extractRuns(columns: List<FlexColumn>, typeLike: String): List<Run> {
val filteredRuns = mutableListOf<Run>()
for (column in columns) {
val runs = column.musicResponsiveListItemFlexColumnRenderer.text?.runs
?: continue

for (run in runs) {
val typeStr = run.navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType
?: run.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType
?: continue

if (typeLike in typeStr) {
filteredRuns.add(run)
}
}
}
return filteredRuns
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ fun String.parseTime(): Int? {
}
return null
}

fun isPrivateId(browseId: String): Boolean {
return browseId.contains("privately")
}

0 comments on commit b0099a1

Please sign in to comment.