diff --git a/fieldbasedtag.h b/fieldbasedtag.h index baf8539..b8be4c7 100644 --- a/fieldbasedtag.h +++ b/fieldbasedtag.h @@ -29,26 +29,27 @@ public: FieldMapBasedTag(); virtual const TagValue &value(const typename FieldType::identifierType &id) const; - virtual const TagValue &value(KnownField field) const; - virtual std::list values(const typename FieldType::identifierType &id) const; - virtual std::list values(KnownField field) const; + const TagValue &value(KnownField field) const; + std::list values(const typename FieldType::identifierType &id) const; + std::list values(KnownField field) const; virtual bool setValue(const typename FieldType::identifierType &id, const TagValue &value); - virtual bool setValue(KnownField field, const TagValue &value); - virtual bool setValues(const typename FieldType::identifierType &id, std::initializer_list values); - virtual bool setValues(KnownField field, std::initializer_list values); - virtual bool hasField(KnownField field) const; + bool setValue(KnownField field, const TagValue &value); + bool setValues(const typename FieldType::identifierType &id, std::initializer_list values); + bool setValues(KnownField field, std::initializer_list values); + bool hasField(KnownField field) const; virtual bool hasField(const typename FieldType::identifierType &id) const; - virtual void removeAllFields(); + void removeAllFields(); const std::multimap &fields() const; std::multimap &fields(); - virtual unsigned int fieldCount() const; + unsigned int fieldCount() const; virtual typename FieldType::identifierType fieldId(KnownField value) const = 0; virtual KnownField knownField(const typename FieldType::identifierType &id) const = 0; - virtual bool supportsField(KnownField field) const; + bool supportsField(KnownField field) const; using Tag::proposedDataType; virtual TagDataType proposedDataType(const typename FieldType::identifierType &id) const; - virtual int insertFields(const FieldMapBasedTag &from, bool overwrite); - virtual unsigned int insertValues(const Tag &from, bool overwrite); + int insertFields(const FieldMapBasedTag &from, bool overwrite); + unsigned int insertValues(const Tag &from, bool overwrite); + void ensureTextValuesAreProperlyEncoded(); typedef FieldType fieldType; private: @@ -290,6 +291,14 @@ unsigned int FieldMapBasedTag::insertValues(const Tag &from, } } +template +void FieldMapBasedTag::ensureTextValuesAreProperlyEncoded() +{ + for(auto &field : fields()) { + field.second.value().convertDataEncodingForTag(this); + } +} + } #endif // FIELDBASEDTAG_H diff --git a/id3/id3v1tag.cpp b/id3/id3v1tag.cpp index 3e76b47..f1aca1c 100644 --- a/id3/id3v1tag.cpp +++ b/id3/id3v1tag.cpp @@ -59,18 +59,18 @@ void Id3v1Tag::parse(std::istream &stream, bool autoSeek) && buffer[1] == 0x41 && buffer[2] == 0x47) { m_size = 128; - readValue(m_title, 30, buffer, 3); - readValue(m_artist, 30, buffer, 33); - readValue(m_album, 30, buffer, 63); - readValue(m_year, 4, buffer, 93); + readValue(m_title, 30, buffer + 3); + readValue(m_artist, 30, buffer + 33); + readValue(m_album, 30, buffer + 63); + readValue(m_year, 4, buffer + 93); if(buffer[125] == 0) { - readValue(m_comment, 28, buffer, 97); + readValue(m_comment, 28, buffer + 97); m_version = "1.1"; } else { - readValue(m_comment, 30, buffer, 97); + readValue(m_comment, 30, buffer + 97); m_version = "1.0"; } - readValue(m_comment, buffer[125] == 0 ? 28 : 30, buffer, 97); + readValue(m_comment, buffer[125] == 0 ? 28 : 30, buffer + 97); if(buffer[125] == 0) { m_trackPos.assignPosition(PositionInSet(*reinterpret_cast(buffer + 126), 0)); } @@ -123,13 +123,13 @@ void Id3v1Tag::make(ostream &stream) try { if(!m_trackPos.isEmpty() && m_trackPos.type() == TagDataType::PositionInSet) buffer[1] = m_trackPos.toPositionInSet().position(); - } catch(ConversionException &) { + } catch(const ConversionException &) { addNotification(NotificationType::Warning, "Track position field can not be set because given value can not be converted appropriately.", context); } // genre try { buffer[2] = m_genre.toStandardGenreIndex(); - } catch(ConversionException &) { + } catch(const ConversionException &) { addNotification(NotificationType::Warning, "Genre field can not be set because given value can not be converted appropriately.", context); } stream.write(buffer, 3); @@ -254,18 +254,28 @@ bool Id3v1Tag::supportsField(KnownField field) const } } -/*! - * \brief Internally used to read values. - */ -void Id3v1Tag::readValue(TagValue &value, size_t length, char *buffer, int offset) +void Id3v1Tag::ensureTextValuesAreProperlyEncoded() { - char *beg = offset + buffer; - char *end = beg + (length - 1); - while((*end == 0x0 || *end == ' ') && end >= beg) { + m_title.convertDataEncodingForTag(this); + m_artist.convertDataEncodingForTag(this); + m_album.convertDataEncodingForTag(this); + m_year.convertDataEncodingForTag(this); + m_comment.convertDataEncodingForTag(this); + m_trackPos.convertDataEncodingForTag(this); + m_genre.convertDataEncodingForTag(this); +} + +/*! + * \brief Internally used to read values with the specified \a maxLength from the specified \a buffer. + */ +void Id3v1Tag::readValue(TagValue &value, size_t maxLength, const char *buffer) +{ + const char *end = buffer + maxLength - 1; + while((*end == 0x0 || *end == ' ') && end >= buffer) { --end; - --length; + --maxLength; } - value.assignData(beg, length, TagDataType::Text, TagTextEncoding::Latin1); + value.assignData(buffer, maxLength, TagDataType::Text, TagTextEncoding::Latin1); } @@ -277,7 +287,7 @@ void Id3v1Tag::writeValue(const TagValue &value, size_t length, char *buffer, os memset(buffer, 0, length); try { value.toString().copy(buffer, length); - } catch(ConversionException &) { + } catch(const ConversionException &) { addNotification(NotificationType::Warning, "Field can not be set because given value can not be converted appropriately.", "making ID3v1 tag field"); } targetStream.write(buffer, length); diff --git a/id3/id3v1tag.h b/id3/id3v1tag.h index 4e00305..3e2039d 100644 --- a/id3/id3v1tag.h +++ b/id3/id3v1tag.h @@ -11,23 +11,24 @@ class LIB_EXPORT Id3v1Tag : public Tag public: Id3v1Tag(); - virtual TagType type() const; - virtual const char *typeName() const; - virtual bool canEncodingBeUsed(TagTextEncoding encoding) const; - virtual const TagValue &value(KnownField value) const; - virtual bool setValue(KnownField field, const TagValue &value); - virtual bool setValueConsideringTypeInfo(KnownField field, const TagValue &value, const std::string &typeInfo); - virtual bool hasField(KnownField field) const; - virtual void removeAllFields(); - virtual unsigned int fieldCount() const; - virtual bool supportsField(KnownField field) const; + TagType type() const; + const char *typeName() const; + bool canEncodingBeUsed(TagTextEncoding encoding) const; + const TagValue &value(KnownField value) const; + bool setValue(KnownField field, const TagValue &value); + bool setValueConsideringTypeInfo(KnownField field, const TagValue &value, const std::string &typeInfo); + bool hasField(KnownField field) const; + void removeAllFields(); + unsigned int fieldCount() const; + bool supportsField(KnownField field) const; + void ensureTextValuesAreProperlyEncoded(); void parse(std::istream &sourceStream, bool autoSeek); void parse(std::iostream &sourceStream); void make(std::ostream &targetStream); private: - void readValue(TagValue &value, size_t length, char *buffer, int offset); + void readValue(TagValue &value, size_t maxLength, const char *buffer); void writeValue(const TagValue &value, size_t length, char *buffer, std::ostream &targetStream); TagValue m_title; diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index a54664b..161d7b6 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -248,7 +248,7 @@ void Id3v2Tag::parse(istream &stream, const uint64 maximalSize) if(Id3v2FrameIds::isTextFrame(frame.id()) && fields().count(frame.id()) == 1) { addNotification(NotificationType::Warning, "The text frame " + frame.frameIdString() + " exists more than once.", context); } - fields().insert(pair(frame.id(), frame)); + fields().insert(make_pair(frame.id(), frame)); } } catch(const NoDataFoundException &) { if(frame.hasPaddingReached()) { diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index 67075ff..b14c600 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -492,24 +492,28 @@ void MediaFileInfo::parseEverything() * \param treatUnknownFilesAsMp3Files Specifies whether unknown file formats should be treated as MP3 (might break the file). * \param id3v1usage Specifies the usage of ID3v1 when creating tags for MP3 files (has no effect when the file is no MP3 file or not treated as one). * \param id3v2usage Specifies the usage of ID3v2 when creating tags for MP3 files (has no effect when the file is no MP3 file or not treated as one). + * \param id3InitOnCreate Indicates whether to initialize newly created ID3 tags (according to \a id3v1usage and \a id3v2usage) with the values of the already present ID3 tags. + * \param id3TransferValuesOnRemoval Indicates whether values of removed ID3 tags (according to \a id3v1usage and \a id3v2usage) should be transfered to remaining ID3 tags (no values will be overwritten). * \param mergeMultipleSuccessiveId3v2Tags Specifies whether multiple successive ID3v2 tags should be merged (see mergeId3v2Tags()). * \param keepExistingId3v2version Specifies whether the version of existing ID3v2 tags should be adjusted to \a id3v2version (otherwise \a id3v2version is only used when creating a new ID3v2 tag). * \param id3v2version Specifies the ID3v2 version to be used. Valid values are 2, 3 and 4. * \param requiredTargets Specifies the required targets. If targets are not supported by the container an informal notification is added. * \return Returns whether appropriate tags could be created for the file. * \remarks - * - The ID3 related arguments are only practiced when the file format is MP3 or when the file format - * is unknown and \a treatUnknownFilesAsMp3Files is true. - * - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed - * if \a id3v1Usage is set to TagUsage::Never. + * - Tags must have been parsed before invoking this method (otherwise it will just return false). + * - The ID3 related arguments are only practiced when the file format is MP3 or when the file format is unknown and \a treatUnknownFilesAsMp3Files is true. + * - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed if \a id3v1Usage is set to TagUsage::Never. * - The method might do nothing if present tag already match the given specifications. - * - This is only a convenience method. The task could be done by manually using the methods createId3v1Tag(), - * createId3v2Tag(), removeId3v1Tag() ... as well. - * - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (\a id3v2usage is set to TagUsage::Never) - * and an ID3v1 tag will be created instead not all fields can be transfered. + * - This is only a convenience method. The task could be done by manually using the methods createId3v1Tag(), createId3v2Tag(), removeId3v1Tag() ... as well. + * - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (\a id3v2usage is set to TagUsage::Never) and an ID3v1 tag will be created instead not all fields can be transfered. + * - TODO: Refactoring required, there are too much params (not sure how to refactor though, since not all of the params are simple flags). */ -bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagUsage id3v1usage, TagUsage id3v2usage, bool mergeMultipleSuccessiveId3v2Tags, bool keepExistingId3v2version, uint32 id3v2version, const std::vector &requiredTargets) +bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagUsage id3v1usage, TagUsage id3v2usage, bool id3InitOnCreate, bool id3TransferValuesOnRemoval, bool mergeMultipleSuccessiveId3v2Tags, bool keepExistingId3v2version, byte id3v2MajorVersion, const std::vector &requiredTargets) { + // check if tags have been parsed yet (tags must have been parsed yet to create appropriate tags) + if(tagsParsingStatus() == ParsingStatus::NotParsedYet) { + return false; + } // check if tags need to be created/adjusted/removed bool targetsRequired = !requiredTargets.empty() && (requiredTargets.size() != 1 || !requiredTargets.front().isEmpty()); bool targetsSupported = false; @@ -557,12 +561,26 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU // create ID3 tags according to id3v2usage and id3v2usage if(id3v1usage == TagUsage::Always) { // always create ID3v1 tag -> ensure there is one - createId3v1Tag(); + if(!id3v1Tag()) { + Id3v1Tag *id3v1Tag = createId3v1Tag(); + if(id3InitOnCreate) { + for(const auto &id3v2Tag : id3v2Tags()) { + // overwrite existing values to ensure default ID3v1 genre "Blues" is updated as well + id3v1Tag->insertValues(*id3v2Tag, true); + // ID3v1 does not support all text encodings which might be used in ID3v2 + id3v1Tag->ensureTextValuesAreProperlyEncoded(); + } + } + } } if(id3v2usage == TagUsage::Always) { // always create ID3v2 tag -> ensure there is one and set version if(!hasId3v2Tag()) { - createId3v2Tag()->setVersion(id3v2version, 0); + Id3v2Tag *id3v2Tag = createId3v2Tag(); + id3v2Tag->setVersion(id3v2MajorVersion, 0); + if(id3InitOnCreate && id3v1Tag()) { + id3v2Tag->insertValues(*id3v1Tag(), true); + } } } } @@ -573,15 +591,15 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU // remove ID3 tags according to id3v2usage and id3v2usage if(id3v1usage == TagUsage::Never) { if(hasId3v1Tag()) { - if(hasId3v2Tag()) { - // transfer tags to ID3v2 tag before removing + // transfer tags to ID3v2 tag before removing + if(id3TransferValuesOnRemoval && hasId3v2Tag()) { id3v2Tags().front()->insertValues(*id3v1Tag(), false); } removeId3v1Tag(); } } if(id3v2usage == TagUsage::Never) { - if(hasId3v1Tag()) { + if(id3TransferValuesOnRemoval && hasId3v1Tag()) { // transfer tags to ID3v1 tag before removing for(const auto &tag : id3v2Tags()) { id3v1Tag()->insertValues(*tag, false); @@ -591,7 +609,7 @@ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagU } else if(!keepExistingId3v2version) { // set version of ID3v2 tag according user preferences for(const auto &tag : id3v2Tags()) { - tag->setVersion(id3v2version, 0); + tag->setVersion(id3v2MajorVersion, 0); } } } @@ -1459,7 +1477,7 @@ void MediaFileInfo::invalidated() } /*! - * \brief Internally used to chanings of a MP3 file (or theoretically any file) with ID3 tags. + * \brief Internally used to save chanings of MP3/FLAC files and any other files which might have ID3 tags. */ void MediaFileInfo::makeMp3File() { diff --git a/mediafileinfo.h b/mediafileinfo.h index 3c3399d..2e08b1e 100644 --- a/mediafileinfo.h +++ b/mediafileinfo.h @@ -119,8 +119,8 @@ public: // methods to create/remove tags bool createAppropriateTags(bool treatUnknownFilesAsMp3Files = false, TagUsage id3v1usage = TagUsage::KeepExisting, - TagUsage id3v2usage = TagUsage::Always, bool mergeMultipleSuccessiveId3v2Tags = true, - bool keepExistingId3v2version = true, uint32 id3v2version = 3, const std::vector &requiredTargets = std::vector()); + TagUsage id3v2usage = TagUsage::Always, bool id3InitOnCreate = false, bool id3TransferValuesOnRemoval = true, bool mergeMultipleSuccessiveId3v2Tags = true, + bool keepExistingId3v2version = true, byte id3v2MajorVersion = 3, const std::vector &requiredTargets = std::vector()); bool removeId3v1Tag(); Id3v1Tag *createId3v1Tag(); bool removeId3v2Tag(Id3v2Tag *tag); diff --git a/positioninset.h b/positioninset.h index b6220f1..0fb0ffd 100644 --- a/positioninset.h +++ b/positioninset.h @@ -110,9 +110,8 @@ StringType PositionInSet::toString() const if(m_position) { ss << m_position; } - ss << "/"; if(m_total) { - ss << m_total; + ss << '/' << m_total; } return ss.str(); } diff --git a/tag.cpp b/tag.cpp index 9880a62..deda380 100644 --- a/tag.cpp +++ b/tag.cpp @@ -121,6 +121,8 @@ bool Tag::setValues(KnownField field, std::initializer_list values) * \param from Specifies the Tag the values should be inserted from. * \param overwrite Indicates whether existing values should be overwritten. * \return Returns the number of values that have been inserted. + * \remarks The encoding of the inserted text values might not be supported by the tag. + * To fix this, call ensureTextValuesAreProperlyEncoded() after insertion. */ unsigned int Tag::insertValues(const Tag &from, bool overwrite) { @@ -139,6 +141,12 @@ unsigned int Tag::insertValues(const Tag &from, bool overwrite) return count; } +/*! + * \fn Tag::ensureTextValuesAreProperlyEncoded() + * \brief Ensures the encoding of all assigned text values is supported by the tag by + * converting the character set if neccessary. + */ + //bool Tag::setParent(Tag *tag) //{ // if(m_parent != tag) { diff --git a/tag.h b/tag.h index 965f731..a73e0e2 100644 --- a/tag.h +++ b/tag.h @@ -127,6 +127,7 @@ public: virtual bool supportsDescription(KnownField field) const; virtual bool supportsMimeType(KnownField field) const; virtual unsigned int insertValues(const Tag &from, bool overwrite); + virtual void ensureTextValuesAreProperlyEncoded() = 0; // Tag *parent() const; // bool setParent(Tag *tag); // Tag *nestedTag(size_t index) const; diff --git a/tagvalue.cpp b/tagvalue.cpp index ce39e80..af77cfc 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -1,4 +1,5 @@ #include "./tagvalue.h" +#include "./tag.h" #include "./id3/id3genres.h" @@ -326,6 +327,63 @@ pair encodingParameter(TagTextEncoding tagTextEncoding) } } +/*! + * \brief Converts the currently assigned text value to the specified \a encoding. + * \throws Throws ConversionUtilities::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) { + 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 value of the current TagValue object to its equivalent * std::string representation. diff --git a/tagvalue.h b/tagvalue.h index 9ac9db7..b219a15 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -14,6 +14,8 @@ namespace Media { +class Tag; + /*! * \brief Specifies the text encoding. */ @@ -105,6 +107,8 @@ public: bool isLabeledAsReadonly() const; void setReadonly(bool value); TagTextEncoding dataEncoding() const; + void convertDataEncoding(TagTextEncoding encoding); + void convertDataEncodingForTag(const Tag *tag); TagTextEncoding descriptionEncoding() const; static const TagValue &empty(); @@ -446,7 +450,7 @@ inline void TagValue::setReadonly(bool value) /*! * \brief Returns the data encoding. - * \remarks This method is only useful when a text is assigned. + * \remarks This value is only relevant if type() equals TagDataType::Text. * \sa assignText() */ inline TagTextEncoding TagValue::dataEncoding() const @@ -456,9 +460,8 @@ inline TagTextEncoding TagValue::dataEncoding() const /*! * \brief Returns the description encoding. - * \remarks This method is only useful when a description is assigned. - * \sa description() - * \sa setDescription() + * \remarks This value is only relevant if a description is assigned. + * \sa description(), setDescription() */ inline TagTextEncoding TagValue::descriptionEncoding() const { diff --git a/tests/overall.cpp b/tests/overall.cpp index 9c21b01..85834c9 100644 --- a/tests/overall.cpp +++ b/tests/overall.cpp @@ -872,10 +872,15 @@ void OverallTests::checkMp3Testfile1() case TagType::Id3v2Tag: CPPUNIT_ASSERT(tag->value(KnownField::Title).dataEncoding() == TagTextEncoding::Utf16LittleEndian); CPPUNIT_ASSERT(tag->value(KnownField::Title).toWString() == u"Cohesion"); + CPPUNIT_ASSERT(tag->value(KnownField::Title).toString(TagTextEncoding::Utf8) == "Cohesion"); CPPUNIT_ASSERT(tag->value(KnownField::Artist).toWString() == u"Minutemen"); + CPPUNIT_ASSERT(tag->value(KnownField::Artist).toString(TagTextEncoding::Utf8) == "Minutemen"); CPPUNIT_ASSERT(tag->value(KnownField::Album).toWString() == u"Double Nickels On The Dime"); + CPPUNIT_ASSERT(tag->value(KnownField::Album).toString(TagTextEncoding::Utf8) == "Double Nickels On The Dime"); CPPUNIT_ASSERT(tag->value(KnownField::Genre).toWString() == u"Punk Rock"); + CPPUNIT_ASSERT(tag->value(KnownField::Genre).toString(TagTextEncoding::Utf8) == "Punk Rock"); CPPUNIT_ASSERT(tag->value(KnownField::Comment).toWString() == u"ExactAudioCopy v0.95b4"); + CPPUNIT_ASSERT(tag->value(KnownField::Comment).toString(TagTextEncoding::Utf8) == "ExactAudioCopy v0.95b4"); CPPUNIT_ASSERT(tag->value(KnownField::TrackPosition).toPositionInSet().total() == 43); CPPUNIT_ASSERT(tag->value(KnownField::Length).toTimeSpan().isNull()); CPPUNIT_ASSERT(tag->value(KnownField::Lyricist).isEmpty());