Skip to content
Snippets Groups Projects
SpotifyScript.js 140 KiB
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_UR_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" as const
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";
//#region source methods
source.enable = enable;
source.disable = disable;
source.saveState = saveState;
source.getHome = getHome;
source.getSearchCapabilities = getSearchCapabilities;
source.search = search;
source.searchChannels = searchChannels;
source.isChannelUrl = isChannelUrl;
source.getChannel = getChannel;
source.getChannelCapabilities = getChannelCapabilities;
source.getChannelContents = getChannelContents;
source.isContentDetailsUrl = isContentDetailsUrl;
source.getContentDetails = getContentDetails;
source.isPlaylistUrl = isPlaylistUrl;
source.searchPlaylists = searchPlaylists;
source.getPlaylist = getPlaylist;
source.getUserSubscriptions = getUserSubscriptions;
source.getUserPlaylists = getUserPlaylists;
source.getPlaybackTracker = getPlaybackTracker;
    const assert_source = {
        enable,
        disable,
        saveState,
        getHome,
        search,
        getSearchCapabilities,
        isContentDetailsUrl,
        getContentDetails,
        isChannelUrl,
        getChannel,
        getChannelContents,
        getChannelCapabilities,
        searchChannels,
        isPlaylistUrl,
        getPlaylist,
        searchPlaylists,
        getUserPlaylists,
        getUserSubscriptions,
        getPlaybackTracker
    };
    if (source.enable === undefined) {
        assert_never(source.enable);
    }
    if (source.disable === undefined) {
        assert_never(source.disable);
    }
    if (source.saveState === undefined) {
        assert_never(source.saveState);
    }
    if (source.getHome === undefined) {
        assert_never(source.getHome);
    }
    if (source.search === undefined) {
        assert_never(source.search);
    }
    if (source.getSearchCapabilities === undefined) {
        assert_never(source.getSearchCapabilities);
    }
    if (source.isContentDetailsUrl === undefined) {
        assert_never(source.isContentDetailsUrl);
    }
    if (source.getContentDetails === undefined) {
        assert_never(source.getContentDetails);
    }
    if (source.isChannelUrl === undefined) {
        assert_never(source.isChannelUrl);
    }
    if (source.getChannel === undefined) {
        assert_never(source.getChannel);
    }
    if (source.getChannelContents === undefined) {
        assert_never(source.getChannelContents);
    }
    if (source.getChannelCapabilities === undefined) {
        assert_never(source.getChannelCapabilities);
    }
    if (source.searchChannels === undefined) {
        assert_never(source.searchChannels);
    }
    if (source.isPlaylistUrl === undefined) {
        assert_never(source.isPlaylistUrl);
    }
    if (source.getPlaylist === undefined) {
        assert_never(source.getPlaylist);
    }
    if (source.searchPlaylists === undefined) {
        assert_never(source.searchPlaylists);
    }
    if (source.getUserPlaylists === undefined) {
        assert_never(source.getUserPlaylists);
    }
    if (source.getUserSubscriptions === undefined) {
        assert_never(source.getUserSubscriptions);
    }
    if (source.getPlaybackTracker === undefined) {
        assert_never(source.getPlaybackTracker);
    }
        log(assert_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);
    }
    if (savedState !== null) {
        const state = JSON.parse(savedState);
        local_state = state;
        // the token stored in state might be old
        check_and_update_token();
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        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 = local_http.GET(home_page, {}, 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";
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        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(responses[0].body);
        const license_uri = `https://gue1-spclient.spotify.com/${get_license_response.uri}`;
        const profile_attributes_response = JSON.parse(responses[1].body);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const feature_version_match_result = responses[2].body.match(/"(web-player_(.*?))"/);
        if (feature_version_match_result === null) {
            throw new ScriptException("regex error");
        }
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const feature_version = feature_version_match_result[1];
        if (feature_version === undefined) {
            throw new ScriptException("regex error");
        }
            feature_version,
            expiration_timestamp_ms: token_response.accessTokenExpirationTimestampMs,
Loading
Loading full blame...