Simple update

This commit is contained in:
wapmorgan 2018-08-16 00:50:35 +03:00
parent 1f083faf60
commit 27bb2c55e9
3 changed files with 230 additions and 139 deletions

View File

@ -1,5 +1,7 @@
#!/usr/bin/php
<?php
use wapmorgan\Mp3Info\Mp3Info;
$paths = [
// as a root package or phar
__DIR__.'/../vendor/autoload.php',
@ -18,77 +20,178 @@ function init_composer(array $paths) {
return false;
}
if (!init_composer($paths)) die('Run `composer install` firstly.'.PHP_EOL);
use wapmorgan\Mp3Info\Mp3Info;
$compare = class_exists('getID3');
if ($argc == 1)
die('Specify file names to scan');
function formatTime($time) {
return floor($time / 60).':'.str_pad(floor($time % 60), 2, 0, STR_PAD_LEFT);
}
class Mp3InfoConsoleRunner {
function substrIfLonger($string, $maxLength) {
if (strlen($string) > $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);

View File

@ -9,5 +9,11 @@
"wapmorgan\\Mp3Info\\": "src/"
}
},
"require": {
"ext-mbstring": "*"
},
"require-dev": {
"wapmorgan/terminal-info": "dev-master"
},
"bin": ["bin/mp3scan"]
}

View File

@ -1,6 +1,8 @@
<?php
namespace wapmorgan\Mp3Info;
use Exception;
/**
* This class extracts information about an mpeg audio. (supported mpeg versions: MPEG-1, MPEG-2)
* (supported mpeg audio layers: 1, 2, 3).
@ -20,10 +22,12 @@ namespace wapmorgan\Mp3Info;
* * {@link http://gabriel.mp3-tech.org/mp3infotag.html Xing, Info and Lame tags specifications}
*/
class Mp3Info {
const TAG1_SYNC = "TAG";
const TAG2_SYNC = "ID3";
const VBR_SYNC = "Xing";
const CBR_SYNC = "Info";
const TAG1_SYNC = 'TAG';
const TAG2_SYNC = 'ID3';
const VBR_SYNC = 'Xing';
const CBR_SYNC = 'Info';
const FRAME_SYNC = 0xffe0;
const META = 1;
const TAGS = 2;
@ -35,10 +39,10 @@ class Mp3Info {
const LAYER_2 = 2;
const LAYER_3 = 3;
const STEREO = "stereo";
const JOINT_STEREO = "joint_stereo";
const DUAL_MONO = "dual_mono";
const MONO = "mono";
const STEREO = 'stereo';
const JOINT_STEREO = 'joint_stereo';
const DUAL_MONO = 'dual_mono';
const MONO = 'mono';
/**
* Boolean trigger to enable / disable trace output
@ -157,11 +161,17 @@ class Mp3Info {
/**
* $mode is self::META, self::TAGS or their combination.
*
* @param string $filename
* @param bool $parseTags
*
* @throws \Exception
*/
public function __construct($filename, $parseTags = false) {
if (is_null(self::$_bitRateTable)) self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php';
if (is_null(self::$_sampleRateTable)) self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php';
if (self::$_bitRateTable === null)
self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php';
if (self::$_sampleRateTable === null)
self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php';
if (!file_exists($filename))
throw new \Exception("File ".$filename." is not present!");
@ -183,7 +193,7 @@ class Mp3Info {
*/
private function parseAudio($filename, $fileSize, $mode) {
$time = microtime(true);
$fp = fopen($filename, "rb");
$fp = fopen($filename, 'rb');
/** Size of audio data (exclude tags size)
* @var int */
@ -278,10 +288,10 @@ class Mp3Info {
}
switch ($this->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');
}
}