diff --git a/cli/helper.cpp b/cli/helper.cpp index bbfdcd4..788ea3a 100644 --- a/cli/helper.cpp +++ b/cli/helper.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -313,6 +314,7 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) 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 { TagType tagType = TagType::Unspecified; + bool error = false; for(const auto &part : splitString(fieldDenotationString + 4, ",", EmptyPartsTreat::Omit)) { if(part == "id3v1") { tagType |= TagType::Id3v1Tag; @@ -331,15 +333,44 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) 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 = scope.tagType; + error = true; break; } } - scope.tagType = tagType; - break; + if(!error) { + scope.tagType = tagType; + scope.allTracks = false; + scope.trackIds.clear(); + } } + continue; } else if(applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) { continue; + } else if(!strncmp(fieldDenotationString, "track=", 6)) { + const vector parts = splitString>(fieldDenotationString + 6, ",", EmptyPartsTreat::Omit); + bool allTracks = scope.allTracks; + vector trackIds; + trackIds.reserve(parts.size()); + bool error = false; + for(const auto &part : parts) { + if(part == "all" || part == "any") { + allTracks = true; + break; + } else { + try { + trackIds.emplace_back(stringToNumber(part)); + } catch(const ConversionException &) { + cerr << "Warning: The value provided with the \"track\"-specifier is invalid and will be ignored. It must be a comma-separated list of track IDs." << endl; + error = true; + break; + } + } + } + if(!error) { + scope.allTracks = allTracks; + scope.trackIds = move(trackIds); + } + continue; } // check whether field name starts with + indicating an additional value bool additionalValue = *fieldDenotationString == '+'; @@ -376,7 +407,11 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) } // parse the denoted field ID try { - scope.field = FieldId::fromDenotation(fieldDenotationString, fieldNameLen); + if(scope.isTrack()) { + scope.field = FieldId::fromTrackDenotation(fieldDenotationString, fieldNameLen); + } else { + scope.field = FieldId::fromTagDenotation(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; @@ -397,7 +432,7 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) // file index might have been specified explicitely // if not (mult == 1) use the index of the last value and increase it by one if the value is not an additional 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 + (additionalValue ? 0 : 1)) : fileIndex, (equationPos + 1))); + fieldValues.allValues.emplace_back(type, mult == 1 ? (fieldValues.allValues.empty() ? 0 : fieldValues.allValues.back().fileIndex + (additionalValue ? 0 : 1)) : fileIndex, equationPos + 1); } } if(additionalValue && readOnly) { @@ -426,9 +461,9 @@ bool setValuesForNativeField(const char *idString, std::size_t idStringSize, Tag return static_cast(tag)->setValues(ConcreteTag::fieldType::fieldIdFromString(idString, idStringSize), values); } -inline FieldId::FieldId(const char *nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField) : +inline FieldId::FieldId(const char *nativeField, std::size_t nativeFieldSize, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField) : m_knownField(KnownField::Invalid), - m_nativeField(nativeField), + m_nativeField(nativeField, nativeFieldSize), m_valuesForNativeField(valuesForNativeField), m_setValuesForNativeField(setValuesForNativeField) {} @@ -438,13 +473,13 @@ template FieldId FieldId::fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize) { return FieldId( - nativeFieldId, + nativeFieldId, nativeFieldIdSize, bind(&valuesForNativeField, nativeFieldId, nativeFieldIdSize, _1, _2), bind(&setValuesForNativeField, nativeFieldId, nativeFieldIdSize, _1, _2, _3) ); } -FieldId FieldId::fromDenotation(const char *denotation, size_t denotationSize) +FieldId FieldId::fromTagDenotation(const char *denotation, size_t denotationSize) { // check for native, format-specific denotation if(!strncmp(denotation, "mkv:", 4)) { @@ -461,68 +496,54 @@ FieldId FieldId::fromDenotation(const char *denotation, size_t denotationSize) } // 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"); + static const struct { + const char *knownDenotation; + KnownField knownField; + } fieldMapping[] = { + {"title", KnownField::Title}, + {"album", KnownField::Album}, + {"artist", KnownField::Artist}, + {"genre", KnownField::Genre}, + {"year", KnownField::Year}, + {"comment", KnownField::Comment}, + {"bpm", KnownField::Bpm}, + {"bps", KnownField::Bps}, + {"lyricist", KnownField::Lyricist}, + {"track", KnownField::TrackPosition}, + {"disk", KnownField::DiskPosition}, + {"part", KnownField::PartNumber}, + {"totalparts", KnownField::TotalParts}, + {"encoder", KnownField::Encoder}, + {"recorddate", KnownField::RecordDate}, + {"performers", KnownField::Performers}, + {"duration", KnownField::Length}, + {"language", KnownField::Language}, + {"encodersettings", KnownField::EncoderSettings}, + {"lyrics", KnownField::Lyrics}, + {"synchronizedlyrics", KnownField::SynchronizedLyrics}, + {"grouping", KnownField::Grouping}, + {"recordlabel", KnownField::RecordLabel}, + {"cover", KnownField::Cover}, + {"composer", KnownField::Composer}, + {"rating", KnownField::Rating}, + {"description", KnownField::Description}, + }; + for(const auto &mapping : fieldMapping) { + if(!strncmp(denotation, mapping.knownDenotation, denotationSize)) { + return FieldId(mapping.knownField, nullptr, 0); + } } + throw ConversionException("generic field name is unknown"); +} + +FieldId FieldId::fromTrackDenotation(const char *denotation, size_t denotationSize) +{ + return FieldId(KnownField::Invalid, denotation, denotationSize); } std::vector FieldId::values(const Tag *tag, TagType tagType) const { - if(m_nativeField) { + if(!m_nativeField.empty()) { return m_valuesForNativeField(tag, tagType); } else { return tag->values(m_knownField); @@ -531,7 +552,7 @@ std::vector FieldId::values(const Tag *tag, TagType tagType) c bool FieldId::setValues(Tag *tag, TagType tagType, const std::vector &values) const { - if(m_nativeField) { + if(!m_nativeField.empty()) { return m_setValuesForNativeField(tag, tagType, values); } else { return tag->setValues(m_knownField, values); @@ -554,4 +575,14 @@ string tagName(const Tag *tag) return ss.str(); } +bool stringToBool(const string &str) +{ + if(str == "yes" || str == "true" || str == "1") { + return true; + } else if(str == "no" || str == "false" || str == "0") { + return false; + } + throw ConversionException(argsToString('\"', str, " is not yes or no")); +} + } diff --git a/cli/helper.h b/cli/helper.h index 2a6071e..a07a274 100644 --- a/cli/helper.h +++ b/cli/helper.h @@ -54,37 +54,42 @@ inline TagType &operator|= (TagType &lhs, TagType rhs) class FieldId { + friend struct std::hash; + public: - FieldId(KnownField m_knownField = KnownField::Invalid); - static FieldId fromDenotation(const char *denotation, std::size_t denotationSize); + FieldId(KnownField m_knownField = KnownField::Invalid, const char *denotation = nullptr, std::size_t denotationSize = 0); + static FieldId fromTagDenotation(const char *denotation, std::size_t denotationSize); + static FieldId fromTrackDenotation(const char *denotation, std::size_t denotationSize); bool operator ==(const FieldId &other) const; KnownField knownField() const; - const char *nativeField() const; const char *name() const; + bool denotes(const char *knownDenotation) const; + const std::string &denotation() const; std::vector values(const Tag *tag, TagType tagType) const; bool setValues(Tag *tag, TagType tagType, const std::vector &values) const; private: typedef std::function(const Tag *, TagType)> GetValuesForNativeFieldType; typedef std::function &)> SetValuesForNativeFieldType; - FieldId(const char *m_nativeField, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField); + FieldId(const char *nativeField, std::size_t nativeFieldSize, const GetValuesForNativeFieldType &valuesForNativeField, const SetValuesForNativeFieldType &setValuesForNativeField); template - static FieldId fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize = std::string::npos); + static FieldId fromNativeField(const char *nativeFieldId, std::size_t nativeFieldIdSize); KnownField m_knownField; - const char *m_nativeField; + std::string m_denotation; + std::string m_nativeField; GetValuesForNativeFieldType m_valuesForNativeField; SetValuesForNativeFieldType m_setValuesForNativeField; }; -inline FieldId::FieldId(KnownField knownField) : +inline FieldId::FieldId(KnownField knownField, const char *denotation, std::size_t denotationSize) : m_knownField(knownField), - m_nativeField(nullptr) + m_denotation(denotation, denotationSize) {} inline bool FieldId::operator ==(const FieldId &other) const { - return m_knownField == other.m_knownField && m_nativeField == other.m_nativeField; + return (m_knownField == other.m_knownField || m_denotation == other.m_denotation) && m_nativeField == other.m_nativeField; } inline KnownField FieldId::knownField() const @@ -92,29 +97,39 @@ 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); + return !m_nativeField.empty() ? m_nativeField.data() : Settings::KnownFieldModel::fieldName(m_knownField); +} + +inline bool FieldId::denotes(const char *knownDenotation) const +{ + return !std::strncmp(m_denotation.data(), knownDenotation, m_denotation.size()); +} + +inline const std::string &FieldId::denotation() const +{ + return m_denotation; } struct FieldScope { FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget()); bool operator ==(const FieldScope &other) const; + bool isTrack() const; + FieldId field; TagType tagType; TagTarget tagTarget; + bool allTracks; + std::vector trackIds; }; inline FieldScope::FieldScope(KnownField field, TagType tagType, TagTarget tagTarget) : field(field), tagType(tagType), - tagTarget(tagTarget) + tagTarget(tagTarget), + allTracks(false) {} inline bool FieldScope::operator ==(const FieldScope &other) const @@ -122,6 +137,11 @@ inline bool FieldScope::operator ==(const FieldScope &other) const return field == other.field && tagType == other.tagType && tagTarget == other.tagTarget; } +inline bool FieldScope::isTrack() const +{ + return allTracks || !trackIds.empty(); +} + struct FieldValue { FieldValue(DenotationType type, unsigned int fileIndex, const char *value); @@ -181,7 +201,7 @@ template <> struct hash { using std::hash; return ((hash()(target.level()) - ^ (hash()(target.tracks()) << 1)) >> 1) + ^ (hash()(target.tracks()) << 1)) >> 1) ^ (hash()(target.attachments()) << 1); } }; @@ -191,8 +211,8 @@ template <> struct hash std::size_t operator()(const FieldId &id) const { using std::hash; - return (hash()(id.knownField()) - ^ (hash()(id.nativeField()) << 1)); + return ((id.knownField() != KnownField::Invalid) ? hash()(id.knownField()) : hash()(id.denotation())) + ^ (hash()(id.m_nativeField) << 1); } }; @@ -201,9 +221,10 @@ 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); + return (hash()(scope.field) + ^ (hash()(scope.tagType) << 1) >> 1) + ^ (hash()(scope.tagTarget) ^ (static_cast(scope.allTracks) << 4) + ^ (hash>()(scope.trackIds) << 1) >> 1); } }; @@ -211,7 +232,11 @@ template <> struct hash namespace Cli { -typedef std::vector FieldValues; +struct FieldValues +{ + std::vector allValues; + std::vector relevantValues; +}; typedef std::unordered_map FieldDenotations; // declare/define actual helpers @@ -265,6 +290,7 @@ TagTarget::IdContainerType parseIds(const std::string &concatenatedIds); bool applyTargetConfiguration(TagTarget &target, const std::string &configStr); FieldDenotations parseFieldDenotations(const ApplicationUtilities::Argument &fieldsArg, bool readOnly); std::string tagName(const Tag *tag); +bool stringToBool(const std::string &str); } diff --git a/cli/mainfeatures.cpp b/cli/mainfeatures.cpp index d36793b..b6f91a3 100644 --- a/cli/mainfeatures.cpp +++ b/cli/mainfeatures.cpp @@ -161,6 +161,9 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const printProperty("ID", track->id(), nullptr, true); printProperty("Name", track->name()); printProperty("Type", track->mediaTypeName()); + if(track->language() != "und") { + printProperty("Language", track->language()); + } const char *fmtName = track->formatName(), *fmtAbbr = track->formatAbbreviation(); printProperty("Format", fmtName); if(strcmp(fmtName, fmtAbbr)) { @@ -188,6 +191,29 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const printProperty("Sample count", track->sampleCount()); printProperty("Creation time", track->creationTime()); printProperty("Modification time", track->modificationTime()); + vector labels; + labels.reserve(7); + if(track->isInterlaced()) { + labels.emplace_back("interlaced"); + } + if(!track->isEnabled()) { + labels.emplace_back("disabled"); + } + if(track->isDefault()) { + labels.emplace_back("default"); + } + if(track->isForced()) { + labels.emplace_back("forced"); + } + if(track->hasLacing()) { + labels.emplace_back("has lacing"); + } + if(track->isEncrypted()) { + labels.emplace_back("encrypted"); + } + if(!labels.empty()) { + printProperty("Labeled as", joinStrings(labels, ", ")); + } cout << endl; } } else { @@ -323,7 +349,7 @@ void setTagInfo(const SetTagInfoArgs &args) vector requiredTargets; for(const auto &fieldDenotation : fields) { const FieldScope &scope = fieldDenotation.first; - if(find(requiredTargets.cbegin(), requiredTargets.cend(), scope.tagTarget) == requiredTargets.cend()) { + if(!scope.isTrack() && find(requiredTargets.cbegin(), requiredTargets.cend(), scope.tagTarget) == requiredTargets.cend()) { requiredTargets.push_back(scope.tagTarget); } } @@ -411,8 +437,29 @@ void setTagInfo(const SetTagInfoArgs &args) cerr << "Warning: Setting the document title is not supported for the file." << endl; } } + // select the relevant values for the current file index + for(auto &fieldDenotation : fields) { + FieldValues &denotedValues = fieldDenotation.second; + vector &relevantDenotedValues = denotedValues.relevantValues; + denotedValues.relevantValues.clear(); + unsigned int currentFileIndex = 0; + for(FieldValue &denotatedValue : denotedValues.allValues) { + if(denotatedValue.fileIndex <= fileIndex) { + if(relevantDenotedValues.empty() || (denotatedValue.fileIndex >= currentFileIndex)) { + if(currentFileIndex != denotatedValue.fileIndex) { + currentFileIndex = denotatedValue.fileIndex; + relevantDenotedValues.clear(); + } + relevantDenotedValues.push_back(&denotatedValue); + } + } + } + } + fileInfo.tags(tags); - if(!tags.empty()) { + if(tags.empty()) { + fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context); + } else { // iterate through all tags for(auto *tag : tags) { // clear current values if option is present @@ -432,30 +479,15 @@ void setTagInfo(const SetTagInfoArgs &args) } } // iterate through all denoted field values - for(auto &fieldDenotation : fields) { + for(const 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 + if(!denotedScope.isTrack() && (denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType & tagType) != TagType::Unspecified) && (!targetSupported || denotedScope.tagTarget == tagTarget)) { - // select the relevant values for the current file index - vector relevantDenotedValues; - unsigned int currentFileIndex = 0; - for(FieldValue &denotatedValue : denotedValues) { - if(denotatedValue.fileIndex <= fileIndex) { - if(relevantDenotedValues.empty() || (denotatedValue.fileIndex >= currentFileIndex)) { - if(currentFileIndex != denotatedValue.fileIndex) { - currentFileIndex = denotatedValue.fileIndex; - relevantDenotedValues.clear(); - } - relevantDenotedValues.push_back(&denotatedValue); - } - } - } // convert the values to TagValue vector convertedValues; - for(FieldValue *relevantDenotedValue : relevantDenotedValues) { + for(const FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) { // one of the denoted values if(!relevantDenotedValue->value.empty()) { if(relevantDenotedValue->type == DenotationType::File) { @@ -478,9 +510,6 @@ void setTagInfo(const SetTagInfoArgs &args) } } else { convertedValues.emplace_back(relevantDenotedValue->value, TagTextEncoding::Utf8, usedEncoding); - if(relevantDenotedValue->type == DenotationType::Increment && tag == tags.back()) { - relevantDenotedValue->value = incremented(relevantDenotedValue->value); - } } } else { // if the denoted value is empty, just assign an empty TagValue to remove the field @@ -496,8 +525,48 @@ void setTagInfo(const SetTagInfoArgs &args) } } } - } else { - fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context); + } + for(AbstractTrack *track : fileInfo.tracks()) { + for(const auto &fieldDenotation : fields) { + const auto &values = fieldDenotation.second.relevantValues; + if(values.empty()) { + continue; + } + + const FieldScope &denotedScope = fieldDenotation.first; + // decide whether the scope of the denotation matches of the current track + if(denotedScope.allTracks || find(denotedScope.trackIds.cbegin(), denotedScope.trackIds.cend(), track->id()) != denotedScope.trackIds.cend()) { + const FieldId &field = denotedScope.field; + const string &value = values.front()->value; + try { + if(field.denotes("name")) { + track->setName(value); + } else if(field.denotes("language")) { + track->setLanguage(value); + } else if(field.denotes("tracknumber")) { + track->setTrackNumber(stringToNumber(value)); + } else if(field.denotes("enabled")) { + track->setEnabled(stringToBool(value)); + } else if(field.denotes("forced")) { + track->setForced(stringToBool(value)); + } else if(field.denotes("default")) { + track->setDefault(stringToBool(value)); + } else { + fileInfo.addNotification(NotificationType::Critical, argsToString("Denoted track property name \"", field.denotation(), "\" is invalid"), argsToString("setting meta-data of track ", track->id())); + } + } catch(const ConversionException &e) { + fileInfo.addNotification(NotificationType::Critical, argsToString("Unable to parse value for track property \"", field.denotation(), "\": ", e.what()), argsToString("setting meta-data of track ", track->id())); + } + } + } + } + // increment relevant values + for(auto &fieldDenotation : fields) { + for(FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) { + if(!relevantDenotedValue->value.empty() && relevantDenotedValue->type == DenotationType::Increment) { + relevantDenotedValue->value = incremented(relevantDenotedValue->value); + } + } } bool attachmentsModified = false; if(args.addAttachmentArg.isPresent() || args.updateAttachmentArg.isPresent() || args.removeAttachmentArg.isPresent() || args.removeExistingAttachmentsArg.isPresent()) { diff --git a/tests/cli.cpp b/tests/cli.cpp index 90d756d..0ce8d3b 100644 --- a/tests/cli.cpp +++ b/tests/cli.cpp @@ -42,6 +42,7 @@ class CliTests : public TestFixture CPPUNIT_TEST(testMultipleValuesPerField); CPPUNIT_TEST(testHandlingAttachments); CPPUNIT_TEST(testDisplayingInfo); + CPPUNIT_TEST(testSettingTrackMetaData); CPPUNIT_TEST(testExtraction); CPPUNIT_TEST(testReadingAndWritingDocumentTitle); CPPUNIT_TEST(testFileLayoutOptions); @@ -63,6 +64,7 @@ public: void testMultipleValuesPerField(); void testHandlingAttachments(); void testDisplayingInfo(); + void testSettingTrackMetaData(); void testExtraction(); void testReadingAndWritingDocumentTitle(); void testFileLayoutOptions(); @@ -639,6 +641,94 @@ void CliTests::testDisplayingInfo() " Modification time 2014-12-10 16:22:41"})); } +/*! + * \brief Tests setting track meta-data. + */ +void CliTests::testSettingTrackMetaData() +{ + cout << "\nSetting track meta-data" << endl; + string stdout, stderr; + + // test Matroska file + const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv")); + const string mp4File(workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a")); + const char *const args1[] = {"tageditor", "set", "title=title of tag", + "track=1863976627", "name=video track", + "track=3134325680", "name=audio track", "language=ger", "default=yes", "forced=yes", + "tag=any", "artist=setting tag value again", + "track=any", "name1=sbr and ps", "language1=eng", + "-f", mkvFile.data(), mp4File.data(), nullptr}; + const char *const args2[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr}; + const char *const args3[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr}; + TESTUTILS_ASSERT_EXEC(args1); + TESTUTILS_ASSERT_EXEC(args2); + CPPUNIT_ASSERT(testContainsSubstrings(stdout, { + " Container format: Matroska\n" + " Document type matroska\n" + " Read version 1\n" + " Version 1\n" + " Document read version 2\n" + " Document version 2\n" + " Duration 47 s 509 ms\n" + " Tag position before data\n" + " Index position before data\n", + " Tracks:\n" + " ID 1863976627\n" + " Name video track\n" + " Type Video\n" + " Format Advanced Video Coding Main Profile\n" + " Abbreviation H.264\n" + " Raw format ID V_MPEG4/ISO/AVC\n" + " FPS 24\n", + " ID 3134325680\n" + " Name audio track\n" + " Type Audio\n" + " Language ger\n" + " Format Advanced Audio Coding Low Complexity Profile\n" + " Abbreviation MPEG-4 AAC-LC\n" + " Raw format ID A_AAC\n" + " Channel config 2 channels: front-left, front-right\n" + " Sampling frequency 48000 Hz\n" + " Labeled as default, forced"})); + TESTUTILS_ASSERT_EXEC(args3); + CPPUNIT_ASSERT(testContainsSubstrings(stdout, { + "Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" + " Title title of tag\n" + " Artist setting tag value again\n" + " Year 2010\n" + " Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC" + })); + + const char *const args4[] = {"tageditor", "info", "-f", mp4File.data(), nullptr}; + TESTUTILS_ASSERT_EXEC(args4); + CPPUNIT_ASSERT(testContainsSubstrings(stdout, { + " Container format: MPEG-4 Part 14\n" + " Document type mp42\n" + " Duration 3 min\n" + " Creation time 2014-12-10 16:22:41\n" + " Modification time 2014-12-10 16:22:41\n", + " Tracks:\n" + " ID 1\n" + " Name sbr and ps\n" + " Type Audio\n" + " Language eng\n" + " Format Advanced Audio Coding Low Complexity Profile\n" + " Abbreviation MPEG-4 AAC-LC\n" + " Extensions Spectral Band Replication and Parametric Stereo / HE-AAC v2\n" + " Raw format ID mp4a\n" + " Size 879.65 KiB (900759 byte)\n" + " Duration 3 min 138 ms\n" + " Channel config 1 channel: front-center\n" + " Extension channel config 2 channels: front-left, front-right\n" + " Bitrate 40 kbit/s\n" + " Bits per sample 16\n" + " Sampling frequency 24000 Hz\n" + " Extension sampling frequency 48000 Hz\n" + " Sample count 4222\n" + " Creation time 2014-12-10 16:22:41\n" + " Modification time 2014-12-10 16:22:41"})); +} + /*! * \brief Tests extraction of field values (used to extract cover or other binary fields). * \remarks Extraction of attachments is already tested in testHandlingAttachments().