Add basic ID3v2 write support, add unit tests

git-svn-id: http://php-reader.googlecode.com/svn/trunk@64 51a70ab9-7547-0410-9469-37e369ee0574
This commit is contained in:
svollbehr
2008-04-01 10:38:12 +00:00
parent 1182f0e0ff
commit 458335dc9f
15 changed files with 950 additions and 475 deletions

View File

@@ -69,4 +69,11 @@ interface ID3_Encoding
* @return integer
*/
public function getEncoding();
/**
* Sets the text encoding.
*
* @param integer $encoding The text encoding.
*/
public function setEncoding($encoding);
}

View File

@@ -36,7 +36,7 @@
*/
/**#@+ @ignore */
require_once("Object.php");
require_once("ID3/Object.php");
/**#@-*/
/**
@@ -96,7 +96,7 @@ final class ID3_ExtendedHeader extends ID3_Object
{
parent::__construct($reader);
$offset = $this->_reader->offset;
$offset = $this->_reader->getOffset();
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
$this->_reader->skip(1);
$this->_flags = $this->_reader->readInt8();
@@ -105,7 +105,7 @@ final class ID3_ExtendedHeader extends ID3_Object
$this->_reader->skip(1);
if ($this->hasFlag(self::CRC32)) {
$this->_reader->skip(1);
$this->_crc = Transform::getInt32BE
$this->_crc = Transform::fromInt32BE
(($this->_reader->read(1) << 4) &
$this->decodeSynchsafe32($this->_reader->read(4)));
}
@@ -114,7 +114,7 @@ final class ID3_ExtendedHeader extends ID3_Object
$this->_restrictions = $this->_reader->readInt8(1);
}
$this->_reader->skip($this->_size - $this->_reader->offset - $offset);
$this->_reader->skip($this->_size - $this->_reader->getOffset() - $offset);
}
/**
@@ -133,12 +133,44 @@ final class ID3_ExtendedHeader extends ID3_Object
*/
public function hasFlag($flag) { return ($this->_flags & $flag) == $flag; }
/**
* Returns the flags byte.
*
* @return integer
*/
public function getFlags($flags) { return $this->_flags; }
/**
* Sets the flags byte.
*
* @param integer $flags The flags byte.
*/
public function setFlags($flags) { $this->_flags = $flags; }
/**
* Returns the CRC-32 data.
*
* @return integer
*/
public function getCRC() { return $this->_crc; }
public function getCrc()
{
if ($this->hasFlag(self::CRC32))
return $this->_crc;
return false;
}
/**
* Sets whether the CRC-32 should be generated upon tag write.
*
* @param boolean $useCrc Whether CRC-32 should be generated
*/
public function setCrc($useCrc)
{
if ($useCrc)
$this->setFlags($this->getFlags() | self::CDC32);
else
$this->setFlags($this->getFlags() & ~self::CDC32);
}
/**
* Returns the restrictions. For some applications it might be desired to
@@ -189,4 +221,30 @@ final class ID3_ExtendedHeader extends ID3_Object
* @return integer
*/
public function getRestrictions() { return $this->_restrictions; }
/**
* Sets the restrictions byte. See {@link #getRestrictions} for more.
*
* @param integer $restrictions The restrictions byte.
*/
public function setRestrictions($restrictions)
{
$this->_restrictions = $restrictions;
}
/**
* Returns the header raw data.
*
* @todo CRC must use safesynch
* @return string
*/
public function toString()
{
return Transform::toUInt32BE($this->encodeSynchsafe32($this->_size)) .
Transform::toInt8(1) . Transform::toInt8($this->_flags) .
($this->hasFlag(self::UPDATE) ? "\0" : "") .
($this->hasFlag(self::CRC32) ? Transform::toInt8(5) . $this->_crc : "") .
($this->hasFlag(self::RESTRICTED) ?
Transform::toInt8(1) . Transform::toInt8($this->_restrictions) : "");
}
}

View File

