Allow specifying native field IDs via CLI

This commit is contained in:
Martchus 2017-01-23 00:27:21 +01:00
parent 5d0db0fafe
commit d99956f4c9
5 changed files with 345 additions and 129 deletions

View File

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

View File

@ -1,8 +1,10 @@
#include "./helper.h"
#include "../application/knownfieldmodel.h"
#include <tagparser/mediafileinfo.h>
#include <tagparser/matroska/matroskatag.h>
#include <tagparser/mp4/mp4tag.h>
#include <tagparser/vorbis/vorbiscomment.h>
#include <tagparser/id3/id3v2tag.h>
#include <c++utilities/application/argumentparser.h>
@ -10,6 +12,7 @@
#include <cstring>
using namespace std;
using namespace std::placeholders;
using namespace ApplicationUtilities;
using namespace ConversionUtilities;
using namespace ChronoUtilities;
@ -140,34 +143,39 @@ void printFieldName(const char *fieldName, size_t fieldNameLen)
}
}
void printField(const FieldScope &scope, const Tag *tag, bool skipEmpty)
void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty)
{
const auto &values = tag->values(scope.field);
if(!skipEmpty || !values.empty()) {
// write field name
const char *fieldName = KnownFieldModel::fieldName(scope.field);
const auto fieldNameLen = strlen(fieldName);
// write field name
const char *fieldName = scope.field.name();
const auto fieldNameLen = strlen(fieldName);
// write value
if(values.empty()) {
printFieldName(fieldName, fieldNameLen);
cout << "none\n";
} else {
for(const auto &value : values) {
try {
const auto &values = scope.field.values(tag, tagType);
if(!skipEmpty || !values.empty()) {
// write value
if(values.empty()) {
printFieldName(fieldName, fieldNameLen);
try {
const auto textValue = value->toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
cout << textValue;
cout << "none\n";
} else {
for(const auto &value : values) {
printFieldName(fieldName, fieldNameLen);
try {
const auto textValue = value->toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
cout << textValue;
}
} catch(const ConversionException &) {
cout << "conversion error";
}
} catch(const ConversionException &) {
cout << "conversion error";
cout << '\n';
}
cout << '\n';
}
}
} catch(const ConversionException &e) {
printFieldName(fieldName, fieldNameLen);
cout << "unable to parse - " << e.what() << '\n';
}
}
@ -366,67 +374,19 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
cerr << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl;
continue;
}
// parse the denoted filed
if(!strncmp(fieldDenotationString, "title", fieldNameLen)) {
scope.field = KnownField::Title;
} else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) {
scope.field = KnownField::Album;
} else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) {
scope.field = KnownField::Artist;
} else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) {
scope.field = KnownField::Genre;
} else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) {
scope.field = KnownField::Year;
} else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) {
scope.field = KnownField::Comment;
} else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) {
scope.field = KnownField::Bpm;
} else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) {
scope.field = KnownField::Bps;
} else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) {
scope.field = KnownField::Lyricist;
} else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) {
scope.field = KnownField::TrackPosition;
} else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) {
scope.field = KnownField::DiskPosition;
} else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) {
scope.field = KnownField::PartNumber;
} else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) {
scope.field = KnownField::TotalParts;
} else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) {
scope.field = KnownField::Encoder;
} else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) {
scope.field = KnownField::RecordDate;
} else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) {
scope.field = KnownField::Performers;
} else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) {
scope.field = KnownField::Length;
} else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) {
scope.field = KnownField::Language;
} else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) {
scope.field = KnownField::EncoderSettings;
} else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) {
scope.field = KnownField::Lyrics;
} else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) {
scope.field = KnownField::SynchronizedLyrics;
} else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) {
scope.field = KnownField::Grouping;
} else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) {
scope.field = KnownField::RecordLabel;
} else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) {
scope.field = KnownField::Cover;
type = DenotationType::File; // read cover always from file
} else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) {
scope.field = KnownField::Composer;
} else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) {
scope.field = KnownField::Rating;
} else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) {
scope.field = KnownField::Description;
} else {
// no "KnownField" value matching -> discard the field denotation
cerr << "Warning: The field name \"" << string(fieldDenotationString, fieldNameLen) << "\" is unknown and will be ingored." << endl;
// parse the denoted field ID
try {
scope.field = FieldId::fromDenotation(fieldDenotationString, fieldNameLen);
} catch(const ConversionException &e) {
// unable to parse field ID denotation -> discard the field denotation
cerr << "Warning: The field denotation \"" << string(fieldDenotationString, fieldNameLen) << "\" could not be parsed and will be ignored: " << e.what() << endl;
continue;
}
// read cover always from file
if(scope.field.knownField() == KnownField::Cover) {
type = DenotationType::File;
}
// add field denotation scope
auto &fieldValues = fields[scope];
// add value to the scope (if present)
@ -448,4 +408,134 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
return fields;
}
template<class ConcreteTag>
std::vector<const TagValue *> valuesForNativeField(const char *idString, std::size_t idStringSize, const Tag *tag, TagType tagType)
{
if(tagType != ConcreteTag::tagType) {
return vector<const TagValue *>();
}
return static_cast<const ConcreteTag *>(tag)->values(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize));
}
template<class ConcreteTag>
bool setValuesForNativeField(const char *idString, std::size_t idStringSize, Tag *tag, TagType tagType, const std::vector<TagValue> &values)
{
if(tagType != ConcreteTag::tagType) {
return false;
}
return static_cast<ConcreteTag *>(tag)->setValues(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize), values);
}
inline FieldId::FieldId(const char *nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField) :
m_knownField(KnownField::Invalid),
m_nativeField(nativeField),
m_valuesForNativeField(valuesForNativeField),
m_setValuesForNativeField(setValuesForNativeField)
{}
/// \remarks This wrapper is required because specifying c'tor template args is not possible.
template<class ConcreteTag>
FieldId FieldId::fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize)
{
return FieldId(
nativeFieldId,
bind(&valuesForNativeField<ConcreteTag>, nativeFieldId, nativeFieldIdSize, _1, _2),
bind(&setValuesForNativeField<ConcreteTag>, nativeFieldId, nativeFieldIdSize, _1, _2, _3)
);
}
FieldId FieldId::fromDenotation(const char *denotation, size_t denotationSize)
{
// check for native, format-specific denotation
if(!strncmp(denotation, "mkv:", 4)) {
return FieldId::fromNativeField<MatroskaTag>(denotation + 4, denotationSize - 4);
} else if(!strncmp(denotation, "mp4:", 4)) {
return FieldId::fromNativeField<Mp4Tag>(denotation + 4, denotationSize - 4);
} else if(!strncmp(denotation, "vorbis:", 7)) {
return FieldId::fromNativeField<VorbisComment>(denotation + 7, denotationSize - 7);
} else if(!strncmp(denotation, "id3:", 7)) {
return FieldId::fromNativeField<Id3v2Tag>(denotation + 4, denotationSize - 4);
} else if(!strncmp(denotation, "generic:", 8)) {
// allow prefix 'generic:' for consistency
denotation += 8, denotationSize -= 8;
}
// determine KnownField for generic denotation
if(!strncmp(denotation, "title", denotationSize)) {
return KnownField::Title;
} else if(!strncmp(denotation, "album", denotationSize)) {
return KnownField::Album;
} else if(!strncmp(denotation, "artist", denotationSize)) {
return KnownField::Artist;
} else if(!strncmp(denotation, "genre", denotationSize)) {
return KnownField::Genre;
} else if(!strncmp(denotation, "year", denotationSize)) {
return KnownField::Year;
} else if(!strncmp(denotation, "comment", denotationSize)) {
return KnownField::Comment;
} else if(!strncmp(denotation, "bpm", denotationSize)) {
return KnownField::Bpm;
} else if(!strncmp(denotation, "bps", denotationSize)) {
return KnownField::Bps;
} else if(!strncmp(denotation, "lyricist", denotationSize)) {
return KnownField::Lyricist;
} else if(!strncmp(denotation, "track", denotationSize)) {
return KnownField::TrackPosition;
} else if(!strncmp(denotation, "disk", denotationSize)) {
return KnownField::DiskPosition;
} else if(!strncmp(denotation, "part", denotationSize)) {
return KnownField::PartNumber;
} else if(!strncmp(denotation, "totalparts", denotationSize)) {
return KnownField::TotalParts;
} else if(!strncmp(denotation, "encoder", denotationSize)) {
return KnownField::Encoder;
} else if(!strncmp(denotation, "recorddate", denotationSize)) {
return KnownField::RecordDate;
} else if(!strncmp(denotation, "performers", denotationSize)) {
return KnownField::Performers;
} else if(!strncmp(denotation, "duration", denotationSize)) {
return KnownField::Length;
} else if(!strncmp(denotation, "language", denotationSize)) {
return KnownField::Language;
} else if(!strncmp(denotation, "encodersettings", denotationSize)) {
return KnownField::EncoderSettings;
} else if(!strncmp(denotation, "lyrics", denotationSize)) {
return KnownField::Lyrics;
} else if(!strncmp(denotation, "synchronizedlyrics", denotationSize)) {
return KnownField::SynchronizedLyrics;
} else if(!strncmp(denotation, "grouping", denotationSize)) {
return KnownField::Grouping;
} else if(!strncmp(denotation, "recordlabel", denotationSize)) {
return KnownField::RecordLabel;
} else if(!strncmp(denotation, "cover", denotationSize)) {
return KnownField::Cover;
} else if(!strncmp(denotation, "composer", denotationSize)) {
return KnownField::Composer;
} else if(!strncmp(denotation, "rating", denotationSize)) {
return KnownField::Rating;
} else if(!strncmp(denotation, "description", denotationSize)) {
return KnownField::Description;
} else {
throw ConversionException("generic field name is unknown");
}
}
std::vector<const TagValue *> FieldId::values(const Tag *tag, TagType tagType) const
{
if(m_nativeField) {
return m_valuesForNativeField(tag, tagType);
} else {
return tag->values(m_knownField);
}
}
bool FieldId::setValues(Tag *tag, TagType tagType, const std::vector<TagValue> &values) const
{
if(m_nativeField) {
return m_setValuesForNativeField(tag, tagType, values);
} else {
return tag->setValues(m_knownField, values);
}
}
}

