Allow setting multiple covers of the same type but different descriptions

The standard actually says that there might be different covers with only
different descriptions:

    Description is a
    short description of the picture, represented as a terminated
    text string. There may be several pictures attached to one file, each
    in their individual "APIC" frame, but only one with the same content
    descriptor. There may only be one picture with the picture type
    declared as picture type $01 and $02 respectively.

I assume "content descriptor" means combination of the type and
description.

This is in accordance with eyeD3.

See https://github.com/Martchus/tageditor/issues/64#issuecomment-833952940
This commit is contained in:
Martchus 2021-05-10 20:51:15 +02:00
parent 49b0cdb1cd
commit 1944773022
2 changed files with 50 additions and 25 deletions

View File

@ -275,16 +275,22 @@ Here are some Bash examples which illustrate getting and setting tag information
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 general syntax is `path:cover-type:description`. The cover type and description are optional.
- 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).
- In this example the front cover is removed (by passing an empty path) and the back cover set to the specified
file and description. Existing covers of other types and with other descriptions 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"`.
- It is possible to add multiple covers of the same type with only different descriptions. However, when
leaving the description out (2nd `:` is absent), all existing covers of the specified type are replaced and
the new cover will have an empty description.
- To affect only the covers with an empty description, be sure to keep the trailing `:`.
- To remove all existing covers of a certain type but regardless of the description
use e.g. `… cover=":back-cover"`.
- The names of all cover types can be shown via `tageditor --print-field-names`.
- This is only supported by the tag formats ID3v2 and Vorbis Comment. The type and description are ignored
when dealing with a different format.

View File

@ -385,30 +385,47 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedAr
}
}
struct Id3v2Cover {
explicit Id3v2Cover(TagValue &&value, CoverType type, std::optional<std::string_view> description)
: value(std::move(value))
, type(type)
, description(description)
{
}
TagValue value;
CoverType type;
std::optional<std::string_view> description;
};
template <class TagType>
bool fieldPredicate(CoverType coverType, const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
bool fieldPredicate(CoverType coverType, std::optional<std::string_view> description,
const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
{
return pair.second.isTypeInfoAssigned() ? (pair.second.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType))
: (coverType == 0);
const auto &[fieldId, field] = pair;
const auto typeMatches
= field.isTypeInfoAssigned() ? (field.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType)) : (coverType == 0);
const auto descMatches = !description.has_value() || field.value().description() == description.value();
return typeMatches && descMatches;
}
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<std::pair<TagValue, CoverType>> &&values)
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<Id3v2Cover> &&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<TagType>, coverType, placeholders::_1));
for (auto &[tagValue, coverType, description] : values) {
// check whether there is already a tag value with the current type and description
auto pair = std::find_if(first, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, placeholders::_1));
if (pair != range.second) {
// there is already a tag value with the current index/type
// there is already a tag value with the current type and description
// -> 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<TagType>, coverType, placeholders::_1))) != range.second) {
// -> remove these values as we only support one value of a type in the same tag
// check whether there are more values with the current type and description
while ((pair = std::find_if(++pair, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, placeholders::_1)))
!= range.second) {
// -> remove these values as we only support one value of a type/description in the same tag
pair->second.setValue(TagValue());
}
} else if (!tagValue.isEmpty()) {
@ -645,7 +662,7 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// convert the values to TagValue
auto convertedValues = std::vector<TagValue>();
auto convertedValuesWithCoverType = std::vector<std::pair<TagValue, CoverType>>();
auto convertedId3v2CoverValues = std::vector<Id3v2Cover>();
for (const FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) {
// assign an empty TagValue to remove the field if denoted value is empty
if (relevantDenotedValue->value.empty()) {
@ -675,8 +692,10 @@ void setTagInfo(const SetTagInfoArgs &args)
value = TagValue(std::move(buff), coverFileInfo.size(), TagDataType::Picture);
value.setMimeType(coverFileInfo.mimeType());
}
auto description = std::optional<std::string_view>();
if (parts.size() > 2) {
value.setDescription(parts[2], TagTextEncoding::Utf8);
description = parts[2];
}
if (parts.size() > 1 && denotedScope.field.knownFieldForTag(tag, tagType) == KnownField::Cover
&& (tagType == TagType::Id3v2Tag || tagType == TagType::VorbisComment)) {
@ -686,7 +705,7 @@ void setTagInfo(const SetTagInfoArgs &args)
argsToString("Specified cover type \"", parts[1], "\" is invalid. Ignoring the specified field/value."),
context);
} else {
convertedValuesWithCoverType.emplace_back(std::pair(std::move(value), coverType));
convertedId3v2CoverValues.emplace_back(std::move(value), coverType, description);
}
} else {
if (parts.size() > 1) {
@ -706,20 +725,20 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// finally set the values
try {
if (!convertedValues.empty() || convertedValuesWithCoverType.empty()) {
if (!convertedValues.empty() || convertedId3v2CoverValues.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()) {
if (!convertedId3v2CoverValues.empty()) {
switch (tagType) {
case TagType::Id3v2Tag:
setId3v2CoverValues(static_cast<Id3v2Tag *>(tag), std::move(convertedValuesWithCoverType));
setId3v2CoverValues(static_cast<Id3v2Tag *>(tag), std::move(convertedId3v2CoverValues));
break;
case TagType::VorbisComment:
setId3v2CoverValues(static_cast<VorbisComment *>(tag), std::move(convertedValuesWithCoverType));
setId3v2CoverValues(static_cast<VorbisComment *>(tag), std::move(convertedId3v2CoverValues));
break;
default:;
}