1###__PERLBIN__### 2# Copyright (C) 2002-2007 Adrian Ulrich <pab at blinkenlights.ch> 3# Part of the gnupod-tools collection 4# 5# URL: http://www.gnu.org/software/gnupod/ 6# 7# GNUpod is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 3 of the License, or 10# (at your option) any later version. 11# 12# GNUpod is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>.# 19# 20# iTunes and iPod are trademarks of Apple 21# 22# This product is not supported/written/published by Apple! 23 24use strict; 25use GNUpod::iTunesDB; 26use GNUpod::XMLhelper; 27use GNUpod::FooBar; 28use Getopt::Long; 29use Data::Dumper; 30 31use constant MODE_SONGS => 1; 32use constant MODE_OLDPL => 2; 33use constant MODE_NEWPL => 3; 34 35use vars qw(%opts); 36$| = 1; 37 38 39print "tunes2pod.pl Version ###__VERSION__### (C) Adrian Ulrich\n"; 40 41$opts{mount} = $ENV{IPOD_MOUNTPOINT}; 42 43GetOptions(\%opts, "version", "force", "help|h", "mount|m=s"); 44GNUpod::FooBar::GetConfig(\%opts, {mount=>'s', force=>'b', model=>'s'}, "tunes2pod"); 45 46 47usage() if $opts{help}; 48version() if $opts{version}; 49 50convert(); 51 52 53sub convert { 54 $opts{_no_sync} = 1; 55 my $con = GNUpod::FooBar::connect(\%opts); 56 usage("$con->{status}\n") if $con->{status}; 57 58 #We disabled all autosyncing (_no_sync set to 1), so we do a test 59 #ourself 60 if(!$opts{force} && !(GNUpod::FooBar::ItunesDBNeedsSync($con))) { 61 print "I don't think that you have to run tunes2pod.pl\n"; 62 print "The GNUtunesDB looks up-to-date\n"; 63 print "\n"; 64 print "If you think i'm wrong, use '$0 --force'\n"; 65 exit(1); 66 } 67 68 open(ITUNES, $con->{itunesdb}) or usage("Could not open $con->{itunesdb}"); 69 70 while(<ITUNES>) {}; sysseek(ITUNES,0,0); # the iPod is a sloooow mass-storage device, slurp it into the fs-cache 71 72 my $self = { ctx => {}, mode => 0, playlist => {}, pc_playlist => {}, count_songs_done => 0, count_songs_total => 0 }; 73 bless($self,__PACKAGE__); 74 $self->ResetPlaylists; 75 76 # Define callbacks 77 my $obj = { offset => 0, childs => 1, fd=>*ITUNES, 78 callback => { 79 PACKAGE=>$self, mhod => { item => 'MhodItem' }, mhit => { start => 'MhitStart', end => 'MhitEnd' }, 80 mhsd => { start => 'MhsdStart' }, mhip => { item => 'MhipItem' }, 81 mhyp => { item => 'MhypItem', end=>'MhypEnd' }, mhlt => { item => 'MhltItem' }, 82 } 83 }; 84 GNUpod::iTunesDB::ParseiTunesDB($obj,0); # Parses the iTunesDB 85 GNUpod::XMLhelper::writexml($con); # Writes out the new XML file 86 87 GNUpod::FooBar::SetItunesDBAsInSync($con); # GNUtunesDB.xml is in-sync with iTunesDB 88 GNUpod::FooBar::SetOnTheGoAsValid($con); # ..and so is the OnTheGo data 89 90 #The iTunes is now set to clean .. maybe we have to 91 #update the otg.. 92 $opts{_no_sync} = 0; 93 $opts{_no_cstest} = 1; 94 GNUpod::FooBar::connect(\%opts); 95 96 print "\n Done\n"; 97 close(ITUNES) or die "Failed to close filehandle of $con->{itunesdb} : $!\n"; 98 exit(0); 99} 100 101####################################################################### 102# Cleans current playlist buffer 103sub ResetPlaylists { 104 my($self) = @_; 105 $self->{playlist} = { name => 'Lost and Found', plid => 0, mpl => 0, podcast => 0, content => [], spl => {} }; 106 $self->{pc_playlist} = { index => 0, lists => {} }; 107} 108 109####################################################################### 110# Set name of current playlist 111sub SetPlaylistName { 112 my($self,$arg) = @_; 113 $self->{playlist}->{name} = $arg if length($arg) != 0; 114} 115 116####################################################################### 117# Set SmartPlaylists preferences for current playlist 118sub SetSplPreferences { 119 my($self,$ref) = @_; 120 $self->{playlist}->{spl}->{preferences} = $ref; 121} 122 123####################################################################### 124# Sets content of current SmartPlaylist 125sub SetSplData { 126 my($self,$ref) = @_; 127 $self->{playlist}->{spl}->{data} = $ref; 128} 129 130####################################################################### 131# Sets Matchrule for current SmartPlaylist 132sub SetSplMatchrule { 133 my($self,$ref) = @_; 134 $self->{playlist}->{spl}->{matchrule} = $ref; 135} 136 137####################################################################### 138# Sets current podcast index 139sub SetPodcastIndex { 140 my($self,$index) = @_; 141 return if $index == 0; 142 $self->{pc_playlist}->{index} = $index; 143 $self->{pc_playlist}->{lists}->{$index} = { name => 'Lost and Found', content => [] }; 144} 145 146####################################################################### 147sub SetPodcastName { 148 my($self,$name) = @_; 149 my $index = $self->{pc_playlist}->{index}; 150 return if $index == 0; 151 $self->{pc_playlist}->{lists}->{$index}->{name} = $name if length($name) != 0; 152} 153 154####################################################################### 155# Append item to podcast playlist 156sub AppendPodcastItem { 157 my($self,$index,$item) = @_; 158 my $index = $self->{pc_playlist}->{index}; 159 return if $index == 0; 160 push(@{$self->{pc_playlist}->{lists}->{$index}->{content}},$item); 161} 162 163 164####################################################################### 165# Dumps object content 166sub Dumpit { 167 my($self,%args) = @_; 168 print Data::Dumper::Dumper(\%args); 169} 170 171####################################################################### 172# Switch to current mhsd mode 173sub MhsdStart { 174 my($self,%args) = @_; 175 my $type = int($args{ref}->{type}); 176 my $old = $self->{mode}; 177 $self->{mode} = $type; 178 179 if($old == MODE_SONGS) { print "\r> $self->{count_songs_done} of $self->{count_songs_total} files found, searching playlists\n" } 180} 181 182####################################################################### 183# A mhit, holds information about size, length.. etc.. Should have a 184# mhod as child 185sub MhitStart { 186 my($self, %args) = @_; 187 if($self->{mode} == MODE_SONGS) { 188 $self->{ctx} = $args{ref}->{ref}; # Swallow-in mhit reference 189 } 190 else { 191 warn "unknown mode: $self->{mode}\n"; 192 } 193} 194 195####################################################################### 196# We've seen all mhit childs, so we can write the <file /> item itself 197sub MhitEnd { 198 my($self, %args) = @_; 199 if($self->{mode} == MODE_SONGS) { 200 GNUpod::XMLhelper::mkfile({file=>$self->{ctx}}); # Add <file element to xml 201 $self->{ctx} = (); # And drop this buffer 202 my $i = ++$self->{count_songs_done}; 203 if($i % 32 == 0) { 204 printf("\r> %d files left, %d%% done ", $self->{count_songs_total}-$i, ($i/(1+$self->{count_songs_total})*100)); 205 } 206 } 207 else { 208 warn "unknown mode: $self->{mode}\n"; 209 } 210} 211 212sub MhltItem { 213 my($self, %args) = @_; 214 $self->{count_songs_total} = $args{ref}->{childs}; 215} 216 217####################################################################### 218# A DataObject 219sub MhodItem { 220 my($self, %args) = @_; 221 222 if($self->{mode} == MODE_SONGS) { 223 # -> Songs mode, just add string to current context 224 my $key = $args{ref}->{type_string}; 225 if(length($key)) { 226 $self->{ctx}->{$key} = $args{ref}->{string}; # Add mhod item 227 } 228 else { 229 warn "$0: skipping unknown entry of type '$args{ref}->{type}'\n"; 230 } 231 } 232 elsif($self->{mode} == MODE_OLDPL) { 233 # Legacy playlist 234 if($args{ref}->{type_string} eq 'title') { 235 # -> Set title of playlist following 236 $self->SetPlaylistName($args{ref}->{string}); 237 } 238 elsif($args{ref}->{type} == 50) { 239 # -> Remember spl preferences 240 $self->SetSplPreferences($args{ref}->{splpref}); 241 } 242 elsif($args{ref}->{type} == 51) { 243 # -> Remember spl data 244 $self->SetSplData($args{ref}->{spldata}); 245 $self->SetSplMatchrule($args{ref}->{matchrule}); 246 } 247 } 248 elsif($self->{mode} == MODE_NEWPL) { 249 # -> Newstyle playlist 250 if($args{ref}->{type_string} eq 'title' && $self->{playlist}->{podcast}) { 251 # Title of playlist: create it 252 $self->SetPodcastName($args{ref}->{string}); 253 } 254 } 255} 256 257 258####################################################################### 259# Playlist item 260sub MhipItem { 261 my($self, %args) = @_; 262 if($self->{mode} == MODE_OLDPL) { 263 # -> Old playlist. Add SongID to current content container 264 push(@{$self->{playlist}->{content}}, $args{ref}->{sid}); 265 } 266 elsif($self->{mode} == MODE_NEWPL && $self->{playlist}->{podcast}) { 267 # Only read podcasts in this mode (we do normal playlists in MODE_OLDPL) 268 if($args{ref}->{podcast_group} == 256 && $args{ref}->{podcast_group_ref} == 0) { 269 # -> Podcast index found 270 $self->SetPodcastIndex($args{ref}->{plid}); 271 } 272 elsif($args{ref}->{podcast_group} == 0 && $args{ref}->{podcast_group_ref} != 0) { 273 # -> New item for an index found, add it 274 $self->AppendPodcastItem($args{ref}->{podcast_group_ref}, $args{ref}->{sid}); 275 } 276 } 277} 278 279####################################################################### 280# Playlist 'uberblock' 281sub MhypItem { 282 my($self, %args) = @_; 283 $self->{playlist}->{plid} = $args{ref}->{plid}; 284 $self->{playlist}->{mpl} = ($args{ref}->{is_mpl} != 0 ? 1 : 0 ); 285 $self->{playlist}->{podcast} = ($args{ref}->{podcast} != 0 ? 1 : 0); 286} 287 288####################################################################### 289# Write out whole playlist 290sub MhypEnd { 291 my($self, %args) = @_; 292 if($self->{mode} == MODE_OLDPL) { 293 if($self->{playlist}->{mpl} == 0 && $self->{playlist}->{podcast} == 0) { 294 # -> 'Old' non-podcast playlist 295 my $plname = $self->{playlist}->{name}; 296 297 if(ref($self->{playlist}->{spl}->{preferences}) eq "HASH" && ref($self->{playlist}->{spl}->{data}) eq "ARRAY") { 298 # -> Handle this as a smart-playlist 299 print ">> Smart-Playlist '$plname'"; 300 my $pref = $self->{playlist}->{spl}->{preferences}; 301 my $ns = 0; 302 my $nr = 0; 303 GNUpod::XMLhelper::addspl($plname, { liveupdate => $pref->{live}, moselected => $pref->{mos}, limititem=>$pref->{iitem}, 304 limitsort=>$pref->{isort}, limitval=>$pref->{value}, 305 matchany=>$self->{playlist}->{spl}->{matchrule}, 306 checkrule=>$pref->{checkrule}, plid=>$self->{playlist}->{plid} } ); 307 foreach my $splitem (@{$self->{playlist}->{spl}->{data}}) { 308 GNUpod::XMLhelper::mkfile({spl=>$splitem}, {splname=> $plname}); 309 $nr++; 310 } 311 foreach my $id (@{$self->{playlist}->{content}}) { 312 GNUpod::XMLhelper::mkfile({splcont=>{id=>$id}}, {splname=>$plname}); 313 $ns++; 314 } 315 print " with $nr rules and $ns songs\n"; 316 } 317 else { 318 # -> This is a normal playlist 319 print ">> Playlist '$plname'"; 320 my $ns = 0; 321 GNUpod::XMLhelper::addpl($plname, {plid=>$self->{playlist}->{plid}}); 322 foreach my $id (@{$self->{playlist}->{content}}) { 323 GNUpod::XMLhelper::mkfile({add => { id => $id } },{plname=>$self->{playlist}->{name}}); 324 $ns++; 325 } 326 print " with $ns songs\n"; 327 } 328 } 329 } 330 elsif($self->{mode} == MODE_NEWPL) { 331 # -> We are supposed to have a complete podcasts list here.. 332 foreach my $pci (sort keys(%{$self->{pc_playlist}->{lists}})) { 333 my $cl = $self->{pc_playlist}->{lists}->{$pci}; 334 my $ns = 0; 335 print ">> Podcast-Playlist '$cl->{name}'"; 336 GNUpod::XMLhelper::addpl($cl->{name}, {podcast=>1}); 337 foreach my $i (@{$cl->{content}}) { 338 GNUpod::XMLhelper::mkfile({add => { id => $i } }, {plname=>$cl->{name}}); 339 $ns ++; 340 } 341 print " with $ns songs\n"; 342 } 343 } 344 $self->ResetPlaylists; # Resets podcast and normal playlist data 345} 346 347 348 349 350 351 352 353sub usage { 354 my($rtxt) = @_; 355die << "EOF"; 356$rtxt 357Usage: tunes2pod.pl [-h] [-m directory] 358 359 -h, --help display this help and exit 360 --version output version information and exit 361 -m, --mount=directory iPod mountpoint, default is \$IPOD_MOUNTPOINT 362 --force Disable 'sync' checking 363 364Report bugs to <bug-gnupod\@nongnu.org> 365EOF 366} 367 368sub version { 369die << "EOF"; 370tunes2pod.pl (gnupod) ###__VERSION__### 371Copyright (C) Adrian Ulrich 2002-2007 372 373This is free software; see the source for copying conditions. There is NO 374warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 375 376EOF 377} 378 379 380 381 382