Implementing Songza Commands for Enso in Python

The songza.com website has changed considerably since I wrote this article. As a result, some of the links to songza.com in this article do not work and have no up-to-date equivalent.

This article describes the Python implementation of a set of commands that integrate the Songza jukebox website and the Enso command line system.

1. Introduction

This article describes a set of commands that integrate Humanized’s Enso, the linguistic command line interface, with Songza, the music search engine and internet jukebox.

  • songza list {song list} inserts the most played songs and the featured songs at Songza.com.
  • songza playlist inserts the playlist of the currently selected Songza.com username.

Songza commands for Enso

The Songza commands insert links to songs marked up in XHTML. For example, if you are working in Word and you want some music, simply insert a list of songs, such as your own playlist, click on a song link and the Songza website will open in your browser and start playing the song you clicked on. Clickable song lists are available in any application capable of rendering the list of XHTML links returned by the Songza commands.

The Songza commands for Enso are implemented in Python and contained in the SongzaEnsoExtension.py file. The Try It section below explains how to run the Songza commands for Enso with the Enso Developer Prototype.

2. The songza list {song list} Command

The songza list {song list} command inserts the most played songs and the featured songs at Songza.com.

  • songza list top inserts the most played songs.
  • songza list featured inserts the featured songs.

Both commands insert an unordered list of links to the songs at Songza.com marked up in XHTML. For example, the following XHTML markup from the songza command

<ul>
<li><a href="http://songza.com/z/ydulbu" title="Listen to &quot;Black and
    Gold&quot; at Songza.com">Black and Gold</a></li>
<li><a href="http://songza.com/z/vqzfse" title="Listen to &quot;Eiffel 65 - Livin
    In A Bubble&quot; at Songza.com">Eiffel 65 - Livin In A Bubble</a></li>
<li>&hellip;</li>
</ul>

would produce the following unordered list of links

in applications capable of rendering XHTML, such as Microsoft Word and OpenOffice Writer. In other applications, the raw XHTML is inserted.

The Songza.com Public Feed

The songza list {song list} command retrieves song lists in XML format from the public feed of the Songza API, which returns song lists in the following format:

<?xml version="1.0" encoding="UTF-8"?>
<public_feed>
    <name>Top Played Songs</name>
    <songs>
        <song>
            <id>a2r3-eHuebHTD-lY</id>
            <title>Black and Gold</title>
            <date_added>Fri, 16 May 2008 01:00:32 +0000</date_added>
            <link>http://songza.com/z/ydulbu</link>
        </song>
        <song>
            <id>j0k8-625D43545C5A6A</id>
            <title>Eiffel 65 - Livin In A Bubble</title>
            <date_added>Thu, 15 May 2008 13:00:53 +0000</date_added>
            <link>http://songza.com/z/vqzfse</link>
        </song>
        ...
    </songs>
</public_feed>

Python Implementation

The classes that implement the Songza commands extend the AbstractSongzaCommand class. The AbstractSongzaCommand class extends the threading.Thread class to enable Songza commands to execute on a separate thread.

class AbstractSongzaCommand( threading.Thread ):

    def __init__( self, ensoEndpoint, commandPostfix="" ):

        # initialise this class by calling its parent's constructor

        threading.Thread.__init__( self )

        # store a reference to the XML-RPC endpoint of the Enso Developer

        self.ensoEndpoint = ensoEndpoint;

        # store the command postfix

        self.commandPostfix = commandPostfix


    def getXMLSongList( self, URL, tagName ):
        "Download and parse the XML feed found at URL."

        import urllib

        try:

            # retrieve the song list marked up in XML

            xmlFeed = urllib.urlopen( URL ).read()

        except IOError:

            # can't do anything without the XML feed

            xmlSongList = None

        else:

            from xml.dom import minidom

            from xml.parsers.expat import ExpatError

            try:

                # parse the XML feed into an XML document

                xmlSongList = minidom.parseString( xmlFeed )

            except ExpatError:

                # can't do anything without an error-free XML feed

                xmlSongList = None

            else:

                try:

                    # Ensure that the feed at URL is the feed we asked for.

                    # Check that the main element of the parsed XML feed

                    # document is the same as tagName. For example, if we ask

                    # for the Songza.com public feed with the following URL:

                    #

                    #     URL = "http://api.songza.com/1.0/public_feed/top.xml"

                    #

                    # the main element of the parsed XML document should be

                    # <public_feed>.

                    assert xmlSongList.documentElement.tagName == tagName

                 except AssertionError:

                    # can't do anything without the correct XML feed

                    xmlSongList = None

        # return the parsed XML song list (or None if we failed)

        return xmlSongList

