Improve CLI

* Use formatting
* Use more consistent format
* Show track summary
This commit is contained in:
Martchus 2017-09-22 00:19:24 +02:00
parent 25d570d394
commit 5ffa9b7d2c
4 changed files with 391 additions and 294 deletions

View File

@ -8,6 +8,7 @@
#include <c++utilities/application/argumentparser.h> #include <c++utilities/application/argumentparser.h>
#include <c++utilities/conversion/stringbuilder.h> #include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <iostream> #include <iostream>
#include <cstring> #include <cstring>
@ -64,37 +65,37 @@ void printNotifications(NotificationList &notifications, const char *head, bool
if(!notifications.empty()) { if(!notifications.empty()) {
printNotifications: printNotifications:
if(head) { if(head) {
cout << head << endl; cout << " - " << head << endl;
} }
Notification::sortByTime(notifications); Notification::sortByTime(notifications);
for(const auto &notification : notifications) { for(const auto &notification : notifications) {
switch(notification.type()) { switch(notification.type()) {
case NotificationType::Debug: case NotificationType::Debug:
if(beVerbose) { if(beVerbose) {
cout << "Debug "; cout << " Debug ";
break; break;
} else { } else {
continue; continue;
} }
case NotificationType::Information: case NotificationType::Information:
if(beVerbose) { if(beVerbose) {
cout << "Information "; cout << " Information ";
break; break;
} else { } else {
continue; continue;
} }
case NotificationType::Warning: case NotificationType::Warning:
cout << "Warning "; cout << " Warning ";
break; break;
case NotificationType::Critical: case NotificationType::Critical:
cout << "Error "; cout << " Error ";
break; break;
default: default:
; ;
} }
cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " "; cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
cout << notification.context() << ": "; cout << notification.context() << ": ";
cout << notification.message() << endl; cout << notification.message() << '\n';
} }
} }
} }
@ -137,7 +138,7 @@ void printProperty(const char *propName, ElementPosition elementPosition, const
void printFieldName(const char *fieldName, size_t fieldNameLen) void printFieldName(const char *fieldName, size_t fieldNameLen)
{ {
cout << ' ' << fieldName; cout << " " << fieldName;
// also write padding // also write padding
for(auto i = fieldNameLen; i < 18; ++i) { for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' '; cout << ' ';
@ -151,30 +152,35 @@ void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool s
const auto fieldNameLen = strlen(fieldName); const auto fieldNameLen = strlen(fieldName);
try { try {
// parse field denotation
const auto &values = scope.field.values(tag, tagType); const auto &values = scope.field.values(tag, tagType);
if(!skipEmpty || !values.empty()) {
// write value // skip empty values (unless prevented)
if(values.empty()) { if(skipEmpty && values.empty()) {
printFieldName(fieldName, fieldNameLen); return;
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";
}
cout << '\n';
}
}
} }
// print empty value (if not prevented)
if(values.empty()) {
printFieldName(fieldName, fieldNameLen);
cout << "none\n";
return;
}
// print values
for(const auto &value : values) {
printFieldName(fieldName, fieldNameLen);
try {
cout << value->toString(TagTextEncoding::Utf8);
} catch(const ConversionException &) {
// handle case when value can not be displayed as string
cout << "can't display as string (see --extract)";
}
cout << '\n';
}
} catch(const ConversionException &e) { } catch(const ConversionException &e) {
// handle conversion error which might happen when parsing field denotation
printFieldName(fieldName, fieldNameLen); printFieldName(fieldName, fieldNameLen);
cout << "unable to parse - " << e.what() << '\n'; cout << "unable to parse - " << e.what() << '\n';
} }
@ -585,4 +591,63 @@ bool stringToBool(const string &str)
throw ConversionException(argsToString('\"', str, " is not yes or no")); throw ConversionException(argsToString('\"', str, " is not yes or no"));
} }
bool logLineFinalized = true;
void logStatus(const StatusProvider &statusProvider)
{
static string lastStatus;
if(statusProvider.currentStatus() != lastStatus) {
// the ongoing operation ("status") has changed
// -> finalize previous line and make new line
if(!logLineFinalized) {
cout << "\r - [100%] " << lastStatus << endl;
logLineFinalized = true;
}
// -> update lastStatus
lastStatus = statusProvider.currentStatus();
}
// update current line if an operation is ongoing (status is not empty)
if(!lastStatus.empty()) {
int percentage = static_cast<int>(statusProvider.currentPercentage() * 100);
if(percentage < 0) {
percentage = 0;
}
cout << "\r - [" << setw(3) << percentage << "%] " << lastStatus << flush;
logLineFinalized = false;
}
}
void finalizeLog()
{
if(!logLineFinalized) {
cout << '\n';
logLineFinalized = true;
}
}
std::ostream &operator<< (std::ostream &stream, Phrases phrase)
{
using namespace EscapeCodes;
switch(phrase) {
case Phrases::Error:
setStyle(stream, Color::Red, ColorContext::Foreground, TextAttribute::Bold);
stream << "Error: ";
setStyle(stream, TextAttribute::Reset);
setStyle(stream, TextAttribute::Bold);
break;
case Phrases::Warning:
setStyle(stream, Color::Yellow, ColorContext::Foreground, TextAttribute::Bold);
stream << "Warning: ";
setStyle(stream, TextAttribute::Reset);
setStyle(stream, TextAttribute::Bold);
break;
case Phrases::End:
setStyle(stream, TextAttribute::Reset);
stream << '\n';
break;
}
return stream;
}
} }

