filename = $file; $this->encoding = $encoding ?: Config::get('site_charset'); /* These are needed for the filename mojo */ $this->_file_pattern = $file_pattern; $this->_dir_pattern = $dir_pattern; // FIXME: This looks ugly and probably wrong if(strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { $this->_pathinfo = str_replace('%3A', ':', urlencode($this->filename)); $this->_pathinfo = pathinfo(str_replace('%5C', '\\', $this->_pathinfo)); } else { $this->_pathinfo = pathinfo(str_replace('%2F', '/', urlencode($this->filename))); } $this->_pathinfo['extension'] = strtolower($this->_pathinfo['extension']); // Initialize getID3 engine $this->_getID3 = new getID3(); $this->_getID3->option_md5_data = false; $this->_getID3->option_md5_data_source = false; $this->_getID3->option_tags_html = false; $this->_getID3->option_extra_info = true; $this->_getID3->option_tag_lyrics3 = true; $this->_getID3->option_tags_process = true; $this->_getID3->encoding = $this->encoding; // get id3tag encoding (try to work around off-spec id3v1 tags) try { $this->_raw = $this->_getID3->analyze($file); } catch (Exception $error) { debug_event('getID3', "Broken file detected: $file: " . $error->message, 1); $this->_broken = true; return false; } if (Config::get('mb_detect_order')) { $mb_order = Config::get('mb_detect_order'); } elseif (function_exists('mb_detect_order')) { $mb_order = implode(", ", mb_detect_order()); } else { $mb_order = "auto"; } $test_tags = array('artist', 'album', 'genre', 'title'); if ($encoding_id3v1) { $this->encoding_id3v1 = $encoding_id3v1; } else { foreach ($test_tags as $tag) { if ($value = $this->_raw['id3v1'][$tag]) { $tags[$tag] = $value; } } $this->encoding_id3v1 = self::_detect_encoding($tags, $mb_order); } if (Config::get('getid3_detect_id3v2_encoding')) { // The user has told us to be moronic, so let's do that thing foreach ($test_tags as $tag) { if ($value = $this->_raw['id3v2']['comments'][$tag]) { $tags[$tag] = $value; } } $this->encoding_id3v2 = self::_detect_encoding($tags, $mb_order); $this->_getID3->encoding_id3v2 = $this->encoding_id3v2; } $this->_getID3->encoding_id3v1 = $this->encoding_id3v1; } /** * _detect_encoding * * Takes an array of tags and attempts to automatically detect their * encoding. */ private static function _detect_encoding($tags, $mb_order) { if (function_exists('mb_detect_encoding')) { $encodings = array(); foreach ($tags as $tag) { $encodings[mb_detect_encoding($tag, $mb_order, true)]++; } debug_event('vainfo', 'encoding detection: ' . json_encode($encodings), 5); $high = 0; foreach ($encodings as $key => $value) { if ($value > $high) { $encoding = $key; $high = $value; } } if ($encoding != 'ASCII' && $encoding != '0') { return $encoding; } else { return 'ISO-8859-1'; } } return 'ISO-8859-1'; } /** * get_info * * This function runs the various steps to gathering the metadata */ public function get_info() { // If this is broken, don't waste time figuring it out a second // time, just return their rotting carcass of a media file. if ($this->_broken) { $this->tags = $this->set_broken(); return true; } try { $this->_raw = $this->_getID3->analyze($this->filename); } catch (Exception $error) { debug_event('getID2', 'Unable to catalog file: ' . $error->message, 1); } /* Figure out what type of file we are dealing with */ $this->type = $this->_get_type(); $enabled_sources = (array)Config::get('metadata_order'); if (in_array('filename', $enabled_sources)) { $this->tags['filename'] = $this->_parse_filename($this->filename); } if (in_array('getID3', $enabled_sources)) { $this->tags['getID3'] = $this->_get_tags(); } $this->_get_plugin_tags(); } // get_info /** * get_tag_type * * This takes the result set and the tag_order defined in your config * file and tries to figure out which tag type(s) it should use. If your * tag_order doesn't match anything then it throws up its hands and uses * everything in random order. */ public static function get_tag_type($results, $config_key = 'metadata_order') { $order = (array)Config::get($config_key); // Iterate through the defined key order adding them to an ordered array. foreach($order as $key) { if ($results[$key]) { $returned_keys[] = $key; } } // If we didn't find anything then default to everything. if (!isset($returned_keys)) { $returned_keys = array_keys($results); $returned_keys = sort($returned_keys); } // Unless they explicitly set it, add bitrate/mode/mime/etc. if (!in_array('general', $returned_keys)) { $returned_keys[] = 'general'; } return $returned_keys; } /** * clean_tag_info * * This function takes the array from vainfo along with the * key we've decided on and the filename and returns it in a * sanitized format that ampache can actually use */ public static function clean_tag_info($results, $keys, $filename = null) { $info = array(); $info['file'] = $filename; // Iteration! foreach ($keys as $key) { $tags = $results[$key]; $info['file'] = $info['file'] ?: $tags['file']; $info['bitrate'] = $info['bitrate'] ?: intval($tags['bitrate']); $info['rate'] = $info['rate'] ?: intval($tags['rate']); $info['mode'] = $info['mode'] ?: $tags['mode']; $info['size'] = $info['size'] ?: $tags['size']; $info['mime'] = $info['mime'] ?: $tags['mime']; $info['encoding'] = $info['encoding'] ?: $tags['encoding']; $info['rating'] = $info['rating'] ?: $tags['rating']; $info['time'] = $info['time'] ?: intval($tags['time']); $info['channels'] = $info['channels'] ?: $tags['channels']; $info['title'] = $info['title'] ?: stripslashes(trim($tags['title'])); $info['year'] = $info['year'] ?: intval($tags['year']); $info['disk'] = $info['disk'] ?: intval($tags['disk']); $info['totaldisks'] = $info['totaldisks'] ?: intval($tags['totaldisks']); $info['artist'] = $info['artist'] ?: trim($tags['artist']); $info['album'] = $info['album'] ?: trim($tags['album']); // multiple genre support if ((!$info['genre']) && $tags['genre']) { if (!is_array($tags['genre'])) { // not all tag formats will return an array, but we need one $info['genre'][] = trim($tags['genre']); } else { // if we trim the array we lose everything after 1st entry foreach ($tags['genre'] as $genre) { $info['genre'][] = trim($genre); } } } $info['mb_trackid'] = $info['mb_trackid'] ?: trim($tags['mb_trackid']); $info['mb_albumid'] = $info['mb_albumid'] ?: trim($tags['mb_albumid']); $info['mb_artistid'] = $info['mb_artistid'] ?: trim($tags['mb_artistid']); $info['language'] = $info['language'] ?: Dba::escape($tags['language']); $info['lyrics'] = $info['lyrics'] ?: str_replace( array("\r\n","\r","\n"), '
', strip_tags($tags['lyrics'])); $info['track'] = $info['track'] ?: intval($tags['track']); $info['resolution_x'] = $info['resolution_x'] ?: intval($tags['resolution_x']); $info['resolution_y'] = $info['resolution_y'] ?: intval($tags['resolution_y']); $info['audio_codec'] = $info['audio_codec'] ?: Dba::escape($tags['audio_codec']); $info['video_codec'] = $info['video_codec'] ?: Dba::escape($tags['video_codec']); } // Some things set the disk number even though there aren't multiple if ($info['totaldisks'] == 1 && $info['disk'] == 1) { unset($info['disk']); unset($info['totaldisks']); } return $info; } /** * _get_type * * This function takes the raw information and figures out what type of * file we are dealing with. */ private function _get_type() { // There are a few places that the file type can come from, in the end // we trust the encoding type. if ($type = $this->_raw['video']['dataformat']) { return $this->_clean_type($type); } if ($type = $this->_raw['audio']['streams']['0']['dataformat']) { return $this->_clean_type($type); } if ($type = $this->_raw['audio']['dataformat']) { return $this->_clean_type($type); } if ($type = $this->_raw['fileformat']) { return $this->_clean_type($type); } return false; } /** * _get_tags * * This processes the raw getID3 output and bakes it. */ private function _get_tags() { $results = array(); // The tags can come in many different shapes and colors // depending on the encoding time of day and phase of the moon. foreach ($this->_raw['tags'] as $key => $tag_array) { switch ($key) { case 'ape': case 'avi': case 'flv': case 'matroska': debug_event('vainfo', 'Cleaning ' . $key, 5); $parsed = $this->_cleanup_generic($tag_array); break; case 'vorbiscomment': debug_event('vainfo', 'Cleaning vorbis', 5); $parsed = $this->_cleanup_vorbiscomment($tag_array); break; case 'id3v1': debug_event('vainfo', 'Cleaning id3v1', 5); $parsed = $this->_cleanup_id3v1($tag_array); break; case 'id3v2': debug_event('vainfo', 'Cleaning id3v2', 5); $parsed = $this->_cleanup_id3v2($tag_array); break; case 'quicktime': debug_event('vainfo', 'Cleaning quicktime', 5); $parsed = $this->_cleanup_quicktime($tag_array); break; case 'riff': debug_event('vainfo', 'Cleaning riff', 5); $parsed = $this->_cleanup_riff($tag_array); break; case 'mpg': case 'mpeg': $key = 'mpeg'; debug_event('vainfo', 'Cleaning MPEG', 5); $parsed = $this->_cleanup_generic($tag_array); break; case 'asf': case 'wmv': $key = 'asf'; debug_event('vainfo', 'Cleaning WMV/WMA/ASF', 5); $parsed = $this->_cleanup_generic($tag_array); break; case 'lyrics3': debug_event('vainfo', 'Cleaning lyrics3', 5); $parsed = $this->_cleanup_lyrics($tag_array); break; default: debug_event('vainfo', 'Cleaning unrecognised tag type ' . $key . ' for file ' . $this->filename, 5); $parsed = $this->_cleanup_generic($tag_array); break; } $results[$key] = $parsed; } $results['general'] = $this->_parse_general($this->_raw); $cleaned = self::clean_tag_info($results, self::get_tag_type($results, 'getid3_tag_order'), $this->filename); $cleaned['raw'] = $results; return $cleaned; } /** * _get_plugin_tags * * Get additional metadata from plugins */ private function _get_plugin_tags() { $tag_order = Config::get('metadata_order'); if (!is_array($tag_order)) { $tag_order = array($tag_order); } $plugin_names = Plugin::get_plugins('get_metadata'); foreach ($tag_order as $tag_source) { if (in_array($tag_source, $plugin_names)) { $plugin = new Plugin($tag_source); if ($plugin->load()) { $this->tags[$tag_source] = $plugin->_plugin->get_metadata(self::clean_tag_info($this->tags, self::get_tag_type($this->tags), $this->filename)); } } } } /** * _parse_general * * Gather and return the general information about a file (vbr/cbr, * sample rate, channels, etc.) */ private function _parse_general($tags) { $parsed = array(); $parsed['title'] = urldecode($this->_pathinfo['filename']); $parsed['mode'] = $tags['audio']['bitrate_mode']; if ($parsed['mode'] == 'con') { $parsed['mode'] = 'cbr'; } $parsed['bitrate'] = $tags['audio']['bitrate']; $parsed['channels'] = intval($tags['audio']['channels']); $parsed['rate'] = intval($tags['audio']['sample_rate']); $parsed['size'] = intval($tags['filesize']); $parsed['encoding'] = $tags['encoding']; $parsed['mime'] = $tags['mime_type']; $parsed['time'] = $tags['playtime_seconds']; $parsed['video_codec'] = $tags['video']['fourcc']; $parsed['audio_codec'] = $tags['audio']['dataformat']; $parsed['resolution_x'] = $tags['video']['resolution_x']; $parsed['resolution_y'] = $tags['video']['resolution_y']; return $parsed; } /** * _clean_type * This standardizes the type that we are given into a recognized type. */ private function _clean_type($type) { switch ($type) { case 'mp3': case 'mp2': case 'mpeg3': return 'mp3'; break; case 'vorbis': return 'ogg'; break; case 'flac': case 'flv': case 'mpg': case 'mpeg': case 'asf': case 'wmv': case 'avi': case 'quicktime': return $type; default: /* Log the fact that we couldn't figure it out */ debug_event('vainfo','Unable to determine file type from ' . $type . ' on file ' . $this->filename,'5'); return $type; break; } } /** * _cleanup_generic * * This does generic cleanup. */ private function _cleanup_generic($tags) { $parsed = array(); foreach ($tags as $tagname => $data) { switch ($tagname) { case 'genre': // Pass the array through $parsed[$tagname] = $data; break; default: $parsed[$tagname] = $data[0]; break; } } return $parsed; } /** * _cleanup_lyrics * * This is supposed to handle lyrics3. FIXME: does it? */ private function _cleanup_lyrics($tags) { $parsed = array(); foreach ($tags as $tag => $data) { if ($tag == 'unsynchedlyrics' || $tag == 'unsynchronised lyric') { $tag = 'lyrics'; } $parsed[$tag] = $data[0]; } return $parsed; } /** * _cleanup_vorbiscomment * * Standardises tag names from vorbis. */ private function _cleanup_vorbiscomment($tags) { $parsed = array(); foreach ($tags as $tag => $data) { switch ($tag) { case 'genre': // Pass the array through $parsed[$tag] = $data; break; case 'tracknumber': $parsed['track'] = $data[0]; break; case 'discnumber': $elements = explode('/', $data[0]); $parsed['disk'] = $elements[0]; $parsed['totaldisks'] = $elements[1]; break; case 'date': $parsed['year'] = $data[0]; break; default: $parsed[$tag] = $data[0]; break; } } return $parsed; } /** * _cleanup_id3v1 * * Doesn't do much. */ private function _cleanup_id3v1($tags) { $parsed = array(); foreach ($tags as $tag => $data) { // This is our baseline for naming so everything's already right, // we just need to shuffle off the array. $parsed[$tag] = $data[0]; } return $parsed; } /** * _cleanup_id3v2 * * Whee, v2! */ private function _cleanup_id3v2($tags) { $parsed = array(); foreach ($tags as $tag => $data) { switch ($tag) { case 'genre': // Pass the array through $parsed[$tag] = $data; break; case 'part_of_a_set': $elements = explode('/', $data[0]); $parsed['disk'] = $elements[0]; $parsed['totaldisks'] = $elements[1]; break; case 'track_number': $parsed['track'] = $data[0]; break; case 'comments': $parsed['comment'] = $data[0]; break; default: $parsed[$tag] = $data[0]; break; } } // getID3 doesn't do all the parsing we need, so grab the raw data $id3v2 = $this->_raw['id3v2']; if(!empty($id3v2['UFID'])) { // Find the MBID for the track foreach ($id3v2['UFID'] as $ufid) { if ($ufid['ownerid'] == 'http://musicbrainz.org') { $parsed['mb_trackid'] = $ufid['data']; } } // Find the MBIDs for the album and artist foreach ($id3v2['TXXX'] as $txxx) { switch ($txxx['description']) { case 'MusicBrainz Album Id': $parsed['mb_albumid'] = $txxx['data']; break; case 'MusicBrainz Artist Id': $parsed['mb_artistid'] = $txxx['data']; break; } } } // Find the rating if (is_array($id3v2['POPM'])) { foreach ($id3v2['POPM'] as $popm) { if (array_key_exists('email', $popm) && $user = User::get_from_email($popm['email'])) { // Ratings are out of 255; scale it $parsed['rating'][$user->id] = $popm['rating'] / 255 * 5; } } } return $parsed; } /** * _cleanup_riff */ private function _cleanup_riff($tags) { $parsed = array(); foreach ($tags as $tag => $data) { switch ($tag) { case 'product': $parsed['album'] = $data[0]; break; default: $parsed[$tag] = $data[0]; break; } } return $parsed; } /** * _cleanup_quicktime */ private function _cleanup_quicktime($tags) { $parsed = array(); foreach ($tags as $tag => $data) { switch ($tag) { case 'creation_date': if (strlen($data['0']) > 4) { // Weird date format, attempt to normalize it $data[0] = date('Y', strtotime($data[0])); } $parsed['year'] = $data[0]; break; case 'MusicBrainz Track Id': $parsed['mb_trackid'] = $data[0]; break; case 'MusicBrainz Album Id': $parsed['mb_albumid'] = $data[0]; break; case 'MusicBrainz Artist Id': $parsed['mb_artistid'] = $data[0]; break; default: $parsed[$tag] = $data[0]; break; } } return $parsed; } /** * _parse_filename * * This function uses the file and directory patterns to pull out extra tag * information. */ private function _parse_filename($filename) { $results = array(); // Correctly detect the slash we need to use here if (strpos($filename, '/') !== false) { $slash_type = '/'; } else { $slash_type = '\\'; } // Combine the patterns $pattern = preg_quote($this->_dir_pattern) . $slash_type . preg_quote($this->_file_pattern); // Pull out the pattern codes into an array preg_match_all('/\%\w/', $pattern, $elements); // Mangle the pattern by turning the codes into regex captures $pattern = preg_replace('/\%[Ty]/', '([0-9]+?)', $pattern); $pattern = preg_replace('/\%\w/', '(.+?)', $pattern); $pattern = str_replace('/', '\/', $pattern); $pattern = str_replace(' ', '\s', $pattern); $pattern = '/' . $pattern . '\..+$/'; // Pull out our actual matches preg_match($pattern, $filename, $matches); // The first element is the full match text $matched = array_shift($matches); debug_event('vainfo', $pattern . ' matched ' . $matched . ' on ' . $filename, 5); // Iterate over what we found foreach ($matches as $key => $value) { $new_key = translate_pattern_code($elements['0'][$key]); if ($new_key) { $results[$new_key] = $value; } } $results['title'] = $results['title'] ?: basename($filename); $results['size'] = filesize($filename); return $results; } /** * set_broken * * This fills all tag types with Unknown (Broken) * * @return array Return broken title, album, artist */ public function set_broken() { /* Pull In the config option */ $order = Config::get('tag_order'); if (!is_array($order)) { $order = array($order); } $key = array_shift($order); $broken[$key]['title'] = '**BROKEN** ' . $this->filename; $broken[$key]['album'] = 'Unknown (Broken)'; $broken[$key]['artist'] = 'Unknown (Broken)'; return $broken; } // set_broken } // end class vainfo ?>