Friday, January 14, 2011

Extracting Sonos Playlists - A Simple Sonos JavaScript Application, Take II

Overview

(The code on this page was last checked and verified in June 2014.)

Did you ever want to extract a Sonos playlist so you could share it or keep it for your records outside of Sonos? Well you can. In this post we’ll talk about some simple ways to do it. First, let’s start with a little history. We’ve written about Sonos a couple of other times in the blog. First we showed how to explore your Sonos UPnP topology and then we wrote a simple program using JavaScript to query Sonos (via SOAP requests) to display basic information about what was playing (on GitHub). A reader of the latter blog entry asked the question about generating playlists, and then contributed a Java program which does it (on GitHub). Still later (a year), we developed a WPF application (see WPF Application to Save and Import Sonos Playlists) to extract lists. This post summarizes all the approaches:


  • First Approach - Manual
    The approach requires minimal programming – you work with DeviceSpy (or something equivalent), issue a command, get the returned XML, and transform it. (If you need info on DeviceSpy see the Exploring Sonos via UPnP.) This approach is discussed in detail below.
  • Second Approach - Javascript in the Browser
    The second approach extracts playlists using JavaScript in a browser page. It requires you to configure your browser so that it can communicate to the Sonos devices (disable same origin policy). This approach is discussed below, but only in comparison to the first approach. See A Simple Sonos JavaScript and Java Application for details on how to use this program. (Code is on GitHub.) 
  • Third Approach - Java Program
    The third approach is a simple Java program that takes the IP address of a Sonos device and then spits out what's in its queue. You have to specifically load a playlist in a queue to download it.  (This may be useful for just capturing what's in a queue in general.) This approach requires some knowledge of how to run a basic Java program. See A Simple Sonos JavaScript and Java Application for details on how to do this. (Code is on GitHub.)
  • Fourth Approach - WPF Program
    This is a C# program running on Windows 7/8 which can extract one or many playlists. It can also import into a playlist. This approach is discussed here: WPF Application to Save and Import Sonos Playlists.


First Approach – Manual

Step 1: Open DeviceSpy and go to a zone player that is a master. If all your zones are in one group and you look at a Sonos controller (e.g. the handheld or the iPhone application) the master is the first item in the zone list*. To start with just put all the zone players in the same zone to make it easy.**

*update: Not true. The master is the zone you started the zone group with. You can
**update: Maybe easier is to just create a group of one zone and work with that then you are assured it is the master.

Step 2: In DeviceSpy, go to a zone master expand the Media Server Device, and expand the Content Directory Service.
Browse Action Dialog


Step 3: Open (double click or right Invoke Action) the Browse Action.

Invoke Action Dialog

Step 4: Fill in the parameters in the Browse Action as shown in each query below. The queries end with Query 6 which is what we really want, displaying what’s in a playlist. In each query, you fill in the fields as shown and hit Invoke and the results will be in the Result field as XML. For now you can post the XML in Notepad to get a sense of what it contains. To get the XML from the Result field to the clipboard place your cursor in the field and CTRL + A to select all.

Query 1: Get a Count of What’s in the Queue
- ObjectID = Q
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank]
- StartingIndex = 0
- RequestedCount = 1
- SortCriteria = [leave blank]
- Result = [look for the childCount attribute in the returned XML]

Query 2: Get What’s in the Queue
- ObjectID = Q:0
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank]
- StartingIndex = 0
- RequestedCount = 100 (or something high enough to get what you want; if the queue is very big consider doing this in several operations)
- SortCriteria = [leave blank]
- Result = [lots of XML returned about what’s in the queue]

Query 3: See What’s Available to Query In Terms of Searching for Music
- ObjectID = A:
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank, sure wish I knew the format for using it though…]
- StartingIndex = 0
- RequestedCount = 10
- SortCriteria = [leave blank]
- Result = [XML which shows A:ALBUMARTIST, A:ALBUM, …, A:PLAYLISTS as valid ObjectIDs]

Query 4: See What’s Available to Query System-wide
- ObjectID = 0 [zero]
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank]
- StartingIndex = 0
- RequestedCount = 10
- SortCriteria = [leave blank]
- Result = [XML which shows A:, S:, Q:, SQ:, R:, AI:, and EN: ]

A: Atribute
S: Music Shares
Q: Queues [we’ve seen an example above]
SQ: Saved Queues
R: Internet Radio
AI: Audio Inputs
EN: Entire Network

Query 5: List Saved Queues (Playlist)
- ObjectID = SQ:
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank]
- StartingIndex = 0
- RequestedCount = 100
- SortCriteria = [leave blank]
- Result = [XML which shows SQ:1 to however many playlists you have. ]

Query 6: List Songs in a Particular Saved Queue (Playlist)
- ObjectID = SQ:1 [choose a # you know exists using the previous query]
- BrowseFlag = BrowseDirectChildren
- Filter = [leave blank]
- StartingIndex = 0
- RequestedCount = 100
- SortCriteria = [leave blank]
- Result = [XML which shows contents of saved queue or playlist]

The following example below shows some XML from the Results field. The XML shows one track coming from Rhapsody and one from a local server called mediaserver.

<?xml version="1.0" encoding="utf-8"?>
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
 <item id="SQ:5/radea%3aTra.16238560.mp3:AFreely,Devendra%20Banhart,Smokey%20Rolls%20Down%20Thunder%20Canyon,298" parentID="SQ:5" restricted="true">
   <res protocolInfo="real.com-rhapsody-direct:*:audio/mp3:*" duration="0:04:58">radea:Tra.16238560.mp3</res>
   <upnp:albumArtURI>/getaa?r=1&amp;u=radea%3aTra.16238560.mp3</upnp:albumArtURI>
   <dc:title>Freely</dc:title>
   <upnp:class>object.item.audioItem.musicTrack</upnp:class>
   <dc:creator>Devendra Banhart</dc:creator>
   <upnp:album>Smokey Rolls Down Thunder Canyon</upnp:album>
 </item>
 <item id="S://mediaserver/music/Master/Supertramp/Even%20in%20the%20Quietest%20Moments/Supertramp%20-%2004%20-%20Downstream.flac" parentID="SQ:5" restricted="true">
   <res protocolInfo="x-file-cifs:*:audio/flac:*">x-file-cifs://mediamarx/music/Master/Supertramp/Even%20in%20the%20Quietest%20Moments/Supertramp%20-%2004%20-%20Downstream.flac</res>
   <upnp:albumArtURI>/getaa?u=x-file-cifs%3a%2f%2fmediamarx%2fmusic%2fMaster%2fSupertramp%2fEven%2520in%2520the%2520Quietest%2520Moments%2fSupertramp%2520-%252004%2520-%2520Downstream.flac&amp;v=167</upnp:albumArtURI>
   <dc:title>Downstream</dc:title>
   <upnp:class>object.item.audioItem.musicTrack</upnp:class>
   <dc:creator>Supertramp</dc:creator>
   <upnp:album>Even in the Quietest Moments...</upnp:album>
   <upnp:originalTrackNumber>4</upnp:originalTrackNumber>
 </item>
</DIDL-Lite>


Step 6: Take the XML from the last request (a particular saved queue) and put in a file called playlist.xml.

Step 7: Create a XSLT file in the same location as the XML file. Call it transform.xslt. You can create a text document and change the extension to .xslt.

Step 8: In the XSLT file put the following:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:didl="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
    xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/"
>
    <xsl:output method="html" indent="yes"/>
      <xsl:template match="didl:DIDL-Lite">
        <xsl:for-each select="didl:item">
            <xsl:value-of select="dc:creator"/>,
            <b>
            <xsl:value-of select="upnp:album"/>,
            </b>
            "<xsl:value-of select="dc:title"/>"
            <br></br>
        </xsl:for-each>
      </xsl:template>
</xsl:stylesheet>


Step 9: Open the playlist.xml file and put the following line after the version (?xml) line so that it’s the second line in the file:

<?xml-stylesheet type="text/xsl" href="transform.xslt"?>

Step 10: Close and save both files and double-click on the playlist.xml file. It should open in a browser and show the transformed list. For example for the XML shown above the transform produces:

Output of transforming Sonos XML

The XML the query returned is DIDL which stands for Digital Item Declaration Language and is an XML dialect for MPEG-21. In the XLST file you have to be sure and use the namespace declarations carefully. The XML file has xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" so we have to use that in XSLT as well. In the XLST we define it as xmlns:didl="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" using a prefix of “didl”.

In this example we use the browser (verified in Internet Explorer 9 and Chrome) to do the transform.

Second Approach - Javascript in the Browser


The HTML page (using JavaScript and jQuery) that was given in a previous post is approach shown (or really repeated here). Note that the page (HTML and JavaScript) depends on the ability of the browser to “Allow data access across domains” (a setting). See the post for details on doing that at least for IE and Chrome.

1. Populate a drop down list with defined playlists. Uses Query 5 above.
2. Create two buttons next to playlist drop down list. Uses Query 6 above. One button shows the playlist items in the page and the other to copy the playlist to the clipboard. (Since we are stuck with Internet Explorer, we went ahead and used the clipboardData object.
3. Automatically change the zone drop down to the master zone - was a manual process in the previous version.

An example of the page output is shown below. Get the code from GitHub.



Sunday, January 9, 2011

Picasso in Seattle

Picasso Composed of His Muses
Picasso Composed of the Women in His Life
The Picasso in Seattle exhibit (October 8, 2010 – January 17, 2011) at the Seattle Art Museum (SAM) is based on works from the collection of the Musée National Picasso, Paris. That museum is under rennovation and so the works hit the road. The exhibit at SAM includes over 150 paintings, sculptures, drawings, prints, and photographs from the artist’s personal collection. From Wikipedia: “Since Picasso left no will, his death duties (estate tax) to the French state were paid in the form of his works and others from his collection. These works form the core of the immense and representative collection of the Musée Picasso in Paris.” Mon dieu, estate taxes! And, since the work is from his private collection the idea is that it represents what Picasso wanted to use to shape his legacy.

First and foremost, props to SAM for putting on a good show. We thoroughly enjoyed it. It was crowded and could have used more breathing room, but lots of people seeing the show is good for the museum. The selection of works was great, the layout of them (more or less chronologically) made sense, and the supporting material (printed and audio) was appreciated. In regard to latter, the museum did an excellent job of making it all available on the http://www.picassoinseattle.org/ web site with the actual labels on the works, a sampling of the art exhibited, the FULL audio tour, and the script to the audio tour. Wow. This is is important because much of our processing of what we see happens after the visit and having these materials available is great for the digital pack rats that we are. And, if you brought a phone capable of browsing and playing the MP3s or podcasts you could use that instead of the audioplayer they handed out (which was prefectly fine too).

Second, in regard to Picasso, I can say I have a lukewarm relationship with him (or really his legacy and image in popular culture). I like some of his works and cringe at others. Picasso speaks to me in Art like Hemingway speaks to me in Literature. Part of my uncertain thoughts about Picasso center on his relationships with women (his muses) and how his art changed with each new relationship. Reams have been written about the relationships yet the simple thought I could not get out of my mind was if I was missing something choosing a long term relationship. Was I artistically stunting myself? :-)