Deprecate 'Year' in favor of 'RecordDate' and 'ReleaseDate', fix handling in ID3v2

1. Convert TYER and related fields of old ID3v2 versions to the new TDRC
  field and only expose that via the generic accessors.
2. When writing an old ID3v2 tag, convert TDRC back to the old fields.
3. One can still manually unset the via 1. auto-populated TDRC to disable 2.
   and write the old fields directly. So the automatic handling does not
   reduce the flexibility of the library.
4. Deprecate 'Year'; it is replaced by the already existing 'RecordDate'
   which is now supposed to be used everywhere where 'Year' was used before
5. Introduce 'ReleaseDate' to support this field which is supported in
   ID3v2.4.0 and Matroska via the generic accessors.
6. Use ISO format when converting tag values of the type DateTime to/from
   string. This is closer to what's used in ID3v2 tags internally. (The
   library still allows the old format as fallback when parsing for
   compatibility.)
This commit is contained in:
Martchus 2020-04-22 23:54:10 +02:00
parent a7d359df81
commit d26e594777
11 changed files with 201 additions and 29 deletions

View File

@ -151,6 +151,7 @@ const TagValue &Id3v1Tag::value(KnownField field) const
return m_artist;
case KnownField::Album:
return m_album;
case KnownField::RecordDate:
case KnownField::Year:
return m_year;
case KnownField::Comment:
@ -176,6 +177,7 @@ bool Id3v1Tag::setValue(KnownField field, const TagValue &value)
case KnownField::Album:
m_album = value;
break;
case KnownField::RecordDate:
case KnownField::Year:
m_year = value;
break;
@ -249,6 +251,7 @@ bool Id3v1Tag::supportsField(KnownField field) const
case KnownField::Title:
case KnownField::Artist:
case KnownField::Album:
case KnownField::RecordDate:
case KnownField::Year:
case KnownField::Comment:
case KnownField::TrackPosition:

View File

@ -39,8 +39,6 @@ std::uint32_t convertToShortId(std::uint32_t id)
return sOriginalYear;
case lRecordingDates:
return sRecordingDates;
case lRecordingTime:
return sRecordDate;
case lDate:
return sDate;
case lTime:
@ -99,8 +97,6 @@ std::uint32_t convertToLongId(std::uint32_t id)
return lYear;
case sOriginalYear:
return lOriginalYear;
case sRecordDate:
return lRecordingTime;
case sRecordingDates:
return lRecordingDates;
case sDate:

View File

@ -51,7 +51,6 @@ enum KnownValue : std::uint32_t {
sYear = 0x545945, /**< ?TYE */
sOriginalYear = 0x544F5259, /**< TORY */
sRecordingDates = 0x545244, /**< ?TRD */
sRecordDate = 0x545243, /**< ?TRC */
sDate = 0x544441, /**< ?TDA */
sTime = 0x54494D, /**< ?TIM */
sTitle = 0x545432, /**< ?TT2 */

View File

@ -30,6 +30,7 @@ bool Id3v2Tag::supportsMultipleValues(KnownField field) const
case KnownField::Artist:
case KnownField::Year:
case KnownField::RecordDate:
case KnownField::ReleaseDate:
case KnownField::Title:
case KnownField::Genre:
case KnownField::TrackPosition:
@ -53,7 +54,6 @@ bool Id3v2Tag::supportsMultipleValues(KnownField field) const
return true;
default:
return false;
;
}
}
@ -145,10 +145,11 @@ Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const
return lArtist;
case KnownField::Comment:
return lComment;
case KnownField::Year:
return lYear;
case KnownField::RecordDate:
return lRecordingTime;
case KnownField::Year:
return lRecordingTime; // (de)serializer takes to convert to/from lYear/lRecordingDates/lDate/lTime
case KnownField::ReleaseDate:
return lReleaseTime;
case KnownField::Title:
return lTitle;
case KnownField::Genre:
@ -195,10 +196,9 @@ Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const
return sArtist;
case KnownField::Comment:
return sComment;
case KnownField::Year:
return sYear;
case KnownField::RecordDate:
return sRecordDate;
case KnownField::Year:
return lRecordingTime; // (de)serializer takes to convert to/from sYear/sRecordingDates/sDate/sTime
case KnownField::Title:
return sTitle;
case KnownField::Genre:
@ -251,9 +251,8 @@ KnownField Id3v2Tag::internallyGetKnownField(const IdentifierType &id) const
return KnownField::Artist;
case lComment:
return KnownField::Comment;
case lYear:
return KnownField::Year;
case lRecordingTime:
case lYear:
return KnownField::RecordDate;
case lTitle:
return KnownField::Title;
@ -294,8 +293,6 @@ KnownField Id3v2Tag::internallyGetKnownField(const IdentifierType &id) const
case sComment:
return KnownField::Comment;
case sYear:
return KnownField::Year;
case sRecordDate:
return KnownField::RecordDate;
case sTitle:
return KnownField::Title;
@ -339,6 +336,8 @@ TagDataType Id3v2Tag::internallyGetProposedDataType(const std::uint32_t &id) con
return TagDataType::TimeSpan;
case lBpm:
case sBpm:
case lYear:
case sYear:
return TagDataType::Integer;
case lTrackPosition:
case sTrackPosition:
@ -356,6 +355,71 @@ TagDataType Id3v2Tag::internallyGetProposedDataType(const std::uint32_t &id) con
}
}
/*!
* \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
int year = 1, month = 1, day = 1, hour = 0, minute = 0;
if (const auto &v = value(Id3v2FrameIds::lYear)) {
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)) {
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)) {
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
try {
setValue(Id3v2FrameIds::lRecordingTime, DateTime::fromDateAndTime(year, month, day, hour, minute));
} catch (const ConversionException &e) {
try {
// try to set at least the year
setValue(Id3v2FrameIds::lRecordingTime, DateTime::fromDate(year));
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);
}
}
/*!
* \brief Parses tag information from the specified \a stream.
*
@ -382,8 +446,8 @@ void Id3v2Tag::parse(istream &stream, const std::uint64_t maximalSize, Diagnosti
throw InvalidDataException();
}
// read header data
std::uint8_t majorVersion = reader.readByte();
std::uint8_t revisionVersion = reader.readByte();
const std::uint8_t majorVersion = reader.readByte();
const std::uint8_t revisionVersion = reader.readByte();
setVersion(majorVersion, revisionVersion);
m_flags = reader.readByte();
m_sizeExcludingHeader = reader.readSynchsafeUInt32BE();
@ -451,6 +515,8 @@ void Id3v2Tag::parse(istream &stream, const std::uint64_t maximalSize, Diagnosti
}
}
convertOldRecordDateFields(context, diag);
// check for extended header
if (!hasFooter()) {
return;
@ -579,6 +645,77 @@ bool FrameComparer::operator()(std::uint32_t lhs, std::uint32_t rhs) const
* An instance can be obtained using the Id3v2Tag::prepareMaking() method.
*/
/*!
* \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.
// This allows one to bypass this handling and set the old fields explicitely.
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);
}
/*!
* \brief Prepares making the specified \a tag.
* \sa See Id3v2Tag::prepareMaking() for more information.
@ -596,6 +733,8 @@ Id3v2TagMaker::Id3v2TagMaker(Id3v2Tag &tag, Diagnostics &diag)
throw VersionNotSupportedException();
}
tag.prepareRecordDataForMaking(context, diag);
// prepare frames
m_maker.reserve(tag.fields().size());
for (auto &pair : tag.fields()) {

View File

@ -60,6 +60,7 @@ public:
class TAG_PARSER_EXPORT Id3v2Tag : public FieldMapBasedTag<Id3v2Tag> {
friend class FieldMapBasedTag<Id3v2Tag>;
friend class Id3v2TagMaker;
public:
Id3v2Tag();
@ -97,6 +98,11 @@ protected:
std::vector<const TagValue *> internallyGetValues(const IdentifierType &id) const;
bool internallySetValues(const IdentifierType &id, const std::vector<TagValue> &values);
private:
void convertOldRecordDateFields(const std::string &diagContext, Diagnostics &diag);
void removeOldRecordDateRelatedFields();
void prepareRecordDataForMaking(const std::string &diagContext, Diagnostics &diag);
private:
std::uint8_t m_majorVersion;
std::uint8_t m_revisionVersion;

View File

@ -28,8 +28,9 @@ MatroskaTag::IdentifierType MatroskaTag::internallyGetFieldId(KnownField field)
case KnownField::Comment:
return comment();
case KnownField::RecordDate:
return dateRecorded();
case KnownField::Year:
return dateRecorded();
case KnownField::ReleaseDate:
return dateRelease();
case KnownField::Title:
return title();
@ -78,7 +79,7 @@ KnownField MatroskaTag::internallyGetKnownField(const IdentifierType &id) const
{ album(), KnownField::Album },
{ comment(), KnownField::Comment },
{ dateRecorded(), KnownField::RecordDate },
{ dateRelease(), KnownField::Year },
{ dateRelease(), KnownField::ReleaseDate },
{ title(), KnownField::Title },
{ partNumber(), KnownField::PartNumber },
{ totalParts(), KnownField::TotalParts },

View File

@ -126,6 +126,7 @@ Mp4Tag::IdentifierType Mp4Tag::internallyGetFieldId(KnownField field) const
return Artist;
case KnownField::Comment:
return Comment;
case KnownField::RecordDate:
case KnownField::Year:
return Year;
case KnownField::Title:
@ -177,7 +178,7 @@ KnownField Mp4Tag::internallyGetKnownField(const IdentifierType &id) const
case Comment:
return KnownField::Comment;
case Year:
return KnownField::Year;
return KnownField::RecordDate;
case Title:
return KnownField::Title;
case PreDefinedGenre:

18
tag.h
View File

@ -45,7 +45,7 @@ enum class KnownField : unsigned int {
Album, /**< album/collection */
Artist, /**< artist/band */
Genre, /**< genre */
Year, /**< year */
Year, /**< record date, deprecated - FIXME v10: remove in favor of RecordDate and ReleaseDate */
Comment, /**< comment */
Bpm, /**< beats per minute */
Bps, /**< beats per second */
@ -70,6 +70,7 @@ enum class KnownField : unsigned int {
Description, /**< description */
Vendor, /**< vendor */
AlbumArtist, /**< album artist */
ReleaseDate, /**< release date */
};
/*!
@ -80,7 +81,7 @@ constexpr KnownField firstKnownField = KnownField::Title;
/*!
* \brief The last valid entry in the TagParser::KnownField enum.
*/
constexpr KnownField lastKnownField = KnownField::AlbumArtist;
constexpr KnownField lastKnownField = KnownField::ReleaseDate;
/*!
* \brief The number of valid entries in the TagParser::KnownField enum.
@ -88,11 +89,20 @@ constexpr KnownField lastKnownField = KnownField::AlbumArtist;
constexpr unsigned int knownFieldArraySize = static_cast<unsigned int>(lastKnownField) + 1;
/*!
* \brief Returns the next known field. Returns KnownField::Invalid if there is not next field.
* \brief Returns whether the specified \a field is deprecated and should not be used anymore.
*/
constexpr bool isKnownFieldDeprecated(KnownField field)
{
return field == KnownField::Year;
}
/*!
* \brief Returns the next known field skipping any deprecated fields. Returns KnownField::Invalid if there is not next field.
*/
constexpr KnownField nextKnownField(KnownField field)
{
return field == lastKnownField ? KnownField::Invalid : static_cast<KnownField>(static_cast<int>(field) + 1);
const auto next = field == lastKnownField ? KnownField::Invalid : static_cast<KnownField>(static_cast<int>(field) + 1);
return isKnownFieldDeprecated(next) ? nextKnownField(next) : next;
}
class TAG_PARSER_EXPORT Tag {

View File

@ -473,8 +473,14 @@ DateTime TagValue::toDateTime() const
return DateTime();
}
switch (m_type) {
case TagDataType::Text:
return DateTime::fromString(toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1));
case TagDataType::Text: {
const auto str = toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1);
try {
return DateTime::fromIsoStringGmt(str.data());
} catch (const ConversionException &) {
return DateTime::fromString(str);
}
}
case TagDataType::Integer:
case TagDataType::DateTime:
if (m_size == sizeof(std::int32_t)) {
@ -661,7 +667,7 @@ void TagValue::toString(string &result, TagTextEncoding encoding) const
result = toTimeSpan().toString();
break;
case TagDataType::DateTime:
result = toDateTime().toString();
result = toDateTime().toIsoString();
break;
default:
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));

