diff --git a/id3/id3v1tag.cpp b/id3/id3v1tag.cpp index 05c1a88..6344038 100644 --- a/id3/id3v1tag.cpp +++ b/id3/id3v1tag.cpp @@ -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: diff --git a/id3/id3v2frameids.cpp b/id3/id3v2frameids.cpp index 3da2765..b6167ec 100644 --- a/id3/id3v2frameids.cpp +++ b/id3/id3v2frameids.cpp @@ -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: diff --git a/id3/id3v2frameids.h b/id3/id3v2frameids.h index 739c1ba..73cab2c 100644 --- a/id3/id3v2frameids.h +++ b/id3/id3v2frameids.h @@ -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 */ diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index 9feebfb..8577d64 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -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(std::string_view(str.data() + 0, 2)); + month = stringToNumber(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(std::string_view(str.data() + 0, 2)); + minute = stringToNumber(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()) { diff --git a/id3/id3v2tag.h b/id3/id3v2tag.h index ca33278..5fccb4d 100644 --- a/id3/id3v2tag.h +++ b/id3/id3v2tag.h @@ -60,6 +60,7 @@ public: class TAG_PARSER_EXPORT Id3v2Tag : public FieldMapBasedTag { friend class FieldMapBasedTag; + friend class Id3v2TagMaker; public: Id3v2Tag(); @@ -97,6 +98,11 @@ protected: std::vector internallyGetValues(const IdentifierType &id) const; bool internallySetValues(const IdentifierType &id, const std::vector &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; diff --git a/matroska/matroskatag.cpp b/matroska/matroskatag.cpp index cfa4bab..f806237 100644 --- a/matroska/matroskatag.cpp +++ b/matroska/matroskatag.cpp @@ -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 }, diff --git a/mp4/mp4tag.cpp b/mp4/mp4tag.cpp index 17ee397..9ea7652 100644 --- a/mp4/mp4tag.cpp +++ b/mp4/mp4tag.cpp @@ -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: diff --git a/tag.h b/tag.h index 22c1d8f..0ed3dcb 100644 --- a/tag.h +++ b/tag.h @@ -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(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(static_cast(field) + 1); + const auto next = field == lastKnownField ? KnownField::Invalid : static_cast(static_cast(field) + 1); + return isKnownFieldDeprecated(next) ? nextKnownField(next) : next; } class TAG_PARSER_EXPORT Tag { diff --git a/tagvalue.cpp b/tagvalue.cpp index 82010fd..034ca8a 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -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.")); diff --git a/tagvalue.h b/tagvalue.h index a2c2f5d..a247554 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -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. diff --git a/vorbis/vorbiscomment.cpp b/vorbis/vorbiscomment.cpp index 2795a0b..dde26b7 100644 --- a/vorbis/vorbiscomment.cpp +++ b/vorbis/vorbiscomment.cpp @@ -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 },