#!/usr/bin/perl =head1 NAME itunes-to-banshee.pl - convert iTunes playlists and such to Banshee =head1 SYNOPSIS ./itunes-to-banshee.pl Library.xml banshee.db =head1 DESCRIPTION Reads the iTunes library file you give it (exported from iTunes via C<< File > Library > Export Library... >>) and writes that information into the Banshee database (found at F<~/.config/banshee-1/banshee.db>). This assumes you have already copied your music files over and informed Banshee where to find them. This will copy all standard playlists and playlist folders from iTunes into Banshee as regular playlists. All smart playlists are ignored. This will also copy over yoru song ratings, play counts, and last played date for all songs. The songs are matched based upon the song title and file size. If you have duplicates, only one of them will be changed. It will warn you if songs are missing. =head1 AUTHOR Andrew Sterling Hanenkamp C<< hanenkamp@cpan.org >> =head1 COPYRIGHT & LICENSE Copyright 2009 Andrew Sterling Hanenkamp. This is free software. It may be modified and distributed under the terms of the Artistic License 2.0. =cut use strict; use warnings; use Data::Dumper; use DateTime; use DateTime::Format::ISO8601; use DBI; use MIME::Base64; use XML::Twig; my $df = DateTime::Format::ISO8601->new; my %library; my @current_dict; my @current_key; my $dbh; my %tracks; my $DEBUG = 0; $| = $DEBUG; sub debug { print @_ if $DEBUG }; sub set_value { my ($value) = @_; if (ref $current_dict[0] eq 'HASH') { $current_dict[0]{ $current_key[0] } = $value; } elsif (ref $current_dict[0] eq 'ARRAY') { push @{$current_dict[0]}, $value; } unshift @current_dict, $value if ref $value eq 'HASH' or ref $value eq 'ARRAY'; } sub handle_key { my ($t, $el) = @_; debug($el->text,"=>"); unshift @current_key, $el->text; } sub handle_scalar { my ($t, $el) = @_; debug("'",$el->text,"'"); set_value($el->text); shift_key(); } sub handle_hash { my ($t, $el) = @_; debug("{"); set_value({}); } sub handle_array { my ($t, $el) = @_; debug("["); set_value([]); } sub handle_bool { my ($t, $el) = @_; debug($el->name); set_value($el->name eq 'true'); shift_key(); } sub handle_date { my ($t, $el) = @_; debug($el->text); set_value($df->parse_datetime($el->text)); shift_key(); } sub handle_base64 { my ($t, $el) = @_; my $decoded = decode_base64($el->text); debug(">>'END_OF_DATA'\n",$decoded,"END_OF_DATA\n"); set_value($decoded); shift_key(); } sub shift_key { debug(','); shift @current_key if ref $current_dict[0] eq 'HASH'; } sub shift_key_and_dict { my $t = shift; my $old_key = shift_key(); my $old_value = shift @current_dict; debug(ref $old_value eq 'HASH' ? '}' : ']'); if (ref $old_value eq 'HASH') { if (defined $old_value->{'Track ID'} and scalar keys %$old_value > 1) { handle_track($old_key => $old_value); } elsif (defined $old_value->{'Playlist ID'}) { handle_playlist($old_key => $old_value); } elsif (not defined $old_key) { return; } elsif ($old_key eq 'Playlist Items' or $old_key eq 'Playlists') { return; } warn 'UNEXPECTED DEFINITION (1): ', Dumper(\@current_dict, $old_key) unless ref $current_dict[0] eq 'HASH' or ref $current_dict[0] eq 'ARRAY'; if (ref $current_dict[0] eq 'HASH') { delete $current_dict[0]{$old_key}; } elsif (ref $current_dict[0] eq 'ARRAY') { delete $current_dict[0][0]; } } elsif (not $old_key) { return; } else { warn 'UNEXPECTED DEFINITION (2): ',Dumper($old_key, $old_value); } } sub handle_track { my ($key, $value) = @_; warn 'WEIRD TRACK: ', Dumper($key, $value) unless defined $value->{'Location'}; # Skip Internet radio stations and the like return unless $value->{'Track Type'} eq 'File'; my ($track_id) = $dbh->selectrow_array(q{ SELECT TrackID FROM CoreTracks WHERE Title = ? AND FileSize = ? }, undef, $value->{'Name'}, $value->{'Size'}); unless ($track_id) { warn "NO TRACK FOR $value->{Name} ($value->{Size})\n"; return; } # Map iTune's notion of TrackID to Banshee's $tracks{ $value->{'Track ID'} } = $track_id; my $last_play = defined $value->{'Play Date UTC'} ? $value->{'Play Date UTC'}->epoch : undef; $dbh->do(q{ UPDATE CoreTracks SET Rating = ?, PlayCount = ?, LastPlayedStamp = ? WHERE TrackID = ? }, undef, ($value->{'Rating'}||0)/20, $value->{'Play Count'}, $last_play, $track_id, ); } my ($playlist_id, $entry_id); sub handle_playlist { my ($key, $value) = @_; # Don't import smart playlists, no point in trying to understand iTunes # gobbledygook here return if $value->{'Smart Info'}; return unless defined $value->{'Playlist Items'}; # warn "PLAYLIST: $value->{Name}\n"; # warn " - $tracks{$_->{'Track ID'}}\n" for @{ $value->{'Playlist Items'} }; # warn "\n"; unless (defined $playlist_id) { ($playlist_id) = $dbh->selectrow_array(q{ SELECT MAX(PlaylistID) FROM CorePlaylists }); } $dbh->do(q{ INSERT INTO CorePlaylists(PrimarySourceID, PlaylistID, Name) VALUES (?, ?, ?) }, undef, ++$playlist_id, $value->{'Name'}, ); for my $itunes_track (@{ $value->{'Playlist Items'} }) { my $track_id = $tracks{ $itunes_track->{'Track ID'} }; unless ($track_id) { warn "UNKOWN TRACK ID $itunes_track->{'Track ID'} in play list $value->{'Name'}\n"; next; } $dbh->do(q{ INSERT INTO CorePlaylistEntries(EntryID, PlaylistID, TrackID) VALUES (?, ?, ?) }, undef, ++$entry_id, $playlist_id, $track_id, ); } # warn "PLAYLIST: ", Dumper($key, $value); } sub MAIN { my $library_file_name = shift; my $db_file_name = shift; $library_file_name or die "no library file name given\n"; -f $library_file_name or die "cannot find $library_file_name\n"; $db_file_name or die "no database file name given\n"; -f $db_file_name or die "cannot find $db_file_name\n"; $dbh = DBI->connect("dbi:SQLite:$db_file_name"); my $twig = XML::Twig->new( start_tag_handlers => { dict => \&handle_hash, array => \&handle_array, }, twig_handlers => { key => \&handle_key, integer => \&handle_scalar, string => \&handle_scalar, true => \&handle_bool, false => \&handle_bool, date => \&handle_date, real => \&handle_scalar, data => \&handle_base64, dict => \&shift_key_and_dict, array => \&shift_key_and_dict, }, output_encoding => 'UTF-8', ); $twig->parsefile($library_file_name); } MAIN(@ARGV);