View File

@ -9,10 +9,12 @@
#include <c++utilities/chrono/datetime.h> #include <c++utilities/chrono/datetime.h>
#include <c++utilities/chrono/timespan.h> #include <c++utilities/chrono/timespan.h>
#include <c++utilities/conversion/stringconversion.h> #include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/misc/traits.h>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <functional> #include <functional>
#include <type_traits>
namespace ApplicationUtilities { namespace ApplicationUtilities {
class Argument; class Argument;
@ -272,11 +274,11 @@ inline void printProperty(const char *propName, ChronoUtilities::DateTime dateTi
} }
} }
template<typename intType> template<typename NumberType, Traits::EnableIfAny<std::is_integral<NumberType>, std::is_floating_point<NumberType>>...>
inline void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 4) inline void printProperty(const char *propName, const NumberType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 4)
{ {
if(value != 0 || force) { if(value != 0 || force) {
printProperty(propName, ConversionUtilities::numberToString<intType>(value), suffix, indentation); printProperty(propName, ConversionUtilities::numberToString<NumberType>(value), suffix, indentation);
} }
} }
@ -291,6 +293,16 @@ bool applyTargetConfiguration(TagTarget &target, const std::string &configStr);
FieldDenotations parseFieldDenotations(const ApplicationUtilities::Argument &fieldsArg, bool readOnly); FieldDenotations parseFieldDenotations(const ApplicationUtilities::Argument &fieldsArg, bool readOnly);
std::string tagName(const Tag *tag); std::string tagName(const Tag *tag);
bool stringToBool(const std::string &str); bool stringToBool(const std::string &str);
extern bool logLineFinalized;
void logStatus(const StatusProvider &statusProvider);
void finalizeLog();
enum class Phrases {
Error,
Warning,
End,
};
std::ostream &operator<<(std::ostream &stream, Phrases phrase);
} }

View File

