Refactor CLI
This commit is contained in:
parent
a2c908a140
commit
2f39fd4b1c
|
@ -15,11 +15,15 @@ set(META_VERSION_PATCH 2)
|
|||
|
||||
# add project files
|
||||
set(HEADER_FILES
|
||||
cli/attachmentinfo.h
|
||||
cli/helper.h
|
||||
cli/mainfeatures.h
|
||||
application/knownfieldmodel.h
|
||||
)
|
||||
set(SRC_FILES
|
||||
application/main.cpp
|
||||
cli/attachmentinfo.cpp
|
||||
cli/helper.cpp
|
||||
cli/mainfeatures.cpp
|
||||
application/knownfieldmodel.cpp
|
||||
)
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
#include "./attachmentinfo.h"
|
||||
|
||||
#include <tagparser/abstractcontainer.h>
|
||||
#include <tagparser/abstractattachment.h>
|
||||
|
||||
#include <c++utilities/conversion/conversionexception.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace ConversionUtilities;
|
||||
using namespace Media;
|
||||
|
||||
namespace Cli {
|
||||
|
||||
void AttachmentInfo::parseDenotation(const char *denotation)
|
||||
{
|
||||
if(!strncmp(denotation, "id=", 3)) {
|
||||
try {
|
||||
id = stringToNumber<uint64, string>(denotation + 3);
|
||||
hasId = true;
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "The specified attachment ID \"" << (denotation + 3) << "\" is invalid.";
|
||||
}
|
||||
} else if(!strncmp(denotation, "path=", 5)) {
|
||||
path = denotation + 5;
|
||||
} else if(!strncmp(denotation, "name=", 5)) {
|
||||
name = denotation + 5;
|
||||
} else if(!strncmp(denotation, "mime=", 5)) {
|
||||
mime = denotation + 5;
|
||||
} else if(!strncmp(denotation, "desc=", 5)) {
|
||||
desc = denotation + 5;
|
||||
} else {
|
||||
cerr << "The attachment specification \"" << denotation << "\" is invalid and will be ignored.";
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::apply(AbstractContainer *container)
|
||||
{
|
||||
static const string context("applying specified attachments");
|
||||
AbstractAttachment *attachment = nullptr;
|
||||
bool attachmentFound = false;
|
||||
switch(action) {
|
||||
case AttachmentAction::Add:
|
||||
if(!path || !name) {
|
||||
cerr << "Argument --update-argument specified but no name/path provided." << endl;
|
||||
return;
|
||||
}
|
||||
apply(container->createAttachment());
|
||||
break;
|
||||
case AttachmentAction::Update:
|
||||
if(hasId) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->id() == id) {
|
||||
apply(attachment);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be updated.", context);
|
||||
}
|
||||
} else if(name) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->name() == name) {
|
||||
apply(attachment);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be updated.", context);
|
||||
}
|
||||
} else {
|
||||
cerr << "Argument --update-argument specified but no ID/name provided." << endl;
|
||||
}
|
||||
break;
|
||||
case AttachmentAction::Remove:
|
||||
if(hasId) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->id() == id) {
|
||||
attachment->setIgnored(true);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be removed.", context);
|
||||
}
|
||||
} else if(name) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->name() == name) {
|
||||
attachment->setIgnored(true);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be removed.", context);
|
||||
}
|
||||
} else {
|
||||
cerr << "Argument --remove-argument specified but no ID/name provided." << endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::apply(AbstractAttachment *attachment)
|
||||
{
|
||||
if(hasId) {
|
||||
attachment->setId(id);
|
||||
}
|
||||
if(path) {
|
||||
attachment->setFile(path);
|
||||
}
|
||||
if(name) {
|
||||
attachment->setName(name);
|
||||
}
|
||||
if(mime) {
|
||||
attachment->setMimeType(mime);
|
||||
}
|
||||
if(desc) {
|
||||
attachment->setDescription(desc);
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::reset()
|
||||
{
|
||||
action = AttachmentAction::Add;
|
||||
id = 0;
|
||||
hasId = false;
|
||||
path = name = mime = desc = nullptr;
|
||||
}
|
||||
|
||||
bool AttachmentInfo::next(AbstractContainer *container)
|
||||
{
|
||||
if(!id && !path && !name && !mime && !desc) {
|
||||
// skip empty attachment infos
|
||||
return false;
|
||||
}
|
||||
apply(container);
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
#ifndef CLI_ATTACHMENT_INFO
|
||||
#define CLI_ATTACHMENT_INFO
|
||||
|
||||
#include <c++utilities/conversion/types.h>
|
||||
|
||||
namespace Media {
|
||||
class AbstractContainer;
|
||||
class AbstractAttachment;
|
||||
}
|
||||
|
||||
namespace Cli {
|
||||
|
||||
enum class AttachmentAction {
|
||||
Add,
|
||||
Update,
|
||||
Remove
|
||||
};
|
||||
|
||||
class AttachmentInfo
|
||||
{
|
||||
public:
|
||||
AttachmentInfo();
|
||||
void parseDenotation(const char *denotation);
|
||||
void apply(Media::AbstractContainer *container);
|
||||
void apply(Media::AbstractAttachment *attachment);
|
||||
void reset();
|
||||
bool next(Media::AbstractContainer *container);
|
||||
|
||||
AttachmentAction action;
|
||||
uint64 id;
|
||||
bool hasId;
|
||||
const char *path;
|
||||
const char *name;
|
||||
const char *mime;
|
||||
const char *desc;
|
||||
};
|
||||
|
||||
inline AttachmentInfo::AttachmentInfo() :
|
||||
action(AttachmentAction::Add),
|
||||
id(0),
|
||||
hasId(false),
|
||||
path(nullptr),
|
||||
name(nullptr),
|
||||
mime(nullptr),
|
||||
desc(nullptr)
|
||||
{}
|
||||
|
||||
}
|
||||
|
||||
#endif // CLI_ATTACHMENT_INFO
|
|
@ -0,0 +1,407 @@
|
|||
#include "./helper.h"
|
||||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace ApplicationUtilities;
|
||||
using namespace ConversionUtilities;
|
||||
using namespace ChronoUtilities;
|
||||
using namespace Media;
|
||||
|
||||
namespace Cli {
|
||||
|
||||
string incremented(const string &str, unsigned int toIncrement)
|
||||
{
|
||||
string res;
|
||||
res.reserve(str.size());
|
||||
unsigned int value = 0;
|
||||
bool hasValue = false;
|
||||
for(const char &c : str) {
|
||||
if(toIncrement && c >= '0' && c <= '9') {
|
||||
value = value * 10 + static_cast<unsigned int>(c - '0');
|
||||
hasValue = true;
|
||||
} else {
|
||||
if(hasValue) {
|
||||
res += numberToString(value + 1);
|
||||
hasValue = false;
|
||||
--toIncrement;
|
||||
}
|
||||
res += c;
|
||||
}
|
||||
}
|
||||
if(hasValue) {
|
||||
res += numberToString(value + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void printNotifications(NotificationList ¬ifications, const char *head, bool beVerbose)
|
||||
{
|
||||
if(!beVerbose) {
|
||||
for(const auto ¬ification : notifications) {
|
||||
switch(notification.type()) {
|
||||
case NotificationType::Debug:
|
||||
case NotificationType::Information:
|
||||
break;
|
||||
default:
|
||||
goto printNotifications;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!notifications.empty()) {
|
||||
printNotifications:
|
||||
if(head) {
|
||||
cout << head << endl;
|
||||
}
|
||||
Notification::sortByTime(notifications);
|
||||
for(const auto ¬ification : notifications) {
|
||||
switch(notification.type()) {
|
||||
case NotificationType::Debug:
|
||||
if(beVerbose) {
|
||||
cout << "Debug ";
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
case NotificationType::Information:
|
||||
if(beVerbose) {
|
||||
cout << "Information ";
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
case NotificationType::Warning:
|
||||
cout << "Warning ";
|
||||
break;
|
||||
case NotificationType::Critical:
|
||||
cout << "Error ";
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
|
||||
cout << notification.context() << ": ";
|
||||
cout << notification.message() << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void printNotifications(const MediaFileInfo &fileInfo, const char *head, bool beVerbose)
|
||||
{
|
||||
NotificationList notifications;
|
||||
fileInfo.gatherRelatedNotifications(notifications);
|
||||
printNotifications(notifications, head, beVerbose);
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, const char *value, const char *suffix, Indentation indentation)
|
||||
{
|
||||
if(*value) {
|
||||
cout << indentation << propName << Indentation(30 - strlen(propName)) << value;
|
||||
if(suffix) {
|
||||
cout << ' ' << suffix;
|
||||
}
|
||||
cout << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix, Indentation indentation)
|
||||
{
|
||||
const char *pos;
|
||||
switch(elementPosition) {
|
||||
case ElementPosition::BeforeData:
|
||||
pos = "before data";
|
||||
break;
|
||||
case ElementPosition::AfterData:
|
||||
pos = "after data";
|
||||
break;
|
||||
case ElementPosition::Keep:
|
||||
pos = nullptr;
|
||||
}
|
||||
if(pos) {
|
||||
printProperty(propName, pos, suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage)
|
||||
{
|
||||
if(usageArg.isPresent()) {
|
||||
const auto &val = usageArg.values().front();
|
||||
if(!strcmp(val, "never")) {
|
||||
return TagUsage::Never;
|
||||
} else if(!strcmp(val, "keepexisting")) {
|
||||
return TagUsage::KeepExisting;
|
||||
} else if(!strcmp(val, "always")) {
|
||||
return TagUsage::Always;
|
||||
} else {
|
||||
cerr << "Warning: The specified tag usage \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultUsage;
|
||||
}
|
||||
|
||||
TagTextEncoding parseEncodingDenotation(const Argument &encodingArg, TagTextEncoding defaultEncoding)
|
||||
{
|
||||
if(encodingArg.isPresent()) {
|
||||
const auto &val = encodingArg.values().front();
|
||||
if(!strcmp(val, "utf8")) {
|
||||
return TagTextEncoding::Utf8;
|
||||
} else if(!strcmp(val, "latin1")) {
|
||||
return TagTextEncoding::Latin1;
|
||||
} else if(!strcmp(val, "utf16be")) {
|
||||
return TagTextEncoding::Utf16BigEndian;
|
||||
} else if(!strcmp(val, "utf16le")) {
|
||||
return TagTextEncoding::Utf16LittleEndian;
|
||||
} else if(!strcmp(val, "auto")) {
|
||||
} else {
|
||||
cerr << "Warning: The specified encoding \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultEncoding;
|
||||
}
|
||||
|
||||
ElementPosition parsePositionDenotation(const Argument &posArg, const Argument &valueArg, ElementPosition defaultPos)
|
||||
{
|
||||
if(posArg.isPresent()) {
|
||||
const char *val = valueArg.values(0).front();
|
||||
if(!strcmp(val, "front")) {
|
||||
return ElementPosition::BeforeData;
|
||||
} else if(!strcmp(val, "back")) {
|
||||
return ElementPosition::AfterData;
|
||||
} else if(!strcmp(val, "keep")) {
|
||||
return ElementPosition::Keep;
|
||||
} else {
|
||||
cerr << "Warning: The specified position \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultPos;
|
||||
}
|
||||
|
||||
uint64 parseUInt64(const Argument &arg, uint64 defaultValue)
|
||||
{
|
||||
if(arg.isPresent()) {
|
||||
try {
|
||||
if(*arg.values().front() == '0' && *(arg.values().front() + 1) == 'x') {
|
||||
return stringToNumber<uint64>(arg.values().front() + 2, 16);
|
||||
} else {
|
||||
return stringToNumber<uint64>(arg.values().front());
|
||||
}
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "Warning: The specified value \"" << arg.values().front() << "\" is no valid unsigned integer and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
TagTarget::IdContainerType parseIds(const std::string &concatenatedIds)
|
||||
{
|
||||
auto splittedIds = splitString(concatenatedIds, ",", EmptyPartsTreat::Omit);
|
||||
TagTarget::IdContainerType convertedIds;
|
||||
convertedIds.reserve(splittedIds.size());
|
||||
for(const auto &id : splittedIds) {
|
||||
try {
|
||||
convertedIds.push_back(stringToNumber<TagTarget::IdType>(id));
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "Warning: The specified ID \"" << id << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return convertedIds;
|
||||
}
|
||||
|
||||
bool applyTargetConfiguration(TagTarget &target, const std::string &configStr)
|
||||
{
|
||||
if(!configStr.empty()) {
|
||||
if(configStr.compare(0, 13, "target-level=") == 0) {
|
||||
try {
|
||||
target.setLevel(stringToNumber<uint64>(configStr.substr(13)));
|
||||
} catch (const ConversionException &) {
|
||||
cerr << "Warning: The specified target level \"" << configStr.substr(13) << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
} else if(configStr.compare(0, 17, "target-levelname=") == 0) {
|
||||
target.setLevelName(configStr.substr(17));
|
||||
} else if(configStr.compare(0, 14, "target-tracks=") == 0) {
|
||||
target.tracks() = parseIds(configStr.substr(14));
|
||||
} else if(configStr.compare(0, 16, "target-chapters=") == 0) {
|
||||
target.chapters() = parseIds(configStr.substr(16));
|
||||
} else if(configStr.compare(0, 16, "target-editions=") == 0) {
|
||||
target.editions() = parseIds(configStr.substr(16));
|
||||
} else if(configStr.compare(0, 19, "target-attachments=") == 0) {
|
||||
target.attachments() = parseIds(configStr.substr(17));
|
||||
} else if(configStr.compare(0, 13, "target-reset=") == 0) {
|
||||
if(*(configStr.data() + 13)) {
|
||||
cerr << "Warning: Invalid assignment " << (configStr.data() + 13) << " for target-reset will be ignored." << endl;
|
||||
}
|
||||
target.clear();
|
||||
} else if(configStr == "target-reset") {
|
||||
target.clear();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
|
||||
{
|
||||
FieldDenotations fields;
|
||||
if(fieldsArg.isPresent()) {
|
||||
const vector<const char *> &fieldDenotations = fieldsArg.values();
|
||||
FieldScope scope;
|
||||
for(const char *fieldDenotationString : fieldDenotations) {
|
||||
// check for tag or target specifier
|
||||
const auto fieldDenotationLen = strlen(fieldDenotationString);
|
||||
if(!strncmp(fieldDenotationString, "tag=", 4)) {
|
||||
if(fieldDenotationLen == 4) {
|
||||
cerr << "Warning: The \"tag\"-specifier has been used with no value(s) and hence is ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
|
||||
} else {
|
||||
TagType tagType = TagType::Unspecified;
|
||||
for(const auto &part : splitString(fieldDenotationString + 4, ",", EmptyPartsTreat::Omit)) {
|
||||
if(part == "id3v1") {
|
||||
tagType |= TagType::Id3v1Tag;
|
||||
} else if(part == "id3v2") {
|
||||
tagType |= TagType::Id3v2Tag;
|
||||
} else if(part == "id3") {
|
||||
tagType |= TagType::Id3v1Tag | TagType::Id3v2Tag;
|
||||
} else if(part == "itunes" || part == "mp4") {
|
||||
tagType |= TagType::Mp4Tag;
|
||||
} else if(part == "vorbis") {
|
||||
tagType |= TagType::VorbisComment;
|
||||
} else if(part == "matroska") {
|
||||
tagType |= TagType::MatroskaTag;
|
||||
} else if(part == "all" || part == "any") {
|
||||
tagType = TagType::Unspecified;
|
||||
break;
|
||||
} else {
|
||||
cerr << "Warning: The value provided with the \"tag\"-specifier is invalid and will be ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
|
||||
tagType = scope.tagType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
scope.tagType = tagType;
|
||||
break;
|
||||
}
|
||||
} else if(applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) {
|
||||
continue;
|
||||
}
|
||||
// check whether field name starts with + indicating an additional value
|
||||
bool additionalValue = *fieldDenotationString == '+';
|
||||
if(additionalValue) {
|
||||
++fieldDenotationString;
|
||||
}
|
||||
// read field name
|
||||
const auto equationPos = strchr(fieldDenotationString, '=');
|
||||
size_t fieldNameLen = equationPos ? static_cast<size_t>(equationPos - fieldDenotationString) : strlen(fieldDenotationString);
|
||||
// field name might denote increment ("+") or path disclosure (">")
|
||||
DenotationType type = DenotationType::Normal;
|
||||
if(fieldNameLen && equationPos) {
|
||||
switch(*(equationPos - 1)) {
|
||||
case '+':
|
||||
type = DenotationType::Increment;
|
||||
--fieldNameLen;
|
||||
break;
|
||||
case '>':
|
||||
type = DenotationType::File;
|
||||
--fieldNameLen;
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
}
|
||||
// field name might specify a file index
|
||||
unsigned int fileIndex = 0, mult = 1;
|
||||
for(const char *digitPos = fieldDenotationString + fieldNameLen - 1; fieldNameLen && isDigit(*digitPos); --fieldNameLen, --digitPos, mult *= 10) {
|
||||
fileIndex += static_cast<unsigned int>(*digitPos - '0') * mult;
|
||||
}
|
||||
if(!fieldNameLen) {
|
||||
cerr << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl;
|
||||
continue;
|
||||
}
|
||||
// parse the denoted filed
|
||||
if(!strncmp(fieldDenotationString, "title", fieldNameLen)) {
|
||||
scope.field = KnownField::Title;
|
||||
} else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) {
|
||||
scope.field = KnownField::Album;
|
||||
} else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) {
|
||||
scope.field = KnownField::Artist;
|
||||
} else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) {
|
||||
scope.field = KnownField::Genre;
|
||||
} else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) {
|
||||
scope.field = KnownField::Year;
|
||||
} else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) {
|
||||
scope.field = KnownField::Comment;
|
||||
} else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) {
|
||||
scope.field = KnownField::Bpm;
|
||||
} else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) {
|
||||
scope.field = KnownField::Bps;
|
||||
} else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyricist;
|
||||
} else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) {
|
||||
scope.field = KnownField::TrackPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) {
|
||||
scope.field = KnownField::DiskPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) {
|
||||
scope.field = KnownField::PartNumber;
|
||||
} else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) {
|
||||
scope.field = KnownField::TotalParts;
|
||||
} else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) {
|
||||
scope.field = KnownField::Encoder;
|
||||
} else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordDate;
|
||||
} else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) {
|
||||
scope.field = KnownField::Performers;
|
||||
} else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) {
|
||||
scope.field = KnownField::Length;
|
||||
} else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) {
|
||||
scope.field = KnownField::Language;
|
||||
} else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) {
|
||||
scope.field = KnownField::EncoderSettings;
|
||||
} else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::SynchronizedLyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) {
|
||||
scope.field = KnownField::Grouping;
|
||||
} else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordLabel;
|
||||
} else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) {
|
||||
scope.field = KnownField::Cover;
|
||||
type = DenotationType::File; // read cover always from file
|
||||
} else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) {
|
||||
scope.field = KnownField::Composer;
|
||||
} else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) {
|
||||
scope.field = KnownField::Rating;
|
||||
} else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) {
|
||||
scope.field = KnownField::Description;
|
||||
} else {
|
||||
// no "KnownField" value matching -> discard the field denotation
|
||||
cerr << "Warning: The field name \"" << string(fieldDenotationString, fieldNameLen) << "\" is unknown and will be ingored." << endl;
|
||||
continue;
|
||||
}
|
||||
// add field denotation scope
|
||||
auto &fieldValues = fields[scope];
|
||||
// add value to the scope (if present)
|
||||
if(equationPos) {
|
||||
if(readOnly) {
|
||||
cerr << "Warning: Specified value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
|
||||
} else {
|
||||
// file index might have been specified explicitely
|
||||
// if not (mult == 1) use the index of the last value and increase it by one if the value is not an additional one
|
||||
// if there are no previous values, just use the index 0
|
||||
fieldValues.emplace_back(FieldValue(type, mult == 1 ? (fieldValues.empty() ? 0 : fieldValues.back().fileIndex + (additionalValue ? 0 : 1)) : fileIndex, (equationPos + 1)));
|
||||
}
|
||||
}
|
||||
if(additionalValue && readOnly) {
|
||||
cerr << "Warning: Indication of an additional value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
#ifndef CLI_HELPER
|
||||
#define CLI_HELPER
|
||||
|
||||
#include <tagparser/tag.h>
|
||||
|
||||
#include <c++utilities/application/commandlineutils.h>
|
||||
#include <c++utilities/chrono/datetime.h>
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ApplicationUtilities {
|
||||
class Argument;
|
||||
}
|
||||
|
||||
namespace Media {
|
||||
class MediaFileInfo;
|
||||
enum class TagUsage;
|
||||
enum class ElementPosition;
|
||||
}
|
||||
|
||||
using namespace Media;
|
||||
|
||||
namespace Cli {
|
||||
|
||||
// define enums, operators and structs to handle specified field denotations
|
||||
|
||||
enum class DenotationType
|
||||
{
|
||||
Normal,
|
||||
Increment,
|
||||
File
|
||||
};
|
||||
|
||||
inline TagType operator| (TagType lhs, TagType rhs)
|
||||
{
|
||||
return static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
inline TagType operator& (TagType lhs, TagType rhs)
|
||||
{
|
||||
return static_cast<TagType>(static_cast<unsigned int>(lhs) & static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
inline TagType &operator|= (TagType &lhs, TagType rhs)
|
||||
{
|
||||
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
struct FieldScope
|
||||
{
|
||||
FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget());
|
||||
bool operator ==(const FieldScope &other) const;
|
||||
KnownField field;
|
||||
TagType tagType;
|
||||
TagTarget tagTarget;
|
||||
};
|
||||
|
||||
inline FieldScope::FieldScope(KnownField field, TagType tagType, TagTarget tagTarget) :
|
||||
field(field),
|
||||
tagType(tagType),
|
||||
tagTarget(tagTarget)
|
||||
{}
|
||||
|
||||
inline bool FieldScope::operator ==(const FieldScope &other) const
|
||||
{
|
||||
return field == other.field && tagType == other.tagType && tagTarget == other.tagTarget;
|
||||
}
|
||||
|
||||
struct FieldValue
|
||||
{
|
||||
FieldValue(DenotationType type, unsigned int fileIndex, const char *value);
|
||||
DenotationType type;
|
||||
unsigned int fileIndex;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
inline FieldValue::FieldValue(DenotationType type, unsigned int fileIndex, const char *value) :
|
||||
type(type),
|
||||
fileIndex(fileIndex),
|
||||
value(value)
|
||||
{}
|
||||
|
||||
}
|
||||
|
||||
// define hash functions for custom data types
|
||||
|
||||
namespace std {
|
||||
|
||||
using namespace Cli;
|
||||
|
||||
template <> struct hash<KnownField>
|
||||
{
|
||||
std::size_t operator()(const KnownField &ids) const
|
||||
{
|
||||
using type = typename std::underlying_type<KnownField>::type;
|
||||
return std::hash<type>()(static_cast<type>(ids));
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagType>
|
||||
{
|
||||
std::size_t operator()(const TagType &ids) const
|
||||
{
|
||||
using type = typename std::underlying_type<TagType>::type;
|
||||
return std::hash<type>()(static_cast<type>(ids));
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagTarget::IdContainerType>
|
||||
{
|
||||
std::size_t operator()(const TagTarget::IdContainerType &ids) const
|
||||
{
|
||||
using std::hash;
|
||||
auto seed = ids.size();
|
||||
for(auto id : ids) {
|
||||
seed ^= id + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagTarget>
|
||||
{
|
||||
std::size_t operator()(const TagTarget& target) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<uint64>()(target.level())
|
||||
^ (hash<TagTarget::IdContainerType>()(target.tracks()) << 1)) >> 1)
|
||||
^ (hash<TagTarget::IdContainerType>()(target.attachments()) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<FieldScope>
|
||||
{
|
||||
std::size_t operator()(const FieldScope& scope) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<KnownField>()(scope.field)
|
||||
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
|
||||
^ (hash<TagTarget>()(scope.tagTarget) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace Cli {
|
||||
|
||||
typedef std::vector<FieldValue> FieldValues;
|
||||
typedef std::unordered_map<FieldScope, FieldValues> FieldDenotations;
|
||||
|
||||
// declare/define actual helpers
|
||||
|
||||
constexpr bool isDigit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
std::string incremented(const std::string &str, unsigned int toIncrement = 1);
|
||||
|
||||
void printNotifications(NotificationList ¬ifications, const char *head = nullptr, bool beVerbose = false);
|
||||
void printNotifications(const MediaFileInfo &fileInfo, const char *head = nullptr, bool beVerbose = false);
|
||||
void printProperty(const char *propName, const char *value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 4);
|
||||
void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 4);
|
||||
|
||||
inline void printProperty(const char *propName, const std::string &value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 4)
|
||||
{
|
||||
printProperty(propName, value.data(), suffix, indentation);
|
||||
}
|
||||
|
||||
inline void printProperty(const char *propName, ChronoUtilities::TimeSpan timeSpan, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 4)
|
||||
{
|
||||
if(!timeSpan.isNull()) {
|
||||
printProperty(propName, timeSpan.toString(ChronoUtilities::TimeSpanOutputFormat::WithMeasures), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
inline void printProperty(const char *propName, ChronoUtilities::DateTime dateTime, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 4)
|
||||
{
|
||||
if(!dateTime.isNull()) {
|
||||
printProperty(propName, dateTime.toString(), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename intType>
|
||||
inline void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 4)
|
||||
{
|
||||
if(value != 0 || force) {
|
||||
printProperty(propName, ConversionUtilities::numberToString<intType>(value), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
TagUsage parseUsageDenotation(const ApplicationUtilities::Argument &usageArg, TagUsage defaultUsage);
|
||||
TagTextEncoding parseEncodingDenotation(const ApplicationUtilities::Argument &encodingArg, TagTextEncoding defaultEncoding);
|
||||
ElementPosition parsePositionDenotation(const ApplicationUtilities::Argument &posArg, const ApplicationUtilities::Argument &valueArg, ElementPosition defaultPos);
|
||||
uint64 parseUInt64(const ApplicationUtilities::Argument &arg, uint64 defaultValue);
|
||||
TagTarget::IdContainerType parseIds(const std::string &concatenatedIds);
|
||||
bool applyTargetConfiguration(TagTarget &target, const std::string &configStr);
|
||||
FieldDenotations parseFieldDenotations(const ApplicationUtilities::Argument &fieldsArg, bool readOnly);
|
||||
|
||||
}
|
||||
|
||||
#endif // CLI_HELPER
|
|
@ -1,4 +1,6 @@
|
|||
#include "./mainfeatures.h"
|
||||
#include "./helper.h"
|
||||
#include "./attachmentinfo.h"
|
||||
|
||||
#include "../application/knownfieldmodel.h"
|
||||
#if defined(GUI_QTWIDGETS) || defined(GUI_QTQUICK)
|
||||
|
@ -8,6 +10,7 @@
|
|||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
#include <tagparser/tag.h>
|
||||
#include <tagparser/tagvalue.h>
|
||||
#include <tagparser/abstracttrack.h>
|
||||
#include <tagparser/abstractattachment.h>
|
||||
#include <tagparser/abstractchapter.h>
|
||||
|
@ -29,8 +32,6 @@
|
|||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace ApplicationUtilities;
|
||||
|
@ -46,219 +47,6 @@ using namespace Utility;
|
|||
|
||||
namespace Cli {
|
||||
|
||||
// define enums, operators and structs to handle specified field denotations
|
||||
|
||||
enum class DenotationType
|
||||
{
|
||||
Normal,
|
||||
Increment,
|
||||
File
|
||||
};
|
||||
|
||||
inline TagType operator| (TagType lhs, TagType rhs)
|
||||
{
|
||||
return static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
inline TagType operator& (TagType lhs, TagType rhs)
|
||||
{
|
||||
return static_cast<TagType>(static_cast<unsigned int>(lhs) & static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
inline TagType &operator|= (TagType &lhs, TagType rhs)
|
||||
{
|
||||
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
|
||||
}
|
||||
|
||||
struct FieldScope
|
||||
{
|
||||
FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget());
|
||||
bool operator ==(const FieldScope &other) const;
|
||||
KnownField field;
|
||||
TagType tagType;
|
||||
TagTarget tagTarget;
|
||||
};
|
||||
|
||||
FieldScope::FieldScope(KnownField field, TagType tagType, TagTarget tagTarget) :
|
||||
field(field),
|
||||
tagType(tagType),
|
||||
tagTarget(tagTarget)
|
||||
{}
|
||||
|
||||
bool FieldScope::operator ==(const FieldScope &other) const
|
||||
{
|
||||
return field == other.field && tagType == other.tagType && tagTarget == other.tagTarget;
|
||||
}
|
||||
|
||||
struct FieldValue
|
||||
{
|
||||
FieldValue(DenotationType type, unsigned int fileIndex, const char *value);
|
||||
DenotationType type;
|
||||
unsigned int fileIndex;
|
||||
string value;
|
||||
};
|
||||
|
||||
inline FieldValue::FieldValue(DenotationType type, unsigned int fileIndex, const char *value) :
|
||||
type(type),
|
||||
fileIndex(fileIndex),
|
||||
value(value)
|
||||
{}
|
||||
|
||||
}
|
||||
|
||||
namespace std {
|
||||
|
||||
using namespace Cli;
|
||||
|
||||
template <> struct hash<KnownField>
|
||||
{
|
||||
std::size_t operator()(const KnownField &ids) const
|
||||
{
|
||||
using type = typename std::underlying_type<KnownField>::type;
|
||||
return std::hash<type>()(static_cast<type>(ids));
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagType>
|
||||
{
|
||||
std::size_t operator()(const TagType &ids) const
|
||||
{
|
||||
using type = typename std::underlying_type<TagType>::type;
|
||||
return std::hash<type>()(static_cast<type>(ids));
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagTarget::IdContainerType>
|
||||
{
|
||||
std::size_t operator()(const TagTarget::IdContainerType &ids) const
|
||||
{
|
||||
using std::hash;
|
||||
auto seed = ids.size();
|
||||
for(auto id : ids) {
|
||||
seed ^= id + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<TagTarget>
|
||||
{
|
||||
std::size_t operator()(const TagTarget& target) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<uint64>()(target.level())
|
||||
^ (hash<TagTarget::IdContainerType>()(target.tracks()) << 1)) >> 1)
|
||||
^ (hash<TagTarget::IdContainerType>()(target.attachments()) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<FieldScope>
|
||||
{
|
||||
std::size_t operator()(const FieldScope& scope) const
|
||||
{
|
||||
using std::hash;
|
||||
return ((hash<KnownField>()(scope.field)
|
||||
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
|
||||
^ (hash<TagTarget>()(scope.tagTarget) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace Cli {
|
||||
|
||||
typedef vector<FieldValue> FieldValues;
|
||||
typedef unordered_map<FieldScope, FieldValues> FieldDenotations;
|
||||
|
||||
constexpr bool isDigit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
string incremented(const string &str, unsigned int toIncrement = 1)
|
||||
{
|
||||
string res;
|
||||
res.reserve(str.size());
|
||||
unsigned int value = 0;
|
||||
bool hasValue = false;
|
||||
for(const char &c : str) {
|
||||
if(toIncrement && c >= '0' && c <= '9') {
|
||||
value = value * 10 + static_cast<unsigned int>(c - '0');
|
||||
hasValue = true;
|
||||
} else {
|
||||
if(hasValue) {
|
||||
res += numberToString(value + 1);
|
||||
hasValue = false;
|
||||
--toIncrement;
|
||||
}
|
||||
res += c;
|
||||
}
|
||||
}
|
||||
if(hasValue) {
|
||||
res += numberToString(value + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void printNotifications(NotificationList ¬ifications, const char *head = nullptr, bool beVerbose = false)
|
||||
{
|
||||
if(!beVerbose) {
|
||||
for(const auto ¬ification : notifications) {
|
||||
switch(notification.type()) {
|
||||
case NotificationType::Debug:
|
||||
case NotificationType::Information:
|
||||
break;
|
||||
default:
|
||||
goto printNotifications;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!notifications.empty()) {
|
||||
printNotifications:
|
||||
if(head) {
|
||||
cout << head << endl;
|
||||
}
|
||||
Notification::sortByTime(notifications);
|
||||
for(const auto ¬ification : notifications) {
|
||||
switch(notification.type()) {
|
||||
case NotificationType::Debug:
|
||||
if(beVerbose) {
|
||||
cout << "Debug ";
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
case NotificationType::Information:
|
||||
if(beVerbose) {
|
||||
cout << "Information ";
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
case NotificationType::Warning:
|
||||
cout << "Warning ";
|
||||
break;
|
||||
case NotificationType::Critical:
|
||||
cout << "Error ";
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
|
||||
cout << notification.context() << ": ";
|
||||
cout << notification.message() << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void printNotifications(const MediaFileInfo &fileInfo, const char *head = nullptr, bool beVerbose = false)
|
||||
{
|
||||
NotificationList notifications;
|
||||
fileInfo.gatherRelatedNotifications(notifications);
|
||||
printNotifications(notifications, head, beVerbose);
|
||||
}
|
||||
|
||||
#define FIELD_NAMES "title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
|
||||
"recorddate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" \
|
||||
"recordlabel cover composer rating description"
|
||||
|
@ -279,448 +67,6 @@ void printFieldNames(const ArgumentOccurrence &occurrence)
|
|||
cout << "\nTarget modifier: " << TARGET_MODIFIER << endl;
|
||||
}
|
||||
|
||||
TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage)
|
||||
{
|
||||
if(usageArg.isPresent()) {
|
||||
const auto &val = usageArg.values().front();
|
||||
if(!strcmp(val, "never")) {
|
||||
return TagUsage::Never;
|
||||
} else if(!strcmp(val, "keepexisting")) {
|
||||
return TagUsage::KeepExisting;
|
||||
} else if(!strcmp(val, "always")) {
|
||||
return TagUsage::Always;
|
||||
} else {
|
||||
cerr << "Warning: The specified tag usage \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultUsage;
|
||||
}
|
||||
|
||||
TagTextEncoding parseEncodingDenotation(const Argument &encodingArg, TagTextEncoding defaultEncoding)
|
||||
{
|
||||
if(encodingArg.isPresent()) {
|
||||
const auto &val = encodingArg.values().front();
|
||||
if(!strcmp(val, "utf8")) {
|
||||
return TagTextEncoding::Utf8;
|
||||
} else if(!strcmp(val, "latin1")) {
|
||||
return TagTextEncoding::Latin1;
|
||||
} else if(!strcmp(val, "utf16be")) {
|
||||
return TagTextEncoding::Utf16BigEndian;
|
||||
} else if(!strcmp(val, "utf16le")) {
|
||||
return TagTextEncoding::Utf16LittleEndian;
|
||||
} else if(!strcmp(val, "auto")) {
|
||||
} else {
|
||||
cerr << "Warning: The specified encoding \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultEncoding;
|
||||
}
|
||||
|
||||
ElementPosition parsePositionDenotation(const Argument &posArg, const Argument &valueArg, ElementPosition defaultPos)
|
||||
{
|
||||
if(posArg.isPresent()) {
|
||||
const char *val = valueArg.values(0).front();
|
||||
if(!strcmp(val, "front")) {
|
||||
return ElementPosition::BeforeData;
|
||||
} else if(!strcmp(val, "back")) {
|
||||
return ElementPosition::AfterData;
|
||||
} else if(!strcmp(val, "keep")) {
|
||||
return ElementPosition::Keep;
|
||||
} else {
|
||||
cerr << "Warning: The specified position \"" << val << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultPos;
|
||||
}
|
||||
|
||||
uint64 parseUInt64(const Argument &arg, uint64 defaultValue)
|
||||
{
|
||||
if(arg.isPresent()) {
|
||||
try {
|
||||
if(*arg.values().front() == '0' && *(arg.values().front() + 1) == 'x') {
|
||||
return stringToNumber<uint64>(arg.values().front() + 2, 16);
|
||||
} else {
|
||||
return stringToNumber<uint64>(arg.values().front());
|
||||
}
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "Warning: The specified value \"" << arg.values().front() << "\" is no valid unsigned integer and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
TagTarget::IdContainerType parseIds(const std::string &concatenatedIds)
|
||||
{
|
||||
auto splittedIds = splitString(concatenatedIds, ",", EmptyPartsTreat::Omit);
|
||||
TagTarget::IdContainerType convertedIds;
|
||||
convertedIds.reserve(splittedIds.size());
|
||||
for(const auto &id : splittedIds) {
|
||||
try {
|
||||
convertedIds.push_back(stringToNumber<TagTarget::IdType>(id));
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "Warning: The specified ID \"" << id << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
return convertedIds;
|
||||
}
|
||||
|
||||
bool applyTargetConfiguration(TagTarget &target, const std::string &configStr)
|
||||
{
|
||||
if(!configStr.empty()) {
|
||||
if(configStr.compare(0, 13, "target-level=") == 0) {
|
||||
try {
|
||||
target.setLevel(stringToNumber<uint64>(configStr.substr(13)));
|
||||
} catch (const ConversionException &) {
|
||||
cerr << "Warning: The specified target level \"" << configStr.substr(13) << "\" is invalid and will be ignored." << endl;
|
||||
}
|
||||
} else if(configStr.compare(0, 17, "target-levelname=") == 0) {
|
||||
target.setLevelName(configStr.substr(17));
|
||||
} else if(configStr.compare(0, 14, "target-tracks=") == 0) {
|
||||
target.tracks() = parseIds(configStr.substr(14));
|
||||
} else if(configStr.compare(0, 16, "target-chapters=") == 0) {
|
||||
target.chapters() = parseIds(configStr.substr(16));
|
||||
} else if(configStr.compare(0, 16, "target-editions=") == 0) {
|
||||
target.editions() = parseIds(configStr.substr(16));
|
||||
} else if(configStr.compare(0, 19, "target-attachments=") == 0) {
|
||||
target.attachments() = parseIds(configStr.substr(17));
|
||||
} else if(configStr.compare(0, 13, "target-reset=") == 0) {
|
||||
if(*(configStr.data() + 13)) {
|
||||
cerr << "Warning: Invalid assignment " << (configStr.data() + 13) << " for target-reset will be ignored." << endl;
|
||||
}
|
||||
target.clear();
|
||||
} else if(configStr == "target-reset") {
|
||||
target.clear();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
|
||||
{
|
||||
FieldDenotations fields;
|
||||
if(fieldsArg.isPresent()) {
|
||||
const vector<const char *> &fieldDenotations = fieldsArg.values();
|
||||
FieldScope scope;
|
||||
for(const char *fieldDenotationString : fieldDenotations) {
|
||||
// check for tag or target specifier
|
||||
const auto fieldDenotationLen = strlen(fieldDenotationString);
|
||||
if(!strncmp(fieldDenotationString, "tag=", 4)) {
|
||||
if(fieldDenotationLen == 4) {
|
||||
cerr << "Warning: The \"tag\"-specifier has been used with no value(s) and hence is ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
|
||||
} else {
|
||||
TagType tagType = TagType::Unspecified;
|
||||
for(const auto &part : splitString(fieldDenotationString + 4, ",", EmptyPartsTreat::Omit)) {
|
||||
if(part == "id3v1") {
|
||||
tagType |= TagType::Id3v1Tag;
|
||||
} else if(part == "id3v2") {
|
||||
tagType |= TagType::Id3v2Tag;
|
||||
} else if(part == "id3") {
|
||||
tagType |= TagType::Id3v1Tag | TagType::Id3v2Tag;
|
||||
} else if(part == "itunes" || part == "mp4") {
|
||||
tagType |= TagType::Mp4Tag;
|
||||
} else if(part == "vorbis") {
|
||||
tagType |= TagType::VorbisComment;
|
||||
} else if(part == "matroska") {
|
||||
tagType |= TagType::MatroskaTag;
|
||||
} else if(part == "all" || part == "any") {
|
||||
tagType = TagType::Unspecified;
|
||||
break;
|
||||
} else {
|
||||
cerr << "Warning: The value provided with the \"tag\"-specifier is invalid and will be ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
|
||||
tagType = scope.tagType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
scope.tagType = tagType;
|
||||
break;
|
||||
}
|
||||
} else if(applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) {
|
||||
continue;
|
||||
}
|
||||
// check whether field name starts with + indicating an additional value
|
||||
bool additionalValue = *fieldDenotationString == '+';
|
||||
if(additionalValue) {
|
||||
++fieldDenotationString;
|
||||
}
|
||||
// read field name
|
||||
const auto equationPos = strchr(fieldDenotationString, '=');
|
||||
size_t fieldNameLen = equationPos ? static_cast<size_t>(equationPos - fieldDenotationString) : strlen(fieldDenotationString);
|
||||
// field name might denote increment ("+") or path disclosure (">")
|
||||
DenotationType type = DenotationType::Normal;
|
||||
if(fieldNameLen && equationPos) {
|
||||
switch(*(equationPos - 1)) {
|
||||
case '+':
|
||||
type = DenotationType::Increment;
|
||||
--fieldNameLen;
|
||||
break;
|
||||
case '>':
|
||||
type = DenotationType::File;
|
||||
--fieldNameLen;
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
}
|
||||
// field name might specify a file index
|
||||
unsigned int fileIndex = 0, mult = 1;
|
||||
for(const char *digitPos = fieldDenotationString + fieldNameLen - 1; fieldNameLen && isDigit(*digitPos); --fieldNameLen, --digitPos, mult *= 10) {
|
||||
fileIndex += static_cast<unsigned int>(*digitPos - '0') * mult;
|
||||
}
|
||||
if(!fieldNameLen) {
|
||||
cerr << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl;
|
||||
continue;
|
||||
}
|
||||
// parse the denoted filed
|
||||
if(!strncmp(fieldDenotationString, "title", fieldNameLen)) {
|
||||
scope.field = KnownField::Title;
|
||||
} else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) {
|
||||
scope.field = KnownField::Album;
|
||||
} else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) {
|
||||
scope.field = KnownField::Artist;
|
||||
} else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) {
|
||||
scope.field = KnownField::Genre;
|
||||
} else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) {
|
||||
scope.field = KnownField::Year;
|
||||
} else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) {
|
||||
scope.field = KnownField::Comment;
|
||||
} else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) {
|
||||
scope.field = KnownField::Bpm;
|
||||
} else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) {
|
||||
scope.field = KnownField::Bps;
|
||||
} else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyricist;
|
||||
} else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) {
|
||||
scope.field = KnownField::TrackPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) {
|
||||
scope.field = KnownField::DiskPosition;
|
||||
} else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) {
|
||||
scope.field = KnownField::PartNumber;
|
||||
} else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) {
|
||||
scope.field = KnownField::TotalParts;
|
||||
} else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) {
|
||||
scope.field = KnownField::Encoder;
|
||||
} else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordDate;
|
||||
} else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) {
|
||||
scope.field = KnownField::Performers;
|
||||
} else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) {
|
||||
scope.field = KnownField::Length;
|
||||
} else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) {
|
||||
scope.field = KnownField::Language;
|
||||
} else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) {
|
||||
scope.field = KnownField::EncoderSettings;
|
||||
} else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::Lyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) {
|
||||
scope.field = KnownField::SynchronizedLyrics;
|
||||
} else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) {
|
||||
scope.field = KnownField::Grouping;
|
||||
} else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) {
|
||||
scope.field = KnownField::RecordLabel;
|
||||
} else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) {
|
||||
scope.field = KnownField::Cover;
|
||||
type = DenotationType::File; // read cover always from file
|
||||
} else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) {
|
||||
scope.field = KnownField::Composer;
|
||||
} else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) {
|
||||
scope.field = KnownField::Rating;
|
||||
} else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) {
|
||||
scope.field = KnownField::Description;
|
||||
} else {
|
||||
// no "KnownField" value matching -> discard the field denotation
|
||||
cerr << "Warning: The field name \"" << string(fieldDenotationString, fieldNameLen) << "\" is unknown and will be ingored." << endl;
|
||||
continue;
|
||||
}
|
||||
// add field denotation scope
|
||||
auto &fieldValues = fields[scope];
|
||||
// add value to the scope (if present)
|
||||
if(equationPos) {
|
||||
if(readOnly) {
|
||||
cerr << "Warning: Specified value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
|
||||
} else {
|
||||
// file index might have been specified explicitely
|
||||
// if not (mult == 1) use the index of the last value and increase it by one if the value is not an additional one
|
||||
// if there are no previous values, just use the index 0
|
||||
fieldValues.emplace_back(FieldValue(type, mult == 1 ? (fieldValues.empty() ? 0 : fieldValues.back().fileIndex + (additionalValue ? 0 : 1)) : fileIndex, (equationPos + 1)));
|
||||
}
|
||||
}
|
||||
if(additionalValue && readOnly) {
|
||||
cerr << "Warning: Indication of an additional value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
enum class AttachmentAction {
|
||||
Add,
|
||||
Update,
|
||||
Remove
|
||||
};
|
||||
|
||||
class AttachmentInfo
|
||||
{
|
||||
public:
|
||||
AttachmentInfo();
|
||||
void parseDenotation(const char *denotation);
|
||||
void apply(AbstractContainer *container);
|
||||
void apply(AbstractAttachment *attachment);
|
||||
void reset();
|
||||
bool next(AbstractContainer *container);
|
||||
|
||||
AttachmentAction action;
|
||||
uint64 id;
|
||||
bool hasId;
|
||||
const char *path;
|
||||
const char *name;
|
||||
const char *mime;
|
||||
const char *desc;
|
||||
};
|
||||
|
||||
AttachmentInfo::AttachmentInfo() :
|
||||
action(AttachmentAction::Add),
|
||||
id(0),
|
||||
hasId(false),
|
||||
path(nullptr),
|
||||
name(nullptr),
|
||||
mime(nullptr),
|
||||
desc(nullptr)
|
||||
{}
|
||||
|
||||
void AttachmentInfo::parseDenotation(const char *denotation)
|
||||
{
|
||||
if(!strncmp(denotation, "id=", 3)) {
|
||||
try {
|
||||
id = stringToNumber<uint64, string>(denotation + 3);
|
||||
hasId = true;
|
||||
} catch(const ConversionException &) {
|
||||
cerr << "The specified attachment ID \"" << (denotation + 3) << "\" is invalid.";
|
||||
}
|
||||
} else if(!strncmp(denotation, "path=", 5)) {
|
||||
path = denotation + 5;
|
||||
} else if(!strncmp(denotation, "name=", 5)) {
|
||||
name = denotation + 5;
|
||||
} else if(!strncmp(denotation, "mime=", 5)) {
|
||||
mime = denotation + 5;
|
||||
} else if(!strncmp(denotation, "desc=", 5)) {
|
||||
desc = denotation + 5;
|
||||
} else {
|
||||
cerr << "The attachment specification \"" << denotation << "\" is invalid and will be ignored.";
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::apply(AbstractContainer *container)
|
||||
{
|
||||
static const string context("applying specified attachments");
|
||||
AbstractAttachment *attachment = nullptr;
|
||||
bool attachmentFound = false;
|
||||
switch(action) {
|
||||
case AttachmentAction::Add:
|
||||
if(!path || !name) {
|
||||
cerr << "Argument --update-argument specified but no name/path provided." << endl;
|
||||
return;
|
||||
}
|
||||
apply(container->createAttachment());
|
||||
break;
|
||||
case AttachmentAction::Update:
|
||||
if(hasId) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->id() == id) {
|
||||
apply(attachment);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be updated.", context);
|
||||
}
|
||||
} else if(name) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->name() == name) {
|
||||
apply(attachment);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be updated.", context);
|
||||
}
|
||||
} else {
|
||||
cerr << "Argument --update-argument specified but no ID/name provided." << endl;
|
||||
}
|
||||
break;
|
||||
case AttachmentAction::Remove:
|
||||
if(hasId) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->id() == id) {
|
||||
attachment->setIgnored(true);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be removed.", context);
|
||||
}
|
||||
} else if(name) {
|
||||
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
|
||||
attachment = container->attachment(i);
|
||||
if(attachment->name() == name) {
|
||||
attachment->setIgnored(true);
|
||||
attachmentFound = true;
|
||||
}
|
||||
}
|
||||
if(!attachmentFound) {
|
||||
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be removed.", context);
|
||||
}
|
||||
} else {
|
||||
cerr << "Argument --remove-argument specified but no ID/name provided." << endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::apply(AbstractAttachment *attachment)
|
||||
{
|
||||
if(hasId) {
|
||||
attachment->setId(id);
|
||||
}
|
||||
if(path) {
|
||||
attachment->setFile(path);
|
||||
}
|
||||
if(name) {
|
||||
attachment->setName(name);
|
||||
}
|
||||
if(mime) {
|
||||
attachment->setMimeType(mime);
|
||||
}
|
||||
if(desc) {
|
||||
attachment->setDescription(desc);
|
||||
}
|
||||
}
|
||||
|
||||
void AttachmentInfo::reset()
|
||||
{
|
||||
action = AttachmentAction::Add;
|
||||
id = 0;
|
||||
hasId = false;
|
||||
path = name = mime = desc = nullptr;
|
||||
}
|
||||
|
||||
bool AttachmentInfo::next(AbstractContainer *container)
|
||||
{
|
||||
if(!id && !path && !name && !mime && !desc) {
|
||||
// skip empty attachment infos
|
||||
return false;
|
||||
}
|
||||
apply(container);
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg)
|
||||
{
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
|
@ -757,62 +103,6 @@ void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg,
|
|||
#endif
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, const char *value, const char *suffix = nullptr, Indentation indentation = 4)
|
||||
{
|
||||
if(*value) {
|
||||
std::cout << indentation << propName << Indentation(30 - strlen(propName)) << value;
|
||||
if(suffix) {
|
||||
std::cout << ' ' << suffix;
|
||||
}
|
||||
std::cout << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, const string &value, const char *suffix = nullptr, Indentation indentation = 4)
|
||||
{
|
||||
printProperty(propName, value.data(), suffix, indentation);
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, TimeSpan timeSpan, const char *suffix = nullptr, Indentation indentation = 4)
|
||||
{
|
||||
if(!timeSpan.isNull()) {
|
||||
printProperty(propName, timeSpan.toString(TimeSpanOutputFormat::WithMeasures), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, DateTime dateTime, const char *suffix = nullptr, Indentation indentation = 4)
|
||||
{
|
||||
if(!dateTime.isNull()) {
|
||||
printProperty(propName, dateTime.toString(), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix = nullptr, Indentation indentation = 4)
|
||||
{
|
||||
const char *pos;
|
||||
switch(elementPosition) {
|
||||
case ElementPosition::BeforeData:
|
||||
pos = "before data";
|
||||
break;
|
||||
case ElementPosition::AfterData:
|
||||
pos = "after data";
|
||||
break;
|
||||
case ElementPosition::Keep:
|
||||
pos = nullptr;
|
||||
}
|
||||
if(pos) {
|
||||
printProperty(propName, pos, suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename intType>
|
||||
void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, Indentation indentation = 4)
|
||||
{
|
||||
if(value != 0 || force) {
|
||||
printProperty(propName, numberToString<intType>(value), suffix, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg)
|
||||
{
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
#ifndef MAINFEATURES_H
|
||||
#define MAINFEATURES_H
|
||||
|
||||
#include <tagparser/tagvalue.h>
|
||||
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ApplicationUtilities {
|
||||
class Argument;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue