#!/usr/bin/perl -w use strict; $|++; use Getopt::Long; my %options; my $library; ############################################################################### # creates a list of your mp3 albums, based on itunes XML. I assume things # # about how you id3 your collection, as well as how you want the output. # # the actual expectations are explained in the output, available online: # # http://www.disobey.com/detergent/lists/albums.html - fun for everyone! # # # # v2.0: 2003-09-01, morbus@disobey.com, email me if you use/modify. # ############################################################################### # to run this script, use the terminal to enter the following command # # (your library is usually at "~Music/iTunes/iTunes Music Library.xml"): # # # # perl itunes2html.txt "/path/to/iTunes Music Library.xml" # # # # if you don't want to specify your library path each and everytime, you can # # hardcode it into the script by looking for the $library variable a little # # further down. more help is available with: perl itunes2html.txt --help # ############################################################################### # changes (2003-09-01, version 2.0): # # - you can now hardcode your library path in $library. # # - much stronger and more readable stylesheets/display. # # - fixed case-insensitive sorting bug. i'm a retard. # # - HTML comments for proper sorting no longer needed. # # - you can show non-alphanumeric albums is you want. # # - final file size shrunken even further. # # - we now show lowest bitrate of an album. # # # # changes (2003-02-03, version 1.0): # # - stylesheet built in, not off of disobey.com. # # - spelling and wording corrections. # # - help dialog and command line options. # # - can process songs that are missing track or disc numbers (at # # the expense of losing some display features. fix 'em, dammit!). # # - sorting comments are removed from output, making page size smaller. # # - HTTP::Date is now optional (error is spit, but progress continues). # # # # biggest bug (that I probably won't fix): # # - since we sort by artists, we falsely assume that only one artist # # can produce an album. thus, an album that has multiple artists that # # has NOT been designated as "Various Artists" will severely alter # # the listings (one entry for each artist per that album), as well as # # the total album count. supposedly, "part of a compilation" should # # be used to work around this, but that would require a restructuring # # of our in-memory data structure, and in other words, force a rewrite # # unless we think of a cute way around it (an array of compilations?) # ############################################################################### # if you want to hardcode your library path, # specify it here. command line overrides this. $library = "/Users/morbus/Music/iTunes/iTunes Music Library.xml"; # no modification/reading below this line is necessary. # dedicated to all those shareware people that charge # money for relatively mindless tasks such as this. ############################################################################### # our options matrix. see the comments here, or a -h on the command line. # ############################################################################### GetOptions(\%options, 'help|h|?', # print out our help dialog. 'listallalbums', # even non-alphanumeric ones. 'missingdiscnumbers', # ignore missing disc numbers. 'missingtracknumbers', # ignore missing track numbers. ); ############################################################################### # spit out our help if necessary (either, it's been requested via the command # # line, or no one filled in the path to the itunes library xml file. # ############################################################################### if ($options{help} or (!$library and !$ARGV[0])) { print <<"END_OF_HELP"; itunes2html - converts your music library into an html page. Usage: perl itunes2html.txt [OPTION] [FILE]... (typically "~/Music/Itunes/iTunes Music Library.xml") -h, -?, --help Display this message and exit. --listallalbums Normally, if an album title does NOT start with letters or numbers, we would remove it from display. If you'd like them to be listed anyways, use this. --missingdiscnumbers If your tracks aren't labeled with the "disc # of #" id3 tag, then they're normally ignored by itunes2html. If you'd like itunes2html to accept tracks with this missing information, use this flag. Turning on this option will ignore "disc # of #" for ALL YOUR TRACKS. --missingtracknumbers Tracks that don't have "track # of #" id3 tags are normally ignored for not having "proper" id3 info. If you'd like itunes2html to accept tracks without track numbers, add this flag to your command line. Turning on this option will ignore "track # of #" for ALL YOUR TRACKS. If you'd like certain albums or tracks not to be listed in the export, add the pipe character (|) to the beginning of the track's album name. Mail bug reports and suggestions to . END_OF_HELP exit ;} ############################################################################### # check to see if the user has the non-default HTTP:: Date installed. # # if not, give an error about it and continue with no date checking. # ############################################################################### eval("use HTTP::Date;"); my $check_dates = 1; if ($@) { print STDERR "ERROR: HTTP::Date is not installed - ". "skipping \"last 30 days\" feature.\n"; $check_dates = 0; } # get the path of our XML file and open the bad boy. my $file = $ARGV[0] || $library; die "$file does not exist.\n" unless -e $file; open (XML, "<$file") or die "$file could not be opened: $!."; ############################################################################### # process each line of our XML file. # ############################################################################### my ($albums, $total_albums, $total_tracks); $/ = ""; while () { next unless /Artist/i; # skips starting instances. s/[\t\r\n\f]//g; # remove all tabs, newlines, and so forth. # used in our data structure. my ($artist) = $_ =~ m!Artist(.*?)!; my ($album) = $_ =~ m!Album(.*?)!; my ($track_number) = $_ =~ m!Track Number(.*?)!; my ($disc_number) = $_ =~ m!Disc Number(.*?)!; # skip !alphanumeric albums. next if ($album =~ /^\|/ && !$options{listallalbums}); # spit an error if some of this stuff is missing. unless ($artist and $album and $track_number and $disc_number) { # there's probably a simpler way of doing this. my @missing; # a list of missing fields per track. push(@missing, "artist") unless defined($artist); push(@missing, "album") unless defined($album); push(@missing, "track_number") unless defined($track_number); push(@missing, "disc_number") unless defined($disc_number); # print out the error message to STDERR. boring code here. my ($file) = $_ =~ m!Location(.*?)!; $file =~ s!(file://|localhost|Volumes)!!gi; # garbage for removal. $file =~ s/%20/ /g; # quickie URL encoding to happier reading. print STDERR "Missing ", join(", ", @missing), " for $file.\n"; next; # well, that was certainly boring. who rules?! not me. } # check our command line options. if either have been set, # then we use dummy track and disc numbers for this track. # this is regardless if some of the tracks have proper # information (hey... fix 'em or get crap, buddy). we # do this after we spit out an error to STDERR (above). if ($options{missingdiscnumbers}) { $disc_number = 1; } if ($options{missingtracknumbers}) { $track_number = 1; } # and continue on with some extra information. my ($disc_count) = $_ =~ m!Disc Count(.*?)!; $albums->{$artist}{$album}{"Disc Count"} = $disc_count; # and now the rest of the fields in one fell swoop. $albums->{$artist}{$album}{$disc_number}{$track_number}{$1} = $3 while (m!(.*?)<(integer|string|date)>(.*?)!g); $total_tracks++; } close(XML); ############################################################################### # create aggregate information for the album (totals, globals, etc.) # ############################################################################### foreach my $artist ( keys %{$albums} ) { foreach my $album ( keys %{$albums->{$artist}} ) { # make impossible bit rate to start. we use # this to determine the smallest bitrate for # an entire album (which is then displayed). $albums->{$artist}{$album}{"Bit Rate"} = 999; # get track counts. my $album_total_tracks; # all tracks, regardless of disc. for (my $i = 1; $i <= $albums->{$artist}{$album}{"Disc Count"}; $i++) { foreach my $track ( sort keys %{$albums->{$artist}{$album}{$i}} ) { $album_total_tracks++; # increment the track counter. # has this track been played before? if so, add to the count. if ($albums->{$artist}{$album}{$i}{$track}{"Play Count"}) { my $play_count = $albums->{$artist}{$album}{$i}{$track}{"Play Count"}; $albums->{$artist}{$album}{"Play Count"} += $play_count; } # other global values. we set them here to make our outputting code smaller. # we really should set these only if they're not set already. less work. $albums->{$artist}{$album}{Comments} = $albums->{$artist}{$album}{$i}{$track}{Comments} || undef; $albums->{$artist}{$album}{Genre} = $albums->{$artist}{$album}{$i}{$track}{Genre} || "(blank)"; $albums->{$artist}{$album}{Year} = $albums->{$artist}{$album}{$i}{$track}{Year} || "????"; $albums->{$artist}{$album}{"Date Added"} = $albums->{$artist}{$album}{$i}{$track}{"Date Added"}; $albums->{$artist}{$album}{"Play Count"} |= 0; # if it's not defined, zero it out. # we show the lowest bitrate in our output, in hopes # this will spur people to find better quality mp3s. # some tracks iTunes can't figure out, so skip. next unless $albums->{$artist}{$album}{$i}{$track}{"Bit Rate"}; if ($albums->{$artist}{$album}{"Bit Rate"} > $albums->{$artist}{$album}{$i}{$track}{"Bit Rate"}) { $albums->{$artist}{$album}{"Bit Rate"} = $albums->{$artist}{$album}{$i}{$track}{"Bit Rate"}; } } } # finalize our incrementers and totals. $albums->{$artist}{$album}{"Track Count"} = $album_total_tracks; $total_albums++; } } ############################################################################### # now, pretty print everything out. i want one script, so no templating. # ############################################################################### my $updated = localtime(time); print < Albums in MP3 Format ($updated)

Albums in MP3 Format

Got a list of your own or have questions? Send email to <morbus\@disobey.com>. The below contains listings for $total_albums albums, comprising $total_tracks total tracks and was last generated $updated. It was created by a Perl script from Morbus Iff that reads the exported XML provided by Apple's iTunes. This automation is only possible because I'm insanely ana... pedantic about the quality of my id3 tags. Some assumptions:

  • All tracks have Title, Artist, Album, Year, and Track # of #.
  • All tracks have a "more info" URL in their Comments.
  • All albums (and tracks) have Disc # of # information.
  • Displayed "Bit Rate" is the lowest for the entire album.
  • These are full albums only - no singles.
  • The script caters to large collections.
EVIL_HEREDOC_HEADER_OF_ORMS_BY_GORE # if HTTP::Date is installed, spit out our color information. if ($check_dates) { print "

Albums with this background color have been added in the past 30 days.

\n\n"; } # now, go through each artist and album. we actually add all this stuff # to genre specific arrays first, since we'll display them in those categories. my (%genres, $current_genre); # we fixed our damn case sensitive sorting. wow! foreach my $artist (sort { lc $a cmp lc $b } keys %{$albums}) { my %pushed_header; # did we push this artist header for this genre? my $header = "AlbumYear" . (defined $options{missingdiscnumbers} ? "" : "Discs") . (defined $options{missingtracknumbers} ? "" : "Trk Count") . (defined($options{missingtracknumbers}) ? "" : "Trk Played") . "Bit Rate\n"; # create the header early for pushing to genre. # now go through each album for this artist. foreach my $album (sort { lc $a cmp lc $b } keys %{$albums->{$artist}}) { $current_genre = $albums->{$artist}{$album}{Genre}; # categories. $genres{$current_genre} = [] unless defined $genres{$current_genre}; if (!$pushed_header{$current_genre}) { # create the html header push @{$genres{$current_genre}}, $header; # for this artist, if we $pushed_header{$current_genre}++; # haven't already done so. } my $name = "$artist - $album"; # this used to be more uber-complicated. my $year = $albums->{$artist}{$album}{Year}; # filler comment! filler!! my $play_count = $albums->{$artist}{$album}{"Play Count"}; # shorter. my $disc_count = $albums->{$artist}{$album}{"Disc Count"}; # shorter. my $track_count = $albums->{$artist}{$album}{"Track Count"}; # shorter. my $bit_rate = $albums->{$artist}{$album}{"Bit Rate"} . " kbps"; if ($bit_rate eq "999 kbps") { $bit_rate = "????"; } # get our comment string and make it a URL. if it's an Amazon # URL, make it an affiliate clickthrough. we, of course, only do # this if there's a Comments string to be had. bad id3er! my $link; if (defined $albums->{$artist}{$album}{Comments}) { $albums->{$artist}{$album}{Comments} =~ s!(http://[^\s<]+)!$1!i; if ($albums->{$artist}{$album}{Comments} =~ /amazon.com/) { $albums->{$artist}{$album}{Comments} .= "disobeycom"; } $link = $albums->{$artist}{$album}{Comments}; # shorter. undef $link unless $link =~ /^http/; # if link isn't a url. } # this should really be in id3's URL, but iTunes doesn't support it. # create a linked name if a URL was found. if (defined $link) { $name = "$name"; } # when was this album added? if it's within the # past 30 days, add a class="new" to our tag. my $class = ""; # turns to "new" if, indeed, it's new. if ($check_dates) { # only do this if HTTP::Date is installed. my $current_seconds = time; my $added_seconds = str2time($albums->{$artist}{$album}{"Date Added"}); if ( ($current_seconds - $added_seconds) < 2592000) { $class = " class=new"; } } # who wishes to rub the back of Morbus Iff?!! # now push to our genre array for later printing. push @{$genres{$current_genre}}, "$name$year" . (defined($options{missingdiscnumbers}) ? "" : "$disc_count") . (defined($options{missingtracknumbers}) ? "" : "$track_count") . (defined($options{missingtracknumbers}) ? "" : "$play_count") . "$bit_rate\n"; # i am master of 'leet whitespace! } } # now, print out each genre. foreach my $genre (sort keys %genres) { # create a giant string for display. my $output = join ("", @{$genres{$genre}}); # print this crazy chicken. print "

$genre:

\n\n"; # bacCoOOCkckck! bacooOCocock! print "
$output
\n"; } print "\n";