Add scale info to Popularity for furture extension
This would allow implementing a way to convert between different scales and which in turn would allow the UI to provide an editor with a generic scale (e.g. stars) instead of only allowing to edit raw values as string. This also make it assume that a single number is meant to be the rating (instead of the user). That should make editing the rating a bit more straight forward (if one doesn't care about the user and play counter).
This commit is contained in:
parent
d17f04864d
commit
98d28ede9f
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include "../diagnostics.h"
|
#include "../diagnostics.h"
|
||||||
#include "../exceptions.h"
|
#include "../exceptions.h"
|
||||||
|
#include "../tagtype.h"
|
||||||
|
|
||||||
#include <c++utilities/conversion/stringbuilder.h>
|
#include <c++utilities/conversion/stringbuilder.h>
|
||||||
#include <c++utilities/conversion/stringconversion.h>
|
#include <c++utilities/conversion/stringconversion.h>
|
||||||
|
@ -386,7 +387,7 @@ void Id3v2Frame::parse(BinaryReader &reader, std::uint32_t version, std::uint32_
|
||||||
|
|
||||||
} else if (((version >= 3 && id() == Id3v2FrameIds::lRating) || (version < 3 && id() == Id3v2FrameIds::sRating))) {
|
} else if (((version >= 3 && id() == Id3v2FrameIds::lRating) || (version < 3 && id() == Id3v2FrameIds::sRating))) {
|
||||||
// parse popularimeter frame
|
// parse popularimeter frame
|
||||||
auto popularity = Popularity();
|
auto popularity = Popularity{ .scale = TagType::Id3v2Tag };
|
||||||
auto userEncoding = TagTextEncoding::Latin1;
|
auto userEncoding = TagTextEncoding::Latin1;
|
||||||
auto substr = parseSubstring(buffer.get(), m_dataSize, userEncoding, true, diag);
|
auto substr = parseSubstring(buffer.get(), m_dataSize, userEncoding, true, diag);
|
||||||
auto end = buffer.get() + m_dataSize;
|
auto end = buffer.get() + m_dataSize;
|
||||||
|
|
23
tagvalue.cpp
23
tagvalue.cpp
|
@ -307,7 +307,8 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options
|
||||||
} else if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) {
|
} else if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) {
|
||||||
if (options & TagValueComparisionFlags::CaseInsensitive) {
|
if (options & TagValueComparisionFlags::CaseInsensitive) {
|
||||||
const auto lhs = toPopularity(), rhs = other.toPopularity();
|
const auto lhs = toPopularity(), rhs = other.toPopularity();
|
||||||
return lhs.rating == rhs.rating && lhs.playCounter == rhs.playCounter && compareData(lhs.user, rhs.user, true);
|
return lhs.rating == rhs.rating && lhs.playCounter == rhs.playCounter && lhs.scale == rhs.scale
|
||||||
|
&& compareData(lhs.user, rhs.user, true);
|
||||||
} else {
|
} else {
|
||||||
return toPopularity() == other.toPopularity();
|
return toPopularity() == other.toPopularity();
|
||||||
}
|
}
|
||||||
|
@ -646,6 +647,7 @@ Popularity TagValue::toPopularity() const
|
||||||
popularity.user = reader.readLengthPrefixedString();
|
popularity.user = reader.readLengthPrefixedString();
|
||||||
popularity.rating = reader.readFloat64LE();
|
popularity.rating = reader.readFloat64LE();
|
||||||
popularity.playCounter = reader.readUInt64LE();
|
popularity.playCounter = reader.readUInt64LE();
|
||||||
|
popularity.scale = static_cast<TagType>(reader.readUInt64LE());
|
||||||
} catch (const std::ios_base::failure &) {
|
} catch (const std::ios_base::failure &) {
|
||||||
throw ConversionException(argsToString("Assigned popularity is invalid"));
|
throw ConversionException(argsToString("Assigned popularity is invalid"));
|
||||||
}
|
}
|
||||||
|
@ -1081,6 +1083,7 @@ void TagValue::assignPopularity(const Popularity &value)
|
||||||
writer.writeLengthPrefixedString(value.user);
|
writer.writeLengthPrefixedString(value.user);
|
||||||
writer.writeFloat64LE(value.rating);
|
writer.writeFloat64LE(value.rating);
|
||||||
writer.writeUInt64LE(value.playCounter);
|
writer.writeUInt64LE(value.playCounter);
|
||||||
|
writer.writeUInt64LE(static_cast<std::uint64_t>(value.scale));
|
||||||
auto size = static_cast<std::size_t>(s.tellp());
|
auto size = static_cast<std::size_t>(s.tellp());
|
||||||
auto ptr = std::make_unique<char[]>(size);
|
auto ptr = std::make_unique<char[]>(size);
|
||||||
s.read(ptr.get(), s.tellp());
|
s.read(ptr.get(), s.tellp());
|
||||||
|
@ -1179,12 +1182,13 @@ const TagValue &TagValue::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns the popularity as string in the format "user|rating|play-counter" or an empty
|
* \brief Returns the popularity as string in the format "rating" if only a rating is present
|
||||||
* string if the popularity isEmpty().
|
* or in the format "user|rating|play-counter" or an empty string if the popularity isEmpty().
|
||||||
*/
|
*/
|
||||||
std::string Popularity::toString() const
|
std::string Popularity::toString() const
|
||||||
{
|
{
|
||||||
return isEmpty() ? std::string() : user % '|' % numberToString(rating) % '|' + playCounter;
|
return isEmpty() ? std::string()
|
||||||
|
: ((user.empty() && !playCounter) ? numberToString(rating) : (user % '|' % numberToString(rating) % '|' + playCounter));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -1198,8 +1202,17 @@ Popularity Popularity::fromString(std::string_view str)
|
||||||
if (parts.empty()) {
|
if (parts.empty()) {
|
||||||
return res;
|
return res;
|
||||||
} else if (parts.size() > 3) {
|
} else if (parts.size() > 3) {
|
||||||
throw ConversionException("Wrong format, expected \"user|rating|play-counter\"");
|
throw ConversionException("Wrong format, expected \"rating\" or \"user|rating|play-counter\"");
|
||||||
}
|
}
|
||||||
|
// treat a single number as rating
|
||||||
|
if (parts.size() == 1) {
|
||||||
|
try {
|
||||||
|
res.rating = stringToNumber<decltype(res.rating)>(parts.front());
|
||||||
|
return res;
|
||||||
|
} catch (const ConversionException &) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise, read user, rating and play counter
|
||||||
res.user = parts.front();
|
res.user = parts.front();
|
||||||
if (parts.size() > 1) {
|
if (parts.size() > 1) {
|
||||||
res.rating = stringToNumber<decltype(res.rating)>(parts[1]);
|
res.rating = stringToNumber<decltype(res.rating)>(parts[1]);
|
||||||
|
|
23
tagvalue.h
23
tagvalue.h
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "./localehelper.h"
|
#include "./localehelper.h"
|
||||||
#include "./positioninset.h"
|
#include "./positioninset.h"
|
||||||
|
#include "./tagtype.h"
|
||||||
|
|
||||||
#include <c++utilities/chrono/datetime.h>
|
#include <c++utilities/chrono/datetime.h>
|
||||||
#include <c++utilities/chrono/timespan.h>
|
#include <c++utilities/chrono/timespan.h>
|
||||||
|
@ -75,16 +76,28 @@ struct TAG_PARSER_EXPORT Popularity {
|
||||||
double rating = 0.0;
|
double rating = 0.0;
|
||||||
/// \brief Play counter specific to the user.
|
/// \brief Play counter specific to the user.
|
||||||
std::uint64_t playCounter = 0;
|
std::uint64_t playCounter = 0;
|
||||||
|
/// \brief Specifies the scale used for \a rating by the tag defining that scale.
|
||||||
|
/// \remarks The value TagType::Unspecified is preserved to denote a generic scale is used (no
|
||||||
|
/// conversions to/from a generic scale to tag format specific scales have been implemented at
|
||||||
|
/// this point).
|
||||||
|
TagType scale = TagType::Unspecified;
|
||||||
|
|
||||||
bool isEmpty() const
|
|
||||||
{
|
|
||||||
return user.empty() && rating != 0.0 && !playCounter;
|
|
||||||
}
|
|
||||||
std::string toString() const;
|
std::string toString() const;
|
||||||
static Popularity fromString(std::string_view str);
|
static Popularity fromString(std::string_view str);
|
||||||
|
|
||||||
|
/// \brief Returns whether the Popularity is empty. The \a scale and zero-values don't count.
|
||||||
|
bool isEmpty() const
|
||||||
|
{
|
||||||
|
return user.empty() && rating == 0.0 && !playCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief Returns whether two instances are equal.
|
||||||
|
/// \remarks Currently they must match exactly but in the future conversions between different
|
||||||
|
/// scales might be implemented and two instances would be considered equal if the ratings are
|
||||||
|
/// considered equal (even specified using different scales).
|
||||||
bool operator==(const Popularity &other) const
|
bool operator==(const Popularity &other) const
|
||||||
{
|
{
|
||||||
return playCounter == other.playCounter && rating == other.rating && user == other.user;
|
return playCounter == other.playCounter && rating == other.rating && user == other.user && scale == other.scale;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -179,13 +179,16 @@ void TagValueTests::testDateTime()
|
||||||
|
|
||||||
void TagValueTests::testPopularity()
|
void TagValueTests::testPopularity()
|
||||||
{
|
{
|
||||||
const auto tagValue = TagValue(Popularity{ .user = "foo", .rating = 42, .playCounter = 123 });
|
const auto tagValue = TagValue(Popularity{ .user = "foo", .rating = 42, .playCounter = 123, .scale = TagType::VorbisComment });
|
||||||
const auto popularity = tagValue.toPopularity();
|
const auto popularity = tagValue.toPopularity();
|
||||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (user)", "foo"s, popularity.user);
|
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 (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 popularity (play counter)", std::uint64_t(123), popularity.playCounter);
|
||||||
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to popularity (scale)", TagType::VorbisComment, popularity.scale);
|
||||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to string", "foo|42|123"s, tagValue.toString());
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to string", "foo|42|123"s, tagValue.toString());
|
||||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to string", 42, tagValue.toInteger());
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to string (only rating)", "43"s, TagValue(Popularity{ .rating = 43 }).toString());
|
||||||
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to integer", 42, tagValue.toInteger());
|
||||||
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to unsigned integer", static_cast<std::uint64_t>(42), tagValue.toUnsignedInteger());
|
||||||
CPPUNIT_ASSERT_THROW_MESSAGE(
|
CPPUNIT_ASSERT_THROW_MESSAGE(
|
||||||
"failing conversion to other type", TagValue("foo|bar"sv, TagTextEncoding::Latin1).toPopularity(), ConversionException);
|
"failing conversion to other type", TagValue("foo|bar"sv, TagTextEncoding::Latin1).toPopularity(), ConversionException);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,8 +96,19 @@ template <class StreamType> void VorbisCommentField::internalParse(StreamType &s
|
||||||
throw Failure();
|
throw Failure();
|
||||||
}
|
}
|
||||||
} else if (id().size() + 1 < size) {
|
} else if (id().size() + 1 < size) {
|
||||||
// extract other values (as string)
|
const auto str = std::string_view(data.get() + idSize + 1, size - idSize - 1);
|
||||||
setValue(TagValue(string(data.get() + idSize + 1, size - idSize - 1), TagTextEncoding::Utf8));
|
if (id() == VorbisCommentIds::rating()) {
|
||||||
|
try {
|
||||||
|
// set rating as Popularity to preserve the scale information
|
||||||
|
value().assignPopularity(Popularity{ .rating = stringToNumber<double>(str), .scale = TagType::VorbisComment });
|
||||||
|
} catch (const ConversionException &) {
|
||||||
|
// fallback to text
|
||||||
|
value().assignText(str, TagTextEncoding::Utf8);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// extract other values (as string)
|
||||||
|
value().assignText(str, TagTextEncoding::Utf8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
diag.emplace_back(DiagLevel::Critical, argsToString("Field at ", static_cast<std::streamoff>(stream.tellg()), " is truncated."), context);
|
diag.emplace_back(DiagLevel::Critical, argsToString("Field at ", static_cast<std::streamoff>(stream.tellg()), " is truncated."), context);
|
||||||
|
|
Loading…
Reference in New Issue