Stories
Slash Boxes
Comments
NOTE: use Perl; is on undef hiatus. You can read content, but you can't post it. More info will be forthcoming forthcomingly.

All the Perl that's Practical to Extract and Report

use Perl Log In

Log In

[ Create a new account ]

ziggy (25)

ziggy
  (email not shown publicly)
AOL IM: ziggyatpanix (Add Buddy, Send Message)

Journal of ziggy (25)

Sunday January 05, 2003
06:34 PM

iTunes and ID3

[ #9780 ]
After reading brian's experiences with iTunes this morning, I decided to scratch an itch I've had for quite some time now. I've long since gotten rid of my MP3s that I ripped on my (now defunct) FreeBSD box. Since July, I've ripped (and re-ripped) CDs through iTunes because it's just so quick. iTunes does a very nice job of organizing a music directory, and collects a lot of data from CDDB.

Unfortunately, none of this data ever winds up in ID3 tags, so copying MP3s from my Mac to my FreeBSD laptop loses information. The trick is to mine the "iTunes Music Library.xml" file for data and populate the ID3 tags using pudge's MP3::Info.

The first step is to glean out the relevant fields from the XML file. I could sit down and write an XML parser handler for the iTunes file. Or I could be lazy and use brian's Mac::PropertyList, since this XML file is simply a property list. But I didn't do either. Instead, I used my favorite XML gleaning tool: XSLT. I started by looking at the specific XML data in the iTunes file (the Tracks dictionary) and converted the information I was looking for into text. Building this stylesheet was an iterative process, and here is the final result:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">

<xsl:output method="text"/>

<xsl:template match="text()"/>

<xsl:template match="plist/dict/dict[preceding-sibling::key[text() = 'Tracks']]">
<xsl:apply-templates select="dict" mode="display"/>
</xsl:template>

<xsl:template match="dict" mode="display">
<xsl:apply-templates select="key[. = 'Location']" mode="display"/>
<xsl:apply-templates select="key[. = 'Name']" mode="display"/>
<xsl:apply-templates select="key[. = 'Artist']" mode="display"/>
<xsl:apply-templates select="key[. = 'Album']" mode="display"/>
<xsl:apply-templates select="key[. = 'Year']" mode="display"/>
<xsl:apply-templates select="key[. = 'Genre']" mode="display"/>
<xsl:apply-templates select="key[. = 'Track Number']" mode="display"/>
<xsl:text>&#xa;</xsl:text>
</xsl:template>

<xsl:template match="key" mode="display">
<xsl:value-of select="concat(text(), ': ', following-sibling::*/text(), '&#xa;')"/>
</xsl:template>

</xsl:stylesheet>

This produces output like this:

Location: file://localhost/Users/ziggy/Music/iTunes/iTunes%20Music/Louis%20Armstrong/Great est%20Hits/01%20Sugar.mp3
Name: Sugar
Artist: Louis Armstrong
Album: Greatest Hits
Genre: Jazz
Track Number: 1

I can then easily parse this output and use set_mp3tag from MP3::Info to add the ID3 tags these files were missing.

Here is the full script for anyone who is interested. Note that the XSLT stylesheet was included in the __DATA__ segment to reduce an external dependency.

#!/usr/bin/perl -w

use strict;

use MP3::Info;

my @id3_fields = qw(Location Name Artist Album Year Comment Genre);
push(@id3_fields, "Track Number");

use_winamp_genres();

sub read_metadata {
    my $filename = shift;
    use XML::LibXSLT;
    use XML::LibXML;

    $/ = undef;

    my $parser = XML::LibXML->new();
    my $xslt = XML::LibXSLT->new();

    my $source = $parser->parse_file($filename);
    my $style_doc = $parser->parse_string(<DATA>);
    my $stylesheet = $xslt->parse_stylesheet($style_doc);

    my $results = $stylesheet->transform($source);

    return $stylesheet->output_string($results);
}

## Update ID3 tags

print STDERR "Processing 'iTunes Music Library.xml'...";
my $metadata = read_metadata("$ENV{HOME}/Music/iTunes/iTunes Music Library.xml");
print STDERR "done\n";

my @blocks = split("\n\n", $metadata);

foreach my $block (@blocks) {
    my (%info) = map {m/^(\w+): (.*)$/} split("\n", $block);

    $info{Location} =~ s{^file://localhost}{};
    $info{Location} =~ s{%20}{ }g;

    print STDERR "$info{Artist}: $info{Album}, $info{Name}\n";

    $info{Genre} = 'Dance' if $info{Genre} eq "Electronica/Dance";
    $info{Genre} = 'Other' if $info{Genre} eq "World";
    $info{Genre} = 'Alternative' if $info{Genre} eq "Alternative & Punk";

    set_mp3tag(@info{@id3_fields});
}

__DATA__
## stylesheet, as above

The Fine Print: The following comments are owned by whoever posted them. We are not responsible for them in any way.
 Full
 Abbreviated
 Hidden
More | Login | Reply
Loading... please wait.
  • This only sets the ID3v1 tag. MP3::Info does not write ID3v2 yet. I hope to change that at some point in the next few months. In case you (or a reader) doesn't know, ID3v1's main disadvantage is that each field (artist, album, title) is limited to 30 characters. MP3::Info can read ID3v2.2.0 through ID3v2.4.0 (in theory; I have some bug reports that need looking into), but cannot yet write.

    So if you writing ID3v1 is fine with you, you will either want to remove the ID3v2 tag (MP3::Info can do that for y
    • I love the id3convert utility that comes with the id3lib package [sourceforge.net]. It solved my tagging hell (which was different from yours: ripped in RealAudio, tags buggered in iTunes).

      --Nat

  • Can anyone elaborate on what iTunes is not updating in the file? I checked a few of the files I updated through iTunes and everything seemed to update.
  • I've done simplistic parsing of the .xml file in pure Perl, to generate a list of albums here [disobey.com]. It works well enough for my needs, and I generate and curl the list every morning...