#!/usr/local/bin/perl -w use strict ; $0 =~ s/.*\/// ; # -*- perl -*- # A Perl script to export the music of an iTunes playlist as files, e. g. to # copy the music files of your favourite playlist to an USB stick. First export # the playlist from iTunes as a text file, then call # # export-playlist-music playlist-file target-directory # # Tested on the Macintosh only; assumes CR line endings in the playlist file. # Preserves the Artist/Album directory structure. Does not copy files that are # already present with the correct size. # # Version 1 [ni 2010-01-27]. # # Copyright (c) 2009, 2010 # Juergen Nickelsen . All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. my $filename_field = "Location"; # file name column title die("usage: $0 playlist to-location\n") unless @ARGV == 2; my ($playlist, $target_dir) = @ARGV; create_dir($target_dir); $/ = "\r"; # traditional Macintosh text file open(PL, $playlist) or die("$0: cannot open $playlist\n"); # column titles are in the first line my $first_line = ; chomp($first_line); my @column_titles = split(/\t/, $first_line); #print(join(":", @column_titles), "\n"); while () { chomp(); # I could just use the last field for the file name, but I want to have # this code more generic so I can reuse it for other playlist handling # code. Runtime is insignificant compared with copying the file anyway. my @fields = split(/\t/); my %data = (); for (my $i = 0; $i < @fields; $i++) { $data{$column_titles[$i]} = $fields[$i] || ""; } # file name is missing in some lines, dunno why; perhaps vanished files unless ($data{$filename_field}) { warn("$playlist:$.: $filename_field undefined\n"); next; } copy_file($data{$filename_field}, $target_dir); } # create a directory if it does not yet exist, die on fail sub create_dir { my ($dir) = @_; unless (-d $dir) { mkdir($dir) or die("$0: cannot create $dir ($!)\n"); } } sub filesize { my ($fname) = @_; my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat($fname) or warn("$0: stat $fname: $!\n"); #print("size is $size\n"); return $size; } sub copy_file { my ($fname, $to_dir) = @_; my $ufname = $fname; # the Unix filename $ufname =~ tr{:/}{/:}; $ufname =~ s{^[^/]*}{}; my @path = split(/:/, $fname); my $artist = $path[-3]; # this is always this way, right? my $album = $path[-2]; my $title = $path[-1]; my $tfname = "$to_dir/$artist/$album/$title"; # the target file name if (-f $tfname && (filesize($tfname) == filesize($ufname))) { print("skip $ufname\n"); return; } print("$tfname\n"); create_dir("$to_dir/$artist"); create_dir("$to_dir/$artist/$album"); local $/; unless (open(IN, $ufname)) { warn("$0: cannot open $ufname ($!)\n"); return; } unless (open(OUT, ">$tfname")) { warn("$0: cannot write to $tfname ($!)\n"); close(IN); return; } unless (print OUT ()) { warn("$0: error writing to $tfname ($!)\n"); } close(IN); close(OUT) or warn("$0: error closing $tfname ($!)\n"); } #EOF