Added album and playlist requests to Jellyfin Connection
This commit is contained in:
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@@ -161,7 +161,7 @@ declare global {
|
|||||||
name: string
|
name: string
|
||||||
type: 'playlist'
|
type: 'playlist'
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
createdBy?: {
|
createdBy?: { // Optional, in the case that a playlist is auto-generated or it's the user's playlist in which case this is unnecessary
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const jellyfinLogo = 'https://raw.githubusercontent.com/jellyfin/jellyfin-ux/556
|
|||||||
export class Jellyfin implements Connection {
|
export class Jellyfin implements Connection {
|
||||||
public readonly id: string
|
public readonly id: string
|
||||||
private readonly userId: string
|
private readonly userId: string
|
||||||
private readonly jfUserId: string
|
private readonly jellyfinUserId: string
|
||||||
private readonly serverUrl: string
|
private readonly serverUrl: string
|
||||||
private readonly accessToken: string
|
private readonly accessToken: string
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export class Jellyfin implements Connection {
|
|||||||
constructor(id: string, userId: string, jellyfinUserId: string, serverUrl: string, accessToken: string) {
|
constructor(id: string, userId: string, jellyfinUserId: string, serverUrl: string, accessToken: string) {
|
||||||
this.id = id
|
this.id = id
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.jfUserId = jellyfinUserId
|
this.jellyfinUserId = jellyfinUserId
|
||||||
this.serverUrl = serverUrl
|
this.serverUrl = serverUrl
|
||||||
this.accessToken = accessToken
|
this.accessToken = accessToken
|
||||||
|
|
||||||
@@ -22,19 +22,22 @@ export class Jellyfin implements Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getConnectionInfo() {
|
public async getConnectionInfo() {
|
||||||
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl)
|
const userUrl = new URL(`Users/${this.jellyfinUserId}`, this.serverUrl)
|
||||||
const systemUrl = new URL('System/Info', this.serverUrl)
|
const systemUrl = new URL('System/Info', this.serverUrl)
|
||||||
|
|
||||||
const userData = await fetch(userUrl, { headers: this.authHeader })
|
const getUserData = () =>
|
||||||
.then((response) => response.json() as Promise<JellyfinAPI.User>)
|
fetch(userUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => response.json() as Promise<JellyfinAPI.UserResponse>)
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
|
|
||||||
|
const getSystemData = () =>
|
||||||
|
fetch(systemUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => response.json() as Promise<JellyfinAPI.SystemResponse>)
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
const [userData, systemData] = await Promise.all([getUserData(), getSystemData()])
|
||||||
|
|
||||||
if (!userData) console.error(`Fetch to ${userUrl.toString()} failed`)
|
if (!userData) console.error(`Fetch to ${userUrl.toString()} failed`)
|
||||||
|
|
||||||
const systemData = await fetch(systemUrl, { headers: this.authHeader })
|
|
||||||
.then((response) => response.json() as Promise<JellyfinAPI.System>)
|
|
||||||
.catch(() => null)
|
|
||||||
|
|
||||||
if (!systemData) console.error(`Fetch to ${systemUrl.toString()} failed`)
|
if (!systemData) console.error(`Fetch to ${systemUrl.toString()} failed`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -43,27 +46,11 @@ export class Jellyfin implements Connection {
|
|||||||
type: 'jellyfin',
|
type: 'jellyfin',
|
||||||
serverUrl: this.serverUrl,
|
serverUrl: this.serverUrl,
|
||||||
serverName: systemData?.ServerName,
|
serverName: systemData?.ServerName,
|
||||||
jellyfinUserId: this.jfUserId,
|
jellyfinUserId: this.jellyfinUserId,
|
||||||
username: userData?.Name,
|
username: userData?.Name,
|
||||||
} satisfies ConnectionInfo
|
} satisfies ConnectionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecommendations(): Promise<(Song | Album | Artist | Playlist)[]> {
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
SortBy: 'PlayCount',
|
|
||||||
SortOrder: 'Descending',
|
|
||||||
IncludeItemTypes: 'Audio',
|
|
||||||
Recursive: 'true',
|
|
||||||
limit: '10',
|
|
||||||
})
|
|
||||||
|
|
||||||
const mostPlayedSongsURL = new URL(`/Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
|
||||||
|
|
||||||
const mostPlayed: { Items: JellyfinAPI.Song[] } = await fetch(mostPlayedSongsURL, { headers: this.authHeader }).then((response) => response.json())
|
|
||||||
|
|
||||||
return mostPlayed.Items.map((song) => this.parseSong(song))
|
|
||||||
}
|
|
||||||
|
|
||||||
public async search(searchTerm: string, filter: 'song'): Promise<Song[]>
|
public async search(searchTerm: string, filter: 'song'): Promise<Song[]>
|
||||||
public async search(searchTerm: string, filter: 'album'): Promise<Album[]>
|
public async search(searchTerm: string, filter: 'album'): Promise<Album[]>
|
||||||
public async search(searchTerm: string, filter: 'artist'): Promise<Artist[]>
|
public async search(searchTerm: string, filter: 'artist'): Promise<Artist[]>
|
||||||
@@ -78,7 +65,7 @@ export class Jellyfin implements Connection {
|
|||||||
recursive: 'true',
|
recursive: 'true',
|
||||||
})
|
})
|
||||||
|
|
||||||
const searchURL = new URL(`Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
const searchURL = new URL(`Users/${this.jellyfinUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||||
const searchResponse = await fetch(searchURL, { headers: this.authHeader })
|
const searchResponse = await fetch(searchURL, { headers: this.authHeader })
|
||||||
if (!searchResponse.ok) throw new JellyfinFetchError('Failed to search Jellyfin', searchResponse.status, searchURL.toString())
|
if (!searchResponse.ok) throw new JellyfinFetchError('Failed to search Jellyfin', searchResponse.status, searchURL.toString())
|
||||||
const searchResults = (await searchResponse.json()).Items as (JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist)[]
|
const searchResults = (await searchResponse.json()).Items as (JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist)[]
|
||||||
@@ -97,19 +84,121 @@ export class Jellyfin implements Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAudioStream = async (id: string, headers: Headers): Promise<Response> => {
|
public async getRecommendations(): Promise<(Song | Album | Artist | Playlist)[]> {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
SortBy: 'PlayCount',
|
||||||
|
SortOrder: 'Descending',
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
Recursive: 'true',
|
||||||
|
limit: '10',
|
||||||
|
})
|
||||||
|
|
||||||
|
const mostPlayedSongsURL = new URL(`/Users/${this.jellyfinUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||||
|
|
||||||
|
const mostPlayed: { Items: JellyfinAPI.Song[] } = await fetch(mostPlayedSongsURL, { headers: this.authHeader }).then((response) => response.json())
|
||||||
|
|
||||||
|
return mostPlayed.Items.map((song) => this.parseSong(song))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Figure out why seeking a jellyfin song takes so much longer than ytmusic (hls?)
|
||||||
|
public async getAudioStream(id: string, headers: Headers) {
|
||||||
const audoSearchParams = new URLSearchParams({
|
const audoSearchParams = new URLSearchParams({
|
||||||
MaxStreamingBitrate: '2000000',
|
MaxStreamingBitrate: '2000000',
|
||||||
Container: 'opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg',
|
Container: 'opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg',
|
||||||
TranscodingContainer: 'ts',
|
TranscodingContainer: 'ts',
|
||||||
TranscodingProtocol: 'hls',
|
TranscodingProtocol: 'hls',
|
||||||
AudioCodec: 'aac',
|
AudioCodec: 'aac',
|
||||||
userId: this.jfUserId,
|
userId: this.jellyfinUserId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const audioUrl = new URL(`Audio/${id}/universal?${audoSearchParams.toString()}`, this.serverUrl)
|
const audioUrl = new URL(`Audio/${id}/universal?${audoSearchParams.toString()}`, this.serverUrl)
|
||||||
|
|
||||||
return fetch(audioUrl, { headers })
|
return fetch(audioUrl, { headers: Object.assign(headers, this.authHeader) })
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAlbum(id: string) {
|
||||||
|
const albumUrl = new URL(`/Users/${this.jellyfinUserId}/Items/${id}`, this.serverUrl)
|
||||||
|
|
||||||
|
const album = await fetch(albumUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status >= 500) throw Error(`Jellyfin Server of connection ${this.id} experienced and internal server error`)
|
||||||
|
throw TypeError(`Invalid album ${id} of jellyfin connection ${this.id}`)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<JellyfinAPI.Album>
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!album) throw Error(`Failed to fetch album ${id} of jellyfin connection ${this.id}`)
|
||||||
|
|
||||||
|
return this.parseAlbum(album)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAlbumItems(id: string) {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
parentId: id,
|
||||||
|
sortBy: 'ParentIndexNumber,IndexNumber,SortName',
|
||||||
|
})
|
||||||
|
|
||||||
|
const albumItemsUrl = new URL(`/Users/${this.jellyfinUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||||
|
|
||||||
|
const albumItems = await fetch(albumItemsUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status >= 500) throw Error(`Jellyfin Server of connection ${this.id} experienced and internal server error`)
|
||||||
|
throw TypeError(`Invalid album ${id} of jellyfin connection ${this.id}`)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<{ Items: JellyfinAPI.Song[] }>
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!albumItems) throw Error(`Failed to fetch album ${id} items of jellyfin connection ${this.id}`)
|
||||||
|
|
||||||
|
return albumItems.Items.map((item) => this.parseSong(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPlaylist(id: string) {
|
||||||
|
const playlistUrl = new URL(`/Users/${this.jellyfinUserId}/Items/${id}`, this.serverUrl)
|
||||||
|
|
||||||
|
const playlist = await fetch(playlistUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status >= 500) throw Error(`Jellyfin Server of connection ${this.id} experienced and internal server error`)
|
||||||
|
throw TypeError(`Invalid playlist ${id} of jellyfin connection ${this.id}`)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<JellyfinAPI.Playlist>
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!playlist) throw Error(`Failed to fetch playlist ${id} of jellyfin connection ${this.id}`)
|
||||||
|
|
||||||
|
return this.parsePlaylist(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPlaylistItems(id: string, startIndex?: number, limit?: number) {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
parentId: id,
|
||||||
|
includeItemTypes: 'Audio',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (startIndex) searchParams.append('startIndex', startIndex.toString())
|
||||||
|
if (limit) searchParams.append('limit', limit.toString())
|
||||||
|
|
||||||
|
const playlistItemsUrl = new URL(`/Users/${this.jellyfinUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||||
|
|
||||||
|
const playlistItems = await fetch(playlistItemsUrl, { headers: this.authHeader })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status >= 500) throw Error(`Jellyfin Server of connection ${this.id} experienced and internal server error`)
|
||||||
|
throw TypeError(`Invalid playlist ${id} of jellyfin connection ${this.id}`)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<{ Items: JellyfinAPI.Song[] }>
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!playlistItems) throw Error(`Failed to fetch playlist ${id} items of jellyfin connection ${this.id}`)
|
||||||
|
|
||||||
|
return playlistItems.Items.map((item) => this.parseSong(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSong = (song: JellyfinAPI.Song): Song => {
|
private parseSong = (song: JellyfinAPI.Song): Song => {
|
||||||
@@ -128,7 +217,7 @@ export class Jellyfin implements Connection {
|
|||||||
id: song.Id,
|
id: song.Id,
|
||||||
name: song.Name,
|
name: song.Name,
|
||||||
type: 'song',
|
type: 'song',
|
||||||
duration: ticksToSeconds(song.RunTimeTicks),
|
duration: Math.floor(song.RunTimeTicks / 10000000),
|
||||||
thumbnailUrl,
|
thumbnailUrl,
|
||||||
releaseDate: song.ProductionYear ? new Date(song.ProductionYear.toString()).toISOString() : undefined,
|
releaseDate: song.ProductionYear ? new Date(song.ProductionYear.toString()).toISOString() : undefined,
|
||||||
artists,
|
artists,
|
||||||
@@ -177,7 +266,7 @@ export class Jellyfin implements Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static authenticateByName = async (username: string, password: string, serverUrl: URL, deviceId: string): Promise<JellyfinAPI.AuthData> => {
|
public static authenticateByName = async (username: string, password: string, serverUrl: URL, deviceId: string): Promise<JellyfinAPI.AuthenticationResponse> => {
|
||||||
const authUrl = new URL('/Users/AuthenticateByName', serverUrl.origin).toString()
|
const authUrl = new URL('/Users/AuthenticateByName', serverUrl.origin).toString()
|
||||||
return fetch(authUrl, {
|
return fetch(authUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -195,13 +284,11 @@ export class Jellyfin implements Connection {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) throw new JellyfinFetchError('Failed to Authenticate', 401, authUrl)
|
if (!response.ok) throw new JellyfinFetchError('Failed to Authenticate', 401, authUrl)
|
||||||
return response.json() as Promise<JellyfinAPI.AuthData>
|
return response.json() as Promise<JellyfinAPI.AuthenticationResponse>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticksToSeconds = (ticks: number): number => Math.floor(ticks / 10000)
|
|
||||||
|
|
||||||
export class JellyfinFetchError extends Error {
|
export class JellyfinFetchError extends Error {
|
||||||
public httpCode: number
|
public httpCode: number
|
||||||
public url: string
|
public url: string
|
||||||
@@ -214,20 +301,6 @@ export class JellyfinFetchError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare namespace JellyfinAPI {
|
declare namespace JellyfinAPI {
|
||||||
interface User {
|
|
||||||
Name: string
|
|
||||||
Id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthData {
|
|
||||||
User: JellyfinAPI.User
|
|
||||||
AccessToken: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface System {
|
|
||||||
ServerName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Song = {
|
type Song = {
|
||||||
Name: string
|
Name: string
|
||||||
Id: string
|
Id: string
|
||||||
@@ -269,6 +342,15 @@ declare namespace JellyfinAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Artist = {
|
||||||
|
Name: string
|
||||||
|
Id: string
|
||||||
|
Type: 'MusicArtist'
|
||||||
|
ImageTags?: {
|
||||||
|
Primary?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Playlist = {
|
type Playlist = {
|
||||||
Name: string
|
Name: string
|
||||||
Id: string
|
Id: string
|
||||||
@@ -280,12 +362,17 @@ declare namespace JellyfinAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Artist = {
|
interface UserResponse {
|
||||||
Name: string
|
Name: string
|
||||||
Id: string
|
Id: string
|
||||||
Type: 'MusicArtist'
|
|
||||||
ImageTags?: {
|
|
||||||
Primary?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthenticationResponse {
|
||||||
|
User: JellyfinAPI.UserResponse
|
||||||
|
AccessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SystemResponse {
|
||||||
|
ServerName: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
return fetch(url, { headers, method: 'POST', body: JSON.stringify(body) })
|
return fetch(url, { headers, method: 'POST', body: JSON.stringify(body) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out why this still breaks sometimes
|
// TODO: Figure out why this still breaks sometimes (Figured out one cause: "Episodes" can appear as videos)
|
||||||
public async search(searchTerm: string, filter: 'song'): Promise<Song[]>
|
public async search(searchTerm: string, filter: 'song'): Promise<Song[]>
|
||||||
public async search(searchTerm: string, filter: 'album'): Promise<Album[]>
|
public async search(searchTerm: string, filter: 'album'): Promise<Album[]>
|
||||||
public async search(searchTerm: string, filter: 'artist'): Promise<Artist[]>
|
public async search(searchTerm: string, filter: 'artist'): Promise<Artist[]>
|
||||||
@@ -187,7 +187,12 @@ export class YouTubeMusic implements Connection {
|
|||||||
parsedSearchResults.push(parseMusicCardShelfRenderer(section.musicCardShelfRenderer))
|
parsedSearchResults.push(parseMusicCardShelfRenderer(section.musicCardShelfRenderer))
|
||||||
section.musicCardShelfRenderer.contents?.forEach((item) => {
|
section.musicCardShelfRenderer.contents?.forEach((item) => {
|
||||||
if ('musicResponsiveListItemRenderer' in item) {
|
if ('musicResponsiveListItemRenderer' in item) {
|
||||||
|
try {
|
||||||
|
// ! TEMPORARY I need to rework all my parsers to be able to handle edge cases
|
||||||
parsedSearchResults.push(parseResponsiveListItemRenderer(item.musicResponsiveListItemRenderer))
|
parsedSearchResults.push(parseResponsiveListItemRenderer(item.musicResponsiveListItemRenderer))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
@@ -196,8 +201,14 @@ export class YouTubeMusic implements Connection {
|
|||||||
const sectionType = section.musicShelfRenderer.title.runs[0].text
|
const sectionType = section.musicShelfRenderer.title.runs[0].text
|
||||||
if (!goodSections.includes(sectionType)) continue
|
if (!goodSections.includes(sectionType)) continue
|
||||||
|
|
||||||
const parsedSectionContents = section.musicShelfRenderer.contents.map((item) => parseResponsiveListItemRenderer(item.musicResponsiveListItemRenderer))
|
section.musicShelfRenderer.contents.forEach((item) => {
|
||||||
parsedSearchResults.push(...parsedSectionContents)
|
try {
|
||||||
|
// ! TEMPORARY I need to rework all my parsers to be able to handle edge cases
|
||||||
|
parsedSearchResults.push(parseResponsiveListItemRenderer(item.musicResponsiveListItemRenderer))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.scrapedToMediaItems(parsedSearchResults)
|
return this.scrapedToMediaItems(parsedSearchResults)
|
||||||
@@ -208,7 +219,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out why this still breaks sometimes
|
// TODO: Figure out why this still breaks sometimes (Figured out one cause: "Episodes" can appear as videos)
|
||||||
public async getRecommendations() {
|
public async getRecommendations() {
|
||||||
const homeResponse = (await this.ytMusicv1ApiRequest({ type: 'browse', browseId: 'FEmusic_home' }).then((response) => response.json())) as InnerTube.HomeResponse
|
const homeResponse = (await this.ytMusicv1ApiRequest({ type: 'browse', browseId: 'FEmusic_home' }).then((response) => response.json())) as InnerTube.HomeResponse
|
||||||
|
|
||||||
@@ -235,7 +246,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAudioStream(id: string, headers: Headers): Promise<Response> {
|
public async getAudioStream(id: string, headers: Headers) {
|
||||||
if (!/^[a-zA-Z0-9-_]{11}$/.test(id)) throw TypeError('Invalid youtube video Id')
|
if (!/^[a-zA-Z0-9-_]{11}$/.test(id)) throw TypeError('Invalid youtube video Id')
|
||||||
|
|
||||||
// ? In the future, may want to implement the TVHTML5_SIMPLY_EMBEDDED_PLAYER client method both in order to bypass age-restrictions and just to serve as a fallback
|
// ? In the future, may want to implement the TVHTML5_SIMPLY_EMBEDDED_PLAYER client method both in order to bypass age-restrictions and just to serve as a fallback
|
||||||
@@ -688,7 +699,9 @@ function parseResponsiveListItemRenderer(listContent: InnerTube.musicResponsiveL
|
|||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
if (!id) throw TypeError('Encountered a bad responsiveListItemRenderer, potentially and "Episode or something like that"') // ! I need to rework all my parsers to be able to handle these kinds of edge cases
|
||||||
|
|
||||||
const isVideo =
|
const isVideo =
|
||||||
listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.watchEndpointMusicSupportedConfigs.watchEndpointMusicConfig.musicVideoType !==
|
listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.watchEndpointMusicSupportedConfigs.watchEndpointMusicConfig.musicVideoType !==
|
||||||
'MUSIC_VIDEO_TYPE_ATV'
|
'MUSIC_VIDEO_TYPE_ATV'
|
||||||
|
|||||||
Reference in New Issue
Block a user