@ -30,6 +30,7 @@
#endif #endif
#include <iostream> #include <iostream>
#include <iomanip>
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
#include <memory> #include <memory>
@ -89,22 +90,22 @@ void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg,
if(file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, origNotify)) && file.flush()) { if(file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, origNotify)) && file.flush()) {
cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl; cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl;
} else { } else {
cerr << "Error: An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << endl; cerr << Phrases::Error << "An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << Phrases::End;
} }
} else { } else {
cout << HtmlInfo::generateInfo(inputFileInfo, origNotify).data() << endl; cout << HtmlInfo::generateInfo(inputFileInfo, origNotify).data() << endl;
} }
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::End;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; cerr << Phrases::Error << "An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::End;
} }
#else #else
VAR_UNUSED(inputFileArg); VAR_UNUSED(inputFileArg);
VAR_UNUSED(outputFileArg); VAR_UNUSED(outputFileArg);
VAR_UNUSED(validateArg); VAR_UNUSED(validateArg);
cerr << "Error: Generating HTML info is only available if built with Qt support." << endl; cerr << Phrases::Error << "Generating HTML info is only available if built with Qt support." << endl;
#endif #endif
} }
@ -112,7 +113,7 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
{ {
CMD_UTILS_START_CONSOLE; CMD_UTILS_START_CONSOLE;
if(!filesArg.isPresent() || filesArg.values().empty()) { if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl; cerr << Phrases::Error << "No files have been specified." << Phrases::End;
return; return;
} }
MediaFileInfo fileInfo; MediaFileInfo fileInfo;
@ -125,145 +126,147 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
fileInfo.parseTracks(); fileInfo.parseTracks();
fileInfo.parseAttachments(); fileInfo.parseAttachments();
fileInfo.parseChapters(); fileInfo.parseChapters();
cout << "Technical information for \"" << file << "\":" << endl;
cout << " Container format: " << fileInfo.containerFormatName() << endl; // print general/container-related info
{ cout << "Technical information for \"" << file << "\":\n";
if(const auto container = fileInfo.container()) { cout << " - " << TextAttribute::Bold << "Container format: " << fileInfo.containerFormatName() << Phrases::End;
size_t segmentIndex = 0; if(const auto container = fileInfo.container()) {
for(const auto &title : container->titles()) { size_t segmentIndex = 0;
if(segmentIndex) { for(const auto &title : container->titles()) {
printProperty("Title", title % " (segment " % ++segmentIndex + ")"); if(segmentIndex) {
} else { printProperty("Title", title % " (segment " % ++segmentIndex + ")");
++segmentIndex; } else {
printProperty("Title", title); ++segmentIndex;
} printProperty("Title", title);
}
printProperty("Document type", container->documentType());
printProperty("Read version", container->readVersion());
printProperty("Version", container->version());
printProperty("Document read version", container->doctypeReadVersion());
printProperty("Document version", container->doctypeVersion());
printProperty("Duration", container->duration());
printProperty("Creation time", container->creationTime());
printProperty("Modification time", container->modificationTime());
printProperty("Tag position", container->determineTagPosition());
printProperty("Index position", container->determineIndexPosition());
}
if(fileInfo.paddingSize()) {
printProperty("Padding", dataSizeToString(fileInfo.paddingSize()));
}
}
{ // tracks
const auto tracks = fileInfo.tracks();
if(!tracks.empty()) {
cout << " Tracks:" << endl;
for(const auto *track : tracks) {
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)) {
printProperty("Abbreviation", fmtAbbr);
}
printProperty("Extensions", track->format().extensionName());
printProperty("Raw format ID", track->formatId());
if(track->size()) {
printProperty("Size", dataSizeToString(track->size(), true));
}
printProperty("Duration", track->duration());
printProperty("FPS", track->fps());
if(track->channelConfigString()) {
printProperty("Channel config", track->channelConfigString());
} else {
printProperty("Channel count", track->channelCount());
}
if(track->extensionChannelConfigString()) {
printProperty("Extension channel config", track->extensionChannelConfigString());
}
printProperty("Bitrate", track->bitrate(), "kbit/s");
printProperty("Bits per sample", track->bitsPerSample());
printProperty("Sampling frequency", track->samplingFrequency(), "Hz");
printProperty("Extension sampling frequency", track->extensionSamplingFrequency(), "Hz");
printProperty(track->mediaType() == MediaType::Video
? "Frame count"
: "Sample count",
track->sampleCount());
printProperty("Creation time", track->creationTime());
printProperty("Modification time", track->modificationTime());
vector<string> 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 {
cout << " File has no (supported) tracks." << endl;
}
}
{ // attachments
const auto attachments = fileInfo.attachments();
if(!attachments.empty()) {
cout << "Attachments:" << endl;
for(const auto *attachment : attachments) {
printProperty("ID", attachment->id());
printProperty("Name", attachment->name());
printProperty("MIME-type", attachment->mimeType());
printProperty("Description", attachment->description());
if(attachment->data()) {
printProperty("Size", dataSizeToString(attachment->data()->size(), true));
}
cout << endl;
} }
} }
printProperty("Document type", container->documentType());
printProperty("Read version", container->readVersion());
printProperty("Version", container->version());
printProperty("Document read version", container->doctypeReadVersion());
printProperty("Document version", container->doctypeVersion());
printProperty("Duration", container->duration());
printProperty("Creation time", container->creationTime());
printProperty("Modification time", container->modificationTime());
printProperty("Tag position", container->determineTagPosition());
printProperty("Index position", container->determineIndexPosition());
} }
{ // chapters if(fileInfo.paddingSize()) {
const auto chapters = fileInfo.chapters(); printProperty("Padding", dataSizeToString(fileInfo.paddingSize()));
if(!chapters.empty()) { }
cout << "Chapters:" << endl;
for(const auto *chapter : chapters) { // print tracks
printProperty("ID", chapter->id()); const auto tracks = fileInfo.tracks();
if(!chapter->names().empty()) { if(!tracks.empty()) {
printProperty("Name", static_cast<string>(chapter->names().front())); cout << " - " << TextAttribute::Bold << "Tracks: " << fileInfo.technicalSummary() << Phrases::End;
} for(const auto *track : tracks) {
if(!chapter->startTime().isNegative()) { printProperty("ID", track->id(), nullptr, true);
printProperty("Start time", chapter->startTime().toString()); printProperty("Name", track->name());
} printProperty("Type", track->mediaTypeName());
if(!chapter->endTime().isNegative()) { if(track->language() != "und") {
printProperty("End time", chapter->endTime().toString()); printProperty("Language", track->language());
}
cout << endl;
} }
const char *fmtName = track->formatName(), *fmtAbbr = track->formatAbbreviation();
printProperty("Format", fmtName);
if(strcmp(fmtName, fmtAbbr)) {
printProperty("Abbreviation", fmtAbbr);
}
printProperty("Extensions", track->format().extensionName());
printProperty("Raw format ID", track->formatId());
if(track->size()) {
printProperty("Size", dataSizeToString(track->size(), true));
}
printProperty("Duration", track->duration());
printProperty("FPS", track->fps());
if(track->channelConfigString()) {
printProperty("Channel config", track->channelConfigString());
} else {
printProperty("Channel count", track->channelCount());
}
if(track->extensionChannelConfigString()) {
printProperty("Extension channel config", track->extensionChannelConfigString());
}
printProperty("Bitrate", track->bitrate(), "kbit/s");
printProperty("Bits per sample", track->bitsPerSample());
printProperty("Sampling frequency", track->samplingFrequency(), "Hz");
printProperty("Extension sampling frequency", track->extensionSamplingFrequency(), "Hz");
printProperty(track->mediaType() == MediaType::Video
? "Frame count"
: "Sample count",
track->sampleCount());
printProperty("Creation time", track->creationTime());
printProperty("Modification time", track->modificationTime());
vector<string> 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 << '\n';
}
} else {
cout << " - File has no (supported) tracks.\n";
}
// print attachments
const auto attachments = fileInfo.attachments();
if(!attachments.empty()) {
cout << " - " << TextAttribute::Bold << "Attachments:" << TextAttribute::Reset << '\n';
for(const auto *attachment : attachments) {
printProperty("ID", attachment->id());
printProperty("Name", attachment->name());
printProperty("MIME-type", attachment->mimeType());
printProperty("Description", attachment->description());
if(attachment->data()) {
printProperty("Size", dataSizeToString(static_cast<uint64>(attachment->data()->size()), true));
}
cout << '\n';
} }
} }
// print chapters
const auto chapters = fileInfo.chapters();
if(!chapters.empty()) {
cout << " - " << TextAttribute::Bold << "Chapters:" << TextAttribute::Reset << '\n';
for(const auto *chapter : chapters) {
printProperty("ID", chapter->id());
if(!chapter->names().empty()) {
printProperty("Name", static_cast<string>(chapter->names().front()));
}
if(!chapter->startTime().isNegative()) {
printProperty("Start time", chapter->startTime().toString());
}
if(!chapter->endTime().isNegative()) {
printProperty("End time", chapter->endTime().toString());
}
cout << '\n';
}
}
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::End;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::End;
} }
printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent()); printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent());
cout << endl; cout << endl;
} }
@ -273,7 +276,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
{ {
CMD_UTILS_START_CONSOLE; CMD_UTILS_START_CONSOLE;
if(!filesArg.isPresent() || filesArg.values().empty()) { if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl; cerr << Phrases::Error << "No files have been specified." << Phrases::End;
return; return;
} }
const auto fields = parseFieldDenotations(fieldsArg, true); const auto fields = parseFieldDenotations(fieldsArg, true);
@ -285,8 +288,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
fileInfo.open(true); fileInfo.open(true);
fileInfo.parseContainerFormat(); fileInfo.parseContainerFormat();
fileInfo.parseTags(); fileInfo.parseTags();
cout << file << endl; cout << "Tag information for \"" << file << "\":\n";
cout << "Tag information for \"" << file << "\":" << endl;
const auto tags = fileInfo.tags(); const auto tags = fileInfo.tags();
if(!tags.empty()) { if(!tags.empty()) {
// iterate through all tags // iterate through all tags
@ -294,7 +296,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
// determine tag type // determine tag type
const TagType tagType = tag->type(); const TagType tagType = tag->type();
// write tag name and target, eg. MP4/iTunes tag // write tag name and target, eg. MP4/iTunes tag
cout << tagName(tag) << endl; cout << " - " << TextAttribute::Bold << tagName(tag) << TextAttribute::Reset << '\n';
// iterate through fields specified by the user // iterate through fields specified by the user
if(fields.empty()) { if(fields.empty()) {
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) { for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
@ -310,13 +312,13 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
} }
} }
} else { } else {
cout << " File has no (supported) tag information." << endl; cout << " - File has no (supported) tag information.\n";
} }
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::End;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::End;
} }
printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent()); printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent());
cout << endl; cout << endl;
@ -327,11 +329,11 @@ void setTagInfo(const SetTagInfoArgs &args)
{ {
CMD_UTILS_START_CONSOLE; CMD_UTILS_START_CONSOLE;
if(!args.filesArg.isPresent() || args.filesArg.values().empty()) { if(!args.filesArg.isPresent() || args.filesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl; cerr << Phrases::Error << "No files have been specified." << Phrases::End;
return; return;
} }
if(args.outputFilesArg.isPresent() && args.outputFilesArg.values().size() != args.filesArg.values().size()) { if(args.outputFilesArg.isPresent() && args.outputFilesArg.values().size() != args.filesArg.values().size()) {
cerr << "Error: The number of output files does not match the number of input files." << endl; cerr << Phrases::Error << "The number of output files does not match the number of input files." << Phrases::End;
return; return;
} }
auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector<const char *>(); auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector<const char *>();
@ -346,7 +348,7 @@ void setTagInfo(const SetTagInfoArgs &args)
&& !args.id3v1UsageArg.isPresent() && !args.id3v1UsageArg.isPresent()
&& !args.id3v2UsageArg.isPresent() && !args.id3v2UsageArg.isPresent()
&& !args.id3v2VersionArg.isPresent()) { && !args.id3v2VersionArg.isPresent()) {
cerr << "Warning: No fields/attachments have been specified." << endl; cerr << Phrases::Warning << "No fields/attachments have been specified." << Phrases::End;
} }
// determine required targets // determine required targets
vector<TagTarget> requiredTargets; vector<TagTarget> requiredTargets;
@ -369,7 +371,7 @@ void setTagInfo(const SetTagInfoArgs &args)
} else if(applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) { } else if(applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) {
validRemoveTargetsSpecified = true; validRemoveTargetsSpecified = true;
} else { } else {
cerr << "Warning: The given target specification \"" << targetDenotation << "\" is invalid and will be ignored." << endl; cerr << Phrases::Warning << "The given target specification \"" << targetDenotation << "\" is invalid and will be ignored." << Phrases::End;
} }
} }
} }
@ -383,7 +385,7 @@ void setTagInfo(const SetTagInfoArgs &args)
} }
} catch (const ConversionException &) { } catch (const ConversionException &) {
id3v2Version = 3; id3v2Version = 3;
cerr << "Warning: The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid and will be ingored." << endl; cerr << Phrases::Warning << "The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid and will be ingored." << Phrases::End;
} }
} }
const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8); const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8);
@ -405,7 +407,7 @@ void setTagInfo(const SetTagInfoArgs &args)
for(const char *file : args.filesArg.values()) { for(const char *file : args.filesArg.values()) {
try { try {
// parse tags // parse tags
cout << "Setting tag information for \"" << file << "\" ..." << endl; cout << TextAttribute::Bold << "Setting tag information for \"" << file << "\" ..." << TextAttribute::Reset << endl;
notifications.clear(); notifications.clear();
fileInfo.setPath(file); fileInfo.setPath(file);
fileInfo.parseContainerFormat(); fileInfo.parseContainerFormat();
@ -432,12 +434,12 @@ void setTagInfo(const SetTagInfoArgs &args)
if(segmentIndex < segmentCount) { if(segmentIndex < segmentCount) {
container->setTitle(newTitle, segmentIndex); container->setTitle(newTitle, segmentIndex);
} else { } else {
cerr << "Warning: The specified document title \"" << newTitle << "\" can not be set because the file has not that many segments." << endl; cerr << Phrases::Warning << "The specified document title \"" << newTitle << "\" can not be set because the file has not that many segments." << Phrases::End;
} }
++segmentIndex; ++segmentIndex;
} }
} else { } else {
cerr << "Warning: Setting the document title is not supported for the file." << endl; cerr << Phrases::Warning << "Setting the document title is not supported for the file." << Phrases::End;
} }
} }
// select the relevant values for the current file index // select the relevant values for the current file index
@ -619,17 +621,22 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.setSaveFilePath(currentOutputFile != noMoreOutputFiles ? string(*currentOutputFile) : string()); fileInfo.setSaveFilePath(currentOutputFile != noMoreOutputFiles ? string(*currentOutputFile) : string());
fileInfo.gatherRelatedNotifications(notifications); fileInfo.gatherRelatedNotifications(notifications);
fileInfo.invalidateNotifications(); fileInfo.invalidateNotifications();
fileInfo.registerCallback(logStatus);
fileInfo.applyChanges(); fileInfo.applyChanges();
fileInfo.gatherRelatedNotifications(notifications); fileInfo.gatherRelatedNotifications(notifications);
cout << "Changes have been applied." << endl; finalizeLog();
cout << " - Changes have been applied." << endl;
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: Failed to apply changes." << endl; finalizeLog();
cerr << " - " << Phrases::Error << "Failed to apply changes." << endl;
} }
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading/writing the file \"" << file << "\"." << endl; finalizeLog();
cerr << " - " << Phrases::Error << "A parsing failure occured when reading/writing the file \"" << file << "\"." << endl;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading/writing the file \"" << file << "\"." << endl; finalizeLog();
cerr << " - " << Phrases::Error << "An IO failure occured when reading/writing the file \"" << file << "\"." << endl;
} }
printNotifications(notifications, "Notifications:", args.verboseArg.isPresent()); printNotifications(notifications, "Notifications:", args.verboseArg.isPresent());
@ -652,11 +659,11 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
} }
if(((fieldDenotations.size() != 1) || (!attachmentInfo.hasId && !attachmentInfo.name)) if(((fieldDenotations.size() != 1) || (!attachmentInfo.hasId && !attachmentInfo.name))
&& ((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; cerr << Phrases::Error << "Excactly one field or attachment needs to be specified." << Phrases::End;
return; return;
} }
if(!inputFilesArg.isPresent() || inputFilesArg.values().empty()) { if(!inputFilesArg.isPresent() || inputFilesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl; cerr << Phrases::Error << "No files have been specified." << Phrases::End;
return; return;
} }
@ -688,7 +695,7 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
} }
} }
if(values.empty()) { if(values.empty()) {
cerr << " None of the specified files has a (supported) " << fieldArg.values().front() << " field." << endl; cerr << " - " << Phrases::Error << "None of the specified files has a (supported) " << fieldArg.values().front() << " field." << Phrases::End;
} else if(outputFileArg.isPresent()) { } else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension; string outputFilePathWithoutExtension, outputFileExtension;
if(values.size() > 1) { if(values.size() > 1) {
@ -703,10 +710,10 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
outputFileStream.open(path, ios_base::out | ios_base::binary); outputFileStream.open(path, ios_base::out | ios_base::binary);
outputFileStream.write(value.first->dataPointer(), value.first->dataSize()); outputFileStream.write(value.first->dataPointer(), value.first->dataSize());
outputFileStream.flush(); outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl; cout << " - Value has been saved to \"" << path << "\"." << endl;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl; cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::End;
} }
} }
} else { } else {
@ -736,7 +743,7 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
} }
} }
if(attachments.empty()) { if(attachments.empty()) {
cerr << " None of the specified files has a (supported) attachment with the specified ID/name." << endl; cerr << " - " << Phrases::Error << "None of the specified files has a (supported) attachment with the specified ID/name." << Phrases::End;
} else if(outputFileArg.isPresent()) { } else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension; string outputFilePathWithoutExtension, outputFileExtension;
if(attachments.size() > 1) { if(attachments.size() > 1) {
@ -751,10 +758,10 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
outputFileStream.open(path, ios_base::out | ios_base::binary); outputFileStream.open(path, ios_base::out | ios_base::binary);
attachment.first->data()->copyTo(outputFileStream); attachment.first->data()->copyTo(outputFileStream);
outputFileStream.flush(); outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl; cout << " - Value has been saved to \"" << path << "\"." << endl;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl; cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::End;
} }
} }
} else { } else {
@ -765,10 +772,10 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
} }
} catch(const ApplicationUtilities::Failure &) { } catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::End;
} catch(...) { } catch(...) {
::IoUtilities::catchIoFailure(); ::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl; cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::End;
} }
printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent()); printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent());
} }

