Movable Type Home Page

Movable Type Scripts


XSLT script to generate album listing from iTunes XML

When I got myself an iPod, I found the printed listings available from the iTunes program were not particularly great (despite the interactive interface being superb), and I couldn’t get a concise summary list of albums. Since iTunes stores its library as XML, I used XSLT to create a listing more to my liking. Since I tend to listen to albums rather than singles, I wanted a list of albums grouped by genre.

To use this script, save the albumList.xml and albumList.xsl shown below to your My Music/iTunes folder, and open albumList.xml in Internet Explorer 6+ or Mozilla Firefox (IE5 won’t work; I haven’t tested any other browsers).

The iTunes XML ‘property list’

The iTunes library is stored as XML in a file called iTunes Music Library.xml. It uses a generic XML language known as property list, or plist, which holds information as key/value pairs. Rather than being tailored to describing songs, it can be used to describe properties of more or less anything. This is how the XML looks:

<plist version="1.0">
  <dict>
    <key>Tracks</key>
    <dict>
      <key>638</key>
      <dict>
        <key>Track ID</key><integer>638</integer>
        <key>Name</key><string>Take Me To The River</string>
        <key>Artist</key><string>Talking Heads</string>
        <key>Composer</key><string>Al Green &#38; M. Hodges</string>
        <key>Album</key><string>Stop Making Sense</string>
        <key>Genre</key><string>Alternative &#38; Punk</string>
        ... etc ...
      </dict>
    </dict>
  </dict>
</plist>

This means that to find, say, the album name, we need to match a <string> with an immediately preceding sibling <key> of ‘Album’:

string[preceding-sibling::key[1]='Album']

That is, the <string> element where the 1st <key> element in the preceding-sibling axis has the value ‘Album’.

While this is a convenient general-purpose XML language, it is definitely more complex to manipulate than an XML language tailored to represent a list of tracks, so take a deep breath for what follows!

Getting a list of albums

The iTunes XML is a list of songs. The album the song appears on is nothing more, really, than an attribute of the song, so a simple approach will give the album name once for each song which appears on it. So to get a correct list of albums, I use rather clever tricks available with the <xsl:key> element and the key() and generate-id() functions.

Other people have explained the use of <xsl:key> better then I can, but in brief, here, the <xsl:key> element builds a list of <dict> nodes representing songs. The <xsl:for-each> element then selects <dict> (i.e. song) elements in which the automatically generated id matches the automatically generated id of the first node returned by the key() function. No, it makes my head spin, too. The resulting stylesheet to get a list of albums (with the associated artists) is:

<xsl:key name="songsByAlbum" match="dict"
  use="string[preceding-sibling::key[1]='Album']"/>

<xsl:template match="/plist/dict/dict">
  <html>
    <body>
      <table>

        <xsl:for-each select="dict[generate-id(.)=
            generate-id(key('songsByAlbum',string)[1])]">
          <xsl:sort select="string[preceding-sibling::key[1]='Album']"/>
          <tr>
            <td><xsl:value-of select="string[preceding-sibling::key[1]='Album']"/></td>
            <td><xsl:value-of select="string[preceding-sibling::key[1]='Artist']"/></td>
          </tr>
        </xsl:for-each>

      </table>
    </body>
  </html>
</xsl:template>

This has a problems with compilations, though. It will pick up whoever is the the artist for the first song of a compilation, which looks somewhat strange. To fix this, I use the ‘Part of a Compilation’ flag as follows:

<xsl:key name="songsByAlbum" match="dict"
  use="string[preceding-sibling::key[1]='Album']"/>

<xsl:template match="/plist/dict/dict">
  <html>
    <body>
      <table>

        <xsl:for-each select="dict[generate-id(.)=generate-id(key('albums',string)[1])]">
          <xsl:sort select="string[preceding-sibling::key[1]='Album']"/>
          <tr>
            <td><xsl:call-template name="albumName"/></td>
            <td><xsl:call-template name="artistName"/></td>
          </tr>
        </xsl:for-each>

      </table>
    </body>
  </html>
</xsl:template>

<xsl:template name="albumName">
  <xsl:value-of select="string[preceding-sibling::key[1]='Album']"/>
</xsl:template>

