123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- #------------------------------------------------------------------------------
- # File: Audible.pm
- #
- # Description: Read metadata from Audible audio books
- #
- # Revisions: 2015/04/05 - P. Harvey Created
- #
- # References: 1) https://github.com/jteeuwen/audible
- # 2) https://code.google.com/p/pyaudibletags/
- # 3) http://wiki.multimedia.cx/index.php?title=Audible_Audio
- #------------------------------------------------------------------------------
- package Image::ExifTool::Audible;
- use strict;
- use vars qw($VERSION);
- use Image::ExifTool qw(:DataAccess :Utils);
- $VERSION = '1.02';
- sub ProcessAudible_meta($$$);
- sub ProcessAudible_cvrx($$$);
- %Image::ExifTool::Audible::Main = (
- GROUPS => { 2 => 'Audio' },
- NOTES => q{
- ExifTool will extract any information found in the metadata dictionary of
- Audible .AA files, even if not listed in the table below.
- },
- # tags found in the metadata dictionary (chunk 2)
- pubdate => { Name => 'PublishDate', Groups => { 2 => 'Time' } },
- pub_date_start => { Name => 'PublishDateStart', Groups => { 2 => 'Time' } },
- author => { Name => 'Author', Groups => { 2 => 'Author' } },
- copyright => { Name => 'Copyright', Groups => { 2 => 'Author' } },
- # also seen (ref PH):
- # product_id, parent_id, title, provider, narrator, price, description,
- # long_description, short_title, is_aggregation, title_id, codec, HeaderSeed,
- # EncryptedBlocks, HeaderKey, license_list, CPUType, license_count, <12 hex digits>,
- # parent_short_title, parent_title, aggregation_id, short_description, user_alias
- # information extracted from other chunks
- _chapter_count => { Name => 'ChapterCount' }, # from chunk 6
- _cover_art => { # from chunk 11
- Name => 'CoverArt',
- Groups => { 2 => 'Preview' },
- Binary => 1,
- },
- );
- # 'tags' atoms observed in Audible .m4b audio books (ref PH)
- %Image::ExifTool::Audible::tags = (
- GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
- NOTES => 'Information found in "tags" atom of Audible M4B audio books.',
- meta => {
- Name => 'Audible_meta',
- SubDirectory => { TagTable => 'Image::ExifTool::Audible::meta' },
- },
- cvrx => {
- Name => 'Audible_cvrx',
- SubDirectory => { TagTable => 'Image::ExifTool::Audible::cvrx' },
- },
- tseg => {
- Name => 'Audible_tseg',
- SubDirectory => { TagTable => 'Image::ExifTool::Audible::tseg' },
- },
- );
- # 'meta' information observed in Audible .m4b audio books (ref PH)
- %Image::ExifTool::Audible::meta = (
- PROCESS_PROC => \&ProcessAudible_meta,
- GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
- NOTES => 'Information found in Audible M4B "meta" atom.',
- Album => 'Album',
- ALBUMARTIST => { Name => 'AlbumArtist', Groups => { 2 => 'Author' } },
- Artist => { Name => 'Artist', Groups => { 2 => 'Author' } },
- Comment => 'Comment',
- Genre => 'Genre',
- itunesmediatype => { Name => 'iTunesMediaType', Description => 'iTunes Media Type' },
- SUBTITLE => 'Subtitle',
- Title => 'Title',
- TOOL => 'CreatorTool',
- Year => { Name => 'Year', Groups => { 2 => 'Time' } },
- track => 'ChapterName', # (found in 'meta' of 'tseg' atom)
- );
- # 'cvrx' information observed in Audible .m4b audio books (ref PH)
- %Image::ExifTool::Audible::cvrx = (
- PROCESS_PROC => \&ProcessAudible_cvrx,
- GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
- NOTES => 'Audible cover art information in M4B audio books.',
- VARS => { NO_ID => 1 },
- CoverArtType => 'CoverArtType',
- CoverArt => {
- Name => 'CoverArt',
- Groups => { 2 => 'Preview' },
- Binary => 1,
- },
- );
- # 'tseg' information observed in Audible .m4b audio books (ref PH)
- %Image::ExifTool::Audible::tseg = (
- GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
- tshd => {
- Name => 'ChapterNumber',
- Format => 'int32u',
- ValueConv => '$val + 1', # start counting from 1
- },
- meta => {
- Name => 'Audible_meta2',
- SubDirectory => { TagTable => 'Image::ExifTool::Audible::meta' },
- },
- );
- #------------------------------------------------------------------------------
- # Process Audible 'meta' tags from M4B files (ref PH)
- # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
- # Returns: 1 on success
- sub ProcessAudible_meta($$$)
- {
- my ($et, $dirInfo, $tagTablePtr) = @_;
- my $dataPt = $$dirInfo{DataPt};
- my $dataPos = $$dirInfo{DataPos};
- my $dirLen = length $$dataPt;
- return 0 if $dirLen < 4;
- my $num = Get32u($dataPt, 0);
- $et->VerboseDir('Audible_meta', $num);
- my $pos = 4;
- my $index;
- for ($index=0; $index<$num; ++$index) {
- last if $pos + 3 > $dirLen;
- my $unk = Get8u($dataPt, $pos); # ? (0x80 or 0x00)
- last unless $unk eq 0x80 or $unk eq 0x00;
- my $len = Get16u($dataPt, $pos + 1); # tag length
- $pos += 3;
- last if $pos + $len + 6 > $dirLen or not $len;
- my $tag = substr($$dataPt, $pos, $len); # tag ID
- my $ver = Get16u($dataPt, $pos + $len); # version?
- last unless $ver eq 0x0001;
- my $size = Get32u($dataPt, $pos + $len + 2);# data size
- $pos += $len + 6;
- last if $pos + $size > $dirLen;
- my $val = $et->Decode(substr($$dataPt, $pos, $size), 'UTF8');
- unless ($$tagTablePtr{$tag}) {
- my $name = Image::ExifTool::MakeTagName(($tag =~ /[a-z]/) ? $tag : lc($tag));
- AddTagToTable($tagTablePtr, $tag, { Name => $name });
- }
- $et->HandleTag($tagTablePtr, $tag, $val,
- DataPt => $dataPt,
- DataPos => $dataPos,
- Start => $pos,
- Size => $size,
- Index => $index,
- );
- $pos += $size;
- }
- return 1;
- }
- #------------------------------------------------------------------------------
- # Process Audible 'cvrx' cover art atom from M4B files (ref PH)
- # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
- # Returns: 1 on success
- sub ProcessAudible_cvrx($$$)
- {
- my ($et, $dirInfo, $tagTablePtr) = @_;
- my $dataPt = $$dirInfo{DataPt};
- my $dataPos = $$dirInfo{DataPos};
- my $dirLen = length $$dataPt;
- return 0 if 0x0a > $dirLen;
- my $len = Get16u($dataPt, 0x08);
- return 0 if 0x0a + $len + 6 > $dirLen;
- my $size = Get32u($dataPt, 0x0a + $len + 2);
- return 0 if 0x0a + $len + 6 + $size > $dirLen;
- $et->VerboseDir('Audible_cvrx', undef, $dirLen);
- $et->HandleTag($tagTablePtr, 'CoverArtType', undef,
- DataPt => $dataPt,
- DataPos => $dataPos,
- Start => 0x0a,
- Size => $len,
- );
- $et->HandleTag($tagTablePtr, 'CoverArt', undef,
- DataPt => $dataPt,
- DataPos => $dataPos,
- Start => 0x0a + $len + 6,
- Size => $size,
- );
- return 1;
- }
- #------------------------------------------------------------------------------
- # Read information from an Audible .AA file
- # Inputs: 0) ExifTool ref, 1) dirInfo ref
- # Returns: 1 on success, 0 if this wasn't a valid AA file
- sub ProcessAA($$)
- {
- my ($et, $dirInfo) = @_;
- my $raf = $$dirInfo{RAF};
- my ($buff, $toc, $entry, $i);
- # check magic number
- return 0 unless $raf->Read($buff, 16) == 16 and $buff=~/^.{4}\x57\x90\x75\x36/s;
- # check file size
- if (defined $$et{VALUE}{FileSize}) {
- # first 4 bytes of the file should be the filesize
- unpack('N', $buff) == $$et{VALUE}{FileSize} or return 0;
- }
- $et->SetFileType();
- SetByteOrder('MM');
- my $bytes = 12 * Get32u(\$buff, 8); # table of contents size in bytes
- $bytes > 0xc00 and $et->Warn('Invalid TOC'), return 1;
- # read the table of contents
- $raf->Read($toc, $bytes) == $bytes or $et->Warn('Truncated TOC'), return 1;
- my $tagTablePtr = GetTagTable('Image::ExifTool::Audible::Main');
- # parse table of contents (in $toc)
- for ($entry=0; $entry<$bytes; $entry+=12) {
- my $type = Get32u(\$toc, $entry);
- next unless $type == 2 or $type == 6 or $type == 11;
- my $offset = Get32u(\$toc, $entry + 4);
- my $length = Get32u(\$toc, $entry + 8) or next;
- $raf->Seek($offset, 0) or $et->Warn("Chunk $type seek error"), last;
- if ($type == 6) { # offset table
- next if $length < 4 or $raf->Read($buff, 4) != 4; # only read the chapter count
- $et->HandleTag($tagTablePtr, '_chapter_count', Get32u(\$buff, 0));
- next;
- }
- # read the chunk
- $length > 100000000 and $et->Warn("Chunk $type too big"), next;
- $raf->Read($buff, $length) == $length or $et->Warn("Chunk $type read error"), last;
- if ($type == 11) { # cover art
- next if $length < 8;
- my $len = Get32u(\$buff, 0);
- my $off = Get32u(\$buff, 4);
- next if $off < $offset + 8 or $off - $offset + $len > $length;
- $et->HandleTag($tagTablePtr, '_cover_art', substr($buff, $off-$offset, $len));
- next;
- }
- # parse metadata dictionary (in $buff)
- $length < 4 and $et->Warn('Bad dictionary'), next;
- my $num = Get32u(\$buff, 0);
- $num > 0x200 and $et->Warn('Bad dictionary count'), next;
- my $pos = 4; # dictionary starts immediately after count
- require Image::ExifTool::HTML; # (for UnescapeHTML)
- $et->VerboseDir('Audible Metadata', $num);
- for ($i=0; $i<$num; ++$i) {
- my $tagPos = $pos + 9; # position of tag string
- $tagPos > $length and $et->Warn('Truncated dictionary'), last;
- # (1 unknown byte ignored at start of each dictionary entry)
- my $tagLen = Get32u(\$buff, $pos + 1); # tag string length
- my $valLen = Get32u(\$buff, $pos + 5); # value string length
- my $valPos = $tagPos + $tagLen; # position of value string
- my $nxtPos = $valPos + $valLen; # position of next entry
- $nxtPos > $length and $et->Warn('Bad dictionary entry'), last;
- my $tag = substr($buff, $tagPos, $tagLen);
- my $val = substr($buff, $valPos, $valLen);
- unless ($$tagTablePtr{$tag}) {
- my $name = Image::ExifTool::MakeTagName($tag);
- $name =~ s/_(.)/\U$1/g; # change from underscore-separated to mixed case
- AddTagToTable($tagTablePtr, $tag, { Name => $name });
- }
- # unescape HTML character references and convert from UTF-8
- $val = $et->Decode(Image::ExifTool::HTML::UnescapeHTML($val), 'UTF8');
- $et->HandleTag($tagTablePtr, $tag, $val,
- DataPos => $offset,
- DataPt => \$buff,
- Start => $valPos,
- Size => $valLen,
- Index => $i,
- );
- $pos = $nxtPos; # step to next dictionary entry
- }
- }
- return 1;
- }
- 1; # end
- __END__
- =head1 NAME
- Image::ExifTool::Audible - Read meta information from Audible audio books
- =head1 SYNOPSIS
- This module is used by Image::ExifTool
- =head1 DESCRIPTION
- This module contains definitions required by Image::ExifTool to read meta
- information from Audible audio books.
- =head1 AUTHOR
- Copyright 2003-2016, Phil Harvey (phil at owl.phy.queensu.ca)
- This library is free software; you can redistribute it and/or modify it
- under the same terms as Perl itself.
- =head1 REFERENCES
- =over 4
- =item L<https://github.com/jteeuwen/audible>
- =item L<https://code.google.com/p/pyaudibletags/>
- =item L<http://wiki.multimedia.cx/index.php?title=Audible_Audio>
- =back
- =head1 SEE ALSO
- L<Image::ExifTool::TagNames/Audible Tags>,
- L<Image::ExifTool(3pm)|Image::ExifTool>
- =cut
|