File Coverage

blib/lib/Image/ExifTool/Audible.pm
Criterion Covered Total %
statement 61 106 57.5
branch 23 62 37.1
condition 8 21 38.1
subroutine 4 6 66.6
pod 0 3 0.0
total 96 198 48.4


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Audible.pm
3             #
4             # Description: Read metadata from Audible audio books
5             #
6             # Revisions: 2015/04/05 - P. Harvey Created
7             #
8             # References: 1) https://github.com/jteeuwen/audible
9             # 2) https://code.google.com/p/pyaudibletags/
10             # 3) http://wiki.multimedia.cx/index.php?title=Audible_Audio
11             #------------------------------------------------------------------------------
12              
13             package Image::ExifTool::Audible;
14              
15 1     1   4450 use strict;
  1         3  
  1         33  
16 1     1   5 use vars qw($VERSION);
  1         3  
  1         38  
17 1     1   6 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         1691  
18              
19             $VERSION = '1.02';
20              
21             sub ProcessAudible_meta($$$);
22             sub ProcessAudible_cvrx($$$);
23              
24             %Image::ExifTool::Audible::Main = (
25             GROUPS => { 2 => 'Audio' },
26             NOTES => q{
27             ExifTool will extract any information found in the metadata dictionary of
28             Audible .AA files, even if not listed in the table below.
29             },
30             # tags found in the metadata dictionary (chunk 2)
31             pubdate => { Name => 'PublishDate', Groups => { 2 => 'Time' } },
32             pub_date_start => { Name => 'PublishDateStart', Groups => { 2 => 'Time' } },
33             author => { Name => 'Author', Groups => { 2 => 'Author' } },
34             copyright => { Name => 'Copyright', Groups => { 2 => 'Author' } },
35             # also seen (ref PH):
36             # product_id, parent_id, title, provider, narrator, price, description,
37             # long_description, short_title, is_aggregation, title_id, codec, HeaderSeed,
38             # EncryptedBlocks, HeaderKey, license_list, CPUType, license_count, <12 hex digits>,
39             # parent_short_title, parent_title, aggregation_id, short_description, user_alias
40              
41             # information extracted from other chunks
42             _chapter_count => { Name => 'ChapterCount' }, # from chunk 6
43             _cover_art => { # from chunk 11
44             Name => 'CoverArt',
45             Groups => { 2 => 'Preview' },
46             Binary => 1,
47             },
48             );
49              
50             # 'tags' atoms observed in Audible .m4b audio books (ref PH)
51             %Image::ExifTool::Audible::tags = (
52             GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
53             NOTES => 'Information found in "tags" atom of Audible M4B audio books.',
54             meta => {
55             Name => 'Audible_meta',
56             SubDirectory => { TagTable => 'Image::ExifTool::Audible::meta' },
57             },
58             cvrx => {
59             Name => 'Audible_cvrx',
60             SubDirectory => { TagTable => 'Image::ExifTool::Audible::cvrx' },
61             },
62             tseg => {
63             Name => 'Audible_tseg',
64             SubDirectory => { TagTable => 'Image::ExifTool::Audible::tseg' },
65             },
66             );
67              
68             # 'meta' information observed in Audible .m4b audio books (ref PH)
69             %Image::ExifTool::Audible::meta = (
70             PROCESS_PROC => \&ProcessAudible_meta,
71             GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
72             NOTES => 'Information found in Audible M4B "meta" atom.',
73             Album => 'Album',
74             ALBUMARTIST => { Name => 'AlbumArtist', Groups => { 2 => 'Author' } },
75             Artist => { Name => 'Artist', Groups => { 2 => 'Author' } },
76             Comment => 'Comment',
77             Genre => 'Genre',
78             itunesmediatype => { Name => 'iTunesMediaType', Description => 'iTunes Media Type' },
79             SUBTITLE => 'Subtitle',
80             Title => 'Title',
81             TOOL => 'CreatorTool',
82             Year => { Name => 'Year', Groups => { 2 => 'Time' } },
83             track => 'ChapterName', # (found in 'meta' of 'tseg' atom)
84             );
85              
86             # 'cvrx' information observed in Audible .m4b audio books (ref PH)
87             %Image::ExifTool::Audible::cvrx = (
88             PROCESS_PROC => \&ProcessAudible_cvrx,
89             GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
90             NOTES => 'Audible cover art information in M4B audio books.',
91             VARS => { NO_ID => 1 },
92             CoverArtType => 'CoverArtType',
93             CoverArt => {
94             Name => 'CoverArt',
95             Groups => { 2 => 'Preview' },
96             Binary => 1,
97             },
98             );
99              
100             # 'tseg' information observed in Audible .m4b audio books (ref PH)
101             %Image::ExifTool::Audible::tseg = (
102             GROUPS => { 0 => 'QuickTime', 2 => 'Audio' },
103             tshd => {
104             Name => 'ChapterNumber',
105             Format => 'int32u',
106             ValueConv => '$val + 1', # start counting from 1
107             },
108             meta => {
109             Name => 'Audible_meta2',
110             SubDirectory => { TagTable => 'Image::ExifTool::Audible::meta' },
111             },
112             );
113              
114             #------------------------------------------------------------------------------
115             # Process Audible 'meta' tags from M4B files (ref PH)
116             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
117             # Returns: 1 on success
118             sub ProcessAudible_meta($$$)
119             {
120 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
121 0         0 my $dataPt = $$dirInfo{DataPt};
122 0         0 my $dataPos = $$dirInfo{DataPos};
123 0         0 my $dirLen = length $$dataPt;
124 0 0       0 return 0 if $dirLen < 4;
125 0         0 my $num = Get32u($dataPt, 0);
126 0         0 $et->VerboseDir('Audible_meta', $num);
127 0         0 my $pos = 4;
128 0         0 my $index;
129 0         0 for ($index=0; $index<$num; ++$index) {
130 0 0       0 last if $pos + 3 > $dirLen;
131 0         0 my $unk = Get8u($dataPt, $pos); # ? (0x80 or 0x00)
132 0 0 0     0 last unless $unk == 0x80 or $unk == 0x00;
133 0         0 my $len = Get16u($dataPt, $pos + 1); # tag length
134 0         0 $pos += 3;
135 0 0 0     0 last if $pos + $len + 6 > $dirLen or not $len;
136 0         0 my $tag = substr($$dataPt, $pos, $len); # tag ID
137 0         0 my $ver = Get16u($dataPt, $pos + $len); # version?
138 0 0       0 last unless $ver == 0x0001;
139 0         0 my $size = Get32u($dataPt, $pos + $len + 2);# data size
140 0         0 $pos += $len + 6;
141 0 0       0 last if $pos + $size > $dirLen;
142 0         0 my $val = $et->Decode(substr($$dataPt, $pos, $size), 'UTF8');
143 0 0       0 unless ($$tagTablePtr{$tag}) {
144 0 0       0 my $name = Image::ExifTool::MakeTagName(($tag =~ /[a-z]/) ? $tag : lc($tag));
145 0         0 AddTagToTable($tagTablePtr, $tag, { Name => $name });
146             }
147 0         0 $et->HandleTag($tagTablePtr, $tag, $val,
148             DataPt => $dataPt,
149             DataPos => $dataPos,
150             Start => $pos,
151             Size => $size,
152             Index => $index,
153             );
154 0         0 $pos += $size;
155             }
156 0         0 return 1;
157             }
158              
159             #------------------------------------------------------------------------------
160             # Process Audible 'cvrx' cover art atom from M4B files (ref PH)
161             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
162             # Returns: 1 on success
163             sub ProcessAudible_cvrx($$$)
164             {
165 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
166 0         0 my $dataPt = $$dirInfo{DataPt};
167 0         0 my $dataPos = $$dirInfo{DataPos};
168 0         0 my $dirLen = length $$dataPt;
169 0 0       0 return 0 if 0x0a > $dirLen;
170 0         0 my $len = Get16u($dataPt, 0x08);
171 0 0       0 return 0 if 0x0a + $len + 6 > $dirLen;
172 0         0 my $size = Get32u($dataPt, 0x0a + $len + 2);
173 0 0       0 return 0 if 0x0a + $len + 6 + $size > $dirLen;
174 0         0 $et->VerboseDir('Audible_cvrx', undef, $dirLen);
175 0         0 $et->HandleTag($tagTablePtr, 'CoverArtType', undef,
176             DataPt => $dataPt,
177             DataPos => $dataPos,
178             Start => 0x0a,
179             Size => $len,
180             );
181 0         0 $et->HandleTag($tagTablePtr, 'CoverArt', undef,
182             DataPt => $dataPt,
183             DataPos => $dataPos,
184             Start => 0x0a + $len + 6,
185             Size => $size,
186             );
187 0         0 return 1;
188             }
189              
190             #------------------------------------------------------------------------------
191             # Read information from an Audible .AA file
192             # Inputs: 0) ExifTool ref, 1) dirInfo ref
193             # Returns: 1 on success, 0 if this wasn't a valid AA file
194             sub ProcessAA($$)
195             {
196 1     1 0 5 my ($et, $dirInfo) = @_;
197 1         3 my $raf = $$dirInfo{RAF};
198 1         4 my ($buff, $toc, $entry, $i);
199              
200             # check magic number
201 1 50 33     4 return 0 unless $raf->Read($buff, 16) == 16 and $buff=~/^.{4}\x57\x90\x75\x36/s;
202             # check file size
203 1 50       6 if (defined $$et{VALUE}{FileSize}) {
204             # first 4 bytes of the file should be the filesize
205 1 50       10 unpack('N', $buff) == $$et{VALUE}{FileSize} or return 0;
206             }
207 1         6 $et->SetFileType();
208 1         4 SetByteOrder('MM');
209 1         7 my $bytes = 12 * Get32u(\$buff, 8); # table of contents size in bytes
210 1 50       5 $bytes > 0xc00 and $et->Warn('Invalid TOC'), return 1;
211             # read the table of contents
212 1 50       6 $raf->Read($toc, $bytes) == $bytes or $et->Warn('Truncated TOC'), return 1;
213 1         4 my $tagTablePtr = GetTagTable('Image::ExifTool::Audible::Main');
214             # parse table of contents (in $toc)
215 1         5 for ($entry=0; $entry<$bytes; $entry+=12) {
216 12         27 my $type = Get32u(\$toc, $entry);
217 12 100 100     77 next unless $type == 2 or $type == 6 or $type == 11;
      100        
218 3         10 my $offset = Get32u(\$toc, $entry + 4);
219 3 100       10 my $length = Get32u(\$toc, $entry + 8) or next;
220 2 50       12 $raf->Seek($offset, 0) or $et->Warn("Chunk $type seek error"), last;
221 2 50       10 if ($type == 6) { # offset table
222 0 0 0     0 next if $length < 4 or $raf->Read($buff, 4) != 4; # only read the chapter count
223 0         0 $et->HandleTag($tagTablePtr, '_chapter_count', Get32u(\$buff, 0));
224 0         0 next;
225             }
226             # read the chunk
227 2 50       8 $length > 100000000 and $et->Warn("Chunk $type too big"), next;
228 2 50       9 $raf->Read($buff, $length) == $length or $et->Warn("Chunk $type read error"), last;
229 2 100       17 if ($type == 11) { # cover art
230 1 50       4 next if $length < 8;
231 1         4 my $len = Get32u(\$buff, 0);
232 1         4 my $off = Get32u(\$buff, 4);
233 1 50 33     6 next if $off < $offset + 8 or $off - $offset + $len > $length;
234 1         7 $et->HandleTag($tagTablePtr, '_cover_art', substr($buff, $off-$offset, $len));
235 1         4 next;
236             }
237             # parse metadata dictionary (in $buff)
238 1 50       6 $length < 4 and $et->Warn('Bad dictionary'), next;
239 1         5 my $num = Get32u(\$buff, 0);
240 1 50       4 $num > 0x200 and $et->Warn('Bad dictionary count'), next;
241 1         3 my $pos = 4; # dictionary starts immediately after count
242 1         639 require Image::ExifTool::HTML; # (for UnescapeHTML)
243 1         12 $et->VerboseDir('Audible Metadata', $num);
244 1         6 for ($i=0; $i<$num; ++$i) {
245 28         42 my $tagPos = $pos + 9; # position of tag string
246 28 50       58 $tagPos > $length and $et->Warn('Truncated dictionary'), last;
247             # (1 unknown byte ignored at start of each dictionary entry)
248 28         74 my $tagLen = Get32u(\$buff, $pos + 1); # tag string length
249 28         70 my $valLen = Get32u(\$buff, $pos + 5); # value string length
250 28         50 my $valPos = $tagPos + $tagLen; # position of value string
251 28         41 my $nxtPos = $valPos + $valLen; # position of next entry
252 28 50       57 $nxtPos > $length and $et->Warn('Bad dictionary entry'), last;
253 28         59 my $tag = substr($buff, $tagPos, $tagLen);
254 28         49 my $val = substr($buff, $valPos, $valLen);
255 28 100       65 unless ($$tagTablePtr{$tag}) {
256 24         51 my $name = Image::ExifTool::MakeTagName($tag);
257 24         119 $name =~ s/_(.)/\U$1/g; # change from underscore-separated to mixed case
258 24         90 AddTagToTable($tagTablePtr, $tag, { Name => $name });
259             }
260             # unescape HTML character references and convert from UTF-8
261 28         67 $val = $et->Decode(Image::ExifTool::HTML::UnescapeHTML($val), 'UTF8');
262 28         122 $et->HandleTag($tagTablePtr, $tag, $val,
263             DataPos => $offset,
264             DataPt => \$buff,
265             Start => $valPos,
266             Size => $valLen,
267             Index => $i,
268             );
269 28         82 $pos = $nxtPos; # step to next dictionary entry
270             }
271             }
272 1         3 return 1;
273             }
274              
275             1; # end
276              
277             __END__