Add data type for ID3v2's Popularimeter field

See https://github.com/Martchus/tageditor/issues/84
This commit is contained in:
Martchus 2022-06-16 00:54:34 +02:00
parent 9511d61371
commit 46014def51
5 changed files with 167 additions and 2 deletions

View File

@ -9,8 +9,8 @@ set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags")
set(META_VERSION_MAJOR 11)
set(META_VERSION_MINOR 2)
set(META_VERSION_PATCH 2)
set(META_VERSION_MINOR 3)
set(META_VERSION_PATCH 0)
set(META_REQUIRED_CPP_UNIT_VERSION 1.14.0)
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)

View File

@ -379,6 +379,9 @@ TagDataType Id3v2Tag::internallyGetProposedDataType(const std::uint32_t &id) con
case lCover:
case sCover:
return TagDataType::Picture;
case lRating:
case sRating:
return TagDataType::Popularity;
default:
if (Id3v2FrameIds::isTextFrame(id)) {
return TagDataType::Text;

View File

@ -9,9 +9,12 @@
#include <c++utilities/conversion/conversionexception.h>
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/binaryreader.h>
#include <c++utilities/io/binarywriter.h>
#include <algorithm>
#include <cstring>
#include <sstream>
#include <utility>
using namespace std;
@ -269,6 +272,13 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options
return toTimeSpan() == other.toTimeSpan();
case TagDataType::DateTime:
return toDateTime() == other.toDateTime();
case TagDataType::Popularity:
if (options & TagValueComparisionFlags::CaseInsensitive) {
const auto lhs = toPopularity(), rhs = other.toPopularity();
return lhs.rating == rhs.rating && lhs.playCounter == rhs.playCounter && compareData(lhs.user, rhs.user, true);
} else {
return toPopularity() == other.toPopularity();
}
case TagDataType::Picture:
case TagDataType::Binary:
case TagDataType::Undefined:
@ -291,6 +301,9 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options
}
}
try {
if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) {
return toPopularity() == other.toPopularity();
}
return compareData(toString(), other.toString(m_encoding), options & TagValueComparisionFlags::CaseInsensitive);
} catch (const ConversionException &) {
return false;
@ -492,6 +505,44 @@ DateTime TagValue::toDateTime() const
}
}
/*!
* \brief Converts the value of the current TagValue object to its equivalent
* Popularity representation.
* \throws Throws ConversionException on failure.
*/
Popularity TagValue::toPopularity() const
{
auto popularity = Popularity();
if (isEmpty()) {
return popularity;
}
switch (m_type) {
case TagDataType::Text:
popularity = Popularity::fromString(std::string_view(toString(TagTextEncoding::Utf8)));
break;
case TagDataType::Integer:
popularity.rating = static_cast<double>(toInteger());
break;
case TagDataType::Popularity: {
auto s = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
auto reader = BinaryReader(&s);
try {
s.exceptions(std::ios_base::failbit | std::ios_base::badbit);
s.rdbuf()->pubsetbuf(m_ptr.get(), static_cast<std::streamsize>(m_size));
popularity.user = reader.readLengthPrefixedString();
popularity.rating = reader.readFloat64LE();
popularity.playCounter = reader.readUInt64LE();
} catch (const std::ios_base::failure &) {
throw ConversionException(argsToString("Assigned popularity is invalid"));
}
break;
}
default:
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time."));
}
return popularity;
}
/*!
* \brief Converts the currently assigned text value to the specified \a encoding.
* \throws Throws CppUtilities::ConversionException() if the conversion fails.
@ -666,6 +717,9 @@ void TagValue::toString(string &result, TagTextEncoding encoding) const
case TagDataType::DateTime:
result = toDateTime().toString(DateTimeOutputFormat::IsoOmittingDefaultComponents);
break;
case TagDataType::Popularity:
result = toPopularity().toString();
break;
default:
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
}
@ -750,6 +804,9 @@ void TagValue::toWString(std::u16string &result, TagTextEncoding encoding) const
case TagDataType::DateTime:
regularStrRes = toDateTime().toString(DateTimeOutputFormat::IsoOmittingDefaultComponents);
break;
case TagDataType::Popularity:
regularStrRes = toPopularity().toString();
break;
default:
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
}
@ -876,6 +933,27 @@ void TagValue::assignData(unique_ptr<char[]> &&data, size_t length, TagDataType
m_ptr = move(data);
}
/*!
* \brief Assigns the specified popularity \a value.
*/
void TagValue::assignPopularity(const Popularity &value)
{
auto s = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
auto writer = BinaryWriter(&s);
try {
s.exceptions(std::ios_base::failbit | std::ios_base::badbit);
writer.writeLengthPrefixedString(value.user);
writer.writeFloat64LE(value.rating);
writer.writeUInt64LE(value.playCounter);
auto size = static_cast<std::size_t>(s.tellp());
auto ptr = std::make_unique<char[]>(size);
s.read(ptr.get(), s.tellp());
assignData(std::move(ptr), size, TagDataType::Popularity);
} catch (const std::ios_base::failure &) {
throw ConversionException("Unable to serialize specified Popularity");
}
}
/*!
* \brief Strips the byte order mask from the specified \a text.
*/
@ -964,4 +1042,35 @@ const TagValue &TagValue::empty()
return emptyTagValue;
}
/*!
* \brief Returns the popularity as string in the format "user|rating|play-counter".
*/
std::string Popularity::toString() const
{
return user % '|' % numberToString(rating) % '|' + playCounter;
}
/*!
* \brief Parses the popularity from \a str assuming the same format as toString() produces.
* \throws Throws ConversionException() if the format is invalid.
*/
Popularity Popularity::fromString(std::string_view str)
{
const auto parts = splitStringSimple<std::vector<std::string_view>>(str, "|");
auto res = Popularity();
if (parts.empty()) {
return res;
} else if (parts.size() > 3) {
throw ConversionException("Wrong format, expected \"user|rating|play-counter\"");
}
res.user = parts.front();
if (parts.size() > 1) {
res.rating = stringToNumber<decltype(res.rating)>(parts[1]);
}
if (parts.size() > 2) {
res.playCounter = stringToNumber<decltype(res.playCounter)>(parts[2]);
}
return res;
}
} // namespace TagParser

