2015-09-06 19:57:33 +02:00
|
|
|
#include "./id3v2tag.h"
|
|
|
|
#include "./id3v2frameids.h"
|
2015-09-06 15:42:18 +02:00
|
|
|
|
2018-03-05 17:49:29 +01:00
|
|
|
#include "../diagnostics.h"
|
2018-03-07 01:17:50 +01:00
|
|
|
#include "../exceptions.h"
|
2015-04-22 19:22:01 +02:00
|
|
|
|
2017-01-27 18:59:22 +01:00
|
|
|
#include <c++utilities/conversion/stringbuilder.h>
|
2018-03-07 01:17:50 +01:00
|
|
|
#include <c++utilities/conversion/stringconversion.h>
|
2015-04-22 19:22:01 +02:00
|
|
|
|
2018-06-02 19:27:29 +02:00
|
|
|
#include <iostream>
|
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
using namespace std;
|
2019-06-10 22:49:11 +02:00
|
|
|
using namespace CppUtilities;
|
2015-04-22 19:22:01 +02:00
|
|
|
|
2018-03-06 23:09:15 +01:00
|
|
|
namespace TagParser {
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
/*!
|
2018-06-03 20:38:32 +02:00
|
|
|
* \class TagParser::Id3v2Tag
|
|
|
|
* \brief Implementation of TagParser::Tag for ID3v2 tags.
|
2015-04-22 19:22:01 +02:00
|
|
|
*/
|
|
|
|
|
2018-07-13 12:25:00 +02:00
|
|
|
/*!
|
|
|
|
* \brief Allows multiple values for some fields.
|
|
|
|
* \remarks The standard defines no general rule applicable to all fields.
|
|
|
|
*/
|
|
|
|
bool Id3v2Tag::supportsMultipleValues(KnownField field) const
|
|
|
|
{
|
|
|
|
switch (field) {
|
|
|
|
case KnownField::Album:
|
|
|
|
case KnownField::Artist:
|
|
|
|
case KnownField::RecordDate:
|
2020-04-22 23:54:10 +02:00
|
|
|
case KnownField::ReleaseDate:
|
2018-07-13 12:25:00 +02:00
|
|
|
case KnownField::Title:
|
|
|
|
case KnownField::Genre:
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
case KnownField::DiskPosition:
|
|
|
|
case KnownField::Encoder:
|
|
|
|
case KnownField::Bpm:
|
|
|
|
case KnownField::Lyricist:
|
|
|
|
case KnownField::Length:
|
|
|
|
case KnownField::Language:
|
|
|
|
case KnownField::EncoderSettings:
|
|
|
|
case KnownField::Grouping:
|
|
|
|
case KnownField::RecordLabel:
|
|
|
|
case KnownField::Composer:
|
2019-01-01 23:38:39 +01:00
|
|
|
case KnownField::AlbumArtist:
|
2018-07-13 12:25:00 +02:00
|
|
|
return m_majorVersion > 3;
|
|
|
|
case KnownField::Rating:
|
|
|
|
case KnownField::Comment:
|
|
|
|
case KnownField::Cover:
|
|
|
|
case KnownField::Lyrics:
|
|
|
|
case KnownField::SynchronizedLyrics:
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-01 22:36:08 +02:00
|
|
|
void Id3v2Tag::ensureTextValuesAreProperlyEncoded()
|
|
|
|
{
|
|
|
|
const auto encoding = proposedTextEncoding();
|
|
|
|
for (auto &field : fields()) {
|
|
|
|
auto &value = field.second.value();
|
|
|
|
value.convertDataEncoding(encoding);
|
|
|
|
value.convertDescriptionEncoding(encoding);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-11 15:53:23 +02:00
|
|
|
/*!
|
2021-01-30 19:12:04 +01:00
|
|
|
* \brief Adds additional values as well.
|
2018-07-11 15:53:23 +02:00
|
|
|
*/
|
2021-01-30 19:12:04 +01:00
|
|
|
void Id3v2Tag::internallyGetValuesFromField(const Id3v2Tag::FieldType &field, std::vector<const TagValue *> &values) const
|
2018-07-11 15:53:23 +02:00
|
|
|
{
|
2021-01-30 19:12:04 +01:00
|
|
|
if (!field.value().isEmpty()) {
|
|
|
|
values.emplace_back(&field.value());
|
|
|
|
}
|
|
|
|
for (const auto &value : field.additionalValues()) {
|
|
|
|
if (!value.isEmpty()) {
|
|
|
|
values.emplace_back(&value);
|
2018-07-11 15:53:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Uses default implementation for non-text frames and applies special handling to text frames.
|
|
|
|
*
|
2020-12-16 17:49:02 +01:00
|
|
|
* - Ensure text frames are unique.
|
2018-07-11 15:53:23 +02:00
|
|
|
* - Allow to store multiple values inside the same text frame.
|
|
|
|
*/
|
|
|
|
bool Id3v2Tag::internallySetValues(const IdentifierType &id, const std::vector<TagValue> &values)
|
|
|
|
{
|
|
|
|
// use default implementation for non-text frames
|
|
|
|
if (!Id3v2FrameIds::isTextFrame(id)) {
|
|
|
|
return CRTPBase::internallySetValues(id, values);
|
|
|
|
}
|
|
|
|
|
|
|
|
// find existing text frame
|
|
|
|
auto range = fields().equal_range(id);
|
|
|
|
auto frameIterator = range.first;
|
|
|
|
|
|
|
|
// use existing frame or insert new text frame
|
2018-07-12 12:33:54 +02:00
|
|
|
auto valuesIterator = values.cbegin();
|
2018-07-11 15:53:23 +02:00
|
|
|
if (frameIterator != range.second) {
|
|
|
|
++range.first;
|
2018-07-12 12:33:54 +02:00
|
|
|
// add primary value to existing frame
|
|
|
|
if (valuesIterator != values.cend()) {
|
|
|
|
frameIterator->second.setValue(*valuesIterator);
|
|
|
|
++valuesIterator;
|
|
|
|
} else {
|
|
|
|
frameIterator->second.value().clearDataAndMetadata();
|
|
|
|
}
|
2018-07-11 15:53:23 +02:00
|
|
|
} else {
|
2018-07-12 12:33:54 +02:00
|
|
|
// skip if there is no existing frame but also no values to be assigned
|
|
|
|
if (valuesIterator == values.cend()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// add primary value to new frame
|
|
|
|
frameIterator = fields().insert(make_pair(id, Id3v2Frame(id, *valuesIterator)));
|
2018-07-11 15:53:23 +02:00
|
|
|
++valuesIterator;
|
|
|
|
}
|
2018-07-12 12:33:54 +02:00
|
|
|
|
2018-07-11 15:53:23 +02:00
|
|
|
// add additional values to frame
|
2018-07-12 12:33:54 +02:00
|
|
|
frameIterator->second.additionalValues() = vector<TagValue>(valuesIterator, values.cend());
|
2018-07-11 15:53:23 +02:00
|
|
|
|
|
|
|
// remove remaining existing values (there are more existing values than specified ones)
|
|
|
|
for (; range.first != range.second; ++range.first) {
|
|
|
|
range.first->second.setValue(TagValue());
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-03-07 17:16:17 +01:00
|
|
|
Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
using namespace Id3v2FrameIds;
|
2018-03-07 01:17:50 +01:00
|
|
|
if (m_majorVersion >= 3) {
|
|
|
|
switch (field) {
|
|
|
|
case KnownField::Album:
|
|
|
|
return lAlbum;
|
|
|
|
case KnownField::Artist:
|
|
|
|
return lArtist;
|
|
|
|
case KnownField::Comment:
|
|
|
|
return lComment;
|
|
|
|
case KnownField::RecordDate:
|
2020-04-22 23:54:10 +02:00
|
|
|
return lRecordingTime; // (de)serializer takes to convert to/from lYear/lRecordingDates/lDate/lTime
|
|
|
|
case KnownField::ReleaseDate:
|
|
|
|
return lReleaseTime;
|
2018-03-07 01:17:50 +01:00
|
|
|
case KnownField::Title:
|
|
|
|
return lTitle;
|
|
|
|
case KnownField::Genre:
|
|
|
|
return lGenre;
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
return lTrackPosition;
|
|
|
|
case KnownField::DiskPosition:
|
|
|
|
return lDiskPosition;
|
|
|
|
case KnownField::Encoder:
|
|
|
|
return lEncoder;
|
|
|
|
case KnownField::Bpm:
|
|
|
|
return lBpm;
|
|
|
|
case KnownField::Cover:
|
|
|
|
return lCover;
|
|
|
|
case KnownField::Lyricist:
|
|
|
|
return lWriter;
|
|
|
|
case KnownField::Length:
|
|
|
|
return lLength;
|
|
|
|
case KnownField::Language:
|
|
|
|
return lLanguage;
|
|
|
|
case KnownField::EncoderSettings:
|
|
|
|
return lEncoderSettings;
|
|
|
|
case KnownField::Lyrics:
|
|
|
|
return lUnsynchronizedLyrics;
|
|
|
|
case KnownField::SynchronizedLyrics:
|
|
|
|
return lSynchronizedLyrics;
|
|
|
|
case KnownField::Grouping:
|
2019-01-01 23:38:39 +01:00
|
|
|
return lContentGroupDescription;
|
2018-03-07 01:17:50 +01:00
|
|
|
case KnownField::RecordLabel:
|
|
|
|
return lRecordLabel;
|
|
|
|
case KnownField::Composer:
|
|
|
|
return lComposer;
|
|
|
|
case KnownField::Rating:
|
|
|
|
return lRating;
|
2019-01-01 23:38:39 +01:00
|
|
|
case KnownField::AlbumArtist:
|
2019-06-16 17:45:49 +02:00
|
|
|
return lAlbumArtist;
|
2018-03-07 01:17:50 +01:00
|
|
|
default:;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
} else {
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (field) {
|
|
|
|
case KnownField::Album:
|
|
|
|
return sAlbum;
|
|
|
|
case KnownField::Artist:
|
|
|
|
return sArtist;
|
|
|
|
case KnownField::Comment:
|
|
|
|
return sComment;
|
|
|
|
case KnownField::RecordDate:
|
2020-04-22 23:54:10 +02:00
|
|
|
return lRecordingTime; // (de)serializer takes to convert to/from sYear/sRecordingDates/sDate/sTime
|
2018-03-07 01:17:50 +01:00
|
|
|
case KnownField::Title:
|
|
|
|
return sTitle;
|
|
|
|
case KnownField::Genre:
|
|
|
|
return sGenre;
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
return sTrackPosition;
|
|
|
|
case KnownField::DiskPosition:
|
2018-06-02 23:08:14 +02:00
|
|
|
return sDiskPosition;
|
2018-03-07 01:17:50 +01:00
|
|
|
case KnownField::Encoder:
|
|
|
|
return sEncoder;
|
|
|
|
case KnownField::Bpm:
|
|
|
|
return sBpm;
|
|
|
|
case KnownField::Cover:
|
|
|
|
return sCover;
|
|
|
|
case KnownField::Lyricist:
|
|
|
|
return sWriter;
|
|
|
|
case KnownField::Length:
|
|
|
|
return sLength;
|
|
|
|
case KnownField::Language:
|
|
|
|
return sLanguage;
|
|
|
|
case KnownField::EncoderSettings:
|
|
|
|
return sEncoderSettings;
|
|
|
|
case KnownField::Lyrics:
|
|
|
|
return sUnsynchronizedLyrics;
|
|
|
|
case KnownField::SynchronizedLyrics:
|
|
|
|
return sSynchronizedLyrics;
|
|
|
|
case KnownField::Grouping:
|
2019-01-01 23:38:39 +01:00
|
|
|
return sContentGroupDescription;
|
2018-03-07 01:17:50 +01:00
|
|
|
case KnownField::RecordLabel:
|
|
|
|
return sRecordLabel;
|
|
|
|
case KnownField::Composer:
|
|
|
|
return sComposer;
|
|
|
|
case KnownField::Rating:
|
|
|
|
return sRating;
|
2019-01-01 23:38:39 +01:00
|
|
|
case KnownField::AlbumArtist:
|
2019-06-16 17:45:49 +02:00
|
|
|
return sAlbumArtist;
|
2018-03-07 01:17:50 +01:00
|
|
|
default:;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-03-07 17:16:17 +01:00
|
|
|
KnownField Id3v2Tag::internallyGetKnownField(const IdentifierType &id) const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
using namespace Id3v2FrameIds;
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (id) {
|
|
|
|
case lAlbum:
|
|
|
|
return KnownField::Album;
|
|
|
|
case lArtist:
|
|
|
|
return KnownField::Artist;
|
|
|
|
case lComment:
|
|
|
|
return KnownField::Comment;
|
2020-04-22 19:14:34 +02:00
|
|
|
case lRecordingTime:
|
2020-04-22 23:54:10 +02:00
|
|
|
case lYear:
|
2018-03-07 01:17:50 +01:00
|
|
|
return KnownField::RecordDate;
|
|
|
|
case lTitle:
|
|
|
|
return KnownField::Title;
|
|
|
|
case lGenre:
|
|
|
|
return KnownField::Genre;
|
|
|
|
case lTrackPosition:
|
|
|
|
return KnownField::TrackPosition;
|
|
|
|
case lDiskPosition:
|
|
|
|
return KnownField::DiskPosition;
|
|
|
|
case lEncoder:
|
|
|
|
return KnownField::Encoder;
|
|
|
|
case lBpm:
|
|
|
|
return KnownField::Bpm;
|
|
|
|
case lCover:
|
|
|
|
return KnownField::Cover;
|
|
|
|
case lWriter:
|
|
|
|
return KnownField::Lyricist;
|
|
|
|
case lLanguage:
|
|
|
|
return KnownField::Language;
|
|
|
|
case lLength:
|
|
|
|
return KnownField::Length;
|
|
|
|
case lEncoderSettings:
|
|
|
|
return KnownField::EncoderSettings;
|
|
|
|
case lUnsynchronizedLyrics:
|
|
|
|
return KnownField::Lyrics;
|
|
|
|
case lSynchronizedLyrics:
|
|
|
|
return KnownField::SynchronizedLyrics;
|
2019-06-16 17:45:49 +02:00
|
|
|
case lAlbumArtist:
|
2019-01-01 23:38:39 +01:00
|
|
|
return KnownField::AlbumArtist;
|
|
|
|
case lContentGroupDescription:
|
2018-03-07 01:17:50 +01:00
|
|
|
return KnownField::Grouping;
|
|
|
|
case lRecordLabel:
|
|
|
|
return KnownField::RecordLabel;
|
|
|
|
case sAlbum:
|
|
|
|
return KnownField::Album;
|
|
|
|
case sArtist:
|
|
|
|
return KnownField::Artist;
|
|
|
|
case sComment:
|
|
|
|
return KnownField::Comment;
|
|
|
|
case sYear:
|
|
|
|
return KnownField::RecordDate;
|
|
|
|
case sTitle:
|
|
|
|
return KnownField::Title;
|
|
|
|
case sGenre:
|
|
|
|
return KnownField::Genre;
|
|
|
|
case sTrackPosition:
|
|
|
|
return KnownField::TrackPosition;
|
|
|
|
case sEncoder:
|
|
|
|
return KnownField::Encoder;
|
|
|
|
case sBpm:
|
|
|
|
return KnownField::Bpm;
|
|
|
|
case sCover:
|
|
|
|
return KnownField::Cover;
|
|
|
|
case sWriter:
|
|
|
|
return KnownField::Lyricist;
|
|
|
|
case sLanguage:
|
|
|
|
return KnownField::Language;
|
|
|
|
case sLength:
|
|
|
|
return KnownField::Length;
|
|
|
|
case sEncoderSettings:
|
|
|
|
return KnownField::EncoderSettings;
|
|
|
|
case sUnsynchronizedLyrics:
|
|
|
|
return KnownField::Lyrics;
|
|
|
|
case sSynchronizedLyrics:
|
|
|
|
return KnownField::SynchronizedLyrics;
|
2019-06-16 17:45:49 +02:00
|
|
|
case sAlbumArtist:
|
2018-03-07 01:17:50 +01:00
|
|
|
return KnownField::Grouping;
|
|
|
|
case sRecordLabel:
|
|
|
|
return KnownField::RecordLabel;
|
|
|
|
default:
|
|
|
|
return KnownField::Invalid;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-13 19:06:42 +01:00
|
|
|
TagDataType Id3v2Tag::internallyGetProposedDataType(const std::uint32_t &id) const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
using namespace Id3v2FrameIds;
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (id) {
|
|
|
|
case lLength:
|
|
|
|
case sLength:
|
2015-04-22 19:22:01 +02:00
|
|
|
return TagDataType::TimeSpan;
|
2018-03-07 01:17:50 +01:00
|
|
|
case lBpm:
|
|
|
|
case sBpm:
|
2020-04-22 23:54:10 +02:00
|
|
|
case lYear:
|
|
|
|
case sYear:
|
2015-04-22 19:22:01 +02:00
|
|
|
return TagDataType::Integer;
|
2018-03-07 01:17:50 +01:00
|
|
|
case lTrackPosition:
|
|
|
|
case sTrackPosition:
|
2015-04-22 19:22:01 +02:00
|
|
|
case lDiskPosition:
|
|
|
|
return TagDataType::PositionInSet;
|
2018-03-07 01:17:50 +01:00
|
|
|
case lCover:
|
|
|
|
case sCover:
|
2015-04-22 19:22:01 +02:00
|
|
|
return TagDataType::Picture;
|
|
|
|
default:
|
2018-03-07 01:17:50 +01:00
|
|
|
if (Id3v2FrameIds::isTextFrame(id)) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return TagDataType::Text;
|
|
|
|
} else {
|
|
|
|
return TagDataType::Undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-22 23:54:10 +02:00
|
|
|
/*!
|
|
|
|
* \brief Converts the lYear/lRecordingDates/lDate/lTime/sYear/sRecordingDates/sDate/sTime fields found in v2.3.0 to lRecordingTime.
|
|
|
|
* \remarks
|
|
|
|
* - Do not get rid of the "old" fields after the conversion so the raw fields can still be checked.
|
|
|
|
* - The make function converts back if necassary and deletes unsupported fields.
|
|
|
|
*/
|
|
|
|
void Id3v2Tag::convertOldRecordDateFields(const std::string &diagContext, Diagnostics &diag)
|
|
|
|
{
|
|
|
|
// skip if it is a v2.4.0 tag and lRecordingTime is present
|
|
|
|
if (majorVersion() >= 4 && fields().find(Id3v2FrameIds::lRecordingTime) != fields().cend()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse values of lYear/lRecordingDates/lDate/lTime/sYear/sRecordingDates/sDate/sTime fields
|
2021-05-09 12:15:57 +02:00
|
|
|
bool hasAnyValue = false;
|
2020-04-22 23:54:10 +02:00
|
|
|
int year = 1, month = 1, day = 1, hour = 0, minute = 0;
|
|
|
|
if (const auto &v = value(Id3v2FrameIds::lYear)) {
|
2021-05-09 12:15:57 +02:00
|
|
|
hasAnyValue = true;
|
2020-04-22 23:54:10 +02:00
|
|
|
try {
|
|
|
|
year = v.toInteger();
|
|
|
|
} catch (const ConversionException &e) {
|
|
|
|
diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse year from \"TYER\" frame: ", e.what()), diagContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (const auto &v = value(Id3v2FrameIds::lDate)) {
|
2021-05-09 12:15:57 +02:00
|
|
|
hasAnyValue = true;
|
2020-04-22 23:54:10 +02:00
|
|
|
try {
|
|
|
|
auto str = v.toString();
|
|
|
|
if (str.size() != 4) {
|
|
|
|
throw ConversionException("format is not DDMM");
|
|
|
|
}
|
|
|
|
day = stringToNumber<unsigned short>(std::string_view(str.data() + 0, 2));
|
|
|
|
month = stringToNumber<unsigned short>(std::string_view(str.data() + 2, 2));
|
|
|
|
} catch (const ConversionException &e) {
|
|
|
|
diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse month and day from \"TDAT\" frame: ", e.what()), diagContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (const auto &v = value(Id3v2FrameIds::lTime)) {
|
2021-05-09 12:15:57 +02:00
|
|
|
hasAnyValue = true;
|
2020-04-22 23:54:10 +02:00
|
|
|
try {
|
|
|
|
auto str = v.toString();
|
|
|
|
if (str.size() != 4) {
|
|
|
|
throw ConversionException("format is not HHMM");
|
|
|
|
}
|
|
|
|
hour = stringToNumber<unsigned short>(std::string_view(str.data() + 0, 2));
|
|
|
|
minute = stringToNumber<unsigned short>(std::string_view(str.data() + 2, 2));
|
|
|
|
} catch (const ConversionException &e) {
|
|
|
|
diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse hour and minute from \"TIME\" frame: ", +e.what()), diagContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set the field values as DateTime
|
2021-05-09 12:15:57 +02:00
|
|
|
if (!hasAnyValue) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-22 23:54:10 +02:00
|
|
|
try {
|
2020-12-05 20:48:57 +01:00
|
|
|
setValue(Id3v2FrameIds::lRecordingTime, TagValue(DateTime::fromDateAndTime(year, month, day, hour, minute)));
|
2020-04-22 23:54:10 +02:00
|
|
|
} catch (const ConversionException &e) {
|
|
|
|
try {
|
|
|
|
// try to set at least the year
|
2020-12-05 20:48:57 +01:00
|
|
|
setValue(Id3v2FrameIds::lRecordingTime, TagValue(DateTime::fromDate(year)));
|
2020-04-22 23:54:10 +02:00
|
|
|
diag.emplace_back(DiagLevel::Critical,
|
|
|
|
argsToString(
|
|
|
|
"Unable to parse the full date of the recording. Only the 'Year' frame could be parsed; related frames failed: ", e.what()),
|
|
|
|
diagContext);
|
|
|
|
} catch (const ConversionException &) {
|
|
|
|
}
|
|
|
|
diag.emplace_back(
|
|
|
|
DiagLevel::Critical, argsToString("Unable to parse a valid date from the 'Year' frame and related frames: ", e.what()), diagContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
/*!
|
|
|
|
* \brief Parses tag information from the specified \a stream.
|
|
|
|
*
|
|
|
|
* \throws Throws std::ios_base::failure when an IO error occurs.
|
2018-06-03 20:38:32 +02:00
|
|
|
* \throws Throws TagParser::Failure or a derived exception when a parsing
|
2015-04-22 19:22:01 +02:00
|
|
|
* error occurs.
|
|
|
|
*/
|
2019-03-13 19:06:42 +01:00
|
|
|
void Id3v2Tag::parse(istream &stream, const std::uint64_t maximalSize, Diagnostics &diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
// prepare parsing
|
2017-02-06 18:52:21 +01:00
|
|
|
static const string context("parsing ID3v2 tag");
|
2015-04-22 19:22:01 +02:00
|
|
|
BinaryReader reader(&stream);
|
2019-03-13 19:06:42 +01:00
|
|
|
const auto startOffset = static_cast<std::uint64_t>(stream.tellg());
|
2015-12-22 17:01:25 +01:00
|
|
|
|
|
|
|
// check whether the header is truncated
|
2018-03-07 01:17:50 +01:00
|
|
|
if (maximalSize && maximalSize < 10) {
|
2018-03-05 17:49:29 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "ID3v2 header is truncated (at least 10 bytes expected).", context);
|
2015-12-22 17:01:25 +01:00
|
|
|
throw TruncatedDataException();
|
|
|
|
}
|
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
// read signature: ID3
|
2018-03-07 01:17:50 +01:00
|
|
|
if (reader.readUInt24BE() != 0x494433u) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Signature is invalid.", context);
|
|
|
|
throw InvalidDataException();
|
|
|
|
}
|
|
|
|
// read header data
|
2020-04-22 23:54:10 +02:00
|
|
|
const std::uint8_t majorVersion = reader.readByte();
|
|
|
|
const std::uint8_t revisionVersion = reader.readByte();
|
2018-03-07 01:11:42 +01:00
|
|
|
setVersion(majorVersion, revisionVersion);
|
|
|
|
m_flags = reader.readByte();
|
|
|
|
m_sizeExcludingHeader = reader.readSynchsafeUInt32BE();
|
|
|
|
m_size = 10 + m_sizeExcludingHeader;
|
2018-03-07 01:17:50 +01:00
|
|
|
if (m_sizeExcludingHeader == 0) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Warning, "ID3v2 tag seems to be empty.", context);
|
|
|
|
return;
|
|
|
|
}
|
2015-12-22 17:01:25 +01:00
|
|
|
|
2018-03-07 01:11:42 +01:00
|
|
|
// check if the version
|
2018-03-07 01:17:50 +01:00
|
|
|
if (!isVersionSupported()) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "The ID3v2 tag couldn't be parsed, because its version is not supported.", context);
|
|
|
|
throw VersionNotSupportedException();
|
|
|
|
}
|
2015-12-22 17:01:25 +01:00
|
|
|
|
2018-03-07 01:11:42 +01:00
|
|
|
// read extended header (if present)
|
2018-03-07 01:17:50 +01:00
|
|
|
if (hasExtendedHeader()) {
|
|
|
|
if (maximalSize && maximalSize < 14) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Extended header denoted but not present.", context);
|
|
|
|
throw TruncatedDataException();
|
|
|
|
}
|
|
|
|
m_extendedHeaderSize = reader.readSynchsafeUInt32BE();
|
2018-03-07 01:17:50 +01:00
|
|
|
if (m_extendedHeaderSize < 6 || m_extendedHeaderSize > m_sizeExcludingHeader || (maximalSize && maximalSize < (10 + m_extendedHeaderSize))) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Extended header is invalid/truncated.", context);
|
|
|
|
throw TruncatedDataException();
|
|
|
|
}
|
|
|
|
stream.seekg(m_extendedHeaderSize - 4, ios_base::cur);
|
|
|
|
}
|
2015-12-22 17:01:25 +01:00
|
|
|
|
2018-03-07 01:11:42 +01:00
|
|
|
// how many bytes remain for frames and padding?
|
2019-03-13 19:06:42 +01:00
|
|
|
std::uint32_t bytesRemaining = m_sizeExcludingHeader - m_extendedHeaderSize;
|
2018-03-07 01:17:50 +01:00
|
|
|
if (maximalSize && bytesRemaining > maximalSize) {
|
2019-03-13 19:06:42 +01:00
|
|
|
bytesRemaining = static_cast<std::uint32_t>(maximalSize);
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Frames are truncated.", context);
|
|
|
|
}
|
2015-12-22 17:01:25 +01:00
|
|
|
|
2018-03-07 01:11:42 +01:00
|
|
|
// read frames
|
2019-03-13 19:06:42 +01:00
|
|
|
auto pos = static_cast<std::uint64_t>(stream.tellg());
|
2018-03-07 01:17:50 +01:00
|
|
|
while (bytesRemaining) {
|
2018-03-07 01:11:42 +01:00
|
|
|
// seek to next frame
|
2018-05-31 00:25:32 +02:00
|
|
|
stream.seekg(static_cast<streamoff>(pos));
|
2018-03-07 01:11:42 +01:00
|
|
|
// parse frame
|
2018-03-11 22:27:12 +01:00
|
|
|
Id3v2Frame frame;
|
2018-03-07 01:11:42 +01:00
|
|
|
try {
|
|
|
|
frame.parse(reader, majorVersion, bytesRemaining, diag);
|
2018-03-11 22:27:12 +01:00
|
|
|
if (Id3v2FrameIds::isTextFrame(frame.id()) && fields().count(frame.id()) == 1) {
|
2018-07-11 11:08:48 +02:00
|
|
|
diag.emplace_back(DiagLevel::Warning, "The text frame " % frame.idToString() + " exists more than once.", context);
|
2015-12-22 17:01:25 +01:00
|
|
|
}
|
2018-03-11 22:27:12 +01:00
|
|
|
fields().emplace(frame.id(), move(frame));
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const NoDataFoundException &) {
|
|
|
|
if (frame.hasPaddingReached()) {
|
2018-03-07 01:11:42 +01:00
|
|
|
m_paddingSize = startOffset + m_size - pos;
|
|
|
|
break;
|
2015-12-22 17:01:25 +01:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const Failure &) {
|
2018-03-07 01:11:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// calculate next frame offset
|
2018-03-07 01:17:50 +01:00
|
|
|
if (frame.totalSize() <= bytesRemaining) {
|
2018-03-07 01:11:42 +01:00
|
|
|
pos += frame.totalSize();
|
|
|
|
bytesRemaining -= frame.totalSize();
|
|
|
|
} else {
|
|
|
|
pos += bytesRemaining;
|
|
|
|
bytesRemaining = 0;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
2018-03-07 01:11:42 +01:00
|
|
|
}
|
|
|
|
|
2021-05-13 16:07:06 +02:00
|
|
|
if (m_handlingFlags & Id3v2HandlingFlags::ConvertRecordDateFields) {
|
|
|
|
convertOldRecordDateFields(context, diag);
|
|
|
|
}
|
2020-04-22 23:54:10 +02:00
|
|
|
|
2018-03-07 01:11:42 +01:00
|
|
|
// check for extended header
|
2018-03-07 01:17:50 +01:00
|
|
|
if (!hasFooter()) {
|
2018-03-07 01:11:42 +01:00
|
|
|
return;
|
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (maximalSize && m_size + 10 < maximalSize) {
|
2018-03-07 01:11:42 +01:00
|
|
|
// the footer does not provide additional information, just check the signature
|
2018-05-31 00:25:32 +02:00
|
|
|
stream.seekg(static_cast<streamoff>(startOffset + (m_size += 10)));
|
2018-03-07 01:17:50 +01:00
|
|
|
if (reader.readUInt24LE() != 0x494433u) {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Footer signature is invalid.", context);
|
|
|
|
}
|
|
|
|
// skip remaining footer
|
|
|
|
stream.seekg(7, ios_base::cur);
|
2015-04-22 19:22:01 +02:00
|
|
|
} else {
|
2018-03-07 01:11:42 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "Footer denoted but not present.", context);
|
|
|
|
throw TruncatedDataException();
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-22 23:54:35 +01:00
|
|
|
/*!
|
|
|
|
* \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.
|
2018-06-03 20:38:32 +02:00
|
|
|
* \throws Throws TagParser::Failure or a derived exception when a making error occurs.
|
2015-12-22 23:54:35 +01:00
|
|
|
*
|
|
|
|
* This method might be useful when it is necessary to know the size of the tag before making it.
|
|
|
|
* \sa make()
|
|
|
|
*/
|
2018-03-05 17:49:29 +01:00
|
|
|
Id3v2TagMaker Id3v2Tag::prepareMaking(Diagnostics &diag)
|
2015-12-22 23:54:35 +01:00
|
|
|
{
|
2018-03-05 17:49:29 +01:00
|
|
|
return Id3v2TagMaker(*this, diag);
|
2015-12-22 23:54:35 +01:00
|
|
|
}
|
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
/*!
|
|
|
|
* \brief Writes tag information to the specified \a stream.
|
|
|
|
*
|
|
|
|
* \throws Throws std::ios_base::failure when an IO error occurs.
|
2018-06-03 20:38:32 +02:00
|
|
|
* \throws Throws TagParser::Failure or a derived exception when a making
|
2015-04-22 19:22:01 +02:00
|
|
|
* error occurs.
|
|
|
|
*/
|
2019-03-13 19:06:42 +01:00
|
|
|
void Id3v2Tag::make(ostream &stream, std::uint32_t padding, Diagnostics &diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2018-03-05 17:49:29 +01:00
|
|
|
prepareMaking(diag).make(stream, padding, diag);
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Sets the version to the specified \a majorVersion and
|
|
|
|
* the specified \a revisionVersion.
|
|
|
|
*/
|
2019-03-13 19:06:42 +01:00
|
|
|
void Id3v2Tag::setVersion(std::uint8_t majorVersion, std::uint8_t revisionVersion)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
m_majorVersion = majorVersion;
|
|
|
|
m_revisionVersion = revisionVersion;
|
2017-01-30 00:42:35 +01:00
|
|
|
m_version = argsToString('2', '.', majorVersion, '.', revisionVersion);
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
2018-06-03 20:38:32 +02:00
|
|
|
* \class TagParser::FrameComparer
|
2015-04-22 19:22:01 +02:00
|
|
|
* \brief Defines the order which is used to store ID3v2 frames.
|
|
|
|
*
|
|
|
|
* The order is: unique file id, title, other text frames, other frames, cover
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Returns true if \a lhs goes before \a rhs; otherwise returns false.
|
2018-07-13 12:37:21 +02:00
|
|
|
* \remarks Long and short IDs are treated equal if the short ID can be converted to
|
|
|
|
* the corresponding long ID. Otherwise short IDs go before long IDs.
|
2015-04-22 19:22:01 +02:00
|
|
|
*/
|
2019-03-13 19:06:42 +01:00
|
|
|
bool FrameComparer::operator()(std::uint32_t lhs, std::uint32_t rhs) const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2018-03-07 01:17:50 +01:00
|
|
|
if (lhs == rhs) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return false;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-05-31 00:25:06 +02:00
|
|
|
|
2016-12-27 23:57:21 +01:00
|
|
|
const bool lhsLong = Id3v2FrameIds::isLongId(lhs);
|
|
|
|
const bool rhsLong = Id3v2FrameIds::isLongId(rhs);
|
2018-05-31 00:25:06 +02:00
|
|
|
if (lhsLong != rhsLong) {
|
|
|
|
if (!lhsLong) {
|
|
|
|
lhs = Id3v2FrameIds::convertToLongId(lhs);
|
2018-07-13 12:37:21 +02:00
|
|
|
if (!lhs) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-05-31 00:25:06 +02:00
|
|
|
} else if (!rhsLong) {
|
|
|
|
rhs = Id3v2FrameIds::convertToLongId(rhs);
|
2018-07-13 12:37:21 +02:00
|
|
|
if (!rhs) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-05-31 00:25:06 +02:00
|
|
|
}
|
2016-12-27 23:57:21 +01:00
|
|
|
}
|
|
|
|
|
2018-03-07 01:17:50 +01:00
|
|
|
if (lhs == Id3v2FrameIds::lUniqueFileId || lhs == Id3v2FrameIds::sUniqueFileId) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return true;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (rhs == Id3v2FrameIds::lUniqueFileId || rhs == Id3v2FrameIds::sUniqueFileId) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return false;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (lhs == Id3v2FrameIds::lTitle || lhs == Id3v2FrameIds::sTitle) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return true;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (rhs == Id3v2FrameIds::lTitle || rhs == Id3v2FrameIds::sTitle) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return false;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-05-31 00:25:32 +02:00
|
|
|
|
|
|
|
const bool lhstextfield = Id3v2FrameIds::isTextFrame(lhs);
|
|
|
|
const bool rhstextfield = Id3v2FrameIds::isTextFrame(rhs);
|
2018-03-07 01:17:50 +01:00
|
|
|
if (lhstextfield && !rhstextfield) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return true;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (!lhstextfield && rhstextfield) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return false;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-05-31 00:25:32 +02:00
|
|
|
|
2018-03-07 01:17:50 +01:00
|
|
|
if (lhs == Id3v2FrameIds::lCover || lhs == Id3v2FrameIds::sCover) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return false;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2018-03-07 01:17:50 +01:00
|
|
|
if (rhs == Id3v2FrameIds::lCover || rhs == Id3v2FrameIds::sCover) {
|
2015-04-22 19:22:01 +02:00
|
|
|
return true;
|
2015-07-27 23:10:35 +02:00
|
|
|
}
|
2015-04-22 19:22:01 +02:00
|
|
|
return lhs < rhs;
|
|
|
|
}
|
|
|
|
|
2016-08-04 00:16:19 +02:00
|
|
|
/*!
|
2018-06-03 20:38:32 +02:00
|
|
|
* \class TagParser::Id3v2TagMaker
|
2016-08-04 00:16:19 +02:00
|
|
|
* \brief The Id3v2TagMaker class helps writing ID3v2 tags.
|
|
|
|
*
|
|
|
|
* An instance can be obtained using the Id3v2Tag::prepareMaking() method.
|
|
|
|
*/
|
|
|
|
|
2020-04-22 23:54:10 +02:00
|
|
|
/*!
|
|
|
|
* \brief Removes all old (major version <= 3) record date related fields.
|
|
|
|
*/
|
|
|
|
void Id3v2Tag::removeOldRecordDateRelatedFields()
|
|
|
|
{
|
|
|
|
for (auto field : { Id3v2FrameIds::lYear, Id3v2FrameIds::lRecordingDates, Id3v2FrameIds::lDate, Id3v2FrameIds::lTime }) {
|
|
|
|
fields().erase(field);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Prepare the fields to save the record data according to the ID3v2 version.
|
|
|
|
*/
|
|
|
|
void Id3v2Tag::prepareRecordDataForMaking(const std::string &diagContext, Diagnostics &diag)
|
|
|
|
{
|
|
|
|
// get rid of lYear/lRecordingDates/lDate/lTime/sYear/sRecordingDates/sDate/sTime if writing v2.4.0 or newer
|
|
|
|
// note: If the tag was initially v2.3.0 or older the "old" fields have already been converted to lRecordingTime when
|
|
|
|
// parsing and the generic accessors propose using lRecordingTime in any case.
|
|
|
|
if (majorVersion() >= 4) {
|
|
|
|
removeOldRecordDateRelatedFields();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert lRecordingTime to old fields for v2.3.0 and older
|
|
|
|
const auto recordingTimeFieldIterator = fields().find(Id3v2FrameIds::lRecordingTime);
|
|
|
|
// -> If the auto-created lRecordingTime field (see note above) has been completely removed write the old fields as-is.
|
2021-07-02 03:00:50 +02:00
|
|
|
// This allows one to bypass this handling and set the old fields explicitly.
|
2020-04-22 23:54:10 +02:00
|
|
|
if (recordingTimeFieldIterator == fields().cend()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// -> simply remove all old fields if lRecordingTime is set to an empty value
|
|
|
|
const auto &recordingTime = recordingTimeFieldIterator->second.value();
|
|
|
|
if (recordingTime.isEmpty()) {
|
|
|
|
removeOldRecordDateRelatedFields();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// -> convert lRecordingTime (which is supposed to be an ISO string) to a DateTime
|
|
|
|
try {
|
|
|
|
const auto asDateTime = recordingTime.toDateTime();
|
|
|
|
// -> remove any existing old fields to avoid any leftovers
|
|
|
|
removeOldRecordDateRelatedFields();
|
|
|
|
// -> assign old fields from parsed DateTime
|
|
|
|
std::stringstream year, date, time;
|
|
|
|
year << std::setfill('0') << std::setw(4) << asDateTime.year();
|
|
|
|
setValue(Id3v2FrameIds::lYear, TagValue(year.str()));
|
|
|
|
date << std::setfill('0') << std::setw(2) << asDateTime.day() << std::setfill('0') << std::setw(2) << asDateTime.month();
|
|
|
|
setValue(Id3v2FrameIds::lDate, TagValue(date.str()));
|
|
|
|
time << std::setfill('0') << std::setw(2) << asDateTime.hour() << std::setfill('0') << std::setw(2) << asDateTime.minute();
|
|
|
|
setValue(Id3v2FrameIds::lTime, TagValue(time.str()));
|
|
|
|
if (asDateTime.second() || asDateTime.millisecond()) {
|
|
|
|
diag.emplace_back(DiagLevel::Warning,
|
|
|
|
"The recording time field (TRDA) has been truncated to full minutes when converting to corresponding fields for older ID3v2 "
|
|
|
|
"versions.",
|
|
|
|
diagContext);
|
|
|
|
}
|
|
|
|
} catch (const ConversionException &e) {
|
|
|
|
try {
|
|
|
|
diag.emplace_back(DiagLevel::Critical,
|
|
|
|
argsToString("Unable to convert recording time field (TRDA) with the value \"", recordingTime.toString(),
|
|
|
|
"\" to corresponding fields for older ID3v2 versions: ", e.what()),
|
|
|
|
diagContext);
|
|
|
|
} catch (const ConversionException &) {
|
|
|
|
diag.emplace_back(DiagLevel::Critical,
|
|
|
|
argsToString("Unable to convert recording time field (TRDA) to corresponding fields for older ID3v2 versions: ", e.what()),
|
|
|
|
diagContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// -> get rid of lRecordingTime
|
|
|
|
fields().erase(Id3v2FrameIds::lRecordingTime);
|
|
|
|
}
|
|
|
|
|
2015-12-22 23:54:35 +01:00
|
|
|
/*!
|
|
|
|
* \brief Prepares making the specified \a tag.
|
|
|
|
* \sa See Id3v2Tag::prepareMaking() for more information.
|
|
|
|
*/
|
2018-03-07 01:17:50 +01:00
|
|
|
Id3v2TagMaker::Id3v2TagMaker(Id3v2Tag &tag, Diagnostics &diag)
|
|
|
|
: m_tag(tag)
|
|
|
|
, m_framesSize(0)
|
2015-12-22 23:54:35 +01:00
|
|
|
{
|
2017-02-06 18:52:21 +01:00
|
|
|
static const string context("making ID3v2 tag");
|
2015-12-22 23:54:35 +01:00
|
|
|
|
|
|
|
// check if version is supported
|
|
|
|
// (the version could have been changed using setVersion())
|
2018-03-07 01:17:50 +01:00
|
|
|
if (!tag.isVersionSupported()) {
|
2018-03-05 17:49:29 +01:00
|
|
|
diag.emplace_back(DiagLevel::Critical, "The ID3v2 tag version isn't supported.", context);
|
2015-12-22 23:54:35 +01:00
|
|
|
throw VersionNotSupportedException();
|
|
|
|
}
|
|
|
|
|
2021-05-13 16:07:06 +02:00
|
|
|
if (m_tag.m_handlingFlags & Id3v2HandlingFlags::ConvertRecordDateFields) {
|
|
|
|
tag.prepareRecordDataForMaking(context, diag);
|
|
|
|
}
|
2020-04-22 23:54:10 +02:00
|
|
|
|
2015-12-22 23:54:35 +01:00
|
|
|
// prepare frames
|
|
|
|
m_maker.reserve(tag.fields().size());
|
2018-03-07 01:17:50 +01:00
|
|
|
for (auto &pair : tag.fields()) {
|
2015-12-22 23:54:35 +01:00
|
|
|
try {
|
2018-03-05 17:49:29 +01:00
|
|
|
m_maker.emplace_back(pair.second.prepareMaking(tag.majorVersion(), diag));
|
2015-12-22 23:54:35 +01:00
|
|
|
m_framesSize += m_maker.back().requiredSize();
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const Failure &) {
|
2015-12-22 23:54:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2018-06-03 20:38:32 +02:00
|
|
|
* throw TagParser::Failure or a derived exception.
|
2015-12-22 23:54:35 +01:00
|
|
|
*/
|
2019-03-13 19:06:42 +01:00
|
|
|
void Id3v2TagMaker::make(std::ostream &stream, std::uint32_t padding, Diagnostics &diag)
|
2015-12-22 23:54:35 +01:00
|
|
|
{
|
2019-06-12 20:40:45 +02:00
|
|
|
CPP_UTILITIES_UNUSED(diag)
|
2018-06-02 22:56:08 +02:00
|
|
|
|
2015-12-22 23:54:35 +01:00
|
|
|
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
|
2018-03-07 01:17:50 +01:00
|
|
|
for (auto &maker : m_maker) {
|
2015-12-22 23:54:35 +01:00
|
|
|
maker.make(writer);
|
|
|
|
}
|
|
|
|
|
|
|
|
// write padding
|
2018-03-07 01:17:50 +01:00
|
|
|
for (; padding; --padding) {
|
2015-12-22 23:54:35 +01:00
|
|
|
stream.put(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-07 01:17:50 +01:00
|
|
|
} // namespace TagParser
|