Improve CLI
* Use formatting * Use more consistent format * Show track summary
This commit is contained in:
parent
25d570d394
commit
5ffa9b7d2c
121
cli/helper.cpp
121
cli/helper.cpp
|
@ -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 ¬ifications, 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 ¬ification : notifications) {
|
for(const auto ¬ification : 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
18
cli/helper.h
18
cli/helper.h
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
213
tests/cli.cpp
213
tests/cli.cpp
|
@ -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};
|
||||||
|
|
Loading…
Reference in New Issue