diff --git a/README.md b/README.md index 0ba0384..3934659 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,24 @@ Here are some Bash examples which illustrate getting and setting tag information **Note**: The *+* sign after the field name *track* which indicates that the field value should be increased after a file has been processed. +* Sets a cover of a special type with a description: + ``` + tageditor set cover=":front-cover" cover0="/path/to/back-cover.jpg:back-cover:The description" -f foo.mp3 + ``` + + - The syntax is `path:cover-type:description`. The cover type and description are optional. + - In this example the front cover is removed (by passing an empty path) and the back cover set to the specified + file. Other cover types are not affected. + - When specifying a cover without type, all existing covers are replaced and the new cover will be of the + type "other". + - To replace all existing covers when specifying a cover type + use e.g. `… cover= cover0="/path/to/back-cover.jpg:back-cover"`. + - The names of all cover types can be shown via `tageditor --print-field-names`. + - The `0` after the 2nd `cover` is required. Otherwise the 2nd cover would only be set in the 2nd file (which + is not even specified in this example). + - This is only supported by the tag formats ID3v2 and Vorbis Comment. The type and description are ignored + when dealing with a different format. + ## Text encoding / unicode support 1. It is possible to set the preferred encoding used *within* the tags via CLI option ``--encoding`` and in the GUI settings. diff --git a/cli/helper.cpp b/cli/helper.cpp index 56d9d2f..418788c 100644 --- a/cli/helper.cpp +++ b/cli/helper.cpp @@ -28,6 +28,39 @@ using namespace Settings; namespace Cli { +const std::vector &id3v2CoverTypeNames() +{ + static const auto t + = std::vector{ "other"sv, "file-icon"sv, "other-file-icon"sv, "front-cover"sv, "back-cover"sv, "leaflet-page"sv, "media"sv, + "lead-performer"sv, "artist"sv, "conductor"sv, "band"sv, "composer"sv, "lyricist"sv, "recording-location"sv, "during-recording"sv, + "during-performance"sv, "movie-screen-capture"sv, "bright-colored-fish"sv, "illustration"sv, "artist-logotype"sv, "publisher"sv }; + return t; +} + +CoverType id3v2CoverType(std::string_view coverName) +{ + static const auto mapping = [] { + const auto &names = id3v2CoverTypeNames(); + auto map = std::map(); + auto index = CoverType(); + for (const auto name : names) { + map[name] = index++; + } + return map; + }(); + if (const auto i = mapping.find(coverName); i != mapping.end()) { + return i->second; + } else { + return invalidCoverType; + } +} + +std::string_view id3v2CoverName(CoverType coverType) +{ + const auto &names = id3v2CoverTypeNames(); + return coverType < names.size() ? names[coverType] : "?"sv; +} + CppUtilities::TimeSpanOutputFormat timeSpanOutputFormat = TimeSpanOutputFormat::WithMeasures; /*! @@ -208,16 +241,16 @@ void printProperty(const char *propName, ElementPosition elementPosition, const } } -void printFieldName(const char *fieldName, size_t fieldNameLen) +void printFieldName(std::string_view fieldName) { cout << " " << fieldName; // also write padding - if (fieldNameLen >= 18) { + if (fieldName.size() >= 18) { // write at least one space cout << ' '; return; } - for (auto i = fieldNameLen; i < 18; ++i) { + for (auto i = fieldName.size(); i < 18; ++i) { cout << ' '; } } @@ -233,13 +266,30 @@ void printTagValue(const TagValue &value) cout << '\n'; } +template static void printId3v2CoverValues(TagType *tag) +{ + const auto &fields = tag->fields(); + const auto id = tag->fieldId(KnownField::Cover); + for (auto range = fields.equal_range(id); range.first != range.second; ++range.first) { + const auto &field = range.first->second; + printFieldName(argsToString("Cover (", id3v2CoverName(static_cast(field.typeInfo())), ")")); + printTagValue(field.value()); + } +} + void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty) { - // write field name - const char *fieldName = scope.field.name(); - const auto fieldNameLen = strlen(fieldName); - + const auto fieldName = std::string_view(scope.field.name()); try { + if (scope.field.knownFieldForTag(tag, tagType) == KnownField::Cover) { + if (tagType == TagType::Id3v2Tag) { + printId3v2CoverValues(static_cast(tag)); + } else { + printId3v2CoverValues(static_cast(tag)); + } + return; + } + // parse field denotation const auto &values = scope.field.values(tag, tagType); @@ -255,20 +305,20 @@ void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool s // print empty value (if not prevented) if (values.first.empty()) { - printFieldName(fieldName, fieldNameLen); + printFieldName(fieldName); cout << "none\n"; return; } // print values for (const auto &value : values.first) { - printFieldName(fieldName, fieldNameLen); + printFieldName(fieldName); printTagValue(*value); } } catch (const ConversionException &e) { // handle conversion error which might happen when parsing field denotation - printFieldName(fieldName, fieldNameLen); + printFieldName(fieldName); cout << "unable to parse - " << e.what() << '\n'; } } @@ -283,7 +333,7 @@ template void printNativeFields(const Tag *tag) } const auto fieldId(ConcreteTag::FieldType::fieldIdToString(field.first)); - printFieldName(fieldId.data(), fieldId.size()); + printFieldName(fieldId); printTagValue(field.second.value()); } } @@ -613,7 +663,7 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) } template -std::pair, bool> valuesForNativeField(std::string_view idString, const Tag *tag, TagType tagType) +static std::pair, bool> valuesForNativeField(std::string_view idString, const Tag *tag, TagType tagType) { auto res = make_pair, bool>({}, false); if (!(tagType & tagTypeMask)) { @@ -625,7 +675,7 @@ std::pair, bool> valuesForNativeField(std::string_ } template -bool setValuesForNativeField(std::string_view idString, Tag *tag, TagType tagType, const std::vector &values) +static bool setValuesForNativeField(std::string_view idString, Tag *tag, TagType tagType, const std::vector &values) { if (!(tagType & tagTypeMask)) { return false; @@ -633,20 +683,35 @@ bool setValuesForNativeField(std::string_view idString, Tag *tag, TagType tagTyp return static_cast(tag)->setValues(ConcreteTag::FieldType::fieldIdFromString(idString), values); } -inline FieldId::FieldId( - std::string_view nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField) +template +static KnownField knownFieldForNativeField(std::string_view idString, const Tag *tag, TagType tagType) +{ + if (!(tagType & tagTypeMask)) { + return KnownField::Invalid; + } + try { + return static_cast(tag)->knownField(ConcreteTag::FieldType::fieldIdFromString(idString)); + } catch (const ConversionException &) { + return KnownField::Invalid; + } +} + +inline FieldId::FieldId(std::string_view nativeField, GetValuesForNativeFieldType &&valuesForNativeField, + SetValuesForNativeFieldType &&setValuesForNativeField, KnownFieldForNativeFieldType &&knownFieldForNativeField) : m_knownField(KnownField::Invalid) , m_nativeField(nativeField) - , m_valuesForNativeField(valuesForNativeField) - , m_setValuesForNativeField(setValuesForNativeField) + , m_valuesForNativeField(std::move(valuesForNativeField)) + , m_setValuesForNativeField(std::move(setValuesForNativeField)) + , m_knownFieldForNativeField(std::move(knownFieldForNativeField)) { } /// \remarks This wrapper is required because specifying c'tor template args is not possible. template FieldId FieldId::fromNativeField(std::string_view nativeFieldId) { - return FieldId(nativeFieldId, bind(&valuesForNativeField, nativeFieldId, _1, _2), - bind(&setValuesForNativeField, nativeFieldId, _1, _2, _3)); + return FieldId(nativeFieldId, std::bind(&valuesForNativeField, nativeFieldId, _1, _2), + std::bind(&setValuesForNativeField, nativeFieldId, _1, _2, _3), + std::bind(&knownFieldForNativeField, nativeFieldId, _1, _2)); } FieldId FieldId::fromTagDenotation(const char *denotation, size_t denotationSize) @@ -700,6 +765,15 @@ bool FieldId::setValues(Tag *tag, TagType tagType, const std::vector & } } +KnownField FieldId::knownFieldForTag(const Tag *tag, TagType tagType) const +{ + if (!m_nativeField.empty()) { + return m_knownFieldForNativeField(tag, tagType); + } else { + return m_knownField; + } +} + string tagName(const Tag *tag) { stringstream ss; diff --git a/cli/helper.h b/cli/helper.h index cb408d3..aa044e4 100644 --- a/cli/helper.h +++ b/cli/helper.h @@ -3,7 +3,9 @@ #include "../application/knownfieldmodel.h" +#include #include +#include #include #include @@ -13,6 +15,7 @@ #include #include +#include #include #include #include @@ -44,11 +47,18 @@ CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(TagParser, TagParser::TagType) namespace Cli { +using CoverType = std::conditional_t= sizeof(typename VorbisComment::FieldType::TypeInfoType), + typename Id3v2Tag::FieldType::TypeInfoType, typename VorbisComment::FieldType::TypeInfoType>; +constexpr auto invalidCoverType = std::numeric_limits::max(); +const std::vector &id3v2CoverTypeNames(); +CoverType id3v2CoverType(std::string_view coverName); +std::string_view id3v2CoverName(CoverType coverType); + class FieldId { friend struct std::hash; public: - FieldId(KnownField m_knownField = KnownField::Invalid, const char *denotation = nullptr, std::size_t denotationSize = 0); + explicit FieldId(KnownField m_knownField = KnownField::Invalid, const char *denotation = nullptr, std::size_t denotationSize = 0); static FieldId fromTagDenotation(const char *denotation, std::size_t denotationSize); static FieldId fromTrackDenotation(const char *denotation, std::size_t denotationSize); bool operator==(const FieldId &other) const; @@ -58,12 +68,14 @@ public: const std::string &denotation() const; std::pair, bool> values(const Tag *tag, TagType tagType) const; bool setValues(Tag *tag, TagType tagType, const std::vector &values) const; + KnownField knownFieldForTag(const Tag *tag, TagType tagType) const; private: using GetValuesForNativeFieldType = std::function, bool>(const Tag *, TagType)>; using SetValuesForNativeFieldType = std::function &)>; - FieldId(std::string_view nativeField, const GetValuesForNativeFieldType &valuesForNativeField, - const SetValuesForNativeFieldType &setValuesForNativeField); + using KnownFieldForNativeFieldType = std::function; + FieldId(std::string_view nativeField, GetValuesForNativeFieldType &&valuesForNativeField, SetValuesForNativeFieldType &&setValuesForNativeField, + KnownFieldForNativeFieldType &&knownFieldForNativeField); template static FieldId fromNativeField(std::string_view nativeFieldId); KnownField m_knownField; @@ -71,6 +83,7 @@ private: std::string m_nativeField; GetValuesForNativeFieldType m_valuesForNativeField; SetValuesForNativeFieldType m_setValuesForNativeField; + KnownFieldForNativeFieldType m_knownFieldForNativeField; }; inline FieldId::FieldId(KnownField knownField, const char *denotation, std::size_t denotationSize) @@ -105,7 +118,7 @@ inline const std::string &FieldId::denotation() const } struct FieldScope { - FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget()); + explicit FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget()); bool operator==(const FieldScope &other) const; bool isTrack() const; @@ -150,7 +163,7 @@ inline FieldValue::FieldValue(DenotationType type, unsigned int fileIndex, const class InterruptHandler { public: - InterruptHandler(std::function handler); + explicit InterruptHandler(std::function handler); ~InterruptHandler(); private: diff --git a/cli/mainfeatures.cpp b/cli/mainfeatures.cpp index 3a5b2e1..8a35d61 100644 --- a/cli/mainfeatures.cpp +++ b/cli/mainfeatures.cpp @@ -18,11 +18,13 @@ #include #include #include +#include #include #include #include #include #include +#include #ifdef TAGEDITOR_JSON_EXPORT #include @@ -52,6 +54,7 @@ #include #include #include +#include using namespace std; using namespace CppUtilities; @@ -93,6 +96,9 @@ void printFieldNames(const ArgumentOccurrence &) " - Tag modifier: " TAG_MODIFIER "\n" " - Track modifier: track=id1,id2,id3,... track=all\n" " - Target modifier:\n " TARGET_MODIFIER "\n" + "ID3v2 cover types:\n" + << joinStrings, std::string>(id3v2CoverTypeNames(), "\n"sv, false, " - "sv, ""sv) + << '\n' << flush; } @@ -379,6 +385,41 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedAr } } +template +bool fieldPredicate(CoverType coverType, const std::pair &pair) +{ + return pair.second.isTypeInfoAssigned() ? (pair.second.typeInfo() == static_cast(coverType)) + : (coverType == 0); +} + +template static void setId3v2CoverValues(TagType *tag, std::vector> &&values) +{ + auto &fields = tag->fields(); + const auto id = tag->fieldId(KnownField::Cover); + const auto range = fields.equal_range(id); + const auto first = range.first; + + for (auto &[tagValue, coverType] : values) { + // check whether there is already a tag value with the current index/type + auto pair = find_if(first, range.second, std::bind(fieldPredicate, coverType, placeholders::_1)); + if (pair != range.second) { + // there is already a tag value with the current index/type + // -> update this value + pair->second.setValue(tagValue); + // check whether there are more values with the current index/type assigned + while ((pair = find_if(++pair, range.second, std::bind(fieldPredicate, coverType, placeholders::_1))) != range.second) { + // -> remove these values as we only support one value of a type in the same tag + pair->second.setValue(TagValue()); + } + } else if (!tagValue.isEmpty()) { + using FieldType = typename TagType::FieldType; + auto newField = FieldType(id, tagValue); + newField.setTypeInfo(static_cast(coverType)); + fields.insert(std::pair(id, std::move(newField))); + } + } +} + void setTagInfo(const SetTagInfoArgs &args) { CMD_UTILS_START_CONSOLE; @@ -603,7 +644,8 @@ void setTagInfo(const SetTagInfoArgs &args) continue; } // convert the values to TagValue - vector convertedValues; + auto convertedValues = std::vector(); + auto convertedValuesWithCoverType = std::vector>(); for (const FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) { // assign an empty TagValue to remove the field if denoted value is empty if (relevantDenotedValue->value.empty()) { @@ -616,32 +658,72 @@ void setTagInfo(const SetTagInfoArgs &args) continue; } // add value from file + const auto parts = splitStringSimple>(relevantDenotedValue->value, ":", 3); + const auto path = parts.empty() ? std::string_view() : parts.front(); try { // assume the file refers to a picture - MediaFileInfo coverFileInfo(relevantDenotedValue->value); - Diagnostics coverDiag; - AbortableProgressFeedback coverProgress; // FIXME: actually use the progress object - coverFileInfo.open(true); - coverFileInfo.parseContainerFormat(coverDiag, coverProgress); - auto buff = make_unique(coverFileInfo.size()); - coverFileInfo.stream().seekg(static_cast(coverFileInfo.containerOffset())); - coverFileInfo.stream().read(buff.get(), static_cast(coverFileInfo.size())); - TagValue value(move(buff), coverFileInfo.size(), TagDataType::Picture); - value.setMimeType(coverFileInfo.mimeType()); - convertedValues.emplace_back(move(value)); + auto value = TagValue(); + if (!path.empty()) { + auto coverFileInfo = MediaFileInfo(path); + auto coverDiag = Diagnostics(); + auto coverProgress = AbortableProgressFeedback(); // FIXME: actually use the progress object + coverFileInfo.open(true); + coverFileInfo.parseContainerFormat(coverDiag, coverProgress); + auto buff = make_unique(coverFileInfo.size()); + coverFileInfo.stream().seekg(static_cast(coverFileInfo.containerOffset())); + coverFileInfo.stream().read(buff.get(), static_cast(coverFileInfo.size())); + value = TagValue(std::move(buff), coverFileInfo.size(), TagDataType::Picture); + value.setMimeType(coverFileInfo.mimeType()); + } + if (parts.size() > 2) { + value.setDescription(parts[2], TagTextEncoding::Utf8); + } + if (parts.size() > 1 && denotedScope.field.knownFieldForTag(tag, tagType) == KnownField::Cover + && (tagType == TagType::Id3v2Tag || tagType == TagType::VorbisComment)) { + const auto coverType = id3v2CoverType(parts[1]); + if (coverType == invalidCoverType) { + diag.emplace_back(DiagLevel::Warning, + argsToString("Specified cover type \"", parts[1], "\" is invalid. Ignoring the specified field/value."), + context); + } else { + convertedValuesWithCoverType.emplace_back(std::pair(std::move(value), coverType)); + } + } else { + if (parts.size() > 1) { + diag.emplace_back(DiagLevel::Warning, + argsToString("Ignoring cover type \"", parts[1], "\" for ", tag->typeName(), + ". It is only supported by the cover field and the tag formats ID3v2 and Vorbis Comment."), + context); + } + convertedValues.emplace_back(std::move(value)); + } } catch (const TagParser::Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to parse specified cover file.", context); - } catch (const std::ios_base::failure &) { - diag.emplace_back(DiagLevel::Critical, "An IO error occured when parsing the specified cover file.", context); + } catch (const std::ios_base::failure &e) { + diag.emplace_back(DiagLevel::Critical, + argsToString("An IO error occured when parsing the specified cover file: ", e.what()), context); } } // finally set the values try { - denotedScope.field.setValues(tag, tagType, convertedValues); + if (!convertedValues.empty() || convertedValuesWithCoverType.empty()) { + denotedScope.field.setValues(tag, tagType, convertedValues); + } } catch (const ConversionException &e) { diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse denoted field ID \"", denotedScope.field.name(), "\": ", e.what()), context); } + if (!convertedValuesWithCoverType.empty()) { + switch (tagType) { + case TagType::Id3v2Tag: + setId3v2CoverValues(static_cast(tag), std::move(convertedValuesWithCoverType)); + break; + case TagType::VorbisComment: + setId3v2CoverValues(static_cast(tag), std::move(convertedValuesWithCoverType)); + break; + default:; + } + } } } }