Improve CLI

- Implement tests (not everything covered yet)
- Improve documentation in README.md
- Fix issues when editing tags of multiple files at once
This commit is contained in:
Martchus 2016-07-30 23:17:49 +02:00
parent dc70d92b0f
commit 6d8833fe9d
5 changed files with 453 additions and 104 deletions

View File

@ -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)

View File

@ -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).

View File

@ -27,6 +27,7 @@
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
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<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
}
inline TagType operator& (TagType lhs, TagType rhs)
{
return static_cast<TagType>(static_cast<unsigned int>(lhs) & static_cast<unsigned int>(rhs));
}
inline TagType &operator|= (TagType &lhs, TagType rhs)
{
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(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<pair<unsigned int, string> > 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<TagTarget::IdContainerType>
{
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<TagTarget>
{
std::size_t operator()(const TagTarget& target) const
{
using std::hash;
return ((hash<uint64>()(target.level())
^ (hash<TagTarget::IdContainerType>()(target.tracks()) << 1)) >> 1)
^ (hash<TagTarget::IdContainerType>()(target.attachments()) << 1);
}
};
template <> struct hash<FieldScope>
{
std::size_t operator()(const FieldScope& scope) const
{
using std::hash;
return ((hash<KnownField>()(scope.field)
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
^ (hash<TagTarget>()(scope.tagTarget) << 1);
}
};
}
namespace Cli {
typedef vector<FieldValue> FieldValues;
typedef unordered_map<FieldScope, FieldValues> 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 &notifications, 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<FieldDenotation> parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
{
vector<FieldDenotation> fields;
FieldDenotations fields;
if(fieldsArg.isPresent()) {
const vector<const char *> &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<FieldDenotation> 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<FieldDenotation> 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<TagTarget> 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<unsigned int, string> *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<char []>(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())}, "-"));
}

226
tests/cli.cpp Normal file
View File

@ -0,0 +1,226 @@
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/tests/testutils.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestFixture.h>
#include <iostream>
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

1
tests/cppunit.cpp Normal file
View File

@ -0,0 +1 @@
#include <c++utilities/tests/cppunit.h>