The __songzaList() function below implements the songza list {song list} command.

def __songzaList( self, commandPostfix ):
    """
    Implementation of the 'songza list {song list}' command:

        songza list top
            returns the most played songs at Songza.com

        songza list featured
            returns the featured songs at Songza.com

    Both commands insert an unordered list of links to the songs at
    Songza.com marked up in XHTML.

    For more information about the 'songza list {song list}' command, visit

    http://www.ensowiki.com/wiki/index.php?title=Songza

    """

    class SongzaListCommand( EnsoExtensionMethods.AbstractSongzaCommand ):

        def __init__( self, ensoEndpoint, commandPostfix ):

            # initialise this class by calling its parent's constructor
            EnsoExtensionMethods.AbstractSongzaCommand.__init__( \
                self, ensoEndpoint, commandPostfix )


        def run( self ):
            "Execute the 'songza list {song list}' command on a separate thread"

            if self.commandPostfix == '':

                # display 'no song list' message
                self.ensoEndpoint.enso.displayMessage( "<p>No song list!</p>" )

                # can't do anything without the postfix so exit
                return

            # display 'fetching' message
            self.ensoEndpoint.enso.displayMessage( \
                "<p>Fetching song list...</p><caption>from Songza.com</caption>" )

            # URL of the Songza.com XML public feed
            URL = "http://api.songza.com/1.0/public_feed/%s.xml" % \
		self.commandPostfix

            # download and parse the XML feed
            xmlSongList = self.getXMLSongList( URL, "public_feed" )

            if xmlSongList == None:

                # display 'problem downloading playlist' message
                self.ensoEndpoint.enso.displayMessage( \
                    "<p>Couldn't download song list</p>" \
                    "<caption>from Songza.com</caption>" )

                # can't do anything without the XML song list so exit
                return

            # get the name of the song list from the XML document
            # (the name of the song list is stored in the <name> tag)
            songListName = xmlSongList.getElementsByTagName( \
		"name" )[0].firstChild.data

            # get a list of song DOM nodes from the XML document
            # (each song is stored in a <song> tag)
            songNodeList = xmlSongList.getElementsByTagName( "song" )

            # build the XHTML song list
            xhtmlSongList = self.buildXHTMLSongList( songNodeList )

            # insert the XHTML song list
            self.ensoEndpoint.enso.insertUnicodeAtCursor( \
                xhtmlSongList, "songza list {song list}" )

            # tell the user which song list was retrieved
            self.ensoEndpoint.enso.displayMessage( \
                "<p>%s</p><caption>at Songza.com</caption>" % songListName )

            # release the memory used by the XML document
            xmlSongList.unlink()


    # create the 'songza list {song list}' command as a separate thread
    command = SongzaListCommand( self, commandPostfix )

    # execute the command
    command.start()

The SongzaListCommand class uses the buildXHTMLSongList() method of the AbstractSongzaCommand class to build the list of links marked up in XHTML.

3. The songza playlist Command

The songza playlist command inserts the playlist of the currently selected Songza.com username. The songs on the playlist are marked up in XHTML as an unordered list of links to the songs at Songza.com, as described above in the section on the songza list {song list} command.

The Songza.com Feed

The songza playlist command retrieves song lists in XML format from the feed of the Songza API, which returns song lists in the following format:

<feed>
    <username>srobbin</username>
    <songs>
        <song>
            <id>j0k4-cPMABtr6US5</id>
            <title>Flight of the Conchords - The Most Beautiful Girl</title>
            <date_added>Sat, 17 May 2008 20:00:00 +0000</date_added>
            <link>http://songza.com/srobbin?z=xufn8k</link>
        </song>
        <song>
            <id>a2r3-GoLJJRIWCLU</id>
            <title>Radiohead - Jigsaw Falling Into Place (thumbs down
		version)</title>
            <date_added>Wed, 14 May 2008 21:47:05 +0000</date_added>
            <link>http://songza.com/srobbin?z=fgmnit</link>
        </song>
        ...
    </songs>
</feed>

Python Implementation

The __songzaPlaylist() function below implements the songza playlist command.

