Skip to content
Snippets Groups Projects
SpotifyScript.ts 161 KiB
Newer Older
//#region constants
    type AlbumResponse,
    type AlbumTracksResponse,
    type ArtistDetails,
    type ArtistMetadataResponse,
    type ContentType,
    type EpisodeMetadataResponse,
    type FileManifestResponse,
    type GetLicenseResponse,
    type LyricsResponse,
    type PlaylistContent,
    type PlaylistContentResponse,
    type PlaylistResponse,
    type PlaylistType,
    type ShowMetadataResponse,
    type SongMetadataResponse,
    type State,
    type TrackMetadataResponse,
    type Tracks,
    type TranscriptResponse,
    type ProfileAttributesResponse,
    type ChannelType,
    type BrowsePageResponse,
    type GenrePlaylistSection,
    type ChannelTypeCapabilities,
    type SectionItemAlbum,
    type SectionItemPlaylist,
    type BrowseSectionResponse,
    type Section,
    type BookChaptersResponse,
    type PodcastEpisodesResponse,
    type UserPlaylistsResponse,
    type DiscographyResponse,
    type HomeResponse,
    type HomePlaylistSection,
    type SectionItemEpisode,
    type WhatsNewResponse,
    type WhatsNewSection,
    type SearchResponse,
    type SearchTypes,
    type CollectionType,
    type LikedEpisodesResponse,
    type LikedTracksResponse,
    type LibraryResponse,
    type FollowingResponse,
    type UriType,
    type SpotifySource,
    type RecentlyPlayedUris,
    type RecentlyPlayedDetails,
    type RecentlyPlayedSection,
    type SectionItemPseudoPlaylist,
Kai DeLorenzo's avatar
Kai DeLorenzo committed
    type Settings,
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/" as const
const EPISODE_URL_PREFIX = "https://open.spotify.com/episode/" as const
const SHOW_URL_PREFIX = "https://open.spotify.com/show/" as const
const ARTIST_URL_PREFIX = "https://open.spotify.com/artist/" as const
const USER_URL_PREFIX = "https://open.spotify.com/user/" as const
const ALBUM_URL_PREFIX = "https://open.spotify.com/album/" as const
const PAGE_URL_PREFIX = "https://open.spotify.com/genre/" as const
const SECTION_URL_PREFIX = "https://open.spotify.com/section/" as const
const PLAYLIST_URL_PREFIX = "https://open.spotify.com/playlist/" as const
const COLLECTION_URL_PREFIX = "https://open.spotify.com/collection/" as const
const QUERY_URL = "https://api-partner.spotify.com/pathfinder/v1/query" as const
const IMAGE_URL_PREFIX = "https://i.scdn.co/image/" as const

const PLATFORM = "Spotify" as const
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" as const
const SPOTIFY_CONNECT_NAME = "Web Player (Grayjay)" as const
const CLIENT_VERSION = "harmony:4.42.0-2780565f" as const

const HARDCODED_ZERO = 0 as const
const HARDCODED_EMPTY_STRING = "" as const
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"

Kai DeLorenzo's avatar
Kai DeLorenzo committed
let local_settings: Settings
Kai DeLorenzo's avatar
Kai DeLorenzo committed
const local_source: SpotifySource = {
    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<
    T extends { readonly [key: string]: string },
    S extends string,
    ChannelTypes extends FeedType,
    SearchTypes extends FeedType,
    ChannelSearchTypes extends FeedType
Kai DeLorenzo's avatar
Kai DeLorenzo committed
>(local_source: Source<T, S, ChannelTypes, SearchTypes, ChannelSearchTypes, Settings>) {
Kai DeLorenzo's avatar
Kai DeLorenzo committed
    for (const method_key of Object.keys(local_source)) {
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        // @ts-expect-error assign to readonly constant source object
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        source[method_key] = local_source[method_key]
//#region enable
Kai DeLorenzo's avatar
Kai DeLorenzo committed
function enable(conf: SourceConfig, settings: Settings, savedState?: string | null) {
    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
Kai DeLorenzo's avatar
Kai DeLorenzo committed
    if (savedState !== null && savedState !== undefined) {
        const state: 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
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const html = throw_if_not_ok(local_http.GET(home_page, { "User-Agent": USER_AGENT }, true)).body
Kai DeLorenzo's avatar
Kai DeLorenzo committed

        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: {
            readonly isPremium: boolean
            readonly userCountry: string
        } = JSON.parse(token_match_result[1])

        const token_response: {
            readonly accessToken: string,
            readonly accessTokenExpirationTimestampMs: number
        } = 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}` },
Loading
Loading full blame...