diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ed92e3..b6be78e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index 629c029..b47ed06 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -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; diff --git a/tagvalue.cpp b/tagvalue.cpp index 0d2a279..bac1fa0 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -9,9 +9,12 @@ #include #include #include +#include +#include #include #include +#include #include 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(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(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 &&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(s.tellp()); + auto ptr = std::make_unique(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>(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(parts[1]); + } + if (parts.size() > 2) { + res.playCounter = stringToNumber(parts[2]); + } + return res; +} + } // namespace TagParser diff --git a/tagvalue.h b/tagvalue.h index 18476e7..cdca161 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -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. diff --git a/tests/tagvalue.cpp b/tests/tagvalue.cpp index 6ec2931..5515d72 100644 --- a/tests/tagvalue.cpp +++ b/tests/tagvalue.cpp @@ -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);