As someone who listens to a lot of music, and often forgets what exactly I’ve listened to, I’ve been interested in systems that effectively track my listening over time. I’ve used last.fm for a while, but it’s a bit more of a closed system than I’d prefer. ListenBrainz is a service very similar to last.fm, but from the MetaBrainz Foundation, that is both open source and makes all listening data available. It also has a very handy API.
Listenbrainz API
The ListenBrainz API has a fair amount of functionality, but today I’m going to be dealing only with the part that pertains to submitting listens. According to the API’s documentation, to submit a listen, we have to make an authenticated HTTP POST request with the listen info in the contents formatted as JSON. Let’s break this down.
We need:
- A listen to submit.
- The listen data needs to be formatted as valid JSON
- The JSON needs to go in a POST request
- The POST request needs to be authenticated
- We need to send the POST to the API endpoint
- We should probably check the response to the request.
What’s in a Listen?
To determine the information that constitutes a ’listen’ we reference the ListenBrainz JSON docs . Here we can see that there are multiple ways to submit listens; we can submit a single listen, a playing now listen, or a batch of multiple listens. For now we’ll keep it simple and focus only on submitting a single listen. So we set our ’listen_type’ to ‘single’.
{
'listen_type' : 'single'
'payload' : { [
// listen payload goes here
]
}
}
Okay, now that we’ve specified that we’re only submitting a single listen, what actually goes in the payload? Three things:
- Time of listen - Unix time in seconds
- Artist Name
- Track Name
Everything beyond these three pieces of information is extra, in that the API will accept a listen with nothing more than the above, but worth submitting if you have it. Better yet would be submitting a MusicBrainz Identifier (MBID) but we’ll leave that for another time.
I wrote up a little class to manage these pieces of info and make it easier to get the pieces formatted and nested correctly.
import time
class Listen(object):
def __init__(self, trackName, artistName, releaseName = None, listenTime = None):
self.trackName = trackName
self.artistName = artistName
self.releaseName = releaseName
if listenTime:
self.listenedAt = listenTime
else:
self.listenedAt = int(time.time())
def getTrackMetadata(self):
metadata = {}
metadata["artist_name"] = self.artistName
metadata["track_name"] = self.trackName
if self.releaseName:
metadata["release_name"] = self.releaseName
return metadata
def getListenInfo(self):
listenInfo = {}
listenInfo["listened_at"] = self.listenedAt
listenInfo["track_metadata"] = self.getTrackMetadata()
return listenInfo
As I said above, the class is mostly just to keep the listen information in one place and to have a way to get the information out in the format specified by the API. It doesn’t do a whole lot at this point and the same functionality could probably be had with a namedtuple and a specialized function, but if I’m to extend this at some point down the line I think the class will simplify things. One note about the ’listenTime’ field; at this point, since I’m mostly just trying to get my request accepted by the API, instead of submitting ‘real’ listens, if I don’t have a time for the listen, I just take the time the listen was created.
Formatting the Listen as Valid JSON
I’m using Python for this, and Python has a library for JSON support. In the development process at first I was using the json library to take the dictionary output from my class and encode it as proper json, but that was before I learned that the python library Requests can actually take a dictionary, and submit it as json in the request body. Which brings me to Requests.
Requests
Requests is a really useful python library for dealing with http requests and responses. It handles the formatting and parsing of http packets and serves as a useful wrapper for the underlying networking to actually connect to the endpoint. Here I’ll be using it to connect to the ListenBrainz API endpoint and submit our formatted listen.
import requests
def get_single_listen(listen):
listen_dict = {}
listen_dict["listen_type"] = "single"
listen_dict["payload"] = [listen.getListenInfo()]
return listen_dict
def postListen(apiUrl, userToken, listen):
headerDict = {}
authTokenStr = "Token %s" % userToken
headerDict['Authorization'] = authTokenStr
r = requests.post(apiUrl, headers=headerDict, json=get_single_listen(listen))
return r
Here we have a simple wrapper function to get a single listen from our Listen class, and the function to make the http request. We pass in the url for the endpoint, the listen, which it will format as JSON for us, and the headers which contains an authorization token to use the endpoint.
Authentication
In order to successfully post a listen via the listenbrainz api, we need to authenticate with a user-token for the listener. The user token is available on the listener’s profile, and must be passed in the http request header in the ‘Authorization’ field. For now I just saved my token in a text file, and I read it and pass it to the function that makes the request. I’m sure there’s a better more secure way to do this, but as a test this suffices for now.
def get_user_token(userTokenFilename):
with open(userTokenFilename, 'r') as userTokenFile:
txt = userTokenFile.read()
txt = txt.strip()
return txt
Make it Go
Finally, I’ve put all the pieces together as part of a little main function, and for now have just hardcoded in a worthy listen.
def main():
print("Listenbrainz test")
apiUrl = "https://api.listenbrainz.org/1/submit-listens"
userToken = get_user_token('listenbrainz_user_token.txt')
myListen = Listen("Rydeen", "Yellow Magic Orchestra", "Solid State Survivor")
listenRequest = postListen(apiUrl, userToken, myListen)
print(listenRequest)
if __name__ == "__main__":
main()
If everything goes well we should get a 200 response to the request, indicating success, and the listen should show up on my ListenBrainz page.