// // available at http://getid3.sourceforge.net /// ///////////////////////////////////////////////////////////////// // // // getid3.id3v2.php - part of getID3() // // See getid3.readme.txt for more details // // // ///////////////////////////////////////////////////////////////// function getID3v2Filepointer($fd, &$ThisFileInfo) { // Overall tag structure: // +-----------------------------+ // | Header (10 bytes) | // +-----------------------------+ // | Extended Header | // | (variable length, OPTIONAL) | // +-----------------------------+ // | Frames (variable length) | // +-----------------------------+ // | Padding | // | (variable length, OPTIONAL) | // +-----------------------------+ // | Footer (10 bytes, OPTIONAL) | // +-----------------------------+ // Header // ID3v2/file identifier "ID3" // ID3v2 version $04 00 // ID3v2 flags (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x) // ID3v2 size 4 * %0xxxxxxx require_once(GETID3_INCLUDEPATH.'getid3.frames.php'); // ID3v2FrameProcessing() rewind($fd); $header = fread ($fd, 10); if (substr($header, 0, 3) == 'ID3') { $ThisFileInfo['id3v2']['header'] = true; $ThisFileInfo['id3v2']['majorversion'] = ord($header{3}); $ThisFileInfo['id3v2']['minorversion'] = ord($header{4}); } if (isset($ThisFileInfo['id3v2']['header']) && ($ThisFileInfo['id3v2']['majorversion'] <= 4)) { // this script probably won't correctly parse ID3v2.5.x and above. $id3_flags = BigEndian2Bin($header{5}); switch ($ThisFileInfo['id3v2']['majorversion']) { case 2: // %ab000000 in v2.2 $ThisFileInfo['id3v2']['flags']['unsynch'] = $id3_flags{0}; // a - Unsynchronisation $ThisFileInfo['id3v2']['flags']['compression'] = $id3_flags{1}; // b - Compression break; case 3: // %abc00000 in v2.3 $ThisFileInfo['id3v2']['flags']['unsynch'] = $id3_flags{0}; // a - Unsynchronisation $ThisFileInfo['id3v2']['flags']['exthead'] = $id3_flags{1}; // b - Extended header $ThisFileInfo['id3v2']['flags']['experim'] = $id3_flags{2}; // c - Experimental indicator break; case 4: // %abcd0000 in v2.4 $ThisFileInfo['id3v2']['flags']['unsynch'] = $id3_flags{0}; // a - Unsynchronisation $ThisFileInfo['id3v2']['flags']['exthead'] = $id3_flags{1}; // b - Extended header $ThisFileInfo['id3v2']['flags']['experim'] = $id3_flags{2}; // c - Experimental indicator $ThisFileInfo['id3v2']['flags']['isfooter'] = $id3_flags{3}; // d - Footer present break; } $ThisFileInfo['id3v2']['headerlength'] = BigEndian2Int(substr($header, 6, 4), 1) + ID3v2HeaderLength($ThisFileInfo['id3v2']['majorversion']); // Extended Header if (isset($ThisFileInfo['id3v2']['flags']['exthead']) && $ThisFileInfo['id3v2']['flags']['exthead']) { // Extended header size 4 * %0xxxxxxx // Number of flag bytes $01 // Extended Flags $xx // Where the 'Extended header size' is the size of the whole extended header, stored as a 32 bit synchsafe integer. $extheader = fread ($fd, 4); $ThisFileInfo['id3v2']['extheaderlength'] = BigEndian2Int($extheader, 1); // The extended flags field, with its size described by 'number of flag bytes', is defined as: // %0bcd0000 // b - Tag is an update // Flag data length $00 // c - CRC data present // Flag data length $05 // Total frame CRC 5 * %0xxxxxxx // d - Tag restrictions // Flag data length $01 $extheaderflagbytes = fread ($fd, 1); $extheaderflags = fread ($fd, $extheaderflagbytes); $id3_exthead_flags = BigEndian2Bin(substr($header, 5, 1)); $ThisFileInfo['id3v2']['exthead_flags']['update'] = substr($id3_exthead_flags, 1, 1); $ThisFileInfo['id3v2']['exthead_flags']['CRC'] = substr($id3_exthead_flags, 2, 1); if ($ThisFileInfo['id3v2']['exthead_flags']['CRC']) { $extheaderrawCRC = fread ($fd, 5); $ThisFileInfo['id3v2']['exthead_flags']['CRC'] = BigEndian2Int($extheaderrawCRC, 1); } $ThisFileInfo['id3v2']['exthead_flags']['restrictions'] = substr($id3_exthead_flags, 3, 1); if ($ThisFileInfo['id3v2']['exthead_flags']['restrictions']) { // Restrictions %ppqrrstt $extheaderrawrestrictions = fread ($fd, 1); $ThisFileInfo['id3v2']['exthead_flags']['restrictions_tagsize'] = (bindec('11000000') & ord($extheaderrawrestrictions)) >> 6; // p - Tag size restrictions $ThisFileInfo['id3v2']['exthead_flags']['restrictions_textenc'] = (bindec('00100000') & ord($extheaderrawrestrictions)) >> 5; // q - Text encoding restrictions $ThisFileInfo['id3v2']['exthead_flags']['restrictions_textsize'] = (bindec('00011000') & ord($extheaderrawrestrictions)) >> 3; // r - Text fields size restrictions $ThisFileInfo['id3v2']['exthead_flags']['restrictions_imgenc'] = (bindec('00000100') & ord($extheaderrawrestrictions)) >> 2; // s - Image encoding restrictions $ThisFileInfo['id3v2']['exthead_flags']['restrictions_imgsize'] = (bindec('00000011') & ord($extheaderrawrestrictions)) >> 0; // t - Image size restrictions } } // end extended header // Frames // All ID3v2 frames consists of one frame header followed by one or more // fields containing the actual information. The header is always 10 // bytes and laid out as follows: // // Frame ID $xx xx xx xx (four characters) // Size 4 * %0xxxxxxx // Flags $xx xx $sizeofframes = $ThisFileInfo['id3v2']['headerlength'] - ID3v2HeaderLength($ThisFileInfo['id3v2']['majorversion']); if (isset($ThisFileInfo['id3v2']['extheaderlength'])) { $sizeofframes -= $ThisFileInfo['id3v2']['extheaderlength']; } if (isset($ThisFileInfo['id3v2']['flags']['isfooter']) && $ThisFileInfo['id3v2']['flags']['isfooter']) { $sizeofframes -= 10; // footer takes last 10 bytes of ID3v2 header, after frame data, before audio } if ($sizeofframes > 0) { $framedata = fread($fd, $sizeofframes); // read all frames from file into $framedata variable // if entire frame data is unsynched, de-unsynch it now (ID3v2.3.x) if (isset($ThisFileInfo['id3v2']['flags']['unsynch']) && $ThisFileInfo['id3v2']['flags']['unsynch'] && ($ThisFileInfo['id3v2']['majorversion'] <= 3)) { $framedata = DeUnSynchronise($framedata); } // [in ID3v2.4.0] Unsynchronisation [S:6.1] is done on frame level, instead // of on tag level, making it easier to skip frames, increasing the streamability // of the tag. The unsynchronisation flag in the header [S:3.1] indicates that // there exists an unsynchronised frame, while the new unsynchronisation flag in // the frame header [S:4.1.2] indicates unsynchronisation. $framedataoffset = 10; // how many bytes into the stream - start from after the 10-byte header while (isset($framedata) && (strlen($framedata) > 0)) { // cycle through until no more frame data is left to parse if ($ThisFileInfo['id3v2']['majorversion'] == 2) { // Frame ID $xx xx xx (three characters) // Size $xx xx xx (24-bit integer) // Flags $xx xx $frame_header = substr($framedata, 0, 6); // take next 6 bytes for header $framedata = substr($framedata, 6); // and leave the rest in $framedata $frame_name = substr($frame_header, 0, 3); $frame_size = BigEndian2Int(substr($frame_header, 3, 3), 0); $frame_flags = ''; // not used for anything, just to avoid E_NOTICEs } elseif ($ThisFileInfo['id3v2']['majorversion'] > 2) { // Frame ID $xx xx xx xx (four characters) // Size $xx xx xx xx (32-bit integer in v2.3, 28-bit synchsafe in v2.4+) // Flags $xx xx $frame_header = substr($framedata, 0, 10); // take next 10 bytes for header $framedata = substr($framedata, 10); // and leave the rest in $framedata $frame_name = substr($frame_header, 0, 4); if ($ThisFileInfo['id3v2']['majorversion'] == 3) { $frame_size = BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer } else { // ID3v2.4+ $frame_size = BigEndian2Int(substr($frame_header, 4, 4), 1); // 32-bit synchsafe integer (28-bit value) } if ($frame_size < (strlen($framedata) + 4)) { $nextFrameID = substr($framedata, $frame_size, 4); if (IsValidID3v2FrameName($nextFrameID, $ThisFileInfo['id3v2']['majorversion'])) { // next frame is OK } elseif (($frame_name == chr(0).'MP3') || ($frame_name == chr(0).chr(0).'MP') || ($frame_name == ' MP3') || ($frame_name == 'MP3e')) { // MP3ext known broken frames - "ok" for the purposes of this test } elseif (($ThisFileInfo['id3v2']['majorversion'] == 4) && (IsValidID3v2FrameName(substr($framedata, BigEndian2Int(substr($frame_header, 4, 4), 0), 4), 3))) { $ThisFileInfo['warning'] .= "\n".'ID3v2 tag written as ID3v2.4, but with non-synchsafe integers (ID3v2.3 style). Older versions of Helium2 (www.helium2.com) is a known culprit of this. Tag has been parsed as ID3v2.3'; $ThisFileInfo['id3v2']['majorversion'] = 3; $frame_size = BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer } } $frame_flags = BigEndian2Bin(substr($frame_header, 8, 2)); } if ((($ThisFileInfo['id3v2']['majorversion'] == 2) && ($frame_name == chr(0).chr(0).chr(0))) || ($frame_name == chr(0).chr(0).chr(0).chr(0))) { // padding encountered $ThisFileInfo['id3v2']['padding']['start'] = $framedataoffset; $ThisFileInfo['id3v2']['padding']['length'] = strlen($framedata); $ThisFileInfo['id3v2']['padding']['valid'] = true; for ($i = 0; $i < $ThisFileInfo['id3v2']['padding']['length']; $i++) { if (substr($framedata, $i, 1) != chr(0)) { $ThisFileInfo['id3v2']['padding']['valid'] = false; $ThisFileInfo['id3v2']['padding']['errorpos'] = $ThisFileInfo['id3v2']['padding']['start'] + $i; break; } } break; // skip rest of ID3v2 header } if ($frame_name == 'COM ') { $ThisFileInfo['warning'] .= "\n".'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace(chr(0), ' ', $frame_name).'", '.$ThisFileInfo['id3v2']['majorversion'].'))). [Note: this particular error has been known to happen with tags edited by iTunes (versions "X v2.0.3", "v3.0.1" are known-guilty, probably others too)]'; $frame_name = 'COMM'; } if (($frame_size <= strlen($framedata)) && (IsValidID3v2FrameName($frame_name, $ThisFileInfo['id3v2']['majorversion']))) { $ThisFileInfo['id3v2']["$frame_name"]['data'] = substr($framedata, 0, $frame_size); $ThisFileInfo['id3v2']["$frame_name"]['datalength'] = CastAsInt($frame_size); $ThisFileInfo['id3v2']["$frame_name"]['dataoffset'] = $framedataoffset; $framedata = substr($framedata, $frame_size); // in getid3.frames.php - this function does all the FrameID-level parsing ID3v2FrameProcessing($frame_name, $frame_flags, $ThisFileInfo); } else { // invalid frame length or FrameID if ($frame_size <= strlen($framedata)) { if (IsValidID3v2FrameName(substr($framedata, $frame_size, 4), $ThisFileInfo['id3v2']['majorversion'])) { // next frame is valid, just skip the current frame $framedata = substr($framedata, $frame_size); $InvalidFrameMessageType = 'warning'; $InvalidFrameMessageText = ' Next frame is valid, skipping current frame.'; } else { // next frame is invalid too, abort processing unset($framedata); $InvalidFrameMessageType = 'error'; $InvalidFrameMessageText = ' Next frame is also invalid, aborting processing.'; } } elseif ($frame_size == strlen($framedata)) { // this is the last frame, just skip $InvalidFrameMessageType = 'warning'; $InvalidFrameMessageText = ' This was the last frame.'; } else { // next frame is invalid too, abort processing unset($framedata); $InvalidFrameMessageType = 'error'; $InvalidFrameMessageText = ' Invalid frame size, aborting.'; } if (!IsValidID3v2FrameName($frame_name, $ThisFileInfo['id3v2']['majorversion'])) { switch ($frame_name) { case chr(0).chr(0).'MP': case chr(0).'MP3': case ' MP3': case 'MP3e': case chr(0).'MP': case ' MP': case 'MP3': $InvalidFrameMessageType = 'warning'; $ThisFileInfo["$InvalidFrameMessageType"] .= "\n".'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace(chr(0), ' ', $frame_name).'", '.$ThisFileInfo['id3v2']['majorversion'].'))). [Note: this particular error has been known to happen with tags edited by "MP3ext (www.mutschler.de/mp3ext/)"]'; break; default: $ThisFileInfo["$InvalidFrameMessageType"] .= "\n".'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace(chr(0), ' ', $frame_name).'", '.$ThisFileInfo['id3v2']['majorversion'].'))).'; break; } } elseif ($frame_size > strlen($framedata)){ $ThisFileInfo["$InvalidFrameMessageType"] .= "\n".'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].' tag). (ERROR: $frame_size ('.$frame_size.') > strlen($framedata) ('.strlen($framedata).')).'; } else { $ThisFileInfo["$InvalidFrameMessageType"] .= "\n".'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].' tag).'; } $ThisFileInfo["$InvalidFrameMessageType"] .= $InvalidFrameMessageText; } $framedataoffset += ($frame_size + ID3v2HeaderLength($ThisFileInfo['id3v2']['majorversion'])); } } // Footer // The footer is a copy of the header, but with a different identifier. // ID3v2 identifier "3DI" // ID3v2 version $04 00 // ID3v2 flags %abcd0000 // ID3v2 size 4 * %0xxxxxxx if (isset($ThisFileInfo['id3v2']['flags']['isfooter']) && $ThisFileInfo['id3v2']['flags']['isfooter']) { $footer = fread ($fd, 10); if (substr($footer, 0, 3) == '3DI') { $ThisFileInfo['id3v2']['footer'] = true; $ThisFileInfo['id3v2']['majorversion_footer'] = ord(substr($footer, 3, 1)); $ThisFileInfo['id3v2']['minorversion_footer'] = ord(substr($footer, 4, 1)); } if ($ThisFileInfo['id3v2']['majorversion_footer'] <= 4) { $id3_flags = BigEndian2Bin(substr($footer, 5, 1)); $ThisFileInfo['id3v2']['flags']['unsynch_footer'] = substr($id3_flags, 0, 1); $ThisFileInfo['id3v2']['flags']['extfoot_footer'] = substr($id3_flags, 1, 1); $ThisFileInfo['id3v2']['flags']['experim_footer'] = substr($id3_flags, 2, 1); $ThisFileInfo['id3v2']['flags']['isfooter_footer'] = substr($id3_flags, 3, 1); $ThisFileInfo['id3v2']['footerlength'] = BigEndian2Int(substr($footer, 6, 4), 1); } } // end footer if (isset($ThisFileInfo['id3v2']['comments']['genre'])) { foreach ($ThisFileInfo['id3v2']['comments']['genre'] as $key => $value) { unset($ThisFileInfo['id3v2']['comments']['genre'][$key]); $ThisFileInfo['id3v2']['comments'] = array_merge_noclobber($ThisFileInfo['id3v2']['comments'], ParseID3v2GenreString($value)); } } if (isset($ThisFileInfo['id3v2']['comments']['track'])) { foreach ($ThisFileInfo['id3v2']['comments']['track'] as $key => $value) { if (strstr($value, '/')) { list($ThisFileInfo['id3v2']['comments']['track'][$key], $ThisFileInfo['id3v2']['comments']['totaltracks'][$key]) = explode('/', $ThisFileInfo['id3v2']['comments']['track'][$key]); } // Convert track number to integer (ID3v2 track numbers could be returned as a // string ('03' for example) - this will ensure all track numbers are integers $ThisFileInfo['id3v2']['comments']['track'][$key] = intval($ThisFileInfo['id3v2']['comments']['track'][$key]); } } } else { // MajorVersion is > 4, or no ID3v2 header present if (isset($ThisFileInfo['id3v2']['header'])) { // MajorVersion is > 4 $ThisFileInfo['error'] .= "\n".'this script only parses up to ID3v2.4.x - this tag is ID3v2.'.$ThisFileInfo['id3v2']['majorversion'].'.'.$ThisFileInfo['id3v2']['minorversion']; } else { // no ID3v2 header present - this is fine, just don't process anything. } } return true; } function ParseID3v2GenreString($genrestring) { // Parse genres into arrays of genreName and genreID // ID3v2.2.x, ID3v2.3.x: '(21)' or '(4)Eurodisco' or '(51)(39)' or '(55)((I think...)' // ID3v2.4.x: '21' $00 'Eurodisco' $00 require_once(GETID3_INCLUDEPATH.'getid3.id3.php'); $returnarray = null; if (strpos($genrestring, chr(0)) !== false) { $unprocessed = trim($genrestring); // trailing nulls will cause an infinite loop. $genrestring = ''; while (strpos($unprocessed, chr(0)) !== false) { // convert null-seperated v2.4-format into v2.3 ()-seperated format $endpos = strpos($unprocessed, chr(0)); $genrestring .= '('.substr($unprocessed, 0, $endpos).')'; $unprocessed = substr($unprocessed, $endpos + 1); } unset($unprocessed); } while (strpos($genrestring, '(') !== false) { $startpos = strpos($genrestring, '('); $endpos = strpos($genrestring, ')'); if (substr($genrestring, $startpos + 1, 1) == '(') { $genrestring = substr($genrestring, 0, $startpos).substr($genrestring, $startpos + 1); $endpos--; } $element = substr($genrestring, $startpos + 1, $endpos - ($startpos + 1)); $genrestring = substr($genrestring, 0, $startpos).substr($genrestring, $endpos + 1); if (LookupGenre($element) !== '') { // $element is a valid genre id/abbreviation if (!is_array($returnarray['genre']) || !in_array(LookupGenre($element), $returnarray['genre'])) { // avoid duplicate entires if (($element == 'CR') && ($element == 'RX')) { $returnarray['genreid'][] = $element; } else { $returnarray['genreid'][] = (int) $element; } $returnarray['genre'][] = LookupGenre($element); } } else { if (!is_array($returnarray['genre']) || !in_array($element, $returnarray['genre'])) { // avoid duplicate entires $returnarray['genreid'][] = ''; $returnarray['genre'][] = $element; } } } if ($genrestring) { if (!is_array($returnarray['genre']) || !in_array($genrestring, $returnarray['genre'])) { // avoid duplicate entires $returnarray['genreid'][] = ''; $returnarray['genre'][] = $genrestring; } } return $returnarray; } ?>