@@ -36,7 +36,7 @@
*/
/**#@+ @ignore */
require_once("Object.php");
require_once("ID3/Object.php");
/**#@-*/
/**
@@ -117,17 +117,17 @@ class ID3_Frame extends ID3_Object
private $_identifier;
/** @var integer */
private $_size;
private $_size = 0;
/** @var integer */
private $_flags;
private $_flags = 0;
/**
* Raw content read from the frame.
* Raw content of the frame.
*
* @var string
*/
protected $_data;
protected $_data = "";
/**
* Constructs the class with given parameters and reads object related data
@@ -136,14 +136,18 @@ class ID3_Frame extends ID3_Object
* @todo Only limited subset of flags are processed.
* @param Reader $reader The reader object.
*/
public function __construct($reader)
public function __construct($reader = null)
{
parent::__construct($reader);
$this->_identifier = $this->_reader->readString8(4);
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
$this->_flags = $this->_reader->readUInt16BE();
$this->_data = $this->_reader->read($this->_size);
if ($reader === null) {
$this->_identifier = substr(get_class($this), -4);
} else {
$this->_identifier = $this->_reader->readString8(4);
$this->_size = $this->decodeSynchsafe32($this->_reader->readUInt32BE());
$this->_flags = $this->_reader->readUInt16BE();
$this->_data = $this->_reader->read($this->_size);
}
}
/**
@@ -185,18 +189,35 @@ class ID3_Frame extends ID3_Object
*
* @return integer
*/
public function getFlags($flags)
{
return $this->_flags;
}
public function getFlags($flags) { return $this->_flags; }
/**
* Sets the frame flags byte.
*
* @param string $flags The flags byte.
*/
public function setFlags($flags)
public function setFlags($flags) { $this->_flags = $flags; }
/**
* Sets the frame raw data.
*
* @return string
*/
protected function setData($data)
{
$this->_flags = $flags;
$this->_data = $data;
$this->_size = strlen($data);
}
/**
* Returns the frame raw data.
*
* @return string
*/
public function __toString()
{
return Transform::toString8(substr($this->_identifier, 0, 4), 4) .
Transform::toUInt32BE($this->encodeSynchsafe32($this->_size)) .
Transform::toUInt16BE($this->_flags) . $this->_data;
}
}

View File

@@ -59,10 +59,12 @@ abstract class ID3_Frame_AbstractLink extends ID3_Frame
*
* @param Reader $reader The reader object.
*/
public function __construct($reader)
public function __construct($reader = null)
{
parent::__construct($reader);
$this->_link = preg_split("/\\x00/", $this->_data, 1);
if ($reader !== null)
$this->_link = preg_split("/\\x00/", $this->_data, 1);
}
/**
@@ -71,4 +73,22 @@ abstract class ID3_Frame_AbstractLink extends ID3_Frame
* @return string
*/
public function getLink() { return $this->_link; }
/**
* Sets the link.
*
* @param string $link The link.
*/
public function setLink($link) { $this->_link = $link; }
/**
* Returns the frame raw data.
*
* @return string
*/
public function __toString()
{
$this->setData($link);
return parent::__toString();
}
}

View File

