module.tag.apetag.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. <?php
  2. /////////////////////////////////////////////////////////////////
  3. /// getID3() by James Heinrich <info@getid3.org> //
  4. // available at http://getid3.sourceforge.net //
  5. // or http://www.getid3.org //
  6. /////////////////////////////////////////////////////////////////
  7. // See readme.txt for more details //
  8. /////////////////////////////////////////////////////////////////
  9. // //
  10. // module.tag.apetag.php //
  11. // module for analyzing APE tags //
  12. // dependencies: NONE //
  13. // ///
  14. /////////////////////////////////////////////////////////////////
  15. class getid3_apetag extends getid3_handler
  16. {
  17. var $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory
  18. var $overrideendoffset = 0;
  19. function Analyze() {
  20. $info = &$this->getid3->info;
  21. if (!getid3_lib::intValueSupported($info['filesize'])) {
  22. $info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
  23. return false;
  24. }
  25. $id3v1tagsize = 128;
  26. $apetagheadersize = 32;
  27. $lyrics3tagsize = 10;
  28. if ($this->overrideendoffset == 0) {
  29. fseek($this->getid3->fp, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END);
  30. $APEfooterID3v1 = fread($this->getid3->fp, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize);
  31. //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) {
  32. if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') {
  33. // APE tag found before ID3v1
  34. $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize;
  35. //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) {
  36. } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') {
  37. // APE tag found, no ID3v1
  38. $info['ape']['tag_offset_end'] = $info['filesize'];
  39. }
  40. } else {
  41. fseek($this->getid3->fp, $this->overrideendoffset - $apetagheadersize, SEEK_SET);
  42. if (fread($this->getid3->fp, 8) == 'APETAGEX') {
  43. $info['ape']['tag_offset_end'] = $this->overrideendoffset;
  44. }
  45. }
  46. if (!isset($info['ape']['tag_offset_end'])) {
  47. // APE tag not found
  48. unset($info['ape']);
  49. return false;
  50. }
  51. // shortcut
  52. $thisfile_ape = &$info['ape'];
  53. fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET);
  54. $APEfooterData = fread($this->getid3->fp, 32);
  55. if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) {
  56. $info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end'];
  57. return false;
  58. }
  59. if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
  60. fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET);
  61. $thisfile_ape['tag_offset_start'] = ftell($this->getid3->fp);
  62. $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize);
  63. } else {
  64. $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'];
  65. fseek($this->getid3->fp, $thisfile_ape['tag_offset_start'], SEEK_SET);
  66. $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize']);
  67. }
  68. $info['avdataend'] = $thisfile_ape['tag_offset_start'];
  69. if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) {
  70. $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data';
  71. unset($info['id3v1']);
  72. foreach ($info['warning'] as $key => $value) {
  73. if ($value == 'Some ID3v1 fields do not use NULL characters for padding') {
  74. unset($info['warning'][$key]);
  75. sort($info['warning']);
  76. break;
  77. }
  78. }
  79. }
  80. $offset = 0;
  81. if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
  82. if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) {
  83. $offset += $apetagheadersize;
  84. } else {
  85. $info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start'];
  86. return false;
  87. }
  88. }
  89. // shortcut
  90. $info['replay_gain'] = array();
  91. $thisfile_replaygain = &$info['replay_gain'];
  92. for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) {
  93. $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
  94. $offset += 4;
  95. $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
  96. $offset += 4;
  97. if (strstr(substr($APEtagData, $offset), "\x00") === false) {
  98. $info['error'][] = 'Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($thisfile_ape['tag_offset_start'] + $offset);
  99. return false;
  100. }
  101. $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset;
  102. $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength));
  103. // shortcut
  104. $thisfile_ape['items'][$item_key] = array();
  105. $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key];
  106. $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset;
  107. $offset += ($ItemKeyLength + 1); // skip 0x00 terminator
  108. $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size);
  109. $offset += $value_size;
  110. $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags);
  111. switch ($thisfile_ape_items_current['flags']['item_contents_raw']) {
  112. case 0: // UTF-8
  113. case 3: // Locator (URL, filename, etc), UTF-8 encoded
  114. $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data']));
  115. break;
  116. default: // binary data
  117. break;
  118. }
  119. switch (strtolower($item_key)) {
  120. case 'replaygain_track_gain':
  121. $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  122. $thisfile_replaygain['track']['originator'] = 'unspecified';
  123. break;
  124. case 'replaygain_track_peak':
  125. $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  126. $thisfile_replaygain['track']['originator'] = 'unspecified';
  127. if ($thisfile_replaygain['track']['peak'] <= 0) {
  128. $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
  129. }
  130. break;
  131. case 'replaygain_album_gain':
  132. $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  133. $thisfile_replaygain['album']['originator'] = 'unspecified';
  134. break;
  135. case 'replaygain_album_peak':
  136. $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  137. $thisfile_replaygain['album']['originator'] = 'unspecified';
  138. if ($thisfile_replaygain['album']['peak'] <= 0) {
  139. $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
  140. }
  141. break;
  142. case 'mp3gain_undo':
  143. list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]);
  144. $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left);
  145. $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right);
  146. $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false);
  147. break;
  148. case 'mp3gain_minmax':
  149. list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]);
  150. $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min);
  151. $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max);
  152. break;
  153. case 'mp3gain_album_minmax':
  154. list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]);
  155. $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min);
  156. $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max);
  157. break;
  158. case 'tracknumber':
  159. if (is_array($thisfile_ape_items_current['data'])) {
  160. foreach ($thisfile_ape_items_current['data'] as $comment) {
  161. $thisfile_ape['comments']['track'][] = $comment;
  162. }
  163. }
  164. break;
  165. case 'cover art (artist)':
  166. case 'cover art (back)':
  167. case 'cover art (band logo)':
  168. case 'cover art (band)':
  169. case 'cover art (colored fish)':
  170. case 'cover art (composer)':
  171. case 'cover art (conductor)':
  172. case 'cover art (front)':
  173. case 'cover art (icon)':
  174. case 'cover art (illustration)':
  175. case 'cover art (lead)':
  176. case 'cover art (leaflet)':
  177. case 'cover art (lyricist)':
  178. case 'cover art (media)':
  179. case 'cover art (movie scene)':
  180. case 'cover art (other icon)':
  181. case 'cover art (other)':
  182. case 'cover art (performance)':
  183. case 'cover art (publisher logo)':
  184. case 'cover art (recording)':
  185. case 'cover art (studio)':
  186. // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html
  187. list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2);
  188. $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00");
  189. $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']);
  190. $thisfile_ape_items_current['image_mime'] = '';
  191. $imageinfo = array();
  192. $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo);
  193. $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
  194. do {
  195. if ($this->inline_attachments === false) {
  196. // skip entirely
  197. unset($thisfile_ape_items_current['data']);
  198. break;
  199. }
  200. if ($this->inline_attachments === true) {
  201. // great
  202. } elseif (is_int($this->inline_attachments)) {
  203. if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) {
  204. // too big, skip
  205. $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)';
  206. unset($thisfile_ape_items_current['data']);
  207. break;
  208. }
  209. } elseif (is_string($this->inline_attachments)) {
  210. $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
  211. if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
  212. // cannot write, skip
  213. $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
  214. unset($thisfile_ape_items_current['data']);
  215. break;
  216. }
  217. }
  218. // if we get this far, must be OK
  219. if (is_string($this->inline_attachments)) {
  220. $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset'];
  221. if (!file_exists($destination_filename) || is_writable($destination_filename)) {
  222. file_put_contents($destination_filename, $thisfile_ape_items_current['data']);
  223. } else {
  224. $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
  225. }
  226. $thisfile_ape_items_current['data_filename'] = $destination_filename;
  227. unset($thisfile_ape_items_current['data']);
  228. } else {
  229. if (!isset($info['ape']['comments']['picture'])) {
  230. $info['ape']['comments']['picture'] = array();
  231. }
  232. $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']);
  233. }
  234. } while (false);
  235. break;
  236. default:
  237. if (is_array($thisfile_ape_items_current['data'])) {
  238. foreach ($thisfile_ape_items_current['data'] as $comment) {
  239. $thisfile_ape['comments'][strtolower($item_key)][] = $comment;
  240. }
  241. }
  242. break;
  243. }
  244. }
  245. if (empty($thisfile_replaygain)) {
  246. unset($info['replay_gain']);
  247. }
  248. return true;
  249. }
  250. function parseAPEheaderFooter($APEheaderFooterData) {
  251. // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
  252. // shortcut
  253. $headerfooterinfo['raw'] = array();
  254. $headerfooterinfo_raw = &$headerfooterinfo['raw'];
  255. $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8);
  256. if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
  257. return false;
  258. }
  259. $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4));
  260. $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
  261. $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
  262. $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
  263. $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8);
  264. $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000;
  265. if ($headerfooterinfo['tag_version'] >= 2) {
  266. $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
  267. }
  268. return $headerfooterinfo;
  269. }
  270. function parseAPEtagFlags($rawflagint) {
  271. // "Note: APE Tags 1.0 do not use any of the APE Tag flags.
  272. // All are set to zero on creation and ignored on reading."
  273. // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
  274. $flags['header'] = (bool) ($rawflagint & 0x80000000);
  275. $flags['footer'] = (bool) ($rawflagint & 0x40000000);
  276. $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000);
  277. $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1;
  278. $flags['read_only'] = (bool) ($rawflagint & 0x00000001);
  279. $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
  280. return $flags;
  281. }
  282. function APEcontentTypeFlagLookup($contenttypeid) {
  283. static $APEcontentTypeFlagLookup = array(
  284. 0 => 'utf-8',
  285. 1 => 'binary',
  286. 2 => 'external',
  287. 3 => 'reserved'
  288. );
  289. return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
  290. }
  291. function APEtagItemIsUTF8Lookup($itemkey) {
  292. static $APEtagItemIsUTF8Lookup = array(
  293. 'title',
  294. 'subtitle',
  295. 'artist',
  296. 'album',
  297. 'debut album',
  298. 'publisher',
  299. 'conductor',
  300. 'track',
  301. 'composer',
  302. 'comment',
  303. 'copyright',
  304. 'publicationright',
  305. 'file',
  306. 'year',
  307. 'record date',
  308. 'record location',
  309. 'genre',
  310. 'media',
  311. 'related',
  312. 'isrc',
  313. 'abstract',
  314. 'language',
  315. 'bibliography'
  316. );
  317. return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
  318. }
  319. }
  320. ?>