Continuously extract metadata in real-time from a open.spotify.com page to a file with a custom Python format string
Click to view the video
Note
New! This scripts now works on the Desktop Spotify Client, thanks to Spicetify! You just have to install the scraper.user.js file as an extension, see the documentation
- Does not use Spotify's Web API, so you are not subject to rate-limiting
- It relies on the web page, so the playback does not have to actually happen on the device running the script (i.e. it works fine when playing remotely with Spotify Connect)
- It's pretty dang fast. This is because it reacts as soon as the UI changes, using
MutationObserver
, and the HTTP traffic does not exit the machine (except if you use an actual IP forHOST
instead oflocalhost
) - It provides the following information on the current track:
- The artist(s) (
artist
) (,-separated) - The track's name (
title
) - If it was liked (
liked
) - If we are in shuffle mode (
shuffle
) - If we are looping ('repeat current track', i.e. a loop icon with a 1 in the UI) (
loop
) - If we are repeating (repeating the current album/playlist/whatever) (
repeat
) (loop
impliesrepeat
, but not the other way around) - If the track is playing right now (
playing
) - The current playback position in seconds (
elapsed
) - The total duration of the current track in seconds (
duration
) - The upcoming lyrics line (only works when the lyrics are open) (
upcoming_lyric
) - The current lyrics line (only works when the lyrics are open) (
current_lyric
)
- The artist(s) (
- You format your string using Python f-strings, so you can do pretty much anything (even execute remote code, technically).
- It relies on the web page, so if Spotify changes its DOM structure, things will break. As long as I use the script, any breakage will be fixed relatively soon. A bit of experience with your browser's DevTools is enough to fix it by yourself, though.
- It does not provide the album's name at the moment, since this does not appear anywhere in the persistent player controls at the bottom.
- It's a bit more complicated to setup than simply running
sudo pacman -S spotify-tui
, but this can be improved with setup scripts, I'm considering that. - You format your strings using Python f-strings, so there are some annoying gotchas, like:
- You can't use backlashes (so no escape sequences) inside an
{expression}
(a workaround: usechr(92)
. Yeah, it ain't pretty. You could also use a visually similar character like set minus∖
instead. For escape sequences, you could also usechr
. Useord
in a Python shell to get the desired escape sequence's number.) - You can't use single quotes inside an
{expression}
(a workaround: use single quotation marks‘
or’
instead of an apostrophe'
)
- You can't use backlashes (so no escape sequences) inside an
This is made of two parts:
-
A client userscript that injects itself in any
open.spotify.com
page:- It listens to DOM changes in the Spotify player controls bar at the bottom of the page
- When anything interesting changes:
- It extracts information from the DOM into a status object
- It sends a GET HTTP request to
localhost:8887
(the port can be changed by editing the userscript), transmitting the status object via query parameters
-
A server that receives status updates
- It listens for requests to the provided host and port
- When a request arrives, it uses the provided f-string to render a status text
- That status text is written to a file at the provided location
Then, you can use that file anywhere to, for example, display it in your status bar
- Install the Greasemonkey extension/addon through the store
- Install the userscript with OpenUserJS at https://openuserjs.org/scripts/ewen-lbh/Spotify_Browser_Metadata_--_Scraper_userscript
Place scraper.user.js
in ~/.config/qutebrowser/greasemonkey/
It requires python ≥3.6. This can be placed anywhere, you'll just have to let it run in the background. If you ever want to reload it, be sure to terminate all previous instances, it can't use the same port twice.
Be sure to select a port that will be free.
Specify that port in scraper.user.js
by modifying PORT
at the top:
// ==UserScript==
// @name Spotify Browser Metadata -- Scraper userscript
// @namespace https://open.spotify.com
// @description Sends a GET request to notify a server of playback state changes
// @include https://open.spotify.com/*
// @author ewen-lbh <hey@ewen.works>
// ==/UserScript==
const PORT = 8887 // <---- PUT YOUR PORT HERE
const HOST = "localhost" // <--- PUT THE HOST HERE (you probably won't need to modify this)
The server takes four arguments:
python receiver.py HOST PORT FORMAT_STRING OUTPUT_FILEPATH
-
PORT
: Use the same port asscraper.user.js
's -
HOST
: Use the same hostname asscraper.user.js
's (probably "localhost") -
FORMAT_STRING
: A python f-string, without thef"
at the start nor the"
at the end. Allowed variables are the ones described in Features. Note thatreceiver.py
quotes your f-string with single quotes. This is to be consistent with how you'll pass the string to your shell, since you'll probably use single quotes too to avoid having to escape backslashes twice, for example. This means that you'll have to escape single quotes inside the format string. A small function is available for convenience:i(text, boolean)
, which outputstext
ifboolean
is True and""
otherwise.There is also
duration_string(seconds)
, which returns a hours:minutes'seconds" string from a number of seconds (The hours: part is omitted ifseconds
< 3600) -
OUTPUT_FILEPATH
: A filepath to write the evaluatedFORMAT_STRING
(i.e. the output) to. The filepath's parent directory must exist before running the script.~
and~user
constructs are handled, usingpathlib.Path.expanduser
Currently, one Polybar example is available in examples/polybar/
. (It's a trim of my polybar configuration with just enough to illustrate this Spotify script)
I might add others.
I'll happily accept PRs too, if you have any other setup (be it just another bar like lemonbar or i3status, or something completely different)