Adjust values()/setValues() to handle multiple values per text frame

See https://github.com/Martchus/tagparser/issues/10
This commit is contained in:
Martchus 2018-07-11 15:53:23 +02:00
parent af4b43ff79
commit e75a8d25c5
4 changed files with 158 additions and 37 deletions

View File

@ -35,7 +35,6 @@ public:
using FieldType = typename FieldMapBasedTagTraits<ImplementationType>::FieldType;
using IdentifierType = typename FieldMapBasedTagTraits<ImplementationType>::FieldType::IdentifierType;
using Compare = typename FieldMapBasedTagTraits<ImplementationType>::Compare;
using FieldMapBasedTagBase = FieldMapBasedTag<ImplementationType>;
FieldMapBasedTag();
@ -66,8 +65,12 @@ public:
void ensureTextValuesAreProperlyEncoded();
protected:
using CRTPBase = FieldMapBasedTag<ImplementationType>;
const TagValue &internallyGetValue(const IdentifierType &id) const;
std::vector<const TagValue *> internallyGetValues(const IdentifierType &id) const;
bool internallySetValue(const IdentifierType &id, const TagValue &value);
bool internallySetValues(const IdentifierType &id, const std::vector<TagValue> &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 <class ImplementationType> const TagValue &FieldMapBasedTag<Implementat
return i != m_fields.end() ? i->second.value() : TagValue::empty();
}
/*!
* \brief Default implementation for values().
* \remarks Shadow in subclass to provide custom implementation.
*/
template <class ImplementationType>
std::vector<const TagValue *> FieldMapBasedTag<ImplementationType>::internallyGetValues(const IdentifierType &id) const
{
auto range = m_fields.equal_range(id);
std::vector<const TagValue *> 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 <class ImplementationType> inline const TagValue &FieldMapBasedTag<Impl
*/
template <class ImplementationType> inline std::vector<const TagValue *> FieldMapBasedTag<ImplementationType>::values(const IdentifierType &id) const
{
auto range = m_fields.equal_range(id);
std::vector<const TagValue *> 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<const ImplementationType *>(this)->internallyGetValues(id);
}
template <class ImplementationType> inline std::vector<const TagValue *> FieldMapBasedTag<ImplementationType>::values(KnownField field) const
{
return values(fieldId(field));
return static_cast<const ImplementationType *>(this)->values(fieldId(field));
}
template <class ImplementationType> inline bool FieldMapBasedTag<ImplementationType>::setValue(KnownField field, const TagValue &value)
@ -181,22 +194,11 @@ template <class ImplementationType> bool FieldMapBasedTag<ImplementationType>::i
}
/*!
* \brief Assigns the given \a value to the field with the specified \a id.
* \sa Tag::setValue()
*/
template <class ImplementationType> bool FieldMapBasedTag<ImplementationType>::setValue(const IdentifierType &id, const TagParser::TagValue &value)
{
return static_cast<ImplementationType *>(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 <class ImplementationType>
bool FieldMapBasedTag<ImplementationType>::setValues(const IdentifierType &id, const std::vector<TagValue> &values)
bool FieldMapBasedTag<ImplementationType>::internallySetValues(const FieldMapBasedTag::IdentifierType &id, const std::vector<TagValue> &values)
{
auto valuesIterator = values.cbegin();
auto range = m_fields.equal_range(id);
@ -219,6 +221,27 @@ bool FieldMapBasedTag<ImplementationType>::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 <class ImplementationType> bool FieldMapBasedTag<ImplementationType>::setValue(const IdentifierType &id, const TagParser::TagValue &value)
{
return static_cast<ImplementationType *>(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 <class ImplementationType>
bool FieldMapBasedTag<ImplementationType>::setValues(const IdentifierType &id, const std::vector<TagValue> &values)
{
return static_cast<ImplementationType *>(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

View File

@ -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<const TagValue *> Id3v2Tag::internallyGetValues(const IdentifierType &id) const
{
auto range = fields().equal_range(id);
std::vector<const TagValue *> 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<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
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<TagValue>(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;

View File

@ -92,6 +92,8 @@ protected:
IdentifierType internallyGetFieldId(KnownField field) const;
KnownField internallyGetKnownField(const IdentifierType &id) const;
TagDataType internallyGetProposedDataType(const uint32 &id) const;
std::vector<const TagValue *> internallyGetValues(const IdentifierType &id) const;
bool internallySetValues(const IdentifierType &id, const std::vector<TagValue> &values);
private:
byte m_majorVersion;

View File

@ -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") });
}
/*!