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