@@ -54,7 +54,7 @@ abstract class ID3_Frame_AbstractText extends ID3_Frame
implements ID3_Encoding
{
/** @var integer */
private $_encoding;
private $_encoding = ID3_Encoding::UTF8;
/** @var string */
private $_text;
@@ -64,10 +64,13 @@ abstract class ID3_Frame_AbstractText extends ID3_Frame
*
* @param Reader $reader The reader object.
*/
public function __construct($reader)
public function __construct($reader = null)
{
parent::__construct($reader);
if ($reader === null)
return;
$this->_encoding = ord($this->_data{0});
$this->_data = substr($this->_data, 1);
switch ($this->_encoding) {
@@ -91,11 +94,61 @@ abstract class ID3_Frame_AbstractText extends ID3_Frame
* @return integer
*/
public function getEncoding() { return $this->_encoding; }
/**
* Sets the text encoding.
*
* @param integer $encoding The text encoding.
*/
public function setEncoding($encoding) { $this->_encoding = $encoding; };
/**
* Returns the first text chunk the frame contains.
*
* @return string
*/
public function getText() { return $this->_text[0]; }
/**
* Returns an array of texts the frame contains.
*
* @return Array
*/
public function getText() { return $this->_text; }
public function getTexts() { return $this->_text; }
/**
* Sets the text using given encoding.
*
* @param mixed $text The test string or an array of strings.
*/
public function setText($text, $encoding = ID3_Encoding::UTF8)
{
$this->_encoding = $encoding;
$this->_text = is_array($text) ? $text : array($text);
}
/**
* Returns the frame raw data.
*
* @return string
*/
public function __toString()
{
$data = Transform::toInt8($this->_encoding);
switch ($this->_encoding) {
case self::UTF16:
$data .= Transform::toString16(implode("\0\0", $this->_text));
break;
case self::UTF16BE:
$data .= Transform::toString16BE(implode("\0\0", $this->_text));
break;
case self::UTF16LE:
$data .= Transform::toString16LE(implode("\0\0", $this->_text));
break;
default:
$data .= implode("\0", $this->_text);
}
$this->setData($data);
return parent::__toString();
}
}

View File

@@ -36,7 +36,7 @@
*/
/**#@+ @ignore */
require_once("Object.php");
require_once("ID3/Object.php");
/**#@-*/
/**
@@ -72,13 +72,13 @@ final class ID3_Header extends ID3_Object
const FOOTER = 32;
/** @var integer */
private $_version;
private $_version = 4;
/** @var integer */
private $_revision;
private $_revision = 0;
/** @var integer */
private $_flags;
private $_flags = 0;
/** @var integer */
private $_size;
@@ -89,10 +89,13 @@ final class ID3_Header extends ID3_Object
*
* @param Reader $reader The reader object.
*/
public function __construct($reader)
public function __construct($reader = null)
{
parent::__construct($reader);
if ($reader === null)
return;
$this->_version = $this->_reader->readInt8();
$this->_revision = $this->_reader->readInt8();
$this->_flags = $this->_reader->readInt8();
@@ -122,10 +125,45 @@ final class ID3_Header extends ID3_Object
*/
public function hasFlag($flag) { return ($this->_flags & $flag) == $flag; }
/**
* Returns the flags byte.
*
* @return integer
*/
public function getFlags($flags) { return $this->_flags; }
/**
* Sets the flags byte.
*
* @param string $flags The flags byte.
*/
public function setFlags($flags) { $this->_flags = $flags; }
/**
* Returns the tag size, excluding the header and the footer.
*
* @return integer
*/
public function getSize() { return $this->_size; }
/**
* Sets the tag size, excluding the header and the footer. Called
* automatically upon tag generation to adjust the tag size.
*
* @param integer $size The size of the tag, in bytes.
*/
public function setSize($size) { $this->_size = $size; }
/**
* Returns the header/footer raw data without the identifier.
*
* @return string
*/
protected function __toString()
{
return Transform::toInt8($this->_version) .
Transform::toInt8($this->_revision) .
Transform::toInt8($this->_flags) .
Transform::toUInt32BE($this->encodeSynchsafe32($this->_size));
}
}

View File

@@ -59,4 +59,11 @@ interface ID3_Language
* @return string
*/
public function getLanguage();
/**
* Sets the text language code.
*
* @param string $language The text language code.
*/
public function setLanguage($language);
}

View File

@@ -63,4 +63,11 @@ interface ID3_Timing
* @return integer
*/
public function getFormat();
/**
* Sets the timing format.
*
* @param integer $format The timing format.
*/
public function setFormat($format);
}

View File

