rlm@3: | rlm@3: // | Allan Hansen | rlm@3: // +----------------------------------------------------------------------+ rlm@3: // | module.tag.apetag.php | rlm@3: // | module for analyzing APE tags | rlm@3: // | dependencies: NONE | rlm@3: // +----------------------------------------------------------------------+ rlm@3: // rlm@3: // $Id: module.tag.apetag.php,v 1.5 2006/11/16 14:05:21 ah Exp $ rlm@3: rlm@3: rlm@3: rlm@3: class getid3_apetag extends getid3_handler rlm@3: { rlm@3: /* rlm@3: ID3v1_TAG_SIZE = 128; rlm@3: APETAG_HEADER_SIZE = 32; rlm@3: LYRICS3_TAG_SIZE = 10; rlm@3: */ rlm@3: rlm@3: public $option_override_end_offset = 0; rlm@3: rlm@3: rlm@3: rlm@3: public function Analyze() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: if ($this->option_override_end_offset == 0) { rlm@3: rlm@3: fseek($getid3->fp, 0 - 170, SEEK_END); // 170 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE + LYRICS3_TAG_SIZE rlm@3: $apetag_footer_id3v1 = fread($getid3->fp, 170); // 170 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE + LYRICS3_TAG_SIZE rlm@3: rlm@3: // APE tag found before ID3v1 rlm@3: if (substr($apetag_footer_id3v1, strlen($apetag_footer_id3v1) - 160, 8) == 'APETAGEX') { // 160 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE rlm@3: $getid3->info['ape']['tag_offset_end'] = filesize($getid3->filename) - 128; // 128 = ID3v1_TAG_SIZE rlm@3: } rlm@3: rlm@3: // APE tag found, no ID3v1 rlm@3: elseif (substr($apetag_footer_id3v1, strlen($apetag_footer_id3v1) - 32, 8) == 'APETAGEX') { // 32 = APETAG_HEADER_SIZE rlm@3: $getid3->info['ape']['tag_offset_end'] = filesize($getid3->filename); rlm@3: } rlm@3: rlm@3: } rlm@3: else { rlm@3: rlm@3: fseek($getid3->fp, $this->option_override_end_offset - 32, SEEK_SET); // 32 = APETAG_HEADER_SIZE rlm@3: if (fread($getid3->fp, 8) == 'APETAGEX') { rlm@3: $getid3->info['ape']['tag_offset_end'] = $this->option_override_end_offset; rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: // APE tag not found rlm@3: if (!@$getid3->info['ape']['tag_offset_end']) { rlm@3: return false; rlm@3: } rlm@3: rlm@3: // Shortcut rlm@3: $info_ape = &$getid3->info['ape']; rlm@3: rlm@3: // Read and parse footer rlm@3: fseek($getid3->fp, $info_ape['tag_offset_end'] - 32, SEEK_SET); // 32 = APETAG_HEADER_SIZE rlm@3: $apetag_footer_data = fread($getid3->fp, 32); rlm@3: if (!($this->ParseAPEHeaderFooter($apetag_footer_data, $info_ape['footer']))) { rlm@3: throw new getid3_exception('Error parsing APE footer at offset '.$info_ape['tag_offset_end']); rlm@3: } rlm@3: rlm@3: if (isset($info_ape['footer']['flags']['header']) && $info_ape['footer']['flags']['header']) { rlm@3: fseek($getid3->fp, $info_ape['tag_offset_end'] - $info_ape['footer']['raw']['tagsize'] - 32, SEEK_SET); rlm@3: $info_ape['tag_offset_start'] = ftell($getid3->fp); rlm@3: $apetag_data = fread($getid3->fp, $info_ape['footer']['raw']['tagsize'] + 32); rlm@3: } rlm@3: else { rlm@3: $info_ape['tag_offset_start'] = $info_ape['tag_offset_end'] - $info_ape['footer']['raw']['tagsize']; rlm@3: fseek($getid3->fp, $info_ape['tag_offset_start'], SEEK_SET); rlm@3: $apetag_data = fread($getid3->fp, $info_ape['footer']['raw']['tagsize']); rlm@3: } rlm@3: $getid3->info['avdataend'] = $info_ape['tag_offset_start']; rlm@3: rlm@3: if (isset($getid3->info['id3v1']['tag_offset_start']) && ($getid3->info['id3v1']['tag_offset_start'] < $info_ape['tag_offset_end'])) { rlm@3: $getid3->warning('ID3v1 tag information ignored since it appears to be a false synch in APEtag data'); rlm@3: unset($getid3->info['id3v1']); rlm@3: } rlm@3: rlm@3: $offset = 0; rlm@3: if (isset($info_ape['footer']['flags']['header']) && $info_ape['footer']['flags']['header']) { rlm@3: if (!$this->ParseAPEHeaderFooter(substr($apetag_data, 0, 32), $info_ape['header'])) { rlm@3: throw new getid3_exception('Error parsing APE header at offset '.$info_ape['tag_offset_start']); rlm@3: } rlm@3: $offset = 32; rlm@3: } rlm@3: rlm@3: // Shortcut rlm@3: $getid3->info['replay_gain'] = array (); rlm@3: $info_replaygain = &$getid3->info['replay_gain']; rlm@3: rlm@3: for ($i = 0; $i < $info_ape['footer']['raw']['tag_items']; $i++) { rlm@3: $value_size = getid3_lib::LittleEndian2Int(substr($apetag_data, $offset, 4)); rlm@3: $item_flags = getid3_lib::LittleEndian2Int(substr($apetag_data, $offset + 4, 4)); rlm@3: $offset += 8; rlm@3: rlm@3: if (strstr(substr($apetag_data, $offset), "\x00") === false) { rlm@3: throw new getid3_exception('Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts ' . $offset . ' bytes into the APE tag, at file offset '.($info_ape['tag_offset_start'] + $offset)); rlm@3: } rlm@3: rlm@3: $item_key_length = strpos($apetag_data, "\x00", $offset) - $offset; rlm@3: $item_key = strtolower(substr($apetag_data, $offset, $item_key_length)); rlm@3: rlm@3: // Shortcut rlm@3: $info_ape['items'][$item_key] = array (); rlm@3: $info_ape_items_current = &$info_ape['items'][$item_key]; rlm@3: rlm@3: $offset += $item_key_length + 1; // skip 0x00 terminator rlm@3: $info_ape_items_current['data'] = substr($apetag_data, $offset, $value_size); rlm@3: $offset += $value_size; rlm@3: rlm@3: rlm@3: $info_ape_items_current['flags'] = $this->ParseAPEtagFlags($item_flags); rlm@3: rlm@3: switch ($info_ape_items_current['flags']['item_contents_raw']) { rlm@3: case 0: // UTF-8 rlm@3: case 3: // Locator (URL, filename, etc), UTF-8 encoded rlm@3: $info_ape_items_current['data'] = explode("\x00", trim($info_ape_items_current['data'])); rlm@3: break; rlm@3: rlm@3: default: // binary data rlm@3: break; rlm@3: } rlm@3: rlm@3: switch (strtolower($item_key)) { rlm@3: case 'replaygain_track_gain': rlm@3: $info_replaygain['track']['adjustment'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! rlm@3: $info_replaygain['track']['originator'] = 'unspecified'; rlm@3: break; rlm@3: rlm@3: case 'replaygain_track_peak': rlm@3: $info_replaygain['track']['peak'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! rlm@3: $info_replaygain['track']['originator'] = 'unspecified'; rlm@3: if ($info_replaygain['track']['peak'] <= 0) { rlm@3: $getid3->warning('ReplayGain Track peak from APEtag appears invalid: '.$info_replaygain['track']['peak'].' (original value = "'.$info_ape_items_current['data'][0].'")'); rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'replaygain_album_gain': rlm@3: $info_replaygain['album']['adjustment'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! rlm@3: $info_replaygain['album']['originator'] = 'unspecified'; rlm@3: break; rlm@3: rlm@3: case 'replaygain_album_peak': rlm@3: $info_replaygain['album']['peak'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! rlm@3: $info_replaygain['album']['originator'] = 'unspecified'; rlm@3: if ($info_replaygain['album']['peak'] <= 0) { rlm@3: $getid3->warning('ReplayGain Album peak from APEtag appears invalid: '.$info_replaygain['album']['peak'].' (original value = "'.$info_ape_items_current['data'][0].'")'); rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'mp3gain_undo': rlm@3: list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $info_ape_items_current['data'][0]); rlm@3: $info_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); rlm@3: $info_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); rlm@3: $info_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); rlm@3: break; rlm@3: rlm@3: case 'mp3gain_minmax': rlm@3: list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $info_ape_items_current['data'][0]); rlm@3: $info_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); rlm@3: $info_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); rlm@3: break; rlm@3: rlm@3: case 'mp3gain_album_minmax': rlm@3: list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $info_ape_items_current['data'][0]); rlm@3: $info_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); rlm@3: $info_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); rlm@3: break; rlm@3: rlm@3: case 'tracknumber': rlm@3: foreach ($info_ape_items_current['data'] as $comment) { rlm@3: $info_ape['comments']['track'][] = $comment; rlm@3: } rlm@3: break; rlm@3: rlm@3: default: rlm@3: foreach ($info_ape_items_current['data'] as $comment) { rlm@3: $info_ape['comments'][strtolower($item_key)][] = $comment; rlm@3: } rlm@3: break; rlm@3: } rlm@3: rlm@3: } rlm@3: if (empty($info_replaygain)) { rlm@3: unset($getid3->info['replay_gain']); rlm@3: } rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: protected function ParseAPEheaderFooter($data, &$target) { rlm@3: rlm@3: // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html rlm@3: rlm@3: if (substr($data, 0, 8) != 'APETAGEX') { rlm@3: return false; rlm@3: } rlm@3: rlm@3: // shortcut rlm@3: $target['raw'] = array (); rlm@3: $target_raw = &$target['raw']; rlm@3: rlm@3: $target_raw['footer_tag'] = 'APETAGEX'; rlm@3: rlm@3: getid3_lib::ReadSequence("LittleEndian2Int", $target_raw, $data, 8, rlm@3: array ( rlm@3: 'version' => 4, rlm@3: 'tagsize' => 4, rlm@3: 'tag_items' => 4, rlm@3: 'global_flags' => 4 rlm@3: ) rlm@3: ); rlm@3: $target_raw['reserved'] = substr($data, 24, 8); rlm@3: rlm@3: $target['tag_version'] = $target_raw['version'] / 1000; rlm@3: if ($target['tag_version'] >= 2) { rlm@3: rlm@3: $target['flags'] = $this->ParseAPEtagFlags($target_raw['global_flags']); rlm@3: } rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: protected function ParseAPEtagFlags($raw_flag_int) { rlm@3: rlm@3: // "Note: APE Tags 1.0 do not use any of the APE Tag flags. rlm@3: // All are set to zero on creation and ignored on reading." rlm@3: // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html rlm@3: rlm@3: $target['header'] = (bool) ($raw_flag_int & 0x80000000); rlm@3: $target['footer'] = (bool) ($raw_flag_int & 0x40000000); rlm@3: $target['this_is_header'] = (bool) ($raw_flag_int & 0x20000000); rlm@3: $target['item_contents_raw'] = ($raw_flag_int & 0x00000006) >> 1; rlm@3: $target['read_only'] = (bool) ($raw_flag_int & 0x00000001); rlm@3: rlm@3: $target['item_contents'] = getid3_apetag::APEcontentTypeFlagLookup($target['item_contents_raw']); rlm@3: rlm@3: return $target; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function APEcontentTypeFlagLookup($content_type_id) { rlm@3: rlm@3: static $lookup = array ( rlm@3: 0 => 'utf-8', rlm@3: 1 => 'binary', rlm@3: 2 => 'external', rlm@3: 3 => 'reserved' rlm@3: ); rlm@3: return (isset($lookup[$content_type_id]) ? $lookup[$content_type_id] : 'invalid'); rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function APEtagItemIsUTF8Lookup($item_key) { rlm@3: rlm@3: static $lookup = array ( rlm@3: 'title', rlm@3: 'subtitle', rlm@3: 'artist', rlm@3: 'album', rlm@3: 'debut album', rlm@3: 'publisher', rlm@3: 'conductor', rlm@3: 'track', rlm@3: 'composer', rlm@3: 'comment', rlm@3: 'copyright', rlm@3: 'publicationright', rlm@3: 'file', rlm@3: 'year', rlm@3: 'record date', rlm@3: 'record location', rlm@3: 'genre', rlm@3: 'media', rlm@3: 'related', rlm@3: 'isrc', rlm@3: 'abstract', rlm@3: 'language', rlm@3: 'bibliography' rlm@3: ); rlm@3: return in_array(strtolower($item_key), $lookup); rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: ?>