Allow specifying native field IDs via CLI
This commit is contained in:
parent
5d0db0fafe
commit
d99956f4c9
|
@ -214,7 +214,7 @@ if(WIDGETS_GUI OR QUICK_GUI)
|
|||
endif()
|
||||
|
||||
# find tagparser
|
||||
find_package(tagparser 6.1.0 REQUIRED)
|
||||
find_package(tagparser 6.2.0 REQUIRED)
|
||||
use_tag_parser()
|
||||
list(APPEND TEST_LIBRARIES ${TAG_PARSER_SHARED_LIB})
|
||||
|
||||
|
|
254
cli/helper.cpp
254
cli/helper.cpp
|
@ -1,8 +1,10 @@
|
|||
#include "./helper.h"
|
||||
|
||||
#include "../application/knownfieldmodel.h"
|
||||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
#include <tagparser/matroska/matroskatag.h>
|
||||
#include <tagparser/mp4/mp4tag.h>
|
||||
#include <tagparser/vorbis/vorbiscomment.h>
|
||||
#include <tagparser/id3/id3v2tag.h>
|
||||
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
|
||||
|
@ -10,6 +12,7 @@
|
|||
#include <cstring>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
using namespace ApplicationUtilities;
|
||||
using namespace ConversionUtilities;
|
||||
using namespace ChronoUtilities;
|
||||
|
@ -140,34 +143,39 @@ void printFieldName(const char *fieldName, size_t fieldNameLen)
|
|||
}
|
||||
}
|
||||
|
||||
void printField(const FieldScope &scope, const Tag *tag, bool skipEmpty)
|
||||
void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty)
|
||||
{
|
||||
const auto &values = tag->values(scope.field);
|
||||
if(!skipEmpty || !values.empty()) {
|
||||
// write field name
|
||||
const char *fieldName = KnownFieldModel::fieldName(scope.field);
|
||||
const auto fieldNameLen = strlen(fieldName);
|
||||
// write field name
|
||||
const char *fieldName = scope.field.name();
|
||||
const auto fieldNameLen = strlen(fieldName);
|
||||
|
||||
// write value
|
||||
if(values.empty()) {
|
||||
printFieldName(fieldName, fieldNameLen);
|
||||
cout << "none\n";
|
||||
} else {
|
||||
for(const auto &value : values) {
|
||||
try {
|
||||
const auto &values = scope.field.values(tag, tagType);
|
||||
if(!skipEmpty || !values.empty()) {
|
||||
// write value
|
||||
if(values.empty()) {
|
||||
printFieldName(fieldName, fieldNameLen);
|
||||
try {
|
||||
const auto textValue = value->toString(TagTextEncoding::Utf8);
|
||||
if(textValue.empty()) {
|
||||
cout << "can't display here (see --extract)";
|
||||
} else {
|
||||
cout << textValue;
|
||||
cout << "none\n";
|
||||
} else {
|
||||
for(const auto &value : values) {
|
||||
printFieldName(fieldName, fieldNameLen);
|
||||
try {
|
||||
const auto textValue = value->toString(TagTextEncoding::Utf8);
|
||||
if(textValue.empty()) {
|
||||
cout << "can't display here (see --extract)";
|
||||
} else {
|
||||
cout << textValue;
|
||||
}
|
||||
} catch(const ConversionException &) {
|
||||
cout << "conversion error";
|
||||
}
|
||||
} catch(const ConversionException &) {
|
||||
cout << "conversion error";
|
||||
cout << '\n';
|
||||
}
|
||||
cout << '\n';
|
||||
}
|
||||
}
|
||||
} catch(const ConversionException &e) {
|
||||
printFieldName(fieldName, fieldNameLen);
|
||||
cout << "unable to parse - " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -366,67 +374,19 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
|
|||
cerr << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl;
|
||||
continue;
|
||||
}
|
||||
// parse the denoted filed
|
||||
if(!strncmp(fieldDenotationString, "title", fieldNameLen)) {
|
||||
scope.field = KnownField::Title;
|
||||
} else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) {
|
||||
scope.field = KnownField::Album;
|
||||
} else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) {
|
||||
scope.field = KnownField::Artist;
|
||||
} else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) {
|
||||
scope.field = KnownField::Genre;
|
||||
} else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) {
|
||||
scope.field = KnownField::Year;
|
||||
} else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) {
|
||||
scope.field = KnownField::Comment;
|
||||
} else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) {
|
||||
scope.field = KnownField::Bpm;
|
||||
} else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) {
|
||||
scope.field = KnownField::Bps;
|
||||
} else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyricist;
|
||||
} else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) {
|
||||
scope.field = KnownField::TrackPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) {
|
||||
scope.field = KnownField::DiskPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) {
|
||||
scope.field = KnownField::PartNumber;
|
||||
} else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) {
|
||||
scope.field = KnownField::TotalParts;
|
||||
} else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) {
|
||||
scope.field = KnownField::Encoder;
|
||||
} else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordDate;
|
||||
} else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) {
|
||||
scope.field = KnownField::Performers;
|
||||
} else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) {
|
||||
scope.field = KnownField::Length;
|
||||
} else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) {
|
||||
scope.field = KnownField::Language;
|
||||
} else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) {
|
||||
scope.field = KnownField::EncoderSettings;
|
||||
} else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::SynchronizedLyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) {
|
||||
scope.field = KnownField::Grouping;
|
||||
} else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordLabel;
|
||||
} else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) {
|
||||
scope.field = KnownField::Cover;
|
||||
type = DenotationType::File; // read cover always from file
|
||||
} else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) {
|
||||
scope.field = KnownField::Composer;
|
||||
} else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) {
|
||||
scope.field = KnownField::Rating;
|
||||
} else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) {
|
||||
scope.field = KnownField::Description;
|
||||
} else {
|
||||
// no "KnownField" value matching -> discard the field denotation
|
||||
cerr << "Warning: The field name \"" << string(fieldDenotationString, fieldNameLen) << "\" is unknown and will be ingored." << endl;
|
||||
// parse the denoted field ID
|
||||
try {
|
||||
scope.field = FieldId::fromDenotation(fieldDenotationString, fieldNameLen);
|
||||
} catch(const ConversionException &e) {
|
||||
// unable to parse field ID denotation -> discard the field denotation
|
||||
cerr << "Warning: The field denotation \"" << string(fieldDenotationString, fieldNameLen) << "\" could not be parsed and will be ignored: " << e.what() << endl;
|
||||
continue;
|
||||
}
|
||||
// read cover always from file
|
||||
if(scope.field.knownField() == KnownField::Cover) {
|
||||
type = DenotationType::File;
|
||||
}
|
||||
|
||||
// add field denotation scope
|
||||
auto &fieldValues = fields[scope];
|
||||
// add value to the scope (if present)
|
||||
|
@ -448,4 +408,134 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
|
|||
return fields;
|
||||
}
|
||||
|
||||
template<class ConcreteTag>
|
||||
std::vector<const TagValue *> valuesForNativeField(const char *idString, std::size_t idStringSize, const Tag *tag, TagType tagType)
|
||||
{
|
||||
if(tagType != ConcreteTag::tagType) {
|
||||
return vector<const TagValue *>();
|
||||
}
|
||||
return static_cast<const ConcreteTag *>(tag)->values(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize));
|
||||
}
|
||||
|
||||
template<class ConcreteTag>
|
||||
bool setValuesForNativeField(const char *idString, std::size_t idStringSize, Tag *tag, TagType tagType, const std::vector<TagValue> &values)
|
||||
{
|
||||
if(tagType != ConcreteTag::tagType) {
|
||||
return false;
|
||||
}
|
||||
return static_cast<ConcreteTag *>(tag)->setValues(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize), values);
|
||||
}
|
||||
|
||||
inline FieldId::FieldId(const char *nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField) :
|
||||
m_knownField(KnownField::Invalid),
|
||||
m_nativeField(nativeField),
|
||||
m_valuesForNativeField(valuesForNativeField),
|
||||
m_setValuesForNativeField(setValuesForNativeField)
|
||||
{}
|
||||
|
||||
/// \remarks This wrapper is required because specifying c'tor template args is not possible.
|
||||
template<class ConcreteTag>
|
||||
FieldId FieldId::fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize)
|
||||
{
|
||||
return FieldId(
|
||||
nativeFieldId,
|
||||
bind(&valuesForNativeField<ConcreteTag>, nativeFieldId, nativeFieldIdSize, _1, _2),
|
||||
bind(&setValuesForNativeField<ConcreteTag>, nativeFieldId, nativeFieldIdSize, _1, _2, _3)
|
||||
);
|
||||
}
|
||||
|
||||
FieldId FieldId::fromDenotation(const char *denotation, size_t denotationSize)
|
||||
{
|
||||
// check for native, format-specific denotation
|
||||
if(!strncmp(denotation, "mkv:", 4)) {
|
||||
return FieldId::fromNativeField<MatroskaTag>(denotation + 4, denotationSize - 4);
|
||||
} else if(!strncmp(denotation, "mp4:", 4)) {
|
||||
return FieldId::fromNativeField<Mp4Tag>(denotation + 4, denotationSize - 4);
|
||||
} else if(!strncmp(denotation, "vorbis:", 7)) {
|
||||
return FieldId::fromNativeField<VorbisComment>(denotation + 7, denotationSize - 7);
|
||||
} else if(!strncmp(denotation, "id3:", 7)) {
|
||||
return FieldId::fromNativeField<Id3v2Tag>(denotation + 4, denotationSize - 4);
|
||||
} else if(!strncmp(denotation, "generic:", 8)) {
|
||||
// allow prefix 'generic:' for consistency
|
||||
denotation += 8, denotationSize -= 8;
|
||||
}
|
||||
|
||||
// determine KnownField for generic denotation
|
||||
if(!strncmp(denotation, "title", denotationSize)) {
|
||||
return KnownField::Title;
|
||||
} else if(!strncmp(denotation, "album", denotationSize)) {
|
||||
return KnownField::Album;
|
||||
} else if(!strncmp(denotation, "artist", denotationSize)) {
|
||||
return KnownField::Artist;
|
||||
} else if(!strncmp(denotation, "genre", denotationSize)) {
|
||||
return KnownField::Genre;
|
||||
} else if(!strncmp(denotation, "year", denotationSize)) {
|
||||
return KnownField::Year;
|
||||
} else if(!strncmp(denotation, "comment", denotationSize)) {
|
||||
return KnownField::Comment;
|
||||
} else if(!strncmp(denotation, "bpm", denotationSize)) {
|
||||
return KnownField::Bpm;
|
||||
} else if(!strncmp(denotation, "bps", denotationSize)) {
|
||||
return KnownField::Bps;
|
||||
} else if(!strncmp(denotation, "lyricist", denotationSize)) {
|
||||
return KnownField::Lyricist;
|
||||
} else if(!strncmp(denotation, "track", denotationSize)) {
|
||||
return KnownField::TrackPosition;
|
||||
} else if(!strncmp(denotation, "disk", denotationSize)) {
|
||||
return KnownField::DiskPosition;
|
||||
} else if(!strncmp(denotation, "part", denotationSize)) {
|
||||
return KnownField::PartNumber;
|
||||
} else if(!strncmp(denotation, "totalparts", denotationSize)) {
|
||||
return KnownField::TotalParts;
|
||||
} else if(!strncmp(denotation, "encoder", denotationSize)) {
|
||||
return KnownField::Encoder;
|
||||
} else if(!strncmp(denotation, "recorddate", denotationSize)) {
|
||||
return KnownField::RecordDate;
|
||||
} else if(!strncmp(denotation, "performers", denotationSize)) {
|
||||
return KnownField::Performers;
|
||||
} else if(!strncmp(denotation, "duration", denotationSize)) {
|
||||
return KnownField::Length;
|
||||
} else if(!strncmp(denotation, "language", denotationSize)) {
|
||||
return KnownField::Language;
|
||||
} else if(!strncmp(denotation, "encodersettings", denotationSize)) {
|
||||
return KnownField::EncoderSettings;
|
||||
} else if(!strncmp(denotation, "lyrics", denotationSize)) {
|
||||
return KnownField::Lyrics;
|
||||
} else if(!strncmp(denotation, "synchronizedlyrics", denotationSize)) {
|
||||
return KnownField::SynchronizedLyrics;
|
||||
} else if(!strncmp(denotation, "grouping", denotationSize)) {
|
||||
return KnownField::Grouping;
|
||||
} else if(!strncmp(denotation, "recordlabel", denotationSize)) {
|
||||
return KnownField::RecordLabel;
|
||||
} else if(!strncmp(denotation, "cover", denotationSize)) {
|
||||
return KnownField::Cover;
|
||||
} else if(!strncmp(denotation, "composer", denotationSize)) {
|
||||
return KnownField::Composer;
|
||||
} else if(!strncmp(denotation, "rating", denotationSize)) {
|
||||
return KnownField::Rating;
|
||||
} else if(!strncmp(denotation, "description", denotationSize)) {
|
||||
return KnownField::Description;
|
||||
} else {
|
||||
throw ConversionException("generic field name is unknown");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<const TagValue *> FieldId::values(const Tag *tag, TagType tagType) const
|
||||
{
|
||||
if(m_nativeField) {
|
||||
return m_valuesForNativeField(tag, tagType);
|
||||
} else {
|
||||
return tag->values(m_knownField);
|
||||
}
|
||||
}
|
||||
|
||||
bool FieldId::setValues(Tag *tag, TagType tagType, const std::vector<TagValue> &values) const
|
||||
{
|
||||
if(m_nativeField) {
|
||||
return m_setValuesForNativeField(tag, tagType, values);
|
||||
} else {
|
||||
return tag->setValues(m_knownField, values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
81
cli/helper.h
81
cli/helper.h
|
@ -1,6 +1,8 @@
|
|||
#ifndef CLI_HELPER
|
||||
#define CLI_HELPER
|
||||
|
||||
#include "../application/knownfieldmodel.h"
|
||||
|
||||
#include <tagparser/tag.h>
|
||||
|
||||
#include <c++utilities/application/commandlineutils.h>
|
||||
|
@ -10,6 +12,7 @@
|
|||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
|
||||
namespace ApplicationUtilities {
|
||||
class Argument;
|
||||
|
@ -49,29 +52,61 @@ inline TagType &operator|= (TagType &lhs, TagType rhs)
|
|||
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
struct FieldId
|
||||
class FieldId
|
||||
{
|
||||
constexpr FieldId(KnownField field);
|
||||
constexpr FieldId(const char *field);
|
||||
KnownField genericField;
|
||||
const char *nativeField;
|
||||
public:
|
||||
FieldId(KnownField m_knownField = KnownField::Invalid);
|
||||
static FieldId fromDenotation(const char *denotation, std::size_t denotationSize);
|
||||
bool operator ==(const FieldId &other) const;
|
||||
KnownField knownField() const;
|
||||
const char *nativeField() const;
|
||||
const char *name() const;
|
||||
std::vector<const TagValue *> values(const Tag *tag, TagType tagType) const;
|
||||
bool setValues(Tag *tag, TagType tagType, const std::vector<TagValue> &values) const;
|
||||
|
||||
private:
|
||||
typedef std::function<std::vector<const TagValue *>(const Tag *, TagType)> GetValuesForNativeFieldType;
|
||||
typedef std::function<bool(Tag *, TagType, const std::vector<TagValue> &)> SetValuesForNativeFieldType;
|
||||
FieldId(const char *m_nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField);
|
||||
template<class ConcreteTag>
|
||||
static FieldId fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize = std::string::npos);
|
||||
|
||||
KnownField m_knownField;
|
||||
const char *m_nativeField;
|
||||
GetValuesForNativeFieldType m_valuesForNativeField;
|
||||
SetValuesForNativeFieldType m_setValuesForNativeField;
|
||||
};
|
||||
|
||||
constexpr FieldId::FieldId(KnownField field) :
|
||||
genericField(field),
|
||||
nativeField(nullptr)
|
||||
inline FieldId::FieldId(KnownField knownField) :
|
||||
m_knownField(knownField),
|
||||
m_nativeField(nullptr)
|
||||
{}
|
||||
|
||||
constexpr FieldId::FieldId(const char *field) :
|
||||
genericField(KnownField::Invalid),
|
||||
nativeField(field)
|
||||
{}
|
||||
inline bool FieldId::operator ==(const FieldId &other) const
|
||||
{
|
||||
return m_knownField == other.m_knownField && m_nativeField == other.m_nativeField;
|
||||
}
|
||||
|
||||
inline KnownField FieldId::knownField() const
|
||||
{
|
||||
return m_knownField;
|
||||
}
|
||||
|
||||
inline const char *FieldId::nativeField() const
|
||||
{
|
||||
return m_nativeField;
|
||||
}
|
||||
|
||||
inline const char *FieldId::name() const
|
||||
{
|
||||
return m_nativeField ? m_nativeField : Settings::KnownFieldModel::fieldName(m_knownField);
|
||||
}
|
||||
|
||||
struct FieldScope
|
||||
{
|
||||
FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget());
|
||||
bool operator ==(const FieldScope &other) const;
|
||||
KnownField field;
|
||||
FieldId field;
|
||||
TagType tagType;
|
||||
TagTarget tagTarget;
|
||||
};
|
||||
|
@ -142,7 +177,7 @@ template <> struct hash<TagTarget::IdContainerType>
|
|||
|
||||
template <> struct hash<TagTarget>
|
||||
{
|
||||
std::size_t operator()(const TagTarget& target) const
|
||||
std::size_t operator()(const TagTarget &target) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<uint64>()(target.level())
|
||||
|
@ -151,12 +186,22 @@ template <> struct hash<TagTarget>
|
|||
}
|
||||
};
|
||||
|
||||
template <> struct hash<FieldScope>
|
||||
template <> struct hash<FieldId>
|
||||
{
|
||||
std::size_t operator()(const FieldScope& scope) const
|
||||
std::size_t operator()(const FieldId &id) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<KnownField>()(scope.field)
|
||||
return (hash<KnownField>()(id.knownField())
|
||||
^ (hash<const char *>()(id.nativeField()) << 1));
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<FieldScope>
|
||||
{
|
||||
std::size_t operator()(const FieldScope &scope) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<FieldId>()(scope.field)
|
||||
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
|
||||
^ (hash<TagTarget>()(scope.tagTarget) << 1);
|
||||
}
|
||||
|
@ -210,7 +255,7 @@ inline void printProperty(const char *propName, const intType value, const char
|
|||
}
|
||||
}
|
||||
|
||||
void printField(const FieldScope &scope, const Tag *tag, bool skipEmpty);
|
||||
void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty);
|
||||
|
||||
TagUsage parseUsageDenotation(const ApplicationUtilities::Argument &usageArg, TagUsage defaultUsage);
|
||||
TagTextEncoding parseEncodingDenotation(const ApplicationUtilities::Argument &encodingArg, TagTextEncoding defaultEncoding);
|
||||
|
|
|
@ -47,12 +47,16 @@ using namespace Utility;
|
|||
|
||||
namespace Cli {
|
||||
|
||||
#define FIELD_NAMES "title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
|
||||
#define FIELD_NAMES \
|
||||
"title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
|
||||
"recorddate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" \
|
||||
"recordlabel cover composer rating description"
|
||||
|
||||
#define TAG_MODIFIER "tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all"
|
||||
#define TARGET_MODIFIER "target-level target-levelname target-tracks target-tracks\n" \
|
||||
#define TAG_MODIFIER \
|
||||
"tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all"
|
||||
|
||||
#define TARGET_MODIFIER \
|
||||
"target-level target-levelname target-tracks target-tracks\n" \
|
||||
"target-chapters target-editions target-attachments target-reset"
|
||||
|
||||
const char *const fieldNames = FIELD_NAMES;
|
||||
|
@ -272,13 +276,13 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
|
|||
// iterate through fields specified by the user
|
||||
if(fields.empty()) {
|
||||
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
|
||||
printField(FieldScope(field), tag, true);
|
||||
printField(FieldScope(field), tag, tagType, true);
|
||||
}
|
||||
} else {
|
||||
for(const auto &fieldDenotation : fields) {
|
||||
const FieldScope &denotedScope = fieldDenotation.first;
|
||||
if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) {
|
||||
printField(denotedScope, tag, false);
|
||||
printField(denotedScope, tag, tagType, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -487,7 +491,11 @@ void setTagInfo(const SetTagInfoArgs &args)
|
|||
}
|
||||
}
|
||||
// finally set the values
|
||||
tag->setValues(denotedScope.field, convertedValues);
|
||||
try {
|
||||
denotedScope.field.setValues(tag, tagType, convertedValues);
|
||||
} catch(const ConversionException &e) {
|
||||
fileInfo.addNotification(NotificationType::Critical, "Unable to parse denoted field ID \"" + string(denotedScope.field.name()) + "\": " + e.what(), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -599,10 +607,14 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
|
|||
vector<pair<const TagValue *, string> > values;
|
||||
// iterate through all tags
|
||||
for(const Tag *tag : tags) {
|
||||
for(const auto &fieldDenotation : fieldDenotations) {
|
||||
const auto &value = tag->value(fieldDenotation.first.field);
|
||||
if(!value.isEmpty()) {
|
||||
values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
|
||||
const TagType tagType = tag->type();
|
||||
for(const pair<FieldScope, FieldValues> &fieldDenotation : fieldDenotations) {
|
||||
try {
|
||||
for(const TagValue *value : fieldDenotation.first.field.values(tag, tagType)) {
|
||||
values.emplace_back(value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
|
||||
}
|
||||
} catch(const ConversionException &e) {
|
||||
inputFileInfo.addNotification(NotificationType::Critical, "Unable to parse denoted field ID \"" + string(fieldDenotation.first.field.name()) + "\": " + e.what(), "extracting field");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
105
tests/cli.cpp
105
tests/cli.cpp
|
@ -32,6 +32,7 @@ class CliTests : public TestFixture
|
|||
CPPUNIT_TEST_SUITE(CliTests);
|
||||
#ifdef PLATFORM_UNIX
|
||||
CPPUNIT_TEST(testBasicReadingAndWriting);
|
||||
CPPUNIT_TEST(testSpecifyingNativeFieldIds);
|
||||
CPPUNIT_TEST(testHandlingOfTargets);
|
||||
CPPUNIT_TEST(testId3SpecificOptions);
|
||||
CPPUNIT_TEST(testMultipleFiles);
|
||||
|
@ -51,6 +52,7 @@ public:
|
|||
|
||||
#ifdef PLATFORM_UNIX
|
||||
void testBasicReadingAndWriting();
|
||||
void testSpecifyingNativeFieldIds();
|
||||
void testHandlingOfTargets();
|
||||
void testId3SpecificOptions();
|
||||
void testMultipleFiles();
|
||||
|
@ -76,6 +78,34 @@ void CliTests::tearDown()
|
|||
{}
|
||||
|
||||
#ifdef PLATFORM_UNIX
|
||||
template <typename StringType, bool negateErrorCond = false>
|
||||
bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
|
||||
{
|
||||
bool res = containsSubstrings(str, substrings);
|
||||
if(negateErrorCond) {
|
||||
res = !res;
|
||||
}
|
||||
if(!res) {
|
||||
if(!negateErrorCond) {
|
||||
cout << " - test failed: output does NOT contain required substrings\n";
|
||||
} else {
|
||||
cout << " - test failed: output DOES contain substrings it shouldn't\n";
|
||||
}
|
||||
cout << "Output:\n" << str;
|
||||
cout << "Substrings:\n";
|
||||
for(const auto &substr : substrings) {
|
||||
cout << substr << "\n";
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename StringType>
|
||||
bool testNotContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
|
||||
{
|
||||
return testContainsSubstrings<StringType, true>(str, substrings);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests basic reading and writing of tags.
|
||||
*/
|
||||
|
@ -90,7 +120,7 @@ void CliTests::testBasicReadingAndWriting()
|
|||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
// context of the following fields is the album (so "Title" means the title of the album)
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"album",
|
||||
"Title Elephant Dream - test 2"
|
||||
}));
|
||||
|
@ -100,7 +130,7 @@ void CliTests::testBasicReadingAndWriting()
|
|||
const char *const args2[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Title Elephant Dream - test 2",
|
||||
"Year 2010",
|
||||
"Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC"
|
||||
|
@ -112,7 +142,7 @@ void CliTests::testBasicReadingAndWriting()
|
|||
CPPUNIT_ASSERT(stdout.find("Changes have been applied") != string::npos);
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Title A new title",
|
||||
"Genre Testfile",
|
||||
"Year 2010",
|
||||
|
@ -126,7 +156,7 @@ void CliTests::testBasicReadingAndWriting()
|
|||
TESTUTILS_ASSERT_EXEC(args4);
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Title Foo",
|
||||
"Artist Bar"
|
||||
}));
|
||||
|
@ -134,10 +164,49 @@ void CliTests::testBasicReadingAndWriting()
|
|||
CPPUNIT_ASSERT(stdout.find("Comment") == string::npos);
|
||||
CPPUNIT_ASSERT(stdout.find("Genre") == string::npos);
|
||||
|
||||
remove(mkvFile.c_str());
|
||||
remove(mkvFile.data());
|
||||
remove(mkvFileBackup.data());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests specifying native fields IDs when getting and setting fields.
|
||||
*/
|
||||
void CliTests::testSpecifyingNativeFieldIds()
|
||||
{
|
||||
cout << "\nSpecifying native field IDs" << endl;
|
||||
string stdout, stderr;
|
||||
|
||||
// get specific field
|
||||
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
|
||||
const string mkvFileBackup(mkvFile + ".bak");
|
||||
const string mp4File(workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a"));
|
||||
const string mp4FileBackup(mp4File + ".bak");
|
||||
const char *const args1[] = {"tageditor", "set", "mkv:FOO=bar", "mp4:©foo=bar", "mp4:invalid", "-f", mkvFile.data(), mp4File.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
// FIXME: provide a way to specify raw data type
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"making MP4 tag field ©foo: It was not possible to find an appropriate raw data type id. UTF-8 will be assumed."}));
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Unable to parse denoted field ID \"invalid\": MP4 ID must be exactly 4 chars"}));
|
||||
|
||||
const char *const args2[] = {"tageditor", "get", "mkv:FOO", "mp4:©foo", "generic:year", "-f", mkvFile.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Year 2010"}));
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"FOO bar"}));
|
||||
|
||||
const char *const args3[] = {"tageditor", "get", "mkv:FOO", "mp4:©foo", "mp4:invalid", "generic:year", "-f", mp4File.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args3);
|
||||
CPPUNIT_ASSERT(stderr.empty());
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"test"}));
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Year none"}));
|
||||
// FIXME: number of whitespaces currently not correct because UTF-8 ©-sign counts as two characters
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"©foo bar"}));
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"invalid unable to parse - MP4 ID must be exactly 4 chars"}));
|
||||
|
||||
remove(mkvFile.data()), remove(mkvFileBackup.data());
|
||||
remove(mp4File.data()), remove(mp4FileBackup.data());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests adding and removing of targets.
|
||||
*/
|
||||
|
@ -153,12 +222,12 @@ void CliTests::testHandlingOfTargets()
|
|||
const char *const args2[] = {"tageditor", "set", "target-level=30", "title=The song title", "genre=The song genre", "target-level=50", "genre=The album genre", "-f", mkvFile.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"song",
|
||||
"Title The song title",
|
||||
"Genre The song genre"
|
||||
}));
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"album",
|
||||
"Title Elephant Dream - test 2",
|
||||
"Genre The album genre"
|
||||
|
@ -169,10 +238,10 @@ void CliTests::testHandlingOfTargets()
|
|||
const char *const args3[] = {"tageditor", "set", "target-level=30", "target-tracks=3134325680", "title=The audio track", "encoder=likely some AAC encoder", "--remove-target", "target-level=30", "--remove-target", "target-level=50", "-f", mkvFile.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args3);
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {"song"}));
|
||||
CPPUNIT_ASSERT(!containsSubstrings(stdout, {"song", "song"}));
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"song"}));
|
||||
CPPUNIT_ASSERT(testNotContainsSubstrings(stdout, {"song", "song"}));
|
||||
CPPUNIT_ASSERT(stdout.find("album") == string::npos);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"3134325680",
|
||||
"Title The audio track",
|
||||
"Encoder likely some AAC encoder"
|
||||
|
@ -270,7 +339,7 @@ void CliTests::testMultipleFiles()
|
|||
// get tags of 3 files at once
|
||||
const char *const args1[] = {"tageditor", "get", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Title Big Buck Bunny - test 1",
|
||||
"Title Elephant Dream - test 2",
|
||||
"Title Elephant Dream - test 3"
|
||||
|
@ -282,7 +351,7 @@ void CliTests::testMultipleFiles()
|
|||
const char *const args2[] = {"tageditor", "set", "target-level=30", "title=test1", "title=test2", "title=test3", "part+=1", "target-level=50", "title=MKV testfiles", "totalparts=3", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n"
|
||||
" Title MKV testfiles\n"
|
||||
" Year 2010\n"
|
||||
|
@ -337,7 +406,7 @@ void CliTests::testOutputFile()
|
|||
// specified output files contain new titles
|
||||
const char *const args3[] = {"tageditor", "get", "-f", "/tmp/test1.mkv", "/tmp/test2.mkv", nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args3);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
|
||||
" Title test1\n",
|
||||
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
|
||||
|
@ -362,7 +431,7 @@ void CliTests::testMultipleValuesPerField()
|
|||
|
||||
const char *const args2[] = {"tageditor", "get", "-f", mkvFile1.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Artist test1",
|
||||
"Artist test2",
|
||||
"Artist test3"
|
||||
|
@ -396,7 +465,7 @@ void CliTests::testHandlingAttachments()
|
|||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
const char *const args1[] = {"tageditor", "info", "-f", mkvFile1.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Attachments:",
|
||||
"Name test2.mkv",
|
||||
"MIME-type video/x-matroska",
|
||||
|
@ -410,7 +479,7 @@ void CliTests::testHandlingAttachments()
|
|||
const char *const args3[] = {"tageditor", "set", "--update-attachment", "name=test2.mkv", "desc=Updated test attachment", "-f", mkvFile1.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args3);
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
"Attachments:",
|
||||
"Name test2.mkv",
|
||||
"MIME-type video/x-matroska",
|
||||
|
@ -457,7 +526,7 @@ void CliTests::testDisplayingInfo()
|
|||
const string mkvFile(testFilePath("matroska_wave1/test2.mkv"));
|
||||
const char *const args1[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args1);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
" Container format: Matroska\n"
|
||||
" Document type matroska\n"
|
||||
" Read version 1\n"
|
||||
|
@ -486,7 +555,7 @@ void CliTests::testDisplayingInfo()
|
|||
const string mp4File(testFilePath("mtx-test-data/aac/he-aacv2-ps.m4a"));
|
||||
const char *const args2[] = {"tageditor", "info", "-f", mp4File.data(), nullptr};
|
||||
TESTUTILS_ASSERT_EXEC(args2);
|
||||
CPPUNIT_ASSERT(containsSubstrings(stdout, {
|
||||
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
|
||||
" Container format: MPEG-4 Part 14\n"
|
||||
" Document type mp42\n"
|
||||
" Duration 3 min\n"
|
||||
|
|
Loading…
Reference in New Issue