[GH-ISSUE #12] What does the websocket code do in this context? #11

Open
opened 2026-03-04 14:58:34 +03:00 by kerem · 23 comments
Owner

Originally created by @cynthia2006 on GitHub (Feb 26, 2026).
Original GitHub issue: https://github.com/GladistonXD/votify-fix/issues/12

Looking at the current code the purpose of websockets seems rather unclear, because the API works fine without it?

Originally created by @cynthia2006 on GitHub (Feb 26, 2026). Original GitHub issue: https://github.com/GladistonXD/votify-fix/issues/12 Looking at the current code the purpose of `websockets` seems rather unclear, because the API works fine without it?
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

It doesn't work, you get an IP ban. The socket is necessary to simulate playing the music and for the server to know that it's possibly a real user. If you remove that, you can't download more than 28 songs.

<!-- gh-comment-id:3966030219 --> @GladistonXD commented on GitHub (Feb 26, 2026): It doesn't work, you get an IP ban. The socket is necessary to simulate playing the music and for the server to know that it's possibly a real user. If you remove that, you can't download more than 28 songs.
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

I put that separation there precisely to indicate that from there upwards it's solely for simulating play, and the decryption problem might even be some websocket-based function that hasn't been identified yet.

github.com/GladistonXD/votify-fix@bcb13b81a2/votify/spotify_api.py (L401)

<!-- gh-comment-id:3966122377 --> @GladistonXD commented on GitHub (Feb 26, 2026): I put that separation there precisely to indicate that from there upwards it's solely for simulating play, and the decryption problem might even be some websocket-based function that hasn't been identified yet. https://github.com/GladistonXD/votify-fix/blob/bcb13b81a2e0e0825408e99c441c930c60f473aa/votify/spotify_api.py#L401
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

It doesn't work, you get an IP ban. The socket is necessary to simulate playing the music and for the server to know that it's possibly a real user. If you remove that, you can't download more than 28 songs.

Really? But I've downloaded over 100 songs in one go.

<!-- gh-comment-id:3966141867 --> @cynthia2006 commented on GitHub (Feb 26, 2026): > It doesn't work, you get an IP ban. The socket is necessary to simulate playing the music and for the server to know that it's possibly a real user. If you remove that, you can't download more than 28 songs. Really? But I've downloaded over 100 songs in one go.
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

Really? But I've downloaded over 100 songs in one go.

Without sending that put request? lol, mine freezes after 27 songs, without sending a put request to the server, the only exception is if the account is open and running in the background:
github.com/GladistonXD/votify-fix@bcb13b81a2/votify/spotify_api.py (L328)

<!-- gh-comment-id:3966163640 --> @GladistonXD commented on GitHub (Feb 26, 2026): > Really? But I've downloaded over 100 songs in one go. Without sending that put request? lol, mine freezes after 27 songs, without sending a put request to the server, the only exception is if the account is open and running in the background: https://github.com/GladistonXD/votify-fix/blob/bcb13b81a2e0e0825408e99c441c930c60f473aa/votify/spotify_api.py#L328
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

I put that separation there precisely to indicate that from there upwards it's solely for simulating play, and the decryption problem might even be some websocket-based function that hasn't been identified yet.

Inspecting DevTools, there appears to be a single WebSocket connection, which is the one you're controlling in your code. And, the decryption issue seems to be on a per-account basis; not someting related to a particular kind of CDM per se.

Apparently, I'm getting this error.

...
  File "/home/cynthia/Public/votifast/api/__init__.py", line 211, in get_widevine_key
    session_id = self.cdm.open()
  File "/home/cynthia/Public/votifast/.venv/lib/python3.14/site-packages/pywidevine/cdm.py", line 145, in open
    raise TooManySessions(f"Too many Sessions open ({self.MAX_NUM_OF_SESSIONS}).")
pywidevine.exceptions.TooManySessions: Too many Sessions open (16).

Even though the CDM is closed after use 🤷‍♀️

Ah, it was my bad.

<!-- gh-comment-id:3966181874 --> @cynthia2006 commented on GitHub (Feb 26, 2026): > I put that separation there precisely to indicate that from there upwards it's solely for simulating play, and the decryption problem might even be some websocket-based function that hasn't been identified yet. Inspecting DevTools, there appears to be a single WebSocket connection, which is the one you're controlling in your code. And, the decryption issue seems to be on a per-account basis; not someting related to a particular kind of CDM per se. ~~Apparently, I'm getting this error.~~ ``` ... File "/home/cynthia/Public/votifast/api/__init__.py", line 211, in get_widevine_key session_id = self.cdm.open() File "/home/cynthia/Public/votifast/.venv/lib/python3.14/site-packages/pywidevine/cdm.py", line 145, in open raise TooManySessions(f"Too many Sessions open ({self.MAX_NUM_OF_SESSIONS}).") pywidevine.exceptions.TooManySessions: Too many Sessions open (16). ``` ~~Even though the CDM is closed after use 🤷‍♀️~~ Ah, it was my bad.
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

Without sending that put request? lol, mine freezes after 27 songs, without sending a put request to the server, the only exception is if the account is open and running in the background:

Well, I'm taking a shortcut here: I'm not using the pathfinder API for downloading tracks, just the track playback API.

<!-- gh-comment-id:3966225936 --> @cynthia2006 commented on GitHub (Feb 26, 2026): > Without sending that put request? lol, mine freezes after 27 songs, without sending a put request to the server, the only exception is if the account is open and running in the background: Well, I'm taking a shortcut here: I'm not using the pathfinder API for downloading tracks, just the track playback API.
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

The following PUT sequences exist:

/state PUT

seq_num: 2
debug_source: "before_track_load"

seq_num: 3
debug_source: "speed_changed"

seq_num: 4
debug_source: "speed_changed"
playback_speed: 1

seq_num: 5
debug_source: "started_playing" < ===== I use this in the request.

seq_num: 5
debug_source: "track_data_finalized"

next song:

seq_num: 6
debug_source: "before_track_load"

Each time the music plays, the number continues the sequence.

I noticed that there is a debug_source: "track_data_finalized statement; perhaps this is what identifies it, as it doesn't send a finished song PUT to the server. I'll test it later.

<!-- gh-comment-id:3966356350 --> @GladistonXD commented on GitHub (Feb 26, 2026): The following PUT sequences exist: /state PUT seq_num: 2 debug_source: "before_track_load" seq_num: 3 debug_source: "speed_changed" seq_num: 4 debug_source: "speed_changed" playback_speed: 1 seq_num: 5 debug_source: "started_playing" < ===== I use this in the request. seq_num: 5 debug_source: "track_data_finalized" next song: seq_num: 6 debug_source: "before_track_load" Each time the music plays, the number continues the sequence. I noticed that there is a `debug_source: "track_data_finalized` statement; perhaps this is what identifies it, as it doesn't send a finished song PUT to the server. I'll test it later.
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

The best bet is to see how album downloads work in the official Spotify client.

<!-- gh-comment-id:3966735687 --> @cynthia2006 commented on GitHub (Feb 26, 2026): The best bet is to see how album downloads work in the official Spotify client.
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

Yes, analyzing the Spotify program for offline music would resolve the doubts, but I believe the program uses some kind of encryption and its own format to play music offline; they divide it into parts like \Spotify\Data\00\00c62b9d1a8bd34c9a3db769118470b455cdf910.file

Converting that into an audible format would be a challenge.

<!-- gh-comment-id:3966823099 --> @GladistonXD commented on GitHub (Feb 26, 2026): Yes, analyzing the Spotify program for offline music would resolve the doubts, but I believe the program uses some kind of encryption and its own format to play music offline; they divide it into parts like _\Spotify\Data\00\00c62b9d1a8bd34c9a3db769118470b455cdf910.file_ Converting that into an audible format would be a challenge.
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

00c62b9d1a8bd34c9a3db769118470b455cdf910 is stored in 00 folder because of easier lookup, and is probably just the CENC-encrypted MP4 file. However, I specifically want to analyse what network requests it makes to mass download licenses.

By the way, I've noticed this additional PUT sequence.

{
  "seq_num": 10,
  "state_ref": {
    "state_machine_id": "...",
    "state_id": "...",
    "paused": false
  },
  "sub_state": {
    "playback_speed": 1,
    "position": 30218,
    "duration": 269607,
    "media_type": "AUDIO",
    "bitrate": 128000,
    "audio_quality": "HIGH",
    "format": 10,
    "is_video_on": false
  },
  "previous_position": 30218,
  "debug_source": "played_threshold_reached"
}

And, the track_data_finalized sequence is indeed sent when the song ends. I've also noticed that a seek (to the end) generates this PUT request.

{
  "seq_num": 27,
  "state_ref": {
    "state_machine_id": "...",
    "state_id": "...",
    "paused": true
  },
  "sub_state": {
    "playback_speed": 0,
    "position": 289889,
    "duration": 289889,
    "media_type": "AUDIO",
    "bitrate": 128000,
    "audio_quality": "HIGH",
    "format": 10,
    "is_video_on": false
  },
  "previous_position": 60120,
  "debug_source": "position_changed"
}

And, it's preceeded by a speed_change request. So the most natural, non-invasive sequences are in order:

  1. before_track_loadspeed_changedspeed_changed (playback_speed = 1)played_threshold_reachedtrack_data_finalized
  2. before_track_loadspeed_changedspeed_changed (playback_speed = 1)speed_changed (playback_speed = 0)position_changed (position = duration)track_data_finalized
  3. before_track_loadspeed_changedspeed_changed (playback_speed = 1)track_data_finalized
<!-- gh-comment-id:3966993348 --> @cynthia2006 commented on GitHub (Feb 26, 2026): `00c62b9d1a8bd34c9a3db769118470b455cdf910` is stored in `00` folder because of easier lookup, and is probably just the CENC-encrypted MP4 file. However, I specifically want to analyse what network requests it makes to mass download licenses. By the way, I've noticed this additional PUT sequence. ``` { "seq_num": 10, "state_ref": { "state_machine_id": "...", "state_id": "...", "paused": false }, "sub_state": { "playback_speed": 1, "position": 30218, "duration": 269607, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10, "is_video_on": false }, "previous_position": 30218, "debug_source": "played_threshold_reached" } ``` And, the `track_data_finalized` sequence is indeed sent when the song ends. I've also noticed that a seek (to the end) generates this PUT request. ``` { "seq_num": 27, "state_ref": { "state_machine_id": "...", "state_id": "...", "paused": true }, "sub_state": { "playback_speed": 0, "position": 289889, "duration": 289889, "media_type": "AUDIO", "bitrate": 128000, "audio_quality": "HIGH", "format": 10, "is_video_on": false }, "previous_position": 60120, "debug_source": "position_changed" } ``` And, it's preceeded by a `speed_change` request. So the most natural, non-invasive sequences are in order: 1. `before_track_load` → `speed_changed` → `speed_changed (playback_speed = 1)` → `played_threshold_reached` → `track_data_finalized` 2. `before_track_load` → `speed_changed` → `speed_changed (playback_speed = 1)` → `speed_changed (playback_speed = 0)` → `position_changed (position = duration)` → `track_data_finalized` 3. `before_track_load` → `speed_changed` → `speed_changed (playback_speed = 1)` → `track_data_finalized`
Author
Owner

@GladistonXD commented on GitHub (Feb 26, 2026):

This quota limit might be for renewing license request quotas, but it's impossible to know for sure:
played_threshold_reached
I don't have time to test it right now.

<!-- gh-comment-id:3967053788 --> @GladistonXD commented on GitHub (Feb 26, 2026): This quota limit might be for renewing license request quotas, but it's impossible to know for sure: `played_threshold_reached` I don't have time to test it right now.
Author
Owner

@cynthia2006 commented on GitHub (Feb 26, 2026):

How funny, they don't even use Widevine in the official client; they use Playplay through and through. I've used mitmproxy to record a log of HTTP requests, and it seems that only the Widevine endpoint is restricted. I'm not sure if Playplay is only available to premium users.

<!-- gh-comment-id:3968527777 --> @cynthia2006 commented on GitHub (Feb 26, 2026): How funny, they don't even use Widevine in the official client; they use Playplay through and through. I've used [mitmproxy](https://www.mitmproxy.org/) to record a log of HTTP requests, and it seems that only the Widevine endpoint is restricted. I'm not sure if Playplay is only available to premium users.
Author
Owner

@GladistonXD commented on GitHub (Feb 27, 2026):

I imagined it would be a different system; I think the easiest thing would be to understand the reverse engineering of the Android application, which is easier to manipulate.

<!-- gh-comment-id:3970075316 --> @GladistonXD commented on GitHub (Feb 27, 2026): I imagined it would be a different system; I think the easiest thing would be to understand the reverse engineering of the Android application, which is easier to manipulate.
Author
Owner

@cynthia2006 commented on GitHub (Feb 27, 2026):

They probably use Widevine, because it's readily available on Android, or they might use Playplay. Unfortunately, I've never worked with Android development, so I don't have any experience on how to decompile it (because it must've been obfuscated somehow).

<!-- gh-comment-id:3970477301 --> @cynthia2006 commented on GitHub (Feb 27, 2026): They probably use Widevine, because it's readily available on Android, or they might use Playplay. Unfortunately, I've never worked with Android development, so I don't have any experience on how to decompile it (because it must've been obfuscated somehow).
Author
Owner

@GladistonXD commented on GitHub (Feb 27, 2026):

Is this Playplay the Microsoft PlayReady? If so, it's used on Windows systems, I don't think it will be used on Android, and an easier way to analyze Spotify web is via embed, it gives cleaner outputs:
https://open.spotify.com/embed/track/18gqCQzqYb0zvurQPlRkpo
If the problem is "track_data_finalized", this request will be a lot of work.

<!-- gh-comment-id:3970546171 --> @GladistonXD commented on GitHub (Feb 27, 2026): Is this Playplay the Microsoft PlayReady? If so, it's used on Windows systems, I don't think it will be used on Android, and an easier way to analyze Spotify web is via embed, it gives cleaner outputs: `https://open.spotify.com/embed/track/18gqCQzqYb0zvurQPlRkpo` If the problem is "track_data_finalized", this request will be a lot of work.
Author
Owner

@cynthia2006 commented on GitHub (Feb 27, 2026):

No, it's not Microsoft's PlayReady, because then it wouldn't be used on Linux. I'm using ArchLinux here. Playplay is Spotify's own DRM. And, in the Votify code it seems as if it's just AES-CTR.

<!-- gh-comment-id:3970616603 --> @cynthia2006 commented on GitHub (Feb 27, 2026): No, it's not Microsoft's PlayReady, because then it wouldn't be used on Linux. I'm using ArchLinux here. Playplay is Spotify's own DRM. And, in the Votify code it seems as if it's just AES-CTR.
Author
Owner

@cynthia2006 commented on GitHub (Feb 27, 2026):

@GladistonXD Okay, so I've confirmed it now. These motherfolkers use Playplay on Android too.

<!-- gh-comment-id:3971267128 --> @cynthia2006 commented on GitHub (Feb 27, 2026): @GladistonXD Okay, so I've confirmed it now. These motherfolkers use Playplay on Android too.
Author
Owner

@GladistonXD commented on GitHub (Feb 27, 2026):

When DRM is less well-known, it's usually easier to crack, but reverse engineering Java is tricky.

<!-- gh-comment-id:3972508406 --> @GladistonXD commented on GitHub (Feb 27, 2026): When DRM is less well-known, it's usually easier to crack, but reverse engineering Java is tricky.
Author
Owner

@cynthia2006 commented on GitHub (Feb 27, 2026):

Playplay has been cracked, but Spotify DMCA's any repo on Github; it's available on various other sites (use DuckDuckGo).

<!-- gh-comment-id:3973062038 --> @cynthia2006 commented on GitHub (Feb 27, 2026): Playplay has been cracked, but Spotify DMCA's any repo on Github; it's available on various other sites (use DuckDuckGo).
Author
Owner

@cynthia2006 commented on GitHub (Feb 27, 2026):

Honestly, instead of all this fuss it's just better to use YouTube Music, which has superior audio quality anyway (not protected by DRM) 🤷‍♀️

Besides, Anna's Archive has scraped the entire Spotify anyway; so it's just wait until they release it.

<!-- gh-comment-id:3973877499 --> @cynthia2006 commented on GitHub (Feb 27, 2026): Honestly, instead of all this fuss it's just better to use YouTube Music, which has superior audio quality anyway (not protected by DRM) 🤷‍♀️ Besides, Anna's Archive has scraped the entire Spotify anyway; so it's just wait until they release it.
Author
Owner

@GladistonXD commented on GitHub (Feb 27, 2026):

Lol, there's this site that downloads Apple Music without an account too.

https://t2tunes.site/

But I use Spotify because of the playlists, someday when I have time I'll find a solution, even if I have to use Selenium for it.

<!-- gh-comment-id:3975770481 --> @GladistonXD commented on GitHub (Feb 27, 2026): Lol, there's this site that downloads Apple Music without an account too. https://t2tunes.site/ But I use Spotify because of the playlists, someday when I have time I'll find a solution, even if I have to use Selenium for it.
Author
Owner

@cynthia2006 commented on GitHub (Feb 28, 2026):

That's great :)

In the meantime, we could perhaps study librespot and adapt it to fit our needs. It has all the procedures in place, and it won't take much effort to port it to use free accounts.

That project is close to dead; it doesn't work for me. And yes, I'm using a premium account.

[2026-02-28T05:44:05Z INFO  librespot_playback::player] Loading <TSLAMP> with Spotify URI <spotify:track:43rJQjd6zfyTP58TNI9JAi>
[2026-02-28T05:44:05Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-02-28T05:44:05Z WARN  librespot_playback::player] Unable to load key, continuing without decryption: Service unavailable { audio key error }
[2026-02-28T05:44:07Z ERROR librespot_playback::player] Unable to read audio file: Symphonia Decoder Error: end of stream
[2026-02-28T05:44:07Z ERROR librespot_playback::player] Skipping to next track, unable to load track <SpotifyUri("spotify:track:43rJQjd6zfyTP58TNI9JAi")>: ()
<!-- gh-comment-id:3975925027 --> @cynthia2006 commented on GitHub (Feb 28, 2026): That's great :) ~~In the meantime, we could perhaps study [librespot](https://github.com/librespot-org/librespot) and adapt it to fit our needs. It has all the procedures in place, and it won't take much effort to port it to use free accounts.~~ That project is close to dead; it doesn't work for me. And yes, I'm using a premium account. ``` [2026-02-28T05:44:05Z INFO librespot_playback::player] Loading <TSLAMP> with Spotify URI <spotify:track:43rJQjd6zfyTP58TNI9JAi> [2026-02-28T05:44:05Z ERROR librespot_core::audio_key] error audio key 0 1 [2026-02-28T05:44:05Z WARN librespot_playback::player] Unable to load key, continuing without decryption: Service unavailable { audio key error } [2026-02-28T05:44:07Z ERROR librespot_playback::player] Unable to read audio file: Symphonia Decoder Error: end of stream [2026-02-28T05:44:07Z ERROR librespot_playback::player] Skipping to next track, unable to load track <SpotifyUri("spotify:track:43rJQjd6zfyTP58TNI9JAi")>: () ```
Author
Owner

@cynthia2006 commented on GitHub (Feb 28, 2026):

However, in that scenario it might potentially get banned off of Github; so we have to move somewhere else.

Apparently, unplayplay doesn't work anymore or perhaps they've changed the IV of AES or maybe Glomatico's code (in Votify) is plain wrong? Either way, it's impossible to tell unless the official client reverse engineered. I don't know how to, thus Widevine remains as the only option.

<!-- gh-comment-id:3975936481 --> @cynthia2006 commented on GitHub (Feb 28, 2026): ~~However, in that scenario it might potentially get banned off of Github; so we have to move somewhere else.~~ Apparently, [unplayplay](https://git.gay/glomatico/unplayplay) doesn't work anymore or perhaps they've changed the IV of AES or maybe Glomatico's code (in Votify) is plain wrong? Either way, it's impossible to tell unless the official client reverse engineered. I don't know how to, thus Widevine remains as the only option.
Sign in to join this conversation.
No labels
pull-request
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/votify-fix#11
No description provided.