module.tag.apetag.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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
  16. {
  17. function getid3_apetag(&$fd, &$ThisFileInfo, $overrideendoffset=0) {
  18. if ($ThisFileInfo['filesize'] >= pow(2, 31)) {
  19. $ThisFileInfo['warning'][] = 'Unable to check for APEtags because file is larger than 2GB';
  20. return false;
  21. }
  22. $id3v1tagsize = 128;
  23. $apetagheadersize = 32;
  24. $lyrics3tagsize = 10;
  25. if ($overrideendoffset == 0) {
  26. fseek($fd, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END);
  27. $APEfooterID3v1 = fread($fd, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize);
  28. //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) {
  29. if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') {
  30. // APE tag found before ID3v1
  31. $ThisFileInfo['ape']['tag_offset_end'] = $ThisFileInfo['filesize'] - $id3v1tagsize;
  32. //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) {
  33. } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') {
  34. // APE tag found, no ID3v1
  35. $ThisFileInfo['ape']['tag_offset_end'] = $ThisFileInfo['filesize'];
  36. }
  37. } else {
  38. fseek($fd, $overrideendoffset - $apetagheadersize, SEEK_SET);
  39. if (fread($fd, 8) == 'APETAGEX') {
  40. $ThisFileInfo['ape']['tag_offset_end'] = $overrideendoffset;
  41. }
  42. }
  43. if (!isset($ThisFileInfo['ape']['tag_offset_end'])) {
  44. // APE tag not found
  45. unset($ThisFileInfo['ape']);
  46. return false;
  47. }
  48. // shortcut
  49. $thisfile_ape = &$ThisFileInfo['ape'];
  50. fseek($fd, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET);
  51. $APEfooterData = fread($fd, 32);
  52. if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) {
  53. $ThisFileInfo['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end'];
  54. return false;
  55. }
  56. if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
  57. fseek($fd, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET);
  58. $thisfile_ape['tag_offset_start'] = ftell($fd);
  59. $APEtagData = fread($fd, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize);
  60. } else {
  61. $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'];
  62. fseek($fd, $thisfile_ape['tag_offset_start'], SEEK_SET);
  63. $APEtagData = fread($fd, $thisfile_ape['footer']['raw']['tagsize']);
  64. }
  65. $ThisFileInfo['avdataend'] = $thisfile_ape['tag_offset_start'];
  66. if (isset($ThisFileInfo['id3v1']['tag_offset_start']) && ($ThisFileInfo['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) {
  67. $ThisFileInfo['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data';
  68. unset($ThisFileInfo['id3v1']);
  69. foreach ($ThisFileInfo['warning'] as $key => $value) {
  70. if ($value == 'Some ID3v1 fields do not use NULL characters for padding') {
  71. unset($ThisFileInfo['warning'][$key]);
  72. sort($ThisFileInfo['warning']);
  73. break;
  74. }
  75. }
  76. }
  77. $offset = 0;
  78. if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
  79. if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) {
  80. $offset += $apetagheadersize;
  81. } else {
  82. $ThisFileInfo['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start'];
  83. return false;
  84. }
  85. }
  86. // shortcut
  87. $ThisFileInfo['replay_gain'] = array();
  88. $thisfile_replaygain = &$ThisFileInfo['replay_gain'];
  89. for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) {
  90. $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
  91. $offset += 4;
  92. $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
  93. $offset += 4;
  94. if (strstr(substr($APEtagData, $offset), "\x00") === false) {
  95. $ThisFileInfo['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);
  96. return false;
  97. }
  98. $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset;
  99. $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength));
  100. // shortcut
  101. $thisfile_ape['items'][$item_key] = array();
  102. $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key];
  103. $offset += ($ItemKeyLength + 1); // skip 0x00 terminator
  104. $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size);
  105. $offset += $value_size;
  106. $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags);
  107. switch ($thisfile_ape_items_current['flags']['item_contents_raw']) {
  108. case 0: // UTF-8
  109. case 3: // Locator (URL, filename, etc), UTF-8 encoded
  110. $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data']));
  111. break;
  112. default: // binary data
  113. break;
  114. }
  115. switch (strtolower($item_key)) {
  116. case 'replaygain_track_gain':
  117. $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  118. $thisfile_replaygain['track']['originator'] = 'unspecified';
  119. break;
  120. case 'replaygain_track_peak':
  121. $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  122. $thisfile_replaygain['track']['originator'] = 'unspecified';
  123. if ($thisfile_replaygain['track']['peak'] <= 0) {
  124. $ThisFileInfo['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
  125. }
  126. break;
  127. case 'replaygain_album_gain':
  128. $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  129. $thisfile_replaygain['album']['originator'] = 'unspecified';
  130. break;
  131. case 'replaygain_album_peak':
  132. $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
  133. $thisfile_replaygain['album']['originator'] = 'unspecified';
  134. if ($thisfile_replaygain['album']['peak'] <= 0) {
  135. $ThisFileInfo['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
  136. }
  137. break;
  138. case 'mp3gain_undo':
  139. list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]);
  140. $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left);
  141. $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right);
  142. $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false);
  143. break;
  144. case 'mp3gain_minmax':
  145. list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]);
  146. $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min);
  147. $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max);
  148. break;
  149. case 'mp3gain_album_minmax':
  150. list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]);
  151. $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min);
  152. $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max);
  153. break;
  154. case 'tracknumber':
  155. foreach ($thisfile_ape_items_current['data'] as $comment) {
  156. $thisfile_ape['comments']['track'][] = $comment;
  157. }
  158. break;
  159. default:
  160. foreach ($thisfile_ape_items_current['data'] as $comment) {
  161. $thisfile_ape['comments'][strtolower($item_key)][] = $comment;
  162. }
  163. break;
  164. }
  165. }
  166. if (empty($thisfile_replaygain)) {
  167. unset($ThisFileInfo['replay_gain']);
  168. }
  169. return true;
  170. }
  171. function parseAPEheaderFooter($APEheaderFooterData) {
  172. // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
  173. // shortcut
  174. $headerfooterinfo['raw'] = array();
  175. $headerfooterinfo_raw = &$headerfooterinfo['raw'];
  176. $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8);
  177. if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
  178. return false;
  179. }
  180. $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4));
  181. $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
  182. $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
  183. $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
  184. $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8);
  185. $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000;
  186. if ($headerfooterinfo['tag_version'] >= 2) {
  187. $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
  188. }
  189. return $headerfooterinfo;
  190. }
  191. function parseAPEtagFlags($rawflagint) {
  192. // "Note: APE Tags 1.0 do not use any of the APE Tag flags.
  193. // All are set to zero on creation and ignored on reading."
  194. // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
  195. $flags['header'] = (bool) ($rawflagint & 0x80000000);
  196. $flags['footer'] = (bool) ($rawflagint & 0x40000000);
  197. $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000);
  198. $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1;
  199. $flags['read_only'] = (bool) ($rawflagint & 0x00000001);
  200. $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
  201. return $flags;
  202. }
  203. function APEcontentTypeFlagLookup($contenttypeid) {
  204. static $APEcontentTypeFlagLookup = array(
  205. 0 => 'utf-8',
  206. 1 => 'binary',
  207. 2 => 'external',
  208. 3 => 'reserved'
  209. );
  210. return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
  211. }
  212. function APEtagItemIsUTF8Lookup($itemkey) {
  213. static $APEtagItemIsUTF8Lookup = array(
  214. 'title',
  215. 'subtitle',
  216. 'artist',
  217. 'album',
  218. 'debut album',
  219. 'publisher',
  220. 'conductor',
  221. 'track',
  222. 'composer',
  223. 'comment',
  224. 'copyright',
  225. 'publicationright',
  226. 'file',
  227. 'year',
  228. 'record date',
  229. 'record location',
  230. 'genre',
  231. 'media',
  232. 'related',
  233. 'isrc',
  234. 'abstract',
  235. 'language',
  236. 'bibliography'
  237. );
  238. return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
  239. }
  240. }
  241. ?>