[GH-ISSUE #1475] [Notice] open.spotify.com/get_access_token No longer works #665

Closed
opened 2026-02-27 19:31:50 +03:00 by kerem · 74 comments
Owner

Originally created by @yodaluca23 on GitHub (Mar 14, 2025).
Original GitHub issue: https://github.com/librespot-org/librespot/issues/1475

Look for similar bugs

Please check if there's already an issue for your problem.
If you've only a "me too" comment to make, consider if a 👍 reaction
will suffice.

Description

A clear and concise description of what the problem is.
I don't know what Spotify changed but open.spotify.com/get_access_token no longer gives an accessToken, giving the error

{
  "error": {
    "code": 400,
    "message": "Invalid TOTP"
  }
}

Version

What version(s) of librespot does this problem exist in?
All

How to reproduce

Steps to reproduce the behavior in librespot e.g.

  1. Navigate to open.spotify.com/get_access_token as described by https://github.com/librespot-org/librespot/wiki/Options#access-token
  2. View error.

Log

Error Spotify gives:

{
  "error": {
    "code": 400,
    "message": "Invalid TOTP"
  }
}

Host (what you are running librespot on):

NA

Additional context

This really sucks for a lot of small OSS projects that depend on this for getting anonymous tokens aswell...

Originally created by @yodaluca23 on GitHub (Mar 14, 2025). Original GitHub issue: https://github.com/librespot-org/librespot/issues/1475 ### Look for similar bugs Please check if there's [already an issue](https://github.com/librespot-org/librespot/issues) for your problem. If you've only a "me too" comment to make, consider if a :+1: [reaction](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) will suffice. ### Description A clear and concise description of what the problem is. I don't know what Spotify changed but ```open.spotify.com/get_access_token``` no longer gives an accessToken, giving the error ``` { "error": { "code": 400, "message": "Invalid TOTP" } } ``` ### Version What version(s) of *librespot* does this problem exist in? All ### How to reproduce Steps to reproduce the behavior in *librespot* e.g. 1. Navigate to open.spotify.com/get_access_token as described by https://github.com/librespot-org/librespot/wiki/Options#access-token 2. View error. ### Log Error Spotify gives: ``` { "error": { "code": 400, "message": "Invalid TOTP" } } ``` ### Host (what you are running `librespot` on): NA ### Additional context This really sucks for a lot of small OSS projects that depend on this for getting anonymous tokens aswell...
kerem 2026-02-27 19:31:50 +03:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@kingosticks commented on GitHub (Mar 14, 2025):

That does suck. It was very useful for those projects that got screwed over by the recent API changes. Let's see if we can circumvent this check. It's always going to be cat and mouse with these guys.

<!-- gh-comment-id:2725634926 --> @kingosticks commented on GitHub (Mar 14, 2025): That does suck. It was very useful for those projects that got screwed over by the recent API changes. Let's see if we can circumvent this check. It's always going to be cat and mouse with these guys.
Author
Owner

@michaelherger commented on GitHub (Mar 15, 2025):

I don't use this feature. But the error message "Invalid TOTP" makes me wonder whether it's failing for you because you were using MFA, and whether it was simply an auth failure lack of a "time based one time password" (TOTP)?

<!-- gh-comment-id:2726371475 --> @michaelherger commented on GitHub (Mar 15, 2025): I don't use this feature. But the error message "Invalid TOTP" makes me wonder whether it's failing for you because you were using MFA, and whether it was simply an auth failure lack of a "time based one time password" (TOTP)?
Author
Owner

@kingosticks commented on GitHub (Mar 15, 2025):

No, they've changed something on their end to prevent public access. It was previously a super easy way to get an access token with lots (full?) permissions. It only required you have a couple of browser cookies set (done for you when you login to Spotify.com). It was particularly useful since that included access to all the recently deprecated endpoints. They've now closed that loophole as part of their ongoing effort to lose as many customers as possible.

I assume this endpoint is being used by their developer console, worth having a look there to see if we can undo this new protection.

<!-- gh-comment-id:2726393939 --> @kingosticks commented on GitHub (Mar 15, 2025): No, they've changed something on their end to prevent public access. It was previously a super easy way to get an access token with lots (full?) permissions. It only required you have a couple of browser cookies set (done for you when you login to Spotify.com). It was particularly useful since that included access to all the recently deprecated endpoints. They've now closed that loophole as part of their ongoing effort to lose as many customers as possible. I assume this endpoint is being used by their developer console, worth having a look there to see if we can undo this new protection.
Author
Owner

@Wikijito7 commented on GitHub (Mar 15, 2025):

This error also happened at Spotube, a custom Spotify player, and they found a solution.

It seems Spotify changed how the token thingy works. Now, it is required to have a new request to get the final access token. From the current request, you have to create a totp with a timestamp given by Spotify and request this access_token to then use it on the new clienttoken api request.

See:

Something similar like this would need to be implemented here in order to make Spotify integration work again.

<!-- gh-comment-id:2726445254 --> @Wikijito7 commented on GitHub (Mar 15, 2025): This error also happened at [Spotube](https://github.com/KRTirtho/spotube/issues/2494), a custom Spotify player, and they found a solution. It seems Spotify changed how the token thingy works. Now, it is required to have a new request to get the final access token. From the current request, you have to create a totp with a timestamp given by Spotify and request this access_token to then use it on the new clienttoken api request. See: - generateTotp from Spotube: https://github.com/KRTirtho/spotube/blob/59f298a935c87077a6abd50656f8a4ead44bd979/lib/provider/authentication/authentication.dart#L135 - generate credentials cookie: https://github.com/KRTirtho/spotube/blob/59f298a935c87077a6abd50656f8a4ead44bd979/lib/provider/authentication/authentication.dart#L184 Something similar like this would need to be implemented here in order to make Spotify integration work again.
Author
Owner

@kingosticks commented on GitHub (Mar 15, 2025):

Great! For the record, this is just one way to get an access token. We list 2 others on our wiki

<!-- gh-comment-id:2726577096 --> @kingosticks commented on GitHub (Mar 15, 2025): Great! For the record, this is just one way to get an access token. We list 2 others on our wiki
Author
Owner

@yodaluca23 commented on GitHub (Mar 15, 2025):

As others have noticed in the related issues on other projects, Spotify is changing this a lot, when I first made this issue, it returned the error in my original post. Then SpotAPI cracked the TOTP:

github.com/Aran404/SpotAPI@9061bdd53b

And we were able to get in and it gave this:

{
  "clientId": "CLIENTID",
  "accessToken": "ACCESSTOKEN",
  "accessTokenExpirationTimestampMs": TIMESTAMP,
  "isAnonymous": true,
  "totpValidity": -1,
  "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
}

Then a few hours after that commit they openned it up completely and with nothing needed, no params, just like originally it would give this:

{
  "clientId": "CLIENTID",
  "accessToken": "ACCESSTOKEN",
  "accessTokenExpirationTimestampMs": TIMESTAMP,
  "isAnonymous": true,
  "totpValidity": false,
  "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
}

Though in my testing Python Requests didn't work code 429? Idk why but http.client worked.

Now it looks like they've changed it once again, opening it with no params returns:

{
  "error": {
    "code": 400,
    "message": "Unauthorized request",
    "extra": {
      "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
    }
  }
}

While SpotAPIs workaround still works the same, just wanted to document this here, it's very interesting to see what Spotify will decide on.

<!-- gh-comment-id:2726583019 --> @yodaluca23 commented on GitHub (Mar 15, 2025): As others have noticed in the related issues on other projects, Spotify is changing this a lot, when I first made this issue, it returned the error in my original post. Then SpotAPI cracked the TOTP: https://github.com/Aran404/SpotAPI/commit/9061bdd53bbfc4b983394593bad6b7d4464245ed And we were able to get in and it gave this: ``` { "clientId": "CLIENTID", "accessToken": "ACCESSTOKEN", "accessTokenExpirationTimestampMs": TIMESTAMP, "isAnonymous": true, "totpValidity": -1, "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law" } ``` Then a few hours after that commit they openned it up completely and with nothing needed, no params, just like originally it would give this: ``` { "clientId": "CLIENTID", "accessToken": "ACCESSTOKEN", "accessTokenExpirationTimestampMs": TIMESTAMP, "isAnonymous": true, "totpValidity": false, "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law" } ``` Though in my testing Python Requests didn't work code 429? Idk why but http.client worked. Now it looks like they've changed it once again, opening it with no params returns: ``` { "error": { "code": 400, "message": "Unauthorized request", "extra": { "_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law" } } } ``` While SpotAPIs workaround still works the same, just wanted to document this here, it's very interesting to see what Spotify will decide on.
Author
Owner

@roderickvd commented on GitHub (Mar 15, 2025):

Welp, that note is something to take seriously.

<!-- gh-comment-id:2726653855 --> @roderickvd commented on GitHub (Mar 15, 2025): Welp, that note is something to take seriously.
Author
Owner

@yodaluca23 commented on GitHub (Mar 15, 2025):

Also, sometime between all that, (I'm assuming this is related, at least evidence of Spotify changing stuff) Spotify added TOTP login even for non MFA accounts on the web player, if you try logging in now, it'll send a code to your email, to login. It still gives a password login option though.

<!-- gh-comment-id:2726678382 --> @yodaluca23 commented on GitHub (Mar 15, 2025): Also, sometime between all that, (I'm assuming this is related, at least evidence of Spotify changing stuff) Spotify added TOTP login even for non MFA accounts on the web player, if you try logging in now, it'll send a code to your email, to login. It still gives a password login option though.
Author
Owner

@speedx77 commented on GitHub (Apr 29, 2025):

Thank god for y'all. Needed this badly to fix user search functionality in my app that became broke by their changes :)

<!-- gh-comment-id:2839759294 --> @speedx77 commented on GitHub (Apr 29, 2025): Thank god for y'all. Needed this badly to fix user search functionality in my app that became broke by their changes :)
Author
Owner

@greffgreff commented on GitHub (May 5, 2025):

Great! For the record, this is just one way to get an access token. We list 2 others on our wiki

Is there a link for it?

<!-- gh-comment-id:2850795358 --> @greffgreff commented on GitHub (May 5, 2025): > Great! For the record, this is just one way to get an access token. We list 2 others on our wiki Is there a link for it?
Author
Owner

@kingosticks commented on GitHub (May 5, 2025):

If you can't find it: https://github.com/librespot-org/librespot/wiki/Options#access-token

<!-- gh-comment-id:2850803719 --> @kingosticks commented on GitHub (May 5, 2025): If you can't find it: https://github.com/librespot-org/librespot/wiki/Options#access-token
Author
Owner

@sam9116 commented on GitHub (Jun 10, 2025):

looks like Spotify switched over to HMAC-based One-Time Password for authentication in their latest web player

there is no point retrieving server time anymore

Image

<!-- gh-comment-id:2957786713 --> @sam9116 commented on GitHub (Jun 10, 2025): looks like Spotify switched over to HMAC-based One-Time Password for authentication in their latest web player ~~there is no point retrieving server time anymore~~ ![Image](https://github.com/user-attachments/assets/340e2d92-cf10-4708-8d03-3d356d7d0b60)
Author
Owner

@aviwad commented on GitHub (Jun 10, 2025):

@sam9116 I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-)

<!-- gh-comment-id:2957862295 --> @aviwad commented on GitHub (Jun 10, 2025): @sam9116 I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-)
Author
Owner

@sam9116 commented on GitHub (Jun 10, 2025):

@sam9116 I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-)

Image

as far as I can see, the secrets are still being generated the same way. but you will need to implement a persistent counter that keeps track of how many times the OTP have been requested, since Spotify will keep track of that on their server, if the two numbers doesn't match => OTP incorrect => no token

I'm not sure how sTime and cTime are generated or why they are needed as part of the token request, will have to do more testing around that

Image

<!-- gh-comment-id:2957886661 --> @sam9116 commented on GitHub (Jun 10, 2025): > [@sam9116](https://github.com/sam9116) I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-) ![Image](https://github.com/user-attachments/assets/56db9c08-869d-4e8b-836a-6daf32baf814) as far as I can see, the secrets are still being generated the same way. but you will need to implement a persistent counter that keeps track of how many times the OTP have been requested, since Spotify will keep track of that on their server, if the two numbers doesn't match => OTP incorrect => no token I'm not sure how sTime and cTime are generated or why they are needed as part of the token request, will have to do more testing around that ![Image](https://github.com/user-attachments/assets/1ec08c0f-5e82-42c1-a3de-9f5b3387ee58)
Author
Owner

@sam9116 commented on GitHub (Jun 11, 2025):

interesting, it looks like the counter is actually implemented on the client side

Image

<!-- gh-comment-id:2961015826 --> @sam9116 commented on GitHub (Jun 11, 2025): interesting, it looks like the counter is actually implemented on the client side ![Image](https://github.com/user-attachments/assets/642064c1-f35e-4538-9e1d-eac4329f0629)
Author
Owner

@sam9116 commented on GitHub (Jun 11, 2025):

ok, I think I figured out how to get a legit access token from spotify again
the new spotify token url parameter is completed as follows

https://open.spotify.com/api/token?reason=init&productType=web-player&totp={ hotp }&totpServer={ hotp }&totpVer=5&sTime={ serverTimeStamp }&cTime={ timestamp }&buildVer={"web-player_2025-06-10_1749524883369_eef30f4"}&buildDate={"2025-06-10"}

otp is computed using the same secret byte array as discussed earlier in this issue

but instead of a totp, it is now an hotp

and you will need to supply a counter

counter is computed using your current time (unix timestamp, in seconds)
divided by 30, and then floored

it is still SHA1 hash algorithm, 6 digits,

totp and server totp will be the same value

severTimeStamp will be obtained from https://open.spotify.com/api/server-time, no credential or cookie required, you can reach this from an incognito browser

timestamp will be your current timestamp

buildVer and buildDate will be included in the spotify web player javascript file, you just need to search for buildVer and buildDate in the code

<!-- gh-comment-id:2961128642 --> @sam9116 commented on GitHub (Jun 11, 2025): ok, I think I figured out how to get a legit access token from spotify again the new spotify token url parameter is completed as follows > https://open.spotify.com/api/token?reason=init&productType=web-player&totp={ **hotp** }&totpServer={ **hotp** }&totpVer=5&sTime={ **serverTimeStamp** }&cTime={ **timestamp** }&buildVer={"web-player_2025-06-10_1749524883369_eef30f4"}&buildDate={"2025-06-10"} otp is computed using the same secret byte array as discussed earlier in this issue but instead of a totp, it is now an hotp and you will need to supply a counter counter is computed using your current time (unix timestamp, in seconds) divided by 30, and then floored it is still SHA1 hash algorithm, 6 digits, totp and server totp will be the same value **severTimeStamp** will be obtained from https://open.spotify.com/api/server-time, no credential or cookie required, you can reach this from an incognito browser **timestamp** will be your current timestamp **buildVer** and **buildDate** will be included in the spotify web player javascript file, you just need to search for buildVer and buildDate in the code
Author
Owner

@Optimuspime123 commented on GitHub (Jun 16, 2025):

@sam9116 Thank you for trying to figure this out. I still got a 400 error with that ominous note, but the note itself is also present in the response received by spotify web so I guess it's fine. However, I noticed another parameter totpValidUntil which pointed to a date in the past. Any clue on how that works/ if it's required?
My current code looks like so :

def get_spotify_access_token(sp_dc_cookie_value):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
    }

    #raw secret byte array
    raw_secret = bytes([12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54])
    b32_secret = base64.b32encode(raw_secret).decode('utf-8')
    hotp = pyotp.HOTP(b32_secret)

    #HOTP code
    now_sec = int(time.time())
    counter = now_sec // 30
    code = hotp.at(counter)

    server_resp = requests.get("https://open.spotify.com/api/server-time", headers=headers)
    server_ts = server_resp.json()['serverTime']

    url = (
        "https://open.spotify.com/api/token?"
        f"reason=init&productType=web-player"
        f"&totp={code}&totpServer={code}"
        f"&totpVer=5"
        f"&sTime={server_ts}&cTime={now_sec*1000}"
        "&buildVer=unknown"
        "&buildDate=unknown"
        "&totpValidUntil=Wed Jun 16 2025 20:34:15 GMT+0530 (India Standard Time)"
    )

    session = requests.Session()
    session.headers.update(headers)
    session.cookies.set("sp_dc", sp_dc_cookie_value, domain=".spotify.com")

    resp = session.get(url)
    if resp.ok:
        token = resp.json().get("accessToken")
        print("Access Token:", token)
        return token
    else:
        print("❌ Failed:", resp.status_code, resp.text)
        return None
<!-- gh-comment-id:2975394624 --> @Optimuspime123 commented on GitHub (Jun 16, 2025): @sam9116 Thank you for trying to figure this out. I still got a 400 error with that ominous note, but the note itself is also present in the response received by spotify web so I guess it's fine. However, I noticed another parameter totpValidUntil which pointed to a date in the past. Any clue on how that works/ if it's required? My current code looks like so : ```python def get_spotify_access_token(sp_dc_cookie_value): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' } #raw secret byte array raw_secret = bytes([12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54]) b32_secret = base64.b32encode(raw_secret).decode('utf-8') hotp = pyotp.HOTP(b32_secret) #HOTP code now_sec = int(time.time()) counter = now_sec // 30 code = hotp.at(counter) server_resp = requests.get("https://open.spotify.com/api/server-time", headers=headers) server_ts = server_resp.json()['serverTime'] url = ( "https://open.spotify.com/api/token?" f"reason=init&productType=web-player" f"&totp={code}&totpServer={code}" f"&totpVer=5" f"&sTime={server_ts}&cTime={now_sec*1000}" "&buildVer=unknown" "&buildDate=unknown" "&totpValidUntil=Wed Jun 16 2025 20:34:15 GMT+0530 (India Standard Time)" ) session = requests.Session() session.headers.update(headers) session.cookies.set("sp_dc", sp_dc_cookie_value, domain=".spotify.com") resp = session.get(url) if resp.ok: token = resp.json().get("accessToken") print("Access Token:", token) return token else: print("❌ Failed:", resp.status_code, resp.text) return None ```
Author
Owner

@sam9116 commented on GitHub (Jun 16, 2025):

@Optimuspime123

I don't think the totpValidUntil matters, I just tried it with my old implementation and it still works

<!-- gh-comment-id:2975436495 --> @sam9116 commented on GitHub (Jun 16, 2025): @Optimuspime123 I don't think the totpValidUntil matters, I just tried it with my old implementation and it still works
Author
Owner

@Optimuspime123 commented on GitHub (Jun 16, 2025):

@sam9116 I see, would you mind sharing which cookies (other than sp_dc ) you are using, if any? I added an user agent, too - but still get a 400. Shouldn't really be a language specific thing.

<!-- gh-comment-id:2975481293 --> @Optimuspime123 commented on GitHub (Jun 16, 2025): @sam9116 I see, would you mind sharing which cookies (other than sp_dc ) you are using, if any? I added an user agent, too - but still get a 400. Shouldn't really be a language specific thing.
Author
Owner

@sam9116 commented on GitHub (Jun 16, 2025):

@Optimuspime123 I use the entire cookie string provided by the spotify webclient

Image

<!-- gh-comment-id:2975492272 --> @sam9116 commented on GitHub (Jun 16, 2025): @Optimuspime123 I use the entire cookie string provided by the spotify webclient ![Image](https://github.com/user-attachments/assets/e19fe4dc-e6f9-4cf5-8166-6f5c1d7112cf)
Author
Owner

@Optimuspime123 commented on GitHub (Jun 16, 2025):

Thanks, I got it working too :)

<!-- gh-comment-id:2976507472 --> @Optimuspime123 commented on GitHub (Jun 16, 2025): Thanks, I got it working too :)
Author
Owner

@Thereallo1026 commented on GitHub (Jul 1, 2025):

Spotify has updated their TOTP secret used for the open.spotify.com/get_access_token endpoint (now open.spotify.com/api/token). The "Invalid TOTP" error occurs because the hardcoded secret in most implementations is outdated.

What Changed?

Spotify rotates their TOTP secrets periodically for security. I've observed that their implementation maintains an array of versioned secrets, and version 5 has been removed from their current rotation. When everything was working previously, version 5 was still available in their secret array. Now they've moved to newer versions, with version 8 being the current active secret.

{
  "validUntil": "2025-07-02T12:00:00.000Z",
  "secrets": [
    {
      "secret": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
      "version": 8
    },
    {
      "secret": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
      "version": 7
    },
    {
      "secret": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
      "version": 6
    }
  ]
}

The current secret array in Spotify's JavaScript contains versions 6, 7, and 8, but the old implementations were still using the old version 5 secret, which is why they suddenly stopped working.

Solution

The updated secret cipher bytes for version 8 are:

[37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22]

If you're using the old version 5 secret:

[12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54]

You need to update it to the new version 8 values above.

I've successfully tested this fix and can confirm it generates valid TOTP tokens that work with Spotify's current API.

<!-- gh-comment-id:3021776102 --> @Thereallo1026 commented on GitHub (Jul 1, 2025): Spotify has updated their TOTP secret used for the `open.spotify.com/get_access_token` endpoint (now `open.spotify.com/api/token`). The "Invalid TOTP" error occurs because the hardcoded secret in most implementations is outdated. ### What Changed? Spotify rotates their TOTP secrets periodically for security. I've observed that their implementation maintains an array of versioned secrets, and **version 5 has been removed from their current rotation**. When everything was working previously, version 5 was still available in their secret array. Now they've moved to newer versions, with **version 8** being the current active secret. ```json { "validUntil": "2025-07-02T12:00:00.000Z", "secrets": [ { "secret": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22], "version": 8 }, { "secret": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89], "version": 7 }, { "secret": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54], "version": 6 } ] } ``` The current secret array in Spotify's JavaScript contains versions 6, 7, and 8, but the old implementations were still using the old version 5 secret, which is why they suddenly stopped working. ### Solution The updated secret cipher bytes for version 8 are: ```json [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22] ``` If you're using the old version 5 secret: ```json [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54] ``` You need to update it to the new version 8 values above. I've successfully tested this fix and can confirm it generates valid TOTP tokens that work with Spotify's current API.
Author
Owner

@kingosticks commented on GitHub (Jul 1, 2025):

They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also.

<!-- gh-comment-id:3022487628 --> @kingosticks commented on GitHub (Jul 1, 2025): They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also.
Author
Owner

@sam9116 commented on GitHub (Jul 1, 2025):

They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also.

sure, if you are willing to create a new project/discussion for this and redirect-people over there.

I've setup a cron job to monitor the Spotify token acquisition process nightly, if it encounters problems, right now an email will be sent out to me, if a new point of discussion is set up I can set up some sort of automation via GitHub and notify everyone subscribed to it

<!-- gh-comment-id:3025798612 --> @sam9116 commented on GitHub (Jul 1, 2025): > They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also. sure, if you are willing to create a new project/discussion for this and redirect-people over there. I've setup a cron job to monitor the Spotify token acquisition process nightly, if it encounters problems, right now an email will be sent out to me, if a new point of discussion is set up I can set up some sort of automation via GitHub and notify everyone subscribed to it
Author
Owner

@Thereallo1026 commented on GitHub (Jul 2, 2025):

Initially, the process relied on a v8 secret stored in a hardcoded JSON object. As we saw, that JSON had a validUntil field, and right on schedule, Spotify updated the logic.

The next version I analyzed, v9, moved away from the JSON object to a heavily obfuscated string value for its secret. The derivation logic was also new, requiring a multi step process: it took the initial string, performed an XOR cipher on its character codes, converted those values back into a new intermediate string, and then UTF8 encoded that string to get the final bytes for the Base32 key.

The biggest challenge is that halfway through my reverse engineering of that v9 system, they appear to have pushed another significant update. The code has been completely refactored, making the old function names and structures obsolete.

Here are my latest findings from the newest code, based on tracing the live network requests:

  1. New Function Names: The call stack now points to a master function called it(e), which in turn calls a parameter generator function, nt(...), to get the values for the API call.

  2. Dual TOTP Generation Confirmed: The most important discovery is that there are two separate TOTP values required: totp and totpServer.

  3. Timestamp Logic: My debugging shows totp is generated using the client's local clock (Date.now()), while totpServer is generated using a timestamp fetched from the https://open.spotify.com/api/server-time endpoint. There is also a fallback mechanism: if the call to get the server time fails, the code just uses the totp value for both parameters.

Despite reimplementing this exact logic (deriving the secret and generating two distinct TOTPs with their respective timestamps), my requests are still being rejected with an "Unauthorized request" error. This suggests the secret derivation is even more complex than I've mapped out, or there's another subtle detail I am missing.

Posting my findings here in case it helps anyone else looking into this or if a fresh pair of eyes can spot something I've overlooked. I'll keep at it.

<!-- gh-comment-id:3025959055 --> @Thereallo1026 commented on GitHub (Jul 2, 2025): Initially, the process relied on a `v8` secret stored in a hardcoded JSON object. As we saw, that JSON had a `validUntil` field, and right on schedule, Spotify updated the logic. The next version I analyzed, `v9`, moved away from the JSON object to a heavily obfuscated string value for its secret. The derivation logic was also new, requiring a multi step process: it took the initial string, performed an XOR cipher on its character codes, converted those values back into a new intermediate string, and then UTF8 encoded *that* string to get the final bytes for the Base32 key. The biggest challenge is that halfway through my reverse engineering of that `v9` system, they appear to have pushed another significant update. The code has been completely refactored, making the old function names and structures obsolete. Here are my latest findings from the newest code, based on tracing the live network requests: 1. **New Function Names:** The call stack now points to a master function called `it(e)`, which in turn calls a parameter generator function, `nt(...)`, to get the values for the API call. 2. **Dual TOTP Generation Confirmed:** The most important discovery is that there are two separate TOTP values required: `totp` and `totpServer`. 3. **Timestamp Logic:** My debugging shows `totp` is generated using the client's local clock (`Date.now()`), while `totpServer` is generated using a timestamp fetched from the `https://open.spotify.com/api/server-time` endpoint. There is also a fallback mechanism: if the call to get the server time fails, the code just uses the `totp` value for both parameters. Despite reimplementing this exact logic (deriving the secret and generating two distinct TOTPs with their respective timestamps), my requests are still being rejected with an "Unauthorized request" error. This suggests the secret derivation is even more complex than I've mapped out, or there's another subtle detail I am missing. Posting my findings here in case it helps anyone else looking into this or if a fresh pair of eyes can spot something I've overlooked. I'll keep at it.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 2, 2025):

I managed to find the new secret definitions. They've gotten clever and have split the version 9 secret string into three parts to prevent searching for it directly in the source. The string is only reconstructed at runtime.

Here is a snippet showing the definitions for the three most recent versions that I found in the latest code:

// This is where they define the new v9 secret object (obfuscated as 'Ue').
// Notice the secret is concatenated from three parts.
Ue[Le(794, 0, 0, 785)] = Me(585, 569, 584, 578) + Me(569, 586, 573, 580) + "9+$QaH5)N8",
Ue[Le(788, 0, 0, 790)] = 9;

// The v8 secret object for comparison (obfuscated as 'Be').
const Be = {};
Be[Me(0, 586, 0, 591)] = [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
Be.version = 8;

// The v7 secret is now hardcoded directly in an object literal.
const Fe = {
    secret: [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
    version: 7
};

I was able to inspect the final Ue object and can confirm the secret string is the same as the initial v9 version that I was reversing (before they pushed another update):

{
    "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
    "version": 9
}

So, the good news is that the secret string and the derivation logic (XOR -> new string -> UTF8 bytes -> Base32) seem to be confirmed.

The bad news is that my implementation using this key and the dual timestamp logic (local time for totp, server time for totpServer) is still resulting in an "Unauthorized request" error.

I wanted to share this in case it helps anyone else, or if someone spots a flaw in my logic. I'm going to try comparing the final TOTP values generated by my code directly against the ones generated by the browser's JS to see if they differ. I will post another update if I find anything.

<!-- gh-comment-id:3025967835 --> @Thereallo1026 commented on GitHub (Jul 2, 2025): I managed to find the new secret definitions. They've gotten clever and have split the version 9 secret string into three parts to prevent searching for it directly in the source. The string is only reconstructed at runtime. Here is a snippet showing the definitions for the three most recent versions that I found in the latest code: ```javascript // This is where they define the new v9 secret object (obfuscated as 'Ue'). // Notice the secret is concatenated from three parts. Ue[Le(794, 0, 0, 785)] = Me(585, 569, 584, 578) + Me(569, 586, 573, 580) + "9+$QaH5)N8", Ue[Le(788, 0, 0, 790)] = 9; // The v8 secret object for comparison (obfuscated as 'Be'). const Be = {}; Be[Me(0, 586, 0, 591)] = [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22], Be.version = 8; // The v7 secret is now hardcoded directly in an object literal. const Fe = { secret: [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89], version: 7 }; ``` I was able to inspect the final `Ue` object and can confirm the secret string is the same as the initial `v9` version that I was reversing (before they pushed another update): ```json { "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8", "version": 9 } ``` So, the good news is that the secret string and the derivation logic (XOR -> new string -> UTF8 bytes -> Base32) seem to be confirmed. The bad news is that my implementation using this key and the dual timestamp logic (local time for `totp`, server time for `totpServer`) is still resulting in an "Unauthorized request" error. I wanted to share this in case it helps anyone else, or if someone spots a flaw in my logic. I'm going to try comparing the final TOTP values generated by my code directly against the ones generated by the browser's JS to see if they differ. I will post another update if I find anything.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 2, 2025):

They still condense everything into one object after all of the logic:

            Ve[Me(0, 597, 0, 594)] = "2025-07-04" + Le(772, 0, 0, 780) + Le(778, 0, 0, 773),
            Ve[Me(0, 585, 0, 587)] = [Ue, Be, Fe];
            const He = Ve;

The object Ve has the exact same format as the old hardcoded JSON string:

{
  "validUntil": "2025-07-04T13:00:00.000Z",
  "secrets": [
    {
      "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
      "version": 9
    },
    {
      "secret": [37,84,32,76,87,90,87,47,13,75,48,54,44,28,19,21,22],
      "version": 8
    },
    {
      "secret": [59,91,66,74,30,66,74,38,46,50,72,61,44,71,86,39,89]
      "version": 7
    }
  ]
}

This proves that all the complex, obfuscated code: the string splitting, the function wrappers, is a sophisticated obfuscation layer designed only to build this final object at runtime.

<!-- gh-comment-id:3025990804 --> @Thereallo1026 commented on GitHub (Jul 2, 2025): They still condense everything into one object after all of the logic: ```js Ve[Me(0, 597, 0, 594)] = "2025-07-04" + Le(772, 0, 0, 780) + Le(778, 0, 0, 773), Ve[Me(0, 585, 0, 587)] = [Ue, Be, Fe]; const He = Ve; ``` The object `Ve` has the exact same format as the old hardcoded JSON string: ```json { "validUntil": "2025-07-04T13:00:00.000Z", "secrets": [ { "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8", "version": 9 }, { "secret": [37,84,32,76,87,90,87,47,13,75,48,54,44,28,19,21,22], "version": 8 }, { "secret": [59,91,66,74,30,66,74,38,46,50,72,61,44,71,86,39,89] "version": 7 } ] } ``` This proves that all the complex, obfuscated code: the string splitting, the function wrappers, is a sophisticated obfuscation layer designed only to build this final object at runtime.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 2, 2025):

I have successfully reverse-engineered the entire parameter generation flow for the /api/token endpoint in the latest version of the web player.

Here's what I can confirm:

  1. I can reliably find the master function that calls the parameter generator.
  2. I can prove with the debugger that the function generates two distinct TOTP values: totp (from Date.now()) and totpServer (from the /api/server-time endpoint), and it correctly falls back to using the totp value for both if the server time call fails.
  3. By calling their internal function from the console, I can get a valid parameter object that works perfectly in a curl request. This proves the entire logical flow is understood.

I was able to intercept the secret object right before it's used by their internal TOTP generator. This is the final state of the data after all the string splitting, XORing, and other derivations have been completed. The object contains an array of bytes.

{"secret":{"bytes":{"0":49,"1":48,"2":48,"3":49,"4":49,"5":49,"6":56,"7":49,"8":49,"9":49,"10":49,"11":55,"12":57,"13":56,"14":50,"15":49,"16":50,"17":51,"18":49,"19":50,"20":52,"21":54,"22":56,"23":56,"24":52,"25":54,"26":57,"27":51,"28":55,"29":56,"30":49,"31":51,"32":50,"33":54,"34":52,"35":52,"36":50,"37":56,"38":49,"39":57,"40":57,"41":52,"42":55,"43":57,"44":50,"45":51,"46":54,"47":53,"48":51,"49":53,"50":57,"51":49,"52":49,"53":51,"54":54,"55":52,"56":49,"57":48,"58":54,"59":50,"60":50,"61":49,"62":51,"63":49,"64":48,"65":55,"66":51,"67":48}},"version":9}
<!-- gh-comment-id:3026062994 --> @Thereallo1026 commented on GitHub (Jul 2, 2025): I have successfully reverse-engineered the entire parameter generation flow for the `/api/token` endpoint in the latest version of the web player. **Here's what I can confirm:** 1. I can reliably find the master function that calls the parameter generator. 2. I can prove with the debugger that the function generates two distinct TOTP values: `totp` (from `Date.now()`) and `totpServer` (from the `/api/server-time` endpoint), and it correctly falls back to using the `totp` value for both if the server time call fails. 3. By calling their internal function from the console, I can get a valid parameter object that works perfectly in a `curl` request. This proves the entire logical flow is understood. I was able to intercept the secret object right before it's used by their internal TOTP generator. This is the final state of the data after all the string splitting, XORing, and other derivations have been completed. The object contains an array of bytes. ```json {"secret":{"bytes":{"0":49,"1":48,"2":48,"3":49,"4":49,"5":49,"6":56,"7":49,"8":49,"9":49,"10":49,"11":55,"12":57,"13":56,"14":50,"15":49,"16":50,"17":51,"18":49,"19":50,"20":52,"21":54,"22":56,"23":56,"24":52,"25":54,"26":57,"27":51,"28":55,"29":56,"30":49,"31":51,"32":50,"33":54,"34":52,"35":52,"36":50,"37":56,"38":49,"39":57,"40":57,"41":52,"42":55,"43":57,"44":50,"45":51,"46":54,"47":53,"48":51,"49":53,"50":57,"51":49,"52":49,"53":51,"54":54,"55":52,"56":49,"57":48,"58":54,"59":50,"60":50,"61":49,"62":51,"63":49,"64":48,"65":55,"66":51,"67":48}},"version":9} ```
Author
Owner

@Micg25 commented on GitHub (Jul 4, 2025):

that's the secret for totp version 10:

byte_list = 0: 53, 1: 50, 2: 49, 3: 48, 4: 48, 5: 52, 6: 57, 7: 49, 8: 49, 9: 48, 10: 52, 11: 54, 12: 54, 13: 53, 14: 49, 15: 50, 16: 50, 17: 56, 18: 53, 19: 51, 20: 49, 21: 57, 22: 57, 23: 48, 24: 55, 25: 57, 26: 49, 27: 49, 28: 52, 29: 56, 30: 48, 31: 55, 32: 53, 33: 54, 34: 50, 35: 49, 36: 50, 37: 53, 38: 53, 39: 49, 40: 56, 41: 49

as a python variable:
secret=b"535049484852574949485254545349505056534949575748555749495256485553545049505353495649"

<!-- gh-comment-id:3036748902 --> @Micg25 commented on GitHub (Jul 4, 2025): that's the secret for totp version 10: byte_list = 0: 53, 1: 50, 2: 49, 3: 48, 4: 48, 5: 52, 6: 57, 7: 49, 8: 49, 9: 48, 10: 52, 11: 54, 12: 54, 13: 53, 14: 49, 15: 50, 16: 50, 17: 56, 18: 53, 19: 51, 20: 49, 21: 57, 22: 57, 23: 48, 24: 55, 25: 57, 26: 49, 27: 49, 28: 52, 29: 56, 30: 48, 31: 55, 32: 53, 33: 54, 34: 50, 35: 49, 36: 50, 37: 53, 38: 53, 39: 49, 40: 56, 41: 49 as a python variable: secret=b"535049484852574949485254545349505056534949575748555749495256485553545049505353495649"
Author
Owner

@Thereallo1026 commented on GitHub (Jul 5, 2025):

{
    "validUntil": "2025-07-07T09:00:00.000Z",
    "secrets": [
        {
            "secret": "=n:b#OuEfH\\fE])e*K",
            "version": 10
        },
        {
            "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
            "version": 9
        },
        {
            "secret": [
                37,
                84,
                32,
                76,
                87,
                90,
                87,
                47,
                13,
                75,
                48,
                54,
                44,
                28,
                19,
                21,
                22
            ],
            "version": 8
        }
    ]
}
<!-- gh-comment-id:3038830033 --> @Thereallo1026 commented on GitHub (Jul 5, 2025): ```json { "validUntil": "2025-07-07T09:00:00.000Z", "secrets": [ { "secret": "=n:b#OuEfH\\fE])e*K", "version": 10 }, { "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8", "version": 9 }, { "secret": [ 37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22 ], "version": 8 } ] } ```
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 6, 2025):

here got version 10
Ve/Ge:
Image

Ze:

{
  "secret": {
    "bytes": {
      "0": 53,
      "1": 50,
      "2": 49,
      "3": 48,
      "4": 48,
      "5": 52,
      "6": 57,
      "7": 49,
      "8": 49,
      "9": 48,
      "10": 52,
      "11": 54,
      "12": 54,
      "13": 53,
      "14": 49,
      "15": 50,
      "16": 50,
      "17": 56,
      "18": 53,
      "19": 49,
      "20": 49,
      "21": 57,
      "22": 57,
      "23": 48,
      "24": 55,
      "25": 57,
      "26": 49,
      "27": 49,
      "28": 52,
      "29": 56,
      "30": 48,
      "31": 55,
      "32": 53,
      "33": 54,
      "34": 50,
      "35": 49,
      "36": 50,
      "37": 53,
      "38": 53,
      "39": 49,
      "40": 56,
      "41": 49
    }
  },
  "version": 10
}
<!-- gh-comment-id:3041068985 --> @DiamondRoPlayz commented on GitHub (Jul 6, 2025): here got version 10 Ve/Ge: <img width="448" height="414" alt="Image" src="https://github.com/user-attachments/assets/e2e01ea5-15a9-4fbd-9b84-178efe38aaba" /> Ze: ```json { "secret": { "bytes": { "0": 53, "1": 50, "2": 49, "3": 48, "4": 48, "5": 52, "6": 57, "7": 49, "8": 49, "9": 48, "10": 52, "11": 54, "12": 54, "13": 53, "14": 49, "15": 50, "16": 50, "17": 56, "18": 53, "19": 49, "20": 49, "21": 57, "22": 57, "23": 48, "24": 55, "25": 57, "26": 49, "27": 49, "28": 52, "29": 56, "30": 48, "31": 55, "32": 53, "33": 54, "34": 50, "35": 49, "36": 50, "37": 53, "38": 53, "39": 49, "40": 56, "41": 49 } }, "version": 10 } ```
Author
Owner

@misiektoja commented on GitHub (Jul 6, 2025):

Hi @Thereallo1026,

Thanks for giving me a heads-up about the expiring old versions of cipher bytes. I had some free time today, so I took a look at what you did. Overall, you did a fantastic job debugging the whole thing on the client side. You were super close! The secret you got is correct, but it seems starting with v10, Spotify is serving the secret in a different format and thus all the confusion. Try this for v10 and it will work:

[61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75]

PoC: spotify_monitor_totp_test.py, more info in the Debugging Tools section of the spotify_monitor project.

How? Just make a simple list with the plain secret string you managed to get:

 raw10 = b"=n:b#OuEfH\\fE])e*K"

print(list(raw10))
[61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75]

EDIT: updated link

<!-- gh-comment-id:3042334661 --> @misiektoja commented on GitHub (Jul 6, 2025): Hi @Thereallo1026, Thanks for giving me a heads-up about the expiring old versions of cipher bytes. I had some free time today, so I took a look at what you did. Overall, you did a fantastic job debugging the whole thing on the client side. You were super close! The secret you got is correct, but it seems starting with v10, Spotify is serving the secret in a different format and thus all the confusion. Try this for v10 and it will work: ` [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75]` PoC: [spotify_monitor_totp_test.py](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_totp_test.py), more info in the Debugging Tools section of the [spotify_monitor](https://github.com/misiektoja/spotify_monitor#debugging-tools) project. How? Just make a simple list with the plain secret string you managed to get: ``` raw10 = b"=n:b#OuEfH\\fE])e*K" print(list(raw10)) [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75] ``` EDIT: updated link
Author
Owner

@Thereallo1026 commented on GitHub (Jul 6, 2025):

A massive thank you to @misiektoja for providing the final clue! The token generation is now working perfectly for me.

The completely non-obvious step that I was missing was the transformation that happens after the XOR cipher. My incorrect assumption was to convert the resulting array of byte values directly back to a string or to Base32.

As misiektoja's script showed, the correct process is effective:

  1. Perform the XOR cipher to get an array of numbers.
  2. Convert each number in that array to its string representation (e.g., [44, 94, 39] becomes the single string "449439").
  3. That giant string of digits then goes through a "hex round-trip": it's UTF-8 encoded, converted to a hex string, and then that hex string is converted back into the final raw bytes.
  4. Those final bytes are what get Base32 encoded to create the key for the TOTP.

Below is my TypeScript implmentation:

import { TOTP } from "totp-generator";

// generates a TOTP based on Spotify server time
async function generateTotp(): Promise<string> {
	const secretSauce = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

	const secretArray = "=n:b#OuEfH\\fE])e*K"
		.split("")
		.map((char) => char.charCodeAt(0));
	const secretCipherBytes = secretArray.map((e, t) => e ^ ((t % 33) + 9));

	const secretBytes = cleanBuffer(
		new TextEncoder()
			.encode(secretCipherBytes.join(""))
			.reduce((acc, val) => acc + val.toString(16).padStart(2, "0"), ""),
	);

	const secret = base32FromBytes(secretBytes, secretSauce);

	return TOTP.generate(secret).otp;
}

const totp = await generateTotp();
<!-- gh-comment-id:3042895080 --> @Thereallo1026 commented on GitHub (Jul 6, 2025): A massive thank you to @misiektoja for providing the final clue! The token generation is now working perfectly for me. The completely non-obvious step that I was missing was the transformation that happens **after** the XOR cipher. My incorrect assumption was to convert the resulting array of byte values directly back to a string or to Base32. As misiektoja's script showed, the correct process is effective: 1. Perform the XOR cipher to get an array of numbers. 2. Convert each number in that array to its string representation (e.g., `[44, 94, 39]` becomes the single string `"449439"`). 3. That giant string of digits then goes through a "hex round-trip": it's UTF-8 encoded, converted to a hex string, and then that hex string is converted back into the final raw bytes. 4. Those final bytes are what get Base32 encoded to create the key for the TOTP. Below is my TypeScript implmentation: ```typescript import { TOTP } from "totp-generator"; // generates a TOTP based on Spotify server time async function generateTotp(): Promise<string> { const secretSauce = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const secretArray = "=n:b#OuEfH\\fE])e*K" .split("") .map((char) => char.charCodeAt(0)); const secretCipherBytes = secretArray.map((e, t) => e ^ ((t % 33) + 9)); const secretBytes = cleanBuffer( new TextEncoder() .encode(secretCipherBytes.join("")) .reduce((acc, val) => acc + val.toString(16).padStart(2, "0"), ""), ); const secret = base32FromBytes(secretBytes, secretSauce); return TOTP.generate(secret).otp; } const totp = await generateTotp(); ```
Author
Owner

@matthewcamilizer commented on GitHub (Jul 7, 2025):

{
"validUntil": "2025-07-07T09:00:00.000Z",
"secrets": [
{
"secret": "=n:b#OuEfH\fE])e*K",
"version": 10
},
{
"secret": "meZcB\tlUFV1D6W2Hy4@9+$QaH5)N8",
"version": 9
},
{
"secret": [
37,
84,
32,
76,
87,
90,
87,
47,
13,
75,
48,
54,
44,
28,
19,
21,
22
],
"version": 8
}
]
}

How did you find the latest secret? is it an encrypted value in "web-player.xxx.js"?

<!-- gh-comment-id:3043721414 --> @matthewcamilizer commented on GitHub (Jul 7, 2025): > { > "validUntil": "2025-07-07T09:00:00.000Z", > "secrets": [ > { > "secret": "=n:b#OuEfH\\fE])e*K", > "version": 10 > }, > { > "secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8", > "version": 9 > }, > { > "secret": [ > 37, > 84, > 32, > 76, > 87, > 90, > 87, > 47, > 13, > 75, > 48, > 54, > 44, > 28, > 19, > 21, > 22 > ], > "version": 8 > } > ] > } How did you find the latest secret? is it an encrypted value in "web-player.xxx.js"?
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

Image

4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548

{
  "0": 49,
  "1": 48,
  "2": 50,
  "3": 51,
  "4": 57,
  "5": 51,
  "6": 53,
  "7": 54,
  "8": 57,
  "9": 56,
  "10": 50,
  "11": 54,
  "12": 56,
  "13": 52,
  "14": 52,
  "15": 54,
  "16": 57,
  "17": 49,
  "18": 50,
  "19": 48,
  "20": 49,
  "21": 50,
  "22": 49,
  "23": 52,
  "24": 55,
  "25": 49,
  "26": 50,
  "27": 50,
  "28": 51,
  "29": 52,
  "30": 57,
  "31": 52,
  "32": 56,
  "33": 50,
  "34": 57,
  "35": 52,
  "36": 49,
  "37": 48,
  "38": 55,
  "39": 55,
  "40": 51,
  "41": 51,
  "42": 54,
  "43": 54,
  "44": 56,
  "45": 55,
  "46": 48
}
<!-- gh-comment-id:3048041801 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): <img width="540" height="389" alt="Image" src="https://github.com/user-attachments/assets/4f4fb571-8c86-4165-89d7-3c97200ec54e" /> `4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548` ```json { "0": 49, "1": 48, "2": 50, "3": 51, "4": 57, "5": 51, "6": 53, "7": 54, "8": 57, "9": 56, "10": 50, "11": 54, "12": 56, "13": 52, "14": 52, "15": 54, "16": 57, "17": 49, "18": 50, "19": 48, "20": 49, "21": 50, "22": 49, "23": 52, "24": 55, "25": 49, "26": 50, "27": 50, "28": 51, "29": 52, "30": 57, "31": 52, "32": 56, "33": 50, "34": 57, "35": 52, "36": 49, "37": 48, "38": 55, "39": 55, "40": 51, "41": 51, "42": 54, "43": 54, "44": 56, "45": 55, "46": 48 } ```
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

got a few formats for this

Image
{
    "11": [111, 45, 40, 73, 95, 74, 35, 85, 105, 107, 60, 110, 55, 72, 69, 70, 114, 83, 63, 88, 91],
    "10": [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75],
    "9": [109, 101, 90, 99, 66, 92, 116, 108, 85, 70, 86, 49, 68, 54, 87, 50, 72, 121, 52, 64, 57, 43, 36, 81, 97, 72, 53, 41, 78, 56],
    "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
    "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
    "6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
    "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54],
}
<!-- gh-comment-id:3048400992 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): got a few formats for this <img width="927" height="548" alt="Image" src="https://github.com/user-attachments/assets/9765aa48-297f-4711-bb01-c6b2d02049f4" /> ```py { "11": [111, 45, 40, 73, 95, 74, 35, 85, 105, 107, 60, 110, 55, 72, 69, 70, 114, 83, 63, 88, 91], "10": [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75], "9": [109, 101, 90, 99, 66, 92, 116, 108, 85, 70, 86, 49, 68, 54, 87, 50, 72, 121, 52, 64, 57, 43, 36, 81, 97, 72, 53, 41, 78, 56], "8": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22], "7": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89], "6": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54], "5": [12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54], } ```
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

ok the variables that store the secrets got updated from const Ge=Ve to He=Be idk what else
Image

<!-- gh-comment-id:3048597490 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): ok the variables that store the secrets got updated from const `Ge=Ve` to `He=Be` idk what else <img width="507" height="340" alt="Image" src="https://github.com/user-attachments/assets/1bb84394-ccb5-4a31-a14f-962bdd234c1d" />
Author
Owner