<xsl:template name="artistName">
  <xsl:choose>
    <xsl:when test="true[preceding-sibling::key[1]='Compilation']">
      <i>Compilation</i>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="string[preceding-sibling::key[1]='Artist']"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

I’ve broken down the main template into separate templates for album and artist – XSLT can be difficult to follow at the best of times, so I like to modularise the templates as much as I can.

A list of albums grouped by genre

The trouble with this list is the way the music is all mashed up together. I didn’t really like having the St Matthew Passion rubbing shoulders with Stop Making Sense.

To group the albums by genre, I use a technique devised by Steve Muench, Oracle’s ‘XML Technical Evangelist’, dubbed ‘Muenchian grouping’.

Again, other people have explained this better than I could, so suffice to say (if you want to try to follow the operation of the XSL) that it uses the generate-id() function on the XSL keys defined on songs by Genre and Album, and at the point where template albumsInGenre is called, the current context is the first <dict> song within each genre.

Within the albumsInGenre template, the same technique is used to obtain the first song within each Album, in order to get the album name and artist. I also use an additional predicate to further filter this node-set to the genre passed as a parameter to the template.

(Life will be easier with XSLT 2.0 which will have a for-each-group element).

The stylesheet looks like this:

<xsl:key name="songsByAlbum" match="dict"
  use="string[preceding-sibling::key[1]='Album']"/>
<xsl:key name="songsByGenre" match="dict"
  use="string[preceding-sibling::key[1]='Genre']"/>


<xsl:template match="/plist/dict/dict">
  <html>
    <body>
      <table>

        <xsl:for-each select="dict[generate-id(.)=
            generate-id(key('songsByGenre',string)[1])]">
          <xsl:sort select="string[preceding-sibling::key[1]='Genre']"/>
          <xsl:for-each select="key('songsByGenre',string)[1]">
            <xsl:call-template name="albumsInGenre">
              <xsl:with-param name="genre"
                  select="string[preceding-sibling::key[1]='Genre']"/>
            </xsl:call-template>
          </xsl:for-each>
        </xsl:for-each>

      </table>
    </body>
  </html>
</xsl:template>


<xsl:template name="albumsInGenre">
  <xsl:param name="genre"/>

  <tr><td colspan='3'><b><xsl:value-of select="$genre"/></b></td></tr>

  <xsl:variable name="song" select="/plist/dict/dict/dict"/>
  <xsl:for-each select="$song[generate-id(.)=
      generate-id(key('songsByAlbum',string[preceding-sibling::key[1]='Album'])[1])]">
    <xsl:sort select="string[preceding-sibling::key[1]='Album']"/>
    <xsl:for-each select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])
        [string[preceding-sibling::key[1]='Genre']=$genre][1]">
      <tr>
        <td> </td>
        <td><xsl:call-template name="albumName"/></td>
        <td><xsl:call-template name="artistName"/></td>
      </tr>
    </xsl:for-each>
  </xsl:for-each>
</xsl:template>

And the resulting album list looks something like:

Electronica/Dance
  Boulevard St. Germain
  Dehli9 Tosca
  On Land Brian Eno
Jazz
  Madar Jan Garbarek
  The Hot Fives & Sevens  Louis Armstrong & The Hot Five

If you want just the albums on the iPod, rather than all the albums in the iTunes library, then an extra condition has to be added to the inner-most for-each, so that it becomes:

<xsl:for-each select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])
        [string[preceding-sibling::key[1]='Genre']=$genre]
        [not(true[preceding-sibling::key[1]='Disabled'])][1]">

Adding track times

Having got this far, I wanted to see the duration of each album. The duration of each song is stored (in milliseconds) against a Total Time key, so the template to do this is:

<xsl:template name="iTunesTimeAlbum">
  <xsl:variable name="tracksInAlbum"
      select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])"/>
  <xsl:variable name="t"
      select="sum($tracksInAlbum/integer[preceding-sibling::key[1]='Total Time'])"/>
  <xsl:call-template name="formatTime">
    <xsl:with-param name="t" select="$t"/>
  </xsl:call-template>
</xsl:template>

