Saturday, March 28, 2009

XSLT to generate an HTML listing of your iTunes library

The data for the music in an iTunes library is stored in an XML file which means that it should be simple to produce an XSLT file to generate an HTML document listing all the albums in your iTunes library. Well, it would be, but the XML format used by iTunes is, how can I put this, idiosyncratic. Other people may use more fragrant language to describe it…

Anyway I found this very useful article describing how to create an XSLT file to do almost what I wanted, but I wasn’t interested in grouping by genre, since the genre data is often not very accurate or helpful. I’m also a little anal about my music collection and wanted it sorted by artist name, rather than album name. This part was slightly tricky because the data may not be complete. The album artist field is often not entered and the artist field may not be the album artist, even when the album is not a compilation. And finally the compilation flag may be set when you don’t expect it to be or conversely not set when you expect it to be. e.g. a greatest hits album may be flagged as a compilation. This is arguably correct, but I wanted these kind of albums to be grouped with the artist. So quite a few changes were required which is why I’m posting this, it’s not just me trying to get some reflected glory (and hence me linking to the original article repeatedly)

Anyway, the basic idea is the same as the original article. First put an XML file in the same directory as your iTunes library, as follows

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

Next is the XSLT, in a file called albumList.xsl in the same directory, which looks like this.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <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="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>
          body
          {
            font-family:Arial;
          }
        </style>
      </head>
      <body>
        <table>
          <thead>
            <tr>
              <td><b>Artist</b></td>
              <td><b>Album</b></td>
            </tr>
          </thead>
          <xsl:variable name="song" select="/plist/dict/dict/dict"/>

          <!-- output the albums that aren't compilations -->
          <xsl:for-each select="$song[generate-id(.)=
        generate-id(key('songsByAlbum',string[preceding-sibling::key[1]='Album'])[1])]">
            <xsl:sort select="concat(string[preceding-sibling::key[1]='Album Artist'], string[preceding-sibling::key[1]='Artist'])"/>

            <xsl:for-each select="key('songsByAlbum',string[preceding-sibling::key[1]='Album'])
          [not(true[preceding-sibling::key[1]='Disabled'])]
          [not(true[preceding-sibling::key[1]='Compilation'])]            
          [1]">
              <xsl:call-template name="outputAlbum" />
            </xsl:for-each>
          </xsl:for-each>

          <!-- output each compilation -->
          <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'])
          [not(true[preceding-sibling::key[1]='Disabled'])]
          [true[preceding-sibling::key[1]='Compilation']]
          [not(string[preceding-sibling::key[1]='Album Artist'])]
          [1]">
              <xsl:call-template name="outputAlbum" />
            </xsl:for-each>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>

  <xsl:template name="outputAlbum">
    <tr valign='top'>
      <!-- the artist: -->
      <td>
        <xsl:choose>
          <xsl:when test="string[preceding-sibling::key[1]='Album Artist']">
            <xsl:value-of select="string[preceding-sibling::key[1]='Album Artist']"/>
          </xsl:when>
          <xsl:otherwise>
            <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:otherwise>
        </xsl:choose>
      </td>
      <!-- the album name: -->
      <td>
        <xsl:value-of select="string[preceding-sibling::key[1]='Album']"/>
      </td>
    </tr>
  </xsl:template>
  
</xsl:stylesheet>

Now open the XML file in your favoured browser (as long as your favoured browser is IE or FireFox) and the HTML should be generated. It can be slow, it might be the XSLT can be optimised somewhat but I’m not hugely experienced in optimising XSLT so haven’t investigated further. And of course the XSLT can’t cope with all rubbish data in your iTunes library, garbage in, garbage out.

One other improvement would be to improve the sorting so that artists such as The Fall came under F, rather than T, but I have no idea how to achieve that.

2 comments:

Anonymous said...

cheers for this, however there is a slight problem, if 2 albums have the same name it will only display one of them, any idea how to fix this?

Doogal Bell said...

Ah yes, didn't notice that. I'll take a look