#!/usr/bin/perl ## MythTV-iTunes script to match on filename ## taken from the other MythTV-iTunes scripts ## that match on Album and Artist ## AUTHOR ## Brian Phillips - brian (dot) phillips (at) byu (dot) edu use XML::Parser; use URI::Escape; use Encode; use DBI; use Getopt::Long; ## You shouldn't need to change this stuff. $guesses = 20; ## Really, don't edit this stuff. if(@ARGV<1) { $help=true; } GetOptions( 'xml=s' => \$file, 'verbose' => \$verbose, 'notest' => \$notest, 'guesses=i' => \$guesses, 'guess' => \$guess, 'strip=s' => \$strip_prepend_string, 'help|h' => \$help, '<>' => \&invalidoption ); ######################################## ############ CONFIGURE THIS ############ ######################################## ## You shouldn't need to change this, but if you need to, change it. $mythtv_database = "mythconverg"; ## ip address of the backend, or "localhost" for combined FE/BE systems ## ie. where the database is $mythtv_hostname = "192.168.0.101"; $mythtv_user = "mythtv"; $mythtv_password = "mythtv"; ## If you leave this blank, all frontends will have access to ## the playlists generated. If you set it to the hostname of ## a particular frontend, then only that frontend will have ## access to the generated playlists. $mythtv_playlist_host = ""; ## You need to set this to the path where your iTunes generated xml ## file is. It needs to be accessible to this machine. $default_file = "/storage/music/Library.xml"; ## By default, this script will not make changes to your database ## unless you specifically run it with the --notest flag. This ## keeps you from mucking up your playlist database if you don't ## want to. ## ## If you don't like that type of behavior, and are a bit of a ## maverick, you can uncomment the following line, which will cause ## the script to make changes to the database each time it is run. ## ## I recommend getting to know the script a little bit and what it ## can do to your database before uncommenting this line. #$notest=true; if(!$strip_prepend_string) { $nostrip = true; ## This is some data from the XML file. It needs to be removed ## before we can match up songs with mythconverg database. The ## data is different for every computer as it's a string where ## your iTunes library thinks the music is found. If you haven't ## the slightest clue as to what this common path string might ## be, don't change anything, and run the script with the --guess ## flag. This will trigger an internal guesser algorithm that ## will try and find the common path, and then restart this script ## using the common path it found. $strip_prepend_string = ""; ## EXAMPLE ## My iTunes program is on a Windows machine and the music is actually ## stored on my mythbox, which I shared via SAMBA and mapped to my Y: drive. ## ## All the music is stored on my mythbox, but iTunes thinks it's on the ## drive letter Y:. If you open your library.xml file in wordpad and search ## the word "Location", you'll probably see some similar path that starts with ## "file://localhost/common/path/" . # $strip_prepend_string = "file://localhost/Y:/"; } ######################################## ####### END CONFIGURATION ########## ######################################## if($help) { print < Library > Export Library..." and export your library as an xml file for the script to use. The default xml file will be located at /storage/music/Library.xml or it can be set on a case basis by using the --xml="/path/to/Library.xml" option on the command line. You need not export your entire Library. You can export xml files on a single playlist basis. Right click the playlist in iTunes, and select "Export Song List..." Then feed that file to the script and it will only map that specific playlist. This script comes with an algorithm that it can use to guess the base directory structure which can be invoked by using the --guess option. If the script fails to find the common directory structure, you can increase the number of directories it will use to make its guess by setting the --guesses=<#> option. For example, if my music were on a Windows computer with the path C:\\Path\\to\\music\\, and music was a shared folder mounted at /storage/music on the Myth system, the common prefix string would be file://localhost/C:/Path/to/music . Simple enough right? Just use the algorithm. OPTIONS --xml="" - Contains the path local to the machine where the script is running. Can be configured in the configuration section of the script, or invoked on the command line. --verbose - Output every mapped song and every database query that is run. --notest - Make changes to the database. By default this script will not make changes to the database until you explicitly tell it to. Or you can uncomment a line in the config section and change the default behavior. --guesses=<#> - An integer value of directories which the algorithm will then use to guess the common prefix string. If the algorithm is not guessing correctly, increment this #. The algorithm is fast, so don't be afraid to try a large number such as 1000, if you have that many songs. --guesses implies --guess --guess - Invoke the internal guessing algorithm. --strip="" - If you don't like the guess, you can tell the script what path to strip off manually. --help - display this help. WORD OF WARNING This script has problems when it encounters UTF-8 characters in the filenames. If you have UTF-8 characters in the filenames, chances are the files will not map. If someone knows how to do a proper conversion from the UTF-8 data in the iTunes xml file to the Latin1 data in the MythMusic database, I'd be happy to see an updated script which could handle special characters. For now, just take special characters out of the filename and you should be good. UTF8 data in the ID3 tag is unaffected, as this script matches on filename. Also, iTunes (Windows) does not care about case sensitive filenames, but MythMusic does. This script will output your filenames and paths in all lowercase. Don't worry though, this behavior is just for matching purposes and shouldn't affect how your music is played in MythMusic or iTunes. As well, this script will only delete playlists from the MythMusic database for which it finds an equivalent playlist in your iTunes xml file. If you change the playlist name in your iTunes, the old playlist will not be deleted from your MythMusic database by this script. You will need to manually do this in MythMusic. EOF exit 0; } if($file) { $options = " --xml=".$file; } else { print "\nNo xml file specified, using default of $default_file.\nTo change, edit this script, or specify on command line with --xml=\"/path/to/list.xml\".\n\n"; $file = $default_file; } if($verbose) { $options .=" --verbose"; } if($notest) { $options .=" --notest"; } if($guesses) { $options .=" --guesses=".$guesses; $guess=true; } if($guess) { $options .=" --guess"; } $unsuccessful_counter = 0; $successful_counter = 0; sub invalidoption { print "Invalid option: @_\n\nTry $0 --help\n\n"; exit 0; } sub trim { $_ = $_[0]; s/^\s+//o; s/\s+$//o; return $_; } sub start_element { my ($self, $element) = @_; push(@current_xml_elements, $element); } sub end_element { my ($self, $element) = @_; if($element ne pop(@current_xml_elements)) { print "Unexpected name ".($element)." for popped element\n"; } if($element eq 'key') { push(@current_plist_elements, trim($tag_data)); } elsif ($element eq 'integer' || $element eq 'string' || $element eq 'dict' || $element eq 'date' || $element eq 'true' || $element eq 'false' || $element eq 'array' || $element eq 'data') { if($current_xml_elements[$#current_xml_elements] ne "array") { $finished_plist_element = pop(@current_plist_elements); } else { $finished_plist_element = $current_plist_elements[$#current_plist_elements]; } plist_element($finished_plist_element, $element, trim($tag_data)); } $tag_data = ''; } sub characters { my ($self, $data) = @_; $tag_data .= $data; } sub escape { my $str = shift || ''; $str =~ s/([^\w.-])/sprintf("%%%02X",ord($1))/eg; $str; } sub plist_element { my ($element_name, $element_type, $element_data) = @_; if($current_plist_elements[0] eq 'Tracks') { if($element_name eq 'Location') { $location = lc(uri_unescape($element_data)); $location =~ s/$strip_prepend_string//; } elsif(($element_name eq 'Disabled' || $element_name eq 'Has Video' || $element_name eq 'Protected' ) && $element_type eq 'true') { $disabled = 1; } elsif($element_type eq 'dict') { if(!$disabled) { $id=$songs_hash{"$location"}; if( $id ) { $track_map[$element_name] = $id; $successful_counter=$successful_counter+1; if( $verbose ){ print "MAPPED SONG\:\t".$location."\n"; } } else { print "**COULD NOT MAP SONG\:\t".$location."**\n"; $unsuccessful_counter = $unsuccessful_counter+1; push(@paths,$location); } if( ($unsuccessful_counter == $guesses) && (($successful_counter) == 0) ) { if(!$nostrip) { print <do("UNLOCK TABLES"); system("perl $0$options"); exit 0; } print <quote($name); if($notest){ $dbh->do("DELETE FROM music_playlists WHERE playlist_id > 2 AND playlist_name=$playlistname"); } } elsif($element_name eq 'Track ID') { if($track_map[$element_data]) { $tracks .= ",".$track_map[$element_data]; } } elsif($element_name eq 'Playlists' && $element_type eq 'dict') { if($tracks && !($name eq 'Library') && !($name eq 'Music')) { $name =~ s/[^[:ascii:]]+//g; push(@playlist_names,$name); $mythtv_playlist_host_quoted = $dbh->quote($mythtv_playlist_host); @track_num = split(/,/, $tracks); $values .= ",(NULL, ".($dbh->quote($name)).", ".$mythtv_playlist_host_quoted.", ".($dbh->quote(substr($tracks, 1))).", ".(@track_num-1).")"; } $name = $tracks = $playlistname = $undef; } } elsif($element_name eq 'Playlists' && $element_type eq 'array') { if((substr($values, 1)) eq ''){ print "***NO PLAYLISTS TO INSERT***\n"; if ($successful_counter==0) { print < setting on the command line. EOF } exit 0; } if($verbose) { print "INSERT INTO music_playlists (playlist_id, playlist_name, hostname, playlist_songs, songcount) VALUES ".substr($values, 1)."\n"; } if($notest){ $dbh->do("INSERT INTO music_playlists (playlist_id, playlist_name, hostname, playlist_songs, songcount) VALUES ".substr($values, 1)); print "\nInserted these playlists:\n"; foreach (@playlist_names) {print $_."\n";} } else { print "\nFound these playlists:\n"; foreach (@playlist_names) {print $_."\n";} print <do("UNLOCK TABLES"); } } # We'll first make a database access, get all the songids, paths # and filenames from the db. We'll then load them into a hash # where the path and filename are the key, and the songid is the # value. $dbh = DBI->connect("DBI:mysql:database=".$mythtv_database.";host=".$mythtv_hostname, $mythtv_user, $mythtv_password); $dbh->do("LOCK TABLES music_playlists WRITE, music_songs READ, music_directories READ"); $query = "select song_id,filename,path from music_songs left join music_directories on music_songs.directory_id = music_directories.directory_id"; $execute = $dbh->prepare($query); $execute->execute(); %songs_hash=(); while (@results = $execute->fetchrow_array()) { if($results[2]) { $value=$results[0]; $key=$results[2]."/".$results[1]; } else { $value=$results[0]; $key=$results[1]; } $songs_hash{lc($key."")} = $value+0; } $parser = new XML::Parser(Handlers => {Start => \&start_element, End => \&end_element, Char => \&characters}); $parser->parsefile($file);