Cleaned up ytmusic parsers

This commit is contained in:
Eclypsed
2024-04-01 19:23:14 -04:00
parent 78b1f7e140
commit acb45803ac
2 changed files with 109 additions and 146 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"terminal.integrated.scrollback": 1000
}

View File

@@ -130,6 +130,7 @@ export class YouTubeMusic implements Connection {
}) })
const data: InnerTube.BrowseResponse = await response.json() const data: InnerTube.BrowseResponse = await response.json()
console.log(JSON.stringify(data))
const contents = data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents const contents = data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
const homeItems: YouTubeMusic.HomeItems = { const homeItems: YouTubeMusic.HomeItems = {
@@ -162,34 +163,25 @@ export class YouTubeMusic implements Connection {
const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection'] const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection']
const name = rowContent.title.runs[0].text const name = rowContent.title.runs[0].text
function getArtists() { let artists: (Song | Album)['artists']
const artists: (Song | Album)['artists'] = []
for (const run of rowContent.subtitle.runs) { for (const run of rowContent.subtitle.runs) {
if (run.navigationEndpoint && 'browseEndpoint' in run.navigationEndpoint) { if (!run.navigationEndpoint) continue
artists.push({ id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }) const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
} artists ? artists.push(artist) : (artists = [artist])
}
if (artists.length > 0) return artists
return undefined
} }
const thumbnail = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url) const thumbnail = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
if ('watchEndpoint' in rowContent.navigationEndpoint) { if ('watchEndpoint' in rowContent.navigationEndpoint) {
const id = rowContent.navigationEndpoint.watchEndpoint.videoId const id = rowContent.navigationEndpoint.watchEndpoint.videoId
return { connection, id, name, type: 'song', artists: getArtists(), thumbnail } satisfies Song return { connection, id, name, type: 'song', artists, thumbnail } satisfies Song
}
if ('watchPlaylistEndpoint' in rowContent.navigationEndpoint) {
const id = rowContent.navigationEndpoint.watchPlaylistEndpoint.playlistId
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
} }
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
const id = rowContent.navigationEndpoint.browseEndpoint.browseId const id = rowContent.navigationEndpoint.browseEndpoint.browseId
switch (pageType) { switch (pageType) {
case 'MUSIC_PAGE_TYPE_ALBUM': case 'MUSIC_PAGE_TYPE_ALBUM':
return { connection, id, name, type: 'album', artists: getArtists(), thumbnail } satisfies Album return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
case 'MUSIC_PAGE_TYPE_ARTIST': case 'MUSIC_PAGE_TYPE_ARTIST':
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
case 'MUSIC_PAGE_TYPE_PLAYLIST': case 'MUSIC_PAGE_TYPE_PLAYLIST':
@@ -200,55 +192,45 @@ export class YouTubeMusic implements Connection {
private parseResponsiveListItemRenderer = (listContent: InnerTube.musicResponsiveListItemRenderer): Song => { private parseResponsiveListItemRenderer = (listContent: InnerTube.musicResponsiveListItemRenderer): Song => {
const connection = { id: this.id, type: 'youtube-music' } satisfies Song['connection'] const connection = { id: this.id, type: 'youtube-music' } satisfies Song['connection']
const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text
const id = (listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint! as { watchEndpoint: InnerTube.watchEndpoint }).watchEndpoint.videoId const id = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId
const mappedArtists: Song['artists'] = [] let artists: Song['artists']
for (const run of listContent.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs) { for (const run of listContent.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs) {
if ('navigationEndpoint' in run && 'browseEndpoint' in run.navigationEndpoint!) { if (!run.navigationEndpoint) continue
mappedArtists.push({ id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }) const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
artists ? artists.push(artist) : (artists = [artist])
} }
}
const artists = mappedArtists.length > 0 ? mappedArtists : undefined
function getAlbum() { const column2run = listContent.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text.runs?.[0]
// This is like the ONE situation where `text` might not have a run const pageIsAlbum = column2run?.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM'
if (listContent.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text?.runs) { const album: Song['album'] = pageIsAlbum ? { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text } : undefined
return {
id: (listContent.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint! as { browseEndpoint: InnerTube.browseEndpoint }).browseEndpoint.browseId,
name: listContent.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
} satisfies Song['album']
}
return undefined
}
const thumbnail = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url) const thumbnail = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
return { connection, id, name, type: 'song', artists, album: getAlbum(), thumbnail } satisfies Song return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
} }
private parseMusicCardShelfRenderer = (cardContent: InnerTube.musicCardShelfRenderer): Song | Album | Artist | Playlist => { private parseMusicCardShelfRenderer = (cardContent: InnerTube.musicCardShelfRenderer): Song | Album | Artist | Playlist => {
const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection'] const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection']
const name = cardContent.title.runs[0].text const name = cardContent.title.runs[0].text
const artistsSubtitleRuns = cardContent.subtitle.runs.filter(
(run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ARTIST',
)
const artists: (Song | Album)['artists'] =
artistsSubtitleRuns.length > 0
? artistsSubtitleRuns.map((run) => {
return { id: run.navigationEndpoint!.browseEndpoint.browseId, name: run.text }
})
: undefined
const thumbnail = refineThumbnailUrl(cardContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url) const thumbnail = refineThumbnailUrl(cardContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
let album: Song['album'], artists: (Song | Album)['artists']
for (const run of cardContent.subtitle.runs) {
if (!run.navigationEndpoint) continue
const pageType = run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
artists ? artists.push(artist) : (artists = [artist])
} else if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
album = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
}
}
const navigationEndpoint = cardContent.title.runs[0].navigationEndpoint const navigationEndpoint = cardContent.title.runs[0].navigationEndpoint
if ('watchEndpoint' in navigationEndpoint) { if ('watchEndpoint' in navigationEndpoint) {
const id = navigationEndpoint.watchEndpoint.videoId const id = navigationEndpoint.watchEndpoint.videoId
const albumSubtitleRun = cardContent.subtitle.runs.find(
(run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM',
)
const album = albumSubtitleRun ? { id: albumSubtitleRun.navigationEndpoint!.browseEndpoint.browseId, name: albumSubtitleRun.text } : undefined
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
} }
@@ -272,7 +254,7 @@ const refineThumbnailUrl = (urlString: string): string => {
} else if (url.origin === 'https://lh3.googleusercontent.com' || url.origin === 'https://yt3.googleusercontent.com' || url.origin === 'https://yt3.ggpht.com') { } else if (url.origin === 'https://lh3.googleusercontent.com' || url.origin === 'https://yt3.googleusercontent.com' || url.origin === 'https://yt3.ggpht.com') {
return urlString.slice(0, urlString.indexOf('=')) return urlString.slice(0, urlString.indexOf('='))
} else { } else {
console.log(urlString) console.error(urlString)
throw new Error('Invalid thumbnail url origin') throw new Error('Invalid thumbnail url origin')
} }
} }
@@ -356,107 +338,120 @@ declare namespace InnerTube {
} }
interface BrowseResponse { interface BrowseResponse {
responseContext: {
visitorData: string
serviceTrackingParams: object[]
maxAgeSeconds: number
}
contents: { contents: {
singleColumnBrowseResultsRenderer: { singleColumnBrowseResultsRenderer: {
tabs: [ tabs: [
{ {
tabRenderer: { tabRenderer: {
endpoint: object
title: 'Home'
selected: boolean
content: { content: {
sectionListRenderer: { sectionListRenderer: {
contents: { contents: Array<{
musicCarouselShelfRenderer: musicCarouselShelfRenderer musicCarouselShelfRenderer: musicCarouselShelfRenderer
}[] }>
continuations: [object]
trackingParams: string
header: {
chipCloudRenderer: object
} }
} }
} }
icon: object
tabIdentifier: 'FEmusic_home'
trackingParams: string
}
}, },
] ]
} }
} }
trackingParams: string
maxAgeStoreSeconds: number
background: {
musicThumbnailRenderer: {
thumbnail: object
thumbnailCrop: string
thumbnailScale: string
trackingParams: string
}
}
} }
type musicCarouselShelfRenderer = { type musicCarouselShelfRenderer = {
header: { header: {
musicCarouselShelfBasicHeaderRenderer: { musicCarouselShelfBasicHeaderRenderer: {
title: { title: {
runs: [runs] runs: [
{
text: string // 'Listen again' | 'Forgotten favorites' | 'Quick picks'
},
]
} }
thumbnail?: musicThumbnailRenderer
trackingParams: string
} }
} }
contents: contents:
| { | Array<{
musicTwoRowItemRenderer: musicTwoRowItemRenderer musicTwoRowItemRenderer: musicTwoRowItemRenderer
}[] }>
| { | Array<{
musicResponsiveListItemRenderer: musicResponsiveListItemRenderer musicResponsiveListItemRenderer: musicResponsiveListItemRenderer
}[] }>
trackingParams: string
itemSize: string
} }
// type musicDescriptionShelfRenderer = {
// header: {
// runs: [runs]
// }
// description: {
// runs: [runs]
// }
// }
type musicTwoRowItemRenderer = { type musicTwoRowItemRenderer = {
thumbnailRenderer: { thumbnailRenderer: {
musicThumbnailRenderer: musicThumbnailRenderer musicThumbnailRenderer: musicThumbnailRenderer
} }
aspectRatio: string
title: { title: {
runs: [runs] runs: [
{
text: string
},
]
} }
subtitle: { subtitle: {
runs: runs[] runs: Array<{
text: string
navigationEndpoint?: {
browseEndpoint: browseEndpoint
}
}>
}
navigationEndpoint:
| {
watchEndpoint: watchEndpoint
}
| {
browseEndpoint: browseEndpoint
} }
navigationEndpoint: navigationEndpoint
trackingParams: string
menu: unknown
thumbnailOverlay: unknown
} }
type musicResponsiveListItemRenderer = { type musicResponsiveListItemRenderer = {
thumbnail: { thumbnail: {
musicThumbnailRenderer: musicThumbnailRenderer musicThumbnailRenderer: musicThumbnailRenderer
} }
flexColumns: { flexColumns: [
{
musicResponsiveListItemFlexColumnRenderer: { musicResponsiveListItemFlexColumnRenderer: {
text: { runs: [runs] } text: {
runs: [
{
text: string
navigationEndpoint: {
watchEndpoint: watchEndpoint
} }
}[] },
]
}
}
},
{
musicResponsiveListItemFlexColumnRenderer: {
text: {
runs: Array<{
text: string
navigationEndpoint?: {
browseEndpoint: browseEndpoint
}
}>
}
}
},
{
musicResponsiveListItemFlexColumnRenderer: {
text: {
runs?: [
{
text: string
navigationEndpoint: {
browseEndpoint: browseEndpoint
}
},
]
}
}
},
]
playlistItemData: { playlistItemData: {
videoId: string videoId: string
} }
@@ -470,36 +465,10 @@ declare namespace InnerTube {
height: number height: number
}> }>
} }
thumbnailCrop: string
thumbnailScale: string
trackingParams: string
accessibilityData?: accessibilityData
onTap?: navigationEndpoint
targetId?: string
} }
type runs = {
text: string
navigationEndpoint?: navigationEndpoint
}
type navigationEndpoint = {
clickTrackingParams: string
} & (
| {
browseEndpoint: browseEndpoint
}
| {
watchEndpoint: watchEndpoint
}
| {
watchPlaylistEndpoint: watchPlaylistEndpoint
}
)
type browseEndpoint = { type browseEndpoint = {
browseId: string browseId: string
params?: string
browseEndpointContextSupportedConfigs: { browseEndpointContextSupportedConfigs: {
browseEndpointContextMusicConfig: { browseEndpointContextMusicConfig: {
pageType: 'MUSIC_PAGE_TYPE_ALBUM' | 'MUSIC_PAGE_TYPE_ARTIST' | 'MUSIC_PAGE_TYPE_PLAYLIST' pageType: 'MUSIC_PAGE_TYPE_ALBUM' | 'MUSIC_PAGE_TYPE_ARTIST' | 'MUSIC_PAGE_TYPE_PLAYLIST'
@@ -509,20 +478,11 @@ declare namespace InnerTube {
type watchEndpoint = { type watchEndpoint = {
videoId: string videoId: string
params?: string
watchEndpointMusicSupportedConfigs: {
watchEndpointMusicConfig: object
}
}
type watchPlaylistEndpoint = {
playlistId: string playlistId: string
params?: string watchEndpointMusicSupportedConfigs: {
} watchEndpointMusicConfig: {
musicVideoType: 'MUSIC_VIDEO_TYPE_UGC' | 'MUSIC_VIDEO_TYPE_ATV'
type accessibilityData = { }
accessibilityData: {
label: string
} }
} }
} }