View File

@ -86,7 +86,16 @@ void CliTests::tearDown()
template <typename StringType, bool negateErrorCond = false> template <typename StringType, bool negateErrorCond = false>
bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings) bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
{ {
bool res = containsSubstrings(str, substrings); vector<const typename StringType::value_type *> failedSubstrings;
typename StringType::size_type currentPos = 0;
for (const auto *substr : substrings) {
if ((currentPos = str.find(substr, currentPos)) == StringType::npos) {
failedSubstrings.emplace_back(substr);
}
currentPos += std::strlen(substr);
}
bool res = failedSubstrings.empty();
if(negateErrorCond) { if(negateErrorCond) {
res = !res; res = !res;
} }
@ -97,8 +106,8 @@ bool testContainsSubstrings(const StringType &str, std::initializer_list<const t
cout << " - test failed: output DOES contain substrings it shouldn't\n"; cout << " - test failed: output DOES contain substrings it shouldn't\n";
} }
cout << "Output:\n" << str; cout << "Output:\n" << str;
cout << "Substrings:\n"; cout << "Failed substrings:\n";
for(const auto &substr : substrings) { for(const auto &substr : failedSubstrings) {
cout << substr << "\n"; cout << substr << "\n";
} }
} }
@ -268,64 +277,68 @@ void CliTests::testId3SpecificOptions()
// verify both ID3 tags are detected // verify both ID3 tags are detected
const char *const args1[] = {"tageditor", "get", "-f", mp3File1.data(), nullptr}; const char *const args1[] = {"tageditor", "get", "-f", mp3File1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1); TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stdout.find("ID3v1 tag\n" CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Title Cohesion\n" " - \e[1mID3v1 tag\e[0m\n"
" Album Double Nickels On The Dime\n" " Title Cohesion\n"
" Artist Minutemen\n" " Album Double Nickels On The Dime\n"
" Genre Punk Rock\n" " Artist Minutemen\n"
" Year 1984\n" " Genre Punk Rock\n"
" Comment ExactAudioCopy v0.95b4\n" " Year 1984\n"
" Track 4\n" " Comment ExactAudioCopy v0.95b4\n"
"ID3v2 tag (version 2.3.0)\n" " Track 4\n",
" Title Cohesion\n" " - \e[1mID3v2 tag (version 2.3.0)\e[0m\n"
" Album Double Nickels On The Dime\n" " Title Cohesion\n"
" Artist Minutemen\n" " Album Double Nickels On The Dime\n"
" Genre Punk Rock\n" " Artist Minutemen\n"
" Year 1984\n" " Genre Punk Rock\n"
" Comment ExactAudioCopy v0.95b4\n" " Year 1984\n"
" Track 4/43\n" " Comment ExactAudioCopy v0.95b4\n"
" Duration 00:00:00\n" " Track 4/43\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos); " Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)"
}));
// remove ID3v1 tag, convert ID3v2 tag to version 4 // remove ID3v1 tag, convert ID3v2 tag to version 4
const char *const args2[] = {"tageditor", "set", "--id3v1-usage", "never", "--id3v2-version", "4", "-f", mp3File1.data(), nullptr}; const char *const args2[] = {"tageditor", "set", "--id3v1-usage", "never", "--id3v2-version", "4", "-f", mp3File1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2); TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1); TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stdout.find("ID3v1 tag") == string::npos); CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
CPPUNIT_ASSERT(stdout.find("ID3v2 tag (version 2.4.0)\n" " - \e[1mID3v2 tag (version 2.4.0)\e[0m\n"
" Title Cohesion\n" " Title Cohesion\n"
" Album Double Nickels On The Dime\n" " Album Double Nickels On The Dime\n"
" Artist Minutemen\n" " Artist Minutemen\n"
" Genre Punk Rock\n" " Genre Punk Rock\n"
" Year 1984\n" " Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n" " Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n" " Track 4/43\n"
" Duration 00:00:00\n" " Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos); " Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)"
}));
remove(mp3File1Backup.data()); remove(mp3File1Backup.data());
// convert remaining ID3v2 tag to version 2, add an ID3v1 tag again and set a field with unicode char by the way // convert remaining ID3v2 tag to version 2, add an ID3v1 tag again and set a field with unicode char by the way
const char *const args3[] = {"tageditor", "set", "album=Dóuble Nickels On The Dime", "--id3v1-usage", "always", "--id3v2-version", "2", "--id3-init-on-create", "-f", mp3File1.data(), nullptr}; const char *const args3[] = {"tageditor", "set", "album=Dóuble Nickels On The Dime", "--id3v1-usage", "always", "--id3v2-version", "2", "--id3-init-on-create", "-f", mp3File1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr)); CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr)); CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT(stdout.find("ID3v1 tag\n" CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Title Cohesion\n" " - \e[1mID3v1 tag\e[0m\n"
" Album Dóuble Nickels On The Dime\n" " Title Cohesion\n"
" Artist Minutemen\n" " Album Dóuble Nickels On The Dime\n"
" Genre Punk Rock\n" " Artist Minutemen\n"
" Year 1984\n" " Genre Punk Rock\n"
" Comment ExactAudioCopy v0.95b4\n" " Year 1984\n"
" Track 4\n" " Comment ExactAudioCopy v0.95b4\n"
"ID3v2 tag (version 2.2.0)\n" " Track 4\n",
" Title Cohesion\n" " - \e[1mID3v2 tag (version 2.2.0)\e[0m\n"
" Album Dóuble Nickels On The Dime\n" " Title Cohesion\n"
" Artist Minutemen\n" " Album Dóuble Nickels On The Dime\n"
" Genre Punk Rock\n" " Artist Minutemen\n"
" Year 1984\n" " Genre Punk Rock\n"
" Comment ExactAudioCopy v0.95b4\n" " Year 1984\n"
" Track 4/43\n" " Comment ExactAudioCopy v0.95b4\n"
" Duration 00:00:00\n" " Track 4/43\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos); " Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)"}));
remove(mp3File1.data()); remove(mp3File1.data());
remove(mp3File1Backup.data()); remove(mp3File1Backup.data());
} }
@ -411,30 +424,30 @@ void CliTests::testMultipleFiles()
TESTUTILS_ASSERT_EXEC(args2); TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1); TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" Title MKV testfiles\n" " Title MKV testfiles\n"
" Year 2010\n" " Year 2010\n"
" Comment Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock\n" " Comment Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock\n"
" Total parts 3\n" " Total parts 3\n"
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n" " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test1\n" " Title test1\n"
" Part 1", " Part 1",
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" Title MKV testfiles\n" " Title MKV testfiles\n"
" Year 2010\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" " 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" " Total parts 3\n"
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n" " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test2\n" " Title test2\n"
" Part 2", " Part 2",
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" Title MKV testfiles\n" " Title MKV testfiles\n"
" Year 2010\n" " Year 2010\n"
" Comment Matroska Validation File 3, header stripping on the video track and no SimpleBlock\n" " Comment Matroska Validation File 3, header stripping on the video track and no SimpleBlock\n"
" Total parts 3\n" " Total parts 3",
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n" " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test3\n" " Title test3\n"
" Part 3" " Part 3"
})); }));
// clear working copies if all tests have been // clear working copies if all tests have been
@ -466,10 +479,10 @@ void CliTests::testOutputFile()
const char *const args3[] = {"tageditor", "get", "-f", "/tmp/test1.mkv", "/tmp/test2.mkv", nullptr}; const char *const args3[] = {"tageditor", "get", "-f", "/tmp/test1.mkv", "/tmp/test2.mkv", nullptr};
TESTUTILS_ASSERT_EXEC(args3); TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n" " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test1\n", " Title test1\n",
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n" " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test2\n" " Title test2\n"
})); }));
remove(mkvFile1.data()), remove(mkvFile2.data()); remove(mkvFile1.data()), remove(mkvFile2.data());
@ -586,7 +599,7 @@ void CliTests::testDisplayingInfo()
const char *const args1[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr}; const char *const args1[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1); TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: Matroska\n" " - \e[1mContainer format: Matroska\e[0m\n"
" Document type matroska\n" " Document type matroska\n"
" Read version 1\n" " Read version 1\n"
" Version 1\n" " Version 1\n"
@ -595,7 +608,7 @@ void CliTests::testDisplayingInfo()
" Duration 47 s 509 ms\n" " Duration 47 s 509 ms\n"
" Tag position before data\n" " Tag position before data\n"
" Index position before data\n", " Index position before data\n",
" Tracks:\n" " - \e[1mTracks: H.264-576p / AAC-LC-2ch\e[0m\n"
" ID 1863976627\n" " ID 1863976627\n"
" Type Video\n" " Type Video\n"
" Format Advanced Video Coding Main Profile\n" " Format Advanced Video Coding Main Profile\n"
@ -615,12 +628,12 @@ void CliTests::testDisplayingInfo()
const char *const args2[] = {"tageditor", "info", "-f", mp4File.data(), nullptr}; const char *const args2[] = {"tageditor", "info", "-f", mp4File.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2); TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: MPEG-4 Part 14\n" " - \e[1mContainer format: MPEG-4 Part 14\e[0m\n"
" Document type mp42\n" " Document type mp42\n"
" Duration 3 min\n" " Duration 3 min\n"
" Creation time 2014-12-10 16:22:41\n" " Creation time 2014-12-10 16:22:41\n"
" Modification time 2014-12-10 16:22:41\n", " Modification time 2014-12-10 16:22:41\n",
" Tracks:\n" " - \e[1mTracks: HE-AAC-2ch\e[0m\n"
" ID 1\n" " ID 1\n"
" Name soun\n" " Name soun\n"
" Type Audio\n" " Type Audio\n"
@ -663,7 +676,7 @@ void CliTests::testSettingTrackMetaData()
TESTUTILS_ASSERT_EXEC(args1); TESTUTILS_ASSERT_EXEC(args1);
TESTUTILS_ASSERT_EXEC(args2); TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: Matroska\n" " - \e[1mContainer format: Matroska\e[0m\n"
" Document type matroska\n" " Document type matroska\n"
" Read version 1\n" " Read version 1\n"
" Version 1\n" " Version 1\n"
@ -672,7 +685,7 @@ void CliTests::testSettingTrackMetaData()
" Duration 47 s 509 ms\n" " Duration 47 s 509 ms\n"
" Tag position before data\n" " Tag position before data\n"
" Index position before data\n", " Index position before data\n",
" Tracks:\n" " - \e[1mTracks: H.264-576p / AAC-LC-2ch-ger\e[0m\n"
" ID 1863976627\n" " ID 1863976627\n"
" Name video track\n" " Name video track\n"
" Type Video\n" " Type Video\n"
@ -692,22 +705,22 @@ void CliTests::testSettingTrackMetaData()
" Labeled as default, forced"})); " Labeled as default, forced"}));
TESTUTILS_ASSERT_EXEC(args3); TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\n" " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" Title title of tag\n" " Title title of tag\n"
" Artist setting tag value again\n" " Artist setting tag value again\n"
" Year 2010\n" " Year 2010\n"
" Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC" " 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}; const char *const args4[] = {"tageditor", "info", "-f", mp4File.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args4); TESTUTILS_ASSERT_EXEC(args4);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, { CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: MPEG-4 Part 14\n" " - \e[1mContainer format: MPEG-4 Part 14\e[0m\n"
" Document type mp42\n" " Document type mp42\n"
" Duration 3 min\n" " Duration 3 min\n"
" Creation time 2014-12-10 16:22:41\n" " Creation time 2014-12-10 16:22:41\n"
" Modification time 2014-12-10 16:22:41\n", " Modification time 2014-12-10 16:22:41\n",
" Tracks:\n" " - \e[1mTracks: HE-AAC-2ch-eng\e[0m\n"
" ID 1\n" " ID 1\n"
" Name sbr and ps\n" " Name sbr and ps\n"
" Type Audio\n" " Type Audio\n"
@ -815,15 +828,15 @@ void CliTests::testFileLayoutOptions()
const char *const args5[] = {"tageditor", "get", "-f", mp4File2.data(), nullptr}; const char *const args5[] = {"tageditor", "get", "-f", mp4File2.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args5); TESTUTILS_ASSERT_EXEC(args5);
CPPUNIT_ASSERT(stdout.find("MP4/iTunes tag\n" CPPUNIT_ASSERT(stdout.find(" - \e[1mMP4/iTunes tag\e[0m\n"
" Title You Shook Me All Night Long\n" " Title You Shook Me All Night Long\n"
" Album Who Made Who\n" " Album Who Made Who\n"
" Artist ACDC\n" " Artist ACDC\n"
" Genre Rock\n" " Genre Rock\n"
" Year 1986\n" " Year 1986\n"
" Track 2/9\n" " Track 2/9\n"
" Encoder Nero AAC codec / 1.5.3.0, remuxed with Lavf57.56.100\n" " Encoder Nero AAC codec / 1.5.3.0, remuxed with Lavf57.56.100\n"
" Encoder settings ndaudio 1.5.3.0 / -q 0.34") != string::npos); " Encoder settings ndaudio 1.5.3.0 / -q 0.34") != string::npos);
remove((mp4File2 + ".bak").data()); remove((mp4File2 + ".bak").data());
const char *const args6[] = {"tageditor", "set", "--index-pos", "front", "--force", "-f", mp4File2.data(), nullptr}; const char *const args6[] = {"tageditor", "set", "--index-pos", "front", "--force", "-f", mp4File2.data(), nullptr};