Add more CLI tests

This commit is contained in:
Martchus 2016-08-03 17:48:53 +02:00
parent 1a20eea65b
commit 46f006f289
7 changed files with 393 additions and 196 deletions

View File

@ -200,6 +200,7 @@ endif()
# find tagparser
find_package(tagparser 6.0.0 REQUIRED)
use_tag_parser()
list(APPEND TEST_LIBRARIES ${TAG_PARSER_SHARED_LIB})
# add Qt modules which can currently not be detected automatically
list(APPEND ADDITIONAL_QT_MODULES Concurrent Network)

View File

@ -118,6 +118,9 @@ Here are some Bash examples which illustrate getting and setting tag information
- 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:** The current version v1.4.0 has a bug so tagging multiple files at once doesn't work as shown above. As a workaround
use either the Git version or use title0=... title1=... title2=... to specify the different titles for the files.
* *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" \
@ -184,7 +187,6 @@ To build without GUI, add the following parameters to the CMake call:
## Bugs
- Large file information is not shown when using Qt WebEngine.
- It is recommend you to create backups before editing because I can not test whether the
library works with all kind of files. If you force rewriting a backup is always created.
- underlying library: Matroska files composed of more than one segment aren't tested yet and might not work.
- underlying library: To add new features I've had to revise a lot of code since the last release. I always test the library with
files produced by mkvmerge and ffmpeg and several other file but can't verify that it will work with all
files. Hence I recommend to create backups of your files.

View File

@ -147,13 +147,12 @@ int main(int argc, char *argv[])
// verbose option
Argument verboseArg("verbose", 'v', "be verbose");
verboseArg.setCombinable(true);
// recursive option
Argument recursiveArg("recursive", 'r', "includes subdirectories");
// input/output file/files
Argument fileArg("file", 'f', "specifies the path of the file to be opened");
fileArg.setValueNames({"path"});
fileArg.setRequiredValueCount(1);
fileArg.setCombinable(true);
fileArg.setRequired(true);
Argument defaultFileArg(fileArg);
defaultFileArg.setImplicit(true);
Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
@ -185,14 +184,17 @@ int main(int argc, char *argv[])
// set tag info
Cli::SetTagInfoArgs setTagInfoArgs(filesArg, verboseArg);
// extract cover
Argument fieldArg("fields", 'n', "specifies the field to be extracted");
Argument fieldArg("field", 'n', "specifies the field to be extracted");
fieldArg.setValueNames({"field name"});
fieldArg.setRequiredValueCount(1);
fieldArg.setImplicit(true);
Argument extractFieldArg("extract", 'e', "saves the value of the specified field (eg. cover or other binary field) to the specified file or writes it to stdout if no output file has been specified");
extractFieldArg.setSubArguments({&fieldArg, &fileArg, &outputFileArg, &verboseArg});
Argument attachmentArg("attachment", 'a', "specifies the attachment to be extracted");
attachmentArg.setValueNames({"id=..."});
attachmentArg.setRequiredValueCount(1);
Argument extractFieldArg("extract", 'e', "saves the value of the specified field (eg. cover or other binary field) or attachment to the specified file or writes it to stdout if no output file has been specified");
extractFieldArg.setSubArguments({&fieldArg, &attachmentArg, &fileArg, &outputFileArg, &verboseArg});
extractFieldArg.setDenotesOperation(true);
extractFieldArg.setCallback(std::bind(Cli::extractField, std::cref(fieldsArg), std::cref(fileArg), std::cref(outputFileArg), std::cref(verboseArg)));
extractFieldArg.setCallback(std::bind(Cli::extractField, std::cref(fieldArg), std::cref(attachmentArg), std::cref(fileArg), std::cref(outputFileArg), std::cref(verboseArg)));
// file info
Argument validateArg("validate", 'c', "validates the file integrity as accurately as possible; the structure of the file will be parsed completely");
validateArg.setCombinable(true);

View File

