Linux: Spotify Track Notifier with added D-Bus love

I've recently updated the spotify notifier example I'd previously posted as an example along with my spotify screensaver script. First, I made an update to have it listen to the PropertiesChanged signal. Annoyingly for some unknown reason this signal isn't visible in D-feet which had led to the erroneous assumption it wasn't implemented.

Implementing Album Art

After getting it working again, I started to think about how it would be nice to add album art. So I rigged up the script to the last.fm API to grab album art. I'd have preferred to pull the album art from spotify or via open.spotify.com, but annoyingly there doesn't seem to be a way to get the album art url without scraping, which just seemed too much of a kludge. If anyone knows how to get album art from the spotify cache (assuming it's there) then I'd be interested to hear about it.

Listening for Spotify announcing itself on D-Bus

Once I had that done, I then thought about how this script would run. Ideally it needs to run before spotify is necessarily open. This causes a problem though because if you try and listen for D-Bus events when the app in question isn't running you'll get an error.

So I used the NameOwnerChanged signal and listened for Spotify joining D-Bus and leaving it again. This then provided all that was needed to turn on or off the listener for the PropertiesChanged signal that fires when the track changes.

Also I added a check to try and set-up the PropertiesChanged listener at start-up in-case spotify is already running when the script is launched.

The code

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Title: Spotify Notification Demo
Author: Stuart Colville, http://muffinresearch.co.uk
License: BSD

"""

import os
import dbus
import gobject
import pynotify
import httplib2
from urllib import quote
from cgi import escape
from xml.dom.minidom import parseString
from hashlib import md5
from dbus.mainloop.glib import DBusGMainLoop
from dbus.exceptions import DBusException

LASTFM_API_KEY = os.environ.get("LASTFM_API_KEY")
HTTP_CACHE_DIR = os.path.expanduser("~/.cache/spotify/http/")
IMAGE_CACHE_DIR = os.path.expanduser("~/.cache/spotify/art/")
BASE_URL = ("https://ws.audioscrobbler.com/2.0/?method=album."
            "getinfo&api_key=%s&artist=%%s&album=%%s" % LASTFM_API_KEY)

MISSING = os.path.realpath(os.path.join(os.path.dirname(__file__), "missing.png"))

if not os.path.isdir(HTTP_CACHE_DIR):
    os.makedirs(HTTP_CACHE_DIR)
if not os.path.isdir(IMAGE_CACHE_DIR):
    os.makedirs(IMAGE_CACHE_DIR)


class SpotifyNotifier(object):

    def __init__(self):
        """initialise."""
        bus_loop = DBusGMainLoop(set_as_default=True)
        self.bus = dbus.SessionBus(mainloop=bus_loop)
        loop = gobject.MainLoop()
        self.http = httplib2.Http(HTTP_CACHE_DIR)
        self.notify_id = None
        try: 
            self.props_changed_listener()
        except DBusException, e:
            if not ("org.mpris.MediaPlayer2.spotify "
                    "was not provided") in e.get_dbus_message():
                raise
        self.session_bus = self.bus.get_object("org.freedesktop.DBus", 
                                 "/org/freedesktop/DBus")
        self.session_bus.connect_to_signal("NameOwnerChanged", 
                                        self.handle_name_owner_changed,
                                        arg0="org.mpris.MediaPlayer2.spotify")

        loop.run()

    def props_changed_listener(self):
        """Hook up callback to PropertiesChanged event."""
        self.spotify = self.bus.get_object("org.mpris.MediaPlayer2.spotify", 
                                           "/org/mpris/MediaPlayer2")
        self.spotify.connect_to_signal("PropertiesChanged", 
                                        self.handle_properties_changed)

    def handle_name_owner_changed(self, name, older_owner, new_owner):
        """Introspect the NameOwnerChanged signal to work out if spotify has started."""
        if name == "org.mpris.MediaPlayer2.spotify":
            if new_owner:
                # spotify has been launched - hook it up.
                self.props_changed_listener()
            else:
                self.spotify = None


    def handle_properties_changed(self, interface, changed_props, invalidated_props):
        """Handle track changes."""
        metadata = changed_props.get("Metadata", {})
        if metadata:
            if pynotify.init("Spotify Notifier Demo"):

                title = unicode(metadata.get("xesam:title").encode("latin1"))
                album = unicode(metadata.get("xesam:album").encode("latin1"))
                artist = unicode(metadata.get("xesam:artist").encode("latin1"))
                hash_ = md5()
                hash_.update("%s-%s" % (artist, album))
                hash_path = hash_.hexdigest()
                image_path = os.path.join(IMAGE_CACHE_DIR, hash_path)
                if not os.path.exists(image_path):
                    url = BASE_URL % (escape(quote(artist.encode("utf8"))),
                                       escape(quote(album.encode("utf8"))))
                    response, content = self.http.request(url, "GET")
                    dom = parseString(content)
                    images = dom.getElementsByTagName("image")
                    for image in images:
                        for attr, value in image.attributes.items():
                            if attr == "size" and value == "medium":
                                image_url = None
                                if image and image.firstChild:
                                    image_url = image.firstChild.nodeValue
                                if image_url:
                                    response, image_contents = \
                                                self.http.request(image_url)
                                    if image_contents:
                                        fh = open(image_path, "w")
                                        fh.write(image_contents)
                                        fh.close()            

                if not os.path.exists(image_path):
                    image_path = MISSING

                alert = pynotify.Notification(title,
                              "by %s from %s" % (artist, album), image_path)
                if self.notify_id:
                    alert.props.id = self.notify_id
                alert.set_urgency (pynotify.URGENCY_NORMAL)
                alert.show()
                self.notify_id = alert.props.id

if __name__ == "__main__":
    SpotifyNotifier()

Pre-requisites

  • Spotify for Linux
  • A Spotify unlimited or premium account
  • Install httplib2: sudo apt-get install python-httplib2 (I think that's the only dep that isn't installed by default)
  • Set-up the script as outlined below.
Show Comments