One of the joys of Python is the ease with which you can hook things together using only a few lines of code. To give an example, I’ve been using a media player recently that doesn’t natively support scrobbling of music to Last.fm, and I’ve not been able to find a working scrobbler plugin. However, the player is based on JRiver Media Center, which supports interaction via a RESTful web service called MCWS - so building an external solution is possible.
The first step is to figure out how to query the player for the track that is currently playing. The relevant REST query URL is easily found because the player handily serves documentation for MCWS from the same web server that runs the service. Requests can then be used to make the query:
import requests url = "http://localhost:52199/MCWS/v1/Playback/Info/Zone=-1" response = requests.get(url)
The response payload is XML and looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <Response Status="OK"> <Item Name="ZoneID">0</Item> <Item Name="State">2</Item> <Item Name="FileKey">36</Item> <Item Name="NextFileKey">-1</Item> <Item Name="PositionMS">513506</Item> <Item Name="DurationMS">749853</Item> <Item Name="ElapsedTimeDisplay">8:33</Item> <Item Name="RemainingTimeDisplay">-3:56</Item> <Item Name="TotalTimeDisplay">12:29</Item> <Item Name="PositionDisplay">8:33 / 12:29</Item> <Item Name="PlayingNowPosition">4</Item> <Item Name="PlayingNowTracks">5</Item> <Item Name="PlayingNowPositionDisplay">5 of 5</Item> <Item Name="PlayingNowChangeCounter">132</Item> <Item Name="Bitrate">897</Item> <Item Name="Bitdepth">16</Item> <Item Name="SampleRate">44100</Item> <Item Name="Channels">2</Item> <Item Name="Chapter">0</Item> <Item Name="Volume">0.20035</Item> <Item Name="VolumeDisplay">20%</Item> <Item Name="ImageURL">MCWS/v1/File/GetImage?File=36</Item> <Item Name="Artist">Pink Floyd</Item> <Item Name="Album">Wish You Were Here (Experience Edition)</Item> <Item Name="Name">Shine On You Crazy Diamond (Part Two)</Item> <Item Name="Status">Playing</Item> </Response>
This can be parsed easily enough with the implementation of ElementTree
provided in the Python Standard Library, using XPath expressions to pick
out the required data. Some care is needed, to ignore responses which
don’t have a status of
Playing. (My player reports the status as
when playback has been paused and provides the details of the last-played
track, without a status item, when playback is stopped.)
To do the actual scrobbling, pylast can be used. A
object must be created, and this requires that you supply an API key and API
secret (provided when you request an API account), your Last.fm username
and an MD5 hash of your Last.fm password. Scrobbling is done simply by
calling this object’s
scrobble method, supplying the track details and a
UNIX timestamp for the time the track started playing.
import pylast import time from xml.etree import ElementTree lastfm = pylast.LastFMNetwork(...) # arguments not shown data = ElementTree.fromstring(response.content) status = data.find("Item[@Name='Status']") if status is not None and status.text == "Playing": artist = data.find("Item[@Name='Artist']") album = data.find("Item[@Name='Album']") title = data.find("Item[@Name='Name']") lastfm.scrobble( artist=artist.text, album=album.text, title=title.text, timestamp=int(time.time()) )
Code like this can be put into a loop that checks periodically for a playing
track (every 30 seconds, say) and calls the
scrobble method if track
details have changed since the last scrobble. The timestamp will need to
be adjusted backwards by up to 30 seconds, using the elapsed track time
reported by MCWS.
All in all, it’s remarkable how quickly a crude but functional scrobbling application can be constructed. The key, as always, is knowing that powerful libraries are available (Requests, ElementTree, pylast) to do most of the heavy lifting. Of course, none of it would be possible without the media player choosing to expose its functionality via an open API. Once again, I’m reminded that an expressive language, powerful libraries and open APIs are a potent combination.
A generalised and heavily refactored version of this code is available on GitHub.