@ -238,12 +238,12 @@ void printNotifications(const MediaFileInfo &fileInfo, const char *head = nullpt
}
#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"
"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" \
"target-chapters target-editions target-attachments target-reset"
"target-chapters target-editions target-attachments target-reset"
const char *const fieldNames = FIELD_NAMES;
const char *const fieldNamesForSet = FIELD_NAMES " " TAG_MODIFIER " " TARGET_MODIFIER;
@ -252,7 +252,7 @@ void printFieldNames(const ArgumentOccurrence &occurrence)
{
CMD_UTILS_START_CONSOLE;
VAR_UNUSED(occurrence)
cout << fieldNames;
cout << fieldNames;
cout << "\nTag modifier: " << TAG_MODIFIER;
cout << "\nTarget modifier: " << TARGET_MODIFIER << endl;
}
@ -701,13 +701,17 @@ void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg,
inputFileInfo.setForceFullParse(validateArg.isPresent());
inputFileInfo.open(true);
inputFileInfo.parseEverything();
cout << "Saving file info of \"" << inputFileArg.values().front() << "\" ..." << endl;
(outputFileArg.isPresent() ? cout : cerr) << "Saving file info for \"" << inputFileArg.values().front() << "\" ..." << endl;
NotificationList origNotify;
QFile file(QString::fromLocal8Bit(outputFileArg.values().front()));
if(file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, origNotify)) && file.flush()) {
cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl;
if(outputFileArg.isPresent()) {
QFile file(QString::fromLocal8Bit(outputFileArg.values().front()));
if(file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, origNotify)) && file.flush()) {
cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl;
} else {
cerr << "Error: An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << endl;
}
} else {
cerr << "Error: An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << endl;
cout << HtmlInfo::generateInfo(inputFileInfo, origNotify).data() << endl;
}
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;
@ -776,11 +780,12 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
return;
}
MediaFileInfo fileInfo;
for(const auto &file : filesArg.values()) {
for(const char *file : filesArg.values()) {
try {
// parse tags
fileInfo.setPath(file);
fileInfo.open(true);
fileInfo.parseContainerFormat();
fileInfo.parseTracks();
fileInfo.parseAttachments();
fileInfo.parseChapters();
@ -906,69 +911,43 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
}
const auto fields = parseFieldDenotations(fieldsArg, true);
MediaFileInfo fileInfo;
for(const auto &file : filesArg.values()) {
for(const char *file : filesArg.values()) {
try {
// parse tags
fileInfo.setPath(file);
fileInfo.open(true);
fileInfo.parseContainerFormat();
fileInfo.parseTags();
cout << "Tag information for \"" << file << "\":" << endl;
const auto tags = fileInfo.tags();
if(tags.size()) {
if(!tags.empty()) {
// iterate through all tags
for(const auto *tag : tags) {
// determine tag type
TagType tagType = tag->type();
const TagType tagType = tag->type();
// write tag name and target, eg. MP4/iTunes tag
cout << tag->typeName();
if(tagType == TagType::MatroskaTag || !tag->target().isEmpty()) {
cout << " targeting \"" << tag->targetString() << "\"";
cout << " targeting \"" << tag->targetString() << '\"';
}
cout << endl;
// iterate through fields specified by the user
if(fields.empty()) {
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
const auto &value = tag->value(field);
if(!value.isEmpty()) {
// write field name
const auto &values = tag->values(field);
if(!values.empty()) {
const char *fieldName = KnownFieldModel::fieldName(field);
cout << ' ' << fieldName;
// write padding
for(auto i = strlen(fieldName); i < 18; ++i) {
cout << ' ';
}
// write value
try {
const auto textValue = value.toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
cout << textValue;
const auto fieldNameLen = strlen(fieldName);
for(const auto &value : values) {
// write field name
cout << ' ' << fieldName;
// write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
}
} catch(const ConversionException &) {
cout << "conversion error";
}
cout << endl;
}
}
} else {
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(denotedScope.field);
cout << ' ' << fieldName;
// write padding
for(auto i = strlen(fieldName); i < 18; ++i) {
cout << ' ';
}
// write value
if(value.isEmpty()) {
cout << "none";
} else {
// write value
try {
const auto textValue = value.toString(TagTextEncoding::Utf8);
const auto textValue = value->toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
@ -977,8 +956,43 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
} catch(const ConversionException &) {
cout << "conversion error";
}
cout << endl;
}
}
}
} else {
for(const auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) {
const auto &values = tag->values(denotedScope.field);
const char *fieldName = KnownFieldModel::fieldName(denotedScope.field);
const auto fieldNameLen = strlen(fieldName);
if(values.empty()) {
cout << "none";
} else {
for(const auto &value : values) {
// write field name
const char *fieldName = KnownFieldModel::fieldName(denotedScope.field);
cout << ' ' << fieldName;
// write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
}
// write value
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";
}
cout << endl;
}
}
cout << endl;
}
}
}
@ -1068,12 +1082,13 @@ void setTagInfo(const SetTagInfoArgs &args)
unsigned int fileIndex = 0;
static const string context("setting tags");
NotificationList notifications;
for(const auto &file : args.filesArg.values()) {
for(const char *file : args.filesArg.values()) {
try {
// parse tags
cout << "Setting tag information for \"" << file << "\" ..." << endl;
notifications.clear();
fileInfo.setPath(file);
fileInfo.parseContainerFormat();
fileInfo.parseTags();
fileInfo.parseTracks();
vector<Tag *> tags;
@ -1246,67 +1261,135 @@ void setTagInfo(const SetTagInfoArgs &args)
}
}
void extractField(const Argument &fieldsArg, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &verboseArg)
void extractField(const Argument &fieldArg, const Argument &attachmentArg, const Argument &inputFilesArg, const Argument &outputFileArg, const Argument &verboseArg)
{
CMD_UTILS_START_CONSOLE;
const auto fields = parseFieldDenotations(fieldsArg, true);
if(fields.size() != 1) {
cerr << "Error: Excactly one field needs to be specified." << endl;
// parse specified field and attachment
const auto fieldDenotations = parseFieldDenotations(fieldArg, true);
AttachmentInfo attachmentInfo;
if(attachmentArg.isPresent()) {
attachmentInfo.parseDenotation(attachmentArg.values().front());
}
if(((fieldDenotations.size() != 1) || (!attachmentInfo.hasId && !attachmentInfo.name))
&& ((fieldDenotations.size() == 1) && (attachmentInfo.hasId || attachmentInfo.name))) {
cerr << "Error: Excactly one field or attachment needs to be specified." << endl;
return;
}
MediaFileInfo inputFileInfo;
try {
// parse tags
inputFileInfo.setPath(inputFileArg.values().front());
inputFileInfo.open(true);
inputFileInfo.parseTags();
(outputFileArg.isPresent() ? cout : cerr) << "Extracting " << fieldsArg.values().front() << " of \"" << inputFileArg.values().front() << "\" ..." << endl;
auto tags = inputFileInfo.tags();
vector<pair<const TagValue *, string> > values;
// iterate through all tags
for(const Tag *tag : tags) {
for(const auto &fieldDenotation : fields) {
const auto &value = tag->value(fieldDenotation.first.field);
if(!value.isEmpty()) {
values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-"));
}
}
}
if(values.empty()) {
cerr << "File has no (supported) " << fieldsArg.values().front() << " field." << endl;
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(values.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front());
}
for(const auto &value : values) {
fstream outputFileStream;
outputFileStream.exceptions(ios_base::failbit | ios_base::badbit);
auto path = values.size() > 1 ? joinStrings({outputFilePathWithoutExtension, "-", value.second, outputFileExtension}) : outputFileArg.values().front();
try {
outputFileStream.open(path, ios_base::out | ios_base::binary);
outputFileStream.write(value.first->dataPointer(), value.first->dataSize());
outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl;
}
}
} else {
// write data to stdout if no output file has been specified
for(const auto &value : values) {
cout.write(value.first->dataPointer(), value.first->dataSize());
}
}
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;
if(!inputFilesArg.isPresent() || inputFilesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl;
return;
}
MediaFileInfo inputFileInfo;
for(const char *file : inputFilesArg.values()) {
try {
// parse tags
inputFileInfo.setPath(file);
inputFileInfo.open(true);
if(!fieldDenotations.empty()) {
// extract tag field
(outputFileArg.isPresent() ? cout : cerr) << "Extracting field " << fieldArg.values().front() << " of \"" << file << "\" ..." << endl;
inputFileInfo.parseContainerFormat();
inputFileInfo.parseTags();
auto tags = inputFileInfo.tags();
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));
}
}
}
if(values.empty()) {
cerr << " None of the specified files has a (supported) " << fieldArg.values().front() << " field." << endl;
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(values.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front());
}
for(const auto &value : values) {
fstream outputFileStream;
outputFileStream.exceptions(ios_base::failbit | ios_base::badbit);
auto path = values.size() > 1 ? joinStrings({outputFilePathWithoutExtension, "-", value.second, outputFileExtension}) : outputFileArg.values().front();
try {
outputFileStream.open(path, ios_base::out | ios_base::binary);
outputFileStream.write(value.first->dataPointer(), value.first->dataSize());
outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl;
}
}
} else {
// write data to stdout if no output file has been specified
for(const auto &value : values) {
cout.write(value.first->dataPointer(), value.first->dataSize());
}
}
} else {
// extract attachment
auto &logStream = (outputFileArg.isPresent() ? cout : cerr);
logStream << "Extracting attachment with ";
if(attachmentInfo.hasId) {
logStream << "ID " << attachmentInfo.id;
} else {
logStream << "name \"" << attachmentInfo.name << '\"';
}
logStream << " of \"" << file << "\" ..." << endl;
inputFileInfo.parseContainerFormat();
inputFileInfo.parseAttachments();
vector<pair<const AbstractAttachment *, string> > attachments;
// iterate through all attachments
for(const AbstractAttachment *attachment : inputFileInfo.attachments()) {
if((attachmentInfo.hasId && attachment->id() == attachmentInfo.id)
|| (attachment->name() == attachmentInfo.name)) {
attachments.emplace_back(attachment, joinStrings({attachment->name(), numberToString(attachments.size())}, "-", true));
}
}
if(attachments.empty()) {
cerr << " None of the specified files has a (supported) attachment with the specified ID/name." << endl;
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(attachments.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front());
}
for(const auto &attachment : attachments) {
fstream outputFileStream;
outputFileStream.exceptions(ios_base::failbit | ios_base::badbit);
auto path = attachments.size() > 1 ? joinStrings({outputFilePathWithoutExtension, "-", attachment.second, outputFileExtension}) : outputFileArg.values().front();
try {
outputFileStream.open(path, ios_base::out | ios_base::binary);
attachment.first->data()->copyTo(outputFileStream);
outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl;
}
}
} else {
for(const auto &attachment : attachments) {
attachment.first->data()->copyTo(cout);
}
}
}
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl;
}
printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent());
}
printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent());
}
}

