File Coverage

lib/Mac/iTunes/Library/XML.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             package Mac::iTunes::Library::XML;
2              
3 2     2   24166 use 5.006;
  2         7  
  2         84  
4 2     2   10 use warnings;
  2         4  
  2         52  
5 2     2   11 use strict;
  2         3  
  2         84  
6 2     2   9 use Carp;
  2         5  
  2         140  
7              
8 2     2   557 use Mac::iTunes::Library;
  2         5  
  2         84  
9 2     2   649 use Mac::iTunes::Library::Item;
  2         4  
  2         76  
10 2     2   967 use Mac::iTunes::Library::Playlist;
  2         5  
  2         165  
11 2     2   3052 use XML::Parser 2.36;
  0            
  0            
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our %EXPORT_TAGS = ( 'all' => [ qw() ] );
16             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
17             our @EXPORT = qw( );
18              
19             our $VERSION = '1.0';
20              
21             =head1 NAME
22              
23             Mac::iTunes::Library::XML - Perl extension for parsing an iTunes XML library
24              
25             =head1 SYNOPSIS
26              
27             use Mac::iTunes::Library::XML;
28              
29             my $library = Mac::iTunes::Library::XML->parse( 'iTunes Music Library.xml' );
30             print "This library has only " . $library->num() . "item.\n";
31              
32             =head1 DESCRIPTION
33              
34             A parser to read an iTunes XML library and create a Mac::iTunes::Library object.
35              
36             =head2 NOTES ON iTUNES XML FORMAT
37              
38             Whereas someone who understands how to use XML would write this:
39              
40            
41            
42             Library
43             7
44             false
45            
46             14
47             21
48             28
49            
50            
51            
52              
53             Instead, we get this from iTunes:
54              
55             Playlists
56            
57            
58             NameLibrary
59             Master
60             Playlist ID201
61             Playlist Persistent ID707F6A2CE6E601F5
62             Visible
63             All Items
64             Playlist Items
65            
66            
67             Track ID173
68            
69            
70             Track ID175
71            
72            
73             Track ID177
74            
75            
76            
77            
78              
79             The iTunes XML format doesn't make it clear where the parser is in the library,
80             so to parser must keep track itself; this is done with the @stack array in
81             XML.pm, which is used to set $depth in each of the callback methods.
82              
83             Here are the elements that can be found at any depth. The depths are indexed
84             with 0 being outside of any element (before the very first start_element call),
85             1 would be within a single element ( being the outermost of an iTunes
86             library file), 2 within the second element (), and so on. Note that
87             because the iTunes XML library format is so awesome, the name of a key
88             (contained within a element, e.g. Features) occurs at the same
89             level as it's value (e.g. 5). Those XML elements
90             (e.g. ,
91             (e.g. 'Features', 5) are contained at level n+1.
92              
93             =over 4
94              
95             =item * Zeroth
96              
97             - element with version attribute
98              
99             =item * First
100              
101             - Outermost element
102              
103             =item * Second
104              
105             - containing library metadata key name
106             - containing library metadata
107             - containing library metadata
108             - containing library metadata
109             - containing library metadata
110              
111             =item * Third
112              
113             - Library metadata (major/minor version, application version, etc.)
114             - Tracks and Playlists keys
115             - containing library tracks
116             - containing playlists
117              
118             =item * Fourth
119              
120             - with track ID
121             - containing track data
122              
123             =item * Fifth
124              
125             - containing track/playlist metdata key name
126             - containing track/playlist metdata key name
127             - containing track/playlist metdata key name
128             - containing track/playlist metdata key name
129              
130             =item * Sixth
131              
132             - containing a single playlist track
133              
134             =item * Seventh
135              
136             - containing the string "Track ID"
137             - containing a track ID
138              
139             =back
140              
141             =head1 EXPORT
142              
143             None by default.
144              
145             =head1 METHODS
146              
147             =head2 parse( $libraryFile )
148              
149             Parses an iTunes XML library and returns a Mac::iTunes::Library object.
150              
151             =cut
152              
153             # The current 'key' of an item information that we're in
154             my $curKey = undef;
155             # A Mac::iTunes::Library::Item that will be built and added to the library
156             my $item = undef;
157             # A Mac::iTunes::Library that will be built
158             my $library;
159             # Characters that we collect
160             my $characters = undef;
161             # Keep track of where we are; push on each element name as we hit it
162             my (@stack);
163             my ($inTracks, $inPlaylists, $inMajorVersion, $inMinorVersion,
164             $inApplicationVersion, $inFeatures, $inMusicFolder,
165             $inLibraryPersistentID) = undef;
166              
167             sub parse {
168             my $self = shift;
169             my $xmlFile = shift;
170             $library = Mac::iTunes::Library->new();
171              
172             my $parser = XML::Parser->new( Handlers => {
173             Start => \&start_element,
174             End => \&end_element,
175             Char => \&characters,
176             });
177             $parser->parsefile( $xmlFile );
178             return $library;
179             } #parse
180              
181             ### Parser start element
182             sub start_element {
183             my ($expat, $element, %attrs) = @_;
184              
185             # Keep a trail of our depth
186             push @stack, $element;
187             # Note that our $depth is inside of the newly opened element
188             my $depth = scalar(@stack);
189              
190             if ($depth == 0) {
191             } elsif ($depth == 1) {
192             # Hit the initial tag
193             if (defined $attrs{'version'}) {
194             $library->version($attrs{'version'});
195             }
196             } elsif ($depth == 2) {
197             } elsif ($depth == 3) {
198             if($inPlaylists) {
199             } else {
200             if (($element eq 'true') or ($element eq 'false')) {
201             $library->showContentRatings($element);
202             }
203             }
204             } elsif ($depth == 4) {
205             # We hit a new item in the XML; create a new object
206             if($inPlaylists) {
207             $item = Mac::iTunes::Library::Playlist->new() if ($element eq 'dict');
208             } else {
209             $item = Mac::iTunes::Library::Item->new() if ($element eq 'dict');
210             }
211             } elsif($depth == 5){
212             }
213             } #start_element
214              
215             ### Parser end element
216             sub end_element {
217             my ($expat, $element) = @_;
218              
219             # Note that our $depth is still "inside" this ending element
220             my $depth = scalar(@stack);
221             # Hit a closing element; prune the trail
222             pop @stack;
223              
224             if ($depth == 0) { # plist version
225             } elsif ($depth == 1) { # dict
226             } elsif ($depth == 2) {
227             } elsif ($depth == 3) {
228             # Exiting a major section
229             $inTracks = 0 if ($element eq 'dict');
230             $inPlaylists = 0 if ($element eq 'array');
231              
232             if ($inMusicFolder and ($element eq 'string')) {
233             $library->musicFolder($characters);
234             $inMusicFolder = undef;
235             $curKey = undef;
236             $characters = undef;
237             }
238             } elsif ($depth == 4) {
239             # Ending an item; add it to the library and clean up
240             if( $inPlaylists ){
241             if ($item) {
242             $library->addPlaylist($item);
243             }
244             } else {
245             if ($item) {
246             $library->add($item);
247             }
248             }
249              
250             $item = undef if ($element eq 'dict');
251             } elsif ($depth == 5) {
252             # Set the attributes of the Mac::iTunes::Library::Item directly
253             if ( $element =~ /(integer|string|date|data)/ ) {
254             $item->{$curKey} = $characters;
255             $characters = undef;
256             $curKey = undef;
257             } elsif ( $element =~ /true/ ) {
258             $item->{$curKey} = 1;
259             $curKey = undef;
260             } elsif ( $element =~ /false/ ) {
261             $item->{$curKey} = 0;
262             $curKey = undef;
263             }
264             } elsif ($depth == 6){
265             } elsif ($depth == 7){
266             if ( $element =~ /(integer)/ ) {
267             # print "Adding $curKey => $characters\n";
268              
269             my $track = $library->{'ItemsById'}{$characters};
270             if( ref $track and $$track ){
271             $item->addItem( $$track );
272             } else {
273             warn "Couldn't find track '$characters'\n";
274             }
275              
276             $curKey = undef;
277             $characters = undef;
278             }
279             }
280             } #end_element
281              
282             ### Parser element contents
283             sub characters {
284             my ($expat, $string) = @_;
285             my $depth = scalar(@stack);
286              
287             if ( $depth == 0 ) { # plist version
288             } elsif ( $depth == 1 ) { # dict
289             } elsif ( $depth == 2 ) {
290             } elsif ( $depth == 3 ) {
291             # Check the name of the element
292             if ( $stack[$#stack] eq 'key' ) {
293             # Lots of keys at this level
294             if ($string eq 'Major Version') {
295             $inMajorVersion = 1;
296             } elsif ( $string eq 'Minor Version' ) {
297             $inMinorVersion = 1;
298             } elsif ( $string eq 'Application Version' ) {
299             $inApplicationVersion = 1;
300             } elsif ( $string eq 'Features' ) {
301             $inFeatures = 1;
302             } elsif ( $string eq 'Music Folder' ) {
303             $inMusicFolder = 1;
304             } elsif ( $string eq 'Library Persistent ID' ) {
305             $inLibraryPersistentID = 1;
306             } elsif ( $string eq 'Tracks' ) {
307             $inTracks = 1;
308             } elsif ( $string eq 'Playlists' ) {
309             $inPlaylists = 1;
310             }
311             } elsif ( $stack[$#stack] =~ /(integer|string|true|false)/ ) {
312             # TODO This is assumes that each of these come as a single chunk
313             if ($inMajorVersion) {
314             $library->majorVersion($string);
315             $inMajorVersion = undef;
316             } elsif ($inMinorVersion) {
317             $library->minorVersion($string);
318             $inMinorVersion = undef;
319             } elsif ($inApplicationVersion) {
320             $library->applicationVersion($string);
321             $inApplicationVersion = undef;
322             } elsif ($inFeatures) {
323             $library->features($string);
324             $inFeatures = undef;
325             } elsif ($inMusicFolder) {
326             # The music folder could be long; buffer it.
327             $characters .= $string;
328             } elsif ($inLibraryPersistentID) {
329             $library->libraryPersistentID($string);
330             $inLibraryPersistentID = undef;
331             }
332             }
333             } elsif ( $depth == 4 ) {
334             } elsif ( $depth == 5 ) {
335             if ( $stack[$#stack] eq 'key' ) {
336             # Grab the key's name; Normally comes in a single chunk, but accept multiple chunks
337             $curKey .= $string;
338             } elsif ( $stack[$#stack] =~ /(integer|string|date|data)/ ) {
339             # Append it to the characters that we've gathered so far
340             $characters .= $string;
341             }
342             } elsif ( $depth == 6 ) {
343             } elsif ( $depth == 7 ) {
344             if ( $stack[$#stack] eq 'key' ) {
345             # Grab the key's name; Normally comes in a single chunk, but accept multiple chunks
346             $curKey .= $string;
347             } elsif ( $stack[$#stack] =~ /(integer|string|date)/ ) {
348             # Append it to the characters that we've gathered so far
349             $characters .= $string;
350             }
351             }
352             } #characters
353              
354             # Clean up
355             sub DESTROY {
356             # Nothing to do.
357             } #DESTROY
358              
359             1;
360              
361             =head1 SEE ALSO
362              
363             L, L,
364             L
365              
366             =head1 AUTHOR
367              
368             Drew Stephens , http://dinomite.net
369              
370             =head1 CONTRIBUTORS
371              
372             =over4
373              
374             =item *
375              
376             Mark Grimes , L
377              
378             =item *
379              
380             Garrett Scott , L
381              
382             =back
383              
384             =head1 SOURCE REPOSITORY
385              
386             http://mac-itunes.googlecode.com
387              
388             =head1 SVN INFO
389              
390             $Revision: 90 $
391              
392             =head1 COPYRIGHT AND LICENSE
393              
394             Copyright (C) 2007-2008 by Drew Stephens
395              
396             This library is free software; you can redistribute it and/or modify
397             it under the same terms as Perl itself, either Perl version 5.8.8 or,
398             at your option, any later version of Perl 5 you may have available.
399              
400             =cut
401             __END__