@@ -123,9 +123,9 @@ final class ID3v1
return;
$this->_reader = new Reader($filename);
if ($this->_reader->size < 128)
if ($this->_reader->getSize() < 128)
return;
$this->_reader->offset = -128;
$this->_reader->setOffset(-128);
if ($this->_reader->read(3) != "TAG") {
$this->_reader = false; // reset reader, see write
return;

View File

@@ -99,7 +99,7 @@ final class ID3v2
private $_extendedHeader;
/** @var ID3_Header */
private $_footer = null;
private $_footer;
/** @var Array */
private $_frames = array();
@@ -111,7 +111,7 @@ final class ID3v2
* Constructs the ID3v2 class with given file and options.
*
* @todo Only limited subset of flags are processed.
* @todo ID3_Footer
* @todo Utilize the SEEK frame and search for a footer to find the tag
* @param string $filename The path to the file.
* @param Array $options The options array.
*/
@@ -123,8 +123,10 @@ final class ID3v2
}
if (($this->_filename = $filename) === false ||
file_exists($filename) === false)
file_exists($filename) === false) {
$this->_header = new ID3_Header();
return;
}
$this->_reader = new Reader($filename);
@@ -137,12 +139,8 @@ final class ID3v2
("File does not contain ID3v2 tag of supported version: " . $filename);
if ($this->_header->hasFlag(ID3_Header::EXTENDEDHEADER))
$this->_extendedHeader = new ID3_ExtendedHeader($this->_reader);
if ($this->_header->hasFlag(ID3_Header::FOOTER)) {
$offset = $this->_reader->offset;
$this->_reader->offset = $this->_header->getSize() + 10;
$this->_footer = new ID3_Header($this->_reader);
$this->_reader->offset = $offset;
}
if ($this->_header->hasFlag(ID3_Header::FOOTER))
$this->_footer = &$this->_header; // skip footer, and rather copy header
while ($frame = $this->nextFrame()) {
if (!isset($this->_frames[$frame->identifier]))
@@ -166,7 +164,8 @@ final class ID3v2
*/
public function hasExtendedHeader()
{
return $this->_header->hasFlag(ID3_Header::EXTENDEDHEADER);
if ($this->_header)
return $this->_header->hasFlag(ID3_Header::EXTENDEDHEADER);
}
/**
@@ -182,6 +181,20 @@ final class ID3v2
return false;
}
/**
* Sets the extended header object.
*
* @param ID3_ExtendedHeader $extendedHeader The header object
*/
public function setExtendedHeader($extendedHeader)
{
if (is_subclass_of($extendedHeader, "ID3_ExtendedHeader")) {
$this->_header->flags =
$this->_header->flags | ID3_Header::EXTENDEDHEADER;
$this->_extendedHeader = $extendedHeader;
} else throw new ID3_Exception("Invalid argument");
}
/**
* Checks whether there are frames left in the tag. Returns <var>true</var> if
* there are frames left in the tag, <var>false</var> otherwise.
@@ -190,16 +203,16 @@ final class ID3v2
*/
protected function hasFrames()
{
$offset = $this->_reader->offset;
$offset = $this->_reader->getOffset();
// Return false if we reached the end of the tag
if ($offset >= $this->_header->getSize() - 10 -
if ($offset - 10 >= $this->_header->getSize() -
($this->hasFooter() ? 10 : 0))
return false;
// Return false if we reached the last frame, true otherwise
$res = $this->_reader->readUInt32BE() != 0;
$this->_reader->offset = $offset;
$this->_reader->setOffset($offset);
return $res;
}
@@ -214,10 +227,11 @@ final class ID3v2
{
$frame = false;
if ($this->hasFrames()) {
$offset = $this->_reader->offset;
$offset = $this->_reader->getOffset();
$identifier = $this->_reader->readString8(4);
$this->_reader->offset = $offset;
if (file_exists($filename = "ID3/Frame/" . $identifier . ".php"))
$this->_reader->setOffset($offset);
if (@fopen($filename = "ID3/Frame/" .
strtoupper($identifier) . ".php", "r", true) !== false)
require_once($filename);
if (class_exists($classname = "ID3_Frame_" . $identifier))
$frame = new $classname($this->_reader);
@@ -276,6 +290,19 @@ final class ID3v2
return $matches;
}
/**
* Adds a new frame to the tag and returns it.
*
* @param ID3_Frame $frame The frame to add.
* @return ID3_Frame
*/
public function addFrame($frame)
{
if (!$this->hasFrame($frame->getIdentifier()))
$this->_frames[$frame->getIdentifier()] = array();
return $this->_frames[$frame->getIdentifier()][] = $frame;
}
/**
* Checks whether there is a footer present in the tag. Returns
* <var>true</var> if the footer is present, <var>false</var> otherwise.
@@ -299,18 +326,131 @@ final class ID3v2
return false;
}
/**
* Sets whether the tag should have a footer defined.
*
* @param boolean $useFooter Whether the tag should have a footer
*/
public function setFooter($useFooter)
{
if ($useFooter) {
$this->_header->setFlags
($this->_header->getFlags() | ID3_Header::FOOTER);
$this->_footer = &$this->_header;
} else {
/* Count footer bytes towards the tag size, so it gets removed or
overridden upon re-write */
if ($this->hasFooter())
$this->_header->setSize($this->_header->getSize() + 10);
$this->_header->setFlags
($this->_header->getFlags() & ~ID3_Header::FOOTER);
$this->_footer = null;
}
}
/**
* Writes the possibly altered ID3v2 tag back to the file where it was read.
* If the class was constructed without a file name, one can be provided here
* as an argument. Regardless, the write operation will override previous
* tag information, if found.
*
* If write is called without setting any frames to the tag, the tag is
* removed from the file.
*
* @param string $filename The optional path to the file.
*/
public function write($filename = false)
{
if (empty($this->_frames))
throw new ID3_Exception("Tag must contain at least one frame");
if ($filename === false && ($filename = $this->_filename) === false)
throw new ID3_Exception("No file given to write the tag to");
if (($fd = fopen
($filename, file_exists($filename) ? "r+b" : "wb")) === false)
throw new ID3_Exception("Unable to open file for writing: " . $filename);
$oldTagSize = $this->_header->getSize();
$tag = "" . $this;
$tagSize = strlen($tag);
if ($this->_reader === null || $tagSize - 10 > $oldTagSize) {
fseek($fd, 0, SEEK_END);
$oldFileSize = ftell($fd);
ftruncate($fd, $newFileSize = $tagSize - $oldTagSize + $oldFileSize);
for ($i = 1, $cur = $oldFileSize; $cur > 0; $cur -= 1024, $i++) {
fseek($fd, -(($i * 1024) + ($newFileSize - $oldFileSize)), SEEK_END);
$buffer = fread($fd, 1024);
fseek($fd, -($i * 1024), SEEK_END);
fwrite($fd, $buffer, 1024);
}
}
fseek($fd, 0);
fwrite($fd, $tag);
$this->_filename = $filename;
}
/**
* Magic function so that $obj->value will work. The method will attempt to
* return the first frame that matches the identifier.
*
* If there is no frame or field with given name, the method will attempt to
* create a frame with given identifier.
*
* If none of these work, an exception is thrown.
*
* @param string $name The frame or field name.
* @return mixed
*/
public function __get($name) {
if (isset($this->_frames[$name]))
return $this->_frames[$name][0];
if (isset($this->_frames[strtoupper($name)]))
return $this->_frames[strtoupper($name)][0];
if (method_exists($this, "get" . ucfirst($name)))
return call_user_func(array($this, "get" . ucfirst($name)));
else throw new ID3_Exception("Unknown frame/field: " . $name);
if (@fopen($filename =
"ID3/Frame/" . strtoupper($name) . ".php", "r", true) !== false)
require_once($filename);
if (class_exists($classname = "ID3_Frame_" . strtoupper($name)))
return $this->addFrame(new $classname());
throw new ID3_Exception("Unknown frame/field: " . $name);
}
/**
* Returns the tag raw data.
*
* @return string
*/
public function __toString()
{
$data = "";
if ($this->hasExtendedHeader())
$data .= $this->getExtendedHeader();
foreach ($this->_frames as $frames)
foreach ($frames as $frame)
$data .= $frame;
$datalen = strlen($data);
$padlen = 0;
/* The tag padding is calculated as follows. If the tag can be written in
the space of the previous tag, the remaining space is used for padding.
If there is no previous tag or the new tag is bigger than the space taken
by the previous tag, the padding is calculated using the following
logaritmic equation: log(0.2(x + 10)), ranging from some 300 bytes to
almost 5000 bytes given the tag length of 0..256M. */
if ($this->hasFooter() === false) {
if ($this->_reader !== null && $datalen < $this->_header->getSize())
$padlen = $this->_header->getSize() - $datalen;
else
$padlen = ceil(log(0.2 * ($datalen / 1024 + 10), 10) * 1024);
}
$this->_header->setSize($datalen + $padlen);
return "ID3" . $this->_header . str_pad($data, $datalen + $padlen, "\0") .
($this->hasFooter() ? "3DI" . $this->getFooter() : "");
}
}

View File

@@ -332,9 +332,9 @@ final class Transform
public static function fromString16($value)
{
if ($value{0} == 0xfe && $value{1} = 0xff)
return self::fromString16LE(substr($value, 2));
else
return self::fromString16BE(substr($value, 2));
else
return self::fromString16LE(substr($value, 2));
}
/**