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/conversion/stringbuilder.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <iostream>
#include <cstring>
@ -64,37 +65,37 @@ void printNotifications(NotificationList &notifications, const char *head, bool
if(!notifications.empty()) {
printNotifications:
if(head) {
cout << head << endl;
cout << " - " << head << endl;
}
Notification::sortByTime(notifications);
for(const auto &notification : notifications) {
switch(notification.type()) {
case NotificationType::Debug:
if(beVerbose) {
cout << "Debug ";
cout << " Debug ";
break;
} else {
continue;
}
case NotificationType::Information:
if(beVerbose) {
cout << "Information ";
cout << " Information ";
break;
} else {
continue;
}
case NotificationType::Warning:
cout << "Warning ";
cout << " Warning ";
break;
case NotificationType::Critical:
cout << "Error ";
cout << " Error ";
break;
default:
;
}
cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
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)
{
cout << ' ' << fieldName;
cout << " " << fieldName;
// also write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
@ -151,30 +152,35 @@ void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool s
const auto fieldNameLen = strlen(fieldName);
try {
// parse field denotation
const auto &values = scope.field.values(tag, tagType);
if(!skipEmpty || !values.empty()) {
// write value
if(values.empty()) {
printFieldName(fieldName, fieldNameLen);
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';
}
}
// skip empty values (unless prevented)
if(skipEmpty && values.empty()) {
return;
}
// 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) {
// handle conversion error which might happen when parsing field denotation
printFieldName(fieldName, fieldNameLen);
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"));
}
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/timespan.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/misc/traits.h>
#include <vector>
#include <unordered_map>
#include <functional>
#include <type_traits>
namespace ApplicationUtilities {
class Argument;
@ -272,11 +274,11 @@ inline void printProperty(const char *propName, ChronoUtilities::DateTime dateTi
}
}
template<typename intType>
inline void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 4)
template<typename NumberType, Traits::EnableIfAny<std::is_integral<NumberType>, std::is_floating_point<NumberType>>...>
inline void printProperty(const char *propName, const NumberType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 4)
{
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);
std::string tagName(const Tag *tag);
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
#include <iostream>
#include <iomanip>
#include <cstring>
#include <algorithm>
#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()) {
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;
cerr << Phrases::Error << "An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << Phrases::End;
}
} else {
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;
cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::End;
} catch(...) {
::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
VAR_UNUSED(inputFileArg);
VAR_UNUSED(outputFileArg);
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
}
@ -112,7 +113,7 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
{
CMD_UTILS_START_CONSOLE;
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;
}
MediaFileInfo fileInfo;
@ -125,145 +126,147 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
fileInfo.parseTracks();
fileInfo.parseAttachments();
fileInfo.parseChapters();
cout << "Technical information for \"" << file << "\":" << endl;
cout << " Container format: " << fileInfo.containerFormatName() << endl;
{
if(const auto container = fileInfo.container()) {
size_t segmentIndex = 0;
for(const auto &title : container->titles()) {
if(segmentIndex) {
printProperty("Title", title % " (segment " % ++segmentIndex + ")");
} else {
++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;
// print general/container-related info
cout << "Technical information for \"" << file << "\":\n";
cout << " - " << TextAttribute::Bold << "Container format: " << fileInfo.containerFormatName() << Phrases::End;
if(const auto container = fileInfo.container()) {
size_t segmentIndex = 0;
for(const auto &title : container->titles()) {
if(segmentIndex) {
printProperty("Title", title % " (segment " % ++segmentIndex + ")");
} else {
++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());
}
{ // chapters
const auto chapters = fileInfo.chapters();
if(!chapters.empty()) {
cout << "Chapters:" << endl;
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 << endl;
if(fileInfo.paddingSize()) {
printProperty("Padding", dataSizeToString(fileInfo.paddingSize()));
}
// print tracks
const auto tracks = fileInfo.tracks();
if(!tracks.empty()) {
cout << " - " << TextAttribute::Bold << "Tracks: " << fileInfo.technicalSummary() << Phrases::End;
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 << '\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 &) {
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(...) {
::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());
cout << endl;
}
@ -273,7 +276,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
{
CMD_UTILS_START_CONSOLE;
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;
}
const auto fields = parseFieldDenotations(fieldsArg, true);
@ -285,8 +288,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
fileInfo.open(true);
fileInfo.parseContainerFormat();
fileInfo.parseTags();
cout << file << endl;
cout << "Tag information for \"" << file << "\":" << endl;
cout << "Tag information for \"" << file << "\":\n";
const auto tags = fileInfo.tags();
if(!tags.empty()) {
// iterate through all tags
@ -294,7 +296,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const A
// determine tag type
const TagType tagType = tag->type();
// 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
if(fields.empty()) {
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 {
cout << " File has no (supported) tag information." << endl;
cout << " - File has no (supported) tag information.\n";
}
} 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(...) {
::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());
cout << endl;
@ -327,11 +329,11 @@ void setTagInfo(const SetTagInfoArgs &args)
{
CMD_UTILS_START_CONSOLE;
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;
}
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;
}
auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector<const char *>();
@ -346,7 +348,7 @@ void setTagInfo(const SetTagInfoArgs &args)
&& !args.id3v1UsageArg.isPresent()
&& !args.id3v2UsageArg.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
vector<TagTarget> requiredTargets;
@ -369,7 +371,7 @@ void setTagInfo(const SetTagInfoArgs &args)
} else if(applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) {
validRemoveTargetsSpecified = true;
} 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 &) {
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);
@ -405,7 +407,7 @@ void setTagInfo(const SetTagInfoArgs &args)
for(const char *file : args.filesArg.values()) {
try {
// parse tags
cout << "Setting tag information for \"" << file << "\" ..." << endl;
cout << TextAttribute::Bold << "Setting tag information for \"" << file << "\" ..." << TextAttribute::Reset << endl;
notifications.clear();
fileInfo.setPath(file);
fileInfo.parseContainerFormat();
@ -432,12 +434,12 @@ void setTagInfo(const SetTagInfoArgs &args)
if(segmentIndex < segmentCount) {
container->setTitle(newTitle, segmentIndex);
} 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;
}
} 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
@ -619,17 +621,22 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.setSaveFilePath(currentOutputFile != noMoreOutputFiles ? string(*currentOutputFile) : string());
fileInfo.gatherRelatedNotifications(notifications);
fileInfo.invalidateNotifications();
fileInfo.registerCallback(logStatus);
fileInfo.applyChanges();
fileInfo.gatherRelatedNotifications(notifications);
cout << "Changes have been applied." << endl;
finalizeLog();
cout << " - Changes have been applied." << endl;
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: Failed to apply changes." << endl;
finalizeLog();
cerr << " - " << Phrases::Error << "Failed to apply changes." << endl;
}
} 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(...) {
::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());
@ -652,11 +659,11 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
}
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;
cerr << Phrases::Error << "Excactly one field or attachment needs to be specified." << Phrases::End;
return;
}
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;
}
@ -688,7 +695,7 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
}
}
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()) {
string outputFilePathWithoutExtension, outputFileExtension;
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.write(value.first->dataPointer(), value.first->dataSize());
outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl;
cout << " - Value has been saved to \"" << path << "\"." << endl;
} catch(...) {
::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 {
@ -736,7 +743,7 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
}
}
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()) {
string outputFilePathWithoutExtension, outputFileExtension;
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);
attachment.first->data()->copyTo(outputFileStream);
outputFileStream.flush();
cout << "Value has been saved to \"" << path << "\"." << endl;
cout << " - Value has been saved to \"" << path << "\"." << endl;
} catch(...) {
::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 {
@ -765,10 +772,10 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const
}
} 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(...) {
::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());
}

