File Coverage

blib/lib/Mac/iPod/GNUpod.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package Mac::iPod::GNUpod;
4              
5             =head1 NAME
6              
7             Mac::iPod::GNUpod - Add and remove songs from your iPod; read and write
8             databases in iTunes and GNUpod format
9              
10             =head1 ABSTRACT
11              
12             This is the module to do anything with your iPod, with methods for initializing
13             your iPod, adding and removing songs, and reading and writing databases in the
14             iTunes and GNUpod formats. This module was originally based on the GNUpod
15             script package, written and distributed by Adrian Ulrich, (pab at
16             blinkenlights.ch), L. However, a lot of
17             development has occurred since then, making the module more flexible and more
18             appropriate for CPAN. This module and the GNUpod scripts remain completely
19             interoperable--they write the same file format and work in much the same way.
20              
21             =head1 SYNOPSIS
22              
23             use Mac::iPod::GNUpod;
24              
25             my $ipod = Mac::iPod::GNUpod->new(mountpoint => '/mnt/ipod');
26              
27             # Read existing databases
28             $ipod->read_gnupod;
29             $ipod->read_itunes;
30              
31             # Add songs
32             my $id = $ipod->add_song('~/music/The Foo Brothers - All Barred Up.mp3');
33              
34             # Get paths to songs
35             my $path = $ipod->get_path($id);
36              
37             # Find the id numbers of existing songs
38             my @yuck = $ipod->search(artist => 'Yoko Ono');
39              
40             # Remove songs based on id
41             $ipod->rm_song(@yuck);
42              
43             # Write databases
44             $ipod->write_gnupod;
45             $ipod->write_itunes;
46              
47             =cut
48              
49             # Remainder of POD after __END__
50              
51 4     4   196069 use warnings;
  4         12  
  4         155  
52 4     4   24 use warnings::register;
  4         8  
  4         869  
53 4     4   26 use strict;
  4         11  
  4         161  
54              
55 4     4   3373 use Mac::iPod::GNUpod::Utils;
  4         12  
  4         403  
56 4     4   4545 use File::Copy;
  4         14322  
  4         280  
57 4     4   32 use File::Spec;
  4         7  
  4         80  
58 4     4   4756 use File::Spec::Mac;
  4         25516  
  4         167  
59 4     4   6701 use XML::Parser;
  0            
  0            
