From 27bb2c55e9b4a6612ef6f6285c21ffbbbfb59960 Mon Sep 17 00:00:00 2001 From: wapmorgan Date: Thu, 16 Aug 2018 00:50:35 +0300 Subject: [PATCH] Simple update --- bin/mp3scan | 223 +++++++++++++++++++++++++++++++++++------------- composer.json | 6 ++ src/Mp3Info.php | 140 +++++++++++++----------------- 3 files changed, 230 insertions(+), 139 deletions(-) diff --git a/bin/mp3scan b/bin/mp3scan index 8adeba5..3c7544f 100644 --- a/bin/mp3scan +++ b/bin/mp3scan @@ -1,5 +1,7 @@ #!/usr/bin/php $maxLength) { - return substr($string, 0, $maxLength-3).'...'; - } - return $string; -} + /** @var array */ + protected $widths = array( + 'filename' => 0.3, + 'duration' => 6, + 'bitRate' => 6, + 'sampleRate' => 6, + 'song' => 0.13, + 'artist' => 0.125, + 'track' => 5, + 'parseTime' => 4, + ); -function analyze($filename, &$total_duration, &$total_parse_time, $id3v2 = false) { - if (!is_readable($filename)) return; - try { - $audio = new Mp3Info($filename, true); - } catch (Exception $e) { - var_dump($filename.': '.$e->getMessage()); - return null; - } - echo sprintf('%15s | %4s | %7s | %0.1fkHz | %-11s | %-10s | %5d | %.5f', substrIfLonger(basename($filename), 15), formatTime($audio->duration), $audio->isVbr ? 'vbr' : ($audio->bitRate / 1000).'kbps', ($audio->sampleRate / 1000), isset($audio->tags1['song']) ? substrIfLonger($audio->tags1['song'], 11) : null, isset($audio->tags1['artist']) ? substrIfLonger($audio->tags1['artist'], 10) : null, isset($audio->tags1['track']) ? substrIfLonger($audio->tags1['track'], 5) : null, $audio->_parsingTime).PHP_EOL; - if ($id3v2 && !empty($audio->tags2)) { - foreach ($audio->tags2 as $tag=>$value) { - echo ' '.$tag.': '; - if ($tag == 'COMM') { - foreach ($value as $lang => $comment) { - echo '['.$lang.'] '.$comment['short'].'; '.$comment['actual'].PHP_EOL; + /** @var string */ + protected $songRowTempalte; + + /** @var bool */ + protected $compareWithId3; + + protected $totalDuration = 0; + protected $totalParseTime = 0; + protected $totalId3ParseTime = 0; + + /** + * @param array $fileNames + */ + public function run(array $fileNames) + { + $this->adjustOutputSize(); + $this->songRowTempalte = '%'.$this->widths['filename'].'s | %'.$this->widths['duration'].'s | %'.$this->widths['bitRate'].'s | %'.$this->widths['sampleRate'].'s | %' + .$this->widths['song'].'s | %'.$this->widths['artist'].'s | %'.$this->widths['track'].'s | %'.$this->widths['parseTime'].'s'; + $this->compareWithId3 = class_exists('getID3'); + + echo sprintf($this->songRowTempalte, 'File name', 'dur.', 'bitrate', 'sample', 'song', 'artist', 'track', + 'time').PHP_EOL; + + foreach ($fileNames as $arg) { + if (is_dir($arg)) { + foreach (glob(rtrim($arg, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.mp3') as $f) { + if (is_file($f)) { + $this->analyze($f); + if ($this->compareWithId3) $this->analyzeId3($f); + } } - } else - echo $value.PHP_EOL; + } else if (is_file($arg)) { + $this->analyze($arg, true); + if ($this->compareWithId3) $this->analyzeId3($arg); + } + } + + + echo sprintf('%42s | %34s', 'Total duration: '.self::formatTime($this->totalDuration), 'Total parsing time: '.round($this->totalParseTime, 5)).PHP_EOL; + if ($this->compareWithId3) + echo sprintf('%79s', 'Total getId3 parsing time: '.round($this->totalId3ParseTime, 5)).PHP_EOL; + } + + /** + * @param $time + * + * @return string + */ + public static function formatTime($time) { + return floor($time / 60).':'.str_pad(floor($time % 60), 2, 0, STR_PAD_LEFT); + } + + /** + * @param $string + * @param $maxLength + * + * @return string + */ + public static function substrIfLonger($string, $maxLength) { + if (mb_strlen($string) > $maxLength) { + return mb_substr($string, 0, $maxLength-3).'...'; + } + return $string; + } + + /** + * + */ + protected function adjustOutputSize() + { + $terminal_width = \wapmorgan\TerminalInfo\TerminalInfo::getWidth(); + + foreach ($this->widths as $element => $width) { + if ($width >= 1) { + continue; + } + $this->widths[$element] = ceil($width * $terminal_width); } } - $total_duration += $audio->duration; - $total_parse_time += $audio->_parsingTime; -} + /** + * @param $filename + * @param bool $id3v2 + * + * @return null|void + */ + protected function analyze($filename, $id3v2 = false) { + if (!is_readable($filename)) return; + try { + $audio = new Mp3Info($filename, true); + } catch (Exception $e) { + var_dump($filename.': '.$e->getMessage()); + return null; + } -function analyzeId3($filename, &$total_parse_time) { - static $ID3; - if ($ID3 === null) $ID3 = new getID3(); + echo sprintf($this->songRowTempalte, + self::convertToNativeEncoding(self::substrIfLonger(basename($filename), $this->widths['filename'])), + self::formatTime($audio->duration), + $audio->isVbr ? 'vbr' : ($audio->bitRate / 1000).'kbps', + ($audio->sampleRate / 1000), + isset($audio->tags1['song']) ? self::substrIfLonger($audio->tags1['song'], 11) : null, + isset($audio->tags1['artist']) ? self::substrIfLonger($audio->tags1['artist'], 10) : null, + isset($audio->tags1['track']) ? self::substrIfLonger($audio->tags1['track'], 5) : null, + $audio->_parsingTime) + .PHP_EOL; - $t = microtime(true); - $info = $ID3->analyze($filename); - $parse_time = microtime(true) - $t; - echo sprintf('%15s | %4s | %7s | %0.1fkHz | %-11s | %-10s | %.5f | %5d', substrIfLonger(basename($filename), 15), $info['playtime_string'], $info['audio']['bitrate_mode'] == 'vbr' ? 'vbr' : floor($info['audio']['bitrate'] / 1000).'kbps', ($info['audio']['sample_rate'] / 1000), isset($info['tags']['title']) ? substrIfLonger($info['tags']['title'], 11) : null, isset($info['tags']['artist']) ? substrIfLonger($info['tags']['artist'], 10) : null, null, $parse_time).PHP_EOL; - $total_parse_time += $parse_time; + if ($id3v2 && !empty($audio->tags2)) { + foreach ($audio->tags2 as $tag=>$value) { + echo ' '.$tag.': '; + if ($tag == 'COMM') { + foreach ($value as $lang => $comment) { + echo '['.$lang.'] '.$comment['short'].'; '.$comment['actual'].PHP_EOL; + } + } else + echo self::convertToNativeEncoding($value).PHP_EOL; + } + } + $this->totalDuration += $audio->duration; + $this->totalParseTime += $audio->_parsingTime; + } + + /** + * @param $filename + */ + protected function analyzeId3($filename) { + static $ID3; + if ($ID3 === null) $ID3 = new getID3(); + + $t = microtime(true); + $info = $ID3->analyze($filename); + $parse_time = microtime(true) - $t; + echo sprintf($this->songRowTempalte, + self::substrIfLonger(basename($filename), $this->widths['filename']), + $info['playtime_string'], + $info['audio']['bitrate_mode'] == 'vbr' ? 'vbr' : floor($info['audio']['bitrate'] / 1000).'kbps', + ($info['audio']['sample_rate'] / 1000), + isset($info['tags']['title']) ? self::substrIfLonger($info['tags']['title'], 11) : null, + isset($info['tags']['artist']) ? self::substrIfLonger($info['tags']['artist'], 10) : + null, + null, + $parse_time) + .PHP_EOL; + + $this->totalId3ParseTime += $parse_time; + } + + protected static function convertToNativeEncoding($string) + { +// if (strncasecmp(PHP_OS, 'win', 3) === 0) +// return mb_convert_encoding($string, 'cp1251', 'utf-8'); + return $string; + } } array_shift($argv); -echo sprintf('%15s | %4s | %7s | %7s | %11s | %10s | %5s | %4s', 'File name', 'dur.', 'bitrate', 'sample', 'song', 'artist', 'track', - 'time').PHP_EOL; -$total_duration = $total_parse_time = $id3_parse_time = 0; -foreach ($argv as $arg) { - if (is_dir($arg)) { - foreach (glob(rtrim($arg, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.mp3') as $f) { - if (is_file($f)) { - analyze($f, $total_duration, $total_parse_time); - if ($compare) analyzeId3($f, $id3_parse_time); - } - } - } else if (is_file($arg)) { - analyze($arg, $total_duration, $total_parse_time, true); - if ($compare) analyzeId3($f, $id3_parse_time); - } -} -echo sprintf('%42s | %34s', 'Total duration: '.formatTime($total_duration), 'Total parsing time: '.round($total_parse_time, 5)).PHP_EOL; -if ($compare) echo sprintf('%79s', 'Total getId3 parsing time: '.round($id3_parse_time, 5)).PHP_EOL; +$runner = new Mp3InfoConsoleRunner(); +$runner->run($argv); + diff --git a/composer.json b/composer.json index d520eec..ea2bd06 100644 --- a/composer.json +++ b/composer.json @@ -9,5 +9,11 @@ "wapmorgan\\Mp3Info\\": "src/" } }, + "require": { + "ext-mbstring": "*" + }, + "require-dev": { + "wapmorgan/terminal-info": "dev-master" + }, "bin": ["bin/mp3scan"] } diff --git a/src/Mp3Info.php b/src/Mp3Info.php index 4c9a77c..ff2ee17 100644 --- a/src/Mp3Info.php +++ b/src/Mp3Info.php @@ -1,6 +1,8 @@ codecVersion.($this->channel == self::MONO ? 'mono' : 'stereo')) { - case "1stereo": $offset = 36; break; - case "1mono": $offset = 21; break; - case "2stereo": $offset = 21; break; - case "2mono": $offset = 13; break; + case '1stereo': $offset = 36; break; + case '1mono': $offset = 21; break; + case '2stereo': $offset = 21; break; + case '2mono': $offset = 13; break; } fseek($fp, $pos + $offset); if (fread($fp, 4) == self::VBR_SYNC) { @@ -379,7 +389,7 @@ class Mp3Info { private function readId3v2Body($fp) { // read the rest of the id3v2 header $raw = fread($fp, 7); - $data = unpack("cmajor_version/cminor_version/H*", $raw); + $data = unpack('cmajor_version/cminor_version/H*', $raw); $this->id3v2MajorVersion = $data['major_version']; $this->id3v2MinorVersion = $data['minor_version']; $data = str_pad(base_convert($data[1], 16, 2), 40, 0, STR_PAD_LEFT); @@ -436,7 +446,7 @@ class Mp3Info { break; } - $data = unpack("Nframe_size/H2flags", substr($raw, 4)); + $data = unpack('Nframe_size/H2flags', substr($raw, 4)); $frame_size = $data['frame_size']; $flags = base_convert($data['flags'], 16, 2); $this->id3v2TagsFlags[$frame_id] = array( @@ -455,24 +465,16 @@ class Mp3Info { ################# Text information frames case 'TALB': # Album/Movie/Show title - $raw = fread($fp, $frame_size); - // var_dump($raw); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); + case 'TCON': # Content type + case 'TYER': # Year + case 'TXXX': # User defined text information frame + case 'TRCK': # Track number/Position in set + case 'TIT2': # Title/songname/content description + case 'TPE1': # Lead performer(s)/Soloist(s) + $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size)); break; // case 'TBPM': # BPM (beats per minute) // case 'TCOM': # Composer - case 'TCON': # Content type - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; // case 'TCOP': # Copyright message // case 'TDAT': # Date // case 'TDLY': # Playlist delay @@ -481,14 +483,6 @@ class Mp3Info { // case 'TFLT': # File type // case 'TIME': # Time // case 'TIT1': # Content group description - case 'TIT2': # Title/songname/content description - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; // case 'TIT3': # Subtitle/Description refinement // case 'TKEY': # Initial key // case 'TLAN': # Language(s) @@ -500,49 +494,18 @@ class Mp3Info { // case 'TOPE': # Original artist(s)/performer(s) // case 'TORY': # Original release year // case 'TOWN': # File owner/licensee - case 'TPE1': # Lead performer(s)/Soloist(s) - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; // case 'TPE2': # Band/orchestra/accompaniment // case 'TPE3': # Conductor/performer refinement // case 'TPE4': # Interpreted, remixed, or otherwise modified by // case 'TPOS': # Part of a set // case 'TPUB': # Publisher - case 'TRCK': # Track number/Position in set - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; // case 'TRDA': # Recording dates // case 'TRSN': # Internet radio station name // case 'TRSO': # Internet radio station owner // case 'TSIZ': # Size // case 'TSRC': # ISRC (international standard recording code) // case 'TSSE': # Software/Hardware and settings used for encoding - case 'TYER': # Year - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; - case 'TXXX': # User defined text information frame - $raw = fread($fp, $frame_size); - $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); - if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); - else # utf-16 - $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); - break; + ################# Text information frames ################# URL link frames @@ -583,7 +546,7 @@ class Mp3Info { case 'COMM': # Comments $dataEnd = ftell($fp) + $frame_size; $raw = fread($fp, 4); - $data = unpack("C1encoding/A3language", $raw); + $data = unpack('C1encoding/A3language', $raw); // read until \null character $short_description = null; $last_null = false; @@ -622,8 +585,7 @@ class Mp3Info { // case 'GEOB': # General encapsulated object // break; case 'PCNT': # Play counter - $raw = fread($fp, $frame_size); - $data = unpack("L", $raw); + $data = unpack('L', fread($fp, $frame_size)); $this->tags2[$frame_id] = $data[1]; break; // case 'POPM': # Popularimeter @@ -657,15 +619,35 @@ class Mp3Info { /** * Simple function that checks mpeg-audio correctness of given file. - * Actually it checks that first 3 bytes of file is a id3v2 tag mark or that first 11 bits of file is a frame header sync mark. - * To perform full test create an instance of Mp3Info with given file. + * Actually it checks that first 3 bytes of file is a id3v2 tag mark or + * that first 11 bits of file is a frame header sync mark. To perform full + * test create an instance of Mp3Info with given file. + * * @param string $filename File to be tested. + * * @return boolean True if file is looks correct, False otherwise. + * @throws \Exception */ static public function isValidAudio($filename) { if (!file_exists($filename)) - throw new Exception("File ".$filename." is not present!"); + throw new Exception('File '.$filename.' is not present!'); $raw = file_get_contents($filename, false, null, 0, 3); - return ($raw == self::TAG2_SYNC || substr(base_convert(implode(null, unpack('H*', $raw)), 16, 2), 0, 11) == self::FRAME_SYNC); + return ($raw == self::TAG2_SYNC || (self::FRAME_SYNC == (unpack('n*', $raw)[1] & self::FRAME_SYNC))); + } + + /** + * @param $frameSize + * @param $raw + * + * @return array + */ + private function handleTextFrame($frameSize, $raw) + { + $data = unpack('C1encoding/A' . ($frameSize - 1) . 'information', $raw); + + if ($data['encoding'] == 0x00) # ISO-8859-1 + return mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); + else # utf-16 + return mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); } }