View File

@ -68,6 +68,22 @@ constexpr int characterSize(TagTextEncoding encoding)
}
}
struct TAG_PARSER_EXPORT Popularity {
/// \brief The user who gave the rating / played the file, e.g. identified by e-mail address.
std::string user;
/// \brief The rating on a tag type specific scale.
double rating = 0.0;
/// \brief Play counter specific to the user.
std::uint64_t playCounter = 0;
std::string toString() const;
static Popularity fromString(std::string_view str);
bool operator==(const Popularity &other) const
{
return playCounter == other.playCounter && rating == other.rating && user == other.user;
}
};
/*!
* \brief Specifies the data type.
*/
@ -80,6 +96,7 @@ enum class TagDataType : unsigned int {
DateTime, /**< date time, see ChronoUtils::DateTime */
Picture, /**< picture file */
Binary, /**< unspecified binary data */
Popularity, /**< rating with user info and play counter (as in ID3v2's "Popularimeter") */
Undefined /**< undefined/invalid data type */
};
@ -112,6 +129,7 @@ public:
explicit TagValue(PositionInSet value);
explicit TagValue(CppUtilities::DateTime value);
explicit TagValue(CppUtilities::TimeSpan value);
explicit TagValue(const Popularity &value);
TagValue(const TagValue &other);
TagValue(TagValue &&other) = default;
~TagValue();
@ -139,6 +157,7 @@ public:
PositionInSet toPositionInSet() const;
CppUtilities::TimeSpan toTimeSpan() const;
CppUtilities::DateTime toDateTime() const;
Popularity toPopularity() const;
std::size_t dataSize() const;
char *dataPointer();
const char *dataPointer() const;
@ -177,6 +196,7 @@ public:
void assignPosition(PositionInSet value);
void assignTimeSpan(CppUtilities::TimeSpan value);
void assignDateTime(CppUtilities::DateTime value);
void assignPopularity(const Popularity &value);
static void stripBom(const char *&text, std::size_t &length, TagTextEncoding encoding);
static void ensureHostByteOrder(std::u16string &u16str, TagTextEncoding currentEncoding);
@ -367,6 +387,15 @@ inline TagValue::TagValue(CppUtilities::TimeSpan value)
{
}
/*!
* \brief Constructs a new TagValue holding a copy of the given Popularity \a value.
*/
inline TagValue::TagValue(const Popularity &value)
: TagValue()
{
assignPopularity(value);
}
/*!
* \brief Returns whether both instances are equal.
* \sa The same as TagValue::compareTo() with TagValueComparisionOption::None so see TagValue::compareTo() for details.

View File

@ -27,6 +27,7 @@ class TagValueTests : public TestFixture {
CPPUNIT_TEST(testPositionInSet);
CPPUNIT_TEST(testTimeSpan);
CPPUNIT_TEST(testDateTime);
CPPUNIT_TEST(testPopularity);
CPPUNIT_TEST(testString);
CPPUNIT_TEST(testEqualityOperator);
CPPUNIT_TEST_SUITE_END();
@ -41,6 +42,7 @@ public:
void testPositionInSet();
void testTimeSpan();
void testDateTime();
void testPopularity();
void testString();
void testEqualityOperator();
};
@ -147,6 +149,17 @@ void TagValueTests::testDateTime()
CPPUNIT_ASSERT_THROW(dateTime.toPositionInSet(), ConversionException);
}
void TagValueTests::testPopularity()
{
const auto tagValue = TagValue(Popularity{ .user = "foo", .rating = 42, .playCounter = 123 });
const auto popularity = tagValue.toPopularity();
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (user)", "foo"s, popularity.user);
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (rating)", 42.0, popularity.rating);
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (play counter)", std::uint64_t(123), popularity.playCounter);
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to string", "foo|42|123"s, tagValue.toString());
CPPUNIT_ASSERT_THROW_MESSAGE("failing conversion to other type", TagValue("foo|bar"sv, TagTextEncoding::Latin1).toInteger(), ConversionException);
}
void TagValueTests::testString()
{
CPPUNIT_ASSERT_EQUAL("15\xe4"s, TagValue("15ä", 4, TagTextEncoding::Utf8).toString(TagTextEncoding::Latin1));
@ -188,6 +201,11 @@ void TagValueTests::testString()
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to genre from name", 2, TagValue("Country", 7, TagTextEncoding::Latin1).toStandardGenreIndex());
CPPUNIT_ASSERT_THROW_MESSAGE(
"failing conversion to genre", TagValue("Kountry", 7, TagTextEncoding::Latin1).toStandardGenreIndex(), ConversionException);
const auto popularity = TagValue("foo|42|123"sv).toPopularity();
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (user)", "foo"s, popularity.user);
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (rating)", 42.0, popularity.rating);
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (play counter)", std::uint64_t(123), popularity.playCounter);
CPPUNIT_ASSERT_THROW_MESSAGE("failing conversion to popularity", TagValue("foo|bar"sv).toPopularity(), ConversionException);
}
void TagValueTests::testEqualityOperator()
@ -211,6 +229,12 @@ void TagValueTests::testEqualityOperator()
const TagValue fooTagValue("foo", 3, TagDataType::Text), fOoTagValue("fOo", 3, TagDataType::Text);
CPPUNIT_ASSERT_MESSAGE("string comparison case-sensitive by default"s, fooTagValue != fOoTagValue);
CPPUNIT_ASSERT_MESSAGE("case-insensitive string comparison"s, fooTagValue.compareTo(fOoTagValue, TagValueComparisionFlags::CaseInsensitive));
const auto popularity = Popularity{ .user = "some user", .rating = 200, .playCounter = 0 };
const auto first = TagValue(popularity), second = TagValue(popularity);
CPPUNIT_ASSERT_EQUAL_MESSAGE("comparison of equal popularity (string and binary representation)"s, TagValue("some user|200.0"sv), first);
CPPUNIT_ASSERT_EQUAL_MESSAGE("comparison of equal popularity (only binary representation)"s, first, second);
CPPUNIT_ASSERT_MESSAGE("default-popularity not equal to empty tag value"s, TagValue(Popularity()) != TagValue());
CPPUNIT_ASSERT_MESSAGE("popularity not equal"s, first != TagValue(Popularity({ .rating = 200 })));
// meta-data
TagValue withDescription(15);