Add separate classes for local and remote file access

Improves parsing of remote files by using HTTP Range requests so it doesn't have to download the whole file.

Signed-off-by: Markus Birth <markus@birth-online.de>
This commit is contained in:
Markus Birth 2024-05-26 14:15:17 +01:00
parent d749cdd862
commit 3699fd5ecb
3 changed files with 388 additions and 163 deletions

83
src/Mp3FileLocal.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace wapmorgan\Mp3Info;
class Mp3FileLocal
{
public string $fileName;
protected int $fileSize;
private $_filePtr;
/**
* Creates a new local file object.
*
* @param string $fileName URL to open
*/
public function __construct(string $fileName)
{
$this->fileName = $fileName;
$this->_filePtr = fopen($this->fileName, 'rb');
$this->fileSize = filesize($this->fileName);
}
/**
* Returns the file size
*
* @return int File size
*/
public function getFileSize(): int
{
return $this->fileSize;
}
/**
* Returns the given amount of Bytes from the current file position.
*
* @param int $numBytes Bytes to read
*
* @return string Read Bytes
*/
public function getBytes(int $numBytes): string
{
return fread($this->_filePtr, $numBytes);
}
/**
* Returns the current file position
*
* @return int File position
*/
public function getFilePos(): int
{
return ftell($this->_filePtr);
}
/**
* Sets the file point to the given position.
*
* @param int $posBytes Position to jump to
*
* @return bool TRUE if successful
*/
public function seekTo(int $posBytes): bool
{
$result = fseek($this->_filePtr, $posBytes);
return ($result == 0);
}
/**
* Advances the file pointer the given amount.
*
* @param int $posBytes Bytes to advance
*
* @return bool TRUE if successful
*/
public function seekForward(int $posBytes): bool
{
$newPos = $this->getFilePos() + $posBytes;
return $this->seekTo($newPos);
}
}

181
src/Mp3FileRemote.php Normal file
View File

@ -0,0 +1,181 @@
<?php
namespace wapmorgan\Mp3Info;
class Mp3FileRemote
{
public string $fileName;
public int $blockSize;
protected $buffer;
protected int $filePos;
protected $fileSize;
/**
* Creates a new remote file object.
*
* @param string $fileName URL to open
* @param int $blockSize Size of the blocks to query from the server (default: 4096)
*/
public function __construct(string $fileName, int $blockSize = 4096)
{
$this->fileName = $fileName;
$this->blockSize = $blockSize;
$this->buffer = [];
$this->filePos = 0;
$this->fileSize = $this->_readFileSize();
}
/**
* Returns the file size
*
* @return int File size
*/
public function getFileSize(): int
{
return $this->fileSize;
}
/**
* Makes a HEAD request to get the file size
*
* @return int Content-Length header
*/
private function _readFileSize(): int
{
// make HTTP HEAD request to get Content-Length
$context = stream_context_create([
'http' => [
'method' => 'HEAD',
],
]);
$result = get_headers($this->fileName, true, $context);
return $result['Content-Length'];
}
/**
* Returns the given amount of Bytes from the current file position.
*
* @param int $numBytes Bytes to read
*
* @return string Read Bytes
*/
public function getBytes(int $numBytes): string
{
$blockId = intdiv($this->filePos, $this->blockSize);
$blockPos = $this->filePos % $this->blockSize;
$output = [];
do {
$this->downloadBlock($blockId); // make sure we have this block
if ($blockPos + $numBytes >= $this->blockSize) {
// length of request is more than this block has, truncate to block len
$subLen = $this->blockSize - $blockPos;
} else {
// requested length fits inside this block
$subLen = $numBytes;
}
// $subLen = ($blockPos + $numBytes >= $this->blockSize) ? ($this->blockSize - $blockPos) : $numBytes;
$output[] = substr($this->buffer[$blockId], $blockPos, $subLen);
$this->filePos += $subLen;
$numBytes -= $subLen;
// advance to next block
$blockPos = 0;
$blockId++;
} while ($numBytes > 0);
return implode('', $output);
}
/**
* Returns the current file position
*
* @return int File position
*/
public function getFilePos(): int
{
return $this->filePos;
}
/**
* Sets the file pointer to the given position.
*
* @param int $posBytes Position to jump to
*
* @return bool TRUE if successful
*/
public function seekTo(int $posBytes): bool
{
if ($posBytes < 0 || $posBytes > $this->fileSize) {
return false;
}
$this->filePos = $posBytes;
return true;
}
/**
* Advances the file pointer the given amount.
*
* @param int $posBytes Bytes to advance
*
* @return bool TRUE if successful
*/
public function seekForward(int $posBytes): bool
{
$newPos = $this->filePos + $posBytes;
return $this->seekTo($newPos);
}
/**
* Downloads the given block if needed
*
* @param int $blockNo Block to download
*
* @return bool TRUE if successful
*/
protected function downloadBlock(int $blockNo): bool
{
if (array_key_exists($blockNo, $this->buffer)) {
// already downloaded
return true;
}
$bytesFrom = $blockNo * $this->blockSize;
$bytesTo = $bytesFrom + $this->blockSize - 1;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => [
'Range: bytes=' . $bytesFrom . '-' . $bytesTo,
],
],
]);
$filePtr = fopen($this->fileName, 'rb', false, $context);
$this->buffer[$blockNo] = fread($filePtr, $this->blockSize);
$status = stream_get_meta_data($filePtr);
$httpStatus = explode(' ', $status['wrapper_data'][0])[1];
if ($httpStatus != '206') {
if ($httpStatus != '200') {
echo 'Download error!' . PHP_EOL;
var_dump($status);
return false;
}
echo 'Server doesn\'t support partial content!' . PHP_EOL;
// Content received is whole file from start
if ($blockNo != 0) {
// move block to start if needed
$this->buffer[0] =& $this->buffer[$blockNo];
unset($this->buffer[$blockNo]);
$blockNo = 0;
}
// receive remaining parts while we're at it
while (!feof($filePtr)) {
$blockNo++;
$this->buffer[$blockNo] = fread($filePtr, $this->blockSize);
}
}
fclose($filePtr);
return true;
}
}

