Skip to content
Snippets Groups Projects
SpotifyScript.js 135 KiB
Newer Older
                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": {
            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
            });
        }
        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);
        const browse_section_response = JSON.parse(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);
        const chapters_response = JSON.parse(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: 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);
        const chapters_response = JSON.parse(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_data.uri
        });
    });
}
class UserPlaylistPager extends PlaylistPager {
    username;
    limit;
    offset;
    total_playlists;
    constructor(username, offset, limit) {
        const { url, headers } = user_playlists_args(username, offset, limit);
        const playlists_response = JSON.parse(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);
        const playlists_response = JSON.parse(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);
        const library_response = JSON.parse(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;
                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_UR_PREFIX}${id_from_uri(item.uri)}`;
                    case "Audiobook":
                        return [];
                    case "Podcast":
                        return [];
                    case "Artist":
                        return [];
                    default:
                        throw assert_exhaustive(item, "unreachable");
                }
            })
        ];
        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,
        flatten: false,
        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();
    const following_response = JSON.parse(local_http.GET(url, headers, false).body);
    let following = 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);
        const library_response = JSON.parse(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;
                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)}`;
                    default:
                        throw assert_exhaustive(item, "unreachable");
                }
            })
        ];
        if (library_response.data.me.libraryV3.totalCount <= offset + limit) {
            more = false;
        }
        offset += limit;
    }
    return following;
}
function getPlaybackTracker(url) {
    const { content_uri_id } = parse_content_url(url);
    check_and_update_token();
    return new SpotifyPlaybackTracker(content_uri_id);
}
class SpotifyPlaybackTracker extends PlaybackTracker {
    uri_id;
    state_machine_id = "";
    playback_id = "";
    play_recorded = false;
    socket_closed = false;
    in_between = false;
    transfered = false;
    // private device_active = false
    // private transfered = false
    // private start_triggered = false
    connection_id = "";
    socket;
    init_seconds = 0;
    device_id = "b27bde830fd81dbff77339f7ed344db1a40";
    uid = "da3986443f3dbf5a0881";
    duration = 303440;
    constructor(uri_id) {
        const interval_seconds = 10;
        super(interval_seconds * 1000);
        this.uri_id = uri_id;
        // this.device_id = "b27bde830fd81dbff77339f7ed344db1a40"
        log("connecting to websocket");
        const url = `wss://gue1-dealer.spotify.com/?access_token=${local_state.bearer_token}`;
        this.socket = http.socket(url, {}, false);
        this.socket.connect({
            open: () => {
                log("open");
                // this.socket.send(JSON.stringify({
                //     type: "ping"
                // }))
            },
            closed: (code, reason) => {
                console.log(code.toString());
                console.log(reason);
            },
            closing: (code, reason) => {
                console.log(code.toString());
                console.log(reason);
            },
            message: (msg) => {
                log("a message");
                const connection = JSON.parse(msg);
                if (!("method" in connection)) {
                    if (connection.uri === "hm://track-playback/v1/command") {
                        if (connection.payloads[0]?.state_machine.states.length === 0) {
                            log("ignored WS message just informing us of the active device");
                            log(msg);
                            return;
                        }
                        if (this.playback_id !== "" && this.state_machine_id !== "") {
                            log("ignored WS message ids already found");
                            log(msg);
                            return;
                        }
                        // if (this.state_machine_id === "") {
                        log("reading state details");
                        const playback_id = connection.payloads[0]?.state_machine.states.find((state) => {
                            return state.track_uid === this.uid;
                        })?.state_id;
                        // const playback_id = connection.payloads[0]?.state_machine.states[0]?.state_id
                        if (playback_id === undefined || playback_id === "") {
                            log("error missing playback_id");
                            log(msg);
                            return;
                            // throw new ScriptException("missing playback_id")
                        }
                        const state_machine_id = connection.payloads[0]?.state_machine.state_machine_id;
                        if (state_machine_id === undefined || state_machine_id === "") {
                            log("error missing state_machine_id");
                            log(msg);
                            return;
                            // throw new ScriptException("missing state_machine_id")
                        }
                        this.playback_id = playback_id;
                        this.state_machine_id = state_machine_id;
                        log(msg);
                        // }
                        // payloads statemachine states state_id
                        // this.playback_id = "11"
                        // this.state_machine_id = "69"
                        // if (!this.start_triggered && this.transfered) {
                        // }
                        return;
                    }
                    log("ignored WS message");
                    log(msg);
                    return;
                }
                this.connection_id = connection.headers["Spotify-Connection-Id"];
                // register device
                log("registering device");
                const register_url = "https://gue1-spclient.spotify.com/track-playback/v1/devices";
                const response = local_http.POST(register_url, 
                // JSON.stringify({
                //     connection_id: connection.headers["Spotify-Connection-Id"],
                //     device: {
                //         device_id: this.device_id,
                //         model: "web_player",
                //         name: "Web Player (Grayjay)",
                //         // capabilities: {
                //         //     change_volume: false,
                //         //     audio_podcasts: true,
                //         //     manifest_formats: [
                //         //         "file_ids_mp3",
                //         //         "file_urls_mp3"
                //         //     ]
                //         // },
                //         "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"]},
                //         client_version: "harmony:4.42.0-2780565f",
                //         // brand: "spotify",
                //         device_type: "computer"
                //     }
                // }),
                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": "Web Player (Grayjay)", "platform_identifier": "web_player linux undefined;chrome 125.0.0.0;desktop", "is_group": false }, "outro_endcontent_snooping": false, "connection_id": this.connection_id, "client_version": "harmony:4.42.0-2780565f", "volume": 65535 }), { Authorization: `Bearer ${local_state.bearer_token}` }, false);
                log(response);
                // log("grabbing device info")
                // const another_register_thing = `https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_${this.device_id}`
                // const response1 = local_http.requestWithBody(
                //     "PUT",
                //     another_register_thing,
                //     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": this.connection_id
                //     },
                //     false)
                // const device_info = JSON.parse(response1.body)
                // log(device_info)
                // this.device_active = "active_device_id" in device_info
                // this.transfered = true
                // this.registered = true
                // payloads cluster playerstate
                // payloads statemachine staemachine_id
                // if (1 + 2 > 3) {
                // }
                // log(response.body)
                // gives the list of devices
                //https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_5ef1df4daf071872bfe5ae0714efafa29f2
                //https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_ce5888d21908a6372f02c2c0155f3d7d1c9
                // actually registers a device
                //https://gue1-spclient.spotify.com/track-playback/v1/devices
            },
            failure: (exception) => {
                log("failure");
                console.log(exception);
            }
        });
    }
    onInit(seconds) {
        this.init_seconds = seconds;
    }
    onProgress(seconds, is_playing) {
        if (this.socket_closed) {
        if (seconds - this.init_seconds > 50 && is_playing) {
            this.socket.close();
            this.socket_closed = true;
            log("done closing");
        }
        if (this.in_between) {
            return;
        }
        if (seconds - this.init_seconds > 35 && is_playing) {
            if (!this.socket.isOpen) {
                log("socket not open!");
            }
            else {
                log(`recording play of ${this.uri_id}`);
                // this.socket.close()
            }
            const register_playback_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`;
            const response = JSON.parse(local_http.requestWithBody("PUT", register_playback_url, 
            // JSON.stringify({
            //     debug_source: "played_threshold_reached",
            // }),
            JSON.stringify({ "seq_num": 6, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 39536, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": 39536, "debug_source": "played_threshold_reached" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
            log(response);
            this.state_machine_id = response.state_machine.state_machine_id;
            this.in_between = true;
        if (this.play_recorded) {
            return;
        }
        if (seconds - this.init_seconds > 20 && is_playing) {
            this.play_recorded = true;
            // log(this.connection_id)
            // command id is random
            //t = e=>{
            // const t = Math.ceil(e / 2);
            // return function(e) {
            //     let t = "";
            //     for (let n = 0; n < e.length; n++) {
            //         const i = e[n];
            //         i < 16 && (t += "0"),
            //         t += i.toString(16)
            //     }
            //     return t
            // }(gt(t))
            //     const un = /^[0-9a-f]{32}$/i
            //     , pn = ()=>ft(32)
            //     , mn = e=>{
            //       if (e && (t = e,
            //       !un.test(t)))
            //           throw new TypeError(`Invalid commandId. Expected a 32 character hex string but got: ${e}`);
            //       var t;
            //       return e || pn()
            //   }
            log("triggering before play");
            const before_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`;
            const response1 = JSON.parse(local_http.requestWithBody("PUT", before_playling_url, 
            // JSON.stringify({
            //     debug_source: "played_threshold_reached",
            // }),
            JSON.stringify({ "seq_num": 3, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 0, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "debug_source": "before_track_load" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
            log(response1);
            this.state_machine_id = response1.state_machine.state_machine_id;
            // log("speed change 1")
            // // const before_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`
            // const response3: {readonly state_machine: { readonly state_machine_id: string}} = JSON.parse(local_http.requestWithBody(
            //     "PUT",
            //     before_playling_url,
            //     // JSON.stringify({
            //     //     debug_source: "played_threshold_reached",
            //     // }),
            //     JSON.stringify(
            //         { "seq_num": 4, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 0, "position": 0, "duration": duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "debug_source": "speed_changed" }
            //     ),
            //     { Authorization: `Bearer ${local_state.bearer_token}` },
            //     false).body)
            // log(response3)
            // this.state_machine_id = response3.state_machine.state_machine_id
            log("speedchange 2");
            // const before_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`
            const response4 = JSON.parse(local_http.requestWithBody("PUT", before_playling_url, 
            // JSON.stringify({
            //     debug_source: "played_threshold_reached",
            // }),
            JSON.stringify({ "seq_num": 4, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 0, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": 0, "debug_source": "speed_changed" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
            log(response4);
            this.state_machine_id = response4.state_machine.state_machine_id;
            log("triggering play start");
            const started_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`;
            const response = JSON.parse(local_http.requestWithBody("PUT", started_playling_url, 
            // JSON.stringify({
            //     debug_source: "played_threshold_reached",
            // }),
            JSON.stringify({ "seq_num": 5, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 1360, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": 1360, "debug_source": "started_playing" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
            log(response);
            this.state_machine_id = response.state_machine.state_machine_id;
            // this.start_triggered = true
        }
        if (this.transfered) {
            return;
        }
        if (seconds - this.init_seconds > 5 && is_playing) {
            log("transfering to device");
            const transfer_url = `https://gue1-spclient.spotify.com/connect-state/v1/player/command/from/${this.device_id}/to/${this.device_id}`;
            const transfer_response = local_http.POST(transfer_url, 
            // this.device_active ? JSON.stringify(
            //     { "transfer_options": { "restore_paused": "restore" }, "interaction_id": "cf075506-9bc9-4af6-a164-93778f310345", "command_id": "3bcf58bc37afa628c3d441df53efc469" }
            // ) : 
            JSON.stringify({
                "command": {
                    "context": {
                        // "uri": "spotify:track:6CbPF34njo6PpWYTFQrMZN",
                        // "url": "context://spotify:track:6CbPF34njo6PpWYTFQrMZN",
                        uri: "spotify:album:7aJuG4TFXa2hmE4z1yxc3n",
                        url: "context://spotify:album:7aJuG4TFXa2hmE4z1yxc3n",
                        "metadata": {}
                    },
                    "play_origin": {
                        "feature_identifier": "album",
                        // "feature_identifier": "track",
                        "feature_version": "web-player_2024-05-23_1716493666036_b53deef",
                        "referrer_identifier": "your_library"
                    },
                    "options": {
                        "license": "on-demand",
                        "skip_to": {
                            track_index: 2,
                            track_uid: this.uid,
                            track_uri: "spotify:track:7BRD7x5pt8Lqa1eGYC4dzj"
                        },
                        "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"
                }
            }), 
            // JSON.stringify({
            //     command: {
            //         endpoint: "play",
            //         context: {
            //             metadata: {},
            //             uri: "spotify:track:7aohwSiTDju51QmC54AUba",
            //             url: "context://spotify:track:7aohwSiTDju51QmC54AUba"
            //         },
            //         "logging_params": { "page_instance_ids": ["5616d9d6-c44f-4cda-a7c2-167890dd2beb"], "interaction_ids": ["72ab0dbb-7a83-4644-8bad-550d65ff8e77"], "command_id": "0f85a8b2347ff239207f32344d7da9d6" },
            //         "options": {
            //             "license": "on-demand", "skip_to": {}, "player_options_override": {}
            //         },
            //         "play_origin": { "feature_identifier": "track", "feature_version": "web-player_2024-05-23_1716493666036_b53deef", "referrer_identifier": "your_library" },
            //     }
            //     // command_id: "1ec91233c1cd60f69f5de11f513b2887",
            //     // transfer_options: {
            //     //     restore_paused: "pause"
            //     // }
            // }),
            { Authorization: `Bearer ${local_state.bearer_token}` }, false);
            log(transfer_response);
            this.transfered = true;
//#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");
    }
    const maybe_type = match_result[1];
    if (maybe_type === undefined) {
        throw new ScriptException("regex error");
    }
    const uri_type = maybe_type;
    const uri_id = match_result[2];
    if (uri_id === undefined) {
        throw new ScriptException("regex error");
    }
    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;
}
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
const Z = "0123456789abcdef";
const Q = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const ee = [];
ee.length = 256;
for (let ke = 0; ke < 256; ke++)
    // @ts-expect-error
    ee[ke] = Z[ke >> 4] + Z[15 & ke];
const te = [];
te.length = 128;
for (let ke = 0; ke < Q.length; ++ke)
    te[Q.charCodeAt(ke)] = ke;
function get_gid(song_uri_id) {
    return 22 === song_uri_id.length ? function (e) {
        if (22 !== e.length)
            return null;
        const t = 2.3283064365386963e-10, n = 4294967296, i = 238328;
        let o, r, a, s, c;
        // @ts-expect-error
        return o = 56800235584 * te[e.charCodeAt(0)] + 916132832 * te[e.charCodeAt(1)] + 14776336 * te[e.charCodeAt(2)] + 238328 * te[e.charCodeAt(3)] + 3844 * te[e.charCodeAt(4)] + 62 * te[e.charCodeAt(5)] + te[e.charCodeAt(6)],
            r = o * t | 0,
            o -= r * n,
            // @ts-expect-error
            c = 3844 * te[e.charCodeAt(7)] + 62 * te[e.charCodeAt(8)] + te[e.charCodeAt(9)],
            o = o * i + c,
            o -= (c = o * t | 0) * n,
            r = r * i + c,
            // @ts-expect-error
            c = 3844 * te[e.charCodeAt(10)] + 62 * te[e.charCodeAt(11)] + te[e.charCodeAt(12)],
            o = o * i + c,
            o -= (c = o * t | 0) * n,
            r = r * i + c,
            r -= (c = r * t | 0) * n,
            a = c,
            // @ts-expect-error
            c = 3844 * te[e.charCodeAt(13)] + 62 * te[e.charCodeAt(14)] + te[e.charCodeAt(15)],
            o = o * i + c,
            o -= (c = o * t | 0) * n,
            r = r * i + c,
            r -= (c = r * t | 0) * n,
            a = a * i + c,
            // @ts-expect-error
            c = 3844 * te[e.charCodeAt(16)] + 62 * te[e.charCodeAt(17)] + te[e.charCodeAt(18)],
            o = o * i + c,
            o -= (c = o * t | 0) * n,
            r = r * i + c,
            r -= (c = r * t | 0) * n,
            a = a * i + c,
            a -= (c = a * t | 0) * n,
            s = c,
            // @ts-expect-error
            c = 3844 * te[e.charCodeAt(19)] + 62 * te[e.charCodeAt(20)] + te[e.charCodeAt(21)],
            o = o * i + c,
            o -= (c = o * t | 0) * n,
            r = r * i + c,
            r -= (c = r * t | 0) * n,
            a = a * i + c,
            a -= (c = a * t | 0) * n,
            s = s * i + c,
            s -= (c = s * t | 0) * n,
            // @ts-expect-error
            c ? null : ee[s >>> 24] + ee[s >>> 16 & 255] + ee[s >>> 8 & 255] + ee[255 & s] + ee[a >>> 24] + ee[a >>> 16 & 255] + ee[a >>> 8 & 255] + ee[255 & a] + ee[r >>> 24] + ee[r >>> 16 & 255] + ee[r >>> 8 & 255] + ee[255 & r] + ee[o >>> 24] + ee[o >>> 16 & 255] + ee[o >>> 8 & 255] + ee[255 & o];
    }(song_uri_id) : song_uri_id;
}
//#endregion
// export statements are removed during build step
// used for unit testing in SpotifyScript.test.ts
// export { get_gid, assert_never, log_passthrough, getPlaybackTracker };
//# sourceMappingURL=http://localhost:8080/SpotifyScript.js.map