Skip to content
Snippets Groups Projects
SpotifyScript.js 146 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 HARDCODED_ZERO = 0;
const HARDCODED_EMPTY_STRING = "";
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();
        const { token_response, user_data } = download_bearer_token();
        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 responses = local_http
            .batch()
            .GET(get_license_url_url, { Authorization: `Bearer ${bearer_token}` }, false)
            .GET(profile_attributes_url, { Authorization: `Bearer ${bearer_token}` }, false)
            .execute();
        if (responses[0] === undefined || responses[1] === 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);
        let state = {
            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() {
    if (bridge.isLoggedIn()) {
        const home_page = "https://open.spotify.com";
        const regex = /<script id="config" data-testid="config" type="application\/json">({.*?})<\/script><script id="session" data-testid="session" type="application\/json">({.*?})<\/script>/;
        // use the authenticated client to get a logged in bearer token
        const html = local_http.GET(home_page, {}, true).body;
        const match_result = html.match(regex);
        if (match_result === null || match_result[1] === undefined || match_result[2] === undefined) {
            throw new ScriptException("regex error");
        }
        const user_data = JSON.parse(match_result[1]);
        const token_response = JSON.parse(match_result[2]);
        return { token_response, user_data };
Loading
Loading full blame...