diff --git a/fieldbasedtag.h b/fieldbasedtag.h index 9bc8b75..7d5ac04 100644 --- a/fieldbasedtag.h +++ b/fieldbasedtag.h @@ -35,7 +35,6 @@ public: using FieldType = typename FieldMapBasedTagTraits::FieldType; using IdentifierType = typename FieldMapBasedTagTraits::FieldType::IdentifierType; using Compare = typename FieldMapBasedTagTraits::Compare; - using FieldMapBasedTagBase = FieldMapBasedTag; FieldMapBasedTag(); @@ -66,8 +65,12 @@ public: void ensureTextValuesAreProperlyEncoded(); protected: + using CRTPBase = FieldMapBasedTag; + const TagValue &internallyGetValue(const IdentifierType &id) const; + std::vector internallyGetValues(const IdentifierType &id) const; bool internallySetValue(const IdentifierType &id, const TagValue &value); + bool internallySetValues(const IdentifierType &id, const std::vector &values); bool internallyHasField(const IdentifierType &id) const; // no default implementation: IdentifierType internallyGetFieldId(KnownField field) const; // no default implementation: KnownField internallyGetKnownField(const IdentifierType &id) const; @@ -123,6 +126,23 @@ template const TagValue &FieldMapBasedTagsecond.value() : TagValue::empty(); } +/*! + * \brief Default implementation for values(). + * \remarks Shadow in subclass to provide custom implementation. + */ +template +std::vector FieldMapBasedTag::internallyGetValues(const IdentifierType &id) const +{ + auto range = m_fields.equal_range(id); + std::vector values; + for (auto i = range.first; i != range.second; ++i) { + if (!i->second.value().isEmpty()) { + values.push_back(&i->second.value()); + } + } + return values; +} + /*! * \brief Returns the value of the field with the specified \a id. * \sa Tag::value() @@ -143,19 +163,12 @@ template inline const TagValue &FieldMapBasedTag inline std::vector FieldMapBasedTag::values(const IdentifierType &id) const { - auto range = m_fields.equal_range(id); - std::vector values; - for (auto i = range.first; i != range.second; ++i) { - if (!i->second.value().isEmpty()) { - values.push_back(&i->second.value()); - } - } - return values; + return static_cast(this)->internallyGetValues(id); } template inline std::vector FieldMapBasedTag::values(KnownField field) const { - return values(fieldId(field)); + return static_cast(this)->values(fieldId(field)); } template inline bool FieldMapBasedTag::setValue(KnownField field, const TagValue &value) @@ -181,22 +194,11 @@ template bool FieldMapBasedTag::i } /*! - * \brief Assigns the given \a value to the field with the specified \a id. - * \sa Tag::setValue() - */ -template bool FieldMapBasedTag::setValue(const IdentifierType &id, const TagParser::TagValue &value) -{ - return static_cast(this)->internallySetValue(id, value); -} - -/*! - * \brief Assigns the given \a values to the field with the specified \a id. - * \remarks There might me more than one value assigned to an \a id. Whereas setValue() only alters the first value, this - * method will replace all currently assigned values with the specified \a values. - * \sa Tag::setValues() + * \brief Default implementation for setValues(). + * \remarks Shadow in subclass to provide custom implementation. */ template -bool FieldMapBasedTag::setValues(const IdentifierType &id, const std::vector &values) +bool FieldMapBasedTag::internallySetValues(const FieldMapBasedTag::IdentifierType &id, const std::vector &values) { auto valuesIterator = values.cbegin(); auto range = m_fields.equal_range(id); @@ -219,6 +221,27 @@ bool FieldMapBasedTag::setValues(const IdentifierType &id, c return true; } +/*! + * \brief Assigns the given \a value to the field with the specified \a id. + * \sa Tag::setValue() + */ +template bool FieldMapBasedTag::setValue(const IdentifierType &id, const TagParser::TagValue &value) +{ + return static_cast(this)->internallySetValue(id, value); +} + +/*! + * \brief Assigns the given \a values to the field with the specified \a id. + * \remarks There might me more than one value assigned to an \a id. Whereas setValue() only alters the first value, this + * method will replace all currently assigned values with the specified \a values. + * \sa Tag::setValues() + */ +template +bool FieldMapBasedTag::setValues(const IdentifierType &id, const std::vector &values) +{ + return static_cast(this)->internallySetValues(id, values); +} + /*! * \brief Assigns the given \a values to the field with the specified \a id. * \remarks There might me more than one value assigned to a \a field. Whereas setValue() only alters the first value, this diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index 4a4edad..96eaedc 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -20,6 +20,68 @@ namespace TagParser { * \brief Implementation of TagParser::Tag for ID3v2 tags. */ +/*! + * \brief Works like the default implementation but adds additional values as well. + */ +std::vector Id3v2Tag::internallyGetValues(const IdentifierType &id) const +{ + auto range = fields().equal_range(id); + std::vector values; + for (auto i = range.first; i != range.second; ++i) { + const auto &frame(i->second); + if (!frame.value().isEmpty()) { + values.push_back(&frame.value()); + } + for (const auto &value : frame.additionalValues()) { + values.push_back(&value); + } + } + return values; +} + +/*! + * \brief Uses default implementation for non-text frames and applies special handling to text frames. + * + * - Ensure text frames are unique + * - Allow to store multiple values inside the same text frame. + */ +bool Id3v2Tag::internallySetValues(const IdentifierType &id, const std::vector &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 + if (frameIterator != range.second) { + ++range.first; + } else { + frameIterator = fields().insert(make_pair(id, Id3v2Frame())); + } + + // add primary value to frame + auto &frame(frameIterator->second); + auto valuesIterator = values.cbegin(); + if (valuesIterator != values.cend()) { + frame.setValue(*valuesIterator); + ++valuesIterator; + } else { + frame.value().clearDataAndMetadata(); + } + // add additional values to frame + frame.additionalValues() = vector(valuesIterator, values.cend()); + + // 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; +} + Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const { using namespace Id3v2FrameIds; diff --git a/id3/id3v2tag.h b/id3/id3v2tag.h index 5ae4b21..4504d3a 100644 --- a/id3/id3v2tag.h +++ b/id3/id3v2tag.h @@ -92,6 +92,8 @@ protected: IdentifierType internallyGetFieldId(KnownField field) const; KnownField internallyGetKnownField(const IdentifierType &id) const; TagDataType internallyGetProposedDataType(const uint32 &id) const; + std::vector internallyGetValues(const IdentifierType &id) const; + bool internallySetValues(const IdentifierType &id, const std::vector &values); private: byte m_majorVersion; diff --git a/tests/overallmp3.cpp b/tests/overallmp3.cpp index ee6367a..cfcdd51 100644 --- a/tests/overallmp3.cpp +++ b/tests/overallmp3.cpp @@ -121,7 +121,8 @@ void OverallTests::checkMp3Testfile2() CPPUNIT_ASSERT_EQUAL("Infinite (Original Mix)"s, tag->value(KnownField::Title).toString(TagTextEncoding::Utf8)); CPPUNIT_ASSERT_EQUAL("B-Front"s, tag->value(KnownField::Artist).toString(TagTextEncoding::Utf8)); CPPUNIT_ASSERT_EQUAL("Infinite"s, tag->value(KnownField::Album).toString(TagTextEncoding::Utf8)); - CPPUNIT_ASSERT_EQUAL("Hardstyle"s, tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL(m_tagStatus == TagStatus::TestMetaDataPresent ? "Test"s : "Hardstyle"s, + tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); CPPUNIT_ASSERT_EQUAL("Lavf57.83.100"s, tag->value(KnownField::EncoderSettings).toString(TagTextEncoding::Utf8)); CPPUNIT_ASSERT_EQUAL("Roughstate"s, tag->value(KnownField::RecordLabel).toString(TagTextEncoding::Utf8)); CPPUNIT_ASSERT_EQUAL("2017"s, tag->value(KnownField::RecordDate).toString(TagTextEncoding::Utf8)); @@ -129,29 +130,57 @@ void OverallTests::checkMp3Testfile2() CPPUNIT_ASSERT(tag->value(KnownField::Length).toTimeSpan().isNull()); CPPUNIT_ASSERT(tag->value(KnownField::Lyricist).isEmpty()); - // check multiple values + // check additional text frame values const auto &fields = id3v2Tag->fields(); auto genreFields = fields.equal_range(Id3v2FrameIds::lGenre); CPPUNIT_ASSERT_MESSAGE("genre field present"s, genreFields.first != genreFields.second); const auto &genreField = genreFields.first->second; const auto &additionalValues = genreField.additionalValues(); - CPPUNIT_ASSERT_EQUAL("Hardstyle"s, tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); - CPPUNIT_ASSERT_EQUAL(3_st, additionalValues.size()); - CPPUNIT_ASSERT_EQUAL("Test"s, additionalValues[0].toString(TagTextEncoding::Utf8)); - CPPUNIT_ASSERT_EQUAL("Example"s, additionalValues[1].toString(TagTextEncoding::Utf8)); - CPPUNIT_ASSERT_EQUAL("Hard Dance"s, additionalValues[2].toString(TagTextEncoding::Utf8)); - CPPUNIT_ASSERT_EQUAL("Hardstyle"s, tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); + if (m_tagStatus == TagStatus::TestMetaDataPresent) { + CPPUNIT_ASSERT_EQUAL("Test"s, tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL(1_st, additionalValues.size()); + CPPUNIT_ASSERT_EQUAL("Example"s, additionalValues[0].toString(TagTextEncoding::Utf8)); + } else { + CPPUNIT_ASSERT_EQUAL("Hardstyle"s, tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL(3_st, additionalValues.size()); + CPPUNIT_ASSERT_EQUAL("Test"s, additionalValues[0].toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Example"s, additionalValues[1].toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Hard Dance"s, additionalValues[2].toString(TagTextEncoding::Utf8)); + } CPPUNIT_ASSERT_MESSAGE("exactly one genre field present"s, ++genreFields.first == genreFields.second); + + // check whether additional text frame values are returned correctly by values() + const auto artists = id3v2Tag->values(KnownField::Artist); + CPPUNIT_ASSERT_EQUAL(m_tagStatus == TagStatus::TestMetaDataPresent ? 3_st : 2_st, artists.size()); + CPPUNIT_ASSERT_EQUAL("B-Front"s, artists[0]->toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Second Artist Example"s, artists[1]->toString(TagTextEncoding::Utf8)); + if (m_tagStatus == TagStatus::TestMetaDataPresent) { + CPPUNIT_ASSERT_EQUAL("3rd Artist Example"s, artists[2]->toString(TagTextEncoding::Utf8)); + } + + const auto genres = id3v2Tag->values(KnownField::Genre); + if (m_tagStatus == TagStatus::TestMetaDataPresent) { + CPPUNIT_ASSERT_EQUAL(2_st, genres.size()); + CPPUNIT_ASSERT_EQUAL("Test"s, genres[0]->toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Example"s, genres[1]->toString(TagTextEncoding::Utf8)); + } else { + CPPUNIT_ASSERT_EQUAL(4_st, genres.size()); + CPPUNIT_ASSERT_EQUAL("Hardstyle"s, genres[0]->toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Test"s, genres[1]->toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Example"s, genres[2]->toString(TagTextEncoding::Utf8)); + CPPUNIT_ASSERT_EQUAL("Hard Dance"s, genres[3]->toString(TagTextEncoding::Utf8)); + } } break; case TagStatus::Removed: - CPPUNIT_ASSERT_EQUAL(0_st, tracks.size()); + CPPUNIT_ASSERT_EQUAL(0_st, tags.size()); } - if (expectId3v24) { + if (expectId3v24 || m_tagStatus == TagStatus::Removed) { CPPUNIT_ASSERT(m_diag.level() <= DiagLevel::Information); return; } + CPPUNIT_ASSERT(m_diag.level() <= DiagLevel::Warning); int warningCount = 0; for (const auto &msg : m_diag) { @@ -162,8 +191,8 @@ void OverallTests::checkMp3Testfile2() TESTUTILS_ASSERT_LIKE("context", "(parsing|making) (TPE1|TCON)( frame)?", msg.context()); TESTUTILS_ASSERT_LIKE("message", "Multiple strings (found|assigned) .*" - "Additional (value \"Second Artist Example\" is|" - "values \"Test\", \"Example\" and \"Hard Dance\" are) " + "Additional (values \"Second Artist Example\" and \"3rd Artist Example\" are|" + "value \"Example\" is) " "supposed to be ignored.", msg.message()); } @@ -306,7 +335,12 @@ void OverallTests::setMp3TestMetaData2() using namespace Mp3TestFlags; CPPUNIT_ASSERT_EQUAL(1_st, m_fileInfo.id3v2Tags().size()); - m_fileInfo.id3v2Tags().front()->setVersion((m_mode & UseId3v24) ? 4 : 3, 0); + auto &id3v2Tag(m_fileInfo.id3v2Tags().front()); + id3v2Tag->setVersion((m_mode & UseId3v24) ? 4 : 3, 0); + const auto artists = id3v2Tag->values(KnownField::Artist); + CPPUNIT_ASSERT_EQUAL(2_st, artists.size()); + id3v2Tag->setValues(KnownField::Artist, { *artists[0], *artists[1], TagValue("3rd Artist Example") }); + id3v2Tag->setValues(KnownField::Genre, { TagValue("Test"), TagValue("Example") }); } /*!