nodelink / src /sources /pandora.js
flameface's picture
Upload 25 files
b58c6cb verified
import config from '../../config.js'
import { debugLog, makeRequest, encodeTrack, http1makeRequest } from '../utils.js'
import searchWithDefault from './default.js'
let csrfToken = null
let authToken = null
async function init() {
debugLog('pandora', 5, { type: 1, message: 'Setting Pandora auth and CSRF token.' })
const { headers: headers } = await makeRequest('https://www.pandora.com', { method: 'HEAD' })
const csfr = headers['set-cookie']
if (!csfr[1]) return debugLog('pandora', 5, { type: 2, message: 'Failed to set CSRF token from Pandora.' })
csrfToken = { raw: csfr[1], parsed: /csrftoken=([a-f0-9]{16});/.exec(csfr[1])[1] }
const { body: token } = await makeRequest('https://www.pandora.com/api/v1/auth/anonymousLogin', {
headers: {
'Cookie': csrfToken.raw,
'Content-Type': 'application/json',
'Accept': '*/*',
'X-CsrfToken': csrfToken.parsed
},
method: 'POST'
})
if (token.errorCode === 0) return debugLog('pandora', 5, { type: 2, message: 'Failed to set auth token from Pandora.' })
authToken = token.authToken
debugLog('pandora', 5, { type: 1, message: 'Successfully set Pandora auth and CSRF token.' })
}
async function search(query) {
return new Promise(async (resolve) => {
debugLog('search', 4, { type: 1, sourceName: 'Pandora', query })
const body = {
query,
types: ['TR'],
listener: null,
start: 0,
count: config.options.maxResultsLength,
annotate: true,
searchTime: 0,
annotationRecipe: 'CLASS_OF_2019'
}
const { body: data } = await makeRequest('https://www.pandora.com/api/v3/sod/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': '*/*',
'Content-Length': JSON.stringify(body).length
},
body,
disableBodyCompression: true
})
if (data.results.length === 0) {
return {
loadType: 'empty',
data: {}
}
}
const tracks = []
let index = 0
let annotationKeys = Object.keys(data.annotations)
if (annotationKeys.length > config.options.maxResultsLength)
annotationKeys = annotationKeys.slice(0, config.options.maxResultsLength)
annotationKeys.forEach(async (key) => {
if (data.annotations[key].type === 'TR') {
const search = await searchWithDefault(`${data.annotations[key].name} ${data.annotations[key].artistName}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: data.annotations[key].artistName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: data.annotations[key].name,
uri: `https://www.pandora.com${data.annotations[key].shareableUrlPath}`,
artworkUrl: `https://content-images.p-cdn.com/${data.annotations[key].icon.artUrl}`,
isrc: data.annotations[key].isrc,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
}
if (index !== data.results.length - 1) return index++
const new_tracks = []
annotationKeys.nForEach(async (key2) => {
await tracks.nForEach((track) => {
if (track.info.title !== data.annotations[key2].name || track.info.author !== data.annotations[key2].artistName) return false
new_tracks.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
debugLog('search', 4, { type: 2, sourceName: 'Pandora', tracksLen: new_tracks.length, query })
resolve({
loadType: 'search',
data: new_tracks
})
return true
})
})
})
}
async function loadFrom(query) {
return new Promise(async (resolve) => {
const type = /^(https:\/\/www\.pandora\.com\/)((playlist)|(station)|(podcast)|(artist))\/.+/.exec(query)
debugLog('loadtracks', 4, { type: 1, loadType: type[2], sourceName: 'Pandora', query })
if (!type) {
return resolve({
loadType: 'empty',
data: {}
})
}
if (!csrfToken) {
return resolve({
loadType: 'error',
data: {
message: 'Pandora not available in current country.',
severity: 'common',
cause: 'Pandora availability'
}
})
}
let lastPart = query.split('/')
lastPart = lastPart[lastPart.length - 1]
switch (type[2]) {
case 'artist': {
const { body: trackData } = await http1makeRequest('https://www.pandora.com/api/v4/catalog/annotateObjectsSimple', {
body: {
pandoraIds: [ lastPart ],
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken,
'Content-Type': 'application/json',
},
method: 'POST',
disableBodyCompression: true
})
const keysTrackData = Object.keys(trackData)
let trackType = null
switch (trackData[keysTrackData[0]] ? trackData[keysTrackData[0]].type : 'unknown') {
case 'TR': trackType = 'track'; break
case 'AL': trackType = 'album'; break
case 'AR': trackType = 'artist'; break
default: trackType = 'unknown'; break
}
if (keysTrackData.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: trackType, sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
if (trackData.message) {
debugLog('loadtracks', 4, { type: 3, loadType: trackType, sourceName: 'Pandora', query, message: trackData.message })
return resolve({
loadType: 'error',
data: {
message: trackData.message,
severity: 'common',
cause: 'Unknown'
}
})
}
const trackId = trackData[keysTrackData[0]].pandoraId
switch (trackData[keysTrackData].type) {
case 'TR': {
const item = trackData[keysTrackData]
const search = await searchWithDefault(`${item.name} ${item.artistName}`)
if (search.loadType !== 'search')
return resolve(search)
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: item.artistName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: item.name,
uri: `https://www.pandora.com${item.shareableUrlPath}`,
artworkUrl: `https://content-images.p-cdn.com/${item.icon.artUrl}`,
isrc: item.isrc,
sourceName: 'pandora'
}
debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: 'Pandora', track, query })
resolve({
loadType: 'track',
data: {
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
}
})
break
}
case 'AL': {
const { body: data } = await http1makeRequest('https://www.pandora.com/api/v4/catalog/getDetails', {
body: {
pandoraId: trackId
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (data.errors || typeof data !== 'object') {
const errorMessage = typeof data !== 'object' ? 'Unknown error' : data.errors.map((err) => `${err.message} (${err.extensions.code})`).join('; ')
debugLog('loadtracks', 4, { type: 3, loadType: 'album', sourceName: 'Pandora', query, message: errorMessage })
return resolve({
loadType: 'error',
data: {
message: errorMessage,
severity: 'common',
cause: 'Unknown'
}
})
}
const tracks = []
let index = 0
let trackKeys = Object.keys(data.annotations)
if (trackKeys.length > config.options.maxAlbumPlaylistLength)
trackKeys = trackKeys.slice(0, config.options.maxAlbumPlaylistLength)
trackKeys.forEach(async (key) => {
const search = await searchWithDefault(`${data.annotations[key].name} ${data.annotations[key].artistName}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: data.annotations[key].artistName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: data.annotations[key].name,
uri: `https://www.pandora.com${data.annotations[key].shareableUrlPath}`,
artworkUrl: `https://content-images.p-cdn.com/${data.annotations[key].icon.artUrl}`,
isrc: data.annotations[key].isrc,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
if (index !== trackKeys.length - 1) return index++
if (tracks.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'album', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
const new_tracks = []
trackKeys.nForEach(async (key2) => {
await tracks.nForEach((track) => {
if (track.info.title !== data.annotations[key2].name || track.info.author !== data.annotations[key2].artistName) return false
new_tracks.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
debugLog('loadtracks', 4, { type: 2, loadType: 'album', sourceName: 'Pandora', playlistName: trackData[trackId].name })
resolve({
loadType: 'album',
data: {
info: {
name: trackData[trackId].name,
selectedTrack: 0,
},
pluginInfo: {},
tracks: new_tracks,
}
})
return true
})
})
break
}
case 'AR': {
const { body: data } = await http1makeRequest('https://www.pandora.com/api/v1/graphql/graphql', {
body: {
operationName: 'GetArtistDetailsWithCuratorsWeb',
query: 'query GetArtistDetailsWithCuratorsWeb($pandoraId: String!) {\n entity(id: $pandoraId) {\n ... on Artist {\n id\n type\n urlPath\n name\n trackCount\n albumCount\n bio\n canSeedStation\n stationListenerCount\n albumCount\n artistTracksId\n art {\n ...ArtFragment\n __typename\n }\n isMegastar\n headerArt {\n ...ArtFragment\n __typename\n }\n topTracksWithCollaborations {\n ...TrackFragment\n __typename\n }\n artistPlay {\n id\n __typename\n }\n events {\n externalId\n __typename\n }\n latestReleaseWithCollaborations {\n ...AlbumFragment\n __typename\n }\n topAlbumsWithCollaborations {\n ...AlbumFragment\n __typename\n }\n similarArtists {\n id\n name\n art {\n ...ArtFragment\n __typename\n }\n urlPath\n __typename\n }\n twitterHandle\n twitterUrl\n allArtistAlbums {\n totalItems\n __typename\n }\n curator {\n ...CurationFragment\n __typename\n }\n featured(types: [PL, AR, AL, TR, SF, PC, PE]) {\n ... on Playlist {\n ...PlaylistFragment\n __typename\n }\n ... on StationFactory {\n ...StationFactoryFragment\n __typename\n }\n ... on Artist {\n ...ArtistFragment\n __typename\n }\n ... on Album {\n ...AlbumFragment\n __typename\n }\n ... on Track {\n ...TrackFragment\n __typename\n }\n ... on Podcast {\n ...PodcastFragment\n __typename\n }\n ... on PodcastEpisode {\n ...PodcastEpisodeFragment\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment ArtFragment on Art {\n artId\n dominantColor\n artUrl: url(size: WIDTH_500)\n}\n\nfragment TrackFragment on Track {\n pandoraId: id\n type\n name\n sortableName\n duration\n trackNumber\n explicitness\n hasRadio: canSeedStation\n shareableUrlPath: urlPath\n modificationtime: dateModified\n slugPlusPandoraId: slugPlusId\n artistId: artist {\n pandoraId: id\n __typename\n }\n artistName: artist {\n name\n __typename\n }\n albumId: album {\n pandoraId: id\n __typename\n }\n albumName: album {\n name\n __typename\n }\n album {\n urlPath\n __typename\n }\n icon: art {\n ...ArtFragment\n __typename\n }\n rightsInfo: rights {\n ...RightsFragment\n __typename\n }\n}\n\nfragment AlbumFragment on Album {\n pandoraId: id\n type\n name\n sortableName\n duration\n trackCount\n releaseDate\n explicitness\n isCompilation\n shareableUrlPath: urlPath\n modificationTime: dateModified\n slugPlusPandoraId: slugPlusId\n artistId: artist {\n pandoraId: id\n __typename\n }\n artistName: artist {\n name\n __typename\n }\n icon: art {\n ...ArtFragment\n __typename\n }\n artist {\n url\n __typename\n }\n rightsInfo: rights {\n ...RightsFragment\n __typename\n }\n}\n\nfragment CurationFragment on Curator {\n curatedStations {\n items {\n ...StationFactoryFragment\n __typename\n }\n __typename\n }\n playlists {\n items {\n ...PlaylistFragment\n __typename\n }\n __typename\n }\n}\n\nfragment PlaylistFragment on Playlist {\n pandoraId: id\n type\n name\n sortableName\n description\n duration\n totalTracks\n version\n isEditable\n linkedId\n linkedType: origin\n shareableUrlPath: urlPath\n modificationTime: dateModified\n unlocked: isUnlocked\n autogenForListener: isOfAnyOrigin(origins: [PERSONALIZED, SHARED])\n hasVoiceTrack: includesAny(types: [AM])\n listenerIdInfo: owner {\n listenerPandoraId: id\n displayName\n isMe\n __typename\n }\n icon: art {\n ...ArtFragment\n __typename\n }\n}\n\nfragment StationFactoryFragment on StationFactory {\n pandoraId: id\n type\n name\n sortableName\n hasTakeoverModes\n isHosted\n shareableUrlPath: urlPath\n modificationTime: dateModified\n seedId: seed {\n pandoraId: id\n __typename\n }\n seedType: seed {\n type\n __typename\n }\n icon: art {\n ...ArtFragment\n __typename\n }\n listenerCount\n}\n\nfragment ArtistFragment on Artist {\n pandoraId: id\n type\n name\n sortableName\n trackCount\n collaboration: isCollaboration\n megastar: isMegastar\n shareableUrlPath: urlPath\n modificationTime: dateModified\n hasRadio: canSeedStation\n icon: art {\n ...ArtFragment\n __typename\n }\n}\n\nfragment PodcastFragment on Podcast {\n pandoraId: id\n type\n name\n sortableName\n publisherName\n ordering: releaseType\n episodeCount: totalEpisodeCount\n shareableUrlPath: urlPath\n modificationTime: dateModified\n icon: art {\n ...ArtFragment\n __typename\n }\n rightsInfo: rights {\n ...RightsFragment\n __typename\n }\n}\n\nfragment PodcastEpisodeFragment on PodcastEpisode {\n pandoraId: id\n type\n name\n sortableName\n duration\n releaseDate\n explicitness\n shareableUrlPath: urlPath\n modificationTime: dateModified\n podcastId: podcast {\n pandoraId: id\n __typename\n }\n programName: podcast {\n name\n __typename\n }\n elapsedTime: playbackProgress {\n elapsedTime\n __typename\n }\n icon: art {\n ...ArtFragment\n __typename\n }\n rightsInfo: rights {\n ...RightsFragment\n __typename\n }\n}\n\nfragment RightsFragment on Rights {\n expirationTime\n hasInteractive\n hasRadioRights\n hasOffline\n}\n',
variables: {
pandoraId: trackId
}
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (data.errors || typeof data !== 'object') {
const errorMessage = typeof data !== 'object' ? 'Unknown error' : data.errors.map((err) => `${err.message} (${err.extensions.code})`).join('; ')
debugLog('loadtracks', 4, { type: 3, loadType: 'artist', sourceName: 'Pandora', query, message: errorMessage })
return resolve({
loadType: 'error',
data: {
message: errorMessage,
severity: 'common',
cause: 'Unknown'
}
})
}
const tracks = []
let index = 0
let topTracks = data.data.entity.topTracksWithCollaborations
if (topTracks.length > config.options.maxAlbumPlaylistLength)
topTracks = topTracks.slice(0, config.options.maxAlbumPlaylistLength)
topTracks.forEach(async (pTrack) => {
const search = await searchWithDefault(`${pTrack.name} ${pTrack.artistName.name}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: pTrack.artistName.name,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: pTrack.name,
uri: `https://www.pandora.com${pTrack.shareableUrlPath}`,
artworkUrl: pTrack.icon.artUrl,
isrc: null,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
if (index !== topTracks.length - 1) return index++
if (tracks.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'artist', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
const new_tracks = []
topTracks.nForEach(async (pTrack2) => {
await tracks.nForEach((track) => {
if (track.info.title !== pTrack2.name || track.info.author !== pTrack2.artistName.name) return false
track.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
debugLog('loadtracks', 4, { type: 2, loadType: 'playlist', sourceName: 'Pandora', playlistName: data.data.entity.name })
resolve({
loadType: 'artist',
data: {
info: {
name: trackData[trackId].name,
artworkUrl: `https://content-images.p-cdn.com/${trackData[trackId].icon.artUrl}`,
},
pluginInfo: {},
tracks: new_tracks,
}
})
return true
})
})
break
}
}
break
}
case 'playlist': {
const body = {
request: {
pandoraId: lastPart,
playlistVersion: 0,
offset: 0,
limit: config.options.maxAlbumPlaylistLength,
annotationLimit: config.options.maxAlbumPlaylistLength,
allowedTypes: ['TR', 'AM'],
bypassPrivacyRules: true
}
}
const { body: data } = await makeRequest('https://www.pandora.com/api/v7/playlists/getTracks', {
method: 'POST',
headers: {
'Cookie': csrfToken.raw,
'Content-Type': 'application/json',
'Accept': '*/*',
'Content-Length': JSON.stringify(body).length,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
body,
disableBodyCompression: true
})
const tracks = []
let index = 0
let keys = Object.keys(data.annotations).filter((key) => key.indexOf('TR:') !== -1)
if (keys.length > config.options.maxAlbumPlaylistLength)
keys = keys.slice(0, config.options.maxAlbumPlaylistLength)
keys.forEach(async (key) => {
const search = await searchWithDefault(`${data.annotations[key].name} ${data.annotations[key].artistName}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: data.annotations[key].visible,
author: data.annotations[key].artistName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: data.annotations[key].name,
uri: search.data[0].info.uri,
artworkUrl: `https://content-images.p-cdn.com/${data.annotations[key].icon.artUrl}`,
isrc: data.annotations[key].isrc,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
if (index !== keys.length - 1) return index++
if (tracks.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'playlist', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
const new_tracks = []
keys.nForEach(async (key2) => {
await tracks.nForEach((track) => {
if (track.info.title !== data.annotations[key2].name || track.info.author !== data.annotations[key2].artistName) return false
new_tracks.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
debugLog('loadtracks', 4, { type: 2, loadType: 'playlist', sourceName: 'Pandora', playlistName: data.name })
resolve({
loadType: 'playlist',
data: {
info: {
name: data.name,
selectedTrack: 0,
},
pluginInfo: {},
tracks: new_tracks,
}
})
return true
})
})
break
}
case 'station': {
const { body: stationData } = await http1makeRequest('https://www.pandora.com/api/v1/station/getStationDetails', {
body: {
stationId: lastPart
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (stationData.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'station', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
if (stationData.message) {
debugLog('loadtracks', 4, { type: 3, loadType: 'station', sourceName: 'Pandora', query, message: stationData.message })
return resolve({
loadType: 'error',
data: {
message: stationData.message,
severity: 'common',
cause: 'Unknown'
}
})
}
const tracks = []
let index = 0
let seeds = stationData.seeds
if (seeds.length > config.options.maxAlbumPlaylistLength)
seeds = seeds.slice(0, config.options.maxAlbumPlaylistLength)
seeds.forEach(async (seed) => {
const search = await searchWithDefault(`${seed.song.songTitle} ${seed.song.artistSummary}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: seed.song.artistSummary,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: seed.song.songTitle,
uri: seed.song.songDetailUrl,
artworkUrl: seed.art[seed.art.length - 1].url,
isrc: null,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
if (index !== seeds.length - 1) return index++
if (tracks.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'station', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
const new_tracks = []
seeds.nForEach(async (seed2) => {
await tracks.nForEach((track) => {
if (track.info.title !== seed2.song.songTitle || track.info.author !== seed2.song.artistSummary) return false
new_tracks.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
debugLog('loadtracks', 4, { type: 2, loadType: 'station', sourceName: 'Pandora', playlistName: stationData.name })
resolve({
loadType: 'station',
data: {
info: {
name: stationData.name,
selectedTrack: 0,
},
pluginInfo: {},
tracks: new_tracks,
}
})
return true
})
})
break
}
case 'podcast': {
const { body: podcastData } = await http1makeRequest('https://www.pandora.com/api/v1/aesop/getDetails', {
body: {
catalogVersion: 4,
pandoraId: lastPart
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (podcastData.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
if (podcastData.message) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: podcastData.message })
return resolve({
loadType: 'error',
data: {
message: podcastData.message,
severity: 'common',
cause: 'Unknown'
}
})
}
const tracks = []
let index = 0
switch (podcastData.details.podcastProgramDetails ? podcastData.details.podcastProgramDetails.type : podcastData.details.podcastEpisodeDetails.type) {
case 'PE': {
const podcastEpisode = podcastData.details.annotations[Object.keys(podcastData.details.annotations).find((key) => key === podcastData.details.podcastEpisodeDetails.pandoraId)]
const search = await searchWithDefault(`${podcastEpisode.name} ${podcastEpisode.programName}`)
if (search.loadType !== 'search')
return resolve(search)
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: podcastEpisode.programName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: podcastEpisode.name,
uri: `https://www.pandora.com${podcastEpisode.shareableUrlPath}`,
artworkUrl: `https://content-images.p-cdn.com/${podcastEpisode.icon.artUrl}`,
isrc: null,
sourceName: 'pandora'
}
debugLog('loadtracks', 4, { type: 2, loadType: 'track', sourceName: 'Pandora', track, query })
resolve({
loadType: 'track',
data: {
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
}
})
break
}
case 'PC': {
const { body: allEpisodesIdsData } = await http1makeRequest('https://www.pandora.com/api/v1/aesop/getAllEpisodesByPodcastProgram', {
body: {
catalogVersion: 4,
pandoraId: lastPart
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (allEpisodesIdsData.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
if (allEpisodesIdsData.message) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: allEpisodesIdsData.message })
return resolve({
loadType: 'error',
data: {
message: allEpisodesIdsData.message,
severity: 'common',
cause: 'Unknown'
}
})
}
let allEpisodesIds = []
allEpisodesIdsData.episodes.episodesWithLabel.forEach((yearInfo) => {
allEpisodesIds.push(...yearInfo.episodes)
})
if (allEpisodesIds.length > config.options.maxAlbumPlaylistLength)
allEpisodesIds = allEpisodesIds.slice(0, config.options.maxAlbumPlaylistLength)
const { body: allEpisodesData } = await http1makeRequest('https://www.pandora.com/api/v1/aesop/annotateObjects', {
body: {
catalogVersion: 4,
pandoraIds: allEpisodesIds
},
headers: {
'Cookie': csrfToken.raw,
'X-CsrfToken': csrfToken.parsed,
'X-AuthToken': authToken
},
method: 'POST',
disableBodyCompression: true
})
if (allEpisodesData.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
if (allEpisodesData.message) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: allEpisodesData.message })
return resolve({
loadType: 'error',
data: {
message: allEpisodesData.message,
severity: 'common',
cause: 'Unknown'
}
})
}
let episodes = Object.keys(allEpisodesData.annotations)
episodes.forEach(async (episode) => {
episode = allEpisodesData.annotations[episode]
const search = await searchWithDefault(`${episode.name} ${episode.programName}`)
if (search.loadType === 'search') {
const track = {
identifier: search.data[0].info.identifier,
isSeekable: true,
author: episode.programName,
length: search.data[0].info.length,
isStream: false,
position: 0,
title: episode.name,
uri: `https://www.pandora.com${episode.shareableUrlPath}`,
artworkUrl: `https://content-images.p-cdn.com/${episode.icon.artUrl}`,
isrc: null,
sourceName: 'pandora'
}
tracks.push({
encoded: encodeTrack(track),
info: track,
playlistInfo: {}
})
}
if (index !== episodes.length - 1) return index++
if (tracks.length === 0) {
debugLog('loadtracks', 4, { type: 3, loadType: 'podcast', sourceName: 'Pandora', query, message: 'No matches found.' })
return resolve({
loadType: 'empty',
data: {}
})
}
const new_tracks = []
episodes.nForEach(async (episode2) => {
if (typeof episode2 !== 'object') episode2 = allEpisodesData.annotations[episode2]
await tracks.nForEach((track) => {
if (track.info.title !== episode2.name || track.info.author !== episode2.programName) return false
new_tracks.push(track)
return true
})
if (new_tracks.length !== tracks.length) return false
const podcastName = podcastData.details.annotations[Object.keys(podcastData.details.annotations).find((key) => key === podcastData.details.podcastProgramDetails.pandoraId)].name
debugLog('loadtracks', 4, { type: 2, loadType: 'podcast', sourceName: 'Pandora', playlistName: podcastName })
resolve({
loadType: 'podcast',
data: {
info: {
name: podcastName,
selectedTrack: 0,
},
pluginInfo: {},
tracks: new_tracks,
}
})
return true
})
})
break
}
}
}
}
})
}
export default {
init,
search,
loadFrom
}