@Thereallo1026 commented on GitHub (Jul 8, 2025):

ok the variables that store the secrets got updated from const Ge=Ve to He=Be idk what else Image

That's absolutely normal, the values are obfuscated randomly on new builds, and since we have already found the way that they are doing it, the next step is not to monitor their code changes but to figure out an automated solution.

<!-- gh-comment-id:3048632491 --> @Thereallo1026 commented on GitHub (Jul 8, 2025): > ok the variables that store the secrets got updated from const `Ge=Ve` to `He=Be` idk what else <img alt="Image" width="507" height="340" src="https://private-user-images.githubusercontent.com/61511674/463670070-1bb84394-ccb5-4a31-a14f-962bdd234c1d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTE5NzYyMTIsIm5iZiI6MTc1MTk3NTkxMiwicGF0aCI6Ii82MTUxMTY3NC80NjM2NzAwNzAtMWJiODQzOTQtY2NiNS00YTMxLWExNGYtOTYyYmRkMjM0YzFkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA3MDglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNzA4VDExNTgzMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTY1ZDhiNzY3MzVkNzFhZTgzYTdjYTFmNDFjNTFjYzhhNGJiODE1ZWNiNDI0YmUwMzdiMjhjNjhhODE4OWUwZjEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.87VMcPrr93wZ52oJ703Y7PdvoNea0lL54qzBSzC7iDI"> That's absolutely normal, the values are obfuscated randomly on new builds, and since we have already found the way that they are doing it, the next step is not to monitor their code changes but to figure out an automated solution.
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

