Cleaned up ytmusic parsers
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"terminal.integrated.scrollback": 1000
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user