View File

@ -1,6 +1,8 @@
#ifndef CLI_HELPER
#define CLI_HELPER
#include "../application/knownfieldmodel.h"
#include <tagparser/tag.h>
#include <c++utilities/application/commandlineutils.h>
@ -10,6 +12,7 @@
#include <vector>
#include <unordered_map>
#include <functional>
namespace ApplicationUtilities {
class Argument;
@ -49,29 +52,61 @@ inline TagType &operator|= (TagType &lhs, TagType rhs)
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
}
struct FieldId
class FieldId
{
constexpr FieldId(KnownField field);
constexpr FieldId(const char *field);
KnownField genericField;
const char *nativeField;
public:
FieldId(KnownField m_knownField = KnownField::Invalid);
static FieldId fromDenotation(const char *denotation, std::size_t denotationSize);
bool operator ==(const FieldId &other) const;
KnownField knownField() const;
const char *nativeField() const;
const char *name() const;
std::vector<const TagValue *> values(const Tag *tag, TagType tagType) const;
bool setValues(Tag *tag, TagType tagType, const std::vector<TagValue> &values) const;
private:
typedef std::function<std::vector<const TagValue *>(const Tag *, TagType)> GetValuesForNativeFieldType;
typedef std::function<bool(Tag *, TagType, const std::vector<TagValue> &)> SetValuesForNativeFieldType;
FieldId(const char *m_nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField);
template<class ConcreteTag>
static FieldId fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize = std::string::npos);
KnownField m_knownField;
const char *m_nativeField;
GetValuesForNativeFieldType m_valuesForNativeField;
SetValuesForNativeFieldType m_setValuesForNativeField;
};
constexpr FieldId::FieldId(KnownField field) :
genericField(field),
nativeField(nullptr)
inline FieldId::FieldId(KnownField knownField) :
m_knownField(knownField),
m_nativeField(nullptr)
{}
constexpr FieldId::FieldId(const char *field) :
genericField(KnownField::Invalid),
nativeField(field)
{}
inline bool FieldId::operator ==(const FieldId &other) const
{
return m_knownField == other.m_knownField && m_nativeField == other.m_nativeField;
}
inline KnownField FieldId::knownField() const
{
return m_knownField;
}
inline const char *FieldId::nativeField() const
{
return m_nativeField;
}
inline const char *FieldId::name() const
{
return m_nativeField ? m_nativeField : Settings::KnownFieldModel::fieldName(m_knownField);
}
struct FieldScope
{
FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget());
bool operator ==(const FieldScope &other) const;
KnownField field;
FieldId field;
TagType tagType;
TagTarget tagTarget;
};
@ -142,7 +177,7 @@ template <> struct hash<TagTarget::IdContainerType>
template <> struct hash<TagTarget>
{
std::size_t operator()(const TagTarget& target) const
std::size_t operator()(const TagTarget &target) const
{
using std::hash;
return ((hash<uint64>()(target.level())
@ -151,12 +186,22 @@ template <> struct hash<TagTarget>
}
};
template <> struct hash<FieldScope>
template <> struct hash<FieldId>
{
std::size_t operator()(const FieldScope& scope) const
std::size_t operator()(const FieldId &id) const
{
using std::hash;
return ((hash<KnownField>()(scope.field)
return (hash<KnownField>()(id.knownField())
^ (hash<const char *>()(id.nativeField()) << 1));
}
};
template <> struct hash<FieldScope>
{
std::size_t operator()(const FieldScope &scope) const
{
using std::hash;
return ((hash<FieldId>()(scope.field)
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
^ (hash<TagTarget>()(scope.tagTarget) << 1);
}
@ -210,7 +255,7 @@ inline void printProperty(const char *propName, const intType value, const char
}
}
void printField(const FieldScope &scope, const Tag *tag, bool skipEmpty);
void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty);
TagUsage parseUsageDenotation(const ApplicationUtilities::Argument &usageArg, TagUsage defaultUsage);
TagTextEncoding parseEncodingDenotation(const ApplicationUtilities::Argument &encodingArg, TagTextEncoding defaultEncoding);

View File

@ -47,12 +47,16 @@ using namespace Utility;
namespace Cli {
#define FIELD_NAMES "title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
#define FIELD_NAMES \
"title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
"recorddate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" \
"recordlabel cover composer rating description"
#define TAG_MODIFIER "tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all"
#define TARGET_MODIFIER "target-level target-levelname target-tracks target-tracks\n" \
#define TAG_MODIFIER \
"tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all"
#define TARGET_MODIFIER \
"target-level target-levelname target-tracks target-tracks\n" \
"target-chapters target-editions target-attachments target-reset"
const char *const fieldNames = FIELD_NAMES;
@ -272,13 +276,13 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
// iterate through fields specified by the user
if(fields.empty()) {
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
printField(FieldScope(field), tag, true);
printField(FieldScope(field), tag, tagType, true);
}
} else {
for(const auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) {
printField(denotedScope, tag, false);
printField(denotedScope, tag, tagType, false);
}
}
}
@ -487,7 +491,11 @@ void setTagInfo(const SetTagInfoArgs &args)
}
}
// finally set the values
tag->setValues(denotedScope.field, convertedValues);
try {
denotedScope.field.setValues(tag, tagType, convertedValues);
} catch(const ConversionException &e) {
fileInfo.addNotification(NotificationType::Critical, "Unable to parse denoted field ID \"" + string(denotedScope.field.name()) + "\": " + e.what(), context);
}
}
}
}
@ -599,10 +607,14 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
vector<pair<const TagValue *, string> > values;
// iterate through all tags
for(const Tag *tag : tags) {
for(const auto &fieldDenotation : fieldDenotations) {
const auto &value = tag->value(fieldDenotation.first.field);
if(!value.isEmpty()) {
values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
const TagType tagType = tag->type();
for(const pair<FieldScope, FieldValues> &fieldDenotation : fieldDenotations) {
try {
for(const TagValue *value : fieldDenotation.first.field.values(tag, tagType)) {
values.emplace_back(value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
}
} catch(const ConversionException &e) {
inputFileInfo.addNotification(NotificationType::Critical, "Unable to parse denoted field ID \"" + string(fieldDenotation.first.field.name()) + "\": " + e.what(), "extracting field");
}
}
}

View File

@ -32,6 +32,7 @@ class CliTests : public TestFixture
CPPUNIT_TEST_SUITE(CliTests);
#ifdef PLATFORM_UNIX
CPPUNIT_TEST(testBasicReadingAndWriting);
CPPUNIT_TEST(testSpecifyingNativeFieldIds);
CPPUNIT_TEST(testHandlingOfTargets);
CPPUNIT_TEST(testId3SpecificOptions);
CPPUNIT_TEST(testMultipleFiles);
@ -51,6 +52,7 @@ public:
#ifdef PLATFORM_UNIX
void testBasicReadingAndWriting();
void testSpecifyingNativeFieldIds();
void testHandlingOfTargets();
void testId3SpecificOptions();
void testMultipleFiles();
@ -76,6 +78,34 @@ void CliTests::tearDown()
{}
#ifdef PLATFORM_UNIX
template <typename StringType, bool negateErrorCond = false>
bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
{
bool res = containsSubstrings(str, substrings);
if(negateErrorCond) {
res = !res;
}
if(!res) {
if(!negateErrorCond) {
cout << " - test failed: output does NOT contain required substrings\n";
} else {
cout << " - test failed: output DOES contain substrings it shouldn't\n";
}
cout << "Output:\n" << str;
cout << "Substrings:\n";
for(const auto &substr : substrings) {
cout << substr << "\n";
}
}
return res;
}
template <typename StringType>
bool testNotContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
{
return testContainsSubstrings<StringType, true>(str, substrings);
}
/*!
* \brief Tests basic reading and writing of tags.
*/
@ -90,7 +120,7 @@ void CliTests::testBasicReadingAndWriting()
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stderr.empty());
// context of the following fields is the album (so "Title" means the title of the album)
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"album",
"Title Elephant Dream - test 2"
}));
@ -100,7 +130,7 @@ void CliTests::testBasicReadingAndWriting()
const char *const args2[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(stderr.empty());
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Title Elephant Dream - test 2",
"Year 2010",
"Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC"
@ -112,7 +142,7 @@ void CliTests::testBasicReadingAndWriting()
CPPUNIT_ASSERT(stdout.find("Changes have been applied") != string::npos);
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(stderr.empty());
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Title A new title",
"Genre Testfile",
"Year 2010",
@ -126,7 +156,7 @@ void CliTests::testBasicReadingAndWriting()
TESTUTILS_ASSERT_EXEC(args4);
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(stderr.empty());
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Title Foo",
"Artist Bar"
}));
@ -134,10 +164,49 @@ void CliTests::testBasicReadingAndWriting()
CPPUNIT_ASSERT(stdout.find("Comment") == string::npos);
CPPUNIT_ASSERT(stdout.find("Genre") == string::npos);
remove(mkvFile.c_str());
remove(mkvFile.data());
remove(mkvFileBackup.data());
}
/*!
* \brief Tests specifying native fields IDs when getting and setting fields.
*/
void CliTests::testSpecifyingNativeFieldIds()
{
cout << "\nSpecifying native field IDs" << endl;
string stdout, stderr;
// get specific field
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFileBackup(mkvFile + ".bak");
const string mp4File(workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a"));
const string mp4FileBackup(mp4File + ".bak");
const char *const args1[] = {"tageditor", "set", "mkv:FOO=bar", "mp4:©foo=bar", "mp4:invalid", "-f", mkvFile.data(), mp4File.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stderr.empty());
// FIXME: provide a way to specify raw data type
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"making MP4 tag field ©foo: It was not possible to find an appropriate raw data type id. UTF-8 will be assumed."}));
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Unable to parse denoted field ID \"invalid\": MP4 ID must be exactly 4 chars"}));
const char *const args2[] = {"tageditor", "get", "mkv:FOO", "mp4:©foo", "generic:year", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(stderr.empty());
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Year 2010"}));
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"FOO bar"}));
const char *const args3[] = {"tageditor", "get", "mkv:FOO", "mp4:©foo", "mp4:invalid", "generic:year", "-f", mp4File.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(stderr.empty());
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"test"}));
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"Year none"}));
// FIXME: number of whitespaces currently not correct because UTF-8 ©-sign counts as two characters
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"©foo bar"}));
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"invalid unable to parse - MP4 ID must be exactly 4 chars"}));
remove(mkvFile.data()), remove(mkvFileBackup.data());
remove(mp4File.data()), remove(mp4FileBackup.data());
}
/*!
* \brief Tests adding and removing of targets.
*/
@ -153,12 +222,12 @@ void CliTests::testHandlingOfTargets()
const char *const args2[] = {"tageditor", "set", "target-level=30", "title=The song title", "genre=The song genre", "target-level=50", "genre=The album genre", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"song",
"Title The song title",
"Genre The song genre"
}));
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"album",
"Title Elephant Dream - test 2",
"Genre The album genre"
@ -169,10 +238,10 @@ void CliTests::testHandlingOfTargets()
const char *const args3[] = {"tageditor", "set", "target-level=30", "target-tracks=3134325680", "title=The audio track", "encoder=likely some AAC encoder", "--remove-target", "target-level=30", "--remove-target", "target-level=50", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args3);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {"song"}));
CPPUNIT_ASSERT(!containsSubstrings(stdout, {"song", "song"}));
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {"song"}));
CPPUNIT_ASSERT(testNotContainsSubstrings(stdout, {"song", "song"}));
CPPUNIT_ASSERT(stdout.find("album") == string::npos);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"3134325680",
"Title The audio track",
"Encoder likely some AAC encoder"
@ -270,7 +339,7 @@ void CliTests::testMultipleFiles()
// get tags of 3 files at once
const char *const args1[] = {"tageditor", "get", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Title Big Buck Bunny - test 1",
"Title Elephant Dream - test 2",
"Title Elephant Dream - test 3"
@ -282,7 +351,7 @@ void CliTests::testMultipleFiles()
const char *const args2[] = {"tageditor", "set", "target-level=30", "title=test1", "title=test2", "title=test3", "part+=1", "target-level=50", "title=MKV testfiles", "totalparts=3", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n"
" Title MKV testfiles\n"
" Year 2010\n"
@ -337,7 +406,7 @@ void CliTests::testOutputFile()
// specified output files contain new titles
const char *const args3[] = {"tageditor", "get", "-f", "/tmp/test1.mkv", "/tmp/test2.mkv", nullptr};
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
" Title test1\n",
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
@ -362,7 +431,7 @@ void CliTests::testMultipleValuesPerField()
const char *const args2[] = {"tageditor", "get", "-f", mkvFile1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Artist test1",
"Artist test2",
"Artist test3"
@ -396,7 +465,7 @@ void CliTests::testHandlingAttachments()
TESTUTILS_ASSERT_EXEC(args2);
const char *const args1[] = {"tageditor", "info", "-f", mkvFile1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Attachments:",
"Name test2.mkv",
"MIME-type video/x-matroska",
@ -410,7 +479,7 @@ void CliTests::testHandlingAttachments()
const char *const args3[] = {"tageditor", "set", "--update-attachment", "name=test2.mkv", "desc=Updated test attachment", "-f", mkvFile1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args3);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Attachments:",
"Name test2.mkv",
"MIME-type video/x-matroska",
@ -457,7 +526,7 @@ void CliTests::testDisplayingInfo()
const string mkvFile(testFilePath("matroska_wave1/test2.mkv"));
const char *const args1[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: Matroska\n"
" Document type matroska\n"
" Read version 1\n"
@ -486,7 +555,7 @@ void CliTests::testDisplayingInfo()
const string mp4File(testFilePath("mtx-test-data/aac/he-aacv2-ps.m4a"));
const char *const args2[] = {"tageditor", "info", "-f", mp4File.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: MPEG-4 Part 14\n"
" Document type mp42\n"
" Duration 3 min\n"