Add data type for ID3v2's Popularimeter field
See https://github.com/Martchus/tageditor/issues/84
This commit is contained in:
parent
9511d61371
commit
46014def51
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
109
tagvalue.cpp
109
tagvalue.cpp
|
@ -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
|
||||
|
|
29
tagvalue.h
29
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.
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue