Audible.pm 12 KB

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