Skip to content
Snippets Groups Projects
SpotifyScript.ts 147 KiB
Newer Older

                // 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)
            }
        })
    }
    override onInit(seconds: number): void {
        this.init_seconds = seconds
    }
    override onProgress(seconds: number, is_playing: boolean): void {

        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: { 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": 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: { 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": 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: { 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": 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: { readonly state_machine: { readonly state_machine_id: string } } = 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: string) {
    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: "image" | "mosaic" = match_result[1] as "image" | "mosaic"
    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: string): string {
    return parse_uri(uri).uri_id
}
function parse_uri(uri: string) {
    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: UriType = maybe_type as UriType
    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: number) {
    return new Date(milliseconds).toISOString().substring(11, 23)
function assert_never(value: never) {
    log(value)
}
function log_passthrough<T>(value: T): T {
    log(value)
    return value
}
function assert_exhaustive(value: never): void
function assert_exhaustive(value: never, exception_message: string): ScriptException
function assert_exhaustive(value: never, exception_message?: string): ScriptException | undefined {
    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: string[] = []
ee.length = 256
for (let ke = 0; ke < 256; ke++)
    // @ts-expect-error
    ee[ke] = Z[ke >> 4] + Z[15 & ke]
const te: number[] = []
te.length = 128
for (let ke = 0; ke < Q.length; ++ke)
    te[Q.charCodeAt(ke)] = ke

function get_gid(song_uri_id: string) {
    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
    get_gid,
    assert_never,
    log_passthrough,
    getPlaybackTracker