A Script To Convert iTunes XML To Tab Delimited Format

J

jean

Here is a script written in PERL that will take the library.xml file
that iTunes outputs in an export and cleans it up. The output from this
script is a clean Text file with the fields TAB delimited.

The script just takes to parameters. The input XML file and the output
text file you want it to write.

Enjoy

Posted By: Jean-Marie Vaneskahian

----------- Begin PERL Code Here -----------
MAIN:
{
my($InputXMLFile, $OutputTXTFile) = @ARGV;
if (-e $InputXMLFile) {
open(File,$InputXMLFile);
open(FileOut,">$OutputTXTFile");
print FileOut join("\t", 'Album', 'Artist', 'Song', 'Track
Number', 'Genre', 'Running Time', 'File Size', 'File Type', 'File
Location') . "\n";
my(%RecordHash);
while (my $Line = <File>) {
chomp($Line);
$Line = &TrimAndDecode($Line);
if ($Line =~ /\<key\>\d+\<\/key\>/) {
if ($RecordHash{'Song'} ne "") {
if ($RecordHash{'Song'} eq "") {
$RecordHash{'Song'} = "Empty Field";
}
if ($RecordHash{'Artist'} eq "") {
$RecordHash{'Artist'} = "Empty Field";
}
if ($RecordHash{'Album'} eq "") {
$RecordHash{'Album'} = "Empty Field";
}
if ($RecordHash{'File Type'} eq "") {
$RecordHash{'File Type'} = "Empty Field";
}
if ($RecordHash{'Genre'} eq "") {
$RecordHash{'Genre'} = "Empty Field";
}
if ($RecordHash{'Track Number'} eq "") {
$RecordHash{'Track Number'} = "0";
}
if ($RecordHash{'File Size'} eq "") {
$RecordHash{'File Size'} = "0";
}
if ($RecordHash{'Running Time'} eq "") {
$RecordHash{'Running Time'} = "0";
}
my $RecordEntry = join("\t", $RecordHash{'Album'},
$RecordHash{'Artist'}, $RecordHash{'Song'}, $RecordHash{'Track
Number'}, $RecordHash{'Genre'}, $RecordHash{'Running Time'},
$RecordHash{'File Size'}, $RecordHash{'File Type'}, $RecordHash{'File
Location'});
print FileOut $RecordEntry . "\n";
}
undef(%RecordHash);
} elsif($Line =~ /\<key\>(.*)\<\/key\>\<.*\>(.*)\<\/.*\>/)
{
my $Field = $1;
my $Value = $2;
if ($Field =~ /Name/i) {
$RecordHash{'Song'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Artist/i) {
$RecordHash{'Artist'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Album/i) {
$RecordHash{'Album'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Genre/i) {
$RecordHash{'Genre'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Kind/i) {
$RecordHash{'File Type'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Size/i) {
$RecordHash{'File Size'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Total Time/i) {
$RecordHash{'Running Time'} =
&TrimAndDecode($Value);
} elsif ($Field =~ /Track Number/i) {
$RecordHash{'Track Number'} =
&TrimAndDecode($Value);
} elsif ($Field =~ /Location/i) {
$Value =~ s/file\:\/\/localhost\///;
$RecordHash{'File Location'} =
&TrimAndDecode($Value);
}
}
}
close(FileOut);
close(File);
}
}

sub TrimAndDecode
{
my($String);
($String) = @_;
$String =~ s/^\s+//;
$String =~ s/\s+$//;
$String =~ tr/+/ /;
$String =~ s/%([a-fA-F0-9]{2})/pack "H2", $1/eg;
$String =~ s/^\s+//;
$String =~ s/\s+$//;
return($String);
}

----------- End PERL Code Here -----------
 
T

Tad McClellan

Here is a script written in PERL


No it isn't.

It is written in Perl.

open(File,$InputXMLFile);


You should always, yes *always*, check the return value from open():

open(File, $InputXMLFile) or die "could not open '$InputXMLFile' $!";

if ($Line =~ /\<key\>\d+\<\/key\>/) {


Angle brackets are not special in regular expressions, so there
is no need to backslash them.

If you choose a delimiter other than slash, then you don't
have to backslash the slash either:

if ($RecordHash{'Song'} ne "") {
if ($RecordHash{'Song'} eq "") {


Control can never get here, so there is not much point in
putting code here...

if ($RecordHash{'Artist'} eq "") {
$RecordHash{'Artist'} = "Empty Field";
}
if ($RecordHash{'Album'} eq "") {
$RecordHash{'Album'} = "Empty Field";
}
if ($RecordHash{'File Type'} eq "") {
$RecordHash{'File Type'} = "Empty Field";
}
if ($RecordHash{'Genre'} eq "") {
$RecordHash{'Genre'} = "Empty Field";
}


You can replace all 4 of those ifs with:

foreach my $key ( qw/Artist Album Genre/, 'File Type' ) { #untested
$RecordHash{$key} = 'Empty Field' unless length $RecordHash{$key};
}

my $RecordEntry = join("\t", $RecordHash{'Album'},
$RecordHash{'Artist'}, $RecordHash{'Song'}, $RecordHash{'Track
Number'}, $RecordHash{'Genre'}, $RecordHash{'Running Time'},
$RecordHash{'File Size'}, $RecordHash{'File Type'}, $RecordHash{'File
Location'});


That can written more clearly using a "hash slice":

my $RecordEntry = join("\t", @RecordHash{'Album', 'Artist', 'Song',
'Track Number', 'Genre',
'Running Time', 'File Size',
'File Type', 'File Location'
};

if ($Field =~ /Name/i) {
$RecordHash{'Song'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Artist/i) {
$RecordHash{'Artist'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Album/i) {
$RecordHash{'Album'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Genre/i) {
$RecordHash{'Genre'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Kind/i) {
$RecordHash{'File Type'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Size/i) {
$RecordHash{'File Size'} = &TrimAndDecode($Value);
} elsif ($Field =~ /Total Time/i) {
$RecordHash{'Running Time'} =
&TrimAndDecode($Value);
} elsif ($Field =~ /Track Number/i) {
$RecordHash{'Track Number'} =
&TrimAndDecode($Value);


That can be written more clearly by using a hash to contain
the name mappings:

my %names = (
Name => 'Song',
Artist => 'Artist',
Album => 'Album',
Genre => 'Genre',
Kind => 'File Type',
Size => 'File Size',
'Total Time' => 'Running Time',
'Track Number' => 'Track Number'
);
foreach my $name (keys %names) {
$RecordHash{ $names{$name} } = TrimAndDecode($Value);
}
 
S

Sherm Pendley

$Line = &TrimAndDecode($Line);

In addition to what Tad said, you shouldn't use & to call subroutines
unless you know what it does and why you want to do that. The above
should be written as:

$line = TrimAndDecode($Line);

See "perldoc perlsub" for details. You should also update your reference
library - using & hasn't been the preferred way to call subs since Perl 4.

sherm--
 
J

John W. Krahn

Here is a script written in PERL that will take the library.xml file
that iTunes outputs in an export and cleans it up. The output from this
script is a clean Text file with the fields TAB delimited.

The script just takes to parameters. The input XML file and the output
text file you want it to write.

I assume you mean two parameters?


Your code looks more like BASIC then Perl. :-( A more Perlish version:

my ( $InputXMLFile, $OutputTXTFile ) = @ARGV;
my @Headers = ( 'Album', 'Artist', 'Song',
'Track Number', 'Genre', 'Running Time',
'File Size', 'File Type', 'File Location' );
my @Empty = ( 'Empty Field', 'Empty Field', 'Empty Field',
0, 'Empty Field', 0, 0, 'Empty Field' );

open FileIn, '<', $InputXMLFile or die "Cannot open '$InputXMLFile' $!";
open FileOut, '>', $OutputTXTFile or die "Cannot open '$OutputTXTFile' $!";

print FileOut join( "\t", @Headers ), "\n";

my %RecordHash;
@RecordHash{ @Headers } = @Empty;

while ( my $Line = <FileIn> ) {
for ( $Line ) {
tr/+/ /;
s/%([a-fA-F0-9]{2})/pack 'H2', $1/eg;
s/^\s+//;
s/\s+$//;
}

if ( $Line =~ m!<key>\d+</key>! ) {
if ( length $RecordHash{ Song } ) {
print FileOut join( "\t", @RecordHash{ @Headers } ), "\n";
}
@RecordHash{ @Headers } = @Empty;
}
elsif ( $Line =~ m!<key>([^<]*)</key><[^>]*>([^<]*)</[^>]*>! ) {
my $Field = $1;
s/^\s+//, s/\s+$// for my $Value = $2;

if ( $Field =~ /Name/i ) {
$RecordHash{ Song } = $Value;
}
elsif ( $Field =~ /Artist/i ) {
$RecordHash{ Artist } = $Value;
}
elsif ( $Field =~ /Album/i ) {
$RecordHash{ Album } = $Value;
}
elsif ( $Field =~ /Genre/i ) {
$RecordHash{ Genre } = $Value;
}
elsif ( $Field =~ /Kind/i ) {
$RecordHash{ 'File Type' } = $Value;
}
elsif ( $Field =~ /Size/i ) {
$RecordHash{ 'File Size' } = $Value;
}
elsif ( $Field =~ /Total Time/i ) {
$RecordHash{ 'Running Time' } = $Value;
}
elsif ( $Field =~ /Track Number/i ) {
$RecordHash{ 'Track Number' } = $Value;
}
elsif ( $Field =~ /Location/i ) {
$Value =~ s!file://localhost/!!;
$RecordHash{ 'File Location' } = $Value;
}
}

}

close FileOut;
close File;

__END__

:)

John
 
D

Dr.Ruud

John W. Krahn schreef:
A more Perlish version:
[...]
if ( $Field =~ /Name/i ) {
$RecordHash{ Song } = $Value;
}
[...]
elsif ( $Field =~ /Location/i ) {
$Value =~ s!file://localhost/!!;
$RecordHash{ 'File Location' } = $Value;
}

I would loop it:

#!/usr/bin/perl
use strict ;
use warnings ;

my %RecordHash ;

my @Field = (
[ 'Name', 'Song' ],
'Artist',
'Album',
'Genre',
[ 'Kind', 'File Type' ],
[ 'Size', 'File Size' ],
[ 'Total Time', 'Running Time' ],
'Track Number',
[ 'Location', 'File location', 1, 'file://localhost/' ],
) ;

my $Field = '--Location--' ;
my $Value = 'file://localhost/usr/bin/perl' ;

for (@Field)
{
my ($f, $k, $p) = ref() ? ($$_[0], $$_[1], $$_[2]||0)
: ($_, $_, 0) ;
print "f=$f= \t k=$k= \t p=$p=\n" ;
1 == $p and $Value =~ s/^$$_[3]//i ; # 1: remove initial string
if ($Field =~ /$f/i) { $RecordHash{ $k } = $Value }
}

print "\n" ;
print "$_\t: $RecordHash{$_}\n" for keys(%RecordHash) ;
__END__


I wonder if the "=~ /.../i" aren't actually "eq" tests,
in which case I would change them to "=~ /\A...\z/i"
or even to "lc() eq lc()".
 
J

jean

Thank you all VERY VERY VERY much for all the tips on making my code
better and also showing me how to improve my PERL coding skills in
general. I learned Perl after learning Basic and as a result I think
in Basic and convert to Perl.

I will try to improve my style. Thanks again.
Jean-Marie Vaneskahian
 
J

jean

the /.../i were meant to be a little more loose than a strict eq test.
I just wanted to see if there was the sting anywhere in the field
irrelevant of case.
 
G

Guest

(e-mail address removed) wrote:

: better and also showing me how to improve my PERL coding skills in

Just a kind reminder: There ain't no such thing^^^^. There is Perl,
the language, and perl, the interpreter.

: general. I learned Perl after learning Basic and as a result I think
: in Basic and convert to Perl.

Basic is the language with no difference between PRINT, Print and print,
if I'am not mistaken. Try this in Perl:

#!/usr/bin/perl
use warnings;
print "Hello World!\n";
Print "Hello World!\n";
PRINT "Hello World!\n";

With Perl, every bit in a character counts, no matter whether it is bit #6
or any other. This is actually a very good thing, you never have to worry
whether your distinction between uppercase and lowercase is made, or by some
quirk of the program, is not made when you least expect it. Hence it is good
to get away from Basic-ish, and perhaps also DOS-ish all caps typing.

Oliver.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,774
Messages
2,569,596
Members
45,140
Latest member
SweetcalmCBDreview
Top