Began working on ytmusic search parsers
This commit is contained in:
29
src/app.d.ts
vendored
29
src/app.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user