Skip to content
Snippets Groups Projects
SpotifyScript.js 145 KiB
Newer Older
    constructor(artist_uri_id, offset, limit) {
        const { url: discography_url, headers: discography_headers } = discography_args(artist_uri_id, offset, limit);
        const discography_response = JSON.parse(throw_if_not_ok(local_http.GET(discography_url, discography_headers, false)).body);
        const total_albums = discography_response.data.artistUnion.discography.all.totalCount;
        super(load_album_tracks_and_flatten(discography_response), total_albums > offset + limit);
        this.artist_uri_id = artist_uri_id;
        this.limit = limit;
        this.offset = offset + limit;
        this.total_albums = total_albums;
    }
    nextPage() {
        const { url, headers } = discography_args(this.artist_uri_id, this.offset, this.limit);
        const discography_response = JSON.parse(local_http.GET(url, headers, false).body);
        this.results = load_album_tracks_and_flatten(discography_response);
        this.hasMore = this.total_albums > this.offset + this.limit;
        this.offset = this.offset + this.limit;
        return this;
    }
    hasMorePagers() {
        return this.hasMore;
    }
}
//TODO parallelize all of this album track loading code
function load_album_tracks_and_flatten(discography_response) {
    const songs = [];
    for (const album of discography_response.data.artistUnion.discography.all.items) {
        const first_release = album.releases.items[0];
        if (first_release === undefined) {
            throw new ScriptException("unreachable");
        }
        const pagination_limit = 50;
        const offset = 0;
        const { url, headers } = album_metadata_args(first_release.id, offset, pagination_limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const album_metadata_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const album_artist = album_metadata_response.data.albumUnion.artists.items[0];
        if (album_artist === undefined) {
            throw new ScriptException("missing album artist");
        }
        const unix_time = new Date(album_metadata_response.data.albumUnion.date.isoString).getTime() / 1000;
        const album_pager = new AlbumPager(first_release.id, offset, pagination_limit, album_metadata_response, album_artist, unix_time);
        songs.push(...album_pager.results);
        while (album_pager.hasMorePagers()) {
            album_pager.nextPage();
            songs.push(...album_pager.results);
        }
    }
    return songs;
}
function format_discography(discography_response, artist) {
    return discography_response.data.artistUnion.discography.all.items.map(function (album) {
        const first_release = album.releases.items[0];
        if (first_release === undefined) {
            throw new ScriptException("unreachable");
        }
        const thumbnail = first_release.coverArt.sources[0]?.url;
        if (thumbnail === undefined) {
            throw new ScriptException("unreachable");
        }
        return new PlatformPlaylist({
            id: new PlatformID(PLATFORM, first_release.id, plugin.config.id),
            name: first_release.name,
            author: artist,
            datetime: new Date(first_release.date.isoString).getTime() / 1000,
            url: `${ALBUM_URL_PREFIX}${first_release.id}`,
            videoCount: first_release.tracks.totalCount,
            thumbnail
        });
    });
}
function discography_args(artist_uri_id, offset, limit) {
    const variables = JSON.stringify({
        uri: `spotify:artist:${artist_uri_id}`,
        offset,
        limit
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "9380995a9d4663cbcb5113fef3c6aabf70ae6d407ba61793fd01e2a1dd6929b0"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "queryArtistDiscographyAll");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function format_section_item(section, section_as_author) {
    switch (section.__typename) {
        case "Album":
            {
                const album_artist = section.artists.items[0];
                if (album_artist === undefined) {
                    throw new ScriptException("missing album artist");
                }
                const cover_art_url = section.coverArt.sources[0]?.url;
                if (cover_art_url === undefined) {
                    throw new ScriptException("missing album cover art");
                }
                return new PlatformPlaylist({
                    id: new PlatformID(PLATFORM, id_from_uri(section.uri), plugin.config.id),
                    name: section.name,
                    author: new PlatformAuthorLink(new PlatformID(PLATFORM, id_from_uri(album_artist.uri), plugin.config.id), album_artist.profile.name, `${ARTIST_URL_PREFIX}${id_from_uri(album_artist.uri)}`),
                    // TODO load datetime another way datetime: ,
                    url: `${ALBUM_URL_PREFIX}${id_from_uri(section.uri)}`,
                    // TODO load video count some other way videoCount?: number
                    thumbnail: cover_art_url
                });
            }
        case "Playlist": {
            const created_iso = section.attributes.find(function (attribute) {
                return attribute.key === "created";
            })?.value;
            const image_url = section.images.items[0]?.sources[0]?.url;
            if (image_url === undefined) {
                throw new ScriptException(`missing playlist thumbnail for: ${section.uri}`);
            let author = section_as_author;
            // TODO we might want to look up the username of the playlist if it is missing instead of using the section/page/genre as the channel
            if (section.ownerV2.data.username) {
                if (!section.ownerV2.data.username) {
                    throw new ScriptException(`missing username for owner ${section.ownerV2}`);
                }
                author = new PlatformAuthorLink(new PlatformID(PLATFORM, section.ownerV2.data.username, plugin.config.id), section.ownerV2.data.name, `${USER_URL_PREFIX}${section.ownerV2.data.username}`, section.ownerV2.data.avatar?.sources[0]?.url);
            }
            const platform_playlist = {
                id: new PlatformID(PLATFORM, id_from_uri(section.uri), plugin.config.id),
                url: `${PLAYLIST_URL_PREFIX}${id_from_uri(section.uri)}`,
                name: section.name,
                // TODO load some other way videoCount:
                thumbnail: image_url
            };
            if (created_iso !== undefined) {
                return new PlatformPlaylist({
                    ...platform_playlist,
                    datetime: new Date(created_iso).getTime() / 1000
                });
            }
            return new PlatformPlaylist(platform_playlist);
        }
        case "Episode": {
            if (section.podcastV2.data.__typename === "NotFound" || section.releaseDate === null) {
                throw new ScriptException("unreachable");
            }
            return new PlatformVideo({
                id: new PlatformID(PLATFORM, section.id, plugin.config.id),
                name: section.name,
                author: new PlatformAuthorLink(new PlatformID(PLATFORM, id_from_uri(section.podcastV2.data.uri), plugin.config.id), section.podcastV2.data.name, `${SHOW_URL_PREFIX}${id_from_uri(section.podcastV2.data.uri)}`, section.podcastV2.data.coverArt?.sources[0]?.url),
                url: `${EPISODE_URL_PREFIX}${section.id}`,
                thumbnails: new Thumbnails(section.coverArt.sources.map(function (source) {
                    return new Thumbnail(source.url, source.height);
                })),
                duration: section.duration.totalMilliseconds / 1000,
                viewCount: HARDCODED_ZERO,
                isLive: false,
                shareUrl: `${EPISODE_URL_PREFIX}${section.id}`,
                /** unix time */
                datetime: new Date(section.releaseDate.isoString).getTime() / 1000
            });
        }
        case "PseudoPlaylist": {
            const image_url = section.image.sources[0]?.url;
            if (image_url === undefined) {
                throw new ScriptException("missing playlist thumbnail");
            }
            const author = section_as_author;
            const platform_playlist = {
                id: new PlatformID(PLATFORM, id_from_uri(section.uri), plugin.config.id),
                url: `${COLLECTION_URL_PREFIX}${id_from_uri(section.uri)}`,
                name: section.name,
                author,
                // TODO load some other way videoCount:
                thumbnail: image_url
            };
            return new PlatformPlaylist(platform_playlist);
        }
        default:
            throw assert_exhaustive(section, "unreachable");
    }
}
class SectionPager extends ContentPager {
    section_uri_id;
    section_as_author;
    limit;
    offset;
    constructor(section_uri_id, section_items, offset, limit, section_as_author, has_more) {
        const playlists = section_items.map(function (section_item) {
            return format_section_item(section_item, section_as_author);
        });
        super(playlists, has_more);
        this.section_uri_id = section_uri_id;
        this.section_as_author = section_as_author;
        this.offset = offset + limit;
        this.limit = limit;
    }
    nextPage() {
        const { url, headers } = browse_section_args(this.section_uri_id, this.offset, this.limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const browse_section_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const section_items = browse_section_response.data.browseSection.sectionItems.items.flatMap(function (section_item) {
            const section_item_content = section_item.content.data;
            if (section_item_content.__typename === "Album" || section_item_content.__typename === "Playlist") {
                return [section_item_content];
            }
            return [];
        });
        const author = this.section_as_author;
        if (section_items.length === 0) {
            this.results = [];
        }
        else {
            this.results = section_items.map(function (section_item) {
                return format_section_item(section_item, author);
            });
        }
        const next_offset = browse_section_response.data.browseSection.sectionItems.pagingInfo.nextOffset;
        if (next_offset !== null) {
            this.offset = next_offset;
        }
        this.hasMore = next_offset !== null;
        return this;
    }
    hasMorePagers() {
        return this.hasMore;
    }
}
function browse_section_args(page_uri_id, offset, limit) {
    const variables = JSON.stringify({
        uri: `spotify:section:${page_uri_id}`,
        pagination: {
            offset,
            limit
        }
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "8cb45a0fea4341b810e6f16ed2832c7ef9d3099aaf0034ee2a0ce49afbe42748"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "browseSection");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function book_chapters_args(audiobook_uri_id, offset, limit) {
    const variables = JSON.stringify({
        uri: `spotify:show:${audiobook_uri_id}`,
        offset,
        limit
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "9879e364e7cee8e656be5f003ac7956b45c5cc7dea1fd3c8039e6b5b2e1f40b4"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "queryBookChapters");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function podcast_episodes_args(podcast_uri_id, offset, limit) {
    const variables = JSON.stringify({
        uri: `spotify:show:${podcast_uri_id}`,
        offset,
        limit
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "108deda91e2701403d95dc39bdade6741c2331be85737b804a00de22cc0acabf"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "queryPodcastEpisodes");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
class ChapterPager extends VideoPager {
    audiobook_uri_id;
    limit;
    author;
    publish_date_time;
    offset;
    constructor(audiobook_uri_id, chapters_response, offset, limit, author, publish_date_time) {
        const chapters = format_chapters(chapters_response, author, publish_date_time);
        const next_offset = chapters_response.data.podcastUnionV2.chaptersV2.pagingInfo.nextOffset;
        super(chapters, next_offset !== null);
        this.audiobook_uri_id = audiobook_uri_id;
        this.limit = limit;
        this.author = author;
        this.publish_date_time = publish_date_time;
        this.offset = next_offset === null ? offset : next_offset;
    }
    nextPage() {
        const { url, headers } = book_chapters_args(this.audiobook_uri_id, this.offset, this.limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const chapters_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const chapters = format_chapters(chapters_response, this.author, this.publish_date_time);
        const next_offset = chapters_response.data.podcastUnionV2.chaptersV2.pagingInfo.nextOffset;
        this.hasMore = next_offset !== null;
        this.results = chapters;
        this.offset = next_offset === null ? this.offset : next_offset;
        return this;
    }
    hasMorePagers() {
        return this.hasMore;
    }
}
function format_chapters(chapters_response, author, publish_date_time) {
    return chapters_response.data.podcastUnionV2.chaptersV2.items.map(function (chapter_container) {
        const chapter_data = chapter_container.entity.data;
        const thumbnails = new Thumbnails(chapter_data.coverArt.sources.map(function (source) {
            return new Thumbnail(source.url, source.height);
        }));
        return new PlatformVideo({
            id: new PlatformID(PLATFORM, id_from_uri(chapter_data.uri), plugin.config.id),
            name: chapter_data.name,
            author,
            datetime: publish_date_time,
            url: `${EPISODE_URL_PREFIX}${id_from_uri(chapter_data.uri)}`,
            thumbnails,
            duration: chapter_data.duration.totalMilliseconds / 1000,
            viewCount: HARDCODED_ZERO,
            isLive: false,
            shareUrl: `${EPISODE_URL_PREFIX}${id_from_uri(chapter_data.uri)}`
        });
    });
}
class EpisodePager extends VideoPager {
    podcast_uri_id;
    limit;
    author;
    offset;
    constructor(podcast_uri_id, episodes_response, offset, limit, author) {
        const chapters = format_episodes(episodes_response, author);
        const next_offset = episodes_response.data.podcastUnionV2.episodesV2.pagingInfo.nextOffset;
        super(chapters, next_offset !== null);
        this.podcast_uri_id = podcast_uri_id;
        this.limit = limit;
        this.author = author;
        this.offset = next_offset === null ? offset : next_offset;
    }
    nextPage() {
        const { url, headers } = podcast_episodes_args(this.podcast_uri_id, this.offset, this.limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const chapters_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const chapters = format_episodes(chapters_response, this.author);
        const next_offset = chapters_response.data.podcastUnionV2.episodesV2.pagingInfo.nextOffset;
        this.hasMore = next_offset !== null;
        this.results = chapters;
        this.offset = next_offset === null ? this.offset : next_offset;
        return this;
    }
    hasMorePagers() {
        return this.hasMore;
    }
}
function format_episodes(episodes_response, author) {
    return episodes_response.data.podcastUnionV2.episodesV2.items.map(function (chapter_container) {
        const episode_data = chapter_container.entity.data;
        const thumbnails = new Thumbnails(episode_data.coverArt.sources.map(function (source) {
            return new Thumbnail(source.url, source.height);
        }));
        return new PlatformVideo({
            id: new PlatformID(PLATFORM, id_from_uri(episode_data.uri), plugin.config.id),
            name: episode_data.name,
            author,
            datetime: new Date(episode_data.releaseDate.isoString).getTime() / 1000,
            url: `${EPISODE_URL_PREFIX}${id_from_uri(episode_data.uri)}`,
            thumbnails,
            duration: episode_data.duration.totalMilliseconds / 1000,
            viewCount: HARDCODED_ZERO,
            isLive: false,
            shareUrl: `${EPISODE_URL_PREFIX}${id_from_uri(episode_data.uri)}`
        });
    });
}
class UserPlaylistPager extends PlaylistPager {
    username;
    limit;
    offset;
    total_playlists;
    constructor(username, offset, limit) {
        const { url, headers } = user_playlists_args(username, offset, limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const playlists_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const playlists = format_user_playlists(playlists_response);
        const total_playlists = playlists_response.total_public_playlists_count;
        super(playlists, offset + limit < total_playlists);
        this.username = username;
        this.limit = limit;
        this.offset = offset + limit;
        this.total_playlists = total_playlists;
    }
    nextPage() {
        const { url, headers } = user_playlists_args(this.username, this.offset, this.limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const playlists_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        const playlists = format_user_playlists(playlists_response);
        this.hasMore = this.offset + this.limit < this.total_playlists;
        this.results = playlists;
        this.offset = this.offset + this.limit;
        return this;
    }
    hasMorePagers() {
        return this.hasMore;
    }
}
function user_playlists_args(username, offset, limit) {
    const url = new URL(`https://spclient.wg.spotify.com/user-profile-view/v3/profile/${username}/playlists`);
    url.searchParams.set("offset", offset.toString());
    url.searchParams.set("limit", limit.toString());
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function format_user_playlists(playlists_response) {
    return playlists_response.public_playlists.map(function (playlist) {
        const image_uri = playlist.image_url;
        return new PlatformPlaylist({
            id: new PlatformID(PLATFORM, id_from_uri(playlist.uri), plugin.config.id),
            name: playlist.name,
            author: new PlatformAuthorLink(new PlatformID(PLATFORM, id_from_uri(playlist.owner_uri), plugin.config.id), playlist.owner_name, `${USER_URL_PREFIX}${id_from_uri(playlist.owner_uri)}`),
            // TODO load the playlist creation or modificiation date somehow datetime?: number
            url: `${PLAYLIST_URL_PREFIX}${id_from_uri(playlist.uri)}`,
            // TODO load the video count somehow videoCount?: number
            thumbnail: url_from_image_uri(image_uri)
        });
    });
}
//#endregion
//#region other
function getUserPlaylists() {
    let playlists = [];
    let more = true;
    let offset = 0;
    const limit = 50;
    while (more) {
        const { url, headers } = library_args(offset, limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const library_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        playlists = [
            ...playlists,
            ...library_response.data.me.libraryV3.items.flatMap(function (library_item) {
                const item = library_item.item.data;
                // to avoid the never type
                const type = item.__typename;
                switch (item.__typename) {
                    case "Album":
                        return `${ALBUM_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "Playlist":
                        return `${PLAYLIST_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "PseudoPlaylist":
                        return `${COLLECTION_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "Audiobook":
                        return [];
                    case "Podcast":
                        return [];
                    case "Artist":
                        return [];
                    case "Folder":
                        return [];
                    case "NotFound":
                        return [];
                    default:
                        throw assert_exhaustive(item, `unknown item type: ${type}`);
                }
            })
        ];
        if (library_response.data.me.libraryV3.totalCount <= offset + limit) {
            more = false;
        }
        offset += limit;
    }
    return playlists;
}
function library_args(offset, limit) {
    const variables = JSON.stringify({
        filters: [],
        order: null,
        textFilter: "",
        features: ["LIKED_SONGS", "YOUR_EPISODES", "PRERELEASES"],
        limit,
        offset,
        expandedFolders: [],
        folderUri: null,
        includeFoldersWhenFlattening: true,
        withCuration: false
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "cb996f38c4e0f98c53e46546e0b58f1ed34ab6c31cd00d17698af6ce2ac0f3af"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "libraryV3");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function liked_songs_args(offset, limit) {
    const variables = JSON.stringify({
        limit,
        offset
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "f6cdd87d7fc8598e4e7500fbacd4f661b0c4aea382fe28540aeb4cb7ea4d76c8"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "fetchLibraryTracks");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function liked_episodes_args(offset, limit) {
    const variables = JSON.stringify({
        limit,
        offset
    });
    const extensions = JSON.stringify({
        persistedQuery: {
            version: 1,
            sha256Hash: "f6cdd87d7fc8598e4e7500fbacd4f661b0c4aea382fe28540aeb4cb7ea4d76c8"
        }
    });
    const url = new URL(QUERY_URL);
    url.searchParams.set("operationName", "fetchLibraryEpisodes");
    url.searchParams.set("variables", variables);
    url.searchParams.set("extensions", extensions);
    return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function following_args() {
    const url = `https://spclient.wg.spotify.com/user-profile-view/v3/profile/${local_state.username}/following`;
    return { url, headers: { Authorization: `Bearer ${local_state.bearer_token}` } };
}
function getUserSubscriptions() {
    const { url, headers } = following_args();
Kai DeLorenzo's avatar
Kai DeLorenzo committed
    const following_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
    let following = following_response.profiles === undefined ? [] : following_response.profiles.map(function (profile) {
        const { uri_id, uri_type } = parse_uri(profile.uri);
        if (uri_type === "artist") {
            return `${ARTIST_URL_PREFIX}${uri_id}`;
        }
        else if (uri_type === "user") {
            return `${USER_URL_PREFIX}${uri_id}`;
        }
        throw new ScriptException("unreachable");
    });
    let more = true;
    let offset = 0;
    const limit = 50;
    while (more) {
        const { url, headers } = library_args(offset, limit);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        const library_response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
        following = [
            ...following,
            ...library_response.data.me.libraryV3.items.flatMap(function (library_item) {
                const item = library_item.item.data;
                // to avoid the never type
                const type = item.__typename;
                switch (item.__typename) {
                    case "Album":
                        return [];
                    case "Playlist":
                        return [];
                    case "PseudoPlaylist":
                        return [];
                    case "Audiobook":
                        return `${SHOW_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "Podcast":
                        return `${SHOW_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "Artist":
                        return `${ARTIST_URL_PREFIX}${id_from_uri(item.uri)}`;
                    case "Folder":
                        return [];
                    case "NotFound":
                        return [];
                    default:
                        throw assert_exhaustive(item, `unknown item type: ${type}`);
                }
            })
        ];
        if (library_response.data.me.libraryV3.totalCount <= offset + limit) {
            more = false;
        }
        offset += limit;
    }
    return following;
}
function getPlaybackTracker(url) {
    if (!local_settings.spotifyActivity) {
        return null;
    }
    const { content_uri_id, content_type } = parse_content_url(url);
    check_and_update_token();
    return new SpotifyPlaybackTracker(content_uri_id, content_type);
}
class SpotifyPlaybackTracker extends PlaybackTracker {
    recording_play = false;
    play_recorded = false;
    total_seconds_played = 0;
    feature_identifier;
    device_id;
    context_url;
    context_uri;
    skip_to_data;
    duration;
    interval_seconds;
    constructor(uri_id, content_type) {
        const interval_seconds = 2;
        super(interval_seconds * 1000);
        this.interval_seconds = interval_seconds;
        // generate device id
        // from spotify player js code
        const ht = "undefined" != typeof crypto && "function" == typeof crypto.getRandomValues;
        const gt = (e) => ht ? function (e) {
            return crypto.getRandomValues(new Uint8Array(e));
        }(e) : function (e) {
            const t = [];
            for (; t.length < e;)
                t.push(Math.floor(256 * Math.random()));
            return t;
        }(e);
        const ft = (e) => {
            const t = Math.ceil(e / 2);
            return function (e) {
                let t = "";
                for (let n = 0; n < e.length; n++) {
                    const i = e[n];
                    if (i === undefined) {
                        throw new ScriptException("issue generating device id");
                    }
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                    if (i < 16) {
                        (t += "0");
                    }
                    t += i.toString(16);
                }
                return t;
            }(gt(t));
        };
        const vt = () => ft(40);
        this.device_id = vt();
        // load track info
        switch (content_type) {
            case "episode": {
                const { url, headers } = episode_metadata_args(uri_id);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                const response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
                switch (response.data.episodeUnionV2.__typename) {
                    case "Chapter":
                        this.context_uri = response.data.episodeUnionV2.audiobookV2.data.uri;
                        this.feature_identifier = "audiobook";
                        break;
                    case "Episode":
                        this.context_uri = response.data.episodeUnionV2.podcastV2.data.uri;
                        this.feature_identifier = "show";
                        break;
                    default:
                        throw assert_exhaustive(response.data.episodeUnionV2, "unreachable");
                }
                this.context_url = `context://${this.context_uri}`;
                this.skip_to_data = {
                    content_type: "episode",
                    track_uri: response.data.episodeUnionV2.uri
                };
                this.duration = response.data.episodeUnionV2.duration.totalMilliseconds;
                break;
            }
Kai DeLorenzo's avatar
Kai DeLorenzo committed
            case "track": {
                const { url, headers } = track_metadata_args(uri_id);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                const response = JSON.parse(throw_if_not_ok(local_http.GET(url, headers, false)).body);
                const track_album_index = response.data.trackUnion.trackNumber - 1;
                const { url: tracks_url, headers: tracks_headers } = album_tracks_args(id_from_uri(response.data.trackUnion.albumOfTrack.uri), track_album_index, 1);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                const tracks_response = JSON.parse(throw_if_not_ok(local_http.GET(tracks_url, tracks_headers, false)).body);
                this.feature_identifier = "album";
                this.context_uri = response.data.trackUnion.albumOfTrack.uri;
                this.context_url = `context://${this.context_uri}`;
                this.duration = response.data.trackUnion.duration.totalMilliseconds;
                const uid = tracks_response.data.albumUnion.tracks.items[0]?.uid;
                if (uid === undefined) {
                    throw new ScriptException("can't find song uid");
                }
                this.skip_to_data = {
                    content_type: "track",
                    uid,
                    track_uri: response.data.trackUnion.uri,
                    track_album_index
                };
                break;
Kai DeLorenzo's avatar
Kai DeLorenzo committed
            }
            default:
                throw assert_exhaustive(content_type, "unreachable");
        }
    }
    onProgress(_seconds, is_playing) {
        if (is_playing) {
            // this ends up lagging behind. 
            this.total_seconds_played += this.interval_seconds;
        }
        if (is_playing && !this.recording_play && this.total_seconds_played > 30) {
            this.recording_play = true;
            log("creating WebSocket connection");
            // setup WebSocket connection
            const url = `wss://gue1-dealer.spotify.com/?access_token=${local_state.bearer_token}`;
            const socket = http.socket(url, {}, false);
            socket.connect({
                open: () => {
                },
                closed: (code, reason) => {
                    console.log(code.toString());
                    console.log(reason);
                },
                closing: (code, reason) => {
                    console.log(code.toString());
                    console.log(reason);
                },
                message: (msg) => {
                    // ignore queued messages
                    if (this.play_recorded) {
                        log("ignoring queued message");
                        return;
                    }
                    const message = JSON.parse(msg);
                    // this is the initial connection message
                    if ("method" in message) {
                        const connection_id = message.headers["Spotify-Connection-Id"];
                        const track_playback_url = "https://gue1-spclient.spotify.com/track-playback/v1/devices";
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        throw_if_not_ok(local_http.POST(track_playback_url, JSON.stringify({
                            device: {
                                brand: "spotify",
                                capabilities: {
                                    change_volume: true,
                                    enable_play_token: true,
                                    supports_file_media_type: true,
                                    play_token_lost_behavior: "pause",
                                    disable_connect: false,
                                    audio_podcasts: true,
                                    video_playback: true,
                                    manifest_formats: ["file_ids_mp3", "file_urls_mp3", "manifest_urls_audio_ad", "manifest_ids_video", "file_urls_external", "file_ids_mp4", "file_ids_mp4_dual", "manifest_urls_audio_ad"]
                                },
                                device_id: this.device_id,
                                device_type: "computer",
                                metadata: {},
                                model: "web_player",
                                name: SPOTIFY_CONNECT_NAME,
                                platform_identifier: PLATFORM_IDENTIFIER,
                                is_group: false
                            },
                            outro_endcontent_snooping: false,
                            connection_id: connection_id,
                            client_version: CLIENT_VERSION,
                            volume: 65535
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false));
                        const connect_state_url = `https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_${this.device_id.slice(0, 35)}`;
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        throw_if_not_ok(local_http.requestWithBody("PUT", connect_state_url, JSON.stringify({
                            member_type: "CONNECT_STATE",
                            device: {
                                device_info: {
                                    capabilities: {
                                        can_be_player: false,
                                        hidden: true,
                                        needs_full_player_state: true
                                    }
                                }
                            }
                        }), {
                            Authorization: `Bearer ${local_state.bearer_token}`,
                            "X-Spotify-Connection-Id": connection_id
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        }, false));
                        const transfer_url = `https://gue1-spclient.spotify.com/connect-state/v1/player/command/from/${this.device_id}/to/${this.device_id}`;
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        throw_if_not_ok(local_http.POST(transfer_url, JSON.stringify({
                            command: {
                                context: {
                                    uri: this.context_uri,
                                    url: this.context_url,
                                    metadata: {}
                                play_origin: {
                                    feature_identifier: this.feature_identifier,
                                    feature_version: local_state.feature_version,
                                    referrer_identifier: "your_library"
                                options: {
                                    license: "on-demand",
                                    skip_to: this.skip_to_data.content_type === "track" ? {
                                        track_index: this.skip_to_data.track_album_index,
                                        track_uid: this.skip_to_data.uid,
                                        track_uri: this.skip_to_data.track_uri
                                    } : {
                                        track_uri: this.skip_to_data.track_uri
                                    },
                                    player_options_override: {}
                                logging_params: {
                                    page_instance_ids: [
                                        "54d854fb-fcb4-4e1f-a600-4fd9cbfaac2e"
                                    ],
                                    interaction_ids: [
                                        "d3697919-e8be-425d-98bc-1ea70e28963a"
                                    ],
                                    command_id: "46b1903536f6eda76783840368982c5e"
                                endpoint: "play"
Kai DeLorenzo's avatar
Kai DeLorenzo committed
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false));
                        return;
                    }
                    if (message.uri === "hm://track-playback/v1/command") {
                        if (message.payloads[0]?.state_machine.states.length === 0) {
                            log("ignored WS message that was informing of the active device");
                        const state_machine = message.payloads[0]?.state_machine;
                        const playback_id = (() => {
                            const data = this.skip_to_data;
                            switch (data.content_type) {
                                case "episode": {
                                    return state_machine?.states.find((state) => {
                                        return state_machine.tracks[state.track]?.metadata.uri === data.track_uri;
                                    })?.state_id;
                                }
                                case "track": {
                                    return message.payloads[0]?.state_machine.states.find((state) => {
                                        return state.track_uid === data.uid;
                                    })?.state_id;
                                }
                                default:
                                    throw assert_exhaustive(data);
                            }
                        })();
                        if (playback_id === undefined) {
                            log("error missing playback_id");
                            log(msg);
                            return;
                        }
                        let state_machine_id = state_machine?.state_machine_id;
                        if (state_machine_id === undefined) {
                            log("error missing state_machine_id");
                            log(msg);
                            return;
                        }
                        let seq_num = 3;
                        const initial_state_machine_id = state_machine_id;
                        const state_update_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`;
                        const logged_in = bridge.isLoggedIn();
                        const bitrate = logged_in ? 256000 : 128000;
                        const format = logged_in ? 11 : 10;
                        const audio_quality = logged_in ? "VERY_HIGH" : "HIGH";
                        // simulate song play
                        const before_track_load = JSON.parse(local_http.requestWithBody("PUT", state_update_url, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 1, position: 0, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            debug_source: "before_track_load"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
                        state_machine_id = before_track_load.state_machine.state_machine_id;
                        seq_num += 1;
                        local_http.requestWithBody("PUT", state_update_url, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: initial_state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 0, position: 0, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            debug_source: "speed_changed"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false);
                        seq_num += 1;
                        const speed_change = JSON.parse(local_http.requestWithBody("PUT", state_update_url, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 1, position: 0, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            previous_position: 0,
                            debug_source: "speed_changed"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
                        state_machine_id = speed_change.state_machine.state_machine_id;
                        seq_num += 1;
                        const started_playing = JSON.parse(local_http.requestWithBody("PUT", state_update_url, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 1, position: 1360, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            previous_position: 1360,
                            debug_source: "started_playing"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
                        state_machine_id = started_playing.state_machine.state_machine_id;
                        seq_num += 1;
                        const played_threshold_reached = JSON.parse(local_http.requestWithBody("PUT", state_update_url, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 1, position: 30786, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            previous_position: 30786,
                            debug_source: "played_threshold_reached"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
                        state_machine_id = played_threshold_reached.state_machine.state_machine_id;
                        seq_num += 1;
                        // delete the device
                        const url = `https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_${this.device_id.slice(0, 35)}`;
                        local_http.request("DELETE", url, { Authorization: `Bearer ${local_state.bearer_token}` }, false);
                        const deregister = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}`;
                        local_http.requestWithBody("DELETE", deregister, JSON.stringify({
                            seq_num: seq_num,
                            state_ref: { state_machine_id: state_machine_id, state_id: playback_id, paused: false },
                            sub_state: { playback_speed: 1, position: 40786, duration: this.duration, media_type: "AUDIO", bitrate, audio_quality, format },
                            previous_position: 40786,
                            debug_source: "deregister"
                        }), { Authorization: `Bearer ${local_state.bearer_token}` }, false);
                        socket.close();
                        this.play_recorded = true;
                        log("closing WebSocket connection");
                        return;
                    }
                    log("ignored WS message");
                    log(msg);
                    return;
                },
                failure: (exception) => {
                    log("failure");
                    console.log(exception);
//#region utilities
function url_from_image_uri(image_uri) {
    const match_result = image_uri.match(/^spotify:(image|mosaic):([0-9a-zA-Z:]*)$/);
    if (match_result === null) {
        if (/^https:\/\//.test(image_uri)) {
            return image_uri;
        }
        throw new ScriptException("regex error");
    }
    const image_type = match_result[1];
    if (image_type === undefined) {
        throw new ScriptException("regex error");
    }
    const uri_id = match_result[2];
    if (uri_id === undefined) {
        throw new ScriptException("regex error");
    }
    switch (image_type) {
        case "image":
            return `https://i.scdn.co/image/${uri_id}`;
        case "mosaic":
            return `https://mosaic.scdn.co/300/${uri_id.split(":").join("")}`;
        default:
            throw assert_exhaustive(image_type);
    }
}
function id_from_uri(uri) {
    return parse_uri(uri).uri_id;
}
function parse_uri(uri) {
    const match_result = uri.match(/^spotify:(show|album|track|artist|playlist|section|episode|user|genre|collection):([0-9a-zA-Z]*|tracks|your-episodes)$/);
    if (match_result === null) {
        throw new ScriptException(`regex error processing: ${uri}`);
    const maybe_type = match_result[1];
    if (maybe_type === undefined) {
        throw new ScriptException(`regex error processing: ${uri}`);
    }
    const uri_type = maybe_type;
    const uri_id = match_result[2];
    if (uri_id === undefined) {
        throw new ScriptException(`regex error processing: ${uri}`);
    return { uri_id, uri_type };
/**
 * Converts seconds to the timestamp format used in WebVTT
 * @param seconds
 * @returns
 */
function milliseconds_to_WebVTT_timestamp(milliseconds) {
    return new Date(milliseconds).toISOString().substring(11, 23);
}
function assert_never(value) {
    log(value);
}
function log_passthrough(value) {
    log(value);
    return value;
}
Kai DeLorenzo's avatar
Kai DeLorenzo committed
function throw_if_not_ok(response) {
Kelvin's avatar
Kelvin committed
    if (!response.isOk) {
Kai DeLorenzo's avatar
Kai DeLorenzo committed
        throw new ScriptException(`Request failed [${response.code}] for ${response.url}`);
Kai DeLorenzo's avatar
Kai DeLorenzo committed
    }
    return response;
}
function assert_exhaustive(value, exception_message) {
    log(["Spotify log:", value]);
    if (exception_message !== undefined) {
        return new ScriptException(exception_message);
    }
    return;
}
//#endregion
//#region bad
// https://open.spotifycdn.com/cdn/build/web-player/vendor~web-player.391a2438.js