Began working on ytmusic search parsers

This commit is contained in:
Eclypsed
2024-03-31 01:44:48 -04:00
parent a624f375e4
commit dc18005a60
3 changed files with 191 additions and 39 deletions

29
src/app.d.ts vendored
View File

@@ -44,7 +44,7 @@ declare global {
public id: string public id: string
getRecommendations: () => Promise<(Song | Album | Playlist)[]> getRecommendations: () => Promise<(Song | Album | Playlist)[]>
getConnectionInfo: () => Promise<ConnectionInfo> getConnectionInfo: () => Promise<ConnectionInfo>
search: (searchTerm: string) => Promise<(Song | Album | Artist | Playlist)[]> search: (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist') => Promise<(Song | Album | Artist | Playlist)[]>
} }
// These Schemas should only contain general info data that is necessary for data fetching purposes. // These Schemas should only contain general info data that is necessary for data fetching purposes.
// They are NOT meant to be stores for large amounts of data, i.e. Don't include the data for every single song the Playlist type. // They are NOT meant to be stores for large amounts of data, i.e. Don't include the data for every single song the Playlist type.
@@ -53,7 +53,7 @@ declare global {
type Song = { type Song = {
connection: { connection: {
id: string id: string
type: serviceType type: 'jellyfin' | 'youtube-music'
} }
id: string id: string
name: string name: string
@@ -76,7 +76,7 @@ declare global {
type Album = { type Album = {
connection: { connection: {
id: string id: string
type: serviceType type: 'jellyfin' | 'youtube-music'
} }
id: string id: string
name: string name: string
@@ -91,7 +91,23 @@ declare global {
releaseDate?: string releaseDate?: string
} }
// Need to figure out how to do Artists, maybe just query MusicBrainz?
type Artist = {
connection: {
id: string
type: 'jellyfin' | 'youtube-music'
}
id: string
name: string
type: 'artist'
thumbnail?: string
}
type Playlist = { type Playlist = {
connection: {
id: string
type: 'jellyfin' | 'youtube-music'
}
id: string id: string
name: string name: string
type: 'playlist' type: 'playlist'
@@ -99,13 +115,6 @@ declare global {
description?: string description?: string
} }
type Artist = {
id: string
name: string
type: 'artist'
thumbnail?: string
}
namespace Jellyfin { namespace Jellyfin {
// The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will // The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will
// retrun the ServerName, it wont. This must be fetched from /System/Info. // retrun the ServerName, it wont. This must be fetched from /System/Info.

View File

@@ -69,7 +69,7 @@ export class Jellyfin implements Connection {
return Array.from(mostPlayedData.Items as JellyfinAPI.Song[], (song) => this.parseSong(song)) return Array.from(mostPlayedData.Items as JellyfinAPI.Song[], (song) => this.parseSong(song))
} }
public search = async (searchTerm: string): Promise<(Song | Album | Playlist)[]> => { public search = async (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist'): Promise<(Song | Album | Playlist)[]> => {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
searchTerm, searchTerm,
includeItemTypes: 'Audio,MusicAlbum,Playlist', // Potentially add MusicArtist includeItemTypes: 'Audio,MusicAlbum,Playlist', // Potentially add MusicArtist
@@ -153,6 +153,10 @@ export class Jellyfin implements Connection {
const thumbnail = playlist.ImageTags?.Primary ? new URL(`Items/${playlist.Id}/Images/Primary`, this.serverUrl).toString() : undefined const thumbnail = playlist.ImageTags?.Primary ? new URL(`Items/${playlist.Id}/Images/Primary`, this.serverUrl).toString() : undefined
return { return {
connection: {
id: this.id,
type: 'jellyfin',
},
id: playlist.Id, id: playlist.Id,
name: playlist.Name, name: playlist.Name,
type: 'playlist', type: 'playlist',

View File

@@ -74,26 +74,39 @@ export class YouTubeMusic implements Connection {
return listenAgain.concat(quickPicks) return listenAgain.concat(quickPicks)
} }
public search = async (searchTerm: string): Promise<(Song | Album | Playlist)[]> => { public search = async (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist'): Promise<(Song | Album | Artist | Playlist)[]> => {
const headers = Object.assign(this.BASEHEADERS, { authorization: `Bearer ${(await this.getTokens()).accessToken}`, 'X-Goog-Request-Time': `${Date.now()}` }) const headers = Object.assign(this.BASEHEADERS, { authorization: `Bearer ${(await this.getTokens()).accessToken}`, 'X-Goog-Request-Time': `${Date.now()}` })
// const response = await fetch(`https://music.youtube.com/youtubei/v1/search`, { // Figure out how to handle Library and Uploads
// headers, // Depending on how I want to handle the playlist & library sync feature
// method: 'POST',
// body: JSON.stringify({
// query: searchTerm,
// context: {
// client: {
// clientName: 'WEB_REMIX',
// clientVersion: `1.${formatDate()}.01.00`,
// hl: 'en',
// },
// },
// }),
// })
// const data = await response.json() const searchParams = {
// console.log(JSON.stringify(data)) song: 'EgWKAQIIAWoMEA4QChADEAQQCRAF',
album: 'EgWKAQIYAWoMEA4QChADEAQQCRAF',
artist: 'EgWKAQIgAWoMEA4QChADEAQQCRAF',
playlist: 'Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D',
}
const searchResulsts: InnerTube.SearchResponse = await fetch(`https://music.youtube.com/youtubei/v1/search`, {
headers,
method: 'POST',
body: JSON.stringify({
query: searchTerm,
params: filter ? searchParams[filter] : undefined,
context: {
client: {
clientName: 'WEB_REMIX',
clientVersion: `1.${formatDate()}.01.00`,
hl: 'en',
},
},
}),
}).then((response) => response.json())
console.log(JSON.stringify(searchResulsts))
const contents = searchResulsts.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
const parsedSearchResults: (Song | Album | Artist | Playlist)[] = []
return [] return []
} }
@@ -230,6 +243,76 @@ export class YouTubeMusic implements Connection {
return parsedContent return parsedContent
} }
private parseMusicCardShelfRenderer = (cardContent: InnerTube.musicCardShelfRenderer): Song | Album | Artist | Playlist => {
const navigationEndpoint = cardContent.title.runs[0].navigationEndpoint
const artists: Song['artists'] | Album['artists'] = []
cardContent.subtitle.runs.forEach((run) => {
if (run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
artists.push({ id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text })
}
})
const thumbnail = refineThumbnailUrl(cardContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
if ('watchEndpoint' in navigationEndpoint) {
const albumSubtitleRun = cardContent.subtitle.runs.find(
(run) => run.navigationEndpoint?.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM',
)
const song: Song = {
connection: {
id: this.id,
type: 'youtube-music',
},
id: navigationEndpoint.watchEndpoint.videoId,
name: cardContent.title.runs[0].text,
type: 'song',
thumbnail,
}
if (artists.length > 0) song.artists = artists
if (albumSubtitleRun) song.album = { id: albumSubtitleRun.navigationEndpoint!.browseEndpoint.browseId, name: albumSubtitleRun.text }
return song
} else {
const pageType = navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
const album: Album = {
connection: {
id: this.id,
type: 'youtube-music',
},
id: navigationEndpoint.browseEndpoint.browseId,
name: cardContent.title.runs[0].text,
type: 'album',
thumbnail,
}
if (artists.length > 0) album.artists = artists
return album
} else if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
const artist: Artist = {
connection: {
id: this.id,
type: 'youtube-music',
},
id: navigationEndpoint.browseEndpoint.browseId,
name: cardContent.title.runs[0].text,
type: 'artist',
thumbnail,
}
return artist
} else {
// NEED TO GET A SAMPLE FOR THIS
const playlist: Playlist = {
connection: {
id: this.id,
type: 'youtube-music',
},
id: navigationEndpoint.browseEndpoint.browseId,
name: cardContent.title.runs[0].text,
type: 'playlist',
}
return playlist
}
}
}
} }
const refineThumbnailUrl = (urlString: string): string => { const refineThumbnailUrl = (urlString: string): string => {
@@ -254,10 +337,72 @@ const formatDate = (): string => {
} }
declare namespace InnerTube { declare namespace InnerTube {
type Response = BrowseResponse | OtherResponse interface SearchResponse {
contents: {
tabbedSearchResultsRenderer: {
tabs: [
{
tabRenderer: {
title: string
content: {
sectionListRenderer: {
contents: Array<
| {
musicCardShelfRenderer: musicCardShelfRenderer
}
| {
musicShelfRenderer: musicShelfRenderer
}
>
}
}
}
},
]
}
}
}
interface OtherResponse { type musicCardShelfRenderer = {
contents: object title: {
runs: [
{
text: string // Unlike musicShelfRenderer, this is the name of the top search result, be that the name of a song, album, artist, or etc.
navigationEndpoint:
| {
watchEndpoint: watchEndpoint
}
| {
browseEndpoint: browseEndpoint
}
},
]
}
subtitle: {
runs: Array<{
text: string
navigationEndpoint?: {
browseEndpoint: browseEndpoint
}
}>
}
contents?: Array<{
musicResponsiveListItemRenderer: musicResponsiveListItemRenderer
}>
thumbnail: {
musicThumbnailRenderer: musicThumbnailRenderer
}
}
type musicShelfRenderer = {
title: {
runs: Array<{
text: 'Artists' | 'Songs' | 'Videos' | 'Albums' | 'Community playlists' | 'Podcasts' | 'Episodes' | 'Profiles'
}>
}
contents: Array<{
musicResponsiveListItemRenderer: musicResponsiveListItemRenderer
}>
} }
interface BrowseResponse { interface BrowseResponse {
@@ -371,13 +516,11 @@ declare namespace InnerTube {
thumbnail: { thumbnail: {
musicThumbnailRenderer: musicThumbnailRenderer musicThumbnailRenderer: musicThumbnailRenderer
} }
overlay: unknown
flexColumns: { flexColumns: {
musicResponsiveListItemFlexColumnRenderer: { musicResponsiveListItemFlexColumnRenderer: {
text: { runs: [runs] } text: { runs: [runs] }
} }
}[] }[]
menu: unknown
playlistItemData: { playlistItemData: {
videoId: string videoId: string
} }
@@ -385,11 +528,11 @@ declare namespace InnerTube {
type musicThumbnailRenderer = { type musicThumbnailRenderer = {
thumbnail: { thumbnail: {
thumbnails: { thumbnails: Array<{
url: string url: string
width: number width: number
height: number height: number
}[] }>
} }
thumbnailCrop: string thumbnailCrop: string
thumbnailScale: string thumbnailScale: string
@@ -430,11 +573,7 @@ declare namespace InnerTube {
type watchEndpoint = { type watchEndpoint = {
videoId: string videoId: string
playlistId: string
params?: string params?: string
loggingContext: {
vssLoggingContext: object
}
watchEndpointMusicSupportedConfigs: { watchEndpointMusicSupportedConfigs: {
watchEndpointMusicConfig: object watchEndpointMusicConfig: object
} }