Skip to content
Snippets Groups Projects
SpotifyScript.js 146 KiB
Newer Older
            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");
            }
            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": {
            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_UR_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);
        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;
}
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");
            }
            i < 16 && (t += "0"),
                t += i.toString(16);
        }
        return t;
    }(gt(t));
};
const vt = () => ft(40);
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;
    another_one = 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 = vt();
    // private readonly uid = "ccf999d7241e13521c2e"
    // private readonly track_uri = "spotify:track:4pbG9SUmWIvsROVLF0zF9s"
    // private readonly album_uri_id = "7vEJAtP3KgKSpOHVgwm3Eh"
    // private readonly track_album_index = 3
    // private readonly duration = 145746
    uid = "1347b3deaefee32b7d2b";
    track_uri = "spotify:track:2tQG2nFEHhWsH05kFKlC4A";
    album_uri_id = "0BaIaHcyBXuOWeM4Aas4EW";
    track_album_index = 2;
    duration = 109750;
    // private readonly uid = "296cf850453478739645"
    // private readonly track_uri = "spotify:track:77uEkHMJ6EnOZjd1Hh9Tty"
    // private readonly album_uri_id = "6wOJyevNYXevqTZCn6Xk5T"
    // private readonly track_album_index = 4
    // private readonly duration = 171989
    // private readonly uid = "8167260601e9aab35d02"
    // private readonly track_uri = "spotify:track:4Op5aSB6JSVzp7Jhi5hQKp"
    // private readonly album_uri_id = "7skmDXP36SNveM5XKFoLuK"
    // private readonly track_album_index = 5
    // private readonly duration = 111266
    seq_num = 3;
    constructor(uri_id) {
        const interval_seconds = 4;
        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 (Chrome)", "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 devices info");
                const another_register_thing = `https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_${this.device_id.slice(0, 35)}`;
                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) {
        //{"seq_num":15,"state_ref":{"state_machine_id":"ChQ0Jnocbbg7kDx3WOjgNutMUyLLzA","state_id":"8425aeb0fef142d597d1b578c5f31061","paused":true},"sub_state":{"playback_speed":0,"position":33535,"duration":307927,"media_type":"AUDIO","bitrate":128000,"audio_quality":"HIGH","format":10},"previous_position":33535,"debug_source":"pause"}
        if (this.socket_closed) {
        if (seconds - this.init_seconds > 70 && is_playing) {
            this.socket.close();
            this.socket_closed = true;
            log("done closing");
        }
        if (this.another_one) {
            return;
        }
        if (seconds - this.init_seconds > 60 && is_playing) {
            log("deleting device");
            const url = `https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_${this.device_id.slice(0, 35)}`;
            const response = local_http.request("DELETE", url, { Authorization: `Bearer ${local_state.bearer_token}` }, false);
            log(response);
            const url2 = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}`;
            const response2 = local_http.requestWithBody("DELETE", url2, JSON.stringify({ "seq_num": this.seq_num, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 40786, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": 40786, "debug_source": "deregister" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false);
            log(response2);
            /*
            log("trigger finish")

            const initial_state_machine_id = this.state_machine_id
            const register_playback_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`
            const response1: { readonly state_machine: { readonly state_machine_id: string } } = JSON.parse(local_http.requestWithBody(
                "PUT",
                register_playback_url,
                // JSON.stringify({
                //     debug_source: "played_threshold_reached",

                // }),
                JSON.stringify(
                    { "seq_num": this.seq_num, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": this.duration - 158, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": this.duration - 158, "playback_stats": { "ms_total_est": this.duration, "ms_metadata_duration": 0, "ms_manifest_latency": 196, "ms_latency": 1188, "start_offset_ms": 14, "ms_initial_buffering": 451, "ms_initial_rebuffer": 451, "ms_seek_rebuffering": 0, "ms_stalled": 0, "max_ms_seek_rebuffering": 0, "max_ms_stalled": 0, "n_stalls": 0, "n_rendition_upgrade": 0, "n_rendition_downgrade": 0, "bps_bandwidth_max": 0, "bps_bandwidth_min": 0, "bps_bandwidth_avg": 0, "audiocodec": "mp4", "start_bitrate": 128000, "time_weighted_bitrate": 0, "key_system": "widevine", "ms_key_latency": 1796, "total_bytes": 3494928, "local_time_ms": Date.now(), "n_dropped_video_frames": 0, "n_total_video_frames": 0, "resolution_max": 0, "resolution_min": 0, "strategy": "MSE" }, "debug_source": "track_data_finalized" }
                ),
                { Authorization: `Bearer ${local_state.bearer_token}` },
                false).body)
            log(response1)
            this.seq_num += 1
            this.state_machine_id = response1.state_machine.state_machine_id




            log("triggering before play")
            const before_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`
            const response2: { 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": this.seq_num, "state_ref": { "state_machine_id": initial_state_machine_id, "state_id": this.playback_id, "paused": true }, "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(response2)
            this.state_machine_id = response2.state_machine.state_machine_id
            this.seq_num += 1

            // let res = 0
            // for(let i = 0; i<10000;i++){
            //     res = (res+1)*2/2
            // }


            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": this.seq_num, "state_ref": { "state_machine_id": initial_state_machine_id, "state_id": this.playback_id, "paused": true }, "sub_state": { "playback_speed": 0, "position": 0, "duration": this.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.seq_num += 1

            */
            this.another_one = true;
        }
        if (this.in_between) {
            return;
        }
        if (seconds - this.init_seconds > 50 && 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": this.seq_num, "state_ref": { "state_machine_id": this.state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 1, "position": 30786, "duration": this.duration, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10 }, "previous_position": 30786, "debug_source": "played_threshold_reached" }), { Authorization: `Bearer ${local_state.bearer_token}` }, false).body);
            log(response);
            this.seq_num += 1;
            this.state_machine_id = response.state_machine.state_machine_id;
            this.in_between = true;
        if (this.play_recorded) {
            return;
        }
        if (seconds - this.init_seconds > 15 && 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()
            //   }
            const initial_state_machine_id = this.state_machine_id;
            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": this.seq_num, "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;
            this.seq_num += 1;
            // let res = 0
            // for(let i = 0; i<10000;i++){
            //     res = (res+1)*2/2
            // }
            log("speed change 1");
            // const before_playling_url = `https://gue1-spclient.spotify.com/track-playback/v1/devices/${this.device_id}/state`
            const response3 = JSON.parse(local_http.requestWithBody("PUT", before_playling_url, 
            // JSON.stringify({
            //     debug_source: "played_threshold_reached",
            // }),
            JSON.stringify({ "seq_num": this.seq_num, "state_ref": { "state_machine_id": initial_state_machine_id, "state_id": this.playback_id, "paused": false }, "sub_state": { "playback_speed": 0, "position": 0, "duration": this.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.seq_num += 1;
            // this.state_machine_id = response3.state_machine.state_machine_id
            // res = 0
            // for(let i = 0; i<10000;i++){
            //     res = (res+1)*2/2
            // }
            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": this.seq_num, "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;
            this.seq_num += 1;
            // res = 0
            // for(let i = 0; i<10000;i++){
            //     res = (res+1)*2/2
            // }
            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": this.seq_num, "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.seq_num += 1;
            // 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:${this.album_uri_id}`,
                        url: `context://spotify:album:${this.album_uri_id}`,
                        "metadata": {}
                    },
                    "play_origin": {
                        "feature_identifier": "album",
                        // "feature_identifier": "track",
                        "feature_version": "web-player_2024-05-24_1716563359844_29d0a3b",
                        "referrer_identifier": "your_library"
                    },
                    "options": {
                        "license": "on-demand",
                        "skip_to": {
                            track_index: this.track_album_index,
                            track_uid: this.uid,
                            track_uri: this.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"
                }
            }), 
            // 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) {