View File

@ -97,6 +97,7 @@ public:
TagValue &operator=(TagValue &&other) = default;
bool operator==(const TagValue &other) const;
bool operator!=(const TagValue &other) const;
operator bool() const;
// methods
bool isNull() const;
@ -335,6 +336,15 @@ inline bool TagValue::operator!=(const TagValue &other) const
return !compareTo(other, TagValueComparisionFlags::None);
}
/*!
* \brief Returns whether the value is not empty.
* \sa See TagValue::isEmpty() for a definition on what is considered empty.
*/
inline TagParser::TagValue::operator bool() const
{
return !isEmpty();
}
/*!
* \brief Assigns a copy of the given \a text.
* \param text Specifies the text to be assigned.

View File

@ -56,6 +56,7 @@ VorbisComment::IdentifierType VorbisComment::internallyGetFieldId(KnownField fie
return comment();
case KnownField::Cover:
return cover();
case KnownField::RecordDate:
case KnownField::Year:
return date();
case KnownField::Title:
@ -104,7 +105,7 @@ KnownField VorbisComment::internallyGetKnownField(const IdentifierType &id) cons
{ artist(), KnownField::Artist },
{ comment(), KnownField::Comment },
{ cover(), KnownField::Cover },
{ date(), KnownField::Year },
{ date(), KnownField::RecordDate },
{ title(), KnownField::Title },
{ genre(), KnownField::Genre },
{ trackNumber(), KnownField::TrackPosition },