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, 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>

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 Paused 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 LastFMNetwork object must be created, and this requires that you supply an API key and API secret (provided when you request an API account), your username and an MD5 hash of your 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']")

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.


comments powered by Disqus