#!/usr/bin/perl -w # (c) 2002 Armin Obersteiner # License: GPL v2 use MP3::Info; use Unicode::String qw( latin1 utf16 ); use Shell qw ( find gzip ); use Getopt::Std; use File::Copy; use Filesys::DiskFree; use Data::Dumper qw (Dumper); use strict; my $version="0.68"; # # options & config # my %opt; getopts("fcnh",\%opt); if($opt{h}) { print <<"EOF"; $0 [-c] [-f] [Search Pattern 1] [Search Pattern 2] ... -c create: create directory structure on plain ipod before syncing (default: you get a warning if there is no ipod structure) -f force: rename ipod and use it with $0 before syncing (default: an unknown ipod stays untouched) -n name check: checks mp3 names for possible illegal characters Search Patterns: for each search pattern a playlist is created (case insensitive) EOF exit; } my $buffer = 5*1024*1024; # leave some MB free for iTunesDB my @required = qw ( SYNCMODE PLAYLISTDIR IPODDIR BACKUPDIR ); my $rc=readrc("$ENV{HOME}/.ipod/config",\@required); #print Dumper($rc); # # check ipod name # my ($ipod_name, $real_name, $computer_name)=get_ipodname($rc->{IPODDIR}); unless($ipod_name) { die "IPOD dir not found: $rc->{IPODDIR}" unless $opt{c}; } # # check ipod dirs (recreate them if necessary) # mkdir "$rc->{IPODDIR}/iPod_Control",0755 unless(-d "$rc->{IPODDIR}/iPod_Control"); mkdir "$rc->{IPODDIR}/iPod_Control/Music",0755 unless(-d "$rc->{IPODDIR}/iPod_Control/Music"); mkdir "$rc->{IPODDIR}/iPod_Control/iTunes",0755 unless(-d "$rc->{IPODDIR}/iPod_Control/iTunes"); mkdir "$rc->{IPODDIR}/iPod_Control/Device",0755 unless(-d "$rc->{IPODDIR}/iPod_Control/Device"); for(0..19) { my $d=sprintf "%.2d",$_; mkdir "$rc->{IPODDIR}/iPod_Control/Music/F$d",0755 unless(-d "$rc->{IPODDIR}/iPod_Control/Music/F$d"); } unless($opt{c}) { print STDERR "IPOD name: $ipod_name\n"; print STDERR "Synced by: $real_name\n"; print STDERR "Synced on: $computer_name\n"; if($rc->{WRITEDEVICEINFO} && !$opt{f}) { my $exit=0; unless($rc->{IPODNAME} eq $ipod_name) { $exit=1; print STDERR "Your IPOD name: $rc->{IPODNAME}\n"; } unless($rc->{REALNAME} eq $real_name) { $exit=1; print STDERR "Your real name: $rc->{REALNAME}\n"; } unless($rc->{COMPUTERNAME} eq $computer_name) { $exit=1; print STDERR "Your computer: $rc->{COMPUTERNAME}\n"; } die "names mismatch, use -f to override" if $exit; } print STDERR "\n"; } # # write ipod name # if($rc->{WRITEDEVICEINFO}) { set_ipodname( $rc->{IPODDIR},$rc->{BACKUPDIR}, $rc->{IPODNAME},$rc->{REALNAME},$rc->{COMPUTERNAME} ); $ipod_name=$rc->{IPODNAME}; } # # check for songs # my %songs; my %check; my $dir; $dir=$rc->{IPODDIR}."/iPod_Control/Music"; $dir=$rc->{SYNCDIR} if($rc->{SYNCMODE} >= 2); my %tosync; if(($rc->{SYNCLIST}) && ($rc->{SYNCMODE} == 2)) { open IN,$rc->{SYNCLIST} or die "all-playlist: $rc->{SYNCLIST} not found"; while() { chomp; $tosync{$_}=1; } close IN; } my @mp3s; if(($rc->{SYNCMODE} == 3)) { my @pl=find("$rc->{PLAYLISTDIR}/* 2>/dev/null"); my %test; for my $p (@pl) { chomp $p; my ($n) = $p =~ /.*\/(.*?)$/; open IN,$p or die "playlist: $p could not be opened"; while() { unless($test{$_}) { push @mp3s,$_; $test{$_}=1; } } } } else { @mp3s=find($dir); } for(@mp3s) { chomp $_; next unless(/\.(m|M)(p|P)3$/); my $name=$_; if(keys %tosync) { next unless($tosync{$name}); } if($opt{n}) { die "illegal character in filename [$name]\n" unless ($name =~ /^[A-Za-z0-9\.\-_\/\,]+$/); } s/\://g; s/.*\///g; $songs{$name}{name}=$_; if($rc->{SYNCMODE} >= 2) { $songs{$name}{dir}="F".hash($_); } else { ($songs{$name}{dir}) = $name =~ /\/(F\d\d)\//; } { my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($name); $songs{$name}{size}=$size; $songs{$name}{date}=$mtime; } my $tag; $tag = get_mp3tag($name) unless($rc->{ALWAYSTEMPLATES}); my ($artist,$album,$title,$order,$_dummy_); if($tag) { # print Dumper($tag); # YEAR ARTIST COMMENT TRACKNUM TITLE ALBUM GENRE $artist=$tag->{ARTIST}; $album=$tag->{ALBUM}; $title=$tag->{TITLE}; $order=$tag->{TRACKNUM}; $order=$1 if($order =~ /(\d+)\s*\//); } else { for(sort {length($b) <=> length($a)} keys %{$rc->{FILETEMPLATES}}) { if(my @x = $name =~ /$_/) { my $c=0; for my $x (@x) { #print "\$$rc->{FILETEMPLATES}->{$_}->[$c]=\"$x\";\n"; eval "\$$rc->{FILETEMPLATES}->{$_}->[$c]=\"$x\";"; die "eval error: $@" if($@); $c++; } last; } } } unless($title) { die "no title found in: $name"; } $title =~ s/_/ /g; $artist =~ s/_/ /g; $album =~ s/_/ /g; $songs{$name}{title}=$title; $songs{$name}{artist}=""; $songs{$name}{album}=""; $songs{$name}{order}=0; $songs{$name}{artist}=$artist if $artist; $songs{$name}{album}=$album if $album; $songs{$name}{order}=$order if $order; my $info = get_mp3info ($name); $songs{$name}{size}=$info->{SIZE}; $songs{$name}{bitrate}=$info->{BITRATE}; $songs{$name}{duration}=int($info->{SECS}*1000); $songs{$name}{vbr}=$info->{VBR}; #print Dumper($info); my $n=$songs{$name}{dir}."/".$songs{$name}{name}; unless($check{$n}) { $check{$n}=1; } else { die "songname: $songs{$name}{name} not unique"; } } # # deleting unwanted songs # my %known; for(keys %songs) { $known{$songs{$_}{name}}=1; } #print Dumper(\%known); my @ipod = find ("$rc->{IPODDIR}/iPod_Control/Music"); my @todel; for(@ipod) { next unless (/\.mp3$/i); chomp; my ($name) = $_ =~ /\/([^\/]+\.mp3)$/i; unless($known{$name}) { push @todel,$_; } } my $del; if($rc->{DELETEASK} && @todel) { for(@todel) { print "del: $_\n"; } print "Do you really want to delete this songs? (y/N) "; my $in=; chomp $in; $del=1 if($in =~ /^y$/i); } else { $del=1; } if($del) { for(@todel) { print STDERR "deleting: $_\n"; unlink($_); } } # # copy songs # my $main_sl=""; my $main_pl=""; my $index=500; #print Dumper(\%songs); my $df = new Filesys::DiskFree; SONGS: for my $song (keys %songs) { my $attr; my $out=""; my $attr_c=3; if($rc->{SYNCMODE} >= 2) { my $to = "$rc->{IPODDIR}/iPod_Control/Music/$songs{$song}{dir}/$songs{$song}{name}"; #my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, # $atime,$mtime,$ctime,$blksize,$blocks) = stat($to); #$size=0 unless $size; #print "checking: $song [$songs{$song}{size}] -> $to [$size]\n"; #if($size != $songs{$song}{size}) { unless(-e $to) { print STDERR "syncing: $songs{$song}{name}\n"; # cp "\"$song\" \"$to\""; $df->df(); my $free=$df->avail($rc->{IPODDIR}); if($free-$songs{$song}{size}-$buffer>0) { copy($song,$to); } else { print STDERR "no space availiable for: $songs{$song}{name} [$songs{$song}{size}]\n"; delete $songs{$song}; next SONGS; } } } $songs{$song}{index}=$index; $out.=create_mhod($songs{$song}{title},1); if($songs{$song}{artist}) { $attr_c++; $out.=create_mhod($songs{$song}{artist},4); } if($songs{$song}{album}) { $attr_c++; $out.=create_mhod($songs{$song}{album},3); } $out.=create_mhod("MPEG audio file",6); $out.=create_mhod(":iPod_Control:Music:".$songs{$song}{dir}.":".$songs{$song}{name},2); $out=create_mhit( $attr_c,length($out),$index,$songs{$song}{vbr}, $songs{$song}{date},$songs{$song}{size}, $songs{$song}{duration},$songs{$song}{order}, $songs{$song}{bitrate} ).$out; $main_sl.=$out; $main_pl.=create_mhod_mhip($songs{$song}{index}); $index++; } #print Dumper(\%songs); my %playlists; my @pl=find("$rc->{PLAYLISTDIR}/* 2>/dev/null"); for my $p (@pl) { chomp $p; my ($n) = $p =~ /.*\/(.*?)$/; open IN,$p or die "playlist: $p could not be opened"; while() { my $song=$_; chomp $song; unless($songs{$song}) { print STDERR "ignoring song in playlist [$p], [$song] does not exist in syncdir or ipod full\n"; } else { $playlists{$n}{raw}.=create_mhod_mhip($songs{$song}{index}); $playlists{$n}{count}++; } } close IN; } # # creating search pattern playlists # for my $pattern (@ARGV) { my @list; for(keys %songs) { push @list,$songs{$_}{index} if($_ =~ /$pattern/i); } unless(@list) { print STDERR "nothing for searchpattern: $pattern found\n"; } else { my ($name)=$pattern=~/(\S\S\S+)/; unless(length($name)>=3) { $name=$pattern; $name =~ s/[^A-Za-z0-9]//g; } for(@list) { $playlists{$name}{raw}.=create_mhod_mhip($_); $playlists{$name}{count}++; } print STDERR @list." songs for searchpattern: $pattern found\n"; } } #print Dumper(\%playlists); # # build the pieces together # my $output; my $song_c=keys %songs; print STDERR "\nFound songs: $song_c\n"; my $tmp=create_mhlt($song_c).$main_sl; $main_sl=create_mhsd(96+length($tmp),1).$tmp; print STDERR "Songlist created\n"; my $pl_c=keys %playlists; print STDERR "\nFound additional playlists: $pl_c\n"; $tmp=create_mhlp($pl_c+1).create_playlist_main($ipod_name,$song_c).$main_pl; print STDERR "\nMain playlist created: $song_c songs\n\n"; for(keys %playlists) { $tmp.=create_playlist($_,$playlists{$_}{count}).$playlists{$_}{raw}; print STDERR "Playlist \"$_\" created: $playlists{$_}{count} songs\n"; } $main_pl=create_mhsd(96+length($tmp),2).$tmp; $output=create_mhbd(104+length($main_sl.$main_pl)).$main_sl.$main_pl; # backup old iTunesDB if(-e "$rc->{IPODDIR}/iPod_Control/iTunes/iTunesDB") { my $t=time(); copy("$rc->{IPODDIR}/iPod_Control/iTunes/iTunesDB","$rc->{BACKUPDIR}/iTunesDB_$t"); gzip("$rc->{BACKUPDIR}/iTunesDB_$t"); } open OUT,">".$rc->{IPODDIR}."/iPod_Control/iTunes/iTunesDB" or die "cannot write iTunesDB"; print OUT $output; close OUT; print STDERR "\niTunesDB created.\n"; exit; # END # # internal subroutines # sub create_mhbd { my ($size) = @_; my $r= "mhbd"; $r.= pack "V",104; $r.= pack "V",$size; $r.= pack "V",1; $r.= pack "V",1; $r.= pack "V",2; for(1..20) { $r.= pack "V",0; } return $r; } sub create_mhlp { my ($count) = @_; my $r= "mhlp"; $r.= pack "V",92; $r.= pack "V",$count; for(1..20) { $r.= pack "V",0; } return $r; } sub create_playlist { my ($name,$anz) = @_; my $ipod_name=create_mhod($name,1); my $r= "mhyp"; $r.= pack "V",108; $r.= pack "V",108+648+length($ipod_name)+$anz*(76+44); $r.= pack "V",2; $r.= pack "V",$anz; $r.= pack "V",0; $r.= pack "V",3088620292; $r.= pack "V",2317718671; $r.= pack "V",3655876446; for(1..18) { $r.= pack "V",0; } $r.= "mhod"; $r.= pack "V",24; $r.= pack "V",648; $r.= pack "V",100; for(1..3) { $r.= pack "V",0; } $r.= pack "V",12714187; # ?? 12714187 $r.= pack "V",26215000; $r.= pack "V",0; $r.= pack "V",65736; $r.= pack "V",1; # ?? 1 $r.= pack "V",6; # ?? 6 $r.= pack "V",0; # ?? 0 $r.= pack "V",2555905; # ?? 2555905 for(1..3) { $r.= pack "V",0; } $r.= pack "V",13107202; for(1..3) { $r.= pack "V",0; } $r.= pack "V",3276813; for(1..3) { $r.= pack "V",0; } $r.= pack "V",8192004; for(1..3) { $r.= pack "V",0; } $r.= pack "V",8192003; for(1..3) { $r.= pack "V",0; } $r.= pack "V",5242888; for(1..107) { $r.= pack "V",0; } $r.= pack "V",140; for(1..19) { $r.= pack "V",0; } return $r.$ipod_name; } sub create_playlist_main { my ($name,$anz) = @_; my $ipod_name=create_mhod($name,1); my $r= "mhyp"; $r.= pack "V",108; $r.= pack "V",108+648+length($ipod_name)+$anz*(76+44); $r.= pack "V",2; $r.= pack "V",$anz; $r.= pack "V",1; $r.= pack "V",3087491191; $r.= pack "V",837788566; $r.= pack "V",62365; for(1..18) { $r.= pack "V",0; } $r.= "mhod"; $r.= pack "V",24; $r.= pack "V",648; $r.= pack "V",100; for(1..3) { $r.= pack "V",0; } $r.= pack "V",13172927; # ?? 12714187 $r.= pack "V",26215000; $r.= pack "V",0; $r.= pack "V",65736; $r.= pack "V",5; # ?? 1 $r.= pack "V",6; # ?? 6 $r.= pack "V",3; # ?? 0 $r.= pack "V",1179649; # ?? 2555905 for(1..3) { $r.= pack "V",0; } $r.= pack "V",13107202; for(1..3) { $r.= pack "V",0; } $r.= pack "V",3276813; for(1..3) { $r.= pack "V",0; } $r.= pack "V",8192004; for(1..3) { $r.= pack "V",0; } $r.= pack "V",8192003; for(1..3) { $r.= pack "V",0; } $r.= pack "V",5242888; for(1..107) { $r.= pack "V",0; } $r.= pack "V",140; for(1..19) { $r.= pack "V",0; } return $r.$ipod_name; } sub create_mhod_mhip { my ($ref) = @_; my $r= "mhip"; $r.= pack "V",76; $r.= pack "V",76; $r.= pack "V",1; $r.= pack "V",0; $r.= pack "V",$ref-1; $r.= pack "V",$ref; $r.= pack "V",3088619525; for(1..11) { $r.= pack "V",0; } $r.="mhod"; $r.= pack "V",24; $r.= pack "V",44; $r.= pack "V",100; $r.= pack "V",0; $r.= pack "V",0; $r.= pack "V",$ref-1; for(1..4) { $r.= pack "V",0; } return $r; } sub create_mhsd { my ($size,$type) = @_; my $r="mhsd"; $r.= pack "V",96; $r.= pack "V",$size; $r.= pack "V",$type; for(1..20) { $r.= pack "V",0; } return $r; } sub create_mhlt { my ($count) = @_; my $r="mhlt"; $r.= pack "V",92; $r.= pack "V",$count; for(1..20) { $r.= pack "V",0; } return $r; } sub create_mhit { my ($arttr_c,$attr_s,$index,$vbr,$date,$size,$dur,$order,$bitrate) = @_; my $r="mhit"; $r.= pack "V",156; $r.= pack "V",156+$attr_s; $r.= pack "V",$arttr_c; $r.= pack "V",$index; $r.= pack "V",1; $r.= pack "V",0; my $type=256; $type+=1 if($vbr); $r.= pack "V",$type; $r.= pack "V",$date+2082844800; $r.= pack "V",$size; $r.= pack "V",$dur; $r.= pack "V",$order; $r.= pack "V",0; $r.= pack "V",0; $r.= pack "V",$bitrate; $r.= pack "V",2890137600; for(1..23) { $r.= pack "V",0; } return $r; } sub create_mhod { my ($string,$type) = @_; my $len=length($string); my $r="mhod"; $r.= pack "V",24; $r.= pack "V",(40+2*$len); $r.= pack "V",$type; $r.= pack "V2",0; $r.= pack "V",1; $r.= pack "V",(2*$len); $r.= pack "V2",0; my $u=latin1($string); $u->byteswap; $r.= $u->utf16; return $r; } sub set_ipodname { my ($dev,$backup,$name,$real,$cpu)=@_; $dev.="/iPod_Control/iTunes/DeviceInfo"; my $file; for(1..384) { $file.=pack "V",0; } my $l=length($name); substr($file,0,2)=pack "v",$l; my $u=latin1($name); $u->byteswap; substr($file,2,$l*2)=$u->utf16; $l=length($real); substr($file,512,2)=pack "v",$l; $u=latin1($real); $u->byteswap; substr($file,514,$l*2)=$u->utf16; $l=length($cpu); substr($file,1024,2)=pack "v",$l; $u=latin1($cpu); $u->byteswap; substr($file,1026,$l*2)=$u->utf16; if(-e $dev) { my $t=time(); copy($dev,"$backup/DeviceInfo_$t"); gzip("$backup/DeviceInfo_$t"); } open IPOD,">$dev" or die "cannot write DeviceInfo"; print IPOD $file; close IPOD; } sub get_ipodname { my $dev=shift; $dev.="/iPod_Control/iTunes/DeviceInfo"; my $file; my $buff; open IPOD,$dev or return undef; while (read(IPOD, $buff, 8 * 2**10)) { $file.=$buff; } close IPOD; my $l=unpack "v",substr($file,0,2); my $s=substr($file,2,$l*2); my $u=utf16($s); $u->byteswap; my $name=$u->latin1; $l=unpack "v",substr($file,512,2); $s=substr($file,514,$l*2); $u=utf16($s); $u->byteswap; my $realname=$u->latin1; $l=unpack "v",substr($file,1024,2); $s=substr($file,1026,$l*2); $u=utf16($s); $u->byteswap; my $computername=$u->latin1; return ($name,$realname,$computername); } sub hash { my $string=shift; my $key; my $len=length($string); for(my $j=$len-1 ; $j>1 ; $j--) { $key+=ord(substr($string,$j,1)); } return sprintf "%.2d",(substr($key,length($key)-2,2) % 20); } sub readrc { my $file = shift; my $req = shift; my $rc; my $sub; open IN,$file or die "cannot open rc file: $file"; while() { next if /^\s*$/; next if /^\s*#/; if(/^\s*(\S+)\s*=\s*(.*?)\s*$/) { my $k=$1; my $n=$2; ($n) = $n =~ /^\"(.*?)\"$/ if($n =~ /\"/); unless($sub) { $rc->{$k}=$n; } else { ($k) = $k =~ /^\"(.*?)\"$/ if($k =~ /\"/); my @n=split /,/,$n; for(@n) { s/^\s+//g; s/\s+$//g; s/^\"//; s/\"$//; } $rc->{$sub}->{$k}=\@n; } } elsif (/^\s*(\S+)\s*\{/) { $sub=$1; } elsif (/^\s*}/) { $sub=undef; } } if($rc->{SYNCMODE} == 2) { push @$req,"SYNCDIR"; } if($rc->{WRITEDEVICEINFO} == 1) { push @$req,("IPODNAME","REALNAME","COMPUTERNAME"); } if($rc->{ALWAYSTEMPLATES} == 1) { push @$req,"FILETEMPLATES"; } for my $d (keys %$rc) { if($d =~ /DIR$/) { $rc->{$d} =~ s/\~/$ENV{HOME}/; } } $rc->{SYNCLIST} =~ s/\~/$ENV{HOME}/ if $rc->{SYNCLIST}; for(@$req) { die "RC PARAMETER: $_ not found" unless($rc->{$_}); } return $rc; }