View File

@ -86,7 +86,16 @@ void CliTests::tearDown()
template <typename StringType, bool negateErrorCond = false>
bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
{
bool res = containsSubstrings(str, substrings);
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) {
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 << "Output:\n" << str;
cout << "Substrings:\n";
for(const auto &substr : substrings) {
cout << "Failed substrings:\n";
for(const auto &substr : failedSubstrings) {
cout << substr << "\n";
}
}
@ -268,64 +277,68 @@ void CliTests::testId3SpecificOptions()
// verify both ID3 tags are detected
const char *const args1[] = {"tageditor", "get", "-f", mp3File1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stdout.find("ID3v1 tag\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4\n"
"ID3v2 tag (version 2.3.0)\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" - \e[1mID3v1 tag\e[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4\n",
" - \e[1mID3v2 tag (version 2.3.0)\e[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" 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
const char *const args2[] = {"tageditor", "set", "--id3v1-usage", "never", "--id3v2-version", "4", "-f", mp3File1.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(stdout.find("ID3v1 tag") == string::npos);
CPPUNIT_ASSERT(stdout.find("ID3v2 tag (version 2.4.0)\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" - \e[1mID3v2 tag (version 2.4.0)\e[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)"
}));
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
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(args1, stdout, stderr));
CPPUNIT_ASSERT(stdout.find("ID3v1 tag\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4\n"
"ID3v2 tag (version 2.2.0)\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)") != string::npos);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" - \e[1mID3v1 tag\e[0m\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4\n",
" - \e[1mID3v2 tag (version 2.2.0)\e[0m\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
" Genre Punk Rock\n"
" Year 1984\n"
" Comment ExactAudioCopy v0.95b4\n"
" Track 4/43\n"
" Duration 00:00:00\n"
" Encoder settings LAME 64bits version 3.99 (http://lame.sf.net)"}));
remove(mp3File1.data());
remove(mp3File1Backup.data());
}
@ -411,30 +424,30 @@ void CliTests::testMultipleFiles()
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(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"
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\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"
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test1\n"
" Part 1",
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\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"
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test2\n"
" Part 2",
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\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",
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test3\n"
" Part 3"
}));
// 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};
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
" Title test1\n",
"Matroska tag targeting \"level 30 'track, song, chapter'\"\n"
" Title test2\n"
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test1\n",
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" Title test2\n"
}));
remove(mkvFile1.data()), remove(mkvFile2.data());
@ -586,7 +599,7 @@ void CliTests::testDisplayingInfo()
const char *const args1[] = {"tageditor", "info", "-f", mkvFile.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: Matroska\n"
" - \e[1mContainer format: Matroska\e[0m\n"
" Document type matroska\n"
" Read version 1\n"
" Version 1\n"
@ -595,7 +608,7 @@ void CliTests::testDisplayingInfo()
" Duration 47 s 509 ms\n"
" Tag 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"
" Type Video\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};
TESTUTILS_ASSERT_EXEC(args2);
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"
" Duration 3 min\n"
" Creation 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"
" Name soun\n"
" Type Audio\n"
@ -663,7 +676,7 @@ void CliTests::testSettingTrackMetaData()
TESTUTILS_ASSERT_EXEC(args1);
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(testContainsSubstrings(stdout, {
" Container format: Matroska\n"
" - \e[1mContainer format: Matroska\e[0m\n"
" Document type matroska\n"
" Read version 1\n"
" Version 1\n"
@ -672,7 +685,7 @@ void CliTests::testSettingTrackMetaData()
" Duration 47 s 509 ms\n"
" Tag 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"
" Name video track\n"
" Type Video\n"
@ -692,22 +705,22 @@ void CliTests::testSettingTrackMetaData()
" 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"
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\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"
" - \e[1mContainer format: MPEG-4 Part 14\e[0m\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"
" - \e[1mTracks: HE-AAC-2ch-eng\e[0m\n"
" ID 1\n"
" Name sbr and ps\n"
" Type Audio\n"
@ -815,15 +828,15 @@ void CliTests::testFileLayoutOptions()
const char *const args5[] = {"tageditor", "get", "-f", mp4File2.data(), nullptr};
TESTUTILS_ASSERT_EXEC(args5);
CPPUNIT_ASSERT(stdout.find("MP4/iTunes tag\n"
" Title You Shook Me All Night Long\n"
" Album Who Made Who\n"
" Artist ACDC\n"
" Genre Rock\n"
" Year 1986\n"
" Track 2/9\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);
CPPUNIT_ASSERT(stdout.find(" - \e[1mMP4/iTunes tag\e[0m\n"
" Title You Shook Me All Night Long\n"
" Album Who Made Who\n"
" Artist ACDC\n"
" Genre Rock\n"
" Year 1986\n"
" Track 2/9\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);
remove((mp4File2 + ".bak").data());
const char *const args6[] = {"tageditor", "set", "--index-pos", "front", "--force", "-f", mp4File2.data(), nullptr};