def __songzaPlaylist( self ):
    """
    Implementation of the 'songza playlist' command:

    Insert an unordered list of links to the songs on the selected
    Songza user's playlist marked up in XHTML.

    For more information about the 'songza playlist' command, visit

http://www.ensowiki.com/wiki/index.php?title=Songza

    """

    class SongzaPlaylistCommand( EnsoExtensionMethods.AbstractSongzaCommand ):

        def __init__( self, ensoEndpoint ):

            # initialise this class by calling its parent's constructor

            EnsoExtensionMethods.AbstractSongzaCommand.__init__( \
                self, ensoEndpoint )


        def isValidSongzaUsername( self, songzaUsername ):
            """
            Check if songzaUsername is a syntactically valid username.
            Valid usernames have between 3 and 16 alphanumeric characters.
            (This method does not check if username exists.)
            """

            import re

            # validation pattern that matches between 3 and 16 alphanumeric

            # characters (ignoring leading and trailing spaces)

            pattern = "^\s*\w{3,16}\s*$"

            # test the username against the validation pattern

            isValid = re.search( pattern, songzaUsername ) != None

            return isValid


        def run( self ):
            "Execute the 'songza playlist' command on a separate thread"

            # get the Songza user's username from the current selection

            songzaUsername = self.ensoEndpoint.enso.getUnicodeSelection()

            if len( songzaUsername ) == 0:

                # display 'no Songza username selected' message

                self.ensoEndpoint.enso.displayMessage( \
                    "<p>No Songza username selected!</p>" )

                # can't do anything without a username so exit

                return

            if not self.isValidSongzaUsername( songzaUsername ):

                # display 'invalid Songza username' message

                self.ensoEndpoint.enso.displayMessage( \
                    "<p>Invalid Songza username selected!</p>" \
                    "<caption>Songza.com usernames have between " \
                    "3 and 16 alphanumeric characters</caption>" )

                # can't do anything without a valid username so exit

                return

            # remove leading and trailing spaces from the username

            songzaUsername = songzaUsername.strip();

            # display 'fetching' message

            self.ensoEndpoint.enso.displayMessage( \
                "<p>Fetching %s's playlist...</p>" \
                "<caption>from Songza.com</caption>" % songzaUsername )

            # URL of the Songza.com XML feed

            URL = "http://api.songza.com/1.0/feed/%s.xml" % songzaUsername

            # download and parse the XML feed

            xmlSongList = self.getXMLSongList( URL, "feed" )

            if xmlSongList == None:

                # display 'problem downloading playlist' message

                self.ensoEndpoint.enso.displayMessage( \
                    "<p>Couldn't download %s's playlist</p>" \
                    "<caption>from Songza.com</caption>" % songzaUsername )

                # can't do anything without the XML song list so exit

                return

            # get a list of song DOM nodes from the XML document

            # (each song is stored in a <song> tag)

            songNodeList = xmlSongList.getElementsByTagName( "song" )

            if songNodeList == []:

                # display 'empty playlist' message

                self.ensoEndpoint.enso.displayMessage( \
                    "<p>No songs on %s's playlist</p>" \
                    "<caption>at Songza.com</caption>" % songzaUsername )

            else:

                # build the XHTML song list

                xhtmlSongList = self.buildXHTMLSongList( songNodeList )

                # insert the XHTML song list

                self.ensoEndpoint.enso.insertUnicodeAtCursor( \
                    xhtmlSongList, "songza playlist" )

                # tell the user about the playlist

                self.ensoEndpoint.enso.displayMessage(
                    "<p>Songs on %s's playlist</p>" \
                    "<caption>at Songza.com</caption>" % songzaUsername )

            # release the memory used by the XML document

            xmlSongList.unlink()


    # create the 'songza playlist' command as a separate thread

    command = SongzaPlaylistCommand( self )

    # execute the command

    command.start()

The SongzaPlaylistCommand class uses the buildXHTMLSongList() method of the AbstractSongzaCommand class to build the list of links marked up in XHTML.

Try It

To try the Songza commands for Enso for yourself:

  1. Install the Enso Developer Prototype from the Humanized website.
  2. Download the <a href="https://github.com/UsabilityEtc/Songza-Commands-for-Enso" title="Visit the github page for SongzaEnsoExtension.py">SongzaEnsoExtension.py</a> python file from github.
  3. Run SongzaEnsoExtension.py on the command line.

Resources

I found the following resources useful when developing the Songza commands for Enso.