yeah that's what I'm tryna think of. don't know if regex would even be enough since for YouTube I use regex I wrote to extract their decoder for sig but it's quite different

<!-- gh-comment-id:3048643801 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): yeah that's what I'm tryna think of. don't know if regex would even be enough since for YouTube I use regex I wrote to extract their decoder for sig but it's quite different
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

trying with puppeteer ofc

Image
<!-- gh-comment-id:3048892397 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): trying with puppeteer ofc <img width="555" height="256" alt="Image" src="https://github.com/user-attachments/assets/53658139-8ae5-4435-bbd4-65e0fe54cc4c" />
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 8, 2025):

Don't know how long this will work for tho

Image
<!-- gh-comment-id:3048932574 --> @DiamondRoPlayz commented on GitHub (Jul 8, 2025): Don't know how long this will work for tho <img width="733" height="340" alt="Image" src="https://github.com/user-attachments/assets/67c93df1-d8cb-4ea5-83cb-6d0218567748" />
Author
Owner

@staniel359 commented on GitHub (Jul 8, 2025):

@DiamondRoPlayz Would you mind sharing your code for obtaining these values?

<!-- gh-comment-id:3050385175 --> @staniel359 commented on GitHub (Jul 8, 2025): @DiamondRoPlayz Would you mind sharing your code for obtaining these values?
Author
Owner