60             use Carp qw/carp croak/;
61             our @CARP_NOT = qw/XML::Parser XML::Parser::Expat Mac::iPod::GNUpod/;
62              
63             our $VERSION = '1.24';
64              
65             # Global variables
66              
67             sub new {
68             my ($class, %opt) = @_;
69              
70             my $self = {
71             mnt => '', # Mountpoint
72             itunes_db => '', # iTunes DB
73             gnupod_db => '', # GNUpod DB
74             allow_dup => 0, # Whether duplicates are allowed
75             move_files => 1, # Whether to actually move files on add or rm
76             files => [], # List of file hrefs
77             idx => {}, # Indices of song properties (for searching)
78             plorder => [], # List of playlists in order
79             pl_idx => {}, # Playlists by name
80             spl_idx => {} # Smartplaylists by name
81             };
82              
83             bless $self, $class;
84              
85             if ($opt{mountpoint}) {
86             $self->mountpoint($opt{mountpoint});
87             }
88             elsif ($opt{itunes_db} && $opt{gnupod_db}) {
89             $self->itunes_db($opt{itunes_db});
90             $self->gnupod_db($opt{gnupod_db});
91             }
92             else {
93             croak "You must specify either the mountpoint or both the itunes_db and gnupod_db options";
94             }
95              
96             return $self;
97             }
98              
99             sub mountpoint {
100             my $self = shift;
101             if (@_) {
102             $self->{mnt} = File::Spec->canonpath(shift);
103             $self->{itunes_db} = File::Spec->catfile($self->{mnt}, "iPod_Control", "iTunes", "iTunesDB");
104             $self->{gnupod_db} = File::Spec->catfile($self->{mnt}, "iPod_Control", ".gnupod", "GNUtunesDB");
105             }
106             return $self->{mnt};
107             }
108              
109             # Here we define the template get-set funcs
110             my @flags = qw/itunes_db gnupod_db allow_dup move_files/;
111             for my $flag (@flags) {
112             no strict 'refs';
113             *$flag = sub {
114             my $self = shift;
115             if (@_) {
116             $self->{$flag} = shift;
117             }
118             return $self->{$flag};
119             };
120             }
121              
122             # Format a new iPod, create directory structure, prepare for GNUpod
123             sub init {
124             my ($self, %opts) = @_;
125              
126             if (not $self->{mnt}) {
127             croak "Can't init iPod without the mountpoint set";
128             }
129              
130             # Folder structure
131             for my $path( ('Calendars', 'Contacts', 'Notes', 'iPod_Control', 'iPod_Control/Music',
132             'iPod_Control/iTunes', 'iPod_Control/Device', 'iPod_Control/.gnupod') ) {
133             my @path = split('/', $path);
134             my $path = File::Spec->catdir($self->{mnt}, @path);
135             next if -d $path;
136             mkdir $path or croak "Could not create $path ($!)";
137             }
138              
139             # Music folders
140             for(0..19) {
141             my $path = File::Spec->catdir($self->{mnt}, "iPod_Control", "Music", sprintf("f%02d", $_));
142             next if -d $path;
143             mkdir $path or croak "Could not create $path ($!)";
144             }
145              
146             # Convert iTunes db if allowed
147             if(-e $self->{itunes_db} && !$opts{'noconvert'}) {
148             $self->read_itunes;
149             }
150              
151             # Make empty db otherwise
152             else {
153             open(ITUNES, ">", $self->{itunes_db}) or croak "Could not create $self->{itunes_db}: $!";
154             print ITUNES "";
155             close(ITUNES);
156             }
157              
158             $self->write_gnupod;
159              
160             return 1;
161             }
162              
163             # Convert iTunesDB to GNUpodDB
164             #
165             # This function almost entirely copyright (C) 2002-2003 Adrian Ulrich. Adapted
166             # from tunes2pod.pl in the GNUpod toolset
167             sub read_itunes {
168             my ($self) = @_;
169              
170             require Mac::iPod::GNUpod::iTunesDBread or die;
171              
172             $self->_clear;
173              
174             Mac::iPod::GNUpod::iTunesDBread::open_itunesdb($self->{itunes_db})
175             or croak "Could not open $self->{itunes_db}";
176              
177             #Check where the FILES and PLAYLIST part starts..
178             #..and how many files are in this iTunesDB
179             my $itinfo = Mac::iPod::GNUpod::iTunesDBread::get_starts();
180            
181             # These 2 will change while running..
182             my $pos = $itinfo->{position};
183             my $pdi = $itinfo->{pdi};
184              
185             #Get all files
186             for my $i (0 .. ($itinfo->{songs} - 1)) {
187             ($pos, my $href) = Mac::iPod::GNUpod::iTunesDBread::get_mhits($pos); #get the mhit + all child mhods
188             #Seek failed.. this shouldn't happen..
189             if($pos == -1) {
190             croak "Expected to find $itinfo->{data} files, failed to get file $i";
191             }
192             $self->_addfile($href);
193             }
194              
195             #Now get each playlist
196             for my $i (0 .. ($itinfo->{playlists} - 1)) {
197             ($pdi, my $href) = Mac::iPod::GNUpod::iTunesDBread::get_pl($pdi); #Get an mhyp + all child mhods
198             if($pdi == -1) {
199             croak "Expected to find $itinfo->{playlists} playlists, I failed to get playlist $i";
200             }
201             next if $href->{type}; #Don't list the MPL
202             $href->{name} = "NONAME" unless($href->{name}); #Don't create an empty pl
203              
204             #SPL Data present
205             if(ref($href->{splpref}) eq "HASH" && ref($href->{spldata}) eq "ARRAY") {
206             $self->_render_spl($href->{name}, $href->{splpref}, $href->{spldata}, $href->{matchrule}, $href->{content});
207             }
208              
209             #Normal playlist
210             else {
211             $self->_addpl($href->{name});
212             # Render iPod pls in GNUpod format
213             $self->_addtopl($self->{cur_pl}, { add => { id => $_ } }) foreach @{$href->{content}};
214             }
215             }
216              
217             # Close the db
218             Mac::iPod::GNUpod::iTunesDBread::close_itunesdb();
219             }
220              
221             # Parse the GNUpod db (in XML)
222             sub read_gnupod {
223             my ($self, %opts) = shift;
224             unless (-r $self->{gnupod_db}) {
225             croak "Can't read GNUpod database at $self->{gnupod_db}";
226             }
227              
228             $self->_clear;
229              
230             # Call _eventer as a method
231             my $wrapper = sub { $self->_eventer(@_) };
232              
233             my $p = new XML::Parser( Handlers => { Start => $wrapper });
234             # Save this value to be our overall return value
235             my $rv = $p->parsefile($self->gnupod_db);
236              
237             # At end of file parsing unset cur_pl
238             $self->{cur_pl} = undef;
239              
240             # Return value from parsefile
241             return $rv;
242             }
243              
244             # Write the iTunes Db
245             #
246             # This code adapted from the mktunes.pl script, copyright (C) 2002-2003 Adrian
247             # Ulrich.
248             sub write_itunes {
249             my $self = shift;
250              
251             require Mac::iPod::GNUpod::iTunesDBwrite or die;
252            
253             # Undocumented, used only for debugging
254             my %opt = @_;
255             $opt{name} = 'GNUpod' unless $opt{name};
256              
257             my ($num, %data, %length, %ids);
258             my @newfiles = (undef); # Will become $self->{files} at end
259              
260             # Create mhits and mhods for all files
261             for (@{$self->{files}}) {
262             next if not $_ or not keys %$_;
263             # iTunes ID and GNUpod ID are NOT necessarily the same! So we build a
264             # hash of GNUpod => iTunes ids for translating playlists
265             $ids{$_->{id}} = ++$num;
266             $_->{id} = $num;
267             push @newfiles, $_;
268              
269             $data{mhit} .= Mac::iPod::GNUpod::iTunesDBwrite::render_mhit($_, $num);
270             }
271             $length{mhit} = length($data{mhit});
272              
273             # Here, after remaking all files, we remake our indexes and playlists
274             $self->_remake(\@newfiles, \%ids);
275              
276             # Create header for mhits
277             $data{mhlt} = Mac::iPod::GNUpod::iTunesDBwrite::mk_mhlt({ songs => $num });
278             $length{mhlt} = length($data{mhlt});
279              
280             # Create header for the mhlt
281             $data{mhsd_1} = Mac::iPod::GNUpod::iTunesDBwrite::mk_mhsd({
282             size => $length{mhit} + $length{mhlt},
283             type => 1
284             });
285             $length{mhsd_1} = length($data{mhsd_1});
286              
287             # Create the master playlist
288             ($data{playlist}, $num) = Mac::iPod::GNUpod::iTunesDBwrite::r_mpl(
289             name => $opt{name},
290             ids => [ 1 .. $num ],
291             type => 1,
292             curid => $num
293             );
294              
295             # Create child playlists
296             for my $plname (@{$self->{plorder}}) {
297             my %common = ( name => $plname, type => 0, curid => $num );
298             if (my $spl = $self->get_spl($plname)) {
299             (my $dat, $num) = Mac::iPod::GNUpod::iTunesDBwrite::r_mpl(
300             %common,
301             splprefs => $spl->{pref},
302             spldata => $spl->{data}
303             );
304             $data{playlist} .= $dat;
305             }
306             else {
307             (my $dat, $num) = Mac::iPod::GNUpod::iTunesDBwrite::r_mpl(
308             %common,
309             ids => [ $self->render_pl($plname) ]
310             );
311             $data{playlist} .= $dat;
312             }
313             }
314             $data{playlist} = Mac::iPod::GNUpod::iTunesDBwrite::mk_mhlp({ playlists => scalar @{$self->{plorder}} + 1 }) . $data{playlist};
315             $length{playlist} = length($data{playlist});
316              
317             # Make pl headers
318             $data{mhsd_2} = Mac::iPod::GNUpod::iTunesDBwrite::mk_mhsd({ size => $length{playlist}, type => 2 });
319             $length{mhsd_2} = length($data{mhsd_2});
320              
321             # Calculate total file length
322             my $totlength = 0;
323             $totlength += $_ for values %length;
324              
325             # Make master header
326             $data{mhbd} = Mac::iPod::GNUpod::iTunesDBwrite::mk_mhbd({ size => $totlength });
327              
328             # Debug me!
329             if ($opt{dump}) {
330             use Data::Dumper;
331             open DUMP, '>', $opt{dump} or croak "Couldn't open dump file: $!";
332             print DUMP Dumper(\%data);
333             close DUMP;
334             }
335              
336             # Write it all
337             open IT, '>', $self->{itunes_db} or croak "Couldn't write iTunes DB: $!";
338             binmode IT;
339             for ('mhbd', 'mhsd_1', 'mhlt', 'mhit', 'mhsd_2', 'playlist') {
340             no warnings 'uninitialized'; # In case one of these is empty
341             print IT $data{$_};
342             }
343             close IT;
344             }
345              
346             sub _remake {
347             my ($self, $newfiles, $ids) = @_;
348              
349             # Set new files
350             $self->{files} = $newfiles;
351              
352             # Update playlists
353             for (@{$self->{plorder}}) {
354             my $pl = $self->get_pl($_);
355             if ($pl) {
356             for (@$pl) {
357             if ($_ eq int $_) {
358             $_ = $ids->{$_};
359             }
360             }
361             }
362             }
363              
364             # Update index
365             for (values %{$self->{idx}}) { # Fields
366             for (values %$_) { # Values
367             for (@$_) { # Id numbers
368             $_ = $ids->{$_};
369             }
370             }
371             }
372             }
373              
374             # Write the GNUpod DB XML File
375             sub write_gnupod {
376             my($self) = @_;
377             open(OUT, ">$self->{gnupod_db}") or croak "Could not write $self->{gnupod_db}: $!\n";
378             binmode OUT ;
379              
380             # In this section all printing goes to OUT
381             my $oldfh = select OUT;
382              
383             # Values throughout this code are Unicode::String objects, and we want to
384             # make sure that these stringify as utf8
385             Unicode::String::stringify_as('utf8');
386              
387             print "\n";
388             print "\n";
389              
390             # Write the files section
391             print "\t\n";
392             for (@{$self->{files}}) {
393             next if not $_ or not keys %$_;
394             my %filehash = %$_; # Work with a copy, not orig hashref
395              
396             # A few keys that don't need to be written to disk
397             for my $del ('notorig', 'uniq') {
398             delete $filehash{$del};
399             }
400              
401             print "\t\t", mktag("file", \%filehash), "\n";
402             }
403             print "\t\n";
404              
405             #Print all playlists
406             foreach (@{$self->{plorder}}) {
407             my $name;
408             # Smartplaylists
409             if (my $ref = $self->get_spl($_)) {
410             print "\t" . mktag("smartplaylist", $ref->{pref}, noend => 1) . "\n";
411             for my $item (@{$ref->{data}}) {
412             for (keys %$item) {
413             next if not keys %{$item->{$_}};
414             print "\t\t", mktag($_, $item->{$_}), "\n";
415             }
416             }
417             print "";
418             }
419              
420             # Regular playlists
421             elsif ($ref = $self->get_pl($_)) {
422             print "\t" . mktag("playlist", { name => $_ }, noend => 1) . "\n";
423             for (@$ref) {
424             print "\t\t";
425             if ($_ eq int $_) {
426             print mktag("add", { id => $_ });
427             }
428             elsif ($_->{exact}) {
429             my %write = %$_;
430             delete $write{exact};
431             print mktag("add", \%write);
432             }
433             elsif ($_->{nocase}) {
434             my %write = %$_;
435             delete $write{nocase};
436             print mktag("iregex", \%write);
437             }
438             else {
439             print mktag("regex", $_);
440             }
441             print "\n";
442             }
443             print "\t\n";
444             }
445              
446             # Bad playlist entry--can't happen
447             else {
448             warnings::warnif("Unknown playlist $name");
449             next;
450             }
451              
452             }
453             print "\n";
454             close OUT ;
455              
456             # Restore original value of filehandle and stringify
457             select $oldfh;
458             }
459            
460             # Restore an iPod w/ corrupted dbs
461             sub restore {
462             my ($self, %opts) = @_;
463             if (not defined $self->{mnt}) {
464             croak "Can't restore iPod without mountpoint set";
465             }
466              
467             local $self->{move_files} = 0;
468             local $self->{allow_dup} = 1;
469             local $self->{restore} = 1;
470             $self->_clear;
471             $self->add_song(glob(File::Spec->catpath($self->{mnt}, "iPod_Control", "Music", "*", "*")));
472             }
473              
474             # Add a song to the ipod
475             sub add_song {
476             my ($self, @songs) = @_;
477             my @newids;
478              
479             foreach my $song (@songs) {
480             my $filename;
481             if (ref($song) eq 'HASH') {
482             $filename = $song->{filename};
483             }
484             else {
485             $filename = $song;
486             }
487             if (not defined $filename) {
488             warnings::warnif "Undefined filename";
489             next;
490             }
491              
492             # Get the magic hashref
493             my $fh = Mac::iPod::GNUpod::Utils::wtf_is($filename);
494             if (not $fh) {
495             warnings::warnif "$@, skipping '$song'";
496             next;
497             }
498              
499             # Update hashref w/ user-supplied info (if needed)
500             if (ref($song) eq 'HASH') {
501             $fh->{$_} = $song->{$_} for (keys %$song);
502             }
503              
504             # Get the path, etc.
505             ($fh->{path}, my $target) = $self->_getpath($filename);
506              
507             # Check for duplicates
508             unless ($self->allow_dup) {
509             if (my $dup = $self->_chkdup($fh)) {
510             warnings::warnif "'$song' is a duplicate of song $dup, skipping";
511             next;
512             }
513             }
514              
515             # Copy the file
516             if (defined $self->{mnt} and $self->move_files) {
517             File::Copy::copy($filename, $target) or do {
518             warnings::warnif "Couldn't copy $song to $target: $!, skipping";
519             next;
520             }
521             }
522              
523             # Add this to our list of files
524             push @newids, $self->_newfile($fh);
525             }
526              
527             return @newids if wantarray;
528             return $newids[0];
529             }
530              
531             # Remove a song from the ipod
532             sub rm_song {
533             my ($self, @songs) = @_;
534             my $rmcount = 0;
535              
536             foreach my $id (@songs) {
537             if (not exists $self->{files}->[$id]) {
538             warnings::warnif "No song with id $id";
539             next;
540             }
541              
542             if (defined $self->{mnt} and $self->move_files) {
543             my $path = $self->_realpath($self->{files}->[$id]->{path});
544             unless (unlink $path) {
545             warnings::warnif "Remove failed for song $id ($path): $!";
546             next;
547             }
548             }
549              
550              
551             my $gone = delete $self->{files}->[$id];
552             $rmcount++;
553              
554             # Get rid of index entries, dupdb
555             no warnings 'uninitialized';
556             delete $self->{dupdb}->{"$gone->{bitrate}/$gone->{time}/$gone->{filesize}"};
557             for (keys %{$self->{idx}}) {
558             my @list;
559             if (exists $self->{idx}{$_}{$gone->{$_}}) {
560             @list = @{$self->{idx}{$_}{$gone->{$_}}};
561             for (my $i = 0; $i < @list; $i++) {
562             if ($list[$i] eq $id) {
563             splice @list, $i, 1;
564             }
565             }
566             }
567             if (@list) {
568             $self->{idx}{$_}{$gone->{$_}} = \@list;
569             }
570             else {
571             delete $self->{idx}{$_}{$gone->{$_}};
572             }
573             }
574             }
575              
576             return $rmcount;
577             }
578              
579             # Get a song by id
580             sub get_song {
581             my ($self, @ids) = @_;
582             # Must make new hash to prevent tampering w/ internals
583             my @rv;
584             for (@ids) {
585             my $song = $self->{files}[$_];
586             if (ref $song eq 'HASH') {
587             my %song = %$song;
588             push @rv, \%song;
589             }
590             else {
591             push @rv, undef;
592             }
593             }
594             if (wantarray) {
595             return @rv;
596             }
597             else {
598             return $rv[0];
599             }
600             }
601              
602             # Get possible duplicates of a song by ID
603             sub get_dup {
604             my $self = shift;
605             my $fh = Mac::iPod::GNUpod::Utils::wtf_is(shift);
606             return $self->_chkdup($fh);
607             }
608              
609             sub _chkdup {
610             my ($self, $fh) = @_;
611             no warnings 'uninitialized';
612             return $self->{dupdb}->{"$fh->{bitrate}/$fh->{time}/$fh->{filesize}"};
613             }
614              
615             # Get the real path to a song by id
616             sub get_path {
617             my ($self, @ids) = @_;
618             return unless defined $self->{mnt};
619             return map { $self->_realpath($self->{files}->[$_]->{path}) } @ids if wantarray;
620             return $self->_realpath($self->{files}->[$ids[0]]->{path});
621             }
622              
623             # Get all songs
624             sub all_songs {
625             my $self = shift;
626             return grep { defined $self->{files}->[$_] } 1 .. $#{$self->{files}};
627             }
628              
629             # Add a pl to the ipod
630             sub add_pl {
631             my ($self, $name, @songs) = @_;
632             $name = Mac::iPod::GNUpod::Utils::getutf8($name);
633             warnings::warnif $@ if not defined $name;
634              
635             # Sets $self->{cur_pl}
636             $self->_addpl($name);
637              
638             $self->_addtopl($self->{cur_pl}, $_) for (@songs);
639            
640             # Prevent others from accidentally writing to this pl
641             $self->{cur_pl} = undef;
642              
643             return 1;
644             }
645              
646             # Get a pl by name
647             sub get_pl {
648             my ($self, @names) = @_;
649             my @rv;
650             for (@names) {
651             if (exists $self->{pl_idx}->{$_}) {
652             push @rv, $self->{pl_idx}->{$_};
653             }
654             else {
655             push @rv, undef;
656             }
657             }
658             return @rv if wantarray;
659             return $rv[0];
660             }
661              
662             # Get all playlists
663             sub all_pls {
664             my $self = shift;
665             return grep { defined $_ } @{$self->{plorder}};
666             }
667              
668             # Remove a pl by name
669             sub rm_pl {
670             my ($self, @names) = @_;
671             my $count;
672             for (@names) {
673             if (delete $self->{pl_idx}->{$_}) {
674             $count++;
675             for my $i (0 .. $#{$self->{plorder}}) {
676             no warnings 'uninitialized';
677             if ($self->{plorder}->[$i] eq $_) {
678             splice(@{$self->{plorder}}, $i, 1);
679             }
680             }
681             }
682             }
683             return $count;
684             }
685              
686             # Get an spl by name
687             sub get_spl {
688             my ($self, @names) = @_;
689             return @{$self->{spl_idx}}{@names} if wantarray;
690             return $self->{spl_idx}->{$names[0]};
691             }
692              
693             # Get a list of ids by search terms
694             sub search {
695             my ($self, %terms) = @_;
696              
697             # Pick opts out from terms
698             my %opts;
699             for ('nocase', 'nometachar', 'exact') {
700             $opts{$_} = delete $terms{$_};
701             }
702              
703             # Main searches
704             my %count;
705             my $term = 0;
706             while (my ($key, $val) = each %terms) {
707             for my $idxval (keys %{$self->{idx}->{$key}}) {
708             if (matches($idxval, $val, %opts)) {
709             $count{$_}++ for @{$self->{idx}->{$key}->{$idxval}};
710             }
711             }
712             $term++;
713             }
714              
715             # Get the list of everyone that matched
716             # Sort by Artist > Album > Cdnum > Songnum > Title
717             return
718             sort {
719             $self->{files}->[$a]->{uniq} cmp $self->{files}->[$b]->{uniq}
720             } grep {
721             $count{$_} == $term
722             } keys %count;
723             }
724              
725             # Clear the ipod db
726             sub _clear {
727             my $self = shift;
728             $self->{files} = [];
729             $self->{idx} = {};
730             $self->{plorder} = [];
731             $self->{pl_idx} = {};
732             $self->{spl_idx} = {};
733             }
734              
735             # Add a new file to db, create indices
736             sub _newfile {
737             my ($self, $file) = @_;
738              
739             # Find the first open index slot
740             my $idx = 1;
741             $idx++ while defined $self->{files}->[$idx];
742             $file->{id} = $idx;
743              
744             $self->_addfile($file);
745             }
746              
747             # Add info from a file in the db
748             sub _addfile {
749             my ($self, $file) = @_;
750             no warnings 'uninitialized';
751              
752             # Check for bad path
753             {
754             # Get the real path
755             my $rpath;
756             if ($self->{mnt} && $self->move_files) {
757             $rpath = $self->_realpath($file->{path});
758            
759             }
760             last if not $rpath;
761              
762             my $errst;
763             if (not -e $rpath) {
764             $errst = "File does not exist ($rpath)";
765             }
766             if (-d $rpath) {
767             $errst = "Path is a directory ($rpath)";
768             }
769             if ($errst) {
770             warnings::warnif $errst;
771             return;
772             }
773              
774             }
775            
776            
777             # Check for bad ids
778             {
779             my $badid;
780             my $errstr;
781             if ($file->{id} < 1) {
782             $file->{id} = 'MISSING' if not exists $file->{id};
783             warnings::warnif "Bad id ($file->{id}) for file";
784             $badid = 1;
785             }
786             elsif (defined $self->{files}->[$file->{id}]) {
787             warnings::warnif "Duplicate song id ($file->{id})";
788             $badid = 1;
789             }
790              
791             if ($badid) {
792             # Attempt to rescue w/ newfile (which re-assigns id)
793             if (my $r = $self->_newfile($file)) {
794             warnings::warnif " ...fixed";
795             # Note that this song does not have its original id
796             $self->{files}->[$r]->{notorig} = 1;
797             return $r;
798             }
799             # Getting here is bad failure
800             return;
801             }
802             }
803            
804             # Make duplicate index
805             $self->{dupdb}->{"$file->{bitrate}/$file->{time}/$file->{filesize}"} = $file->{id};
806              
807             # Add a uniq for sorting
808             $file->{uniq} = sprintf "%s|%s|%02d|%02d|%s|%s",
809             $file->{artist}, $file->{album}, $file->{cdnum}, $file->{songnum}, $file->{title}, $file->{path};
810              
811             # Make indexes, convert to utf8
812             for (keys %$file) {
813             # Don't index the id or uniq (redundant!)
814             next if $_ eq 'id' or $_ eq 'uniq';
815             push @{$self->{idx}->{$_}->{$file->{$_}}}, $file->{id};
816             $file->{$_} = Mac::iPod::GNUpod::Utils::getutf8($file->{$_});
817             warnings::warnif $@ if not defined $file->{$_};
818             }
819              
820             # Add to file index
821             $self->{files}->[$file->{id}] = $file;
822              
823             return $file->{id};
824             }
825              
826             # Add a playlist
827             sub _addpl {
828             my($self, $name, $opt) = @_;
829              
830             if($self->get_pl($name)) {
831             warnings::warnif "Playlist '$name' is a duplicate, not adding it";
832             return;
833             }
834             $self->{cur_pl} = $self->{pl_idx}->{$name} = [];
835             push(@{$self->{plorder}}, $name);
836             }
837              
838             # Add a smart playlist
839             sub _addspl {
840             my($self, $name, $opt) = @_;
841              
842            
843             if($self->get_spl($name)) {
844             warnings::warnif "Playlist '$name' is a duplicate, not adding it";
845             return;
846             }
847             $self->{spl_idx}->{$name}->{pref} = $opt;
848             $self->{cur_pl} = $self->{spl_idx}->{$name}->{data} = [];
849             push(@{$self->{plorder}}, $name);
850             }
851              
852             # Add a file to a playlist
853             sub _addtopl {
854             my ($self, $pl, $href) = @_;
855              
856             # ids added from add_pl
857             if (ref $href ne 'HASH') {
858             push @$pl, $href;
859             }
860             # tags from db
861             elsif (exists $href->{add}) {
862             if (exists $href->{add}->{id}) {
863             push @$pl, $href->{add}->{id};
864             }
865             else {
866             push @$pl, { %{$href->{add}}, exact => 1 };
867             }
868             }
869             # tags from db
870             elsif (exists $href->{regex}) {
871             push @$pl, $href->{regex};
872             }
873             # tags from db
874             elsif (exists $href->{iregex}) {
875             push @$pl, { %{$href->{iregex}}, nocase => 1 };
876             }
877             # Hash references from add_pl
878             else {
879             push @$pl, $href;
880             }
881             }
882              
883             # create a spl
884             sub _render_spl {
885             my($self, $name, $pref, $data, $mr, $content) = @_;
886             my $of = {};
887             $of->{liveupdate} = $pref->{live};
888             $of->{moselected} = $pref->{mos};
889             $of->{matchany} = $mr;
890             $of->{limitsort} = $pref->{isort};
891             $of->{limitval} = $pref->{value};
892             $of->{limititem} = $pref->{iitem};
893             $of->{checkrule} = $pref->{checkrule};
894              
895             #create this playlist
896             $self->_addspl($name, $of);
897             }
898              
899             # Create a filled-out pl (replacing 'add' and 'regex' entries w/ ids)
900             sub render_pl {
901             my ($self, $name) = @_;
902             my @list;
903              
904             for my $item (@{$self->{pl_idx}->{$name}}) {
905             # Exact id numbers
906             if (int $item eq $item) {
907             push @list, $item;
908             }
909             else {
910             push @list, $self->search(%$item);
911             }
912             }
913             return @list;
914             }
915              
916             #Get all playlists
917             sub _getpl_names {
918             my $self = shift;
919             return @{$self->{plorder}};
920             }
921              
922             # Call events (event handler for XML::Parser)
923             sub _eventer {
924             my $self = shift;
925             my($href, $el, %it) = @_;
926             no warnings 'uninitialized';
927              
928             return undef unless $href->{Context}[0] eq "gnuPod";
929              
930             # Warnings for elements that should have attributes
931             if ( ( $href->{Context}[1] eq 'files'
932             || $href->{Context}[1] eq 'playlist'
933             || $href->{Context}[1] eq 'smartplaylist')
934             && not keys %it) {
935             warnings::warnif "No attributes found for <$el /> tag";
936             return;
937             }
938              
939             # Convert all to utf8
940             for (keys %it) {
941             $it{$_} = Unicode::String::utf8($it{$_})->utf8;
942             }
943              
944             # tags
945             if($href->{Context}[1] eq "files") {
946             if ($el eq 'file') {
947             $self->_addfile(\%it);
948             }
949             else {
950             warnings::warnif "Found improper <$el> tag inside tag";
951             }
952             }
953              
954             # tags
955             elsif($href->{Context}[1] eq "" && $el eq "playlist") {
956             $it{name} = "NONAME" unless $it{name};
957             $self->_addpl($it{name});
958             }
959              
960             # tags inside playlist
961             elsif($href->{Context}[1] eq "playlist") {
962             $self->_addtopl($self->{cur_pl}, { $el => \%it });
963             }
964              
965             # tags
966             elsif($href->{Context}[1] eq "" && $el eq "smartplaylist") {
967             $it{name} = "NONAME" unless $it{name};
968             $self->_addspl($it{name}, \%it);
969             }
970              
971             # tags inside smartplaylist
972             elsif($href->{Context}[1] eq "smartplaylist") {
973             if (not keys %it) {
974             warnings::warnif "No attributes found for <$el /> tag";
975             return;
976             }
977             $self->_addtopl($self->{cur_pl}, { $el => \%it });
978             }
979             }
980              
981             # Get an iPod-safe path for filename
982             sub _getpath {
983             my($self, $filename) = @_;
984             my $path;
985              
986             if (not $self->move_files) { #Don't create a new filename..
987             $path = $filename;
988             }
989              
990             else { #Default action.. new filename to create
991             my $name = (File::Spec->splitpath($filename))[2];
992             $name =~ tr/a-zA-Z0-9\./_/c;
993             #Search a place for the MP3 file
994             for(my $i = 0;; $i++) {
995             my $dir = sprintf("f%02d", int(rand(20)));
996             my $fname = sprintf("%d_$name", $i);
997             $path = File::Spec->catfile($self->{mnt}, "iPod_Control", "Music", $dir, $fname);
998             last unless -e $path;
999             }
1000             }
1001              
1002             # Now break the $ipath into pieces and remake it Mac style Make a path
1003             # Get the ipod-relative path
1004             my $relpath = File::Spec->abs2rel($path, $self->{mnt});
1005             my @pieces = File::Spec->splitpath($relpath);
1006             my @dirs = File::Spec->splitdir($pieces[1]);
1007             my $ipath = File::Spec::Mac->catfile(@dirs, $pieces[2]);
1008              
1009             return ($ipath, $path);
1010             }
1011              
1012             # Convert an ipod path to Unix
1013             sub _realpath {
1014             my ($self, $ipath) = @_;
1015             no warnings 'uninitialized';
1016             my @list = split /:/, $ipath;
1017             return File::Spec->catfile($self->{mnt}, @list);
1018             }
1019              
1020             1;
1021              
1022             __END__