<xsl:template name="formatTime">
  <xsl:param name="t"/>
  <xsl:if test="$t != 0">
    <xsl:variable name="h" select="floor(($t div (1000*60*60)))"/>
    <xsl:variable name="m" select="floor(($t div (1000*60)) mod 60)"/>
    <xsl:variable name="s" select="floor(($t div 1000) mod 60)"/>
    <xsl:if test="$h != 0"><xsl:value-of select="$h"/>h</xsl:if>
    <xsl:value-of select="format-number($m,'00')"/>m
    <xsl:value-of select="format-number($s,'00')"/>s
  </xsl:if>
</xsl:template>

Again, if you want to see the duration on the iPod instead of in the iTunes library (when only some of the tracks on an album have been selected for transfer to the iPod), an extra condition has to be added for the select for variable t:

<xsl:variable name="t"
      select="sum($tracksInAlbum/integer[preceding-sibling::key[1]='Total Time']
      [not(../true[preceding-sibling::key[1]='Disabled'])])"/>

A similar template will give the total time of music in the iTunes library (or on the iPod):

<xsl:template name="iTunesTimeTotal">
  <xsl:variable name="t"
      select="sum(dict/integer[preceding-sibling::key[1]='Total Time'])"/>
  <xsl:call-template name="formatTime">
    <xsl:with-param name="t" select="$t"/>
  </xsl:call-template>
</xsl:template>

And if you wanted to have the total disk size of the songs, it is available against the Size key:

<xsl:template name="iTunesSizeTotal">
  <xsl:variable name="s" select="sum(dict/integer[preceding-sibling::key[1]='Size'])"/>
  <xsl:value-of select="floor($s div (1000*1000)) div 1000"/>GB
</xsl:template>

Opening the listing directly in a browser

Rather than using special XML/XSLT tools, I open the listing directly in a browser. To do this, I create a ‘wrapper’ XML file which specifies which stylesheet to apply to what XML file, using the the <?xml-stylesheet> processing instruction and the <incl> element:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="albumList.xsl" type="text/xsl"?>
<wrapper>
  <incl file="iTunes Music Library.xml"/>
</wrapper>

I then put a template at the top of the XSL stylesheet which matches the <wrapper> element I put around the included XML file, and applies the stylesheet templates to the included XML file.

<xsl:template match="/wrapper">
    <xsl:apply-templates select="document(incl/@file)"/>
  </xsl:template>

This ought to work in any current browser; I’ve checked it on Internet Explorer (6.0+) and Firefox.

Note: an alternative approach would be to pre-process the p-list XML into a more conventional structure, as explained at www.xmldatabases.org/WK/blog/1086?t=item, which would then be simpler to manipulate with XSLT. I’m prepared to live with the preceding-sibling construct in order to be able to generate the list in a single step.


All this is put together in the stylesheet below. It makes for rather a large stylesheet, but with the information above, you should be able to pick it apart and use the parts you want.

... so now I have a quick reference list of the albums on my iPod.


To use this script, in its basic or full form, save the albumList.xml and albumList.xsl shown below to your My Music/iTunes folder, and open albumList.xml in your browser (IE6 or Firefox). If you want to tailor the layout, with the guidelines above you should be able to mix and match the components as required. You are welcome to re-use these scripts [without any warranty express or implied] for personal use. If you have any queries or find any problems, please contact me.

This page has been translated into Serbo-Croatian by Jovana Milutinovich from WebHostingGeeks. Also Hungarian translation provided by Zsolt Boros, Indonesian by ChameleonJohn, Irish by Ava Flynn @ Travel Ticker, and German by Lukas Schmidt @ Gameperiod.com.


File albumList.xml (specifies to apply albumList.xsl to iTunes Music Library.xml):

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="albumList.xsl" type="text/xsl"?>
<wrapper>
  <incl file="iTunes Music Library.xml"/>
</wrapper>