@infinity0 commented on GitHub (Jul 8, 2025):

PoC: spotify_monitor_totp_test.py

Amazing work, thanks. Confirmed working with me. In case someone needs to further automate grabbing the sp_dc cookie which is HttpOnly and therefore not available via document.cookie, I wrote a script in https://github.com/jpramosi/geckordp/pull/17 that automates this via Firefox's remote debugging protocol.

<!-- gh-comment-id:3050515724 --> @infinity0 commented on GitHub (Jul 8, 2025): > PoC: [spotify_monitor_totp_test.py](https://github.com/misiektoja/spotify_monitor/blob/main/debug/spotify_monitor_totp_test.py) Amazing work, thanks. Confirmed working with me. In case someone needs to further automate grabbing the `sp_dc` cookie which is HttpOnly and therefore not available via `document.cookie`, I wrote a script in https://github.com/jpramosi/geckordp/pull/17 that automates this via Firefox's remote debugging protocol.
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

ok the variables that store the secrets got updated from const Ge=Ve to He=Be idk what else Image

That's absolutely normal, the values are obfuscated randomly on new builds, and since we have already found the way that they are doing it, the next step is not to monitor their code changes but to figure out an automated solution.

I doubt there will be an automated solution, Spotify engineers can keep changing the way they express these secrets in the code in arbitrary ways. The only question is does the Spotify engineering manager want to keep spending engineering time, money and attention on this bullshit, just to stop a bunch of hobbyists who are of no threat to their company. Cringey late capitalism vibes.

<!-- gh-comment-id:3052223895 --> @infinity0 commented on GitHub (Jul 9, 2025): > > ok the variables that store the secrets got updated from const `Ge=Ve` to `He=Be` idk what else <img alt="Image" width="507" height="340" src="https://private-user-images.githubusercontent.com/61511674/463670070-1bb84394-ccb5-4a31-a14f-962bdd234c1d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTE5NzYyMTIsIm5iZiI6MTc1MTk3NTkxMiwicGF0aCI6Ii82MTUxMTY3NC80NjM2NzAwNzAtMWJiODQzOTQtY2NiNS00YTMxLWExNGYtOTYyYmRkMjM0YzFkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA3MDglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNzA4VDExNTgzMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTY1ZDhiNzY3MzVkNzFhZTgzYTdjYTFmNDFjNTFjYzhhNGJiODE1ZWNiNDI0YmUwMzdiMjhjNjhhODE4OWUwZjEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.87VMcPrr93wZ52oJ703Y7PdvoNea0lL54qzBSzC7iDI"> > > That's absolutely normal, the values are obfuscated randomly on new builds, and since we have already found the way that they are doing it, the next step is not to monitor their code changes but to figure out an automated solution. I doubt there will be an automated solution, Spotify engineers can keep changing the way they express these secrets in the code in arbitrary ways. The only question is does the Spotify engineering manager want to keep spending engineering time, money and attention on this bullshit, just to stop a bunch of hobbyists who are of no threat to their company. Cringey late capitalism vibes.
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

Secret for TOTP version 12:

secret = [57, 56, 57, 49, 53, 56, 53, 51, 55, 56, 56, 51, 56, 56, 54, 53, 56, 52, 56, 49, 53, 57, 55, 51, 51, 55, 51, 54, 53, 55, 54, 55, 55, 49, 49, 48, 55, 53, 48, 49, 49, 50, 56, 48, 49, 49, 55, 54, 49]

This is the "transformed" secret that you pass directly into the TOTP algorithm, i.e. you can skip the XOR/blah stuff described a bunch of posts back. Python code to use:

>>> secret32 = base64.b32encode(bytes(secret)).decode().rstrip("=")
>>> pyotp.TOTP(secret32, digits=6, interval=30).at(1752066280)
'605735'

BOOOOOOOOOOORIIIIIIING

You can find this by setting a breakpoint in the debugger directly before the TOTP function is called. Search in the source code for "totpServer" and it's a few lines up from there. Takes a few minutes. Spotify you are just wasting your own developer time & money with this shit lmfaooooo

<!-- gh-comment-id:3052644916 --> @infinity0 commented on GitHub (Jul 9, 2025): Secret for TOTP version 12: ~~~~ secret = [57, 56, 57, 49, 53, 56, 53, 51, 55, 56, 56, 51, 56, 56, 54, 53, 56, 52, 56, 49, 53, 57, 55, 51, 51, 55, 51, 54, 53, 55, 54, 55, 55, 49, 49, 48, 55, 53, 48, 49, 49, 50, 56, 48, 49, 49, 55, 54, 49] ~~~~ This is the "transformed" secret that you pass directly into the TOTP algorithm, i.e. you can skip the XOR/blah stuff described [a bunch of posts back](https://github.com/librespot-org/librespot/issues/1475#issuecomment-3042895080). Python code to use: ```py >>> secret32 = base64.b32encode(bytes(secret)).decode().rstrip("=") >>> pyotp.TOTP(secret32, digits=6, interval=30).at(1752066280) '605735' ``` BOOOOOOOOOOORIIIIIIING You can find this by setting a breakpoint in the debugger directly before the TOTP function is called. Search in the source code for "totpServer" and it's a few lines up from there. Takes a few minutes. Spotify you are just wasting your own developer time & money with this shit lmfaooooo
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

For debugging newbies: you can tell it's the TOTP function because it will look something like obj.generate(timestamp). obj is the TOTP object and obj.secret.bytes is the (final, "transformed") secret.

In the source code might be obfuscated and look like Qe[l(0, 0, 483, 514)](p), but you can evaluate these subexpressions in the console to see that they match the above e.g. l(0, 0, 483, 514)] evaluates to "generate". You can work backwards from the definition of "totp" (just before "totpServer"), set a breakpoint just before here, reload the page to hit this earlier breakpoint, evaluate some subexpressions to figure out which earlier parts to set more breakpoints at, rinse and repeat.

It's best to get the secret just before the TOTP function is called, because all this obfuscation with retarded XORs and BS doesn't actually matter, it is just some retarded intern trying to justify their own salary to the non-technical bosses.

Protip: modern browser debuggers should be able to let you view the beautified version of any minified JS, as well as "go to definition" on any function that is in scope (e.g. when stopped at a breakpoint). For example in firefox I can't do this in the source code viewer, but I can do this by inputting the function name into the JS console, then the output will have a button I can click to jump to the definition in the source code.

<!-- gh-comment-id:3052709437 --> @infinity0 commented on GitHub (Jul 9, 2025): For debugging newbies: you can tell it's the TOTP function because it will look something like `obj.generate(timestamp)`. `obj` is the TOTP object and `obj.secret.bytes` is the (final, "transformed") secret. In the source code might be obfuscated and look like `Qe[l(0, 0, 483, 514)](p)`, but you can evaluate these subexpressions in the console to see that they match the above e.g. `l(0, 0, 483, 514)]` evaluates to "generate". You can work backwards from the definition of "totp" (just before "totpServer"), set a breakpoint just before here, reload the page to hit this earlier breakpoint, evaluate some subexpressions to figure out which earlier parts to set more breakpoints at, rinse and repeat. It's best to get the secret just before the TOTP function is called, because all this obfuscation with retarded XORs and BS doesn't actually matter, it is just some retarded intern trying to justify their own salary to the non-technical bosses. Protip: modern browser debuggers should be able to let you view the beautified version of any minified JS, as well as "go to definition" on any function that is in scope (e.g. when stopped at a breakpoint). For example in firefox I can't do this in the source code viewer, but I can do this by inputting the function name into the JS console, then the output will have a button I can click to jump to the definition in the source code.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

Calling their obfuscation "bullshit" from some "retarded intern" is way off the mark. What do you expect them to do?
It's their platform, their private API, and their rules. They're a multi-billion dollar company, of course they're going to spend money protecting their infrastructure from being scraped and abused. It's not "cringy capitalism," it's just standard practice.
Frankly, I don't see this as fighting some idiot intern. I see it as a pretty fun cat-and-mouse game against competent engineers who are just doing their job. It makes cracking it that much more satisfying. This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀

<!-- gh-comment-id:3052777692 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): Calling their obfuscation "bullshit" from some "retarded intern" is way off the mark. What do you expect them to do? It's their platform, their private API, and their rules. They're a multi-billion dollar company, of course they're going to spend money protecting their infrastructure from being scraped and abused. It's not "cringy capitalism," it's just standard practice. Frankly, I don't see this as fighting some idiot intern. I see it as a pretty fun cat-and-mouse game against competent engineers who are just doing their job. It makes cracking it that much more satisfying. This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀

You are projecting my man. Learn something technical from what I just said.

<!-- gh-comment-id:3052797971 --> @infinity0 commented on GitHub (Jul 9, 2025): > This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀 You are projecting my man. Learn something technical from what I just said.
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 9, 2025):

ok still works for v12 I wanna see how long my automation will work for

Image
{
  '10': {
    raw: '=n:b#OuEfH\\fE])e*K',
    unit8: Uint8Array(18) [
       61, 110,  58,  98, 35,  79,
      117,  69, 102,  72, 92, 102,
       69,  93,  41, 101, 42,  75
    ],
    xor: '521004911046651228511990791148075621255181',
    ascii: '535049484852574949485254545349505056534949575748555749495256485553545049505353495649'
  },
  '11': {
    raw: 'o-(I_J#Uik<n7HEFrS?X[',
    unit8: Uint8Array(21) [
      111,  45,  40, 73,  95, 74, 35,
       85, 105, 107, 60, 110, 55, 72,
       69,  70, 114, 83,  63, 88, 91
    ],
    xor: '10239356982684469120121471223494829410773366870',
    ascii: '4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548'
  },
  '12': {
    raw: 'kQ19C]WQEC(]02.[^q)lMk"',
    unit8: Uint8Array(23) [
      107, 81, 49,  57, 67,  93, 87,
       81, 69, 67,  40, 93,  48, 50,
       46, 91, 94, 113, 41, 108, 77,
      107, 34
    ],
    xor: '9891585378838865848159733736576771107501128011761',
    ascii: '57565749535653515556565156565453565256495357555151555154535554555549494855534849495056484949555449'
  }
}
<!-- gh-comment-id:3052818697 --> @DiamondRoPlayz commented on GitHub (Jul 9, 2025): ok still works for v12 I wanna see how long my automation will work for <img width="899" height="876" alt="Image" src="https://github.com/user-attachments/assets/a65ae6bb-ef64-428f-bdfe-8c440980b890" /> ```js { '10': { raw: '=n:b#OuEfH\\fE])e*K', unit8: Uint8Array(18) [ 61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75 ], xor: '521004911046651228511990791148075621255181', ascii: '535049484852574949485254545349505056534949575748555749495256485553545049505353495649' }, '11': { raw: 'o-(I_J#Uik<n7HEFrS?X[', unit8: Uint8Array(21) [ 111, 45, 40, 73, 95, 74, 35, 85, 105, 107, 60, 110, 55, 72, 69, 70, 114, 83, 63, 88, 91 ], xor: '10239356982684469120121471223494829410773366870', ascii: '4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548' }, '12': { raw: 'kQ19C]WQEC(]02.[^q)lMk"', unit8: Uint8Array(23) [ 107, 81, 49, 57, 67, 93, 87, 81, 69, 67, 40, 93, 48, 50, 46, 91, 94, 113, 41, 108, 77, 107, 34 ], xor: '9891585378838865848159733736576771107501128011761', ascii: '57565749535653515556565156565453565256495357555151555154535554555549494855534849495056484949555449' } } ```
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀

You are projecting my man. Learn something technical from what I just said.

I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking.
The challenge was reing the derivation, not just finding the final variable. I already provided the impl a few days ago.

<!-- gh-comment-id:3052823662 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): > > This isn't some moral crusade, it's just a technical challenge, ain't that deep 🥀 > > You are projecting my man. Learn something technical from what I just said. I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking. The challenge was reing the derivation, not just finding the final variable. I already provided the impl a few days ago.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

@DiamondRoPlayz You are doing regex matching I believe?

<!-- gh-comment-id:3052826277 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): @DiamondRoPlayz You are doing regex matching I believe?
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 9, 2025):

@Thereallo1026 Yes from just /(\];const \w{2}=)(\w{2};)/g still where I insert global variable and the site only requires to run those 3 web-player scripts for it to run the main one so all other network requests can be blocked in puppeteer to optimize it 😭

<!-- gh-comment-id:3052830514 --> @DiamondRoPlayz commented on GitHub (Jul 9, 2025): @Thereallo1026 Yes from just `/(\];const \w{2}=)(\w{2};)/g` still where I insert global variable and the site only requires to run those 3 web-player scripts for it to run the main one so all other network requests can be blocked in puppeteer to optimize it 😭
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking.

The world doesn't revolve around you, my comment is to crowdsource the manual effort, to tilt the cost balance in favour of us the FOSS people. And it helps to be as loud as possible so Spotify know it's a futile arms race on their part.

