#include "./tagvalue.h" #include "./caseinsensitivecomparer.h" #include "./tag.h" #include "./id3/id3genres.h" #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace CppUtilities; namespace TagParser { /// \brief The TagValuePrivate struct contains private fields of the TagValue class. struct TagValuePrivate {}; /*! * \brief Returns the string representation of the specified \a dataType. */ std::string_view tagDataTypeString(TagDataType dataType) { switch (dataType) { case TagDataType::Text: return "text"; case TagDataType::Integer: return "integer"; case TagDataType::PositionInSet: return "position in set"; case TagDataType::StandardGenreIndex: return "genre index"; case TagDataType::TimeSpan: return "time span"; case TagDataType::DateTime: return "date time"; case TagDataType::Picture: return "picture"; case TagDataType::Binary: return "binary"; case TagDataType::Popularity: return "popularity"; case TagDataType::UnsignedInteger: return "unsigned integer"; case TagDataType::DateTimeExpression: return "date time expression"; default: return "undefined"; } } /*! * \brief Returns the encoding parameter (name of the character set and bytes per character) for the specified \a tagTextEncoding. */ pair encodingParameter(TagTextEncoding tagTextEncoding) { switch (tagTextEncoding) { case TagTextEncoding::Latin1: return make_pair("ISO-8859-1", 1.0f); case TagTextEncoding::Utf8: return make_pair("UTF-8", 1.0f); case TagTextEncoding::Utf16LittleEndian: return make_pair("UTF-16LE", 2.0f); case TagTextEncoding::Utf16BigEndian: return make_pair("UTF-16BE", 2.0f); default: return make_pair(nullptr, 0.0f); } } /*! * \class TagParser::Popularity * \brief The Popularity class contains a value for ID3v2's "Popularimeter" field. * \remarks It can also be used for other formats than ID3v2. * \sa See documentation of TagParser::Popularity for scaling. */ /*! * \class TagParser::TagValue * \brief The TagValue class wraps values of different types. It is meant to be assigned to a tag field. * * For a list of supported types see TagParser::TagDataType. * * When constructing a TagValue choose the type which suites the value you want to store best. If the * tag format uses a different type the serializer will take care of the neccassary conversion (eg. convert * an integer to a string). * * When consuming a TagValue read from a tag one should not expect that a particular type is used. The * type depends on what the particular tag format uses. However, the conversion functions provided by the * TagValue class take care of neccassary conversions, eg. TagValue::toInteger() will attempt to convert a * string to a number (an possibly throw a ConversionException on failure). * * Values of the type TagDataType::Text can be differently encoded. * - See TagParser::TagTextEncoding for a list of encodings supported by this library. * - Tag formats usually only support a subset of these encodings. The serializers for the various tag * formats provided by this library will keep the encoding if possible and otherwise convert the assigned * text to an encoding supported by the tag format on the fly. Note that ID3v1 does not specify which * encodings are supported (or unsupported) so the serializer will just write text data as-is. * - The deserializers will store text data in the encoding that is used in the tag. * - The functions Tag::canEncodingBeUsed() and Tag::proposedTextEncoding() can be used to check * whether an encoding can be used by a certain tag format to avoid any unnecessary character set * conversions. * - There's also the function Tag::ensureTextValuesAreProperlyEncoded() which can be used to convert all * text values currently assigned to a tag to the encoding which is deemed best for the current tag format. * This function is a bit more agressive than the implict conversions, e.g. it ensures no UTF-16 encoded * text ends up in ID3v1 tags. * - If you want to use UTF-8 everywhere, simply always assign UTF-8 text and use * TagValue::toString(TagTextEncoding::Utf8) when reading text. * * Values of the type TagDataType::Popularity might use different rating scales depending on the tag * format. * - You can assign a Popularity object of any scale. Tag implementations will convert it accordingly. * - You can use TagValue::toScaledPopularity() to retrieve a Popularity object of the desired scale. * - When just working with text data (via TagValue::toString() and TagValue::assignText()), no scaling * of internally assigned Popularity objects is done; so you're working with raw rating values in this * case. * * Values of the type TagDataType::Text are not supposed to contain Byte-Order-Marks. Before assigning text * which might be prepended by a Byte-Order-Mark the helper function TagValue::stripBom() can be used. */ /*! * \brief Constructs a new TagValue holding a copy of the given TagValue instance. * \param other Specifies another TagValue instance. */ TagValue::TagValue(const TagValue &other) : m_size(other.m_size) , m_desc(other.m_desc) , m_mimeType(other.m_mimeType) , m_locale(other.m_locale) , m_type(other.m_type) , m_encoding(other.m_encoding) , m_descEncoding(other.m_descEncoding) , m_flags(TagValueFlags::None) { if (!other.isEmpty()) { m_ptr = make_unique(m_size); std::copy(other.m_ptr.get(), other.m_ptr.get() + other.m_size, m_ptr.get()); } } TagValue::TagValue(TagValue &&other) = default; /*! * \brief Constructs an empty TagValue. */ TagValue::TagValue() : m_size(0) , m_type(TagDataType::Undefined) , m_encoding(TagTextEncoding::Latin1) , m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { } /*! * \brief Constructs a new TagValue holding a copy of the given \a text. * \param text Specifies the text to be assigned. * \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.) * \param textEncoding Specifies the encoding of the given \a text. * \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to * use \a textEncoding without any character set conversions. * \throws Throws a ConversionException if the conversion the specified character set fails. * \remarks Strips the BOM of the specified \a text. */ TagValue::TagValue(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo) : m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { assignText(text, textSize, textEncoding, convertTo); } /*! * \brief Constructs a new TagValue holding a copy of the given \a text. * \param text Specifies the text to be assigned. This string must be null-terminated. * \param textEncoding Specifies the encoding of the given \a text. * \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to * use \a textEncoding without any character set conversions. * \throws Throws a ConversionException if the conversion the specified character set fails. * \remarks Strips the BOM of the specified \a text. */ TagValue::TagValue(const char *text, TagTextEncoding textEncoding, TagTextEncoding convertTo) { assignText(text, std::strlen(text), textEncoding, convertTo); } /*! * \brief Constructs a new TagValue holding a copy of the given \a text. * \param text Specifies the text to be assigned. * \param textEncoding Specifies the encoding of the given \a text. * \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to * use \a textEncoding without any character set conversions. * \throws Throws a ConversionException if the conversion the specified character set fails. * \remarks Strips the BOM of the specified \a text. */ TagValue::TagValue(const std::string &text, TagTextEncoding textEncoding, TagTextEncoding convertTo) : m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { assignText(text, textEncoding, convertTo); } /*! * \brief Constructs a new TagValue holding a copy of the given \a text. * \param text Specifies the text to be assigned. * \param textEncoding Specifies the encoding of the given \a text. * \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to * use \a textEncoding without any character set conversions. * \throws Throws a ConversionException if the conversion the specified character set fails. * \remarks Strips the BOM of the specified \a text. */ TagValue::TagValue(std::string_view text, TagTextEncoding textEncoding, TagTextEncoding convertTo) : m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { assignText(text, textEncoding, convertTo); } /*! * \brief Destroys the TagValue. */ TagValue::~TagValue() { } /*! * \brief Constructs a new TagValue with a copy of the given \a data. * * \param data Specifies a pointer to the data. * \param length Specifies the length of the data. * \param type Specifies the type of the data as TagDataType. * \param encoding Specifies the encoding of the data as TagTextEncoding. The * encoding will only be considered if a text is assigned. * \remarks Strips the BOM of the specified \a data if \a type is TagDataType::Text. */ TagValue::TagValue(const char *data, std::size_t length, TagDataType type, TagTextEncoding encoding) : m_size(length) , m_type(type) , m_encoding(encoding) , m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { if (length) { if (type == TagDataType::Text) { stripBom(data, m_size, encoding); } m_ptr = std::make_unique(m_size); std::copy(data, data + m_size, m_ptr.get()); } } /*! * \brief Constructs a new TagValue holding with the given \a data. * * The \a data is not copied. It is moved. * * \param data Specifies a pointer to the data. * \param length Specifies the length of the data. * \param type Specifies the type of the data as TagDataType. * \param encoding Specifies the encoding of the data as TagTextEncoding. The * encoding will only be considered if a text is assigned. * \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present. */ TagValue::TagValue(std::unique_ptr &&data, std::size_t length, TagDataType type, TagTextEncoding encoding) : m_size(length) , m_type(type) , m_encoding(encoding) , m_descEncoding(TagTextEncoding::Latin1) , m_flags(TagValueFlags::None) { if (length) { m_ptr = std::move(data); } } /*! * \brief Assigns the value of another TagValue to the current instance. */ TagValue &TagValue::operator=(const TagValue &other) { if (this == &other) { return *this; } m_size = other.m_size; m_type = other.m_type; m_desc = other.m_desc; m_mimeType = other.m_mimeType; m_locale = other.m_locale; m_flags = other.m_flags; m_encoding = other.m_encoding; m_descEncoding = other.m_descEncoding; if (other.isEmpty()) { m_ptr.reset(); } else { m_ptr = make_unique(m_size); std::copy(other.m_ptr.get(), other.m_ptr.get() + other.m_size, m_ptr.get()); } return *this; } TagValue &TagValue::operator=(TagValue &&other) = default; /// \cond TagTextEncoding pickUtfEncoding(TagTextEncoding encoding1, TagTextEncoding encoding2) { switch (encoding1) { case TagTextEncoding::Utf8: case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: return encoding1; default: switch (encoding2) { case TagTextEncoding::Utf8: case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: return encoding2; default:; } } return TagTextEncoding::Utf8; } /// \endcond /*! * \brief Returns whether both instances are equal. Meta-data like description and MIME-type is taken into * account as well. * \arg other Specifies the other instance. * \arg options Specifies options to alter the behavior. See TagValueComparisionFlags for details. * \remarks * - If the data types are not equal, two instances are still considered equal if the string representation * is identical. For instance the text "2" is considered equal to the integer 2. This also means that an empty * TagValue and the integer 0 are *not* considered equal. * - The choice to allow implicit conversions was made because different tag formats use different types and * usually one does not care about those internals when comparing values. * - If any of the differently typed values can not be converted to a string (eg. it is binary data) the values * are *not* considered equal. So the text "foo" and the binary value "foo" are not considered equal although * the raw data is identical. * - If the type is TagDataType::Text and the encoding differs values might still be considered equal if they * represent the same characters. The same counts for the description. * - This might be a costly operation due to possible conversions. * \sa * - TagValue::compareData() to compare raw data without any conversions * - TagValueTests::testEqualityOperator() for examples */ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options) const { // check whether meta-data is equal (except description) if (!(options & TagValueComparisionFlags::IgnoreMetaData)) { // check meta-data which always uses UTF-8 (everything but description) if (m_mimeType != other.m_mimeType || m_locale != other.m_locale || m_flags != other.m_flags) { return false; } // check description which might use different encodings if (m_descEncoding == other.m_descEncoding || m_descEncoding == TagTextEncoding::Unspecified || other.m_descEncoding == TagTextEncoding::Unspecified || m_desc.empty() || other.m_desc.empty()) { if (!compareData(m_desc, other.m_desc, options & TagValueComparisionFlags::CaseInsensitive)) { return false; } } else { const auto utfEncodingToUse = pickUtfEncoding(m_descEncoding, other.m_descEncoding); StringData str1, str2; const char *data1, *data2; size_t size1, size2; if (m_descEncoding != utfEncodingToUse) { const auto inputParameter = encodingParameter(m_descEncoding), outputParameter = encodingParameter(utfEncodingToUse); str1 = convertString( inputParameter.first, outputParameter.first, m_desc.data(), m_desc.size(), outputParameter.second / inputParameter.second); data1 = str1.first.get(); size1 = str1.second; } else { data1 = m_desc.data(); size1 = m_desc.size(); } if (other.m_descEncoding != utfEncodingToUse) { const auto inputParameter = encodingParameter(other.m_descEncoding), outputParameter = encodingParameter(utfEncodingToUse); str2 = convertString(inputParameter.first, outputParameter.first, other.m_desc.data(), other.m_desc.size(), outputParameter.second / inputParameter.second); data2 = str2.first.get(); size2 = str2.second; } else { data2 = other.m_desc.data(); size2 = other.m_desc.size(); } if (!compareData(data1, size1, data2, size2, options & TagValueComparisionFlags::CaseInsensitive)) { return false; } } } try { // check for equality if both types are identical if (m_type == other.m_type) { switch (m_type) { case TagDataType::Text: { // compare raw data directly if the encoding is the same if (m_size != other.m_size && m_encoding == other.m_encoding) { return false; } if (m_encoding == other.m_encoding || m_encoding == TagTextEncoding::Unspecified || other.m_encoding == TagTextEncoding::Unspecified) { return compareData(other, options & TagValueComparisionFlags::CaseInsensitive); } // compare UTF-8 or UTF-16 representation of strings avoiding unnecessary conversions const auto utfEncodingToUse = pickUtfEncoding(m_encoding, other.m_encoding); string str1, str2; const char *data1, *data2; size_t size1, size2; if (m_encoding != utfEncodingToUse) { str1 = toString(utfEncodingToUse); data1 = str1.data(); size1 = str1.size(); } else { data1 = m_ptr.get(); size1 = m_size; } if (other.m_encoding != utfEncodingToUse) { str2 = other.toString(utfEncodingToUse); data2 = str2.data(); size2 = str2.size(); } else { data2 = other.m_ptr.get(); size2 = other.m_size; } return compareData(data1, size1, data2, size2, options & TagValueComparisionFlags::CaseInsensitive); } case TagDataType::PositionInSet: return toPositionInSet() == other.toPositionInSet(); case TagDataType::StandardGenreIndex: return toStandardGenreIndex() == other.toStandardGenreIndex(); case TagDataType::TimeSpan: return toTimeSpan() == other.toTimeSpan(); case TagDataType::DateTime: return toDateTime() == other.toDateTime(); case TagDataType::DateTimeExpression: return toDateTimeExpression() == other.toDateTimeExpression(); case TagDataType::Picture: case TagDataType::Binary: case TagDataType::Undefined: return compareData(other); default:; } } // do not attempt implicit conversions for certain types // TODO: Maybe it would actually make sense for some of these types (at least when the other type is // string)? for (const auto dataType : { m_type, other.m_type }) { switch (dataType) { case TagDataType::TimeSpan: case TagDataType::DateTime: case TagDataType::DateTimeExpression: case TagDataType::Picture: case TagDataType::Binary: case TagDataType::Undefined: return false; default:; } } // handle types where an implicit conversion to the specific type can be done if (m_type == TagDataType::Integer || other.m_type == TagDataType::Integer) { return toInteger() == other.toInteger(); } else if (m_type == TagDataType::UnsignedInteger || other.m_type == TagDataType::UnsignedInteger) { return toUnsignedInteger() == other.toUnsignedInteger(); } else if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) { if (options & TagValueComparisionFlags::CaseInsensitive) { const auto lhs = toPopularity(), rhs = other.toPopularity(); return lhs.rating == rhs.rating && lhs.playCounter == rhs.playCounter && lhs.scale == rhs.scale && compareData(lhs.user, rhs.user, true); } else { return toPopularity() == other.toPopularity(); } } // handle other types where an implicit conversion to string can be done by comparing the string representation return compareData(toString(), other.toString(m_encoding), options & TagValueComparisionFlags::CaseInsensitive); } catch (const ConversionException &) { return false; } } /*! * \brief Wipes assigned meta data. * - Clears description, mime type, language and flags. * - Resets the encoding to TagTextEncoding::Latin1. * - Resets the data type to TagDataType::Undefined. */ void TagValue::clearMetadata() { m_desc.clear(); m_mimeType.clear(); m_locale.clear(); m_flags = TagValueFlags::None; m_encoding = TagTextEncoding::Latin1; m_descEncoding = TagTextEncoding::Latin1; 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. * \throws Throws ConversionException on failure. */ std::int32_t TagValue::toInteger() const { if (isEmpty()) { return 0; } switch (m_type) { case TagDataType::Text: switch (m_encoding) { case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: { auto u16str = u16string(reinterpret_cast(m_ptr.get()), m_size / 2); ensureHostByteOrder(u16str, m_encoding); return stringToNumber(u16str); } default: return bufferToNumber(m_ptr.get(), m_size); } case TagDataType::PositionInSet: if (m_size == sizeof(PositionInSet)) { return *reinterpret_cast(m_ptr.get()); } throw ConversionException("Can not convert assigned data to integer because the data size is not appropriate."); case TagDataType::Integer: case TagDataType::StandardGenreIndex: if (m_size == sizeof(std::int32_t)) { return *reinterpret_cast(m_ptr.get()); } throw ConversionException("Can not convert assigned data to integer because the data size is not appropriate."); case TagDataType::Popularity: return static_cast(toPopularity().rating); case TagDataType::UnsignedInteger: { const auto unsignedInteger = toUnsignedInteger(); if (unsignedInteger > std::numeric_limits::max()) { throw ConversionException(argsToString("Unsigned integer too big for conversion to integer.")); } return static_cast(unsignedInteger); } default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to integer.")); } } std::uint64_t TagValue::toUnsignedInteger() const { if (isEmpty()) { return 0; } switch (m_type) { case TagDataType::Text: switch (m_encoding) { case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: { auto u16str = u16string(reinterpret_cast(m_ptr.get()), m_size / 2); ensureHostByteOrder(u16str, m_encoding); return stringToNumber(u16str); } default: return bufferToNumber(m_ptr.get(), m_size); } case TagDataType::PositionInSet: case TagDataType::Integer: case TagDataType::StandardGenreIndex: { const auto integer = toInteger(); if (integer < 0) { throw ConversionException(argsToString("Can not convert negative value to unsigned integer.")); } return static_cast(integer); } case TagDataType::Popularity: return static_cast(toPopularity().rating); case TagDataType::UnsignedInteger: if (m_size == sizeof(std::uint64_t)) { return *reinterpret_cast(m_ptr.get()); } throw ConversionException("Can not convert assigned data to unsigned integer because the data size is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to integer.")); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * standard genre index. * \throws Throws ConversionException on failure. */ int TagValue::toStandardGenreIndex() const { if (isEmpty()) { return Id3Genres::emptyGenreIndex(); } int index = 0; switch (m_type) { case TagDataType::Text: { try { index = toInteger(); } catch (const ConversionException &) { TagTextEncoding encoding = TagTextEncoding::Utf8; if (m_encoding == TagTextEncoding::Latin1) { // no need to convert Latin-1 to UTF-8 (makes no difference in case of genre strings) encoding = TagTextEncoding::Unspecified; } index = Id3Genres::indexFromString(toString(encoding)); } break; } case TagDataType::StandardGenreIndex: case TagDataType::Integer: case TagDataType::UnsignedInteger: if (m_size == sizeof(std::int32_t)) { index = static_cast(*reinterpret_cast(m_ptr.get())); } else if (m_size == sizeof(std::uint64_t)) { const auto unsignedInt = *reinterpret_cast(m_ptr.get()); if (unsignedInt <= std::numeric_limits::max()) { index = static_cast(unsignedInt); } else { index = Id3Genres::genreCount(); } } else { throw ConversionException("The assigned index/integer is of unappropriate size."); } break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to genre index.")); } if (!Id3Genres::isEmptyGenre(index) && !Id3Genres::isIndexSupported(index)) { throw ConversionException("The assigned number is not a valid standard genre index."); } return index; } /*! * \brief Converts the value of the current TagValue object to its equivalent * PositionInSet representation. * \throws Throws ConversionException on failure. */ PositionInSet TagValue::toPositionInSet() const { if (isEmpty()) { return PositionInSet(); } switch (m_type) { case TagDataType::Text: switch (m_encoding) { case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: { auto u16str = u16string(reinterpret_cast(m_ptr.get()), m_size / 2); ensureHostByteOrder(u16str, m_encoding); return PositionInSet(u16str); } default: return PositionInSet(string(m_ptr.get(), m_size)); } case TagDataType::Integer: case TagDataType::PositionInSet: switch (m_size) { case sizeof(std::int32_t): return PositionInSet(*(reinterpret_cast(m_ptr.get()))); case 2 * sizeof(std::int32_t): return PositionInSet( *(reinterpret_cast(m_ptr.get())), *(reinterpret_cast(m_ptr.get() + sizeof(std::int32_t)))); default: throw ConversionException("The size of the assigned data is not appropriate."); } case TagDataType::UnsignedInteger: switch (m_size) { case sizeof(std::uint64_t): { const auto track = *(reinterpret_cast(m_ptr.get())); if (track < std::numeric_limits::max()) { return PositionInSet(static_cast(track)); } } default:; } throw ConversionException("The size of the assigned data is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to position in set.")); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * TimeSpan representation. * \throws Throws ConversionException on failure. */ TimeSpan TagValue::toTimeSpan() const { if (isEmpty()) { return TimeSpan(); } switch (m_type) { case TagDataType::Text: return TimeSpan::fromString(toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1)); case TagDataType::Integer: case TagDataType::TimeSpan: switch (m_size) { case sizeof(std::int32_t): return TimeSpan(*(reinterpret_cast(m_ptr.get()))); case sizeof(std::int64_t): return TimeSpan(*(reinterpret_cast(m_ptr.get()))); default: throw ConversionException("The size of the assigned data is not appropriate for conversion to time span."); } case TagDataType::UnsignedInteger: switch (m_size) { case sizeof(std::uint64_t): { const auto ticks = *(reinterpret_cast(m_ptr.get())); if (ticks < static_cast(std::numeric_limits::max())) { return TimeSpan(static_cast(ticks)); } } default:; } throw ConversionException("The size of the assigned data is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to time span.")); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * DateTime representation (using the UTC timezone). * \throws Throws ConversionException on failure. */ DateTime TagValue::toDateTime() const { if (isEmpty()) { return DateTime(); } switch (m_type) { case TagDataType::Text: { const auto str = toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1); try { return DateTime::fromIsoStringGmt(str.data()); } catch (const ConversionException &) { return DateTime::fromString(str); } } case TagDataType::Integer: case TagDataType::DateTime: case TagDataType::UnsignedInteger: if (m_size == sizeof(std::int32_t)) { return DateTime(*(reinterpret_cast(m_ptr.get()))); } else if (m_size == sizeof(std::uint64_t)) { return DateTime(*(reinterpret_cast(m_ptr.get()))); } else { throw ConversionException("The size of the assigned data is not appropriate for conversion to date time."); } case TagDataType::DateTimeExpression: return toDateTimeExpression().gmt(); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time.")); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * DateTimeExpression representation. * \throws Throws ConversionException on failure. */ CppUtilities::DateTimeExpression TagParser::TagValue::toDateTimeExpression() const { if (isEmpty()) { return DateTimeExpression(); } switch (m_type) { case TagDataType::Text: { const auto str = toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1); try { return DateTimeExpression::fromIsoString(str.data()); } catch (const ConversionException &) { return DateTimeExpression::fromString(str.data()); } } case TagDataType::Integer: case TagDataType::DateTime: case TagDataType::UnsignedInteger: return DateTimeExpression{ .value = toDateTime(), .delta = TimeSpan(), .parts = DateTimeParts::DateTime }; case TagDataType::DateTimeExpression: if (m_size == sizeof(DateTimeExpression)) { return *reinterpret_cast(m_ptr.get()); } else { throw ConversionException("The size of the assigned data is not appropriate for conversion to date time expression."); } default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time.")); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * Popularity representation. * \throws Throws ConversionException on failure. * \remarks * - If text is assigned, the returned popularity's scale will always be TagType::Unspecified * as the text representation does not preserve the scale. Assign the correct scale if needed * manually. Note that tag field implementations provided by this library take care to assign a * popularity (and not just text) when parsing the popularity/rating fields to preserve the * scale information. * - Use TagValue::toScaledPopularity() if you want to convert the rating to a certain scale (to * use that scale consistently without having to deal with multiple scales yourself). */ 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); #if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION) s.rdbuf()->pubsetbuf(m_ptr.get(), static_cast(m_size)); #else s.write(m_ptr.get(), static_cast(m_size)); #endif popularity.user = reader.readLengthPrefixedString(); popularity.rating = reader.readFloat64LE(); popularity.playCounter = reader.readUInt64LE(); popularity.scale = static_cast(reader.readUInt64LE()); } catch (const std::ios_base::failure &) { throw ConversionException(argsToString("Assigned popularity is invalid")); } break; } case TagDataType::UnsignedInteger: popularity.rating = static_cast(toUnsignedInteger()); break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time.")); } return popularity; } /*! * \brief Converts the value of the current TagValue object to its equivalent * Popularity representation using the specified \a scale. * \throws Throws ConversionException on failure, e.g. when Popularity::scaleTo() fails. * \remarks * 1. See Popularity::scaleTo() for details about scaling. * 2. If text is assigned, it is converted like with TagValue::toPopularity(). However, * the specified \a a scale is *assigned* as the popularity's scale assuming that the * text representation already contains a rating with the desired \a scale. That means * if you assign text to a TagValue, the tag implementations (which use this function * internally) will use that text as-is when serializing the popularity/rating field. * 3. Since TagValue::toString() also does not do any scaling the previous point means that * if you only ever use TagValue::assignText() (or equivalent c'tors) and TagValue::toString() * you will always work with raw rating values consistently. * 4. Since tag implementations provided by this library always take care to assign the * popularity/rating as such (and not just as text) you do not need to care about point 2. if * you want to use a certain scale consistently. Just call this function with the desired scal * when reading and assign a popularity object with that scale before saving changes. */ Popularity TagValue::toScaledPopularity(TagType scale) const { auto popularity = toPopularity(); if (m_type == TagDataType::Text) { popularity.scale = scale; } else if (!popularity.scaleTo(scale)) { throw ConversionException(argsToString("Assigned popularity cannot be scaled accordingly")); } return popularity; } /*! * \brief Converts the currently assigned text value to the specified \a encoding. * \throws Throws CppUtilities::ConversionException() if the conversion fails. * \remarks * - Does nothing if dataEncoding() equals \a encoding. * - Sets dataEncoding() to the specified \a encoding if the conversion succeeds. * - Does not do any conversion if the current type() is not TagDataType::Text. * \sa convertDataEncodingForTag() */ void TagValue::convertDataEncoding(TagTextEncoding encoding) { if (m_encoding == encoding) { return; } if (type() == TagDataType::Text) { StringData encodedData; switch (encoding) { case TagTextEncoding::Utf8: // use pre-defined methods when encoding to UTF-8 switch (dataEncoding()) { case TagTextEncoding::Latin1: encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16LittleEndian: encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16BigEndian: encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size); break; default:; } break; default: { // otherwise, determine input and output parameter to use general covertString method const auto inputParameter = encodingParameter(dataEncoding()); const auto outputParameter = encodingParameter(encoding); encodedData = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second); } } // can't just move the encoded data because it needs to be deleted with free m_ptr = make_unique(m_size = encodedData.second); copy(encodedData.first.get(), encodedData.first.get() + encodedData.second, m_ptr.get()); } m_encoding = encoding; } /*! * \brief Ensures the encoding of the currently assigned text value is supported by the specified \a tag. * \sa This is a convenience method for convertDataEncoding(). */ void TagValue::convertDataEncodingForTag(const Tag *tag) { if (type() == TagDataType::Text && !tag->canEncodingBeUsed(dataEncoding())) { convertDataEncoding(tag->proposedTextEncoding()); } } /*! * \brief Converts the assigned description to use the specified \a encoding. */ void TagValue::convertDescriptionEncoding(TagTextEncoding encoding) { if (encoding == m_descEncoding) { return; } if (m_desc.empty()) { m_descEncoding = encoding; return; } StringData encodedData; switch (encoding) { case TagTextEncoding::Utf8: // use pre-defined methods when encoding to UTF-8 switch (descriptionEncoding()) { case TagTextEncoding::Latin1: encodedData = convertLatin1ToUtf8(m_desc.data(), m_desc.size()); break; case TagTextEncoding::Utf16LittleEndian: encodedData = convertUtf16LEToUtf8(m_desc.data(), m_desc.size()); break; case TagTextEncoding::Utf16BigEndian: encodedData = convertUtf16BEToUtf8(m_desc.data(), m_desc.size()); break; default:; } break; default: { // otherwise, determine input and output parameter to use general covertString method const auto inputParameter = encodingParameter(m_descEncoding); const auto outputParameter = encodingParameter(encoding); encodedData = convertString( inputParameter.first, outputParameter.first, m_desc.data(), m_desc.size(), outputParameter.second / inputParameter.second); } } m_desc.assign(encodedData.first.get(), encodedData.second); m_descEncoding = encoding; } /*! * \brief Converts the value of the current TagValue object to its equivalent * std::string representation. * \param result Specifies the string to store the result. * \param encoding Specifies the encoding to to be used; set to TagTextEncoding::Unspecified to use the * present encoding without any character set conversion. * \remarks * - Not all types can be converted to a string, eg. TagDataType::Picture, TagDataType::Binary and * TagDataType::Unspecified will always fail to convert. * - If UTF-16 is the desired output \a encoding, it makes sense to use TagValue::toWString() instead. * - If a popularity is assigned, its string representation is returned without further scaling. * \throws Throws ConversionException on failure. */ void TagValue::toString(string &result, TagTextEncoding encoding) const { if (isEmpty()) { result.clear(); return; } switch (m_type) { case TagDataType::Text: if (encoding == TagTextEncoding::Unspecified || dataEncoding() == TagTextEncoding::Unspecified || encoding == dataEncoding()) { result.assign(m_ptr.get(), m_size); } else { StringData encodedData; switch (encoding) { case TagTextEncoding::Utf8: // use pre-defined methods when encoding to UTF-8 switch (dataEncoding()) { case TagTextEncoding::Latin1: encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16LittleEndian: encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16BigEndian: encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size); break; default:; } break; default: { // otherwise, determine input and output parameter to use general covertString method const auto inputParameter = encodingParameter(dataEncoding()); const auto outputParameter = encodingParameter(encoding); encodedData = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second); } } result.assign(encodedData.first.get(), encodedData.second); } return; case TagDataType::Integer: result = numberToString(toInteger()); break; case TagDataType::PositionInSet: result = toPositionInSet().toString(); break; case TagDataType::StandardGenreIndex: { const auto genreIndex = toInteger(); if (Id3Genres::isEmptyGenre(genreIndex)) { result.clear(); } else if (const auto genreName = Id3Genres::stringFromIndex(genreIndex); !genreName.empty()) { result.assign(genreName); } else { throw ConversionException("No string representation for the assigned standard genre index available."); } break; } case TagDataType::TimeSpan: result = toTimeSpan().toString(); break; case TagDataType::DateTime: result = toDateTime().toIsoString(); break; case TagDataType::Popularity: result = toPopularity().toString(); break; case TagDataType::UnsignedInteger: result = numberToString(toUnsignedInteger()); break; case TagDataType::DateTimeExpression: result = toDateTimeExpression().toIsoString(); break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string.")); } if (encoding == TagTextEncoding::Utf16LittleEndian || encoding == TagTextEncoding::Utf16BigEndian) { auto encodedData = encoding == TagTextEncoding::Utf16LittleEndian ? convertUtf8ToUtf16LE(result.data(), result.size()) : convertUtf8ToUtf16BE(result.data(), result.size()); result.assign(encodedData.first.get(), encodedData.second); } } /*! * \brief Converts the value of the current TagValue object to its equivalent * std::u16string representation. * \throws Throws ConversionException on failure. * \remarks * - Not all types can be converted to a string, eg. TagDataType::Picture, TagDataType::Binary and * TagDataType::Unspecified will always fail to convert. * - Use this only, if \a encoding is an UTF-16 encoding. * \sa toString() */ void TagValue::toWString(std::u16string &result, TagTextEncoding encoding) const { if (isEmpty()) { result.clear(); return; } string regularStrRes; switch (m_type) { case TagDataType::Text: if (encoding == TagTextEncoding::Unspecified || encoding == dataEncoding()) { result.assign(reinterpret_cast(m_ptr.get()), m_size / sizeof(char16_t)); } else { StringData encodedData; switch (encoding) { case TagTextEncoding::Utf8: // use pre-defined methods when encoding to UTF-8 switch (dataEncoding()) { case TagTextEncoding::Latin1: encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16LittleEndian: encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size); break; case TagTextEncoding::Utf16BigEndian: encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size); break; default:; } break; default: { // otherwise, determine input and output parameter to use general covertString method const auto inputParameter = encodingParameter(dataEncoding()); const auto outputParameter = encodingParameter(encoding); encodedData = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second); } } result.assign(reinterpret_cast(encodedData.first.get()), encodedData.second / sizeof(char16_t)); } return; case TagDataType::Integer: regularStrRes = numberToString(toInteger()); break; case TagDataType::PositionInSet: regularStrRes = toPositionInSet().toString(); break; case TagDataType::StandardGenreIndex: { const auto genreIndex = toInteger(); if (Id3Genres::isEmptyGenre(genreIndex)) { regularStrRes.clear(); } else if (const auto genreName = Id3Genres::stringFromIndex(genreIndex); !genreName.empty()) { regularStrRes.assign(genreName); } else { throw ConversionException("No string representation for the assigned standard genre index available."); } break; } case TagDataType::TimeSpan: regularStrRes = toTimeSpan().toString(); break; case TagDataType::DateTime: regularStrRes = toDateTime().toString(DateTimeOutputFormat::IsoOmittingDefaultComponents); break; case TagDataType::Popularity: regularStrRes = toPopularity().toString(); break; case TagDataType::UnsignedInteger: regularStrRes = numberToString(toUnsignedInteger()); break; case TagDataType::DateTimeExpression: regularStrRes = toDateTimeExpression().toIsoString(); break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string.")); } if (encoding == TagTextEncoding::Utf16LittleEndian || encoding == TagTextEncoding::Utf16BigEndian) { auto encodedData = encoding == TagTextEncoding::Utf16LittleEndian ? convertUtf8ToUtf16LE(regularStrRes.data(), result.size()) : convertUtf8ToUtf16BE(regularStrRes.data(), result.size()); result.assign(reinterpret_cast(encodedData.first.get()), encodedData.second / sizeof(const char16_t)); } } /*! * \brief Assigns a copy of the given \a text. * \param text Specifies the text to be assigned. * \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.) * \param textEncoding Specifies the encoding of the given \a text. * \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to * use \a textEncoding without any character set conversions. * \throws Throws a ConversionException if the conversion the specified character set fails. * \remarks Strips the BOM of the specified \a text. */ void TagValue::assignText(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo) { m_type = TagDataType::Text; m_encoding = convertTo == TagTextEncoding::Unspecified ? textEncoding : convertTo; stripBom(text, textSize, textEncoding); if (!textSize) { m_size = 0; m_ptr.reset(); return; } if (convertTo == TagTextEncoding::Unspecified || textEncoding == convertTo) { m_ptr = make_unique(m_size = textSize); copy(text, text + textSize, m_ptr.get()); return; } StringData encodedData; switch (textEncoding) { case TagTextEncoding::Utf8: // use pre-defined methods when encoding to UTF-8 switch (convertTo) { case TagTextEncoding::Latin1: encodedData = convertUtf8ToLatin1(text, textSize); break; case TagTextEncoding::Utf16LittleEndian: encodedData = convertUtf8ToUtf16LE(text, textSize); break; case TagTextEncoding::Utf16BigEndian: encodedData = convertUtf8ToUtf16BE(text, textSize); break; default:; } break; default: { // otherwise, determine input and output parameter to use general covertString method const auto inputParameter = encodingParameter(textEncoding); const auto outputParameter = encodingParameter(convertTo); encodedData = convertString(inputParameter.first, outputParameter.first, text, textSize, outputParameter.second / inputParameter.second); } } // can't just move the encoded data because it needs to be deleted with free m_ptr = make_unique(m_size = encodedData.second); copy(encodedData.first.get(), encodedData.first.get() + encodedData.second, m_ptr.get()); } /*! * \brief Assigns the given integer \a value. * \param value Specifies the integer to be assigned. */ void TagValue::assignInteger(int value) { m_size = sizeof(value); m_ptr = make_unique(m_size); std::copy(reinterpret_cast(&value), reinterpret_cast(&value) + m_size, m_ptr.get()); m_type = TagDataType::Integer; m_encoding = TagTextEncoding::Latin1; } /*! * \brief Assigns the given unsigned integer \a value. * \param value Specifies the unsigned integer to be assigned. */ void TagValue::assignUnsignedInteger(std::uint64_t value) { m_size = sizeof(value); m_ptr = make_unique(m_size); std::copy(reinterpret_cast(&value), reinterpret_cast(&value) + m_size, m_ptr.get()); m_type = TagDataType::UnsignedInteger; m_encoding = TagTextEncoding::Latin1; } /*! * \brief Assigns a copy of the given \a data. * \param data Specifies the data to be assigned. * \param length Specifies the length of the data. * \param type Specifies the type of the data as TagDataType. * \param encoding Specifies the encoding of the data as TagTextEncoding. The * encoding will only be considered if a text is assigned. */ void TagValue::assignData(const char *data, size_t length, TagDataType type, TagTextEncoding encoding) { if (type == TagDataType::Text) { stripBom(data, length, encoding); } if (length > m_size) { m_ptr = make_unique(length); } if (length) { std::copy(data, data + length, m_ptr.get()); } else { m_ptr.reset(); } m_size = length; m_type = type; m_encoding = encoding; } /*! * \brief Assigns the given \a data. Takes ownership. * * The specified data is not copied. It is moved. * * \param data Specifies the data to be assigned. * \param length Specifies the length of the data. * \param type Specifies the type of the data as TagDataType. * \param encoding Specifies the encoding of the data as TagTextEncoding. The * encoding will only be considered if a text is assigned. * \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present. */ void TagValue::assignData(unique_ptr &&data, size_t length, TagDataType type, TagTextEncoding encoding) { m_size = length; m_type = type; m_encoding = encoding; m_ptr = std::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); writer.writeUInt64LE(static_cast(value.scale)); 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. */ void TagValue::stripBom(const char *&text, size_t &length, TagTextEncoding encoding) { switch (encoding) { case TagTextEncoding::Utf8: if ((length >= 3) && (BE::toUInt24(text) == 0x00EFBBBF)) { text += 3; length -= 3; } break; case TagTextEncoding::Utf16LittleEndian: if ((length >= 2) && (LE::toInt(text) == 0xFEFF)) { text += 2; length -= 2; } break; case TagTextEncoding::Utf16BigEndian: if ((length >= 2) && (BE::toInt(text) == 0xFEFF)) { text += 2; length -= 2; } break; default:; } } /*! * \brief Ensures the byte-order of the specified UTF-16 string matches the byte-order of the machine. * \remarks Does nothing if \a currentEncoding already matches the byte-order of the machine. */ void TagValue::ensureHostByteOrder(u16string &u16str, TagTextEncoding currentEncoding) { if (currentEncoding != #if defined(CONVERSION_UTILITIES_BYTE_ORDER_LITTLE_ENDIAN) TagTextEncoding::Utf16LittleEndian #elif defined(CONVERSION_UTILITIES_BYTE_ORDER_BIG_ENDIAN) TagTextEncoding::Utf16BigEndian #else #error "Host byte order not supported" #endif ) { for (auto &c : u16str) { c = swapOrder(static_cast(c)); } } } /*! * \brief Returns whether 2 data buffers are equal. In case one of the sizes is zero, no pointer is dereferenced. */ bool TagValue::compareData(const char *data1, std::size_t size1, const char *data2, std::size_t size2, bool ignoreCase) { if (size1 != size2) { return false; } if (!size1) { return true; } if (ignoreCase) { for (auto i1 = data1, i2 = data2, end = data1 + size1; i1 != end; ++i1, ++i2) { if (CaseInsensitiveCharComparer::toLower(static_cast(*i1)) != CaseInsensitiveCharComparer::toLower(static_cast(*i2))) { return false; } } } else { for (auto i1 = data1, i2 = data2, end = data1 + size1; i1 != end; ++i1, ++i2) { if (*i1 != *i2) { return false; } } } return true; } /*! * \brief Returns a default-constructed TagValue where TagValue::isNull() and TagValue::isEmpty() both return true. * \remarks This is useful if one wants to return a const reference to a TagValue and a null-value is needed to indicate * that the field does not exist at all. */ const TagValue &TagValue::empty() { static TagValue emptyTagValue; return emptyTagValue; } /*! * \brief Scales the rating from the current scale to \a targetScale. * \returns * Returns whether a conversion from the current scale to \a targetScale was possible. If no, the object stays unchanged. * Note that it is not validated whether the currently assigned rating is a valid value in the currently assigned scale. * \remarks * - Providing TagType::Unspecified as \a targetScale will convert to a *generic* scale where the rating is number is between * 1 and 5 with decimal values possible where 5 is the best possible rating and 1 the lowest. The value 0 means there's no * rating. * - If the currently assigned scale is TagType::Unspecified than the currently assigned rating is assumed to use the *generic* * scale described in the previous point. */ bool Popularity::scaleTo(TagType targetScale) { if (scale == targetScale) { return true; } // convert to generic scale first double genericRating; switch (scale) { case TagType::Unspecified: genericRating = rating; break; case TagType::MatroskaTag: genericRating = rating / (5.0 / 4.0) + 1.0; break; case TagType::Id3v2Tag: genericRating = rating < 1.0 ? 0.0 : ((rating - 1.0) / (254.0 / 4.0) + 1.0); break; case TagType::VorbisComment: case TagType::OggVorbisComment: genericRating = rating / 20.0; break; default: return false; } // convert from the generic scale to the target scale switch (targetScale) { case TagType::Unspecified: rating = genericRating; break; case TagType::MatroskaTag: rating = (genericRating - 1.0) * (5.0 / 4.0); break; case TagType::Id3v2Tag: rating = genericRating < 1.0 ? 0.0 : ((genericRating - 1.0) * (254.0 / 4.0) + 1.0); break; case TagType::VorbisComment: case TagType::OggVorbisComment: rating = genericRating * 20.0; break; default: return false; } scale = targetScale; return true; } /*! * \brief Returns the popularity as string in the format "rating" if only a rating is present * or in the format "user|rating|play-counter" or an empty string if the popularity isEmpty(). */ std::string Popularity::toString() const { return isEmpty() ? std::string() : ((user.empty() && !playCounter) ? numberToString(rating) : (user % '|' % numberToString(rating) % '|' + playCounter)); } /*! * \brief Parses the popularity from \a str assuming the same format as toString() produces and * sets TagType::Unspecified as scale. So \a str is expected to contain a rating within * the range of 1.0 and 5.0 or 0.0 to denote there's no rating. * \throws Throws ConversionException() if the format is invalid. */ Popularity Popularity::fromString(std::string_view str) { return fromString(str, TagType::Unspecified); } /*! * \brief Parses the popularity from \a str assuming the same format as toString() produces and assigns the * specified \a scale. So \a str is expected to contain a rating according to the specifications of * the tag format passed via \a scale. * \throws Throws ConversionException() if the format is invalid. */ TagParser::Popularity TagParser::Popularity::fromString(std::string_view str, TagType scale) { const auto parts = splitStringSimple>(str, "|"); auto res = Popularity({ .scale = scale }); if (parts.empty()) { return res; } else if (parts.size() > 3) { 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(parts.front()); return res; } catch (const ConversionException &) { } } // otherwise, read user, rating and 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