File albumList.xsl basic version (lists name & artist of albums on iPod):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <!-- (c) Chris Veness 2005 -->
  <xsl:output method="html" encoding="UTF-8" indent="yes"/>

  <!-- match the wrapper and apply templates to the <incl> xml file -->
  <xsl:template match="/wrapper">
    <xsl:apply-templates select="document(incl/@file)/plist/dict/dict"/>
  </xsl:template>


  <xsl:key name="songsByGenre" match="dict" use="string[preceding-sibling::key[1]='Genre']"/>
  <xsl:key name="songsByAlbum" match="dict" use="string[preceding-sibling::key[1]='Album']"/>


  <xsl:template match="dict">
    <html>
      <head>
        <title>iPod Album Listing</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
      </head>
      <body>
        <table>

          <xsl:for-each select="dict[generate-id(.)=generate-id(key('songsByGenre',string)[1])]">
            <xsl:sort select="string[preceding-sibling::key[1]='Genre']"/>
            <xsl:for-each select="key('songsByGenre',string)[1]">
              <xsl:call-template name="albumsInGenre">
                <xsl:with-param name="genre" select="string[preceding-sibling::key[1]='Genre']"/>
              </xsl:call-template>
            </xsl:for-each>
          </xsl:for-each>

        </table>
      </body>
    </html>
  </xsl:template>


  <xsl:template name="albumsInGenre">
    <xsl:param name="genre"/>

    <!-- genre header -->
    <tr><td colspan='3'><b><xsl:value-of select="$genre"/></b></td></tr>

    <!-- output each album in genre -->
    <xsl:variable name="song" select="/plist/dict/dict/dict"/>
    <xsl:for-each select="$song[generate-id(.)=
        generate-id(key('songsByAlbum',string[preceding-sibling::key[1]='Album'])[1])]">
      <xsl:sort select="string[preceding-sibling::key[1]='Album']"/>
      <xsl:for-each select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])
          [string[preceding-sibling::key[1]='Genre']=$genre]
          [not(true[preceding-sibling::key[1]='Disabled'])][1]">
        <tr valign='top'>
          <td width='20'> </td>
          <!-- the album name: -->
          <td><xsl:value-of select="string[preceding-sibling::key[1]='Album']"/></td>
          <!-- the artist: -->
          <td>
            <xsl:choose>
              <xsl:when test="true[preceding-sibling::key[1]='Compilation']">
                <i>Compilation</i>
              </xsl:when>
              <xsl:otherwise>
                <xsl:value-of select="string[preceding-sibling::key[1]='Artist']"/>
              </xsl:otherwise>
            </xsl:choose>
          </td>
        </tr>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

