Implementing Songza Commands for Enso in Python
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.

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 "Black and Gold" at Songza.com">Black and Gold</a></li>
<li><a href="http://songza.com/z/vqzfse" title="Listen to "Eiffel 65 - Livin In A Bubble" at Songza.com">Eiffel 65 - Livin In A Bubble</a></li>
<li>…</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:
- Install the Enso Developer Prototype from the Humanized website.
- Download the python file from GitHub.
- Run
SongzaEnsoExtension.py
on the command line.
Resources
I found the following resources useful when developing the Songza commands for Enso.