Newer
Older
const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/;
const PLAYLIST_REGEX = /^https:\/\/open\.spotify\.com\/(album|playlist|collection)\/([a-zA-Z0-9]*|your-episodes|tracks)($|\/)/;
const CHANNEL_REGEX = /^https:\/\/open\.spotify\.com\/(show|artist|user|genre|section|content-feed)\/(section|)([a-zA-Z0-9]*|recently-played)($|\/)/;
const SONG_URL_PREFIX = "https://open.spotify.com/track/";
const EPISODE_URL_PREFIX = "https://open.spotify.com/episode/";
const SHOW_URL_PREFIX = "https://open.spotify.com/show/";
const ARTIST_URL_PREFIX = "https://open.spotify.com/artist/";
const USER_URL_PREFIX = "https://open.spotify.com/user/";
const ALBUM_URL_PREFIX = "https://open.spotify.com/album/";
const PAGE_URL_PREFIX = "https://open.spotify.com/genre/";
const SECTION_URL_PREFIX = "https://open.spotify.com/section/";
const PLAYLIST_URL_PREFIX = "https://open.spotify.com/playlist/";
const COLLECTION_URL_PREFIX = "https://open.spotify.com/collection/";
const QUERY_URL = "https://api-partner.spotify.com/pathfinder/v1/query";
const IMAGE_URL_PREFIX = "https://i.scdn.co/image/";
const PLATFORM = "Spotify";
const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0";
const PLATFORM_IDENTIFIER = "web_player linux undefined;firefox 126.0;desktop";
const SPOTIFY_CONNECT_NAME = "Web Player (Grayjay)";
const CLIENT_VERSION = "harmony:4.42.0-2780565f";
const HARDCODED_ZERO = 0;
const HARDCODED_EMPTY_STRING = "";
const EMPTY_AUTHOR = new PlatformAuthorLink(new PlatformID(PLATFORM, "", plugin.config.id), "", "");
const local_http = http;
// const local_utility = utility
// set missing constants
Type.Order.Chronological = "Latest releases";
Type.Order.Views = "Most played";
Type.Order.Favorites = "Most favorited";
Type.Feed.Playlists = "PLAYLISTS";
Type.Feed.Albums = "ALBUMS";
/** State */
let local_state;
//#region source methods
const local_source = {
enable,
disable,
saveState,
getHome,
search,
getSearchCapabilities,
isContentDetailsUrl,
getContentDetails,
isChannelUrl,
getChannel,
getChannelContents,
getChannelCapabilities,
searchChannels,
isPlaylistUrl,
getPlaylist,
searchPlaylists,
getChannelPlaylists,
getPlaybackTracker,
getUserPlaylists,
getUserSubscriptions
};
init_source(local_source);
function init_source(local_source) {
for (const method_key of Object.keys(local_source)) {
}
}
//#endregion
//#region enable
function enable(conf, settings, savedState) {
if (IS_TESTING) {
log("IS_TESTING true");
log("logging configuration");
log(conf);
log("logging settings");
log(settings);
log("logging savedState");
log(savedState);
}
local_settings = settings;
const state = JSON.parse(savedState);
local_state = state;
// the token stored in state might be old
check_and_update_token();
}
else {
const home_page = "https://open.spotify.com";
const token_regex = /<script id="config" data-testid="config" type="application\/json">({.*?})<\/script><script id="session" data-testid="session" type="application\/json">({.*?})<\/script>/;
const web_player_js_regex = /https:\/\/open\.spotifycdn\.com\/cdn\/build\/web-player\/web-player\..{8}\.js/;
// use the authenticated client to get a logged in bearer token
const html = throw_if_not_ok(local_http.GET(home_page, { "User-Agent": USER_AGENT }, true)).body;
const web_player_js_match_result = html.match(web_player_js_regex);
if (web_player_js_match_result === null || web_player_js_match_result[0] === undefined) {
throw new ScriptException("regex error");
}
const token_match_result = html.match(token_regex);
if (token_match_result === null || token_match_result[1] === undefined || token_match_result[2] === undefined) {
throw new ScriptException("regex error");
}
const user_data = JSON.parse(token_match_result[1]);
const token_response = JSON.parse(token_match_result[2]);
const bearer_token = token_response.accessToken;
// download license uri and get logged in user
const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0";
const profile_attributes_url = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=profileAttributes&variables=%7B%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%2253bcb064f6cd18c23f752bc324a791194d20df612d8e1239c735144ab0399ced%22%7D%7D";
const web_player_js_url = web_player_js_match_result[0];
const responses = local_http
.batch()
.GET(get_license_url_url, { Authorization: `Bearer ${bearer_token}` }, false)
.GET(profile_attributes_url, { Authorization: `Bearer ${bearer_token}` }, false)
.GET(web_player_js_url, { Authorization: `Bearer ${bearer_token}` }, false)
if (responses[0] === undefined || responses[1] === undefined || responses[2] === undefined) {
throw new ScriptException("unreachable");
}
const get_license_response = JSON.parse(throw_if_not_ok(responses[0]).body);
const license_uri = `https://gue1-spclient.spotify.com/${get_license_response.uri}`;
const profile_attributes_response = JSON.parse(throw_if_not_ok(responses[1]).body);
const feature_version_match_result = throw_if_not_ok(responses[2]).body.match(/"(web-player_(.*?))"/);
throw new ScriptException("regex error");
}
const feature_version = feature_version_match_result[1];
if (feature_version === undefined) {
throw new ScriptException("regex error");
}
expiration_timestamp_ms: token_response.accessTokenExpirationTimestampMs,
license_uri: license_uri,
is_premium: user_data.isPremium
if (profile_attributes_response.data.me !== null) {
state = {
...state,
username: profile_attributes_response.data.me.profile.username
};
}
if ("userCountry" in user_data) {
state = { ...state, country: user_data.userCountry };
}
local_state = state;
}
}
function download_bearer_token() {
const get_access_token_url = "https://open.spotify.com/get_access_token?reason=transport&productType=web-player";
// use the authenticated client to get a logged in bearer token
const access_token_response = throw_if_not_ok(local_http.GET(get_access_token_url, {}, true)).body;
const token_response = JSON.parse(access_token_response);
}
function check_and_update_token() {
// renew the token with 30 seconds to spare
if (Date.now() - 30 * 1000 < local_state.expiration_timestamp_ms) {
return;
}
log("Spotify log: refreshing bearer token");
const token_response = download_bearer_token();
feature_version: local_state.feature_version,
bearer_token: token_response.accessToken,
expiration_timestamp_ms: token_response.accessTokenExpirationTimestampMs,
license_uri: local_state.license_uri,
};
if (local_state.username !== undefined) {
state = { ...state, username: local_state.username };
}
if (local_state.country !== undefined) {
state = { ...state, country: local_state.country };
}
local_state = state;
}
function disable() {
log("Spotify log: disabling");
}
function saveState() {
return JSON.stringify(local_state);
}
function getHome() {
check_and_update_token();
const { url, headers } = home_args(10);
const { url: new_url, headers: new_headers } = whats_new_args(0, 50);
const { url: recent_url, headers: recent_headers } = recently_played_ids_args(0, 50);
const responses = local_http
.batch()
.GET(url, headers, false)
.GET(new_url, new_headers, false)
.GET(recent_url, recent_headers, false)
if (responses[0] === undefined || responses[1] === undefined || responses[2] === undefined) {
throw new ScriptException("unreachable");
}
const home_response = JSON.parse(throw_if_not_ok(responses[0]).body);
const sections = home_response.data.home.sectionContainer.sections.items;
if (bridge.isLoggedIn()) {
const whats_new_response = JSON.parse(throw_if_not_ok(responses[1]).body);
Loading
Loading full blame...