From 6d8833fe9d6f37ceb8592d4e041cb07e8f7df1dc Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 30 Jul 2016 23:17:49 +0200 Subject: [PATCH] Improve CLI - Implement tests (not everything covered yet) - Improve documentation in README.md - Fix issues when editing tags of multiple files at once --- CMakeLists.txt | 8 ++ README.md | 50 ++++++-- cli/mainfeatures.cpp | 272 ++++++++++++++++++++++++++++--------------- tests/cli.cpp | 226 +++++++++++++++++++++++++++++++++++ tests/cppunit.cpp | 1 + 5 files changed, 453 insertions(+), 104 deletions(-) create mode 100644 tests/cli.cpp create mode 100644 tests/cppunit.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e580bcb..6cf2376 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,13 @@ set(WIDGETS_UI_FILES # resources/icons.qrc #) +set(TEST_HEADER_FILES +) +set(TEST_SRC_FILES + tests/cppunit.cpp + tests/cli.cpp +) + set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts @@ -207,6 +214,7 @@ if(WIDGETS_GUI OR QUICK_GUI) include(QtConfig) endif() include(WindowsResources) +include(TestTarget) include(AppTarget) include(ShellCompletion) include(ConfigHeader) diff --git a/README.md b/README.md index cc28941..8b1f01b 100644 --- a/README.md +++ b/README.md @@ -80,33 +80,59 @@ Checkout the available operations and options with --help. #### Examples Here are some Bash examples which illustrate getting and setting tag information: +##### Reading tags * *Displays* title, album and artist of all *.m4a files in the specified directory: - ``` tageditor get title album artist --files /some/dir/*.m4a ``` - **Note**: All values are printed in UTF-8 encoding, no matter which encoding is actually used within the tag. +* *Displays* all supported fields of all *.mkv files in the specified directory: + ``` + tageditor get --files /some/dir/*.mkv + ``` + + + * *Displays* technical information about all *.m4a files in the specified directory: + ``` + tageditor info --files /some/dir/*.m4a + ``` * *Displays* technical information about all *.m4a files in the specified directory: - ``` tageditor info --files /some/dir/*.m4a ``` +**Note**: All values are printed in UTF-8 encoding, no matter which encoding is actually used within the tag. + +##### Writing tags * *Sets* title, album, artist, cover and track number of all *.m4a files in the specified directory: ``` - tageditor set "title=Title of "{1st,2nd,3rd}" file" "title=Title of "{4..16}"th file" \ - "album=The Album" "artist=The Artist" \ + tageditor set title="Title of "{1st,2nd,3rd}" file" title="Title of "{4..16}"th file" \ + album="The Album" artist="The Artist" \ cover=/path/to/image track={1..16}/16 --files /some/dir/*.m4a ``` - The first file will get the name *Title of 1st file*, the second file will get the name *Title of 2nd file* and so on. - The 16th and following files will all get the name *Title of the 16th file*. The same scheme is used for the track numbers. - All files will get the album name *The Album*, the artist *The Artist* and the cover image from the file */path/to/image*. + - The first file will get the title *Title of 1st file*, the second file will get the name *Title of 2nd file* and so on. + - The 16th and following files will all get the title *Title of the 16th file*. + - The same scheme is used for the track numbers. + - All files will get the album name *The Album*, the artist *The Artist* and the cover image from the file */path/to/image*. - **Note**: All specified values are assumed to be UTF-8 encoded, no matter which encoding has been specified as preferred encoding via ``--encoding`` option. (This mentioned option only affects the encoding to be used *within* the tag.) +* *Sets* title of both specified files and the album of the second specified file: + ``` + tageditor set title0="Title for both files" album1="Album for 2nd file" \ + --files file1.ogg file2.mp3 + ``` + The number after the field name specifies the index of the first file to use the value for. The first index is 0. + +* *Sets* the title specificly for the track with the ID ``3134325680`` and removes + the tags targeting the song/track and the album/movie/episode in general: + ``` + tageditor set target-level=30 target-tracks=3134325680 title="Title for track 3134325680" \ + --remove-targets target-level=50 , target-level=30 \ + --files file.mka + ``` + For more information checkout the [Matroska specification](https://matroska.org/technical/specs/tagging/index.html). * Here is another example, demonstrating the use of arrays and the syntax to auto-increase numeric fields such as the track number: @@ -122,12 +148,14 @@ Here are some Bash examples which illustrate getting and setting tag information titles+=("title=${title%.*}"); \ done # now set the titles and other tag information - tageditor set "${titles[@]}" "album=Some Album" track+=1/25 disk=1/1 -f *.m4a + tageditor set "${titles[@]}" album="Some Album" track+=1/25 disk=1/1 -f *.m4a ``` - Note the *+* sign after the field name *track* which indicates that the field value should be increased after + **Note**: The *+* sign after the field name *track* which indicates that the field value should be increased after a file has been processed. +**Note**: All specified values are assumed to be UTF-8 encoded, no matter which encoding has been specified as preferred encoding via ``--encoding`` option. (This mentioned option only affects the encoding to be used *within* the tag.) + ## Build instructions The application depends on [c++utilities](https://github.com/Martchus/cpp-utilities) and [tagparser](https://github.com/Martchus/tagparser) and is built the same way as these libaries. For basic instructions checkout the README file of [c++utilities](https://github.com/Martchus/cpp-utilities). diff --git a/cli/mainfeatures.cpp b/cli/mainfeatures.cpp index 0d24e2a..2209bb0 100644 --- a/cli/mainfeatures.cpp +++ b/cli/mainfeatures.cpp @@ -27,6 +27,7 @@ #include #include #include +#include using namespace std; using namespace ApplicationUtilities; @@ -41,6 +42,8 @@ using namespace Utility; namespace Cli { +// define enums, operators and structs to handle specified field denotations + enum class DenotationType { Normal, @@ -53,28 +56,99 @@ inline TagType operator| (TagType lhs, TagType rhs) return static_cast(static_cast(lhs) | static_cast(rhs)); } +inline TagType operator& (TagType lhs, TagType rhs) +{ + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + inline TagType &operator|= (TagType &lhs, TagType rhs) { return lhs = static_cast(static_cast(lhs) | static_cast(rhs)); } -struct FieldDenotation +struct FieldScope { - FieldDenotation(KnownField field); + FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget()); + bool operator ==(const FieldScope &other) const; KnownField field; - DenotationType type; TagType tagType; TagTarget tagTarget; - vector > values; }; -FieldDenotation::FieldDenotation(KnownField field) : +FieldScope::FieldScope(KnownField field, TagType tagType, TagTarget tagTarget) : field(field), - type(DenotationType::Normal), - tagType(TagType::Unspecified) + tagType(tagType), + tagTarget(tagTarget) {} -inline bool isDigit(char c) +bool FieldScope::operator ==(const FieldScope &other) const +{ + return field == other.field && tagType == other.tagType && tagTarget == other.tagTarget; +} + +struct FieldValue +{ + FieldValue(DenotationType type, unsigned int fileIndex, const char *value); + DenotationType type; + unsigned int fileIndex; + string value; +}; + +inline FieldValue::FieldValue(DenotationType type, unsigned int fileIndex, const char *value) : + type(type), + fileIndex(fileIndex), + value(value) +{} + +} + +namespace std { + +using namespace Cli; + +template <> struct hash +{ + std::size_t operator()(const TagTarget::IdContainerType &ids) const + { + using std::hash; + auto seed = ids.size(); + for(auto id : ids) { + seed ^= id + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } +}; + +template <> struct hash +{ + std::size_t operator()(const TagTarget& target) const + { + using std::hash; + return ((hash()(target.level()) + ^ (hash()(target.tracks()) << 1)) >> 1) + ^ (hash()(target.attachments()) << 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); + } +}; + +} + +namespace Cli { + +typedef vector FieldValues; +typedef unordered_map FieldDenotations; + +constexpr bool isDigit(char c) { return c >= '0' && c <= '9'; } @@ -98,6 +172,9 @@ string incremented(const string &str, unsigned int toIncrement = 1) res += c; } } + if(hasValue) { + res += numberToString(value + 1); + } return res; } @@ -116,7 +193,7 @@ void printNotifications(NotificationList ¬ifications, const char *head = null return; } if(!notifications.empty()) { - printNotifications: +printNotifications: if(head) { cout << head << endl; } @@ -168,7 +245,7 @@ void printFieldNames(const ArgumentOccurance &occurance) { CMD_UTILS_START_CONSOLE; VAR_UNUSED(occurance) - cout << fieldNames << endl; + cout << fieldNames << endl; } TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage) @@ -286,18 +363,16 @@ bool applyTargetConfiguration(TagTarget &target, const std::string &configStr) } } -vector parseFieldDenotations(const Argument &fieldsArg, bool readOnly) +FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) { - vector fields; + FieldDenotations fields; if(fieldsArg.isPresent()) { const vector &fieldDenotations = fieldsArg.values(); - fields.reserve(fieldDenotations.size()); - TagType currentTagType = TagType::Unspecified; - TagTarget currentTagTarget; + FieldScope scope; for(const char *fieldDenotationString : fieldDenotations) { // check for tag or target specifier const auto fieldDenotationLen = strlen(fieldDenotationString); - if(!strncmp(fieldDenotationString, "tag:", 4)) { + if(!strncmp(fieldDenotationString, "tag=", 4)) { if(fieldDenotationLen == 4) { cerr << "Warning: The \"tag\"-specifier has been used with no value(s) and hence is ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl; } else { @@ -320,14 +395,14 @@ vector parseFieldDenotations(const Argument &fieldsArg, bool re break; } else { cerr << "Warning: The value provided with the \"tag\"-specifier is invalid and will be ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl; - tagType = currentTagType; + tagType = scope.tagType; break; } } - currentTagType = tagType; + scope.tagType = tagType; break; } - } else if(applyTargetConfiguration(currentTagTarget, fieldDenotationString)) { + } else if(applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) { continue; } // read field name @@ -359,78 +434,77 @@ vector parseFieldDenotations(const Argument &fieldsArg, bool re continue; } // parse the denoted filed - KnownField field; if(!strncmp(fieldDenotationString, "title", fieldNameLen)) { - field = KnownField::Title; + scope.field = KnownField::Title; } else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) { - field = KnownField::Album; + scope.field = KnownField::Album; } else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) { - field = KnownField::Artist; + scope.field = KnownField::Artist; } else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) { - field = KnownField::Genre; + scope.field = KnownField::Genre; } else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) { - field = KnownField::Year; + scope.field = KnownField::Year; } else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) { - field = KnownField::Comment; + scope.field = KnownField::Comment; } else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) { - field = KnownField::Bpm; + scope.field = KnownField::Bpm; } else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) { - field = KnownField::Bps; + scope.field = KnownField::Bps; } else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) { - field = KnownField::Lyricist; + scope.field = KnownField::Lyricist; } else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) { - field = KnownField::TrackPosition; + scope.field = KnownField::TrackPosition; } else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) { - field = KnownField::DiskPosition; + scope.field = KnownField::DiskPosition; } else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) { - field = KnownField::PartNumber; + scope.field = KnownField::PartNumber; } else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) { - field = KnownField::TotalParts; + scope.field = KnownField::TotalParts; } else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) { - field = KnownField::Encoder; + scope.field = KnownField::Encoder; } else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) { - field = KnownField::RecordDate; + scope.field = KnownField::RecordDate; } else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) { - field = KnownField::Performers; + scope.field = KnownField::Performers; } else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) { - field = KnownField::Length; + scope.field = KnownField::Length; } else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) { - field = KnownField::Language; + scope.field = KnownField::Language; } else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) { - field = KnownField::EncoderSettings; + scope.field = KnownField::EncoderSettings; } else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) { - field = KnownField::Lyrics; + scope.field = KnownField::Lyrics; } else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) { - field = KnownField::SynchronizedLyrics; + scope.field = KnownField::SynchronizedLyrics; } else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) { - field = KnownField::Grouping; + scope.field = KnownField::Grouping; } else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) { - field = KnownField::RecordLabel; + scope.field = KnownField::RecordLabel; } else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) { - field = KnownField::Cover; + scope.field = KnownField::Cover; type = DenotationType::File; // read cover always from file } else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) { - field = KnownField::Composer; + scope.field = KnownField::Composer; } else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) { - field = KnownField::Rating; + scope.field = KnownField::Rating; } else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) { - field = KnownField::Description; + 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; continue; } - // add field denotation with parsed values - fields.emplace_back(field); - FieldDenotation &fieldDenotation = fields.back(); - fieldDenotation.type = type; - fieldDenotation.tagType = currentTagType; - fieldDenotation.tagTarget = currentTagTarget; + // add field denotation scope + auto &fieldValues = fields[scope]; + // add value to the scope (if present) if(equationPos) { if(readOnly) { cerr << "Warning: Specified value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl; } else { - fieldDenotation.values.emplace_back(make_pair(mult == 1 ? fieldDenotation.values.size() : fileIndex, (equationPos + 1))); + // file index might have been specified explicitely + // if not (mult == 1) use the index of the last value and increase it by one + // if there are no previous values, just use the index 0 + fieldValues.emplace_back(FieldValue(type, mult == 1 ? (fieldValues.empty() ? 0 : fieldValues.back().fileIndex + 1) : fileIndex, (equationPos + 1))); } } } @@ -653,7 +727,7 @@ void displayFileInfo(const ArgumentOccurance &, const Argument &filesArg, const { CMD_UTILS_START_CONSOLE; if(!filesArg.isPresent() || filesArg.values().empty()) { - cout << "Error: No files have been specified." << endl; + cerr << "Error: No files have been specified." << endl; return; } MediaFileInfo fileInfo; @@ -781,7 +855,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A { CMD_UTILS_START_CONSOLE; if(!filesArg.isPresent() || filesArg.values().empty()) { - cout << "Error: No files have been specified." << endl; + cerr << "Error: No files have been specified." << endl; return; } const auto fields = parseFieldDenotations(fieldsArg, true); @@ -801,7 +875,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A TagType tagType = tag->type(); // write tag name and target, eg. MP4/iTunes tag cout << tag->typeName(); - if(!tag->target().isEmpty()) { + if(tagType == TagType::MatroskaTag || !tag->target().isEmpty()) { cout << " targeting \"" << tag->targetString() << "\""; } cout << endl; @@ -832,11 +906,12 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A } } } else { - for(const FieldDenotation &fieldDenotation : fields) { - const auto &value = tag->value(fieldDenotation.field); - if(fieldDenotation.tagType == TagType::Unspecified || (fieldDenotation.tagType | tagType) != TagType::Unspecified) { + for(const auto &fieldDenotation : fields) { + const FieldScope &denotedScope = fieldDenotation.first; + const TagValue &value = tag->value(denotedScope.field); + if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) { // write field name - const char *fieldName = KnownFieldModel::fieldName(fieldDenotation.field); + const char *fieldName = KnownFieldModel::fieldName(denotedScope.field); cout << ' ' << fieldName; // write padding for(auto i = strlen(fieldName); i < 18; ++i) { @@ -884,15 +959,16 @@ void setTagInfo(const SetTagInfoArgs &args) return; } auto fields = parseFieldDenotations(args.valuesArg, false); - if(fields.empty() && args.attachmentsArg.values().empty() && args.docTitleArg.values().empty()) { + if(fields.empty() && (!args.removeTargetsArg.isPresent() || args.removeTargetsArg.values().empty()) && (!args.attachmentsArg.isPresent() || args.attachmentsArg.values().empty()) && (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty())) { cerr << "Error: No fields/attachments have been specified." << endl; return; } // determine required targets vector requiredTargets; - for(const FieldDenotation &fieldDenotation : fields) { - if(find(requiredTargets.cbegin(), requiredTargets.cend(), fieldDenotation.tagTarget) == requiredTargets.cend()) { - requiredTargets.push_back(fieldDenotation.tagTarget); + for(const auto &fieldDenotation : fields) { + const FieldScope &scope = fieldDenotation.first; + if(find(requiredTargets.cbegin(), requiredTargets.cend(), scope.tagTarget) == requiredTargets.cend()) { + requiredTargets.push_back(scope.tagTarget); } } // determine targets to remove @@ -984,31 +1060,38 @@ void setTagInfo(const SetTagInfoArgs &args) if(!tags.empty()) { // iterate through all tags for(auto *tag : tags) { + // clear current values if option is present if(args.removeOtherFieldsArg.isPresent()) { tag->removeAllFields(); } - auto tagType = tag->type(); - bool targetSupported = tag->supportsTarget(); - auto tagTarget = tag->target(); - for(FieldDenotation &fieldDenotation : fields) { - if((fieldDenotation.tagType == TagType::Unspecified - || (fieldDenotation.tagType | tagType) != TagType::Unspecified) - && (!targetSupported || fieldDenotation.tagTarget == tagTarget)) { - pair *selectedDenotatedValue = nullptr; - for(auto &someDenotatedValue : fieldDenotation.values) { - if(someDenotatedValue.first <= fileIndex) { - if(!selectedDenotatedValue || (someDenotatedValue.first > selectedDenotatedValue->first)) { - selectedDenotatedValue = &someDenotatedValue; + // determine required information for deciding whether specified values match the scope of the current tag + const auto tagType = tag->type(); + const bool targetSupported = tag->supportsTarget(); + const auto tagTarget = tag->target(); + // iterate through all denoted field values + for(auto &fieldDenotation : fields) { + const FieldScope &denotedScope = fieldDenotation.first; + FieldValues &denotedValues = fieldDenotation.second; + // decide whether the scope of the denotation matches of the current tag + if((denotedScope.tagType == TagType::Unspecified + || (denotedScope.tagType & tagType) != TagType::Unspecified) + && (!targetSupported || denotedScope.tagTarget == tagTarget)) { + // select the value for the current file index + FieldValue *selectedDenotedValue = nullptr; + for(FieldValue &denotatedValue : denotedValues) { + if(denotatedValue.fileIndex <= fileIndex) { + if(!selectedDenotedValue || (denotatedValue.fileIndex > selectedDenotedValue->fileIndex)) { + selectedDenotedValue = &denotatedValue; } } } - if(selectedDenotatedValue) { - if(fieldDenotation.type == DenotationType::File) { - if(selectedDenotatedValue->second.empty()) { - tag->setValue(fieldDenotation.field, TagValue()); - } else { + if(selectedDenotedValue) { + // one of the denoted values + if(!selectedDenotedValue->value.empty()) { + if(selectedDenotedValue->type == DenotationType::File) { try { - MediaFileInfo fileInfo(selectedDenotatedValue->second); + // assume the file refers to a picture + MediaFileInfo fileInfo(selectedDenotedValue->value); fileInfo.open(true); fileInfo.parseContainerFormat(); auto buff = make_unique(fileInfo.size()); @@ -1016,23 +1099,26 @@ void setTagInfo(const SetTagInfoArgs &args) fileInfo.stream().read(buff.get(), fileInfo.size()); TagValue value(move(buff), fileInfo.size(), TagDataType::Picture); value.setMimeType(fileInfo.mimeType()); - tag->setValue(fieldDenotation.field, move(value)); + tag->setValue(denotedScope.field, move(value)); } catch(const Media::Failure &) { fileInfo.addNotification(NotificationType::Critical, "Unable to parse specified cover file.", context); } catch(...) { ::IoUtilities::catchIoFailure(); fileInfo.addNotification(NotificationType::Critical, "An IO error occured when parsing the specified cover file.", context); } + } else { + TagTextEncoding usedEncoding = denotedEncoding; + if(!tag->canEncodingBeUsed(denotedEncoding)) { + usedEncoding = tag->proposedTextEncoding(); + } + tag->setValue(denotedScope.field, TagValue(selectedDenotedValue->value, TagTextEncoding::Utf8, usedEncoding)); + if(selectedDenotedValue->type == DenotationType::Increment && tag == tags.back()) { + selectedDenotedValue->value = incremented(selectedDenotedValue->value); + } } } else { - TagTextEncoding usedEncoding = denotedEncoding; - if(!tag->canEncodingBeUsed(denotedEncoding)) { - usedEncoding = tag->proposedTextEncoding(); - } - tag->setValue(fieldDenotation.field, TagValue(selectedDenotatedValue->second, TagTextEncoding::Utf8, usedEncoding)); - if(fieldDenotation.type == DenotationType::Increment && tag == tags.back()) { - selectedDenotatedValue->second = incremented(selectedDenotatedValue->second); - } + // if the denoted value is empty, just assign an empty TagValue to remove the field + tag->setValue(denotedScope.field, TagValue()); } } } @@ -1140,7 +1226,7 @@ void extractField(const Argument &fieldsArg, const Argument &inputFileArg, const // iterate through all tags for(const Tag *tag : tags) { for(const auto &fieldDenotation : fields) { - const auto &value = tag->value(fieldDenotation.field); + const auto &value = tag->value(fieldDenotation.first.field); if(!value.isEmpty()) { values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-")); } diff --git a/tests/cli.cpp b/tests/cli.cpp new file mode 100644 index 0000000..14d741a --- /dev/null +++ b/tests/cli.cpp @@ -0,0 +1,226 @@ +#include +#include + +#include +#include + +#include + +using namespace std; +using namespace TestUtilities; + +using namespace CPPUNIT_NS; + +enum class TagStatus +{ + Original, + TestMetaDataPresent, + Removed +}; + +/*! + * \brief The CliTests class tests the command line interface. + */ +class CliTests : public TestFixture +{ + CPPUNIT_TEST_SUITE(CliTests); +#ifdef PLATFORM_UNIX + CPPUNIT_TEST(testBasicReadingAndWriting); + CPPUNIT_TEST(testHandlingOfTargets); + CPPUNIT_TEST(testHandlingOfId3Tags); + CPPUNIT_TEST(testMultipleFiles); + CPPUNIT_TEST(testMultipleValuesPerField); + CPPUNIT_TEST(testHandlingAttachments); + CPPUNIT_TEST(testDisplayingTechnicalInfo); +#endif + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp(); + void tearDown(); + +#ifdef PLATFORM_UNIX + void testBasicReadingAndWriting(); + void testHandlingOfTargets(); + void testHandlingOfId3Tags(); + void testMultipleFiles(); + void testMultipleValuesPerField(); + void testHandlingAttachments(); + void testDisplayingTechnicalInfo(); +#endif + +private: + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(CliTests); + +void CliTests::setUp() +{} + +void CliTests::tearDown() +{} + +#ifdef PLATFORM_UNIX +/*! + * \brief Tests basic reading and writing of tags. + */ +void CliTests::testBasicReadingAndWriting() +{ + string stdout, stderr; + // get specific field + string mkvFile(workingCopyPath("matroska_wave1/test2.mkv")); + const char *const args1[] = {"tageditor", "get", "title", "-f", mkvFile.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); + CPPUNIT_ASSERT(stderr.empty()); + // context of the following fields is the album (so "Title" means the title of the album) + CPPUNIT_ASSERT(stdout.find("album") != string::npos); + CPPUNIT_ASSERT(stdout.find("Title Elephant Dream - test 2") != string::npos); + CPPUNIT_ASSERT(stdout.find("Year 2010") == string::npos); + + // get all fields + const char *const args2[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr)); + CPPUNIT_ASSERT(stderr.empty()); + CPPUNIT_ASSERT(stdout.find("Title Elephant Dream - test 2") != string::npos); + CPPUNIT_ASSERT(stdout.find("Year 2010") != string::npos); + CPPUNIT_ASSERT(stdout.find("Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC") != string::npos); + + // set some fields, keep other field + const char *const args3[] = {"tageditor", "set", "title=A new title", "genre=Testfile", "-f", mkvFile.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr)); + CPPUNIT_ASSERT(stdout.find("Changes have been applied") != string::npos); + CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr)); + CPPUNIT_ASSERT(stderr.empty()); + CPPUNIT_ASSERT(stdout.find("Title A new title") != string::npos); + CPPUNIT_ASSERT(stdout.find("Year 2010") != string::npos); + CPPUNIT_ASSERT(stdout.find("Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC") != string::npos); + CPPUNIT_ASSERT(stdout.find("Genre Testfile") != string::npos); + + // set some fields, discard other + const char *const args4[] = {"tageditor", "set", "title=Foo", "artist=Bar", "--remove-other-fields", "-f", mkvFile.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args4, stdout, stderr)); + CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr)); + CPPUNIT_ASSERT(stderr.empty()); + CPPUNIT_ASSERT(stdout.find("Title Foo") != string::npos); + CPPUNIT_ASSERT(stdout.find("Artist Bar") != string::npos); + CPPUNIT_ASSERT(stdout.find("Year") == string::npos); + CPPUNIT_ASSERT(stdout.find("Comment") == string::npos); + CPPUNIT_ASSERT(stdout.find("Genre") == string::npos); +} + +/*! + * \brief Tests adding and removing of targets. + */ +void CliTests::testHandlingOfTargets() +{ + string stdout, stderr; + string mkvFile(workingCopyPath("matroska_wave1/test2.mkv")); + const char *const args1[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr}; + + // add song title (title field for tag with level 30) + 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}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr)); + CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); + size_t songPos, albumPos; + CPPUNIT_ASSERT((songPos = stdout.find("song")) != string::npos); + CPPUNIT_ASSERT((albumPos = stdout.find("album")) != string::npos); + CPPUNIT_ASSERT(stdout.find("Title The song title") > songPos); + CPPUNIT_ASSERT(stdout.find("Genre The song genre") > songPos); + CPPUNIT_ASSERT(stdout.find("Title Elephant Dream - test 2") > albumPos); + CPPUNIT_ASSERT(stdout.find("Genre The album genre") > albumPos); + + // remove tags targeting level 30 and 50 and add new tag targeting level 30 and the audio track + const char *const args3[] = {"tageditor", "set", "target-level=30", "target-tracks=3134325680", "title=The audio track", "encoder=likely some AAC encoder", "--remove-targets", "target-level=30", ",", "target-level=50", "-f", mkvFile.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr)); + CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); + CPPUNIT_ASSERT((songPos = stdout.find("song")) != string::npos); + CPPUNIT_ASSERT(stdout.find("song", songPos + 1) == string::npos); + CPPUNIT_ASSERT(stdout.find("3134325680") != string::npos); + CPPUNIT_ASSERT((albumPos = stdout.find("album")) == string::npos); + CPPUNIT_ASSERT(stdout.find("Title The audio track") != string::npos); + CPPUNIT_ASSERT(stdout.find("Encoder likely some AAC encoder") != string::npos); +} + +/*! + * \brief Tests handling of ID3v1 and ID3v2 tags. + */ +void CliTests::testHandlingOfId3Tags() +{ + // TODO +} + +/*! + * \brief Tests reading and writing multiple files at once. + */ +void CliTests::testMultipleFiles() +{ + string stdout, stderr; + string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv")); + string mkvFile2(workingCopyPath("matroska_wave1/test2.mkv")); + string mkvFile3(workingCopyPath("matroska_wave1/test3.mkv")); + + // get tags of 3 files at once + const char *const args1[] = {"tageditor", "get", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); + size_t pos1 = stdout.find("Title Big Buck Bunny - test 1"); + size_t pos2 = stdout.find("Title Elephant Dream - test 2"); + size_t pos3 = stdout.find("Title Elephant Dream - test 3"); + CPPUNIT_ASSERT(pos1 != string::npos); + CPPUNIT_ASSERT(pos2 > pos1); + CPPUNIT_ASSERT(pos3 > pos2); + + // set title and part number of 3 files at once + 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}; + CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr)); + CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); + CPPUNIT_ASSERT((pos1 = stdout.find("Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" + " Title MKV testfiles\n" + " Year 2010\n" + " Comment Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock\n" + " Total parts 3\n" + "Matroska tag targeting \"level 30 'track, song, chapter'\"\n" + " Title test1\n" + " Part 1")) != string::npos); + CPPUNIT_ASSERT((pos2 = stdout.find("Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" + " Title MKV testfiles\n" + " Year 2010\n" + " Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC\n" + " Total parts 3\n" + "Matroska tag targeting \"level 30 'track, song, chapter'\"\n" + " Title test2\n" + " Part 2")) > pos1); + CPPUNIT_ASSERT((stdout.find("Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" + " Title MKV testfiles\n" + " Year 2010\n" + " Comment Matroska Validation File 3, header stripping on the video track and no SimpleBlock\n" + " Total parts 3\n" + "Matroska tag targeting \"level 30 'track, song, chapter'\"\n" + " Title test3\n" + " Part 3")) > pos2); +} + +/*! + * \brief Tests tagging multiple values per field. + */ +void CliTests::testMultipleValuesPerField() +{ + // TODO +} + +/*! + * \brief Tests handling attachments. + */ +void CliTests::testHandlingAttachments() +{ + // TODO +} + +/*! + * \brief Tests displaying technical info. + */ +void CliTests::testDisplayingTechnicalInfo() +{ + // TODO +} +#endif diff --git a/tests/cppunit.cpp b/tests/cppunit.cpp new file mode 100644 index 0000000..67aaee6 --- /dev/null +++ b/tests/cppunit.cpp @@ -0,0 +1 @@ +#include