And you don't have to get offended when people express different worldviews from yours. Neither you nor I am right about the internal details of spotify, but it's boring if I have to talk like a f'n robot on github.

The automatic effort is impossible in principle because of the halting problem. If it does work long-term then it just in fact confirms that I was right about a retarded intern being in charge of this.

<!-- gh-comment-id:3052852800 --> @infinity0 commented on GitHub (Jul 9, 2025): > I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking. The world doesn't revolve around you, my comment is to crowdsource the manual effort, to tilt the cost balance in favour of us the FOSS people. And it helps to be as loud as possible so Spotify know it's a futile arms race on their part. And you don't have to get offended when people express different worldviews from yours. Neither you nor I am right about the internal details of spotify, but it's boring if I have to talk like a f'n robot on github. The automatic effort is impossible in principle because of the halting problem. If it does work long-term then it just in fact confirms that I was right about a retarded intern being in charge of this.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking.

The world doesn't revolve around you, my comment is to crowdsource the manual effort, to tilt the cost balance in favour of us the FOSS people. And it helps to be as loud as possible so Spotify know it's a futile arms race on their part.

And you don't have to get offended when people express different worldviews from yours. Neither you nor I am right about the internal details of spotify, but it's boring if I have to talk like a f'n robot on github.

The automatic effort is impossible in principle because of the halting problem. If it does work long-term then it just in fact confirms that I was right about a retarded intern being in charge of this.

Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about.
Automation isn't "impossible in principle", @DiamondRoPlayz literally did it while you were busy writing your manifesto.
And fyi Spotify isn't hosting a board meeting about your little GitHub comments dude
Nobody is asking you to be a robot, we're just asking you not to be a condescending prick

<!-- gh-comment-id:3052886805 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): > > I'm not projecting, I'm just not impressed. You explained how to use a debugger, groundbreaking. > > The world doesn't revolve around you, my comment is to crowdsource the manual effort, to tilt the cost balance in favour of us the FOSS people. And it helps to be as loud as possible so Spotify know it's a futile arms race on their part. > > And you don't have to get offended when people express different worldviews from yours. Neither you nor I am right about the internal details of spotify, but it's boring if I have to talk like a f'n robot on github. > > The automatic effort is impossible in principle because of the halting problem. If it does work long-term then it just in fact confirms that I was right about a retarded intern being in charge of this. Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about. Automation isn't "impossible in principle", @DiamondRoPlayz literally did it while you were busy writing your manifesto. And fyi Spotify isn't hosting a board meeting about your little GitHub comments dude Nobody is asking you to be a robot, we're just asking you not to be a condescending prick
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about.

False, it's entirely relevant.

Automation isn't "impossible in principle", @DiamondRoPlayz literally did it while you were busy writing your manifesto.

We're talking about long-term automation. Again, projecting about "no clue what you're talking about".

Nobody is asking you to be a robot, we're just asking you not to be a condescending prick

You do this by interpreting a random stranger's internet comments (that are directed not even at you, but at third parties unrelated to you) as "trying to impress you". Have a look in the mirror mate.

<!-- gh-comment-id:3052892768 --> @infinity0 commented on GitHub (Jul 9, 2025): > Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about. False, it's entirely relevant. > Automation isn't "impossible in principle", @DiamondRoPlayz literally did it while you were busy writing your manifesto. We're talking about long-term automation. Again, projecting about "no clue what you're talking about". > Nobody is asking you to be a robot, we're just asking you not to be a condescending prick You do this by interpreting a random stranger's internet comments (that are directed not even at you, but at third parties unrelated to you) as "trying to impress you". Have a look in the mirror mate.
Author
Owner

@DiamondRoPlayz commented on GitHub (Jul 9, 2025):

beef on GitHub is something I never imagined to see 😭🥀💔

<!-- gh-comment-id:3052897522 --> @DiamondRoPlayz commented on GitHub (Jul 9, 2025): beef on GitHub is something I never imagined to see 😭🥀💔
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about.

False, it's entirely relevant.

Automation isn't "impossible in principle", @DiamondRoPlayz literally did it while you were busy writing your manifesto.

We're talking about long-term automation. Again, projecting about "no clue what you're talking about".

Nobody is asking you to be a robot, we're just asking you not to be a condescending prick

You do this by interpreting a random stranger's internet comments (that are directed not even at you, but at third parties unrelated to you) as "trying to impress you". Have a look in the mirror mate.

You are embarrassing yourself. The halting problem is about arbitrary programs. This is a specific one. If you can't tell the difference, you have no business calling anyone a "newbie." It's not "relevant," it's fucking idiotic.

You moved the goalposts from "impossible" to "long-term" because you know you're wrong.

And you started the condescension with your "debugging newbies" comment. Don't play the victim now. We are done here.

<!-- gh-comment-id:3052918735 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): > > Invoking the halting problem here is the dumbest thing I've read all week. It proves you have no clue what you're talking about. > > False, it's entirely relevant. > > > Automation isn't "impossible in principle", [@DiamondRoPlayz](https://github.com/DiamondRoPlayz) literally did it while you were busy writing your manifesto. > > We're talking about long-term automation. Again, projecting about "no clue what you're talking about". > > > Nobody is asking you to be a robot, we're just asking you not to be a condescending prick > > You do this by interpreting a random stranger's internet comments (that are directed not even at you, but at third parties unrelated to you) as "trying to impress you". Have a look in the mirror mate. You are embarrassing yourself. The halting problem is about arbitrary programs. This is a specific one. If you can't tell the difference, you have no business calling anyone a "newbie." It's not "relevant," it's fucking idiotic. You moved the goalposts from "impossible" to "long-term" because you know you're wrong. And you started the condescension with your "debugging newbies" comment. Don't play the victim now. We are done here.
Author
Owner

@kingosticks commented on GitHub (Jul 9, 2025):

This has definitely reached the point this is distracting and altogether unwelcome noise. Please take all of this somewhere else.

@roderickvd @photovoltex can we close and lock this one, it's gotten way off subject.

<!-- gh-comment-id:3052925533 --> @kingosticks commented on GitHub (Jul 9, 2025): This has definitely reached the point this is distracting and altogether unwelcome noise. Please take all of this somewhere else. @roderickvd @photovoltex can we close and lock this one, it's gotten way off subject.
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

The halting problem is about arbitrary programs. This is a specific one

Future versions of spotify are arbitrary programs. You sure like to pretend to be confident for someone that isn't technically competent.

<!-- gh-comment-id:3052926859 --> @infinity0 commented on GitHub (Jul 9, 2025): > The halting problem is about arbitrary programs. This is a specific one Future versions of spotify are arbitrary programs. You sure like to pretend to be confident for someone that isn't technically competent.
Author
Owner

@roderickvd commented on GitHub (Jul 9, 2025):

👮 please keep it on topic. Hoping for the best, I'll leave it open to continue constructively and on topic, before I'll be forced to close without further warning. Thanks.

<!-- gh-comment-id:3052933942 --> @roderickvd commented on GitHub (Jul 9, 2025): 👮 please keep it on topic. Hoping for the best, I'll leave it open to continue constructively and on topic, before I'll be forced to close without further warning. Thanks.
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

This has definitely reached the point this is distracting and altogether unwelcome noise. Please take all of this somewhere else.

@roderickvd @photovoltex can we close and lock this one, it's gotten way off subject.

@kingosticks You're absolutely right, my apologies. This has derailed completely from the actual engineering challenge.
I'm done with the back and forth. I shouldn't have gotten drawn into a debate about theoretical computer science concepts on a reverse-engineering thread.
I'll stick to the librespot related technicals from now on. Appreciate your work on the project.

<!-- gh-comment-id:3052937247 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): > This has definitely reached the point this is distracting and altogether unwelcome noise. Please take all of this somewhere else. > > [@roderickvd](https://github.com/roderickvd) [@photovoltex](https://github.com/photovoltex) can we close and lock this one, it's gotten way off subject. @kingosticks You're absolutely right, my apologies. This has derailed completely from the actual engineering challenge. I'm done with the back and forth. I shouldn't have gotten drawn into a debate about theoretical computer science concepts on a reverse-engineering thread. I'll stick to the librespot related technicals from now on. Appreciate your work on the project.
Author
Owner

@kingosticks commented on GitHub (Jul 9, 2025):

Thanks. I still think it's worth taking the ideas and facts here somewhere else. Github issues are really hard to navigate, especially when they get too long and older stuff gets hidden behind a load button.

<!-- gh-comment-id:3052965693 --> @kingosticks commented on GitHub (Jul 9, 2025): Thanks. I still think it's worth taking the ideas and facts here somewhere [else](https://github.com/librespot-org/librespot/wiki/_new). Github issues are really hard to navigate, especially when they get too long and older stuff gets hidden behind a load button.
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

For debugging newbies that would like to contribute, I draw attention to my last comment before the derailing, which will work as a manual method regardless of any additional future obfuscation.

If they change algorithm away from TOTP, you can get relevant starting strings to search for, by setting XHR breakpoints for any network requests that seem like they are grabbing an access token.

<!-- gh-comment-id:3052965794 --> @infinity0 commented on GitHub (Jul 9, 2025): For debugging newbies that would like to contribute, I draw attention to [my last comment before the derailing](https://github.com/librespot-org/librespot/issues/1475#issuecomment-3052709437), which will work as a manual method regardless of any additional future obfuscation. If they change algorithm away from TOTP, you can get relevant starting strings to search for, by setting XHR breakpoints for any network requests that seem like they are grabbing an access token.
Author
Owner

@staniel359 commented on GitHub (Jul 9, 2025):

ok still works for v12 I wanna see how long my automation will work for
Image

{
'10': {
raw: '=n:b#OuEfH\fE])e*K',
unit8: Uint8Array(18) [
61, 110, 58, 98, 35, 79,
117, 69, 102, 72, 92, 102,
69, 93, 41, 101, 42, 75
],
xor: '521004911046651228511990791148075621255181',
ascii: '535049484852574949485254545349505056534949575748555749495256485553545049505353495649'
},
'11': {
raw: 'o-(I_J#Uik<n7HEFrS?X[',
unit8: Uint8Array(21) [
111, 45, 40, 73, 95, 74, 35,
85, 105, 107, 60, 110, 55, 72,
69, 70, 114, 83, 63, 88, 91
],
xor: '10239356982684469120121471223494829410773366870',
ascii: '4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548'
},
'12': {
raw: 'kQ19C]WQEC(]02.[^q)lMk"',
unit8: Uint8Array(23) [
107, 81, 49, 57, 67, 93, 87,
81, 69, 67, 40, 93, 48, 50,
46, 91, 94, 113, 41, 108, 77,
107, 34
],
xor: '9891585378838865848159733736576771107501128011761',
ascii: '57565749535653515556565156565453565256495357555151555154535554555549494855534849495056484949555449'
}
}

@DiamondRoPlayz Yeah, but what's the code for retrieving this?

<!-- gh-comment-id:3052985304 --> @staniel359 commented on GitHub (Jul 9, 2025): > ok still works for v12 I wanna see how long my automation will work for > <img alt="Image" width="899" height="876" src="https://private-user-images.githubusercontent.com/61511674/464226053-a65ae6bb-ef64-428f-bdfe-8c440980b890.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTIwNzMxNzYsIm5iZiI6MTc1MjA3Mjg3NiwicGF0aCI6Ii82MTUxMTY3NC80NjQyMjYwNTMtYTY1YWU2YmItZWY2NC00MjhmLWJkZmUtOGM0NDA5ODBiODkwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA3MDklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNzA5VDE0NTQzNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBmYmE2MzE3NDJlOTE3OTk4NmU1NWU4ZTA3ODliYTljNDExNTRmZGRiN2M5NjFmZTZhMjJhYzY5NmY3Y2ViNzkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.fpe6nUWkGQGmVVP70D0FjWISp2qh2IDRUx8epDoonVk"> > > { > '10': { > raw: '=n:b#OuEfH\\fE])e*K', > unit8: Uint8Array(18) [ > 61, 110, 58, 98, 35, 79, > 117, 69, 102, 72, 92, 102, > 69, 93, 41, 101, 42, 75 > ], > xor: '521004911046651228511990791148075621255181', > ascii: '535049484852574949485254545349505056534949575748555749495256485553545049505353495649' > }, > '11': { > raw: 'o-(I_J#Uik<n7HEFrS?X[', > unit8: Uint8Array(21) [ > 111, 45, 40, 73, 95, 74, 35, > 85, 105, 107, 60, 110, 55, 72, > 69, 70, 114, 83, 63, 88, 91 > ], > xor: '10239356982684469120121471223494829410773366870', > ascii: '4948505157515354575650545652525457495048495049525549505051525752565057524948555551515454565548' > }, > '12': { > raw: 'kQ19C]WQEC(]02.[^q)lMk"', > unit8: Uint8Array(23) [ > 107, 81, 49, 57, 67, 93, 87, > 81, 69, 67, 40, 93, 48, 50, > 46, 91, 94, 113, 41, 108, 77, > 107, 34 > ], > xor: '9891585378838865848159733736576771107501128011761', > ascii: '57565749535653515556565156565453565256495357555151555154535554555549494855534849495056484949555449' > } > } @DiamondRoPlayz Yeah, but what's the code for retrieving this?
Author
Owner

@infinity0 commented on GitHub (Jul 9, 2025):

Thanks. I still think it's worth taking the ideas and facts here somewhere else. Github issues are really hard to navigate, especially then they get too long and older stuff gets hidden behind a load button.

Added https://github.com/librespot-org/librespot/wiki/Reverse-engineering

@DiamondRoPlayz Yeah, but what's the code for retrieving this?

I'm not holding my breath. Best bet is for as many people as possible to learn the reverse-engineering method.

<!-- gh-comment-id:3053075658 --> @infinity0 commented on GitHub (Jul 9, 2025): > Thanks. I still think it's worth taking the ideas and facts here somewhere [else](https://github.com/librespot-org/librespot/wiki/_new). Github issues are really hard to navigate, especially then they get too long and older stuff gets hidden behind a load button. Added https://github.com/librespot-org/librespot/wiki/Reverse-engineering > @DiamondRoPlayz Yeah, but what's the code for retrieving this? I'm not holding my breath. Best bet is for as many people as possible to learn the reverse-engineering method.
Author
Owner

@misiektoja commented on GitHub (Jul 9, 2025):

@Thereallo1026 Yes from just /(\];const \w{2}=)(\w{2};)/g still where I insert global variable and the site only requires to run those 3 web-player scripts for it to run the main one so all other network requests can be blocked in puppeteer to optimize it 😭

But you really don't need to regex-grep the source (it's brittle and will break whenever Spotify tweaks their source code). It's actually much simpler: inject a runtime hook that captures every .secret assignment as the code executes. If you dump all of the objects with hooks in place, you'll see the secrets pop out already reconstructed, for example:

    "obj": {
      "secret": "kQ19C]WQEC(]02.[^q)lMk\"",
      "version": 12
    }

I wrote a Python script that does all the secrets extraction at runtime in the browser (via Playwright) - no source parsing required. The trick is to add a tiny setter before any Spotify code runs, so every obj.secret = call automatically fires your hook and records the value and version.

PoC: spotify_monitor_secret_grabber.py, more info in the Debugging Tools section of the spotify_monitor project.

Install playwright and run:

pip install playwright
playwright install

python3 spotify_monitor_secret_grabber.py

You'll get:

Image
<!-- gh-comment-id:3053296920 --> @misiektoja commented on GitHub (Jul 9, 2025): > [@Thereallo1026](https://github.com/Thereallo1026) Yes from just `/(\];const \w{2}=)(\w{2};)/g` still where I insert global variable and the site only requires to run those 3 web-player scripts for it to run the main one so all other network requests can be blocked in puppeteer to optimize it 😭 But you really don't need to regex-grep the source (it's brittle and will break whenever Spotify tweaks their source code). It's actually much simpler: inject a runtime hook that captures every .secret assignment as the code executes. If you dump all of the objects with hooks in place, you'll see the secrets pop out already reconstructed, for example: ```json "obj": { "secret": "kQ19C]WQEC(]02.[^q)lMk\"", "version": 12 } ``` I wrote a Python script that does all the secrets extraction at runtime in the browser (via Playwright) - no source parsing required. The trick is to add a tiny setter before any Spotify code runs, so every `obj.secret =` call automatically fires your hook and records the value and version. PoC: [spotify_monitor_secret_grabber.py](https://github.com/misiektoja/spotify_monitor/blob/dev/debug/spotify_monitor_secret_grabber.py), more info in the Debugging Tools section of the [spotify_monitor](https://github.com/misiektoja/spotify_monitor#debugging-tools) project. Install playwright and run: ```sh pip install playwright playwright install python3 spotify_monitor_secret_grabber.py ``` You'll get: <img width="1846" height="428" alt="Image" src="https://github.com/user-attachments/assets/820df3a2-559a-4b1b-9d47-cec83278b97e" />
Author
Owner

@sam9116 commented on GitHub (Jul 9, 2025):

I just want to say a huge thank you for everyone involved in cracking spotify's auth process, you guys are giving those overpaid engineers a run for their money

<!-- gh-comment-id:3053478103 --> @sam9116 commented on GitHub (Jul 9, 2025): I just want to say a huge thank you for everyone involved in cracking spotify's auth process, you guys are giving those overpaid engineers a run for their money
Author
Owner

@Thereallo1026 commented on GitHub (Jul 9, 2025):

@misiektoja Amazing work, I wanted to confirm that your runtime hook method works perfectly!

It's a much more robust approach than parsing the source with regex. I was able to build on your idea and create a fully automated solution using GitHub Actions and JS to scrape the secrets on a schedule.

I've open-sourced it here for anyone interested: https://github.com/Thereallo1026/spotify-secrets

<!-- gh-comment-id:3053669317 --> @Thereallo1026 commented on GitHub (Jul 9, 2025): @misiektoja Amazing work, I wanted to confirm that your runtime hook method works perfectly! It's a much more robust approach than parsing the source with regex. I was able to build on your idea and create a fully automated solution using GitHub Actions and JS to scrape the secrets on a schedule. I've open-sourced it here for anyone interested: https://github.com/Thereallo1026/spotify-secrets
Author
Owner

@Thereallo1026 commented on GitHub (Jul 11, 2025):

@misiektoja Amazing work, I wanted to confirm that your runtime hook method works perfectly!

It's a much more robust approach than parsing the source with regex. I was able to build on your idea and create a fully automated solution using GitHub Actions and JS to scrape the secrets on a schedule.

I've open-sourced it here for anyone interested: https://github.com/Thereallo1026/spotify-secrets

To anyone who is using the JSON files in my repo, I have pushed an update that changes the format of secretBytes.json. It now uses a unified format with secrets.json.

interface SpotifySecrets {
	secret: string | number[];
	version: number;
}
[];
<!-- gh-comment-id:3060633335 --> @Thereallo1026 commented on GitHub (Jul 11, 2025): > [@misiektoja](https://github.com/misiektoja) Amazing work, I wanted to confirm that your runtime hook method works perfectly! > > It's a much more robust approach than parsing the source with regex. I was able to build on your idea and create a fully automated solution using GitHub Actions and JS to scrape the secrets on a schedule. > > I've open-sourced it here for anyone interested: https://github.com/Thereallo1026/spotify-secrets To anyone who is using the JSON files in my repo, I have pushed an [update](https://github.com/Thereallo1026/spotify-secrets/commit/1d5a075fda595c124d41e664cf129a54a54bbd17) that changes the format of [secretBytes.json](https://github.com/Thereallo1026/spotify-secrets/blob/main/secrets/secretBytes.json). It now uses a unified format with secrets.json. ```ts interface SpotifySecrets { secret: string | number[]; version: number; } []; ```
Author
Owner

@manhgdev commented on GitHub (Jul 12, 2025):

Well, when I was looking for materials to learn how to reverse engineer source code to find secrets but didn't know where to learn

<!-- gh-comment-id:3064684679 --> @manhgdev commented on GitHub (Jul 12, 2025): Well, when I was looking for materials to learn how to reverse engineer source code to find secrets but didn't know where to learn
Author
Owner

@infinity0 commented on GitHub (Jul 17, 2025):

@manhgdev Have a look at https://github.com/librespot-org/librespot/wiki/Reverse-engineering - practice on the current version until you can do it within 2-3 minutes, then you can do it again after the intern changes the object key from "secret" to "s3kr1t".

<!-- gh-comment-id:3085819467 --> @infinity0 commented on GitHub (Jul 17, 2025): @manhgdev Have a look at https://github.com/librespot-org/librespot/wiki/Reverse-engineering - practice on the current version until you can do it within 2-3 minutes, then you can do it again after the intern changes the object key from "secret" to "s3kr1t".
Author
Owner

@girwin1 commented on GitHub (Jul 21, 2025):

does anyone have the python way to do this?

<!-- gh-comment-id:3099333877 --> @girwin1 commented on GitHub (Jul 21, 2025): does anyone have the python way to do this?
Author
Owner

@EduardLupu commented on GitHub (Aug 4, 2025):

now this topic was a great read, thanks everybody! please, never close this!

<!-- gh-comment-id:3151901339 --> @EduardLupu commented on GitHub (Aug 4, 2025): now this topic was a great read, thanks everybody! please, never close this!
Author
Owner

@roderickvd commented on GitHub (Aug 4, 2025):

Probably should turn it into a discussion?

<!-- gh-comment-id:3152375362 --> @roderickvd commented on GitHub (Aug 4, 2025): Probably should turn it into a discussion?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/librespot#665
No description provided.