File indexing completed on 2025-02-02 05:44:55
0001 <?php 0002 ///////////////////////////////////////////////////////////////// 0003 /// getID3() by James Heinrich <info@getid3.org> // 0004 // available at http://getid3.sourceforge.net // 0005 // or http://www.getid3.org // 0006 // also https://github.com/JamesHeinrich/getID3 // 0007 ///////////////////////////////////////////////////////////////// 0008 // See readme.txt for more details // 0009 ///////////////////////////////////////////////////////////////// 0010 // // 0011 // module.tag.apetag.php // 0012 // module for analyzing APE tags // 0013 // dependencies: NONE // 0014 // /// 0015 ///////////////////////////////////////////////////////////////// 0016 0017 class getid3_apetag extends getid3_handler 0018 { 0019 public $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 0020 public $overrideendoffset = 0; 0021 0022 public function Analyze() { 0023 $info = &$this->getid3->info; 0024 0025 if (!getid3_lib::intValueSupported($info['filesize'])) { 0026 $info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; 0027 return false; 0028 } 0029 0030 $id3v1tagsize = 128; 0031 $apetagheadersize = 32; 0032 $lyrics3tagsize = 10; 0033 0034 if ($this->overrideendoffset == 0) { 0035 0036 $this->fseek(0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); 0037 $APEfooterID3v1 = $this->fread($id3v1tagsize + $apetagheadersize + $lyrics3tagsize); 0038 0039 //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) { 0040 if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') { 0041 0042 // APE tag found before ID3v1 0043 $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize; 0044 0045 //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) { 0046 } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') { 0047 0048 // APE tag found, no ID3v1 0049 $info['ape']['tag_offset_end'] = $info['filesize']; 0050 0051 } 0052 0053 } else { 0054 0055 $this->fseek($this->overrideendoffset - $apetagheadersize); 0056 if ($this->fread(8) == 'APETAGEX') { 0057 $info['ape']['tag_offset_end'] = $this->overrideendoffset; 0058 } 0059 0060 } 0061 if (!isset($info['ape']['tag_offset_end'])) { 0062 0063 // APE tag not found 0064 unset($info['ape']); 0065 return false; 0066 0067 } 0068 0069 // shortcut 0070 $thisfile_ape = &$info['ape']; 0071 0072 $this->fseek($thisfile_ape['tag_offset_end'] - $apetagheadersize); 0073 $APEfooterData = $this->fread(32); 0074 if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) { 0075 $info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end']; 0076 return false; 0077 } 0078 0079 if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { 0080 $this->fseek($thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize); 0081 $thisfile_ape['tag_offset_start'] = $this->ftell(); 0082 $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); 0083 } else { 0084 $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; 0085 $this->fseek($thisfile_ape['tag_offset_start']); 0086 $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize']); 0087 } 0088 $info['avdataend'] = $thisfile_ape['tag_offset_start']; 0089 0090 if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) { 0091 $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data'; 0092 unset($info['id3v1']); 0093 foreach ($info['warning'] as $key => $value) { 0094 if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { 0095 unset($info['warning'][$key]); 0096 sort($info['warning']); 0097 break; 0098 } 0099 } 0100 } 0101 0102 $offset = 0; 0103 if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { 0104 if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) { 0105 $offset += $apetagheadersize; 0106 } else { 0107 $info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start']; 0108 return false; 0109 } 0110 } 0111 0112 // shortcut 0113 $info['replay_gain'] = array(); 0114 $thisfile_replaygain = &$info['replay_gain']; 0115 0116 for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) { 0117 $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); 0118 $offset += 4; 0119 $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); 0120 $offset += 4; 0121 if (strstr(substr($APEtagData, $offset), "\x00") === false) { 0122 $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); 0123 return false; 0124 } 0125 $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset; 0126 $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength)); 0127 0128 // shortcut 0129 $thisfile_ape['items'][$item_key] = array(); 0130 $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key]; 0131 0132 $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset; 0133 0134 $offset += ($ItemKeyLength + 1); // skip 0x00 terminator 0135 $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size); 0136 $offset += $value_size; 0137 0138 $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags); 0139 switch ($thisfile_ape_items_current['flags']['item_contents_raw']) { 0140 case 0: // UTF-8 0141 case 3: // Locator (URL, filename, etc), UTF-8 encoded 0142 $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data'])); 0143 break; 0144 0145 default: // binary data 0146 break; 0147 } 0148 0149 switch (strtolower($item_key)) { 0150 case 'replaygain_track_gain': 0151 $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! 0152 $thisfile_replaygain['track']['originator'] = 'unspecified'; 0153 break; 0154 0155 case 'replaygain_track_peak': 0156 $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! 0157 $thisfile_replaygain['track']['originator'] = 'unspecified'; 0158 if ($thisfile_replaygain['track']['peak'] <= 0) { 0159 $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; 0160 } 0161 break; 0162 0163 case 'replaygain_album_gain': 0164 $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! 0165 $thisfile_replaygain['album']['originator'] = 'unspecified'; 0166 break; 0167 0168 case 'replaygain_album_peak': 0169 $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! 0170 $thisfile_replaygain['album']['originator'] = 'unspecified'; 0171 if ($thisfile_replaygain['album']['peak'] <= 0) { 0172 $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; 0173 } 0174 break; 0175 0176 case 'mp3gain_undo': 0177 list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]); 0178 $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); 0179 $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); 0180 $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); 0181 break; 0182 0183 case 'mp3gain_minmax': 0184 list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]); 0185 $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); 0186 $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); 0187 break; 0188 0189 case 'mp3gain_album_minmax': 0190 list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]); 0191 $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); 0192 $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); 0193 break; 0194 0195 case 'tracknumber': 0196 if (is_array($thisfile_ape_items_current['data'])) { 0197 foreach ($thisfile_ape_items_current['data'] as $comment) { 0198 $thisfile_ape['comments']['track'][] = $comment; 0199 } 0200 } 0201 break; 0202 0203 case 'cover art (artist)': 0204 case 'cover art (back)': 0205 case 'cover art (band logo)': 0206 case 'cover art (band)': 0207 case 'cover art (colored fish)': 0208 case 'cover art (composer)': 0209 case 'cover art (conductor)': 0210 case 'cover art (front)': 0211 case 'cover art (icon)': 0212 case 'cover art (illustration)': 0213 case 'cover art (lead)': 0214 case 'cover art (leaflet)': 0215 case 'cover art (lyricist)': 0216 case 'cover art (media)': 0217 case 'cover art (movie scene)': 0218 case 'cover art (other icon)': 0219 case 'cover art (other)': 0220 case 'cover art (performance)': 0221 case 'cover art (publisher logo)': 0222 case 'cover art (recording)': 0223 case 'cover art (studio)': 0224 // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html 0225 list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2); 0226 $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00"); 0227 $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']); 0228 0229 $thisfile_ape_items_current['image_mime'] = ''; 0230 $imageinfo = array(); 0231 $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); 0232 $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); 0233 0234 do { 0235 if ($this->inline_attachments === false) { 0236 // skip entirely 0237 unset($thisfile_ape_items_current['data']); 0238 break; 0239 } 0240 if ($this->inline_attachments === true) { 0241 // great 0242 } elseif (is_int($this->inline_attachments)) { 0243 if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) { 0244 // too big, skip 0245 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)'; 0246 unset($thisfile_ape_items_current['data']); 0247 break; 0248 } 0249 } elseif (is_string($this->inline_attachments)) { 0250 $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR); 0251 if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) { 0252 // cannot write, skip 0253 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)'; 0254 unset($thisfile_ape_items_current['data']); 0255 break; 0256 } 0257 } 0258 // if we get this far, must be OK 0259 if (is_string($this->inline_attachments)) { 0260 $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset']; 0261 if (!file_exists($destination_filename) || is_writable($destination_filename)) { 0262 file_put_contents($destination_filename, $thisfile_ape_items_current['data']); 0263 } else { 0264 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)'; 0265 } 0266 $thisfile_ape_items_current['data_filename'] = $destination_filename; 0267 unset($thisfile_ape_items_current['data']); 0268 } else { 0269 if (!isset($info['ape']['comments']['picture'])) { 0270 $info['ape']['comments']['picture'] = array(); 0271 } 0272 $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']); 0273 } 0274 } while (false); 0275 break; 0276 0277 default: 0278 if (is_array($thisfile_ape_items_current['data'])) { 0279 foreach ($thisfile_ape_items_current['data'] as $comment) { 0280 $thisfile_ape['comments'][strtolower($item_key)][] = $comment; 0281 } 0282 } 0283 break; 0284 } 0285 0286 } 0287 if (empty($thisfile_replaygain)) { 0288 unset($info['replay_gain']); 0289 } 0290 return true; 0291 } 0292 0293 public function parseAPEheaderFooter($APEheaderFooterData) { 0294 // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html 0295 0296 // shortcut 0297 $headerfooterinfo['raw'] = array(); 0298 $headerfooterinfo_raw = &$headerfooterinfo['raw']; 0299 0300 $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8); 0301 if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') { 0302 return false; 0303 } 0304 $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4)); 0305 $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4)); 0306 $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4)); 0307 $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4)); 0308 $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8); 0309 0310 $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000; 0311 if ($headerfooterinfo['tag_version'] >= 2) { 0312 $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']); 0313 } 0314 return $headerfooterinfo; 0315 } 0316 0317 public function parseAPEtagFlags($rawflagint) { 0318 // "Note: APE Tags 1.0 do not use any of the APE Tag flags. 0319 // All are set to zero on creation and ignored on reading." 0320 // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html 0321 $flags['header'] = (bool) ($rawflagint & 0x80000000); 0322 $flags['footer'] = (bool) ($rawflagint & 0x40000000); 0323 $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000); 0324 $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1; 0325 $flags['read_only'] = (bool) ($rawflagint & 0x00000001); 0326 0327 $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']); 0328 0329 return $flags; 0330 } 0331 0332 public function APEcontentTypeFlagLookup($contenttypeid) { 0333 static $APEcontentTypeFlagLookup = array( 0334 0 => 'utf-8', 0335 1 => 'binary', 0336 2 => 'external', 0337 3 => 'reserved' 0338 ); 0339 return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid'); 0340 } 0341 0342 public function APEtagItemIsUTF8Lookup($itemkey) { 0343 static $APEtagItemIsUTF8Lookup = array( 0344 'title', 0345 'subtitle', 0346 'artist', 0347 'album', 0348 'debut album', 0349 'publisher', 0350 'conductor', 0351 'track', 0352 'composer', 0353 'comment', 0354 'copyright', 0355 'publicationright', 0356 'file', 0357 'year', 0358 'record date', 0359 'record location', 0360 'genre', 0361 'media', 0362 'related', 0363 'isrc', 0364 'abstract', 0365 'language', 0366 'bibliography' 0367 ); 0368 return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup); 0369 } 0370 0371 }