File albumList.xsl full version (includes complete iTunes library with times & totals):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <!-- (c) Chris Veness 2005-2006 -->
  <xsl:output method="html" encoding="UTF-8" indent="yes"/>


  <!-- match the wrapper and apply templates to the <incl> xml file -->
  <xsl:template match="/wrapper">
    <xsl:apply-templates select="document(incl/@file)/plist/dict/dict"/>
  </xsl:template>


  <xsl:key name="songsByGenre" match="dict" use="string[preceding-sibling::key[1]='Genre']"/>
  <xsl:key name="songsByAlbum" match="dict" use="string[preceding-sibling::key[1]='Album']"/>


  <xsl:template match="dict">
    <html>
      <head>
        <title>iTunes Album Listing</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <style type='text/css'> td { vertical-align: top; padding-right: 1em; } </style>
      </head>
      <body>
        <table>

          <xsl:for-each select="dict[generate-id(.)=generate-id(key('songsByGenre',string)[1])]">
            <xsl:sort select="string[preceding-sibling::key[1]='Genre']"/>
            <xsl:for-each select="key('songsByGenre',string)[1]">
              <xsl:call-template name="albumsInGenre">
                <xsl:with-param name="genre" select="string[preceding-sibling::key[1]='Genre']"/>
              </xsl:call-template>
            </xsl:for-each>
          </xsl:for-each>

          <!-- totals -->
          <tr>
            <td colspan='4' style='color: gray'><b>Total</b></td>
            <td style='color: gray' align='right'><xsl:call-template name="iPodTimeTotal"/></td>
            <td style='color: gray' align='right'><xsl:call-template name="iTunesTimeTotal"/></td>
          </tr>
          <tr>
            <td colspan='4'> </td>
            <td style='color: gray' align='right'><xsl:call-template name="iPodSizeTotal"/></td>
            <td style='color: gray' align='right'><xsl:call-template name="iTunesSizeTotal"/></td>
          </tr>

        </table>
      </body>
    </html>
  </xsl:template>


  <xsl:template name="albumsInGenre">
    <xsl:param name="genre"/>

    <tr>  <!-- genre header -->
      <td colspan='4'><b><xsl:value-of select="$genre"/></b></td>
      <td align='right' style='color: gray'><i>iPod</i></td>
      <td align='right' style='color: gray'><i>iTunes</i></td>
    </tr>

    <xsl:variable name="song" select="/plist/dict/dict/dict"/>
    <xsl:for-each select="$song[generate-id(.)=
        generate-id(key('songsByAlbum',string[preceding-sibling::key[1]='Album'])[1])]">
      <xsl:sort select="string[preceding-sibling::key[1]='Album']"/>
      <xsl:for-each select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])
          [string[preceding-sibling::key[1]='Genre']=$genre]
          [1]">
          <!--  for albums on iPod only, add
                [not(true[preceding-sibling::key[1]='Disabled'])] -->
        <tr>
          <td> </td>
          <td><xsl:call-template name="album"/></td>
          <td><xsl:call-template name="artist"/></td>
          <td align='right'><xsl:call-template name="iPodTimeAlbum"/></td>
          <td align='right'><xsl:call-template name="iTunesTimeAlbum"/></td>
        </tr>
      </xsl:for-each>
    </xsl:for-each>
    <tr><td colspan='6'>&#160;</td></tr>  <!-- space between genres -->
  </xsl:template>


  <xsl:template name="album">
    <xsl:value-of select="string[preceding-sibling::key[1]='Album']"/>
  </xsl:template>


  <xsl:template name="artist">
    <xsl:choose>
      <xsl:when test="true[preceding-sibling::key[1]='Compilation']">
        <i>Compilation</i>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="string[preceding-sibling::key[1]='Artist']"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>


  <xsl:template name="iPodTimeAlbum">
    <xsl:variable name="tracksInAlbum"
        select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])"/>
    <xsl:variable name="t"
        select="sum($tracksInAlbum/integer[preceding-sibling::key[1]='Total Time']
            [not(../true[preceding-sibling::key[1]='Disabled'])])"/>
    <xsl:call-template name="formatTime">
      <xsl:with-param name="t" select="$t"/>
    </xsl:call-template>
  </xsl:template>


  <xsl:template name="iTunesTimeAlbum">
    <xsl:variable name="tracksInAlbum"
        select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])"/>
    <xsl:variable name="t"
        select="sum($tracksInAlbum/integer[preceding-sibling::key[1]='Total Time'])"/>
    <xsl:call-template name="formatTime">
      <xsl:with-param name="t" select="$t"/>
    </xsl:call-template>
  </xsl:template>


  <xsl:template name="iPodTimeTotal">
    <xsl:variable name="t" select="sum(dict/integer[preceding-sibling::key[1]='Total Time']
        [not(../true[preceding-sibling::key[1]='Disabled'])])"/>
    <xsl:call-template name="formatTime">
      <xsl:with-param name="t" select="$t"/>
    </xsl:call-template>
  </xsl:template>


  <xsl:template name="iTunesTimeTotal">
    <xsl:variable name="t" select="sum(dict/integer[preceding-sibling::key[1]='Total Time'])"/>
    <xsl:call-template name="formatTime">
      <xsl:with-param name="t" select="$t"/>
    </xsl:call-template>
  </xsl:template>


  <xsl:template name="iPodSizeTotal">
    <xsl:variable name="s" select="sum(dict/integer[preceding-sibling::key[1]='Size']
        [not(../true[preceding-sibling::key[1]='Disabled'])])"/>
    <xsl:value-of select="floor($s div (1000000)) div 1000"/>GB
  </xsl:template>


  <xsl:template name="iTunesSizeTotal">
    <xsl:variable name="s" select="sum(dict/integer[preceding-sibling::key[1]='Size'])"/>
    <xsl:value-of select="floor($s div (1000000)) div 1000"/>GB
  </xsl:template>


  <xsl:template name="formatTime">
    <xsl:param name="t"/>
    <xsl:if test="$t != 0">
      <xsl:variable name="h" select="floor(($t div (1000*60*60)))"/>
      <xsl:variable name="m" select="floor(($t div (1000*60)) mod 60)"/>
      <xsl:variable name="s" select="floor(($t div 1000) mod 60)"/>
      <xsl:if test="$h != 0"><xsl:value-of select="$h"/>:</xsl:if>
      <xsl:value-of select="format-number($m,'00')"/>:<xsl:value-of select="format-number($s,'00')"/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>