View File

@ -1,8 +1,14 @@
<?php <?php
namespace wapmorgan\Mp3Info; namespace wapmorgan\Mp3Info;
use Exception; require __DIR__ . '/Mp3FileLocal.php';
use RuntimeException; require __DIR__ . '/Mp3FileRemote.php';
use \wapmorgan\Mp3Info\Mp3FileLocal;
use \wapmorgan\Mp3Info\Mp3FileRemote;
use \Exception;
use \RuntimeException;
/** /**
* This class extracts information about an mpeg audio. (supported mpeg versions: MPEG-1, MPEG-2) * This class extracts information about an mpeg audio. (supported mpeg versions: MPEG-1, MPEG-2)
@ -79,6 +85,11 @@ class Mp3Info {
public static $framesCountRead = 2; public static $framesCountRead = 2;
/**
* @var Mp3File File object for I/O handling
*/
protected $fileObj;
/** /**
* @var int MPEG codec version (1 or 2 or 2.5 or undefined) * @var int MPEG codec version (1 or 2 or 2.5 or undefined)
*/ */
@ -208,29 +219,21 @@ class Mp3Info {
* *
* @throws \Exception * @throws \Exception
*/ */
public function __construct($filename, $parseTags = false) { public function __construct(string $filename, bool $parseTags = false)
{
if (self::$_bitRateTable === null) if (self::$_bitRateTable === null)
self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php'; self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php';
if (self::$_sampleRateTable === null) if (self::$_sampleRateTable === null)
self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php'; self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php';
$this->_fileName = $filename; if (str_contains($filename, '://')) {
$isLocal = (strpos($filename, '://') === false); $this->fileObj = new Mp3FileRemote($filename);
if (!$isLocal) {
$this->_fileSize = static::getUrlContentLength($filename);
} else { } else {
if (!file_exists($filename)) { $this->fileObj = new Mp3FileLocal($filename);
throw new \Exception('File ' . $filename . ' is not present!');
}
$this->_fileSize = filesize($filename);
}
if ($isLocal and !static::isValidAudio($filename)) {
throw new \Exception('File ' . $filename . ' is not mpeg/audio!');
} }
$mode = $parseTags ? self::META | self::TAGS : self::META; $mode = $parseTags ? self::META | self::TAGS : self::META;
$this->audioSize = $this->parseAudio($this->_fileName, $this->_fileSize, $mode); $this->audioSize = $this->parseAudio($mode);
} }
@ -248,7 +251,7 @@ class Mp3Info {
return false; return false;
} }
fseek($fp, $this->coverProperties['offset']); fseek($fp, $this->coverProperties['offset']);
$data = fread($fp, $this->coverProperties['size']); $data = $this->fileObj->getBytes($this->coverProperties['size']);
fclose($fp); fclose($fp);
return $data; return $data;
} }
@ -259,44 +262,32 @@ class Mp3Info {
* ID3V2 TAG - provides a lot of meta data. [optional] * ID3V2 TAG - provides a lot of meta data. [optional]
* MPEG AUDIO FRAMES - contains audio data. A frame consists of a frame header and a frame data. The first frame may contain extra information about mp3 (marked with "Xing" or "Info" string). Rest of frames can contain only audio data. * MPEG AUDIO FRAMES - contains audio data. A frame consists of a frame header and a frame data. The first frame may contain extra information about mp3 (marked with "Xing" or "Info" string). Rest of frames can contain only audio data.
* ID3V1 TAG - provides a few of meta data. [optional] * ID3V1 TAG - provides a few of meta data. [optional]
* @param string $filename
* @param int $fileSize
* @param int $mode * @param int $mode
* @return float|int * @return float|int
* @throws \Exception * @throws \Exception
*/ */
private function parseAudio($filename, $fileSize, $mode) { private function parseAudio($mode) {
$time = microtime(true); $time = microtime(true);
// create temp storage for media
if (strpos($filename, '://') !== false) {
$fp = fopen('php://memory', 'rwb');
fwrite($fp, file_get_contents($filename));
rewind($fp);
} else {
$fp = fopen($filename, 'rb');
}
/** @var int Size of audio data (exclude tags size) */ /** @var int Size of audio data (exclude tags size) */
$audioSize = $fileSize; $audioSize = $this->fileObj->getFileSize();
// parse tags // parse tags
if (fread($fp, 3) == self::TAG2_SYNC) { if ($this->fileObj->getBytes(3) == self::TAG2_SYNC) {
if ($mode & self::TAGS) $audioSize -= ($this->_id3Size = $this->readId3v2Body($fp)); if ($mode & self::TAGS) {
else { $audioSize -= ($this->_id3Size = $this->readId3v2Body());
fseek($fp, 2, SEEK_CUR); // 2 bytes of tag version } else {
fseek($fp, 1, SEEK_CUR); // 1 byte of tag flags $this->fileObj->seekForward(2); // 2 bytes of tag version
$sizeBytes = $this->readBytes($fp, 4); $this->fileObj->seekForward(1); // 1 byte of tag flags
array_walk($sizeBytes, function (&$value) { $sizeBytes = unpack('C4', $this->fileObj->getBytes(4));
$value = substr(str_pad(base_convert($value, 10, 2), 8, 0, STR_PAD_LEFT), 1); $size = $sizeBytes[1] << 21 | $sizeBytes[2] << 14 | $sizeBytes[3] << 7 | $sizeBytes[4];
}); $size += 10; // add header size
$size = bindec(implode($sizeBytes)) + 10;
$audioSize -= ($this->_id3Size = $size); $audioSize -= ($this->_id3Size = $size);
} }
} }
fseek($fp, $fileSize - 128); $this->fileObj->seekTo($this->fileObj->getFileSize() - 128);
if (fread($fp, 3) == self::TAG1_SYNC) { if ($this->fileObj->getBytes(3) == self::TAG1_SYNC) {
if ($mode & self::TAGS) $audioSize -= $this->readId3v1Body($fp); if ($mode & self::TAGS) $audioSize -= $this->readId3v1Body($fp);
else $audioSize -= 128; else $audioSize -= 128;
} }
@ -305,17 +296,17 @@ class Mp3Info {
$this->fillTags(); $this->fillTags();
} }
fseek($fp, 0); $this->fileObj->seekTo(0);
// audio meta // audio meta
if ($mode & self::META) { if ($mode & self::META) {
if ($this->_id3Size !== null) fseek($fp, $this->_id3Size); if ($this->_id3Size !== null) $this->fileObj->seekTo($this->_id3Size);
/** /**
* First frame can lie. Need to fix in the future. * First frame can lie. Need to fix in the future.
* @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813 * @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813
* Read first N frames * Read first N frames
*/ */
for ($i = 0; $i < self::$framesCountRead; $i++) { for ($i = 0; $i < self::$framesCountRead; $i++) {
$framesCount = $this->readMpegFrame($fp); $framesCount = $this->readMpegFrame();
} }
$this->_framesCount = $framesCount !== null $this->_framesCount = $framesCount !== null
@ -338,7 +329,6 @@ class Mp3Info {
// Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount // Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount
$this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate; $this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate;
} }
fclose($fp);
$this->_parsingTime = microtime(true) - $time; $this->_parsingTime = microtime(true) - $time;
return $audioSize; return $audioSize;
@ -346,48 +336,52 @@ class Mp3Info {
/** /**
* Read first frame information. * Read first frame information.
* @param resource $fp
* @return int Number of frames (if present if first frame of VBR-file) * @return int Number of frames (if present if first frame of VBR-file)
* @throws \Exception * @throws \Exception
*/ */
private function readMpegFrame($fp) { private function readMpegFrame() {
$header_seek_pos = ftell($fp) + self::$headerSeekLimit; $header_seek_pos = $this->fileObj->getFilePos() + self::$headerSeekLimit;
do { do {
$pos = ftell($fp); $pos = $this->fileObj->getFilePos();
$first_header_byte = $this->readBytes($fp, 1); $first_header_byte = $this->fileObj->getBytes(1);
if ($first_header_byte[0] === 0xFF) { if (ord($first_header_byte[0]) === 0xFF) {
$second_header_byte = $this->readBytes($fp, 1); $second_header_byte = $this->fileObj->getBytes(1);
if ((($second_header_byte[0] >> 5) & 0b111) == 0b111) { if (((ord($second_header_byte[0]) >> 5) & 0b111) == 0b111) {
fseek($fp, $pos); $this->fileObj->seekTo($pos);
$header_bytes = $this->readBytes($fp, 4); $header_bytes = $this->fileObj->getBytes(4);
break; break;
} else {
$this->fileObj->seekForward(-1);
} }
} else {
$this->fileObj->seekForward(-1);
} }
fseek($fp, 1, SEEK_CUR); $this->fileObj->seekForward(1);
} while (ftell($fp) <= $header_seek_pos); } while ($this->fileObj->getFilePos() <= $header_seek_pos);
if (!isset($header_bytes) || $header_bytes[0] !== 0xFF || (($header_bytes[1] >> 5) & 0b111) != 0b111) { if (!isset($header_bytes) || ord($header_bytes[0]) !== 0xFF || ((ord($header_bytes[1]) >> 5) & 0b111) != 0b111) {
throw new \Exception('At '.$pos throw new \Exception('At '.$pos
.'(0x'.dechex($pos).') should be a frame header!'); .'(0x'.dechex($pos).') should be a frame header!');
} }
switch ($header_bytes[1] >> 3 & 0b11) { switch (ord($header_bytes[1]) >> 3 & 0b11) {
case 0b00: $this->codecVersion = self::MPEG_25; break; case 0b00: $this->codecVersion = self::MPEG_25; break;
case 0b01: $this->codecVersion = self::CODEC_UNDEFINED; break; case 0b01: return null; break;
case 0b10: $this->codecVersion = self::MPEG_2; break; case 0b10: $this->codecVersion = self::MPEG_2; break;
case 0b11: $this->codecVersion = self::MPEG_1; break; case 0b11: $this->codecVersion = self::MPEG_1; break;
} }
switch ($header_bytes[1] >> 1 & 0b11) { switch (ord($header_bytes[1]) >> 1 & 0b11) {
case 0b01: $this->layerVersion = self::LAYER_3; break; case 0b01: $this->layerVersion = self::LAYER_3; break;
case 0b10: $this->layerVersion = self::LAYER_2; break; case 0b10: $this->layerVersion = self::LAYER_2; break;
case 0b11: $this->layerVersion = self::LAYER_1; break; case 0b11: $this->layerVersion = self::LAYER_1; break;
} }
$this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][$header_bytes[2] >> 4]; $this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][ord($header_bytes[2]) >> 4];
$this->sampleRate = self::$_sampleRateTable[$this->codecVersion][($header_bytes[2] >> 2) & 0b11]; $this->sampleRate = self::$_sampleRateTable[$this->codecVersion][(ord($header_bytes[2]) >> 2) & 0b11];
if ($this->sampleRate === false) return null;
switch ($header_bytes[3] >> 6) { switch (ord($header_bytes[3]) >> 6) {
case 0b00: $this->channel = self::STEREO; break; case 0b00: $this->channel = self::STEREO; break;
case 0b01: $this->channel = self::JOINT_STEREO; break; case 0b01: $this->channel = self::JOINT_STEREO; break;
case 0b10: $this->channel = self::DUAL_MONO; break; case 0b10: $this->channel = self::DUAL_MONO; break;
@ -397,69 +391,54 @@ class Mp3Info {
$vbr_offset = self::$_vbrOffsets[$this->codecVersion][$this->channel == self::MONO ? 0 : 1]; $vbr_offset = self::$_vbrOffsets[$this->codecVersion][$this->channel == self::MONO ? 0 : 1];
// check for VBR // check for VBR
fseek($fp, $pos + $vbr_offset); $this->fileObj->seekTo($pos + $vbr_offset);
if (fread($fp, 4) == self::VBR_SYNC) { if ($this->fileObj->getBytes(4) == self::VBR_SYNC) {
$this->isVbr = true; $this->isVbr = true;
$flagsBytes = $this->readBytes($fp, 4); $flagsBytes = $this->fileObj->getBytes(4);
// VBR frames count presence // VBR frames count presence
if (($flagsBytes[3] & 2)) { if ((ord($flagsBytes[3]) & 2)) {
$this->vbrProperties['frames'] = implode(unpack('N', fread($fp, 4))); $this->vbrProperties['frames'] = implode(unpack('N', $this->fileObj->getBytes(4)));
} }
// VBR stream size presence // VBR stream size presence
if ($flagsBytes[3] & 4) { if (ord($flagsBytes[3]) & 4) {
$this->vbrProperties['bytes'] = implode(unpack('N', fread($fp, 4))); $this->vbrProperties['bytes'] = implode(unpack('N', $this->fileObj->getBytes(4)));
} }
// VBR TOC presence // VBR TOC presence
if ($flagsBytes[3] & 1) { if (ord($flagsBytes[3]) & 1) {
fseek($fp, 100, SEEK_CUR); $this->fileObj->seekForward(100);
} }
// VBR quality // VBR quality
if ($flagsBytes[3] & 8) { if (ord($flagsBytes[3]) & 8) {
$this->vbrProperties['quality'] = implode(unpack('N', fread($fp, 4))); $this->vbrProperties['quality'] = implode(unpack('N', $this->fileObj->getBytes(4)));
} }
} }
// go to the end of frame // go to the end of frame
if ($this->layerVersion == self::LAYER_1) { if ($this->layerVersion == self::LAYER_1) {
$this->_cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1)) * 4); $this->_cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + (ord($header_bytes[2]) >> 1 & 0b1)) * 4);
} else { } else {
$this->_cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1)); $this->_cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + (ord($header_bytes[2]) >> 1 & 0b1));
} }
fseek($fp, $pos + $this->_cbrFrameSize); $this->fileObj->seekTo($pos + $this->_cbrFrameSize);
return isset($this->vbrProperties['frames']) ? $this->vbrProperties['frames'] : null; return isset($this->vbrProperties['frames']) ? $this->vbrProperties['frames'] : null;
} }
/**
* @param $fp
* @param $n
*
* @return array
* @throws \Exception
*/
private function readBytes($fp, $n) {
$raw = fread($fp, $n);
if (strlen($raw) !== $n) throw new \Exception('Unexpected end of file!');
$bytes = array();
for($i = 0; $i < $n; $i++) $bytes[$i] = ord($raw[$i]);
return $bytes;
}
/** /**
* Reads id3v1 tag. * Reads id3v1 tag.
* @return int Returns length of id3v1 tag. * @return int Returns length of id3v1 tag.
*/ */
private function readId3v1Body($fp) { private function readId3v1Body() {
$this->tags1['song'] = trim(fread($fp, 30)); $this->tags1['song'] = trim($this->fileObj->getBytes(30));
$this->tags1['artist'] = trim(fread($fp, 30)); $this->tags1['artist'] = trim($this->fileObj->getBytes(30));
$this->tags1['album'] = trim(fread($fp, 30)); $this->tags1['album'] = trim($this->fileObj->getBytes(30));
$this->tags1['year'] = trim(fread($fp, 4)); $this->tags1['year'] = trim($this->fileObj->getBytes(4));
$this->tags1['comment'] = trim(fread($fp, 28)); $this->tags1['comment'] = trim($this->fileObj->getBytes(28));
fseek($fp, 1, SEEK_CUR); $this->fileObj->seekForward(1);
$this->tags1['track'] = ord(fread($fp, 1)); $this->tags1['track'] = ord($this->fileObj->getBytes(1));
$this->tags1['genre'] = ord(fread($fp, 1)); $this->tags1['genre'] = ord($this->fileObj->getBytes(1));
return 128; return 128;
} }
@ -512,10 +491,10 @@ class Mp3Info {
* @return int Returns length of id3v2 tag. * @return int Returns length of id3v2 tag.
* @throws \Exception * @throws \Exception
*/ */
private function readId3v2Body($fp) private function readId3v2Body()
{ {
// read the rest of the id3v2 header // read the rest of the id3v2 header
$raw = fread($fp, 7); $raw = $this->fileObj->getBytes(7);
$data = unpack('cmajor_version/cminor_version/H*', $raw); $data = unpack('cmajor_version/cminor_version/H*', $raw);
$this->id3v2MajorVersion = $data['major_version']; $this->id3v2MajorVersion = $data['major_version'];
$this->id3v2MinorVersion = $data['minor_version']; $this->id3v2MinorVersion = $data['minor_version'];
@ -562,10 +541,10 @@ class Mp3Info {
/*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/ /*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/
} else if ($this->id3v2MajorVersion == 3) { } else if ($this->id3v2MajorVersion == 3) {
// parse id3v2.3.0 body // parse id3v2.3.0 body
$this->parseId3v23Body($fp, 10 + $size); $this->parseId3v23Body(10 + $size);
} else if ($this->id3v2MajorVersion == 4) { } else if ($this->id3v2MajorVersion == 4) {
// parse id3v2.4.0 body // parse id3v2.4.0 body
$this->parseId3v24Body($fp, 10 + $size); $this->parseId3v24Body(10 + $size);
} }
return 10 + $size; // 10 bytes - header, rest - body return 10 + $size; // 10 bytes - header, rest - body
@ -575,9 +554,9 @@ class Mp3Info {
* Parses id3v2.3.0 tag body. * Parses id3v2.3.0 tag body.
* @todo Complete. * @todo Complete.
*/ */
protected function parseId3v23Body($fp, $lastByte) { protected function parseId3v23Body($lastByte) {
while (ftell($fp) < $lastByte) { while ($this->fileObj->getFilePos() < $lastByte) {
$raw = fread($fp, 10); $raw = $this->fileObj->getBytes(10);
$frame_id = substr($raw, 0, 4); $frame_id = substr($raw, 0, 4);
if ($frame_id == str_repeat(chr(0), 4)) { if ($frame_id == str_repeat(chr(0), 4)) {
@ -643,7 +622,7 @@ class Mp3Info {
case 'TSIZ': # Size case 'TSIZ': # Size
case 'TSRC': # ISRC (international standard recording code) case 'TSRC': # ISRC (international standard recording code)
case 'TSSE': # Software/Hardware and settings used for encoding case 'TSSE': # Software/Hardware and settings used for encoding
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size)); $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, $this->fileObj->getBytes($frame_size));
break; break;
################# Text information frames ################# Text information frames
@ -683,15 +662,15 @@ class Mp3Info {
// case 'SYLT': # Synchronized lyric/text // case 'SYLT': # Synchronized lyric/text
// break; // break;
case 'COMM': # Comments case 'COMM': # Comments
$dataEnd = ftell($fp) + $frame_size; $dataEnd = $this->fileObj->getFilePos() + $frame_size;
$raw = fread($fp, 4); $raw = $this->fileObj->getBytes(4);
$data = unpack('C1encoding/A3language', $raw); $data = unpack('C1encoding/A3language', $raw);
// read until \null character // read until \null character
$short_description = ''; $short_description = '';
$last_null = false; $last_null = false;
$actual_text = false; $actual_text = false;
while (ftell($fp) < $dataEnd) { while ($this->fileObj->getFilePos() < $dataEnd) {
$char = fgetc($fp); $char = $this->fileObj->getBytes(1);
if ($char == "\00" && $actual_text === false) { if ($char == "\00" && $actual_text === false) {
if ($data['encoding'] == 0x1) { # two null-bytes for utf-16 if ($data['encoding'] == 0x1) { # two null-bytes for utf-16
if ($last_null) if ($last_null)
@ -721,20 +700,20 @@ class Mp3Info {
// break; // break;
case 'APIC': # Attached picture case 'APIC': # Attached picture
$this->hasCover = true; $this->hasCover = true;
$last_byte = ftell($fp) + $frame_size; $last_byte = $this->fileObj->getFilePos() + $frame_size;
$this->coverProperties = ['text_encoding' => ord(fread($fp, 1))]; $this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
// fseek($fp, $frame_size - 4, SEEK_CUR); // fseek($fp, $frame_size - 4, SEEK_CUR);
$this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['mime_type'] = $this->readTextUntilNull($last_byte);
$this->coverProperties['picture_type'] = ord(fread($fp, 1)); $this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
$this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['description'] = $this->readTextUntilNull($last_byte);
$this->coverProperties['offset'] = ftell($fp); $this->coverProperties['offset'] = $this->fileObj->getFilePos();
$this->coverProperties['size'] = $last_byte - ftell($fp); $this->coverProperties['size'] = $last_byte - $this->fileObj->getFilePos();
fseek($fp, $last_byte); $this->fileObj->seekTo($last_byte);
break; break;
// case 'GEOB': # General encapsulated object // case 'GEOB': # General encapsulated object
// break; // break;
case 'PCNT': # Play counter case 'PCNT': # Play counter
$data = unpack('L', fread($fp, $frame_size)); $data = unpack('L', $this->fileObj->getBytes($frame_size));
$this->tags2[$frame_id] = $data[1]; $this->tags2[$frame_id] = $data[1];
break; break;
// case 'POPM': # Popularimeter // case 'POPM': # Popularimeter
@ -760,7 +739,7 @@ class Mp3Info {
// case 'PRIV': # Private frame // case 'PRIV': # Private frame
// break; // break;
default: default:
fseek($fp, $frame_size, SEEK_CUR); $this->fileObj->seekForward($frame_size);
break; break;
} }
} }
@ -771,14 +750,14 @@ class Mp3Info {
* @param $fp * @param $fp
* @param $lastByte * @param $lastByte
*/ */
protected function parseId3v24Body($fp, $lastByte) protected function parseId3v24Body($lastByte)
{ {
while (ftell($fp) < $lastByte) { while ($this->fileObj->getFilePos() < $lastByte) {
$raw = fread($fp, 10); $raw = $this->fileObj->getBytes(10);
$frame_id = substr($raw, 0, 4); $frame_id = substr($raw, 0, 4);
if ($frame_id == str_repeat(chr(0), 4)) { if ($frame_id == str_repeat(chr(0), 4)) {
fseek($fp, $lastByte); $this->fileObj->seekTo($lastByte);
break; break;
} }
@ -842,7 +821,7 @@ class Mp3Info {
case 'TSIZ': # Size case 'TSIZ': # Size
case 'TSRC': # ISRC (international standard recording code) case 'TSRC': # ISRC (international standard recording code)
case 'TSSE': # Software/Hardware and settings used for encoding case 'TSSE': # Software/Hardware and settings used for encoding
$this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size)); $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, $this->fileObj->getBytes($frame_size));
break; break;
################# Text information frames ################# Text information frames
@ -883,14 +862,14 @@ class Mp3Info {
// case 'SYLT': # Synchronized lyric/text // case 'SYLT': # Synchronized lyric/text
// break; // break;
case 'COMM': # Comments case 'COMM': # Comments
$dataEnd = ftell($fp) + $frame_size; $dataEnd = $this->fileObj->getFilePos() + $frame_size;
$raw = fread($fp, 4); $raw = $this->fileObj->getBytes(4);
$data = unpack('C1encoding/A3language', $raw); $data = unpack('C1encoding/A3language', $raw);
// read until \null character // read until \null character
$short_description = null; $short_description = null;
$last_null = false; $last_null = false;
$actual_text = false; $actual_text = false;
while (ftell($fp) < $dataEnd) { while ($this->fileObj->getFilePos() < $dataEnd) {
$char = fgetc($fp); $char = fgetc($fp);
if ($char == "\00" && $actual_text === false) { if ($char == "\00" && $actual_text === false) {
if ($data['encoding'] == 0x1) { # two null-bytes for utf-16 if ($data['encoding'] == 0x1) { # two null-bytes for utf-16
@ -921,20 +900,20 @@ class Mp3Info {
// break; // break;
case 'APIC': # Attached picture case 'APIC': # Attached picture
$this->hasCover = true; $this->hasCover = true;
$last_byte = ftell($fp) + $frame_size; $last_byte = $this->fileObj->getFilePos() + $frame_size;
$this->coverProperties = ['text_encoding' => ord(fread($fp, 1))]; $this->coverProperties = ['text_encoding' => ord($this->fileObj->getBytes(1))];
// fseek($fp, $frame_size - 4, SEEK_CUR); // $this->fileObj->seekForward($frame_size - 4);
$this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte);
$this->coverProperties['picture_type'] = ord(fread($fp, 1)); $this->coverProperties['picture_type'] = ord($this->fileObj->getBytes(1));
$this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte); $this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte);
$this->coverProperties['offset'] = ftell($fp); $this->coverProperties['offset'] = $this->fileObj->getFilePos();
$this->coverProperties['size'] = $last_byte - ftell($fp); $this->coverProperties['size'] = $last_byte - $this->fileObj->getFilePos();
fseek($fp, $last_byte); $this->fileObj->seekTo($last_byte);
break; break;
// case 'GEOB': # General encapsulated object // case 'GEOB': # General encapsulated object
// break; // break;
case 'PCNT': # Play counter case 'PCNT': # Play counter
$data = unpack('L', fread($fp, $frame_size)); $data = unpack('L', $this->fileObj->getBytes($frame_size));
$this->tags2[$frame_id] = $data[1]; $this->tags2[$frame_id] = $data[1];
break; break;
// case 'POPM': # Popularimeter // case 'POPM': # Popularimeter
@ -960,7 +939,7 @@ class Mp3Info {
// case 'PRIV': # Private frame // case 'PRIV': # Private frame
// break; // break;
default: default:
fseek($fp, $frame_size, SEEK_CUR); $this->fileObj->seekForward($frame_size);
break; break;
} }
} }
@ -998,11 +977,11 @@ class Mp3Info {
* @param int $dataEnd * @param int $dataEnd
* @return string|null * @return string|null
*/ */
private function readTextUntilNull($fp, $dataEnd) private function readTextUntilNull($dataEnd)
{ {
$text = null; $text = null;
while (ftell($fp) < $dataEnd) { while ($this->fileObj->getFilePos() < $dataEnd) {
$char = fgetc($fp); $char = $this->fileObj->getBytes(1);
if ($char === "\00") { if ($char === "\00") {
return $text; return $text;
} }
@ -1060,22 +1039,4 @@ class Mp3Info {
) // id3v1 tag ) // id3v1 tag
; ;
} }
/**
* @param string $url
* @return int|mixed|string
*/
public static function getUrlContentLength($url) {
$context = stream_context_create(['http' => ['method' => 'HEAD']]);
$head = array_change_key_case(get_headers($url, true, $context));
// content-length of download (in bytes), read from Content-Length: field
$clen = isset($head['content-length']) ? $head['content-length'] : 0;
// cannot retrieve file size, return "-1"
if (!$clen) {
return -1;
}
return $clen; // return size in bytes
}
} }