diff --git a/CMakeLists.txt b/CMakeLists.txt index 51860f1..2064f68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/cli/helper.cpp b/cli/helper.cpp index c49f561..617751d 100644 --- a/cli/helper.cpp +++ b/cli/helper.cpp @@ -1,8 +1,10 @@ #include "./helper.h" -#include "../application/knownfieldmodel.h" - #include +#include +#include +#include +#include #include @@ -10,6 +12,7 @@ #include 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 +std::vector valuesForNativeField(const char *idString, std::size_t idStringSize, const Tag *tag, TagType tagType) +{ + if(tagType != ConcreteTag::tagType) { + return vector(); + } + return static_cast(tag)->values(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize)); +} + +template +bool setValuesForNativeField(const char *idString, std::size_t idStringSize, Tag *tag, TagType tagType, const std::vector &values) +{ + if(tagType != ConcreteTag::tagType) { + return false; + } + return static_cast(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 +FieldId FieldId::fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize) +{ + return FieldId( + nativeFieldId, + bind(&valuesForNativeField, nativeFieldId, nativeFieldIdSize, _1, _2), + bind(&setValuesForNativeField, 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(denotation + 4, denotationSize - 4); + } else if(!strncmp(denotation, "mp4:", 4)) { + return FieldId::fromNativeField(denotation + 4, denotationSize - 4); + } else if(!strncmp(denotation, "vorbis:", 7)) { + return FieldId::fromNativeField(denotation + 7, denotationSize - 7); + } else if(!strncmp(denotation, "id3:", 7)) { + return FieldId::fromNativeField(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 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 &values) const +{ + if(m_nativeField) { + return m_setValuesForNativeField(tag, tagType, values); + } else { + return tag->setValues(m_knownField, values); + } +} + } diff --git a/cli/helper.h b/cli/helper.h index df0ab41..439d0a6 100644 --- a/cli/helper.h +++ b/cli/helper.h @@ -1,6 +1,8 @@ #ifndef CLI_HELPER #define CLI_HELPER +#include "../application/knownfieldmodel.h" + #include #include @@ -10,6 +12,7 @@ #include #include +#include namespace ApplicationUtilities { class Argument; @@ -49,29 +52,61 @@ inline TagType &operator|= (TagType &lhs, TagType rhs) return lhs = static_cast(static_cast(lhs) | static_cast(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 values(const Tag *tag, TagType tagType) const; + bool setValues(Tag *tag, TagType tagType, const std::vector &values) const; + +private: + typedef std::function(const Tag *, TagType)> GetValuesForNativeFieldType; + typedef std::function &)> SetValuesForNativeFieldType; + FieldId(const char *m_nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField); + template + 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 template <> struct hash { - std::size_t operator()(const TagTarget& target) const + std::size_t operator()(const TagTarget &target) const { using std::hash; return ((hash()(target.level()) @@ -151,12 +186,22 @@ template <> struct hash } }; -template <> struct hash +template <> struct hash { - std::size_t operator()(const FieldScope& scope) const + std::size_t operator()(const FieldId &id) const { using std::hash; - return ((hash()(scope.field) + return (hash()(id.knownField()) + ^ (hash()(id.nativeField()) << 1)); + } +}; + +template <> struct hash +{ + std::size_t operator()(const FieldScope &scope) const + { + using std::hash; + return ((hash()(scope.field) ^ (hash()(scope.tagType) << 1)) >> 1) ^ (hash()(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); diff --git a/cli/mainfeatures.cpp b/cli/mainfeatures.cpp index 8b43418..effccd9 100644 --- a/cli/mainfeatures.cpp +++ b/cli/mainfeatures.cpp @@ -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 > 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 &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"); } } } diff --git a/tests/cli.cpp b/tests/cli.cpp index 872772c..3686faf 100644 --- a/tests/cli.cpp +++ b/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 +bool testContainsSubstrings(const StringType &str, std::initializer_list 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 +bool testNotContainsSubstrings(const StringType &str, std::initializer_list substrings) +{ + return testContainsSubstrings(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"