Improve handling of attachments via CLI

This commit is contained in:
Martchus 2016-07-31 23:22:22 +02:00
parent 6d8833fe9d
commit 1a20eea65b
6 changed files with 263 additions and 154 deletions

View File

@ -8,13 +8,12 @@ set(META_APP_CATEGORIES "Utility;Audio;Video;")
set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "A tageditor with Qt GUI and command line interface. Supports MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska")
set(META_VERSION_MAJOR 1)
set(META_VERSION_MINOR 4)
set(META_VERSION_PATCH 1)
set(META_VERSION_MAJOR 2)
set(META_VERSION_MINOR 0)
set(META_VERSION_PATCH 0)
# add project files
set(HEADER_FILES
application/main.h
cli/mainfeatures.h
application/knownfieldmodel.h
)

View File

@ -1,5 +1,3 @@
#include "./main.h"
#include "../cli/mainfeatures.h"
#if defined(GUI_QTWIDGETS)
# include "../gui/initiate.h"
@ -31,16 +29,18 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg) :
filesArg(filesArg),
verboseArg(verboseArg),
docTitleArg("doc-title", 'd', "specifies the document title (has no affect if not supported by the container)"),
removeOtherFieldsArg("remove-other-fields", '\0', "if present ALL unspecified tag fields will be removed (to remove a specific field use eg. \"album=\")"),
removeOtherFieldsArg("remove-other-fields", '\0', "removes ALL fields where no value has been provided for (to remove a specific field use eg. \"album=\")"),
treatUnknownFilesAsMp3FilesArg("treat-unknown-as-mp3", '\0', "if present unknown files will be treatet as MP3 files"),
id3v1UsageArg("id3v1-usage", '\0', "specifies the ID3v1 usage (only used when already present by default); only relevant when dealing with MP3 files (or files treated as such)"),
id3v2UsageArg("id3v2-usage", '\0', "specifies the ID3v2 usage (always used by default); only relevant when dealing with MP3 files (or files treated as such)"),
mergeMultipleSuccessiveTagsArg("merge-successive-tags", '\0', "if present multiple successive ID3v2 tags will be merged"),
id3v2VersionArg("id3v2-version", '\0', "forces a specific ID3v2 version to be used; only relevant when ID3v2 is used"),
encodingArg("encoding", '\0', "specifies the preferred encoding"),
removeTargetsArg("remove-targets", '\0', "removes all tags with the specified targets (which must be separated by \",\")"),
attachmentsArg("attachments", '\0', "specifies attachments to be added/updated/removed (multiple attachments must be separated by \",\""),
removeExistingAttachmentsArg("remove-existing-attachments", 'r', "specifies names/IDs of existing attachments to be removed"),
removeTargetArg("remove-target", '\0', "removes all tags with the specified target"),
addAttachmentArg("add-attachment", '\0', "adds a new attachment"),
updateAttachmentArg("update-attachment", '\0', "updates an existing attachment"),
removeAttachmentArg("remove-attachment", '\0', "removes an existing attachment"),
removeExistingAttachmentsArg("remove-existing-attachments", 'r', "removes ALL existing attachments (to remove a specific attachment use --remove-attachment)"),
minPaddingArg("min-padding", '\0', "specifies the minimum padding before the media data"),
maxPaddingArg("max-padding", '\0', "specifies the maximum padding before the media data"),
prefPaddingArg("preferred-padding", '\0', "specifies the preferred padding before the media data"),
@ -52,7 +52,7 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg) :
indexPosArg("index-pos", '\0', "specifies the preferred index position"),
forceRewriteArg("force-rewrite", '\0', "forces the file to rewritten from the scratch"),
valuesArg("values", 'n', "specifies the values to be set"),
setTagInfoArg("set", 's', "sets the values of all specified tag fields")
setTagInfoArg("set", 's', "sets the specified tag information and attachments")
{
docTitleArg.setCombinable(true);
docTitleArg.setRequiredValueCount(-1);
@ -75,12 +75,28 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg) :
encodingArg.setValueNames({"latin1/utf8/utf16le/utf16be"});
encodingArg.setPreDefinedCompletionValues("latin1 utf8 utf16le utf16be");
encodingArg.setCombinable(true);
removeTargetsArg.setRequiredValueCount(-1);
removeTargetsArg.setValueNames({});
removeTargetsArg.setCombinable(true);
attachmentsArg.setRequiredValueCount(-1);
attachmentsArg.setValueNames({"path=some/file", "name=Some name", "desc=Some desc", "mime=mime/type", ",", "path=another/file"});
attachmentsArg.setCombinable(true);
removeTargetArg.setRequiredValueCount(-1);
removeTargetArg.setValueNames({});
removeTargetArg.setCombinable(true);
removeTargetArg.setConstraints(0, -1);
addAttachmentArg.setRequiredValueCount(-1);
addAttachmentArg.setValueNames({"path=some/file", "name=Some name", "desc=Some desc", "mime=mime/type"});
addAttachmentArg.setCombinable(true);
addAttachmentArg.setConstraints(0, -1);
addAttachmentArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
addAttachmentArg.setPreDefinedCompletionValues("name id path desc mime");
updateAttachmentArg.setRequiredValueCount(-1);
updateAttachmentArg.setValueNames({"path=some/file", "name=Some name", "desc=Some desc", "mime=mime/type"});
updateAttachmentArg.setCombinable(true);
updateAttachmentArg.setConstraints(0, -1);
updateAttachmentArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
updateAttachmentArg.setPreDefinedCompletionValues("name id path desc mime");
removeAttachmentArg.setRequiredValueCount(1);
removeAttachmentArg.setValueNames({"name=to_remove"});
removeAttachmentArg.setCombinable(true);
removeAttachmentArg.setConstraints(0, -1);
removeAttachmentArg.setPreDefinedCompletionValues("name id");
removeAttachmentArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
removeExistingAttachmentsArg.setCombinable(true);
minPaddingArg.setRequiredValueCount(1);
minPaddingArg.setValueNames({"min padding in byte"});
@ -109,12 +125,12 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg) :
valuesArg.setValueNames({"title=foo", "album=bar", "cover=/path/to/file"});
valuesArg.setRequiredValueCount(-1);
valuesArg.setImplicit(true);
valuesArg.setPreDefinedCompletionValues(Cli::fieldNames);
valuesArg.setPreDefinedCompletionValues(Cli::fieldNamesForSet);
valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
setTagInfoArg.setDenotesOperation(true);
setTagInfoArg.setCallback(std::bind(Cli::setTagInfo, std::cref(*this)));
setTagInfoArg.setSubArguments({&valuesArg, &filesArg, &docTitleArg, &removeOtherFieldsArg, &treatUnknownFilesAsMp3FilesArg, &id3v1UsageArg, &id3v2UsageArg,
&mergeMultipleSuccessiveTagsArg, &id3v2VersionArg, &encodingArg, &removeTargetsArg, &attachmentsArg,
&mergeMultipleSuccessiveTagsArg, &id3v2VersionArg, &encodingArg, &removeTargetArg, &addAttachmentArg, &updateAttachmentArg, &removeAttachmentArg,
&removeExistingAttachmentsArg, &minPaddingArg, &maxPaddingArg, &prefPaddingArg, &tagPosArg,
&indexPosArg, &forceRewriteArg, &verboseArg});
}
@ -147,13 +163,12 @@ int main(int argc, char *argv[])
Argument outputFileArg("output-file", 'o', "specifies the path of the output file");
outputFileArg.setValueNames({"path"});
outputFileArg.setRequiredValueCount(1);
outputFileArg.setRequired(true);
outputFileArg.setCombinable(true);
// print field names
Argument printFieldNamesArg("print-field-names", '\0', "prints available field names");
printFieldNamesArg.setCallback(Cli::printFieldNames);
// display general file info
Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
Argument displayFileInfoArg("info", 'i', "displays general file information");
displayFileInfoArg.setDenotesOperation(true);
displayFileInfoArg.setCallback(std::bind(Cli::displayFileInfo, _1, std::cref(filesArg), std::cref(verboseArg)));
displayFileInfoArg.setSubArguments({&filesArg, &verboseArg});
@ -174,7 +189,7 @@ int main(int argc, char *argv[])
fieldArg.setValueNames({"field name"});
fieldArg.setRequiredValueCount(1);
fieldArg.setImplicit(true);
Argument extractFieldArg("extract", 'e', "extracts the specified field from the specified file");
Argument extractFieldArg("extract", 'e', "saves the value of the specified field (eg. cover or other binary field) to the specified file or writes it to stdout if no output file has been specified");
extractFieldArg.setSubArguments({&fieldArg, &fileArg, &outputFileArg, &verboseArg});
extractFieldArg.setDenotesOperation(true);
extractFieldArg.setCallback(std::bind(Cli::extractField, std::cref(fieldsArg), std::cref(fileArg), std::cref(outputFileArg), std::cref(verboseArg)));

View File

@ -1,6 +0,0 @@
#ifndef MAIN_H
#define MAIN_H
int main(int argc, char *argv[]);
#endif // MAIN_H

View File

@ -237,15 +237,24 @@ void printNotifications(const MediaFileInfo &fileInfo, const char *head = nullpt
printNotifications(notifications, head, beVerbose);
}
const char *const fieldNames = "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";
#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"
void printFieldNames(const ArgumentOccurance &occurance)
#define TAG_MODIFIER "tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all"
#define TARGET_MODIFIER "target-level target-levelname target-tracks target-tracks\n" \
"target-chapters target-editions target-attachments target-reset"
const char *const fieldNames = FIELD_NAMES;
const char *const fieldNamesForSet = FIELD_NAMES " " TAG_MODIFIER " " TARGET_MODIFIER;
void printFieldNames(const ArgumentOccurrence &occurrence)
{
CMD_UTILS_START_CONSOLE;
VAR_UNUSED(occurance)
cout << fieldNames << endl;
VAR_UNUSED(occurrence)
cout << fieldNames;
cout << "\nTag modifier: " << TAG_MODIFIER;
cout << "\nTarget modifier: " << TARGET_MODIFIER << endl;
}
TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage)
@ -350,8 +359,13 @@ bool applyTargetConfiguration(TagTarget &target, const std::string &configStr)
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, 17, "target-attachments=") == 0) {
} 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 {
@ -514,16 +528,15 @@ FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
enum class AttachmentAction {
Add,
UpdateById,
UpdateByName,
RemoveById,
RemoveByName
Update,
Remove
};
class AttachmentInfo
{
public:
AttachmentInfo();
void parseDenotation(const char *denotation);
void apply(AbstractContainer *container);
void apply(AbstractAttachment *attachment);
void reset();
@ -531,17 +544,45 @@ public:
AttachmentAction action;
uint64 id;
string path;
string name;
string mime;
string desc;
bool hasId;
const char *path;
const char *name;
const char *mime;
const char *desc;
};
AttachmentInfo::AttachmentInfo() :
action(AttachmentAction::Add),
id(0)
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");
@ -549,58 +590,64 @@ void AttachmentInfo::apply(AbstractContainer *container)
bool attachmentFound = false;
switch(action) {
case AttachmentAction::Add:
if(path.empty() || name.empty()) {
container->addNotification(NotificationType::Critical, "No name or path specified for new attachment to be added.", context);
if(!path || !name) {
cerr << "Argument --update-argument specified but no name/path provided." << endl;
return;
}
apply(container->createAttachment());
break;
case AttachmentAction::UpdateById:
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->id() == id) {
apply(attachment);
attachmentFound = true;
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 == true) {
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be updated.", context);
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::UpdateByName:
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->name() == name) {
apply(attachment);
attachmentFound = true;
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 == true) {
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + name + "\" does not exist and hence can't be updated.", context);
}
break;
case AttachmentAction::RemoveById:
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);
}
}
if(!attachmentFound == true) {
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be removed.", context);
}
break;
case AttachmentAction::RemoveByName:
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->name() == name) {
attachment->setIgnored(true);
attachmentFound = true;
} 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 == true) {
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + name + "\" does not exist and hence can't be removed.", context);
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;
}
@ -608,19 +655,19 @@ void AttachmentInfo::apply(AbstractContainer *container)
void AttachmentInfo::apply(AbstractAttachment *attachment)
{
if(id) {
if(hasId) {
attachment->setId(id);
}
if(!path.empty()) {
if(path) {
attachment->setFile(path);
}
if(!name.empty()) {
if(name) {
attachment->setName(name);
}
if(!mime.empty()) {
if(mime) {
attachment->setMimeType(mime);
}
if(!desc.empty()) {
if(desc) {
attachment->setDescription(desc);
}
}
@ -629,15 +676,13 @@ void AttachmentInfo::reset()
{
action = AttachmentAction::Add;
id = 0;
path.clear();
name.clear();
mime.clear();
desc.clear();
hasId = false;
path = name = mime = desc = nullptr;
}
bool AttachmentInfo::next(AbstractContainer *container)
{
if(!id && path.empty() && name.empty() && mime.empty() && desc.empty()) {
if(!id && !path && !name && !mime && !desc) {
// skip empty attachment infos
return false;
}
@ -646,7 +691,7 @@ bool AttachmentInfo::next(AbstractContainer *container)
return true;
}
void generateFileInfo(const ArgumentOccurance &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg)
void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg)
{
CMD_UTILS_START_CONSOLE;
#if defined(GUI_QTWIDGETS) || defined(GUI_QTQUICK)
@ -723,7 +768,7 @@ void printProperty(const char *propName, const intType value, const char *suffix
}
}
void displayFileInfo(const ArgumentOccurance &, const Argument &filesArg, const Argument &verboseArg)
void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg)
{
CMD_UTILS_START_CONSOLE;
if(!filesArg.isPresent() || filesArg.values().empty()) {
@ -809,11 +854,11 @@ void displayFileInfo(const ArgumentOccurance &, const Argument &filesArg, const
{ // 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("Label", attachment->label());
printProperty("Description", attachment->description());
if(attachment->data()) {
printProperty("Size", dataSizeToString(attachment->data()->size(), true));
@ -825,6 +870,7 @@ void displayFileInfo(const ArgumentOccurance &, const Argument &filesArg, const
{ // chapters
const auto chapters = fileInfo.chapters();
if(!chapters.empty()) {
cout << "Chapters:" << endl;
for(const auto *chapter : chapters) {
printProperty("ID", chapter->id());
if(!chapter->names().empty()) {
@ -959,7 +1005,12 @@ void setTagInfo(const SetTagInfoArgs &args)
return;
}
auto fields = parseFieldDenotations(args.valuesArg, false);
if(fields.empty() && (!args.removeTargetsArg.isPresent() || args.removeTargetsArg.values().empty()) && (!args.attachmentsArg.isPresent() || args.attachmentsArg.values().empty()) && (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty())) {
if(fields.empty()
&& (!args.removeTargetArg.isPresent() || args.removeTargetArg.values().empty())
&& (!args.addAttachmentArg.isPresent() || args.addAttachmentArg.values().empty())
&& (!args.updateAttachmentArg.isPresent() || args.updateAttachmentArg.values().empty())
&& (!args.removeAttachmentArg.isPresent() || args.removeAttachmentArg.values().empty())
&& (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty())) {
cerr << "Error: No fields/attachments have been specified." << endl;
return;
}
@ -973,10 +1024,10 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// determine targets to remove
vector<TagTarget> targetsToRemove;
targetsToRemove.emplace_back();
bool validRemoveTargetsSpecified = false;
if(args.removeTargetsArg.isPresent()) {
for(const auto &targetDenotation : args.removeTargetsArg.values()) {
for(size_t i = 0, max = args.removeTargetArg.occurrences(); i != max; ++i) {
for(const auto &targetDenotation : args.removeTargetArg.values(i)) {
targetsToRemove.emplace_back();
if(!strcmp(targetDenotation, ",")) {
if(validRemoveTargetsSpecified) {
targetsToRemove.emplace_back();
@ -1128,7 +1179,7 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context);
}
bool attachmentsModified = false;
if(args.attachmentsArg.isPresent() || args.removeExistingAttachmentsArg.isPresent()) {
if(args.addAttachmentArg.isPresent() || args.updateAttachmentArg.isPresent() || args.removeAttachmentArg.isPresent() || args.removeExistingAttachmentsArg.isPresent()) {
static const string context("setting attachments");
fileInfo.parseAttachments();
if(fileInfo.attachmentsParsingStatus() == ParsingStatus::Ok) {
@ -1140,40 +1191,29 @@ void setTagInfo(const SetTagInfoArgs &args)
}
attachmentsModified = true;
}
// add/update/remove attachments explicitely
// add/update/remove attachments
AttachmentInfo currentInfo;
for(const char *value : args.attachmentsArg.values()) {
if(!strcmp(value, ",")) {
attachmentsModified |= currentInfo.next(container);
} else if(!strcmp(value, "add")) {
currentInfo.action = AttachmentAction::Add;
} else if(!strcmp(value, "update-by-id")) {
currentInfo.action = AttachmentAction::UpdateById;
} else if(!strcmp(value, "update-by-name")) {
currentInfo.action = AttachmentAction::UpdateByName;
} else if(!strcmp(value, "remove-by-id")) {
currentInfo.action = AttachmentAction::RemoveById;
} else if(!strcmp(value, "remove-by-name")) {
currentInfo.action = AttachmentAction::RemoveByName;
} else if(!strncmp(value, "id=", 3)) {
try {
currentInfo.id = stringToNumber<uint64, string>(value + 3);
} catch(const ConversionException &) {
container->addNotification(NotificationType::Warning, "The specified attachment ID \"" + string(value + 3) + "\" is invalid.", context);
}
} else if(!strncmp(value, "path=", 5)) {
currentInfo.path = value + 5;
} else if(!strncmp(value, "name=", 5)) {
currentInfo.name = value + 5;
} else if(!strncmp(value, "mime=", 5)) {
currentInfo.mime = value + 5;
} else if(!strncmp(value, "desc=", 5)) {
currentInfo.desc = value + 5;
} else {
container->addNotification(NotificationType::Warning, "The attachment specification \"" + string(value) + "\" is invalid and will be ignored.", context);
currentInfo.action = AttachmentAction::Add;
for(size_t i = 0, occurrences = args.addAttachmentArg.occurrences(); i != occurrences; ++i) {
for(const char *value : args.addAttachmentArg.values(i)) {
currentInfo.parseDenotation(value);
}
attachmentsModified |= currentInfo.next(container);
}
currentInfo.action = AttachmentAction::Update;
for(size_t i = 0, occurrences = args.updateAttachmentArg.occurrences(); i != occurrences; ++i) {
for(const char *value : args.updateAttachmentArg.values(i)) {
currentInfo.parseDenotation(value);
}
attachmentsModified |= currentInfo.next(container);
}
currentInfo.action = AttachmentAction::Remove;
for(size_t i = 0, occurrences = args.removeAttachmentArg.occurrences(); i != occurrences; ++i) {
for(const char *value : args.removeAttachmentArg.values(i)) {
currentInfo.parseDenotation(value);
}
attachmentsModified |= currentInfo.next(container);
}
attachmentsModified |= currentInfo.next(container);
} else {
fileInfo.addNotification(NotificationType::Critical, "Unable to assign attachments because the container object has not been initialized.", context);
}
@ -1220,7 +1260,7 @@ void extractField(const Argument &fieldsArg, const Argument &inputFileArg, const
inputFileInfo.setPath(inputFileArg.values().front());
inputFileInfo.open(true);
inputFileInfo.parseTags();
cout << "Extracting " << fieldsArg.values().front() << " of \"" << inputFileArg.values().front() << "\" ..." << endl;
(outputFileArg.isPresent() ? cout : cerr) << "Extracting " << fieldsArg.values().front() << " of \"" << inputFileArg.values().front() << "\" ..." << endl;
auto tags = inputFileInfo.tags();
vector<pair<const TagValue *, string> > values;
// iterate through all tags
@ -1234,7 +1274,7 @@ void extractField(const Argument &fieldsArg, const Argument &inputFileArg, const
}
if(values.empty()) {
cerr << "File has no (supported) " << fieldsArg.values().front() << " field." << endl;
} else {
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(values.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
@ -1254,6 +1294,11 @@ void extractField(const Argument &fieldsArg, const Argument &inputFileArg, const
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl;
}
}
} else {
// write data to stdout if no output file has been specified
for(const auto &value : values) {
cout.write(value.first->dataPointer(), value.first->dataSize());
}
}
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;

View File

@ -26,8 +26,10 @@ struct SetTagInfoArgs
ApplicationUtilities::Argument mergeMultipleSuccessiveTagsArg;
ApplicationUtilities::Argument id3v2VersionArg;
ApplicationUtilities::Argument encodingArg;
ApplicationUtilities::Argument removeTargetsArg;
ApplicationUtilities::Argument attachmentsArg;
ApplicationUtilities::Argument removeTargetArg;
ApplicationUtilities::Argument addAttachmentArg;
ApplicationUtilities::Argument updateAttachmentArg;
ApplicationUtilities::Argument removeAttachmentArg;
ApplicationUtilities::Argument removeExistingAttachmentsArg;
ApplicationUtilities::Argument minPaddingArg;
ApplicationUtilities::Argument maxPaddingArg;
@ -44,9 +46,10 @@ struct SetTagInfoArgs
};
extern const char *const fieldNames;
void printFieldNames(const ApplicationUtilities::ArgumentOccurance &occurance);
void displayFileInfo(const ApplicationUtilities::ArgumentOccurance &, const ApplicationUtilities::Argument &filesArg, const ApplicationUtilities::Argument &verboseArg);
void generateFileInfo(const ApplicationUtilities::ArgumentOccurance &, const ApplicationUtilities::Argument &inputFileArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &validateArg);
extern const char *const fieldNamesForSet;
void printFieldNames(const ApplicationUtilities::ArgumentOccurrence &occurrence);
void displayFileInfo(const ApplicationUtilities::ArgumentOccurrence &, const ApplicationUtilities::Argument &filesArg, const ApplicationUtilities::Argument &verboseArg);
void generateFileInfo(const ApplicationUtilities::ArgumentOccurrence &, const ApplicationUtilities::Argument &inputFileArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &validateArg);
void displayTagInfo(const ApplicationUtilities::Argument &fieldsArg, const ApplicationUtilities::Argument &filesArg, const ApplicationUtilities::Argument &verboseArg);
void setTagInfo(const Cli::SetTagInfoArgs &args);
void extractField(const ApplicationUtilities::Argument &fieldsArg, const ApplicationUtilities::Argument &inputFileArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &verboseArg);

View File

@ -31,7 +31,9 @@ class CliTests : public TestFixture
CPPUNIT_TEST(testMultipleFiles);
CPPUNIT_TEST(testMultipleValuesPerField);
CPPUNIT_TEST(testHandlingAttachments);
CPPUNIT_TEST(testDisplayingTechnicalInfo);
CPPUNIT_TEST(testDisplayingInfo);
CPPUNIT_TEST(testExtraction);
CPPUNIT_TEST(testReadingAndWritingDocumentTitle);
#endif
CPPUNIT_TEST_SUITE_END();
@ -46,7 +48,9 @@ public:
void testMultipleFiles();
void testMultipleValuesPerField();
void testHandlingAttachments();
void testDisplayingTechnicalInfo();
void testDisplayingInfo();
void testExtraction();
void testReadingAndWritingDocumentTitle();
#endif
private:
@ -69,7 +73,7 @@ void CliTests::testBasicReadingAndWriting()
{
string stdout, stderr;
// get specific field
string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const char *const args1[] = {"tageditor", "get", "title", "-f", mkvFile.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT(stderr.empty());
@ -115,7 +119,7 @@ void CliTests::testBasicReadingAndWriting()
void CliTests::testHandlingOfTargets()
{
string stdout, stderr;
string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFile(workingCopyPath("matroska_wave1/test2.mkv"));
const char *const args1[] = {"tageditor", "get", "-f", mkvFile.data(), nullptr};
// add song title (title field for tag with level 30)
@ -131,7 +135,7 @@ void CliTests::testHandlingOfTargets()
CPPUNIT_ASSERT(stdout.find("Genre The album genre") > albumPos);
// remove tags targeting level 30 and 50 and add new tag targeting level 30 and the audio track
const char *const args3[] = {"tageditor", "set", "target-level=30", "target-tracks=3134325680", "title=The audio track", "encoder=likely some AAC encoder", "--remove-targets", "target-level=30", ",", "target-level=50", "-f", mkvFile.data(), nullptr};
const char *const args3[] = {"tageditor", "set", "target-level=30", "target-tracks=3134325680", "title=The audio track", "encoder=likely some AAC encoder", "--remove-target", "target-level=30", "--remove-target", "target-level=50", "-f", mkvFile.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT((songPos = stdout.find("song")) != string::npos);
@ -143,7 +147,7 @@ void CliTests::testHandlingOfTargets()
}
/*!
* \brief Tests handling of ID3v1 and ID3v2 tags.
* \brief Tests handling of ID3v1 and ID3v2 tags and MP3 specific options.
*/
void CliTests::testHandlingOfId3Tags()
{
@ -156,9 +160,9 @@ void CliTests::testHandlingOfId3Tags()
void CliTests::testMultipleFiles()
{
string stdout, stderr;
string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
string mkvFile2(workingCopyPath("matroska_wave1/test2.mkv"));
string mkvFile3(workingCopyPath("matroska_wave1/test3.mkv"));
const string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
const string mkvFile2(workingCopyPath("matroska_wave1/test2.mkv"));
const string mkvFile3(workingCopyPath("matroska_wave1/test3.mkv"));
// get tags of 3 files at once
const char *const args1[] = {"tageditor", "get", "-f", mkvFile1.data(), mkvFile2.data(), mkvFile3.data(), nullptr};
@ -205,21 +209,70 @@ void CliTests::testMultipleFiles()
*/
void CliTests::testMultipleValuesPerField()
{
// TODO
// TODO (feature not implemented yet)
}
/*!
* \brief Tests handling attachments.
*/
void CliTests::testHandlingAttachments()
{
string stdout, stderr;
const string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
const string mkvFile2("path=" + testFilePath("matroska_wave1/test2.mkv"));
// add attachment
const char *const args2[] = {"tageditor", "set", "--add-attachment", "name=test2.mkv", "mime=video/x-matroska", "desc=Test attachment", mkvFile2.data(), "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr));
const char *const args1[] = {"tageditor", "info", "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
size_t pos1;
CPPUNIT_ASSERT((pos1 = stdout.find("Attachments:")) != string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") > pos1);
CPPUNIT_ASSERT(stdout.find("MIME-type video/x-matroska") > pos1);
CPPUNIT_ASSERT(stdout.find("Description Test attachment") > pos1);
CPPUNIT_ASSERT(stdout.find("Size 20.16 MiB (21142764 byte)") > pos1);
// update attachment
const char *const args3[] = {"tageditor", "set", "--update-attachment", "name=test2.mkv", "desc=Updated test attachment", "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT((pos1 = stdout.find("Attachments:")) != string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") > pos1);
CPPUNIT_ASSERT(stdout.find("MIME-type video/x-matroska") > pos1);
CPPUNIT_ASSERT(stdout.find("Description Updated test attachment") > pos1);
CPPUNIT_ASSERT(stdout.find("Size 20.16 MiB (21142764 byte)") > pos1);
// TODO: extract assigned attachment (feature not implemented yet)
// remove assigned attachment
const char *const args5[] = {"tageditor", "set", "--remove-attachment", "name=test2.mkv", "-f", mkvFile1.data(), nullptr};
CPPUNIT_ASSERT_EQUAL(0, execApp(args5, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
CPPUNIT_ASSERT(stdout.find("Attachments:") == string::npos);
CPPUNIT_ASSERT(stdout.find("Name test2.mkv") == string::npos);
}
/*!
* \brief Tests displaying general file info.
*/
void CliTests::testDisplayingInfo()
{
// TODO (not very important)
}
/*!
* \brief Tests extraction (used for cover or other binary fields).
*/
void CliTests::testExtraction()
{
// TODO
}
/*!
* \brief Tests displaying technical info.
* \brief Tests reading and writing the document title.
*/
void CliTests::testDisplayingTechnicalInfo()
void CliTests::testReadingAndWritingDocumentTitle()
{
// TODO
}