make use of padding when applying changes to MP3 files
This commit is contained in:
parent
979427beb3
commit
6f0adce661
15
README.md
15
README.md
|
@ -8,11 +8,25 @@ The tag library can read and write the following tag formats:
|
|||
- Vorbis comments (cover art via "METADATA_BLOCK_PICTURE" is supported)
|
||||
- Matroska/WebM tags and attachments
|
||||
|
||||
## File layout options
|
||||
### Tag position
|
||||
The library allows you to choose whether tags should be placed at the beginning or at
|
||||
the end of an MP4/Matroska file.
|
||||
|
||||
### Padding
|
||||
Padding allows adding additional tag information without rewriting the entire file
|
||||
or appending the tag. Usage of padding can be configured:
|
||||
- minimum/maximum padding: The file is rewritten if the padding would fall below/exceed the specifed limits.
|
||||
- preferred padding: If the file needs to be rewritten the preferred padding is used.
|
||||
However, it is also possible to force rewriting the entire file.
|
||||
|
||||
## Additional features
|
||||
The library can also display technical information such as the ID, format, language, bitrate,
|
||||
duration, size, timestamps, sampling frequency, FPS and other information of the tracks.
|
||||
|
||||
It also allows to inspect and validate the element structure of MP4 and Matroska files.
|
||||
|
||||
## Usage
|
||||
For examples check out the CLI interface of my Tag Editor (which is also on Git).
|
||||
|
||||
## Build instructions
|
||||
|
@ -20,6 +34,5 @@ The tagparser library depends on c++utilities and is built in the same way.
|
|||
It also depends on zlib.
|
||||
|
||||
## TODO
|
||||
- Use padding to prevent rewriting the entire file to save tags.
|
||||
- Support more tag formats (EXIF, PDF metadata, ...).
|
||||
- Do tests with Matroska files which have multiple segments.
|
||||
|
|
|
@ -39,7 +39,7 @@ Id3v2Frame::Id3v2Frame() :
|
|||
/*!
|
||||
* \brief Constructs a new Id3v2Frame with the specified \a id, \a value, \a group and \a flag.
|
||||
*/
|
||||
Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, byte group, int16 flag) :
|
||||
Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, const byte group, const int16 flag) :
|
||||
TagField<Id3v2Frame>(id, value),
|
||||
m_flag(flag),
|
||||
m_group(group),
|
||||
|
@ -59,41 +59,57 @@ Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, byte gro
|
|||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
|
||||
void Id3v2Frame::parse(BinaryReader &reader, const uint32 version, const uint32 maximalSize)
|
||||
{
|
||||
invalidateStatus();
|
||||
string context("parsing ID3v2 frame");
|
||||
Id3v2FrameHelper helper(frameIdString(), *this);
|
||||
|
||||
// parse header
|
||||
if(version < 3) {
|
||||
// parse header for ID3v2.1 and ID3v2.2
|
||||
// -> read ID
|
||||
setId(reader.readUInt24BE());
|
||||
if((id() & 0xFFFF0000u) == 0) {
|
||||
// padding reached
|
||||
m_padding = true;
|
||||
addNotification(NotificationType::Debug, "Frame ID starts with null-byte -> padding reached.", context);
|
||||
throw NoDataFoundException();
|
||||
} else {
|
||||
m_padding = false;
|
||||
}
|
||||
context = "parsing " + helper.id() + " frame";
|
||||
|
||||
// -> update context
|
||||
context = "parsing " + frameIdString() + " frame";
|
||||
|
||||
// -> read size, check whether frame is truncated
|
||||
m_dataSize = reader.readUInt24BE();
|
||||
m_totalSize = m_dataSize + 6;
|
||||
if(m_totalSize > maximalSize) {
|
||||
addNotification(NotificationType::Warning, "The frame is truncated and will be ignored.", "parsing " + frameIdString() + " frame");
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
|
||||
// -> no flags/group in ID3v2.2
|
||||
m_flag = 0;
|
||||
m_group = 0;
|
||||
|
||||
} else {
|
||||
// parse header for ID3v2.3 and ID3v2.4
|
||||
// -> read ID
|
||||
setId(reader.readUInt32BE());
|
||||
if((id() & 0xFF000000u) == 0) {
|
||||
// padding reached
|
||||
m_padding = true;
|
||||
addNotification(NotificationType::Debug, "Frame ID starts with null-byte -> padding reached.", context);
|
||||
throw NoDataFoundException();
|
||||
} else {
|
||||
m_padding = false;
|
||||
}
|
||||
context = "parsing " + helper.id() + " frame";
|
||||
|
||||
// -> update context
|
||||
context = "parsing " + frameIdString() + " frame";
|
||||
|
||||
// -> read size, check whether frame is truncated
|
||||
m_dataSize = version >= 4
|
||||
? reader.readSynchsafeUInt32BE()
|
||||
: reader.readUInt32BE();
|
||||
|
@ -102,27 +118,34 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
|
|||
addNotification(NotificationType::Warning, "The frame is truncated and will be ignored.", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
|
||||
// -> read flags and group
|
||||
m_flag = reader.readUInt16BE();
|
||||
m_group = hasGroupInformation() ? reader.readByte() : 0;
|
||||
if(isEncrypted()) {
|
||||
// encryption is not implemented
|
||||
addNotification(NotificationType::Critical, "Encrypted frames aren't supported.", context);
|
||||
throw VersionNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
// frame size mustn't be 0
|
||||
if(m_dataSize <= 0) {
|
||||
addNotification(NotificationType::Critical, "The frame size is 0.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
// parse the data
|
||||
unique_ptr<char[]> buffer;
|
||||
|
||||
// -> decompress data if compressed; otherwise just read it
|
||||
if(isCompressed()) {
|
||||
// decompress compressed data
|
||||
uLongf decompressedSize = version >= 4 ? reader.readSynchsafeUInt32BE() : reader.readUInt32BE();
|
||||
if(decompressedSize < m_dataSize) {
|
||||
addNotification(NotificationType::Critical, "The decompressed size is smaller then the compressed size.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
unique_ptr<char[]> bufferCompressed = make_unique<char[]>(m_dataSize);;
|
||||
auto bufferCompressed = make_unique<char[]>(m_dataSize);;
|
||||
reader.read(bufferCompressed.get(), m_dataSize);
|
||||
buffer = make_unique<char[]>(decompressedSize);
|
||||
switch(uncompress(reinterpret_cast<Bytef *>(buffer.get()), &decompressedSize, reinterpret_cast<Bytef *>(bufferCompressed.get()), m_dataSize)) {
|
||||
|
@ -136,16 +159,21 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
|
|||
addNotification(NotificationType::Critical, "Decompressing failed. The input data was corrupted or incomplete.", context);
|
||||
throw InvalidDataException();
|
||||
case Z_OK:
|
||||
;
|
||||
break;
|
||||
default:
|
||||
addNotification(NotificationType::Critical, "Decompressing failed (unknown reason).", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
m_dataSize = decompressedSize;
|
||||
} else {
|
||||
buffer = make_unique<char[]>(m_dataSize);
|
||||
reader.read(buffer.get(), m_dataSize);
|
||||
}
|
||||
if(Id3v2FrameIds::isTextfield(id())) {
|
||||
|
||||
// -> get tag value depending of field type
|
||||
if(Id3v2FrameIds::isTextFrame(id())) {
|
||||
// frame contains text
|
||||
TagTextEncoding dataEncoding = helper.parseTextEncodingByte(*buffer.get()); // the first byte stores the encoding
|
||||
TagTextEncoding dataEncoding = parseTextEncodingByte(*buffer.get()); // the first byte stores the encoding
|
||||
if((version >= 3 &&
|
||||
(id() == Id3v2FrameIds::lTrackPosition || id() == Id3v2FrameIds::lDiskPosition))
|
||||
|| (version < 3 && id() == Id3v2FrameIds::sTrackPosition)) {
|
||||
|
@ -153,40 +181,42 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
|
|||
try {
|
||||
PositionInSet position;
|
||||
if(characterSize(dataEncoding) > 1) {
|
||||
position = PositionInSet(helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding));
|
||||
position = PositionInSet(parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding));
|
||||
} else {
|
||||
position = PositionInSet(helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding));
|
||||
position = PositionInSet(parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding));
|
||||
}
|
||||
value().assignPosition(position);
|
||||
} catch(ConversionException &) {
|
||||
addNotification(NotificationType::Warning, "The value of track/disk position frame is not numeric and will be ignored.", context);
|
||||
}
|
||||
|
||||
} else if((version >= 3 && id() == Id3v2FrameIds::lLength) || (version < 3 && id() == Id3v2FrameIds::sLength)) {
|
||||
// frame contains length
|
||||
double milliseconds;
|
||||
try {
|
||||
if(characterSize(dataEncoding) > 1) {
|
||||
wstring millisecondsStr = helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
wstring millisecondsStr = parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
milliseconds = ConversionUtilities::stringToNumber<double, wstring>(millisecondsStr, 10);
|
||||
} else {
|
||||
milliseconds = ConversionUtilities::stringToNumber<double>(helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding), 10);
|
||||
milliseconds = ConversionUtilities::stringToNumber<double>(parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding), 10);
|
||||
}
|
||||
value().assignTimeSpan(TimeSpan::fromMilliseconds(milliseconds));
|
||||
} catch (ConversionException &) {
|
||||
addNotification(NotificationType::Warning, "The value of the length frame is not numeric and will be ignored.", context);
|
||||
}
|
||||
|
||||
} else if((version >= 3 && id() == Id3v2FrameIds::lGenre) || (version < 3 && id() == Id3v2FrameIds::sGenre)) {
|
||||
// genre/content type
|
||||
int genreIndex;
|
||||
try {
|
||||
if(characterSize(dataEncoding) > 1) {
|
||||
wstring indexStr = helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
wstring indexStr = parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
if(indexStr.front() == L'(' && indexStr.back() == L')') {
|
||||
indexStr = indexStr.substr(1, indexStr.length() - 2);
|
||||
}
|
||||
genreIndex = ConversionUtilities::stringToNumber<int, wstring>(indexStr, 10);
|
||||
} else {
|
||||
string indexStr = helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
string indexStr = parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
if(indexStr.front() == '(' && indexStr.back() == ')') {
|
||||
indexStr = indexStr.substr(1, indexStr.length() - 2);
|
||||
}
|
||||
|
@ -196,169 +226,64 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
|
|||
} catch(ConversionException &) {
|
||||
// genre is specified as string
|
||||
// string might be null terminated
|
||||
auto substr = helper.parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
auto substr = parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
value().assignData(get<0>(substr), get<1>(substr), TagDataType::Text, dataEncoding);
|
||||
}
|
||||
} else { // any other text frame
|
||||
// string might be null terminated
|
||||
auto substr = helper.parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
auto substr = parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding);
|
||||
value().assignData(get<0>(substr), get<1>(substr), TagDataType::Text, dataEncoding);
|
||||
}
|
||||
|
||||
} else if(version >= 3 && id() == Id3v2FrameIds::lCover) {
|
||||
// frame stores picture
|
||||
byte type;
|
||||
helper.parsePicture(buffer.get(), m_dataSize, value(), type);
|
||||
parsePicture(buffer.get(), m_dataSize, value(), type);
|
||||
setTypeInfo(type);
|
||||
|
||||
} else if(version < 3 && id() == Id3v2FrameIds::sCover) {
|
||||
// frame stores legacy picutre
|
||||
byte type;
|
||||
helper.parseLegacyPicture(buffer.get(), m_dataSize, value(), type);
|
||||
parseLegacyPicture(buffer.get(), m_dataSize, value(), type);
|
||||
setTypeInfo(type);
|
||||
|
||||
} else if(((version >= 3 && id() == Id3v2FrameIds::lComment) || (version < 3 && id() == Id3v2FrameIds::sComment))
|
||||
|| ((version >= 3 && id() == Id3v2FrameIds::lUnsynchronizedLyrics) || (version < 3 && id() == Id3v2FrameIds::sUnsynchronizedLyrics))) {
|
||||
// comment frame or unsynchronized lyrics frame (these two frame types have the same structure)
|
||||
helper.parseComment(buffer.get(), m_dataSize, value());
|
||||
parseComment(buffer.get(), m_dataSize, value());
|
||||
|
||||
} else {
|
||||
// unknown frame
|
||||
value().assignData(buffer.get(), m_dataSize, TagDataType::Undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Prepares making.
|
||||
* \returns Returns a Id3v2FrameMaker object which can be used to actually make the frame.
|
||||
* \remarks The field must NOT be mutated after making is prepared when it is intended to actually
|
||||
* make the field using the make method of the returned object.
|
||||
* \throws Throws Media::Failure or a derived exception when a making
|
||||
* error occurs.
|
||||
*
|
||||
* This method might be useful when it is necessary to know the size of the field before making it.
|
||||
*/
|
||||
Id3v2FrameMaker Id3v2Frame::prepareMaking(const uint32 version)
|
||||
{
|
||||
return Id3v2FrameMaker(*this, version);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Writes the frame to a stream using the specified \a writer and the
|
||||
* specified ID3v2 version.
|
||||
* specified ID3v2 \a version.
|
||||
*
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Media::Failure or a derived exception when a making
|
||||
* error occurs.
|
||||
*/
|
||||
void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version)
|
||||
void Id3v2Frame::make(BinaryWriter &writer, const uint32 version)
|
||||
{
|
||||
invalidateStatus();
|
||||
Id3v2FrameHelper helper(frameIdString(), *this);
|
||||
const string context("making " + helper.id() + " frame");
|
||||
// check if a valid frame can be build from the data
|
||||
if(value().isEmpty()) {
|
||||
addNotification(NotificationType::Critical, "Cannot make an empty frame.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
if(isEncrypted()) {
|
||||
addNotification(NotificationType::Critical, "Cannot make an encrypted frame (isn't supported by this tagging library).", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
if(m_padding) {
|
||||
addNotification(NotificationType::Critical, "Cannot make a frame which is marked as padding.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
uint32 frameId = id();
|
||||
if(version >= 3) {
|
||||
if(Id3v2FrameIds::isShortId(frameId)) {
|
||||
// try to convert the short frame id to its long equivalent
|
||||
frameId = Id3v2FrameIds::convertToLongId(frameId);
|
||||
if(frameId == 0) {
|
||||
addNotification(NotificationType::Critical, "The short frame ID can't be converted to its long equivalent which is needed to use the frame in a newer version of ID3v2.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(Id3v2FrameIds::isLongId(frameId)) {
|
||||
// try to convert the long frame id to its short equivalent
|
||||
frameId = Id3v2FrameIds::convertToShortId(frameId);
|
||||
if(frameId == 0) {
|
||||
addNotification(NotificationType::Critical, "The long frame ID can't be converted to its short equivalent which is needed to use the frame in the old version of ID3v2.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(version < 3 && (m_flag != 0 || m_group != 0)) {
|
||||
addNotification(NotificationType::Warning, "The existing flag and group information is not supported by the version of ID3v2 and will be ignored/discarted.", context);
|
||||
}
|
||||
// create actual data, depending on the frame type
|
||||
unique_ptr<char[]> buffer;
|
||||
uint32 decompressedSize;
|
||||
// check if the frame to be written is a text frame
|
||||
try {
|
||||
if(Id3v2FrameIds::isTextfield(frameId)) {
|
||||
if((version >= 3 && (frameId == Id3v2FrameIds::lTrackPosition || frameId == Id3v2FrameIds::lDiskPosition))
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sTrackPosition)) {
|
||||
// the track number or the disk number frame
|
||||
helper.makeString(buffer, decompressedSize, value().toString(), TagTextEncoding::Latin1);
|
||||
} else if((version >= 3 && frameId == Id3v2FrameIds::lLength)
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sLength)) {
|
||||
// the length
|
||||
helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1);
|
||||
} else if(value().type() == TagDataType::StandardGenreIndex && ((version >= 3 && frameId == Id3v2FrameIds::lGenre)
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sGenre))) {
|
||||
// genre/content type as standard genre index
|
||||
helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1);
|
||||
} else {
|
||||
// any other text frame
|
||||
helper.makeString(buffer, decompressedSize, value().toString(), value().dataEncoding()); // the same as a normal text frame
|
||||
}
|
||||
} else if(version >= 3 && frameId == Id3v2FrameIds::lCover) {
|
||||
// picture frame
|
||||
helper.makePicture(buffer, decompressedSize, value(), isTypeInfoAssigned() ? typeInfo() : 0);
|
||||
} else if(version < 3 && frameId == Id3v2FrameIds::sCover) {
|
||||
// legacy picture frame
|
||||
helper.makeLegacyPicture(buffer, decompressedSize, value(), isTypeInfoAssigned() ? typeInfo() : 0);
|
||||
} else if(((version >= 3 && id() == Id3v2FrameIds::lComment)
|
||||
|| (version < 3 && id() == Id3v2FrameIds::sComment))
|
||||
|| ((version >= 3 && id() == Id3v2FrameIds::lUnsynchronizedLyrics)
|
||||
|| (version < 3 && id() == Id3v2FrameIds::sUnsynchronizedLyrics))) {
|
||||
// the comment frame or the unsynchronized lyrics frame
|
||||
helper.makeComment(buffer, decompressedSize, value());
|
||||
} else {
|
||||
// an unknown frame
|
||||
// create buffer
|
||||
buffer = make_unique<char[]>(decompressedSize = value().dataSize());
|
||||
// just write the data
|
||||
copy(value().dataPointer(), value().dataPointer() + value().dataSize(), buffer.get());
|
||||
}
|
||||
} catch(ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
unsigned long actualSize;
|
||||
if(version >= 3 && isCompressed()) {
|
||||
actualSize = compressBound(decompressedSize);
|
||||
auto compressedBuffer = make_unique<char[]>(actualSize);
|
||||
switch(compress(reinterpret_cast<Bytef *>(compressedBuffer.get()), &actualSize, reinterpret_cast<Bytef *>(buffer.get()), decompressedSize)) {
|
||||
case Z_MEM_ERROR:
|
||||
addNotification(NotificationType::Critical, "Decompressing failed. The source buffer was too small.", context);
|
||||
throw InvalidDataException();
|
||||
case Z_BUF_ERROR:
|
||||
addNotification(NotificationType::Critical, "Decompressing failed. The destination buffer was too small.", context);
|
||||
throw InvalidDataException();
|
||||
case Z_OK:
|
||||
;
|
||||
}
|
||||
buffer.swap(compressedBuffer);
|
||||
} else {
|
||||
actualSize = decompressedSize;
|
||||
}
|
||||
if(version < 3) {
|
||||
writer.writeUInt24BE(frameId);
|
||||
writer.writeUInt24BE(actualSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(frameId);
|
||||
if(version >= 4) {
|
||||
writer.writeSynchsafeUInt32BE(actualSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(actualSize);
|
||||
}
|
||||
writer.writeUInt16BE(m_flag);
|
||||
if(hasGroupInformation()) {
|
||||
writer.writeByte(m_group);
|
||||
}
|
||||
if(isCompressed()) {
|
||||
if(version >= 4) {
|
||||
writer.writeSynchsafeUInt32BE(decompressedSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(decompressedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.write(buffer.get(), actualSize);
|
||||
prepareMaking(version).make(writer);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -375,19 +300,183 @@ void Id3v2Frame::cleared()
|
|||
}
|
||||
|
||||
/*!
|
||||
* \class Media::Id3v2FrameHelper
|
||||
* \brief The Id3v2FrameHelper class helps parsing and making ID3v2 frames.
|
||||
* \class Media::Id3v2FrameMaker
|
||||
* \brief The Id3v2FrameMaker class helps making ID3v2 frames.
|
||||
* It allows to calculate the required size.
|
||||
* \sa See Id3v2FrameMaker::prepareMaking() for more information.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief The Id3v2FrameHelper class helps parsing and making ID3v2 frames.
|
||||
* \param id Specifies the identifier of the current frame (used to print warnings).
|
||||
* \param provider Specifies the status provider to store warnings.
|
||||
* \brief Prepares making the specified \a frame.
|
||||
* \sa See Id3v2Frame::prepareMaking() for more information.
|
||||
*/
|
||||
Id3v2FrameHelper::Id3v2FrameHelper(const std::string &id, StatusProvider &provider) :
|
||||
m_id(id),
|
||||
m_statusProvider(provider)
|
||||
{}
|
||||
Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, const byte version) :
|
||||
m_frame(frame),
|
||||
m_frameId(m_frame.id()),
|
||||
m_version(version)
|
||||
{
|
||||
m_frame.invalidateStatus();
|
||||
const string context("making " + m_frame.frameIdString() + " frame");
|
||||
|
||||
// validate assigned data
|
||||
if(m_frame.value().isEmpty()) {
|
||||
m_frame.addNotification(NotificationType::Critical, "Cannot make an empty frame.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
if(m_frame.isEncrypted()) {
|
||||
m_frame.addNotification(NotificationType::Critical, "Cannot make an encrypted frame (isn't supported by this tagging library).", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
if(m_frame.hasPaddingReached()) {
|
||||
m_frame.addNotification(NotificationType::Critical, "Cannot make a frame which is marked as padding.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
if(version < 3 && m_frame.isCompressed()) {
|
||||
m_frame.addNotification(NotificationType::Warning, "Compression is not supported by the version of ID3v2 and won't be applied.", context);
|
||||
}
|
||||
if(version < 3 && (m_frame.flag() || m_frame.group())) {
|
||||
m_frame.addNotification(NotificationType::Warning, "The existing flag and group information is not supported by the version of ID3v2 and will be ignored/discarted.", context);
|
||||
}
|
||||
|
||||
// convert frame ID if necessary
|
||||
if(version >= 3) {
|
||||
if(Id3v2FrameIds::isShortId(m_frameId)) {
|
||||
// try to convert the short frame ID to its long equivalent
|
||||
if(!(m_frameId = Id3v2FrameIds::convertToLongId(m_frameId))) {
|
||||
m_frame.addNotification(NotificationType::Critical, "The short frame ID can't be converted to its long equivalent which is needed to use the frame in a newer version of ID3v2.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(Id3v2FrameIds::isLongId(m_frameId)) {
|
||||
// try to convert the long frame ID to its short equivalent
|
||||
if(!(m_frameId = Id3v2FrameIds::convertToShortId(m_frameId))) {
|
||||
m_frame.addNotification(NotificationType::Critical, "The long frame ID can't be converted to its short equivalent which is needed to use the frame in the old version of ID3v2.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make actual data depending on the frame ID
|
||||
try {
|
||||
if(Id3v2FrameIds::isTextFrame(m_frameId)) {
|
||||
// it is a text frame
|
||||
if((version >= 3 && (m_frameId == Id3v2FrameIds::lTrackPosition || m_frameId == Id3v2FrameIds::lDiskPosition))
|
||||
|| (version < 3 && m_frameId == Id3v2FrameIds::sTrackPosition)) {
|
||||
// track number or the disk number frame
|
||||
m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), TagTextEncoding::Latin1);
|
||||
} else if((version >= 3 && m_frameId == Id3v2FrameIds::lLength)
|
||||
|| (version < 3 && m_frameId == Id3v2FrameIds::sLength)) {
|
||||
// length frame
|
||||
m_frame.makeString(m_data, m_decompressedSize, ConversionUtilities::numberToString(m_frame.value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1);
|
||||
} else if(m_frame.value().type() == TagDataType::StandardGenreIndex && ((version >= 3 && m_frameId == Id3v2FrameIds::lGenre)
|
||||
|| (version < 3 && m_frameId == Id3v2FrameIds::sGenre))) {
|
||||
// pre-defined genre frame
|
||||
m_frame.makeString(m_data, m_decompressedSize, ConversionUtilities::numberToString(m_frame.value().toStandardGenreIndex()), TagTextEncoding::Latin1);
|
||||
} else {
|
||||
// any other text frame
|
||||
m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), m_frame.value().dataEncoding()); // the same as a normal text frame
|
||||
}
|
||||
|
||||
} else if(version >= 3 && m_frameId == Id3v2FrameIds::lCover) {
|
||||
// picture frame
|
||||
m_frame.makePicture(m_data, m_decompressedSize, m_frame.value(), m_frame.isTypeInfoAssigned() ? m_frame.typeInfo() : 0);
|
||||
|
||||
} else if(version < 3 && m_frameId == Id3v2FrameIds::sCover) {
|
||||
// legacy picture frame
|
||||
m_frame.makeLegacyPicture(m_data, m_decompressedSize, m_frame.value(), m_frame.isTypeInfoAssigned() ? m_frame.typeInfo() : 0);
|
||||
|
||||
} else if(((version >= 3 && m_frameId == Id3v2FrameIds::lComment)
|
||||
|| (version < 3 && m_frameId == Id3v2FrameIds::sComment))
|
||||
|| ((version >= 3 && m_frameId == Id3v2FrameIds::lUnsynchronizedLyrics)
|
||||
|| (version < 3 && m_frameId == Id3v2FrameIds::sUnsynchronizedLyrics))) {
|
||||
// the comment frame or the unsynchronized lyrics frame
|
||||
m_frame.makeComment(m_data, m_decompressedSize, m_frame.value());
|
||||
|
||||
} else {
|
||||
// an unknown frame
|
||||
// create buffer
|
||||
m_data = make_unique<char[]>(m_decompressedSize = m_frame.value().dataSize());
|
||||
// just write the data
|
||||
copy(m_frame.value().dataPointer(), m_frame.value().dataPointer() + m_decompressedSize, m_data.get());
|
||||
}
|
||||
} catch(ConversionException &) {
|
||||
m_frame.addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
// apply compression if frame should be compressed
|
||||
if(version >= 3 && m_frame.isCompressed()) {
|
||||
m_dataSize = compressBound(m_decompressedSize);
|
||||
auto compressedData = make_unique<char[]>(m_decompressedSize);
|
||||
switch(compress(reinterpret_cast<Bytef *>(compressedData.get()), reinterpret_cast<uLongf *>(&m_dataSize), reinterpret_cast<Bytef *>(m_data.get()), m_decompressedSize)) {
|
||||
case Z_MEM_ERROR:
|
||||
m_frame.addNotification(NotificationType::Critical, "Decompressing failed. The source buffer was too small.", context);
|
||||
throw InvalidDataException();
|
||||
case Z_BUF_ERROR:
|
||||
m_frame.addNotification(NotificationType::Critical, "Decompressing failed. The destination buffer was too small.", context);
|
||||
throw InvalidDataException();
|
||||
case Z_OK:
|
||||
;
|
||||
}
|
||||
m_data.swap(compressedData);
|
||||
} else {
|
||||
m_dataSize = m_decompressedSize;
|
||||
}
|
||||
|
||||
// calculate required size
|
||||
// -> data size
|
||||
m_requiredSize = m_dataSize;
|
||||
if(version < 3) {
|
||||
// -> header size
|
||||
m_requiredSize += 3;
|
||||
} else {
|
||||
// -> header size
|
||||
m_requiredSize += 10;
|
||||
// -> group byte
|
||||
if(m_frame.hasGroupInformation()) {
|
||||
m_requiredSize += 1;
|
||||
}
|
||||
// -> decompressed size
|
||||
if(version >= 3 && m_frame.isCompressed()) {
|
||||
m_requiredSize += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Saves the frame (specified when constructing the object) using
|
||||
* the specified \a writer.
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Assumes the data is already validated and thus does NOT
|
||||
* throw Media::Failure or a derived exception.
|
||||
*/
|
||||
void Id3v2FrameMaker::make(BinaryWriter &writer)
|
||||
{
|
||||
if(m_version < 3) {
|
||||
writer.writeUInt24BE(m_frameId);
|
||||
writer.writeUInt24BE(m_dataSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(m_frameId);
|
||||
if(m_version >= 4) {
|
||||
writer.writeSynchsafeUInt32BE(m_dataSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(m_dataSize);
|
||||
}
|
||||
writer.writeUInt16BE(m_frame.flag());
|
||||
if(m_frame.hasGroupInformation()) {
|
||||
writer.writeByte(m_frame.group());
|
||||
}
|
||||
if(m_version >= 3 && m_frame.isCompressed()) {
|
||||
if(m_version >= 4) {
|
||||
writer.writeSynchsafeUInt32BE(m_decompressedSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(m_decompressedSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.write(m_data.get(), m_dataSize);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the text encoding for the specified \a textEncodingByte.
|
||||
|
@ -395,7 +484,7 @@ Id3v2FrameHelper::Id3v2FrameHelper(const std::string &id, StatusProvider &provid
|
|||
* If the \a textEncodingByte doesn't match any encoding TagTextEncoding::Latin1 is
|
||||
* returned and a parsing notification is added.
|
||||
*/
|
||||
TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte)
|
||||
TagTextEncoding Id3v2Frame::parseTextEncodingByte(byte textEncodingByte)
|
||||
{
|
||||
switch(textEncodingByte) {
|
||||
case 0: // Ascii
|
||||
|
@ -407,7 +496,7 @@ TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte)
|
|||
case 3: // Utf 8
|
||||
return TagTextEncoding::Utf8;
|
||||
default:
|
||||
m_statusProvider.addNotification(NotificationType::Warning, "The charset of the frame is invalid. Latin-1 will be used.", "parsing encoding of frame " + m_id);
|
||||
addNotification(NotificationType::Warning, "The charset of the frame is invalid. Latin-1 will be used.", "parsing encoding of frame " + frameIdString());
|
||||
return TagTextEncoding::Latin1;
|
||||
}
|
||||
}
|
||||
|
@ -415,7 +504,7 @@ TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte)
|
|||
/*!
|
||||
* \brief Returns a text encoding byte for the specified \a textEncoding.
|
||||
*/
|
||||
byte Id3v2FrameHelper::makeTextEncodingByte(TagTextEncoding textEncoding)
|
||||
byte Id3v2Frame::makeTextEncodingByte(TagTextEncoding textEncoding)
|
||||
{
|
||||
switch(textEncoding) {
|
||||
case TagTextEncoding::Latin1:
|
||||
|
@ -445,7 +534,7 @@ byte Id3v2FrameHelper::makeTextEncodingByte(TagTextEncoding textEncoding)
|
|||
* \remarks The length is always returned as the number of bytes, not as the number of characters (makes a difference for
|
||||
* UTF-16 encodings).
|
||||
*/
|
||||
tuple<const char *, size_t, const char *> Id3v2FrameHelper::parseSubstring(const char *buffer, size_t bufferSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
tuple<const char *, size_t, const char *> Id3v2Frame::parseSubstring(const char *buffer, size_t bufferSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
{
|
||||
tuple<const char *, size_t, const char *> res(buffer, 0, buffer + bufferSize);
|
||||
switch(encoding) {
|
||||
|
@ -466,7 +555,7 @@ tuple<const char *, size_t, const char *> Id3v2FrameHelper::parseSubstring(const
|
|||
get<1>(res) += 2;
|
||||
} else {
|
||||
if(addWarnings) {
|
||||
m_statusProvider.addNotification(NotificationType::Warning, "Wide string in frame is not terminated proberly.", "parsing termination of frame " + m_id);
|
||||
addNotification(NotificationType::Warning, "Wide string in frame is not terminated proberly.", "parsing termination of frame " + frameIdString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -485,7 +574,7 @@ tuple<const char *, size_t, const char *> Id3v2FrameHelper::parseSubstring(const
|
|||
++get<1>(res);
|
||||
} else {
|
||||
if(addWarnings) {
|
||||
m_statusProvider.addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + m_id);
|
||||
addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + frameIdString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -500,9 +589,9 @@ tuple<const char *, size_t, const char *> Id3v2FrameHelper::parseSubstring(const
|
|||
/*!
|
||||
* \brief Parses a substring in the specified \a buffer.
|
||||
*
|
||||
* Same as Id3v2FrameHelper::parseSubstring() but returns the substring as string object.
|
||||
* Same as Id3v2Frame::parseSubstring() but returns the substring as string object.
|
||||
*/
|
||||
string Id3v2FrameHelper::parseString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
string Id3v2Frame::parseString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
{
|
||||
auto substr = parseSubstring(buffer, dataSize, encoding, addWarnings);
|
||||
return string(get<0>(substr), get<1>(substr));
|
||||
|
@ -511,9 +600,9 @@ string Id3v2FrameHelper::parseString(const char *buffer, size_t dataSize, TagTex
|
|||
/*!
|
||||
* \brief Parses a substring in the specified \a buffer.
|
||||
*
|
||||
* Same as Id3v2FrameHelper::parseSubstring() but returns the substring as wstring object.
|
||||
* Same as Id3v2Frame::parseSubstring() but returns the substring as wstring object.
|
||||
*/
|
||||
wstring Id3v2FrameHelper::parseWideString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
wstring Id3v2Frame::parseWideString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings)
|
||||
{
|
||||
auto substr = parseSubstring(buffer, dataSize, encoding, addWarnings);
|
||||
return wstring(reinterpret_cast<wstring::const_pointer>(get<0>(substr)), get<1>(substr) / 2);
|
||||
|
@ -528,7 +617,7 @@ wstring Id3v2FrameHelper::parseWideString(const char *buffer, size_t dataSize, T
|
|||
*
|
||||
* \remarks This method is not used anymore and might be deleted.
|
||||
*/
|
||||
void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncoding &encoding)
|
||||
void Id3v2Frame::parseBom(const char *buffer, size_t maxSize, TagTextEncoding &encoding)
|
||||
{
|
||||
switch(encoding) {
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
|
@ -542,7 +631,7 @@ void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncod
|
|||
default:
|
||||
if((maxSize >= 3) && (ConversionUtilities::BE::toUInt24(buffer) == 0x00EFBBBF)) {
|
||||
encoding = TagTextEncoding::Utf8;
|
||||
m_statusProvider.addNotification(NotificationType::Warning, "UTF-8 byte order mark found in text frame.", "parsing byte oder mark of frame " + m_id);
|
||||
addNotification(NotificationType::Warning, "UTF-8 byte order mark found in text frame.", "parsing byte oder mark of frame " + frameIdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -554,11 +643,11 @@ void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncod
|
|||
* \param tagValue Specifies the tag value used to store the results.
|
||||
* \param typeInfo Specifies a byte used to store the type info.
|
||||
*/
|
||||
void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo)
|
||||
void Id3v2Frame::parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo)
|
||||
{
|
||||
static const string context("parsing ID3v2.2 picture frame");
|
||||
if(maxSize < 6) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete.", context);
|
||||
addNotification(NotificationType::Critical, "Picture frame is incomplete.", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
const char *end = buffer + maxSize;
|
||||
|
@ -568,7 +657,7 @@ void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, Ta
|
|||
auto substr = parseSubstring(buffer + 5, end - 5 - buffer, dataEncoding, true);
|
||||
tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding);
|
||||
if(get<2>(substr) >= end) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context);
|
||||
addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
tagValue.assignData(get<2>(substr), end - get<2>(substr), TagDataType::Picture, dataEncoding);
|
||||
|
@ -581,7 +670,7 @@ void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, Ta
|
|||
* \param tagValue Specifies the tag value used to store the results.
|
||||
* \param typeInfo Specifies a byte used to store the type info.
|
||||
*/
|
||||
void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo)
|
||||
void Id3v2Frame::parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo)
|
||||
{
|
||||
static const string context("parsing ID3v2.3 picture frame");
|
||||
const char *end = buffer + maxSize;
|
||||
|
@ -592,18 +681,18 @@ void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue
|
|||
tagValue.setMimeType(string(get<0>(substr), get<1>(substr)));
|
||||
}
|
||||
if(get<2>(substr) >= end) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (type info, description and actual data are missing).", context);
|
||||
addNotification(NotificationType::Critical, "Picture frame is incomplete (type info, description and actual data are missing).", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
typeInfo = static_cast<unsigned char>(*get<2>(substr));
|
||||
if(++get<2>(substr) >= end) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (description and actual data are missing).", context);
|
||||
addNotification(NotificationType::Critical, "Picture frame is incomplete (description and actual data are missing).", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
substr = parseSubstring(get<2>(substr), end - get<2>(substr), dataEncoding, true);
|
||||
tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding);
|
||||
if(get<2>(substr) >= end) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context);
|
||||
addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
tagValue.assignData(get<2>(substr), end - get<2>(substr), TagDataType::Picture, dataEncoding);
|
||||
|
@ -615,12 +704,12 @@ void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue
|
|||
* \param dataSize Specifies the maximal number of bytes to read from the buffer.
|
||||
* \param tagValue Specifies the tag value used to store the results.
|
||||
*/
|
||||
void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValue &tagValue)
|
||||
void Id3v2Frame::parseComment(const char *buffer, size_t dataSize, TagValue &tagValue)
|
||||
{
|
||||
static const string context("parsing comment frame");
|
||||
const char *end = buffer + dataSize;
|
||||
if(dataSize < 6) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Comment frame is incomplete.", context);
|
||||
addNotification(NotificationType::Critical, "Comment frame is incomplete.", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
TagTextEncoding dataEncoding = parseTextEncodingByte(*buffer);
|
||||
|
@ -628,7 +717,7 @@ void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValu
|
|||
auto substr = parseSubstring(buffer += 3, dataSize -= 4, dataEncoding, true);
|
||||
tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding);
|
||||
if(get<2>(substr) >= end) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Comment frame is incomplete (description not terminated?).", context);
|
||||
addNotification(NotificationType::Critical, "Comment frame is incomplete (description not terminated?).", context);
|
||||
throw TruncatedDataException();
|
||||
}
|
||||
substr = parseSubstring(get<2>(substr), end - get<2>(substr), dataEncoding, false);
|
||||
|
@ -641,9 +730,9 @@ void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValu
|
|||
* \param value Specifies the string to make.
|
||||
* \param encoding Specifies the encoding of the string to make.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeString(unique_ptr<char[]> &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding)
|
||||
void Id3v2Frame::makeString(unique_ptr<char[]> &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding)
|
||||
{
|
||||
makeEncodingAndData(buffer, bufferSize, encoding, value.c_str(), value.length());
|
||||
makeEncodingAndData(buffer, bufferSize, encoding, value.data(), value.size());
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -653,7 +742,7 @@ void Id3v2FrameHelper::makeString(unique_ptr<char[]> &buffer, uint32 &bufferSize
|
|||
* \param data Specifies the data.
|
||||
* \param dataSize Specifies the data size.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeEncodingAndData(unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize)
|
||||
void Id3v2Frame::makeEncodingAndData(unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize)
|
||||
{
|
||||
// calculate buffer size
|
||||
if(!data) {
|
||||
|
@ -681,7 +770,7 @@ void Id3v2FrameHelper::makeEncodingAndData(unique_ptr<char[]> &buffer, uint32 &b
|
|||
/*!
|
||||
* \brief Writes the specified picture to the specified buffer (ID3v2.2).
|
||||
*/
|
||||
void Id3v2FrameHelper::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
|
||||
void Id3v2Frame::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
|
||||
{
|
||||
// calculate needed buffer size and create buffer
|
||||
TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
|
||||
|
@ -724,7 +813,7 @@ void Id3v2FrameHelper::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &buf
|
|||
/*!
|
||||
* \brief Writes the specified picture to the specified buffer (ID3v2.3).
|
||||
*/
|
||||
void Id3v2FrameHelper::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
|
||||
void Id3v2Frame::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
|
||||
{
|
||||
// calculate needed buffer size and create buffer
|
||||
TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
|
||||
|
@ -760,18 +849,18 @@ void Id3v2FrameHelper::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSiz
|
|||
/*!
|
||||
* \brief Writes the specified comment to the specified buffer.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment)
|
||||
void Id3v2Frame::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment)
|
||||
{
|
||||
static const string context("making comment frame");
|
||||
// check type and other values are valid
|
||||
TagTextEncoding encoding = comment.dataEncoding();
|
||||
if(!comment.description().empty() && encoding != comment.descriptionEncoding()) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "Data enoding and description encoding aren't equal.", context);
|
||||
addNotification(NotificationType::Critical, "Data enoding and description encoding aren't equal.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
const string &lng = comment.language();
|
||||
if(lng.length() > 3) {
|
||||
m_statusProvider.addNotification(NotificationType::Critical, "The language must be 3 bytes long (ISO-639-2).", context);
|
||||
addNotification(NotificationType::Critical, "The language must be 3 bytes long (ISO-639-2).", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
// calculate needed buffer size and create buffer
|
||||
|
|
117
id3/id3v2frame.h
117
id3/id3v2frame.h
|
@ -19,35 +19,61 @@
|
|||
namespace Media
|
||||
{
|
||||
|
||||
class LIB_EXPORT Id3v2FrameHelper
|
||||
class Id3v2Frame;
|
||||
|
||||
class LIB_EXPORT Id3v2FrameMaker
|
||||
{
|
||||
friend class Id3v2Frame;
|
||||
|
||||
public:
|
||||
Id3v2FrameHelper(const std::string &id, StatusProvider &provider);
|
||||
|
||||
const std::string &id() const;
|
||||
|
||||
TagTextEncoding parseTextEncodingByte(byte textEncodingByte);
|
||||
std::tuple<const char *, size_t, const char *> parseSubstring(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
std::string parseString(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
std::wstring parseWideString(const char *buffer, std::size_t dataSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
void parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
|
||||
void parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
|
||||
void parseComment(const char *buffer, size_t maxSize, TagValue &tagValue);
|
||||
void parseBom(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding);
|
||||
|
||||
byte makeTextEncodingByte(TagTextEncoding textEncoding);
|
||||
void makeString(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const std::string &value, TagTextEncoding encoding);
|
||||
void makeEncodingAndData(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t m_dataSize);
|
||||
void makeLegacyPicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
|
||||
void makePicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
|
||||
void makeComment(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment);
|
||||
void make(IoUtilities::BinaryWriter &writer);
|
||||
const Id3v2Frame &field() const;
|
||||
const std::unique_ptr<char[]> &data() const;
|
||||
uint32 dataSize() const;
|
||||
uint32 requiredSize() const;
|
||||
|
||||
private:
|
||||
std::string m_id;
|
||||
StatusProvider &m_statusProvider;
|
||||
Id3v2FrameMaker(Id3v2Frame &frame, const byte version);
|
||||
Id3v2Frame &m_frame;
|
||||
uint32 m_frameId;
|
||||
const byte m_version;
|
||||
std::unique_ptr<char[]> m_data;
|
||||
uint32 m_dataSize;
|
||||
uint32 m_decompressedSize;
|
||||
uint32 m_requiredSize;
|
||||
};
|
||||
|
||||
class Id3v2Frame;
|
||||
/*!
|
||||
* \brief Returns the associated frame.
|
||||
*/
|
||||
inline const Id3v2Frame &Id3v2FrameMaker::field() const
|
||||
{
|
||||
return m_frame;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the frame data.
|
||||
*/
|
||||
inline const std::unique_ptr<char[]> &Id3v2FrameMaker::data() const
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the size of the array returned by data().
|
||||
*/
|
||||
inline uint32 Id3v2FrameMaker::dataSize() const
|
||||
{
|
||||
return m_dataSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns number of bytes which will be written when making the frame.
|
||||
*/
|
||||
inline uint32 Id3v2FrameMaker::requiredSize() const
|
||||
{
|
||||
return m_requiredSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Defines traits for the TagField implementation of the Id3v2Frame class.
|
||||
|
@ -72,31 +98,26 @@ public:
|
|||
typedef Id3v2Frame implementationType;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Returns the ID of the current frame.
|
||||
*/
|
||||
inline const std::string &Id3v2FrameHelper::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
class LIB_EXPORT Id3v2Frame : public TagField<Id3v2Frame>, public StatusProvider
|
||||
{
|
||||
friend class TagField<Id3v2Frame>;
|
||||
|
||||
public:
|
||||
Id3v2Frame();
|
||||
Id3v2Frame(const identifierType &id, const TagValue &value, byte group = 0, int16 flag = 0);
|
||||
Id3v2Frame(const identifierType &id, const TagValue &value, const byte group = 0, const int16 flag = 0);
|
||||
|
||||
void parse(IoUtilities::BinaryReader &reader, int32 version, uint32 maximalSize = 0);
|
||||
void make(IoUtilities::BinaryWriter &writer, int32 version);
|
||||
// parsing/making
|
||||
void parse(IoUtilities::BinaryReader &reader, const uint32 version, const uint32 maximalSize = 0);
|
||||
Id3v2FrameMaker prepareMaking(const uint32 version);
|
||||
void make(IoUtilities::BinaryWriter &writer, const uint32 version);
|
||||
|
||||
// member access
|
||||
bool isAdditionalTypeInfoUsed() const;
|
||||
bool isValid() const;
|
||||
bool hasPaddingReached() const;
|
||||
std::string frameIdString() const;
|
||||
int16 flag() const;
|
||||
void setFlag(int16 value);
|
||||
uint16 flag() const;
|
||||
void setFlag(uint16 value);
|
||||
uint32 totalSize() const;
|
||||
uint32 dataSize() const;
|
||||
bool toDiscardWhenUnknownAndTagIsAltered() const;
|
||||
|
@ -112,13 +133,31 @@ public:
|
|||
int32 parsedVersion() const;
|
||||
bool supportsNestedFields() const;
|
||||
|
||||
// parsing helper
|
||||
TagTextEncoding parseTextEncodingByte(byte textEncodingByte);
|
||||
std::tuple<const char *, size_t, const char *> parseSubstring(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
std::string parseString(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
std::wstring parseWideString(const char *buffer, std::size_t dataSize, TagTextEncoding &encoding, bool addWarnings = false);
|
||||
void parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
|
||||
void parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
|
||||
void parseComment(const char *buffer, size_t maxSize, TagValue &tagValue);
|
||||
void parseBom(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding);
|
||||
|
||||
// making helper
|
||||
byte makeTextEncodingByte(TagTextEncoding textEncoding);
|
||||
void makeString(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const std::string &value, TagTextEncoding encoding);
|
||||
void makeEncodingAndData(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t m_dataSize);
|
||||
void makeLegacyPicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
|
||||
void makePicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
|
||||
void makeComment(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment);
|
||||
|
||||
protected:
|
||||
void cleared();
|
||||
|
||||
private:
|
||||
uint16 m_flag;
|
||||
byte m_group;
|
||||
int32 m_parsedVersion;
|
||||
uint32 m_parsedVersion;
|
||||
uint32 m_dataSize;
|
||||
uint32 m_totalSize;
|
||||
bool m_padding;
|
||||
|
@ -163,7 +202,7 @@ inline std::string Id3v2Frame::frameIdString() const
|
|||
/*!
|
||||
* \brief Returns the flags.
|
||||
*/
|
||||
inline int16 Id3v2Frame::flag() const
|
||||
inline uint16 Id3v2Frame::flag() const
|
||||
{
|
||||
return m_flag;
|
||||
}
|
||||
|
@ -171,7 +210,7 @@ inline int16 Id3v2Frame::flag() const
|
|||
/*!
|
||||
* \brief Sets the flags.
|
||||
*/
|
||||
inline void Id3v2Frame::setFlag(int16 value)
|
||||
inline void Id3v2Frame::setFlag(uint16 value)
|
||||
{
|
||||
m_flag = value;
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ inline bool isShortId(uint32 id)
|
|||
/*!
|
||||
* \brief Returns an indication whether the specified \a id is a text frame id.
|
||||
*/
|
||||
inline bool isTextfield(uint32 id)
|
||||
inline bool isTextFrame(uint32 id)
|
||||
{
|
||||
if(isShortId(id)) {
|
||||
return (id & 0x00FF0000u) == 0x00540000u;
|
||||
|
|
151
id3/id3v2tag.cpp
151
id3/id3v2tag.cpp
|
@ -138,7 +138,7 @@ TagDataType Id3v2Tag::proposedDataType(const uint32 &id) const
|
|||
case lCover: case sCover:
|
||||
return TagDataType::Picture;
|
||||
default:
|
||||
if(Id3v2FrameIds::isTextfield(id)) {
|
||||
if(Id3v2FrameIds::isTextFrame(id)) {
|
||||
return TagDataType::Text;
|
||||
} else {
|
||||
return TagDataType::Undefined;
|
||||
|
@ -181,7 +181,7 @@ bool Id3v2Tag::setValue(const typename Id3v2Frame::identifierType &id, const Tag
|
|||
* \throws Throws Media::Failure or a derived exception when a parsing
|
||||
* error occurs.
|
||||
*/
|
||||
void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
||||
void Id3v2Tag::parse(istream &stream, const uint64 maximalSize)
|
||||
{
|
||||
// prepare parsing
|
||||
invalidateStatus();
|
||||
|
@ -229,7 +229,7 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
|||
|
||||
// how many bytes remain for frames and padding?
|
||||
uint32 bytesRemaining = m_sizeExcludingHeader - m_extendedHeaderSize;
|
||||
if(bytesRemaining > maximalSize) {
|
||||
if(maximalSize && bytesRemaining > maximalSize) {
|
||||
bytesRemaining = maximalSize;
|
||||
addNotification(NotificationType::Critical, "Frames are truncated.", context);
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
|||
frame.parse(reader, majorVersion, bytesRemaining);
|
||||
if(frame.id()) {
|
||||
// add frame if parsing was successfull
|
||||
if(Id3v2FrameIds::isTextfield(frame.id()) && fields().count(frame.id()) == 1) {
|
||||
if(Id3v2FrameIds::isTextFrame(frame.id()) && fields().count(frame.id()) == 1) {
|
||||
addNotification(NotificationType::Warning, "The text frame " + frame.frameIdString() + " exists more than once.", context);
|
||||
}
|
||||
fields().insert(pair<fieldType::identifierType, fieldType>(frame.id(), frame));
|
||||
|
@ -264,13 +264,18 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
|||
frame.invalidateNotifications();
|
||||
|
||||
// calculate next frame offset
|
||||
bytesRemaining -= frame.totalSize();
|
||||
pos += frame.totalSize();
|
||||
if(frame.totalSize() <= bytesRemaining) {
|
||||
pos += frame.totalSize();
|
||||
bytesRemaining -= frame.totalSize();
|
||||
} else {
|
||||
pos += bytesRemaining;
|
||||
bytesRemaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// check for extended header
|
||||
if(hasFooter()) {
|
||||
if(m_size + 10 < maximalSize) {
|
||||
if(maximalSize && m_size + 10 < maximalSize) {
|
||||
// the footer does not provide additional information, just check the signature
|
||||
stream.seekg(startOffset + (m_size += 10));
|
||||
if(reader.readUInt24LE() != 0x494433u) {
|
||||
|
@ -290,6 +295,21 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Prepares making.
|
||||
* \returns Returns a Id3v2TagMaker object which can be used to actually make the tag.
|
||||
* \remarks The tag must NOT be mutated after making is prepared when it is intended to actually
|
||||
* make the tag using the make method of the returned object.
|
||||
* \throws Throws Media::Failure or a derived exception when a making error occurs.
|
||||
*
|
||||
* This method might be useful when it is necessary to know the size of the tag before making it.
|
||||
* \sa make()
|
||||
*/
|
||||
Id3v2TagMaker Id3v2Tag::prepareMaking()
|
||||
{
|
||||
return Id3v2TagMaker(*this);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Writes tag information to the specified \a stream.
|
||||
*
|
||||
|
@ -297,50 +317,9 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
|
|||
* \throws Throws Media::Failure or a derived exception when a making
|
||||
* error occurs.
|
||||
*/
|
||||
void Id3v2Tag::make(ostream &stream)
|
||||
void Id3v2Tag::make(ostream &stream, uint32 padding)
|
||||
{
|
||||
// prepare making
|
||||
invalidateStatus();
|
||||
const string context("making ID3v2 tag");
|
||||
// check if version is supported
|
||||
// (the version could have been changed using setVersion(...)
|
||||
if(!isVersionSupported()) {
|
||||
addNotification(NotificationType::Critical, "The ID3v2 tag couldn't be created, because the target version isn't supported.", context);
|
||||
throw VersionNotSupportedException();
|
||||
}
|
||||
// prepare for writing
|
||||
BinaryWriter writer(&stream);
|
||||
// write header
|
||||
writer.writeUInt24BE(0x494433u); // signature
|
||||
writer.writeByte(m_majorVersion); // major version
|
||||
writer.writeByte(m_revisionVersion); // revision version
|
||||
writer.writeByte(m_flags & 0xBF); // flags, but without extended header or compression bit set
|
||||
stream.seekp(4, ios_base::cur); // size currently unknown, write it later
|
||||
streamoff framesOffset = stream.tellp();
|
||||
int framesWritten = 0;
|
||||
for(auto i : fields()) {
|
||||
Id3v2Frame &frame = i.second;
|
||||
// write only valid frames
|
||||
if(frame.isValid()) {
|
||||
// make the frame
|
||||
try {
|
||||
frame.make(writer, m_majorVersion);
|
||||
++framesWritten;
|
||||
} catch(Failure &) {
|
||||
// nothing to do here since notifications will be added anyways
|
||||
}
|
||||
// add making notifications
|
||||
addNotifications(context, frame);
|
||||
}
|
||||
}
|
||||
// calculate and write size
|
||||
streamoff endOffset = stream.tellp();
|
||||
stream.seekp(framesOffset - 4, ios_base::beg);
|
||||
writer.writeSynchsafeUInt32BE(endOffset - framesOffset);
|
||||
stream.seekp(endOffset, ios_base::beg);
|
||||
if(framesWritten <= 0) { // add a warning notification if an empty ID3v2 tag has been written
|
||||
addNotification(NotificationType::Warning, "No frames could be written, an empty ID3v2 tag has been written.", context);
|
||||
}
|
||||
prepareMaking().make(stream, padding);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -383,8 +362,8 @@ bool FrameComparer::operator()(const uint32 &lhs, const uint32 &rhs) const
|
|||
if(rhs == Id3v2FrameIds::lTitle || rhs == Id3v2FrameIds::sTitle) {
|
||||
return false;
|
||||
}
|
||||
bool lhstextfield = Id3v2FrameIds::isTextfield(lhs);
|
||||
bool rhstextfield = Id3v2FrameIds::isTextfield(rhs);
|
||||
bool lhstextfield = Id3v2FrameIds::isTextFrame(lhs);
|
||||
bool rhstextfield = Id3v2FrameIds::isTextFrame(rhs);
|
||||
if(lhstextfield && !rhstextfield) {
|
||||
return true;
|
||||
}
|
||||
|
@ -400,4 +379,72 @@ bool FrameComparer::operator()(const uint32 &lhs, const uint32 &rhs) const
|
|||
return lhs < rhs;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Prepares making the specified \a tag.
|
||||
* \sa See Id3v2Tag::prepareMaking() for more information.
|
||||
*/
|
||||
Id3v2TagMaker::Id3v2TagMaker(Id3v2Tag &tag) :
|
||||
m_tag(tag),
|
||||
m_framesSize(0)
|
||||
{
|
||||
tag.invalidateStatus();
|
||||
const string context("making ID3v2 tag");
|
||||
|
||||
// check if version is supported
|
||||
// (the version could have been changed using setVersion())
|
||||
if(!tag.isVersionSupported()) {
|
||||
tag.addNotification(NotificationType::Critical, "The ID3v2 tag version isn't supported.", context);
|
||||
throw VersionNotSupportedException();
|
||||
}
|
||||
|
||||
// prepare frames
|
||||
m_maker.reserve(tag.fields().size());
|
||||
for(auto &pair : tag.fields()) {
|
||||
try {
|
||||
m_maker.emplace_back(pair.second.prepareMaking(tag.majorVersion()));
|
||||
m_framesSize += m_maker.back().requiredSize();
|
||||
} catch(const Failure &) {
|
||||
// nothing to do here; notifications will be added anyways
|
||||
}
|
||||
m_tag.addNotifications(pair.second);
|
||||
}
|
||||
|
||||
// calculate required size
|
||||
// -> header + size of frames
|
||||
m_requiredSize = 10 + m_framesSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Saves the tag (specified when constructing the object) to the
|
||||
* specified \a stream.
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Assumes the data is already validated and thus does NOT
|
||||
* throw Media::Failure or a derived exception.
|
||||
*/
|
||||
void Id3v2TagMaker::make(std::ostream &stream, uint32 padding)
|
||||
{
|
||||
BinaryWriter writer(&stream);
|
||||
|
||||
// write header
|
||||
// -> signature
|
||||
writer.writeUInt24BE(0x494433u);
|
||||
// -> version
|
||||
writer.writeByte(m_tag.majorVersion());
|
||||
writer.writeByte(m_tag.revisionVersion());
|
||||
// -> flags, but without extended header or compression bit set
|
||||
writer.writeByte(m_tag.flags() & 0xBF);
|
||||
// -> size (excluding header)
|
||||
writer.writeSynchsafeUInt32BE(m_framesSize + padding);
|
||||
|
||||
// write frames
|
||||
for(auto &maker : m_maker) {
|
||||
maker.make(writer);
|
||||
}
|
||||
|
||||
// write padding
|
||||
for(; padding; --padding) {
|
||||
stream.put(0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,11 +10,48 @@
|
|||
namespace Media
|
||||
{
|
||||
|
||||
class Id3v2Tag;
|
||||
|
||||
struct LIB_EXPORT FrameComparer
|
||||
{
|
||||
bool operator()(const uint32& lhs, const uint32& rhs) const;
|
||||
bool operator()(const uint32 &lhs, const uint32 &rhs) const;
|
||||
};
|
||||
|
||||
class LIB_EXPORT Id3v2TagMaker
|
||||
{
|
||||
friend class Id3v2Tag;
|
||||
|
||||
public:
|
||||
void make(std::ostream &stream, uint32 padding);
|
||||
const Id3v2Tag &tag() const;
|
||||
uint64 requiredSize() const;
|
||||
|
||||
private:
|
||||
Id3v2TagMaker(Id3v2Tag &tag);
|
||||
|
||||
Id3v2Tag &m_tag;
|
||||
uint32 m_framesSize;
|
||||
uint32 m_requiredSize;
|
||||
std::vector<Id3v2FrameMaker> m_maker;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Returns the associated tag.
|
||||
*/
|
||||
inline const Id3v2Tag &Id3v2TagMaker::tag() const
|
||||
{
|
||||
return m_tag;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the number of bytes which will be written when making the tag.
|
||||
* \remarks Excludes padding!
|
||||
*/
|
||||
inline uint64 Id3v2TagMaker::requiredSize() const
|
||||
{
|
||||
return m_requiredSize;
|
||||
}
|
||||
|
||||
class LIB_EXPORT Id3v2Tag : public FieldMapBasedTag<Id3v2Frame, FrameComparer>
|
||||
{
|
||||
public:
|
||||
|
@ -34,8 +71,9 @@ public:
|
|||
bool supportsDescription(KnownField field) const;
|
||||
bool supportsMimeType(KnownField field) const;
|
||||
|
||||
void parse(std::istream &sourceStream, uint64 maximalSize = 0);
|
||||
void make(std::ostream &targetStream);
|
||||
void parse(std::istream &sourceStream, const uint64 maximalSize = 0);
|
||||
Id3v2TagMaker prepareMaking();
|
||||
void make(std::ostream &targetStream, uint32 padding);
|
||||
|
||||
byte majorVersion() const;
|
||||
byte revisionVersion() const;
|
||||
|
|
|
@ -1320,14 +1320,6 @@ nonRewriteCalculations:
|
|||
throw;
|
||||
}
|
||||
|
||||
|
||||
// define variables needed to handle output stream and backup stream (required when rewriting the file)
|
||||
string backupPath;
|
||||
fstream &outputStream = fileInfo().stream();
|
||||
fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
|
||||
BinaryWriter outputWriter(&outputStream);
|
||||
char buff[8]; // buffer used to make size denotations
|
||||
|
||||
if(isAborted()) {
|
||||
throw OperationAbortedException();
|
||||
}
|
||||
|
@ -1335,6 +1327,14 @@ nonRewriteCalculations:
|
|||
// setup stream(s) for writing
|
||||
// -> update status
|
||||
updateStatus("Preparing streams ...");
|
||||
|
||||
// -> define variables needed to handle output stream and backup stream (required when rewriting the file)
|
||||
string backupPath;
|
||||
fstream &outputStream = fileInfo().stream();
|
||||
fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
|
||||
BinaryWriter outputWriter(&outputStream);
|
||||
char buff[8]; // buffer used to make size denotations
|
||||
|
||||
if(rewriteRequired) {
|
||||
// move current file to temp dir and reopen it as backupStream, recreate original file
|
||||
try {
|
||||
|
@ -1351,7 +1351,6 @@ nonRewriteCalculations:
|
|||
}
|
||||
|
||||
} else { // !rewriteRequired
|
||||
|
||||
// buffer currently assigned attachments
|
||||
for(auto &maker : attachmentMaker) {
|
||||
maker.bufferCurrentAttachments();
|
||||
|
@ -1636,8 +1635,6 @@ nonRewriteCalculations:
|
|||
|
||||
// reparse what is written so far
|
||||
updateStatus("Reparsing output file ...");
|
||||
// -> report new size
|
||||
fileInfo().reportSizeChanged(outputStream.tellp());
|
||||
if(rewriteRequired) {
|
||||
// report new size
|
||||
fileInfo().reportSizeChanged(outputStream.tellp());
|
||||
|
|
|
@ -232,13 +232,13 @@ MatroskaTagMaker::MatroskaTagMaker(MatroskaTag &tag) :
|
|||
}
|
||||
m_tagSize = 2 + EbmlElement::calculateSizeDenotationLength(m_targetsSize) + m_targetsSize;
|
||||
// calculate size of "SimpleTag" elements
|
||||
m_makers.reserve(m_tag.fields().size());
|
||||
m_maker.reserve(m_tag.fields().size());
|
||||
m_simpleTagsSize = 0; // including ID and size
|
||||
for(auto &pair : m_tag.fields()) {
|
||||
try {
|
||||
m_makers.emplace_back(pair.second.prepareMaking());
|
||||
m_simpleTagsSize += m_makers.back().requiredSize();
|
||||
} catch(Failure &) {
|
||||
m_maker.emplace_back(pair.second.prepareMaking());
|
||||
m_simpleTagsSize += m_maker.back().requiredSize();
|
||||
} catch(const Failure &) {
|
||||
// nothing to do here; notifications will be added anyways
|
||||
}
|
||||
m_tag.addNotifications(pair.second);
|
||||
|
@ -298,7 +298,7 @@ void MatroskaTagMaker::make(ostream &stream) const
|
|||
}
|
||||
}
|
||||
// write "SimpleTag" elements using maker objects prepared previously
|
||||
for(const auto &maker : m_makers) {
|
||||
for(const auto &maker : m_maker) {
|
||||
maker.make(stream);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ private:
|
|||
MatroskaTag &m_tag;
|
||||
uint64 m_targetsSize;
|
||||
uint64 m_simpleTagsSize;
|
||||
std::vector<MatroskaTagFieldMaker> m_makers;
|
||||
std::vector<MatroskaTagFieldMaker> m_maker;
|
||||
uint64 m_tagSize;
|
||||
uint64 m_totalSize;
|
||||
};
|
||||
|
|
|
@ -362,7 +362,7 @@ void MediaFileInfo::parseTags()
|
|||
auto id3v2Tag = make_unique<Id3v2Tag>();
|
||||
stream().seekg(offset, ios_base::beg);
|
||||
try {
|
||||
id3v2Tag->parse(stream());
|
||||
id3v2Tag->parse(stream(), size() - offset);
|
||||
m_paddingSize += id3v2Tag->paddingSize();
|
||||
} catch(NoDataFoundException &) {
|
||||
continue;
|
||||
|
@ -622,22 +622,6 @@ void MediaFileInfo::applyChanges()
|
|||
previousParsingSuccessful = false;
|
||||
addNotification(NotificationType::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context);
|
||||
}
|
||||
// switch(chaptersParsingStatus()) {
|
||||
// case ParsingStatus::Ok:
|
||||
// case ParsingStatus::NotSupported:
|
||||
// break;
|
||||
// default:
|
||||
// previousParsingSuccessful = false;
|
||||
// addNotification(NotificationType::Critical, "Chapters have to be parsed without critical errors before changes can be applied.", context);
|
||||
// }
|
||||
// switch(attachmentsParsingStatus()) {
|
||||
// case ParsingStatus::Ok:
|
||||
// case ParsingStatus::NotSupported:
|
||||
// break;
|
||||
// default:
|
||||
// previousParsingSuccessful = false;
|
||||
// addNotification(NotificationType::Critical, "Attachments have to be parsed without critical errors before changes can be applied.", context);
|
||||
// }
|
||||
if(!previousParsingSuccessful) {
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
@ -1359,12 +1343,12 @@ void MediaFileInfo::makeMp3File()
|
|||
{
|
||||
const string context("making MP3 file");
|
||||
// there's no need to rewrite the complete file if there is just are not ID3v2 tags present or to be written
|
||||
if(m_id3v2Tags.size() == 0 && m_actualId3v2TagOffsets.size() == 0) {
|
||||
if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty()) {
|
||||
if(m_actualExistingId3v1Tag) {
|
||||
// there is currently an ID3v1 tag at the end of the file
|
||||
if(m_id3v1Tag) {
|
||||
// the file shall still have an ID3v1 tag
|
||||
updateStatus("No need to rewrite the whole file, just writing ID3v1 tag ...");
|
||||
updateStatus("Updating ID3v1 tag ...");
|
||||
// ensure the file is still open / not readonly
|
||||
open();
|
||||
stream().seekp(-128, ios_base::end);
|
||||
|
@ -1375,7 +1359,7 @@ void MediaFileInfo::makeMp3File()
|
|||
}
|
||||
} else {
|
||||
// the currently existing ID3v1 tag shall be removed
|
||||
updateStatus("No need to rewrite the whole file, just truncating it to remove ID3v1 tag ...");
|
||||
updateStatus("Removing ID3v1 tag ...");
|
||||
stream().close();
|
||||
if(truncate(path().c_str(), size() - 128) == 0) {
|
||||
reportSizeChanged(size() - 128);
|
||||
|
@ -1388,7 +1372,7 @@ void MediaFileInfo::makeMp3File()
|
|||
} else {
|
||||
// there is currently no ID3v1 tag at the end of the file
|
||||
if(m_id3v1Tag) {
|
||||
updateStatus("No need to rewrite the whole file, just writing ID3v1 tag.");
|
||||
updateStatus("Adding ID3v1 tag ...");
|
||||
// ensure the file is still open / not readonly
|
||||
open();
|
||||
stream().seekp(0, ios_base::end);
|
||||
|
@ -1403,49 +1387,110 @@ void MediaFileInfo::makeMp3File()
|
|||
}
|
||||
|
||||
} else {
|
||||
// ID3v2 needs to be modified -> file needs to be rewritten
|
||||
// TODO: take advantage of possibly available padding
|
||||
// ID3v2 needs to be modified
|
||||
updateStatus("Updating ID3v2 tags ...");
|
||||
|
||||
// prepare for rewriting
|
||||
updateStatus("Prepareing for rewriting MP3 file ...");
|
||||
// prepare ID3v2 tags
|
||||
vector<Id3v2TagMaker> makers;
|
||||
makers.reserve(m_id3v2Tags.size());
|
||||
uint32 tagsSize = 0;
|
||||
for(auto &tag : m_id3v2Tags) {
|
||||
try {
|
||||
makers.emplace_back(tag->prepareMaking());
|
||||
tagsSize += makers.back().requiredSize();
|
||||
} catch(const Failure &) {
|
||||
// nothing to do: notifications added anyways
|
||||
}
|
||||
addNotifications(*tag);
|
||||
}
|
||||
|
||||
// determine padding, check whether rewrite is required
|
||||
bool rewriteRequired = isForcingRewrite() || (tagsSize > static_cast<uint32>(m_containerOffset));
|
||||
uint32 padding;
|
||||
if(!rewriteRequired) {
|
||||
padding = static_cast<uint32>(m_containerOffset) - tagsSize;
|
||||
// check whether padding matches specifications
|
||||
if(padding < minPadding() || padding > maxPadding()) {
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
if(rewriteRequired) {
|
||||
// use preferred padding when rewriting
|
||||
padding = preferredPadding();
|
||||
updateStatus("Preparing streams for rewriting ...");
|
||||
} else {
|
||||
updateStatus("Preparing streams for updating ...");
|
||||
}
|
||||
|
||||
// setup stream(s) for writing
|
||||
// -> define variables needed to handle output stream and backup stream (required when rewriting the file)
|
||||
string backupPath;
|
||||
fstream backupStream;
|
||||
fstream &outputStream = stream();
|
||||
fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
|
||||
|
||||
if(rewriteRequired) {
|
||||
// move current file to temp dir and reopen it as backupStream, recreate original file
|
||||
try {
|
||||
// ensure the file is close before moving
|
||||
close();
|
||||
BackupHelper::createBackupFile(path(), backupPath, backupStream);
|
||||
// recreate original file, define buffer variables
|
||||
outputStream.open(path(), ios_base::out | ios_base::binary | ios_base::trunc);
|
||||
} catch(const ios_base::failure &) {
|
||||
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
|
||||
throw;
|
||||
}
|
||||
|
||||
} else { // !rewriteRequired
|
||||
// reopen original file to ensure it is opened for writing
|
||||
try {
|
||||
close();
|
||||
outputStream.open(path(), ios_base::in | ios_base::out | ios_base::binary);
|
||||
} catch(const ios_base::failure &) {
|
||||
addNotification(NotificationType::Critical, "Opening the file with write permissions failed.", context);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// start actual writing
|
||||
try {
|
||||
close();
|
||||
BackupHelper::createBackupFile(path(), backupPath, backupStream);
|
||||
backupStream.seekg(m_containerOffset);
|
||||
|
||||
// recreate original file with new/changed ID3 tags
|
||||
stream().open(path(), ios_base::out | ios_base::binary | ios_base::trunc);
|
||||
updateStatus("Writing ID3v2 tag ...");
|
||||
|
||||
// write ID3v2 tags
|
||||
unsigned int counter = 1;
|
||||
for(auto &id3v2Tag : m_id3v2Tags) {
|
||||
try {
|
||||
id3v2Tag->make(stream());
|
||||
} catch(const Failure &) {
|
||||
if(m_id3v2Tags.size()) {
|
||||
addNotification(NotificationType::Warning, "Unable to write " + ConversionUtilities::numberToString(counter) + ". ID3v2 tag.", context);
|
||||
} else {
|
||||
addNotification(NotificationType::Warning, "Unable to write ID3v2 tag.", context);
|
||||
}
|
||||
if(!makers.empty()) {
|
||||
// write ID3v2 tags
|
||||
updateStatus("Writing ID3v2 tag ...");
|
||||
for(auto i = makers.begin(), end = makers.end() - 1; i != end; ++i) {
|
||||
i->make(outputStream, 0);
|
||||
}
|
||||
// include padding into the last ID3v2 tag
|
||||
makers.back().make(outputStream, padding);
|
||||
} else {
|
||||
// no ID3v2 tags assigned -> just write padding
|
||||
for(; padding; --padding) {
|
||||
outputStream.put(0);
|
||||
}
|
||||
++counter;
|
||||
}
|
||||
|
||||
// write media data
|
||||
updateStatus("Writing MPEG audio frames ...");
|
||||
uint64 bytesRemaining = size() - m_containerOffset;
|
||||
// copy / skip media data
|
||||
// -> determine media data size
|
||||
uint64 mediaDataSize = size() - m_containerOffset;
|
||||
if(m_actualExistingId3v1Tag) {
|
||||
bytesRemaining -= 128;
|
||||
mediaDataSize -= 128;
|
||||
}
|
||||
|
||||
if(rewriteRequired) {
|
||||
// copy data from original file
|
||||
updateStatus("Writing MPEG audio frames ...");
|
||||
backupStream.seekg(m_containerOffset);
|
||||
CopyHelper<0x4000> copyHelper;
|
||||
copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1));
|
||||
updatePercentage(100.0);
|
||||
} else {
|
||||
// just skip media data
|
||||
outputStream.seekp(mediaDataSize, ios_base::cur);
|
||||
}
|
||||
CopyHelper<0x4000> copyHelper;
|
||||
copyHelper.callbackCopy(backupStream, stream(), bytesRemaining, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1));
|
||||
|
||||
// write ID3v1 tag
|
||||
updateStatus("Writing ID3v1 tag ...");
|
||||
if(m_id3v1Tag) {
|
||||
updateStatus("Writing ID3v1 tag ...");
|
||||
try {
|
||||
m_id3v1Tag->make(stream());
|
||||
} catch(Failure &) {
|
||||
|
@ -1453,22 +1498,74 @@ void MediaFileInfo::makeMp3File()
|
|||
}
|
||||
}
|
||||
|
||||
// ensure everything has been actually written
|
||||
stream().flush();
|
||||
// report new size
|
||||
reportSizeChanged(stream().tellp());
|
||||
// stream is useless for further usage because it is write-only
|
||||
close();
|
||||
// handle streams
|
||||
if(rewriteRequired) {
|
||||
// report new size
|
||||
reportSizeChanged(outputStream.tellp());
|
||||
// stream is useless for further usage because it is write-only
|
||||
outputStream.close();
|
||||
} else {
|
||||
const auto newSize = static_cast<uint64>(outputStream.tellp());
|
||||
if(newSize < size()) {
|
||||
// file is smaller after the modification -> truncate
|
||||
// -> close stream before truncating
|
||||
outputStream.close();
|
||||
// -> truncate file
|
||||
if(truncate(path().c_str(), newSize) == 0) {
|
||||
reportSizeChanged(newSize);
|
||||
} else {
|
||||
addNotification(NotificationType::Critical, "Unable to truncate the file.", context);
|
||||
}
|
||||
} else {
|
||||
// file is longer after the modification -> just report new size
|
||||
reportSizeChanged(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
} catch(const OperationAbortedException &) {
|
||||
addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context);
|
||||
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream);
|
||||
if(&stream() != &outputStream) {
|
||||
// a temp/backup file has been created -> restore original file
|
||||
addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
|
||||
try {
|
||||
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
|
||||
addNotification(NotificationType::Information, "The original file has been restored.", context);
|
||||
} catch(const ios_base::failure &ex) {
|
||||
addNotification(NotificationType::Critical, ex.what(), context);
|
||||
}
|
||||
} else {
|
||||
addNotification(NotificationType::Information, "Applying new tag information has been aborted.", context);
|
||||
}
|
||||
throw;
|
||||
} catch(const ios_base::failure &ex) {
|
||||
addNotification(NotificationType::Critical, "IO error occured when rewriting file to apply new tag information.\n" + string(ex.what()), context);
|
||||
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream);
|
||||
} catch(const Failure &) {
|
||||
if(&stream() != &outputStream) {
|
||||
// a temp/backup file has been created -> restore original file
|
||||
addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
|
||||
try {
|
||||
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
|
||||
addNotification(NotificationType::Information, "The original file has been restored.", context);
|
||||
} catch(const ios_base::failure &ex) {
|
||||
addNotification(NotificationType::Critical, ex.what(), context);
|
||||
}
|
||||
} else {
|
||||
addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
|
||||
}
|
||||
throw;
|
||||
} catch(const ios_base::failure &) {
|
||||
if(&stream() != &outputStream) {
|
||||
// a temp/backup file has been created -> restore original file
|
||||
addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
|
||||
try {
|
||||
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
|
||||
addNotification(NotificationType::Information, "The original file has been restored.", context);
|
||||
} catch(const ios_base::failure &ex) {
|
||||
addNotification(NotificationType::Critical, ex.what(), context);
|
||||
}
|
||||
} else {
|
||||
addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
// TODO: reduce code duplication
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -436,15 +436,16 @@ calculatePadding:
|
|||
throw OperationAbortedException();
|
||||
}
|
||||
|
||||
// define variables needed to handle output stream and backup stream (required when rewriting the file)
|
||||
// setup stream(s) for writing
|
||||
// -> update status
|
||||
updateStatus("Preparing streams ...");
|
||||
|
||||
// -> define variables needed to handle output stream and backup stream (required when rewriting the file)
|
||||
string backupPath;
|
||||
fstream &outputStream = fileInfo().stream();
|
||||
fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
|
||||
BinaryWriter outputWriter(&outputStream);
|
||||
|
||||
// setup stream(s) for writing
|
||||
// -> update status
|
||||
updateStatus("Preparing streams ...");
|
||||
if(rewriteRequired) {
|
||||
// move current file to temp dir and reopen it as backupStream, recreate original file
|
||||
try {
|
||||
|
@ -461,7 +462,6 @@ calculatePadding:
|
|||
}
|
||||
|
||||
} else { // !rewriteRequired
|
||||
|
||||
// reopen original file to ensure it is opened for writing
|
||||
try {
|
||||
fileInfo().close();
|
||||
|
|
|
@ -269,12 +269,17 @@ Mp4TagMaker::Mp4TagMaker(Mp4Tag &tag) :
|
|||
m_omitPreDefinedGenre(m_tag.fields().count(Mp4TagAtomIds::PreDefinedGenre) && m_tag.fields().count(Mp4TagAtomIds::Genre))
|
||||
{
|
||||
m_tag.invalidateStatus();
|
||||
m_makers.reserve(m_tag.fields().size());
|
||||
m_maker.reserve(m_tag.fields().size());
|
||||
for(auto &field : m_tag.fields()) {
|
||||
if(!field.second.value().isEmpty() &&
|
||||
(!m_omitPreDefinedGenre || field.first == Mp4TagAtomIds::PreDefinedGenre)) {
|
||||
m_makers.emplace_back(field.second.prepareMaking());
|
||||
m_ilstSize += m_makers.back().requiredSize();
|
||||
try {
|
||||
m_maker.emplace_back(field.second.prepareMaking());
|
||||
m_ilstSize += m_maker.back().requiredSize();
|
||||
} catch(const Failure &) {
|
||||
// nothing to do here; notifications will be added anyways
|
||||
}
|
||||
m_tag.addNotifications(field.second);
|
||||
}
|
||||
}
|
||||
if(m_ilstSize != 8) {
|
||||
|
@ -307,7 +312,7 @@ void Mp4TagMaker::make(ostream &stream)
|
|||
writer.writeUInt32BE(m_ilstSize);
|
||||
writer.writeUInt32BE(Mp4AtomIds::ItunesList);
|
||||
// write fields
|
||||
for(auto &maker : m_makers) {
|
||||
for(auto &maker : m_maker) {
|
||||
maker.make(stream);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -24,7 +24,7 @@ private:
|
|||
Mp4TagMaker(Mp4Tag &tag);
|
||||
|
||||
Mp4Tag &m_tag;
|
||||
std::vector<Mp4TagFieldMaker> m_makers;
|
||||
std::vector<Mp4TagFieldMaker> m_maker;
|
||||
uint64 m_metaSize;
|
||||
uint64 m_ilstSize;
|
||||
bool m_omitPreDefinedGenre;
|
||||
|
|
|
@ -391,7 +391,7 @@ void Mp4TagField::cleared()
|
|||
|
||||
/*!
|
||||
* \brief Prepares making the specified \a field.
|
||||
* \sa See Mp4TagFieldMaker::prepareMaking() for more information.
|
||||
* \sa See Mp4TagField::prepareMaking() for more information.
|
||||
*/
|
||||
Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
|
||||
m_field(field),
|
||||
|
@ -409,6 +409,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
|
|||
m_field.addNotification(NotificationType::Critical, "No tag value assigned.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
try {
|
||||
// try to use appropriate raw data type
|
||||
m_rawDataType = m_field.appropriateRawDataType();
|
||||
|
@ -418,6 +419,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
|
|||
m_rawDataType = RawDataType::Utf8;
|
||||
m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context);
|
||||
}
|
||||
|
||||
try {
|
||||
if(!m_field.value().isEmpty()) { // there might be only mean and name info, but no data
|
||||
m_convertedData.exceptions(std::stringstream::failbit | std::stringstream::badbit);
|
||||
|
@ -480,6 +482,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
|
|||
}
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
// calculate data size
|
||||
m_dataSize = m_field.value().isEmpty()
|
||||
? 0 : (m_convertedData.tellp() ? static_cast<size_t>(m_convertedData.tellp()) : m_field.value().dataSize());
|
||||
|
@ -491,7 +494,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
|
|||
|
||||
/*!
|
||||
* \brief Saves the field (specified when constructing the object) to the
|
||||
* specified \a stream. *
|
||||
* specified \a stream.
|
||||
* \throws Throws std::ios_base::failure when an IO error occurs.
|
||||
* \throws Throws Assumes the data is already validated and thus does NOT
|
||||
* throw Media::Failure or a derived exception.
|
||||
|
|
Loading…
Reference in New Issue