Implement parsing popularimeter ID3v2 frame

This commit is contained in:
Martchus 2022-06-18 16:30:21 +02:00
parent f52b2958df
commit 669b054a48
3 changed files with 120 additions and 3 deletions

View File

@ -121,6 +121,23 @@ u16string wideStringFromSubstring(tuple<const char *, size_t, const char *> subs
return res;
}
/*!
* \brief Reads the play counter from the specified range.
*/
static std::uint64_t readPlayCounter(const char *begin, const char *end, const std::string &context, Diagnostics &diag)
{
auto res = std::uint64_t();
auto pos = end - 1;
if (end - begin > 8) {
diag.emplace_back(DiagLevel::Critical, "Play counter is bigger than eight bytes and therefore not supported.", context);
return res;
}
for (auto shift = 0; pos >= begin; shift += 8, --pos) {
res += static_cast<std::uint64_t>(static_cast<std::uint8_t>(*pos)) << shift;
}
return res;
}
/*!
* \brief Parses a frame from the stream read using the specified \a reader.
*
@ -363,6 +380,24 @@ void Id3v2Frame::parse(BinaryReader &reader, std::uint32_t version, std::uint32_
// parse comment frame or unsynchronized lyrics frame (these two frame types have the same structure)
parseComment(buffer.get(), m_dataSize, value(), diag);
} else if (((version >= 3 && id() == Id3v2FrameIds::lRating) || (version < 3 && id() == Id3v2FrameIds::sRating))) {
// parse popularimeter frame
auto popularity = Popularity();
auto userEncoding = TagTextEncoding::Latin1;
auto substr = parseSubstring(buffer.get(), m_dataSize, userEncoding, true, diag);
auto end = buffer.get() + m_dataSize;
if (std::get<1>(substr)) {
popularity.user.assign(std::get<0>(substr), std::get<1>(substr));
}
auto ratingPos = std::get<2>(substr);
if (ratingPos >= end) {
diag.emplace_back(DiagLevel::Critical, "Popularimeter frame is incomplete (rating is missing).", context);
throw TruncatedDataException();
}
popularity.rating = static_cast<std::uint8_t>(*ratingPos);
popularity.playCounter = readPlayCounter(ratingPos + 1, end, context, diag);
value().assignPopularity(popularity);
} else {
// parse unknown/unsupported frame
value().assignData(buffer.get(), m_dataSize, TagDataType::Undefined);
@ -430,6 +465,28 @@ std::string Id3v2Frame::ignoreAdditionalValuesDiagMsg() const
return argsToString("Additional values ", DiagMessage::formatList(TagValue::toStrings(m_additionalValues)), " are supposed to be ignored.");
}
/*!
* \brief Computes the size required to serialize the specified \a playCounter value.
*/
static std::uint32_t computePlayCounterSize(std::uint64_t playCounter)
{
auto res = 4u;
for (playCounter >>= 32; playCounter; playCounter >>= 8, ++res)
; // additional bytes for play counter into account when it is > 0xFFFFFFFF
return res;
}
/*!
* \brief Writes the specified \a playCounter with the specified \a playCounterSize to the buffer specified
* by the \a last address to write the data to.
*/
static void writePlayCounter(char *last, std::uint32_t playCounterSize, std::uint64_t playCounter)
{
for (; playCounter || playCounterSize; playCounter >>= 8, --playCounterSize, --last) {
*last = static_cast<char>(playCounter & 0xFF);
}
}
/*!
* \class TagParser::Id3v2FrameMaker
* \brief The Id3v2FrameMaker class helps making ID3v2 frames.
@ -640,6 +697,36 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, std::uint8_t version, Diagno
// make comment frame or the unsynchronized lyrics frame
m_frame.makeComment(m_data, m_decompressedSize, *values.front(), version, diag);
} else if (((version >= 3 && m_frameId == Id3v2FrameIds::lRating) || (version < 3 && m_frameId == Id3v2FrameIds::sRating))) {
// make popularimeter frame
auto popularity = Popularity();
try {
popularity = values.front()->toPopularity();
} catch (const ConversionException &) {
diag.emplace_back(DiagLevel::Warning,
argsToString(
"The popularity \"", values.front()->toDisplayString(), "\" is not of the expected form, eg. \"user|rating|counter\"."),
context);
}
// -> clamp rating
if (popularity.rating > 0xFF) {
popularity.rating = 0xFF;
diag.emplace_back(DiagLevel::Warning, argsToString("The rating has been clamped to 255."), context);
} else if (popularity.rating < 0x00) {
popularity.rating = 0x00;
diag.emplace_back(DiagLevel::Warning, argsToString("The rating has been clamped to 0."), context);
}
// -> compute size: user name length + termination + rating byte
m_decompressedSize = static_cast<std::uint32_t>(popularity.user.size() + 2);
const auto playCounterSize = computePlayCounterSize(popularity.playCounter);
m_decompressedSize += playCounterSize;
// -> copy data into buffer
m_data = make_unique<char[]>(m_decompressedSize);
auto pos = popularity.user.size() + 1;
std::memcpy(m_data.get(), popularity.user.data(), pos);
m_data[pos] = static_cast<char>(popularity.rating);
writePlayCounter(m_data.get() + pos + playCounterSize, playCounterSize, popularity.playCounter);
} else {
// make unknown frame
const auto &value(*values.front());
@ -648,7 +735,7 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, std::uint8_t version, Diagno
throw InvalidDataException();
}
m_data = make_unique<char[]>(m_decompressedSize = static_cast<std::uint32_t>(value.dataSize()));
copy(value.dataPointer(), value.dataPointer() + m_decompressedSize, m_data.get());
std::memcpy(m_data.get(), value.dataPointer(), m_decompressedSize);
}
} catch (const ConversionException &) {
try {

View File

@ -332,6 +332,30 @@ void TagValue::clearMetadata()
m_type = TagDataType::Undefined;
}
/*!
* \brief Returns a "display string" for the specified value.
* \returns
* - Returns the just the type if no displayable string can be made of it, eg. "picture", otherwise
* returns the string representation.
* - Returns "invalid …" if a conversion error returned when making the string representation but
* never throws a ConversionException (unlike toString()).
*/
std::string TagParser::TagValue::toDisplayString() const
{
switch (m_type) {
case TagDataType::Undefined:
case TagDataType::Binary:
case TagDataType::Picture:
return std::string(tagDataTypeString(m_type));
default:
try {
return toString(TagTextEncoding::Utf8);
} catch (const ConversionException &e) {
return argsToString("invalid ", tagDataTypeString(m_type), ':', ' ', e.what());
}
}
}
/*!
* \brief Converts the value of the current TagValue object to its equivalent
* integer representation.
@ -1050,11 +1074,12 @@ const TagValue &TagValue::empty()
}
/*!
* \brief Returns the popularity as string in the format "user|rating|play-counter".
* \brief Returns the popularity as string in the format "user|rating|play-counter" or an empty
* string if the popularity isEmpty().
*/
std::string Popularity::toString() const
{
return user % '|' % numberToString(rating) % '|' + playCounter;
return isEmpty() ? std::string() : user % '|' % numberToString(rating) % '|' + playCounter;
}
/*!

View File

@ -76,6 +76,10 @@ struct TAG_PARSER_EXPORT Popularity {
/// \brief Play counter specific to the user.
std::uint64_t playCounter = 0;
bool isEmpty() const
{
return user.empty() && rating != 0.0 && !playCounter;
}
std::string toString() const;
static Popularity fromString(std::string_view str);
bool operator==(const Popularity &other) const
@ -149,6 +153,7 @@ public:
void clearDataAndMetadata();
TagDataType type() const;
std::string toString(TagTextEncoding encoding = TagTextEncoding::Unspecified) const;
std::string toDisplayString() const;
void toString(std::string &result, TagTextEncoding encoding = TagTextEncoding::Unspecified) const;
std::u16string toWString(TagTextEncoding encoding = TagTextEncoding::Unspecified) const;
void toWString(std::u16string &result, TagTextEncoding encoding = TagTextEncoding::Unspecified) const;