Table of Contents
Every year (unless you’re one of those Apple Music people) music fans rejoice to get their Spotify Wrapped, or - the musical year in review. It’s a fun way to explore the most frequently listened to songs and artists. And every year up until this one, if memory serves me right, the experience could be viewed in the browser. And then, I paid the site a visit in the year 2021.
Well that’s a bummer, but I wonder if there is some special technical magic that they absolutely need to do client-side that cannot be rendered in the browser? Surely this is not just an elaborate scheme to get people to engage with the mobile app.
To roleplay as Sherlock Holmes, I decided to count on my trusted friends:
- An iPhone device, where the Spotify app was installed.
- Fiddler on my Windows developer box (or
mitmproxyif you are a macOS person).
- A self-signed certificate that allows me to decrypt local HTTPS traffic.
- A lunch break.
As I was trying to explore this, I was hoping that Spotify does not do certificate pinning, since that would foil my plans. Spoiler alert - they don’t. But as it turns out, I was quite naive in assuming that things will just run right away. After configuring the Fiddler certificate with an iOS 15 device, I still could not capture any HTTPS traffic. It threw me off a bit because I was thinking that maybe between me deciding to write this blog post and putting my hands on the keyboard Spotify actually implemented certificate pinning. Something about Fiddler didn’t work well with iOS despite all the trust settings, so I switched to using
mitmproxy inside WSL.
After a bit of configuration, I was up and running, and inspecting traffic like usual:
First thing that caught my eye was a request to the CDN that was pulling something of type
video/MP2T - could I have struck gold that fast? I quickly grabbed the URL, which had the form of:
Fortunately it seems from the UNIX timestamps included in the
token_ak string, which, by the way, is an Akamai token, that the token is long-lived (I can use it at least for 7 days). I grabbed the URL, threw it in
curl and saw a
.ts file land on disk. Great! Upon opening it, though, nothing really exciting showed up.
Alright, so let’s continue to dig. I restarted the app a couple of times because some of the requests seemed to be cached, and therefore I wasn’t going anywhere when I tried launching the story. And then, I saw this:
Bingo! There’s a request to:
This request has a bunch of headers attached to it, which is great, but the most important is
Authorization which is just a bearer token that you can grab by inspecting your local Spotify traffic (even from a desktop client). Running a
GET request against this endpoint returned something interesting - the content type is
application/protobuf. I haven’t worked with ProtoBufs since my foray into the Nest camera internals, so this was an exciting opportunity to re-familiarize myself with the tooling.
Because I didn’t want to write any code, I thought I would leverage
protoc to decode the data. If you’re on a Debian system (or inside Ubuntu in WSL), you can run:
sudo apt-get install protobuf-compiler
Once installed, I can download the binary data through
curl --location --request GET 'https://spclient.wg.spotify.com/campaigns-service/v1/campaigns/wrapped/consumer' --header 'Authorization: YOUR_BEARER_TOKEN' --output data.proto
And lastly, I now decode the raw message:
protoc --decode_raw < data.proto
What do you know! The structured response for all Spotify Wrapped scenes:
As it turns out, there is very little video media behind the scenes. Most behaviors are encoded in this ProtoBuf payload. If you are adventurous enough, you can even go as far as reconstruct your own Spotify Wrapped experience.
Given that this is just structured data behind the scenes, it will remain a puzzle as to why Spotify decided not to enable Wrapped in the browser. However, it seems that the content in the ProtoBuf payload is just enough (including the preview MP3 audio, which is 96kbps and public) to build a custom experience in any environment.