Improve handling of attachments via CLI
This commit is contained in:
parent
6d8833fe9d
commit
1a20eea65b
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
int main(int argc, char *argv[]);
|
||||
|
||||
#endif // MAIN_H
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue