Spotify Folder Tools

Using Spotify’s Private API to reorganize playlists into folders programmatically

Dec 10, 2024

Check it out on Github (WIP)

Demo video:
Ever wanted to back up your Daylist every time it refreshes in case you want to listen to it again later? How about if you have a script that runs every night at 3am that creates a true-random shuffled version of your Liked Songs (no? just me?) and you want to save those playlists for later? Well, that’s not too hard - just use the Spotify API to create and save a new playlist. But oh no! You don’t want random playlists cluttering up your library? Fear not! At last, we can use code to move those playlists around.
It turns out that many people don’t know you can even make folders in Spotify - it’s only available on the desktop or web versions of Spotify. But you can! And I use them extensively. Sometimes, I’ll want to save a particular Daylist (because when spotify generates ”panicked livestreaming late night” how can I NOT save that?), but I don’t want it taking up space in my normal Spotify library. Needless to say, I’m a big fan! But I always wanted to be able to use the scripts I wrote to move those playlists into sub-folders, which the public Spotify API doesn’t support. Turns out, you can do just about anything with the Private API.
As far as I can tell, no one has done this yet. There are a few github repos that deal with accessing the Private API for other reasons, but none are folder-related. And Spotify users want this feature! [link]. So here we are.
By snooping through network logs while moving playlists around, I was able to find the CURL request that corresponds to a playlist “move”:
curl 'https://spclient.wg.spotify.com/playlist/v2/user/[username]/rootlist/changes' \ -H 'accept: application/json' \ -H 'accept-language: en' \ -H 'app-platform: WebPlayer' \ -H 'authorization: Bearer [bearer]' \ -H 'client-token: [client token]' \ -H 'content-type: application/json;charset=UTF-8' \ -H 'dnt: 1' \ -H 'origin: https://open.spotify.com' \ -H 'priority: u=1, i' \ -H 'referer: https://open.spotify.com/' \ -H 'sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "macOS"' \ -H 'sec-fetch-dest: empty' \ -H 'sec-fetch-mode: cors' \ -H 'sec-fetch-site: same-site' \ -H 'spotify-app-version: 1.2.53.308.gade36c30' \ -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \ --data-raw '{"deltas":[{"ops":[{"kind":4,"mov":{"items":[{"uri":"spotify:playlist:[playlistID]","attributes":{"formatAttributes":[],"availableSignals":[]}}],"addAfterItem":{"uri":"spotify:start-group:[folderID]","attributes":{"formatAttributes":[],"availableSignals":[]}}}}],"info":{"source":{"client":5}}}],"wantResultingRevisions":false,"wantSyncResult":false,"nonces":[]}'
Running that in terminal (with the real data) would successfully move the playlist into the target folder. Great!
This (the --data-raw portion) was the key:
{ "deltas": [ { "ops": [ { "kind": 4, "mov": { "items": [ { "uri": "spotify:playlist:[playlistID]", "attributes": { "formatAttributes": [], "availableSignals": [] } } ], "addAfterItem": { "uri": "spotify:start-group:[folderID]", "attributes": { "formatAttributes": [], "availableSignals": [] } } } } ], "info": { "source": { "client": 5 } } } ], "wantResultingRevisions": false, "wantSyncResult": false, "nonces": [] }
Folder IDs are easy to get by clicking+dragging dragging the folder into a text editor or terminal:
notion image
Now, I just needed to get an auth token. The Spotify Private API is the one that’s used when you as a user interact with the app, and isn’t really intended for use like this. Since I’m not abusing it, I’m hoping it won’t cause an issue.
You need to get a client token, and in this case, the easiest (and somehow most reliable) was to use ChromeDriver as a headless browser, log in, capture the client token from the network logs, then use that for authorization.
My code stores that token along with the refresh timestamp, which indicates how after long you’ll need to refresh the token. If there’s less than 5 minutes of time left, I re-log-in, if not, I use the existing token. So far, I haven’t had an issue with CAPTCHA, but YMMV.