|
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 |
|
} |