Syncing Changes
This commit is contained in:
26
src/app.d.ts
vendored
26
src/app.d.ts
vendored
@@ -56,15 +56,16 @@ declare global {
|
|||||||
id: string
|
id: string
|
||||||
type: 'jellyfin' | 'youtube-music'
|
type: 'jellyfin' | 'youtube-music'
|
||||||
}
|
}
|
||||||
id: string
|
ids: {
|
||||||
|
connection: string
|
||||||
|
musicBrainz?: string
|
||||||
|
}
|
||||||
name: string
|
name: string
|
||||||
type: 'song'
|
type: 'song'
|
||||||
duration: number // Seconds
|
duration: number // Seconds
|
||||||
thumbnailUrl: string // Base/maxres url of song, any scaling for performance purposes will be handled by remoteImage endpoint
|
thumbnailUrl: string // Base/maxres url of song, any scaling for performance purposes will be handled by remoteImage endpoint
|
||||||
releaseDate: string // YYYY-MM-DD || YYYY-MM || YYYY
|
releaseDate: string // YYYY-MM-DD || YYYY-MM || YYYY
|
||||||
} & ({
|
artists?: { // Should try to order
|
||||||
isVideo: true
|
|
||||||
artists: { // Should try to order
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
profilePicture?: string
|
profilePicture?: string
|
||||||
@@ -74,21 +75,23 @@ declare global {
|
|||||||
name: string
|
name: string
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
}
|
}
|
||||||
} | {
|
uploader?: {
|
||||||
isVideo: false
|
|
||||||
uploader: {
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
profilePicture?: string
|
profilePicture?: string
|
||||||
}
|
}
|
||||||
})
|
isVideo: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type Album = {
|
type Album = {
|
||||||
connection: {
|
connection: {
|
||||||
id: string
|
id: string
|
||||||
type: 'jellyfin' | 'youtube-music'
|
type: 'jellyfin' | 'youtube-music'
|
||||||
}
|
}
|
||||||
id: string
|
ids: {
|
||||||
|
connection: string
|
||||||
|
musicBrainz?: string
|
||||||
|
}
|
||||||
name: string
|
name: string
|
||||||
type: 'album'
|
type: 'album'
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
@@ -107,7 +110,10 @@ declare global {
|
|||||||
id: string
|
id: string
|
||||||
type: 'jellyfin' | 'youtube-music'
|
type: 'jellyfin' | 'youtube-music'
|
||||||
}
|
}
|
||||||
id: string
|
ids: {
|
||||||
|
connection: string
|
||||||
|
musicBrainz?: string
|
||||||
|
}
|
||||||
name: string
|
name: string
|
||||||
type: 'artist'
|
type: 'artist'
|
||||||
profilePicture?: string
|
profilePicture?: string
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const mbApi = new MusicBrainzApi({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export class MusicBrainz {
|
export class MusicBrainz {
|
||||||
static async searchAlbum(albumName: string, albumArtists?: string[]): Promise<MusicBrainz.ReleaseSearchResult | null> {
|
static async searchRelease(albumName: string, artistNames?: string[]): Promise<MusicBrainz.ReleaseSearchResult | null> {
|
||||||
const searchResulst = await mbApi.search('release', { query: albumName, limit: 10 })
|
const searchResulst = await mbApi.search('release', { query: albumName, limit: 10 })
|
||||||
if (searchResulst.releases.length === 0) {
|
if (searchResulst.releases.length === 0) {
|
||||||
console.log(JSON.stringify('Nothing returned for ' + albumName))
|
console.log(JSON.stringify('Nothing returned for ' + albumName))
|
||||||
@@ -22,12 +22,11 @@ export class MusicBrainz {
|
|||||||
return prev.score > current.score ? prev : current
|
return prev.score > current.score ? prev : current
|
||||||
})
|
})
|
||||||
|
|
||||||
// const trackCount = bestMatch.media.reduce(accum)
|
const { id, title, date } = bestMatch
|
||||||
// bestMatch.media.forEach((mediaItem) => (trackCount += mediaItem['track-count']))
|
const trackCount = bestMatch.media.reduce((acummulator, current) => acummulator + current['track-count'], 0)
|
||||||
const artists = bestMatch['artist-credit']?.map((artist) => ({ id: artist.artist.id, name: artist.artist.name }))
|
const artists = bestMatch['artist-credit']?.map((artist) => ({ id: artist.artist.id, name: artist.artist.name }))
|
||||||
|
|
||||||
const { id, title, date } = bestMatch
|
return { id, name: title, releaseDate: date, artists, trackCount } satisfies MusicBrainz.ReleaseSearchResult
|
||||||
return { id, name: title, releaseDate: date, artists, trackCount: 0 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { google, type youtube_v3 } from 'googleapis'
|
import { google, run_v1, type youtube_v3 } from 'googleapis'
|
||||||
import ytdl from 'ytdl-core'
|
import ytdl from 'ytdl-core'
|
||||||
import { DB } from './db'
|
import { DB } from './db'
|
||||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||||
@@ -178,14 +178,14 @@ export class YouTubeMusic implements Connection {
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
const recommendations: (Song | Album | Artist | Playlist)[] = []
|
const recommendations: (InnerTube.Home.ScrapedSong | InnerTube.Home.ScrapedAlbum | InnerTube.Home.ScrapedArtist | InnerTube.Home.ScrapedPlaylist)[] = []
|
||||||
const goodSections = ['Listen again', 'Forgotten favorites', 'Quick picks', 'From your library']
|
const goodSections = ['Listen again', 'Forgotten favorites', 'Quick picks', 'From your library']
|
||||||
for (const section of contents) {
|
for (const section of contents) {
|
||||||
const sectionType = section.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text
|
const sectionType = section.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text
|
||||||
if (!goodSections.includes(sectionType)) continue
|
if (!goodSections.includes(sectionType)) continue
|
||||||
|
|
||||||
const parsedContent = section.musicCarouselShelfRenderer.contents.map((content) =>
|
const parsedContent = section.musicCarouselShelfRenderer.contents.map((content) =>
|
||||||
'musicTwoRowItemRenderer' in content ? parseTwoRowItemRenderer(this.id, content.musicTwoRowItemRenderer) : parseResponsiveListItemRenderer(this.id, content.musicResponsiveListItemRenderer),
|
'musicTwoRowItemRenderer' in content ? parseTwoRowItemRenderer(content.musicTwoRowItemRenderer) : parseResponsiveListItemRenderer(content.musicResponsiveListItemRenderer),
|
||||||
)
|
)
|
||||||
recommendations.push(...parsedContent)
|
recommendations.push(...parsedContent)
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
|
|
||||||
public async getUser(id: string): Promise<ScrapedUser> {}
|
public async getUser(id: string): Promise<ScrapedUser> {}
|
||||||
|
|
||||||
private async buildFullSongProfiles(scrapedSongs: ScrapedSong[]): Promise<Song[]> {
|
private async buildFullSongProfiles(scrapedSongs: InnerTube.Home.ScrapedSong[]): Promise<Song[]> {
|
||||||
const connection = { id: this.id, type: 'youtube-music' } satisfies Song['connection']
|
const connection = { id: this.id, type: 'youtube-music' } satisfies Song['connection']
|
||||||
|
|
||||||
const songIds = new Set<string>(),
|
const songIds = new Set<string>(),
|
||||||
@@ -295,107 +295,118 @@ export class YouTubeMusic implements Connection {
|
|||||||
return { connection, id: song.id, name: song.name, type: 'song', duration, thumbnailUrl, artists, album, isVideo: song.isVideo, uploader, releaseDate }
|
return { connection, id: song.id, name: song.name, type: 'song', duration, thumbnailUrl, artists, album, isVideo: song.isVideo, uploader, releaseDate }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async scrapedToFull(scrapedItems: (InnerTube.Home.ScrapedSong | InnerTube.Home.ScrapedAlbum | InnerTube.Home.ScrapedArtist | InnerTube.Home.ScrapedPlaylist)[]): Promise<(Song | Album | Artist | Playlist)[]> {
|
||||||
|
const connection = { id: this.id, type: 'youtube-music' } as const satisfies Album['connection']
|
||||||
|
|
||||||
|
const musicBrainzAlbumData = await Promise.all(scrapedAlbums.map((album) => ({ scrapedAlbum: album, musicBrainzData: MusicBrainz.searchAlbum(album.name) })))
|
||||||
|
|
||||||
|
musicBrainzAlbumData.forEach((album) => {
|
||||||
|
const ids: Album['ids'] = { connection: album.scrapedAlbum.id, musicBrainz: album.musicBrainzData}
|
||||||
|
|
||||||
|
const album = { connection, ids: {}} satisfies Album
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTwoRowItemRenderer(rowContent: InnerTube.musicTwoRowItemRenderer): ScrapedSong | ScrapedAlbum | ScrapedArtist | ScrapedPlaylist {
|
function parseTwoRowItemRenderer(rowContent: InnerTube.musicTwoRowItemRenderer): InnerTube.Home.ScrapedSong | InnerTube.Home.ScrapedAlbum | InnerTube.Home.ScrapedArtist | InnerTube.Home.ScrapedPlaylist {
|
||||||
const name = rowContent.title.runs[0].text
|
const name = rowContent.title.runs[0].text
|
||||||
|
|
||||||
if ('watchEndpoint' in rowContent.navigationEndpoint) {
|
if ('watchEndpoint' in rowContent.navigationEndpoint) {
|
||||||
let album: ScrapedSong['album'],
|
const id = rowContent.navigationEndpoint.watchEndpoint.videoId
|
||||||
artists: ScrapedSong['artists'] = [],
|
const isVideo = rowContent.navigationEndpoint.watchEndpoint.watchEndpointMusicSupportedConfigs.watchEndpointMusicConfig.musicVideoType === 'MUSIC_VIDEO_TYPE_UGC'
|
||||||
uploader: ScrapedSong['uploader']
|
const thumbnailUrl: InnerTube.Home.ScrapedSong['thumbnailUrl'] = isVideo ? undefined : refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||||
rowContent.menu.menuRenderer.items.forEach((menuOption) => {
|
|
||||||
|
let albumId: string | undefined
|
||||||
|
rowContent.menu?.menuRenderer.items.forEach((menuOption) => {
|
||||||
if (
|
if (
|
||||||
'menuNavigationItemRenderer' in menuOption &&
|
'menuNavigationItemRenderer' in menuOption &&
|
||||||
'browseEndpoint' in menuOption.menuNavigationItemRenderer.navigationEndpoint &&
|
'browseEndpoint' in menuOption.menuNavigationItemRenderer.navigationEndpoint &&
|
||||||
menuOption.menuNavigationItemRenderer.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM'
|
menuOption.menuNavigationItemRenderer.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM'
|
||||||
) {
|
) albumId = menuOption.menuNavigationItemRenderer.navigationEndpoint.browseEndpoint.browseId
|
||||||
album = { id: menuOption.menuNavigationItemRenderer.navigationEndpoint.browseEndpoint.browseId }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
rowContent.subtitle.runs.forEach((run) => {
|
|
||||||
if (!run.navigationEndpoint) return
|
|
||||||
|
|
||||||
const pageType = run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
|
||||||
const runData = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
|
||||||
if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
|
||||||
artists.push(runData)
|
|
||||||
} else if (pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL') {
|
|
||||||
uploader = runData
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isUserUploaded = rowContent.navigationEndpoint.watchEndpoint.watchEndpointMusicSupportedConfigs.watchEndpointMusicConfig.musicVideoType === 'MUSIC_VIDEO_TYPE_UGC'
|
const album: InnerTube.Home.ScrapedSong['album'] = albumId ? { id: albumId } : undefined
|
||||||
const thumbnailUrl: ScrapedSong['thumbnailUrl'] = isUserUploaded ? undefined : refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
|
||||||
|
|
||||||
const id = rowContent.navigationEndpoint.watchEndpoint.videoId
|
const artists = rowContent.subtitle.runs.map((run) => {
|
||||||
return { id, name, type: 'song', thumbnailUrl, album, artists, uploader, isVideo: isUserUploaded } satisfies ScrapedSong
|
if (run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||||
|
return { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||||
|
}
|
||||||
|
}).filter((value): value is { id: string, name: string } => value !== undefined)
|
||||||
|
|
||||||
|
const uploaderRun = rowContent.subtitle.runs.find((run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL')
|
||||||
|
const uploader = uploaderRun?.navigationEndpoint?.browseEndpoint.browseId ? { id: uploaderRun.navigationEndpoint.browseEndpoint.browseId, name: uploaderRun.text } : undefined
|
||||||
|
|
||||||
|
return { id, name, type: 'song', thumbnailUrl, artists, album, uploader, isVideo } satisfies InnerTube.Home.ScrapedSong
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||||
rowContent.menu.menuRenderer.items.forEach((menuItem) => {
|
|
||||||
if ('menuServiceItemRenderer' in menuItem) {
|
|
||||||
const queueTarget = menuItem.menuServiceItemRenderer.serviceEndpoint.queueAddEndpoint.queueTarget
|
|
||||||
if ('playlistId' in queueTarget) {
|
|
||||||
const thumbnailUrl = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
|
||||||
if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
|
||||||
const album = { id: queueTarget.playlistId, name, type: 'album', thumbnailUrl } satisfies ScrapedAlbum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const id = rowContent.navigationEndpoint.browseEndpoint.browseId
|
const id = rowContent.navigationEndpoint.browseEndpoint.browseId
|
||||||
const thumbnailUrl = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
const thumbnailUrl = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||||
|
|
||||||
|
const artists = rowContent.subtitle.runs.map((run) => {
|
||||||
|
if (run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||||
|
return { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||||
|
}
|
||||||
|
}).filter((value): value is { id: string, name: string } => value !== undefined)
|
||||||
|
|
||||||
|
const creatorRun = rowContent.subtitle.runs.find((run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL')!
|
||||||
|
const createdBy = { id: creatorRun.navigationEndpoint?.browseEndpoint.browseId!, name: creatorRun.text }
|
||||||
|
|
||||||
switch (pageType) {
|
switch (pageType) {
|
||||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||||
return { id, name, type: 'album', artists, thumbnailUrl } satisfies ScrapedAlbum
|
return { id, name, type: 'album', artists: artists.length > 0 ? artists : undefined, thumbnailUrl } satisfies InnerTube.Home.ScrapedAlbum
|
||||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||||
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
||||||
return { id, name, type: 'artist', profilePicture: thumbnailUrl } satisfies ScrapedArtist
|
return { id, name, type: 'artist', profilePicture: thumbnailUrl } satisfies InnerTube.Home.ScrapedArtist
|
||||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||||
return { id, name, type: 'playlist', createdBy, thumbnailUrl } satisfies ScrapedPlaylist
|
return { id, name, type: 'playlist', createdBy, thumbnailUrl } satisfies InnerTube.Home.ScrapedPlaylist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseResponsiveListItemRenderer(connection: string, listContent: InnerTube.musicResponsiveListItemRenderer): Song | Album | Artist | Playlist {
|
function parseResponsiveListItemRenderer(listContent: InnerTube.musicResponsiveListItemRenderer): InnerTube.Home.ScrapedSong | InnerTube.Home.ScrapedAlbum | InnerTube.Home.ScrapedArtist | InnerTube.Home.ScrapedPlaylist {
|
||||||
const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text
|
const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text
|
||||||
const thumbnail = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
const column1Runs = listContent.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs
|
||||||
|
|
||||||
let artists: (Song | Album)['artists'], createdBy: (Song | Playlist)['createdBy']
|
const artists = column1Runs.map((run) => {
|
||||||
for (const run of listContent.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs) {
|
if (run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||||
if (!run.navigationEndpoint) continue
|
return { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text}
|
||||||
|
|
||||||
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_USER_CHANNEL') {
|
|
||||||
createdBy = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}).filter((artist): artist is { id: string, name: string } => artist !== undefined)
|
||||||
|
|
||||||
if (!('navigationEndpoint' in listContent)) {
|
if (!('navigationEndpoint' in listContent)) {
|
||||||
const id = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId
|
const id = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId
|
||||||
const column2run = listContent.flexColumns[2]?.musicResponsiveListItemFlexColumnRenderer.text.runs?.[0]
|
const isVideo = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.watchEndpointMusicSupportedConfigs.watchEndpointMusicConfig.musicVideoType === 'MUSIC_VIDEO_TYPE_UGC'
|
||||||
let album: Song['album']
|
const thumbnailUrl = isVideo ? undefined : refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||||
if (column2run?.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
|
||||||
album = { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { connection, id, name, type: 'song', artists, album, createdBy, thumbnail } satisfies Song
|
const column2run = listContent.flexColumns[2]?.musicResponsiveListItemFlexColumnRenderer.text.runs?.[0]
|
||||||
|
const album = (() => {
|
||||||
|
if (column2run?.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
||||||
|
return { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text }
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const uploaderRun = column1Runs.find((run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL')
|
||||||
|
const uploader = uploaderRun?.navigationEndpoint?.browseEndpoint.browseId ? { id: uploaderRun.navigationEndpoint.browseEndpoint.browseId, name: uploaderRun.text } : undefined
|
||||||
|
|
||||||
|
return { id, name, type: 'song', thumbnailUrl, artists, album, uploader, isVideo } satisfies InnerTube.Home.ScrapedSong
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = listContent.navigationEndpoint.browseEndpoint.browseId
|
const id = listContent.navigationEndpoint.browseEndpoint.browseId
|
||||||
const pageType = listContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
const pageType = listContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||||
|
const thumbnailUrl = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||||
|
|
||||||
|
const creatorRun = column1Runs.find((run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL')!
|
||||||
|
const createdBy = { id: creatorRun.navigationEndpoint?.browseEndpoint.browseId!, name: creatorRun.text }
|
||||||
|
|
||||||
switch (pageType) {
|
switch (pageType) {
|
||||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
return { id, name, type: 'album', thumbnailUrl, artists: artists.length > 0 ? artists : undefined } satisfies InnerTube.Home.ScrapedAlbum
|
||||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||||
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
||||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
return { id, name, type: 'artist', profilePicture: thumbnailUrl } satisfies InnerTube.Home.ScrapedArtist
|
||||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||||
return { connection, id, name, type: 'playlist', createdBy, thumbnail } satisfies Playlist
|
return { id, name, type: 'playlist', thumbnailUrl, createdBy } satisfies InnerTube.Home.ScrapedPlaylist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,14 +512,16 @@ function formatDate(): string {
|
|||||||
// we don't really need the album's playlistId because the official youtube data API is so useless it doesn't provide anything of value that can't
|
// we don't really need the album's playlistId because the official youtube data API is so useless it doesn't provide anything of value that can't
|
||||||
// also be scraped from the browseId response.
|
// also be scraped from the browseId response.
|
||||||
|
|
||||||
type ScrapedSong = {
|
declare namespace InnerTube {
|
||||||
|
namespace Home {
|
||||||
|
type ScrapedSong = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'song'
|
type: 'song'
|
||||||
thumbnailUrl: string
|
thumbnailUrl?: string
|
||||||
artists: {
|
artists: {
|
||||||
id: string
|
id?: string
|
||||||
name?: string
|
name: string
|
||||||
}[]
|
}[]
|
||||||
album?: {
|
album?: {
|
||||||
id: string
|
id: string
|
||||||
@@ -516,52 +529,40 @@ type ScrapedSong = {
|
|||||||
}
|
}
|
||||||
uploader?: {
|
uploader?: {
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name: string
|
||||||
}
|
}
|
||||||
isVideo: boolean
|
isVideo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedAlbum = {
|
type ScrapedAlbum = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'album'
|
type: 'album'
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
artists:
|
artists?: {
|
||||||
| {
|
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name: string
|
||||||
}[]
|
}[]
|
||||||
| 'Various Artists'
|
}
|
||||||
releaseYear?: string
|
|
||||||
length?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScrapedArtist = {
|
type ScrapedArtist = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'artist'
|
type: 'artist'
|
||||||
profilePicture?: string
|
profilePicture: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedPlaylist = {
|
type ScrapedPlaylist = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'playlist'
|
type: 'playlist'
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
createdBy?: {
|
createdBy: {
|
||||||
id: string
|
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScrapedUser = {
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'user'
|
}
|
||||||
profilePicture?: string
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace InnerTube {
|
|
||||||
interface AlbumResponse {
|
interface AlbumResponse {
|
||||||
contents: {
|
contents: {
|
||||||
singleColumnBrowseResultsRenderer: {
|
singleColumnBrowseResultsRenderer: {
|
||||||
@@ -752,7 +753,6 @@ declare namespace InnerTube {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} & {
|
|
||||||
subtitle: {
|
subtitle: {
|
||||||
runs: Array<{
|
runs: Array<{
|
||||||
text: string
|
text: string
|
||||||
@@ -761,51 +761,53 @@ declare namespace InnerTube {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
navigationEndpoint:
|
||||||
|
| {
|
||||||
|
watchEndpoint: watchEndpoint
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
browseEndpoint: browseEndpoint
|
||||||
|
}
|
||||||
|
menu?: {
|
||||||
|
menuRenderer: {
|
||||||
|
items: Array<
|
||||||
|
| {
|
||||||
|
menuNavigationItemRenderer: {
|
||||||
|
text: {
|
||||||
|
runs: [
|
||||||
|
{
|
||||||
|
text: 'Go to album' | 'Go to artist' |
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
navigationEndpoint:
|
||||||
|
| {
|
||||||
|
browseEndpoint: browseEndpoint
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
watchPlaylistEndpoint: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
addToPlaylistEndpoint: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
shareEntityEndpoint: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
watchEndpoint: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
menuServiceItemRenderer: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
toggleMenuServiceItemRenderer: unknown
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// navigationEndpoint:
|
|
||||||
// | {
|
|
||||||
// watchEndpoint: watchEndpoint
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// browseEndpoint: browseEndpoint
|
|
||||||
// }
|
|
||||||
// menu?: {
|
|
||||||
// menuRenderer: {
|
|
||||||
// items: Array<
|
|
||||||
// | {
|
|
||||||
// menuNavigationItemRenderer: {
|
|
||||||
// text: {
|
|
||||||
// runs: [
|
|
||||||
// {
|
|
||||||
// text: 'Start radio' | 'Save to playlist' | 'Go to album' | 'Go to artist' | 'Share'
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// navigationEndpoint:
|
|
||||||
// | {
|
|
||||||
// watchEndpoint: watchEndpoint
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// addToPlaylistEndpoint: unknown
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// browseEndpoint: browseEndpoint
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// shareEntityEndpoint: unknown
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// menuServiceItemRenderer: unknown
|
|
||||||
// }
|
|
||||||
// | {
|
|
||||||
// toggleMenuServiceItemRenderer: unknown
|
|
||||||
// }
|
|
||||||
// >
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
type musicResponsiveListItemRenderer = {
|
type musicResponsiveListItemRenderer = {
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
|
|||||||
Reference in New Issue
Block a user