Files
bin/SyncPOD
2012-12-16 18:46:30 +01:00

863 lines
17 KiB
Perl
Executable File

#!/usr/bin/perl -w
# (c) 2002 Armin Obersteiner <armin@xos.net>
# 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(<IN>) {
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(<IN>) {
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=<STDIN>;
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(<IN>) {
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(<IN>) {
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;
}