View File

@ -52,7 +52,7 @@ void displayFileInfo(const ApplicationUtilities::ArgumentOccurrence &, const App
void generateFileInfo(const ApplicationUtilities::ArgumentOccurrence &, const ApplicationUtilities::Argument &inputFileArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &validateArg);
void displayTagInfo(const ApplicationUtilities::Argument &fieldsArg, const ApplicationUtilities::Argument &filesArg, const ApplicationUtilities::Argument &verboseArg);
void setTagInfo(const Cli::SetTagInfoArgs &args);
void extractField(const ApplicationUtilities::Argument &fieldsArg, const ApplicationUtilities::Argument &inputFileArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &verboseArg);
void extractField(const ApplicationUtilities::Argument &fieldArg, const ApplicationUtilities::Argument &attachmentArg, const ApplicationUtilities::Argument &inputFilesArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &verboseArg);
}

View File

@ -130,22 +130,24 @@ bool TagEdit::hasField(KnownField field) const
QString TagEdit::generateLabel() const
{
if(!m_tags.isEmpty()) {
TagTarget target = m_tags.at(0)->target();
bool differentTargets = false;
const TagTarget &target = m_tags.at(0)->target();
bool differentTargets = false, haveMatroskaTags = false;
QStringList tagNames;
for(Tag *tag : m_tags) {
tagNames.reserve(m_tags.size());
for(const Tag *tag : m_tags) {
tagNames << QString::fromLocal8Bit(tag->typeName());
if(!differentTargets && !(target == tag->target())) {
differentTargets = true;
}
if(tag->type() == TagType::MatroskaTag) {
haveMatroskaTags = true;
}
}
QString res = tagNames.join(QStringLiteral(", "));
if(differentTargets) {
res.append(tr(" with different targets"));
} else {
if(!target.isEmpty()) {
res.append(tr(" targeting %1").arg(QString::fromLocal8Bit(m_tags.front()->targetString().c_str())));
}
} else if(haveMatroskaTags || !target.isEmpty()) {
res.append(tr(" targeting %1").arg(QString::fromLocal8Bit(m_tags.front()->targetString().c_str())));
}
return res;
}

View File

@ -1,13 +1,19 @@
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/catchiofailure.h>
#include <c++utilities/tests/testutils.h>
#include <tagparser/mediafileinfo.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestFixture.h>
#include <iostream>
#include <fstream>
using namespace std;
using namespace TestUtilities;
using namespace ConversionUtilities;
using namespace Media;
using namespace CPPUNIT_NS;
@ -74,21 +80,26 @@ void CliTests::testBasicReadingAndWriting()
string stdout, stderr;
// get specific field
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFileBackup(mkvFile + ".bak");
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(containsSubstrings(stdout, {
"album",
"Title Elephant Dream - test 2"
}));
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);
CPPUNIT_ASSERT(containsSubstrings(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"
}));
// set some fields, keep other field
const char *const args3[] = {"tageditor", "set", "title=A new title", "genre=Testfile", "-f", mkvFile.data(), nullptr};
@ -96,21 +107,31 @@ void CliTests::testBasicReadingAndWriting()
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);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"Title A new title",
"Genre Testfile",
"Year 2010",
"Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC"
}));
// clear backup file
remove(mkvFileBackup.data());
// 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(containsSubstrings(stdout, {
"Title Foo",
"Artist Bar"
}));
CPPUNIT_ASSERT(stdout.find("Year") == string::npos);
CPPUNIT_ASSERT(stdout.find("Comment") == string::npos);
CPPUNIT_ASSERT(stdout.find("Genre") == string::npos);
// clear working copies if all tests have been
remove(mkvFile.c_str());
remove(mkvFileBackup.data());
}
/*!
@ -120,30 +141,41 @@ void CliTests::testHandlingOfTargets()
{
string stdout, stderr;
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFileBackup(mkvFile + ".bak");
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);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"song",
"Title The song title",
"Genre The song genre"
}));
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"album",
"Title Elephant Dream - test 2",
"Genre The album genre"
}));
remove(mkvFileBackup.data());
// 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-target", "target-level=30", "--remove-target", "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);
CPPUNIT_ASSERT(containsSubstrings(stdout, {"song"}));
CPPUNIT_ASSERT(!containsSubstrings(stdout, {"song", "song"}));
CPPUNIT_ASSERT(stdout.find("album") == string::npos);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"3134325680",
"Title The audio track",
"Encoder likely some AAC encoder"
}));
remove(mkvFileBackup.data());
// clear working copies if all tests have been
remove(mkvFile.c_str());
}
/*!
@ -167,49 +199,73 @@ void CliTests::testMultipleFiles()
// 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);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"Title Big Buck Bunny - test 1",
"Title Elephant Dream - test 2",
"Title Elephant Dream - test 3"
}));
// clear backup files
remove((mkvFile1 + ".bak").c_str()), remove((mkvFile2 + ".bak").c_str()), remove((mkvFile3 + ".bak").c_str());
// 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);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"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",
"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",
"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"
}));
// clear working copies if all tests have been
remove(mkvFile1.c_str()), remove(mkvFile2.c_str()), remove(mkvFile3.c_str());
remove((mkvFile1 + ".bak").c_str()), remove((mkvFile2 + ".bak").c_str()), remove((mkvFile3 + ".bak").c_str());
}
/*!
* \brief Tests tagging multiple values per field.
* \remarks Fails because feature has not been implemented yet.
*/
void CliTests::testMultipleValuesPerField()
{
// TODO (feature not implemented yet)
string stdout, stderr;
const string mkvFile(workingCopyPath("matroska_wave1/test1.mkv"));
const char *const args1[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr};
const char *const args2[] = {"tageditor", "set", "artist=test1", "+artist=test2", "+artist=test3", "-f", mkvFile.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
//cout << stdout << endl;
//cerr << stderr << endl;
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"Artist test1",
"Artist test2",
"Artist test3"
}));
// clear working copies if all tests have been
remove(mkvFile.c_str());
remove((mkvFile + ".bak").c_str());
}
/*!
@ -219,6 +275,7 @@ void CliTests::testHandlingAttachments()
{
string stdout, stderr;
const string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
const string mkvFile1Backup(mkvFile1 + ".bak");
const string mkvFile2("path=" + testFilePath("matroska_wave1/test2.mkv"));
// add attachment
@ -226,24 +283,43 @@ void CliTests::testHandlingAttachments()
CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr));
const char *const args1[] = {"tageditor", "info", "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
size_t pos1;
CPPUNIT_ASSERT((pos1 = stdout.find("Attachments:")) != string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") > pos1);
CPPUNIT_ASSERT(stdout.find("MIME-type video/x-matroska") > pos1);
CPPUNIT_ASSERT(stdout.find("Description Test attachment") > pos1);
CPPUNIT_ASSERT(stdout.find("Size 20.16 MiB (21142764 byte)") > pos1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"Attachments:",
"Name test2.mkv",
"MIME-type video/x-matroska",
"Description Test attachment",
"Size 20.16 MiB (21142764 byte)"
}));
// clear backup file
remove(mkvFile1Backup.data());
// update attachment
const char *const args3[] = {"tageditor", "set", "--update-attachment", "name=test2.mkv", "desc=Updated test attachment", "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT((pos1 = stdout.find("Attachments:")) != string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") > pos1);
CPPUNIT_ASSERT(stdout.find("MIME-type video/x-matroska") > pos1);
CPPUNIT_ASSERT(stdout.find("Description Updated test attachment") > pos1);
CPPUNIT_ASSERT(stdout.find("Size 20.16 MiB (21142764 byte)") > pos1);
CPPUNIT_ASSERT(containsSubstrings(stdout, {
"Attachments:",
"Name test2.mkv",
"MIME-type video/x-matroska",
"Description Updated test attachment",
"Size 20.16 MiB (21142764 byte)"
}));
// clear backup file
remove(mkvFile1Backup.data());
// TODO: extract assigned attachment (feature not implemented yet)
// extract assigned attachment again
const char *const args4[] = {"tageditor", "extract", "--attachment", "name=test2.mkv", "-f", mkvFile1.data(), "-o", "/tmp/extracted.mkv", nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args4, stdout, stderr));
fstream origFile, extFile;
origFile.exceptions(ios_base::failbit | ios_base::badbit), extFile.exceptions(ios_base::failbit | ios_base::badbit);
origFile.open(mkvFile2.data() + 5, ios_base::in | ios_base::binary), extFile.open("/tmp/extracted.mkv", ios_base::in | ios_base::binary);
origFile.seekg(0, ios_base::end), extFile.seekg(0, ios_base::end);
int64 origFileSize = origFile.tellg(), extFileSize = extFile.tellg();
CPPUNIT_ASSERT_EQUAL(origFileSize, extFileSize);
for(origFile.seekg(0), extFile.seekg(0); origFileSize > 0; --origFileSize) {
CPPUNIT_ASSERT_EQUAL(origFile.get(), extFile.get());
}
remove("/tmp/extracted.mkv");
// remove assigned attachment
const char *const args5[] = {"tageditor", "set", "--remove-attachment", "name=test2.mkv", "-f", mkvFile1.data(), nullptr};
@ -251,6 +327,10 @@ void CliTests::testHandlingAttachments()
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT(stdout.find("Attachments:") == string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") == string::npos);
// clear working copies if all tests have been
remove(mkvFile1.data());
remove(mkvFile1Backup.data());
}
/*!
@ -262,11 +342,38 @@ void CliTests::testDisplayingInfo()
}
/*!
* \brief Tests extraction (used for cover or other binary fields).
* \brief Tests extraction of field values (used to extract cover or other binary fields).
* \remarks Extraction of attachments is already tested in testHandlingAttachments().
*/
void CliTests::testExtraction()
{
// TODO
string stdout, stderr;
const string mp41File(testFilePath("mtx-test-data/alac/othertest-itunes.m4a"));
// test extraction of cover
const char *const args1[] = {"tageditor", "extract", "cover", "-f", mp41File.data(), "-o", "/tmp/extracted.jpeg", nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
MediaFileInfo extractedInfo("/tmp/extracted.jpeg");
extractedInfo.open(true);
extractedInfo.parseContainerFormat();
CPPUNIT_ASSERT_EQUAL(22771ul, extractedInfo.size());
CPPUNIT_ASSERT(ContainerFormat::Jpeg == extractedInfo.containerFormat());
extractedInfo.invalidate();
// test assignment of cover by the way
const string mp4File2(workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a"));
const char *const args2[] = {"tageditor", "set", "cover=/tmp/extracted.jpeg", "-f", mp4File2.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr));
const char *const args3[] = {"tageditor", "extract", "cover", "-f", mp4File2.data(), "-o", "/tmp/extracted.jpeg", nullptr};
remove("/tmp/extracted.jpeg");
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
extractedInfo.open(true);
extractedInfo.parseContainerFormat();
CPPUNIT_ASSERT_EQUAL(22771ul, extractedInfo.size());
CPPUNIT_ASSERT(ContainerFormat::Jpeg == extractedInfo.containerFormat());
remove("/tmp/extracted.jpeg");
remove(mp4File2.data());
remove((mp4File2 + ".bak").data());
}
/*!