Skip to main content

Notice

Please note that most of the software linked on this forum is likely to be safe to use. If you are unsure, feel free to ask in the relevant topics, or send a private message to an administrator or moderator. To help curb the problems of false positives, or in the event that you do find actual malware, you can contribute through the article linked here.
Topic: CLI Perl script for reading iTunes AAC tags (Read 1896 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

CLI Perl script for reading iTunes AAC tags

I'm playing with an application that should read FLAC, mp3 and AAC tags.
I have been unable to find a CLI for reading AAC tags such as DISCNUMBER, COMPILATION and GROUPING so I have made my own CLI.

It will only read the tags, no updating of tags have been planned. Options is inspired by metaflac and tag names by VORBIS comment and TGF.

Quote
Usage: metaaac <file>
       Display tags in aac file

       Options:
         --no-utf8-convert  Display texts as utf
         --version              Display version of metaaac
         --help                   Display this text
         --longhelp             List recognised tags
         --verbose             Display atoms
         --debug                Display debug information


Code: [Select]
use Encode;
use Encode 'is_utf8';
use Encode 'decode_utf8';

$version = "0.01";
%atom = (
 # container atoms
 ALB  => { CONTAINER => 1, TAG => ALBUM, FORMAT => TEXT },
 ART  => { CONTAINER => 1, TAG => ARTIST, FORMAT => TEXT },
 CLIP => { CONTAINER => 1},
 CMT  => { CONTAINER => 1, TAG => COMMENT, FORMAT => TEXT },
 COVR => { CONTAINER => 1, },
 CPIL => { CONTAINER => 1, TAG => COMPILATION, FORMAT => INTEGER },
 DAY  => { CONTAINER => 1, TAG => DATE, FORMAT => YEAR },
 DINF => { CONTAINER => 1},
 DISK => { CONTAINER => 1, TAG => DISCNUMBER, FORMAT => INTEGERS },
 DRMS => { CONTAINER => 1},
 EDTS => { CONTAINER => 1},
 GRP  => { CONTAINER => 1, TAG => GROUPING, FORMAT => TEXT },
 ILST => { CONTAINER => 1},
 MATT => { CONTAINER => 1},
 MDIA => { CONTAINER => 1},
 MINF => { CONTAINER => 1},
 MOOV => { CONTAINER => 1},
 NAM  => { CONTAINER => 1, TAG => TITLE, FORMAT => TEXT },
 RTNG => { CONTAINER => 1, TAG => RATING, FORMAT => INTEGER },
 SCHI => { CONTAINER => 1},
 SINF => { CONTAINER => 1},
 STBL => { CONTAINER => 1},
 TMPO => { CONTAINER => 1, TAG => TEMPO, FORMAT => INTEGER },
 TOO  => { CONTAINER => 1, TAG => ENCODER, FORMAT => TEXT },
 TRAK => { CONTAINER => 1, SKIP => 1},
 TRKN => { CONTAINER => 1, TAG => TRACKNUMBER, FORMAT => INTEGERS },
 UDTA => { CONTAINER => 1},
 WRT  => { CONTAINER => 1, TAG => COMPOSER, FORMAT => TEXT },

 APID => { CONTAINER => 1, TAG => USER, FORMAT => TEXT},
 PLID => { CONTAINER => 1},
 AART => { CONTAINER => 1, TAG => ARTIST, FORMAT => TEXT},
 GEID => { CONTAINER => 1},
 AKID => { CONTAINER => 1},
 ATID => { CONTAINER => 1},
 CPRT => { CONTAINER => 1, TAG => COPYRIGHT, FORMAT => TEXT },
 "----" => { CONTAINER => 1, TAG => ITUNES, FORMAT => ITUNES},
 GEN  => { CONTAINER => 1, TAG => GENRE, FORMAT => GENRE },
 GNRE => { CONTAINER => 1, TAG => GENRE, FORMAT => GENRE },
);

@genre = (
   "N/A", "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
   "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
   "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
   "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
   "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
   "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
   "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
   "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
   "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
   "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
   "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
   "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
   "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
   "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
   "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
   "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
   "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
   "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
   "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
   "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
   "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
   "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
   "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
   "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
   "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
   "SynthPop"
);

while (@ARGV and $ARGV[0] =~ /^--/) {
 if ($ARGV[0] =~ /^--version$/) {
   printf "Version %s\n", $version;
   exit 1;
 } elsif ($ARGV[0] =~ /^--help$/) {
   usage();
 } elsif ($ARGV[0] =~ /^--longhelp$/) {
   usage( 1);
 } elsif ($ARGV[0] =~ /^--no-utf8-convert$/) {
   $noutf = 1;
 } elsif ($ARGV[0] =~ /^--verbose$/) {
   $verbose = 1;
 } elsif ($ARGV[0] =~ /^--debug$/) {
   $verbose = 1;
   $debug = 1;
 } else {
   printf STDERR "Unknown option %s\n", $ARGV[0];
   usage();
 }
 shift;
}
$file = shift if @ARGV;

usage() if @ARGV or not defined $file;

unless (-r $file) {
 printf STDERR "Can't open file %s\n", $file;
 exit 1;
}

open FILE, $file or die "Can't open $file";
binmode FILE;
$size = (stat FILE)[7];
$blksize = (stat FILE)[11] || 16384;
parse_atoms( $size);

sub usage {
 printf STDERR "Usage: metaaac <file>\n";
 printf STDERR "       Display tags in aac file\n\n";
 printf STDERR "       Options:\n";
 printf STDERR "         --no-utf8-convert Display texts as utf\n";
 printf STDERR "         --version         Display version of metaaac\n";
 printf STDERR "         --help            Display this text\n";
 printf STDERR "         --longhelp        List recognised tags\n";
 printf STDERR "         --verbose         Display atoms\n";
 printf STDERR "         --debug           Display debug information\n";
 if (@_[0]) {
   printf STDERR "\n       The following tags are supported:\n";
   foreach $atom (sort {$atom{$a}{TAG} cmp $atom{$b}{TAG}} keys %atom) {
     next unless exists $atom{$atom}{TAG};
     printf STDERR "         %s=%s\n", $atom{$atom}{TAG}, lc $atom;
   }
 }
 exit 1;
}

sub readData {
 my $len = shift @_;
 my $read;
 if ($dataLength < $pos) {
   seek FILE, $pos - $dataLength, 1;
   $dataLength = $pos;
 }
 while ($dataLength < $pos + $len) {
   $read = sysread FILE, $data, $blksize, $dataLength;
   $dataLength += $read;
 }
}

sub print_atom {
 my $id = shift @_;
 my $len = shift @_;
 printf "%s%s: %d bytes\n", " " x (2 * $level), lc $id, $len + 8;
}

sub parse_mean {
 my $len = shift @_;
 readData( $len) if $dataLength < $pos + $len;
 $mean = substr( $data, $pos, $len);
 printf STDERR "Mean [%s]\n", $mean if $debug;
 $pos += $len;
 return 0;
}

sub parse_name {
 my $len = shift @_;
 readData( $len) if $dataLength < $pos + $len;
 $name = substr( $data, $pos, $len);
 printf "Name text [%s]\n", $name if $debug;
 $pos += $len;
 return 0;
}

sub parse_data {
 my $len = shift @_;
 readData( $len) if $dataLength < $pos + $len;
 ($type,$spare) = unpack "NN", substr( $data, $pos, 8);
 $len -= 8;
 $pos += 8;
 if ($len == 0) {
 } elsif ($type == 0) {
   @integer = unpack "n" x ($len / 2), substr( $data, $pos, $len);
   if ($debug) {
     printf "Integer tags";
     foreach $i (@integer) {
    printf " %d", $i;
     }
     printf "\n";
   }
   $pos += $len;
 } elsif ($type == 1) {
   $text = substr( $data, $pos, $len);
   $text = decode_utf8( $text) unless $noutf;
   $text = encode( "iso-8859-1", $text) unless $noutf;
   printf "Text tag [%s]\n", $text if $debug;
   $pos += $len;
 } elsif ($type == 13) { # picture data
   $pos += $len;
   printf "Picture\n", $text if $debug;
 } elsif ($type == 21) { # bytes
   @integer = unpack "C" x $len, substr( $data, $pos, $len);
   $pos += $len;
   if ($debug) {
     printf "Byte tags";
     foreach $i (@integer) {
    printf " %d", $i;
     }
     printf "\n";
   }
 } else {
   printf "Unknown data type\n" if $debug;
   $pos += $len;
 }
 return 0;
}

sub parse_atom {
 my $err;
 my $id;

 readData( 8) if $dataLength < $pos + 8;
 ($len,$id) = unpack "Na4", substr( $data, $pos, 8);
 $len -= 8;
 $pos += 8;
 $id =~ s/[^\w\-]//;
 $id =~ tr/a-z/A-Z/;

 print_atom( $id, $len) if $verbose;

 if ($atom{$id}{CONTAINER} and not $atom{$id}{SKIP}) {
   $err = parse_atoms( $len);
   return $err if $err;
 } elsif ($id eq "META") {
   $pos += 4;
   $err = parse_atoms( $len - 4);
   return $err if $err;
   return -1; # Parsing of meta data completed
 } elsif ($id eq "MEAN") {
   $pos += 4;
   $err = parse_mean( $len - 4);
   return $err if $err;
 } elsif ($id eq "NAME") {
   $pos += 4;
   $err = parse_name( $len - 4);
   return $err if $err;
 } elsif ($id eq "DATA") {
   $err = parse_data( $len);
   return $err if $err;
 } else {
   $pos += $len;
 }

 #
 # Print tag info
 #
 if (exists $atom{$id}{TAG}) {
   if ($atom{$id}{FORMAT} eq "TEXT") {
   } elsif ($atom{$id}{FORMAT} eq "GENRE") {
     if ($integer[0] and $integer[0] < @genre) {
    $text = $genre[$integer[0]];
     }
   } elsif ($atom{$id}{FORMAT} eq "YEAR") {
     $text = sprintf "%d", $text;
   } elsif ($atom{$id}{FORMAT} eq "ITUNES") {
     if (@integer) {
    for $i (@integer) {
  $text .= sprintf "%d ", $i;
    }
     }
     $atom{$id}{TAG} = uc $name;
   } elsif ($atom{$id}{FORMAT} eq "INTEGER") {
     $text = sprintf "%d", $integer[0];
   } elsif ($atom{$id}{FORMAT} eq "INTEGERS") {
     $text = sprintf "%d", $integer[1];
     $text .= sprintf "/%d", $integer[2] if $integer[2];
   } else {
     $text = "Missing format";
   }
   printf "%s=%s\n", $atom{$id}{TAG}, $text;
   undef $text;
   undef @integer;
 }
 return 0;
}

sub parse_atoms {
 my $len = shift @_;
 my ($err, $end);
 $level++;
 $end = $pos + $len;
 while ($pos < $end) {
   $err = parse_atom( $len);
   return $err if $err;
 }
 $level--;
 return 0;
}


In order to execute a Perl script you will need Perl on you computer.
For Windows I recommend ActiveState Perl, available here (cost free)

    ActivePerl download

Genre list thanks to tg.
Thanks to hymn for inspiration.