tageditor/cli/mainfeatures.cpp

837 lines
43 KiB
C++
Raw Normal View History

2015-09-06 20:20:00 +02:00
#include "./mainfeatures.h"
2017-01-15 21:43:46 +01:00
#include "./helper.h"
#include "./attachmentinfo.h"
2015-09-06 15:41:17 +02:00
2015-09-06 20:20:00 +02:00
#include "../application/knownfieldmodel.h"
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
# include "../misc/utility.h"
# include "../misc/htmlinfo.h"
#endif
2015-04-22 19:33:53 +02:00
#include <tagparser/mediafileinfo.h>
#include <tagparser/tag.h>
2017-01-15 21:43:46 +01:00
#include <tagparser/tagvalue.h>
2015-08-09 23:53:45 +02:00
#include <tagparser/abstracttrack.h>
#include <tagparser/abstractattachment.h>
#include <tagparser/abstractchapter.h>
2015-04-22 19:33:53 +02:00
#include <c++utilities/application/failure.h>
2015-09-01 20:20:15 +02:00
#include <c++utilities/application/commandlineutils.h>
2015-04-22 19:33:53 +02:00
#include <c++utilities/conversion/stringconversion.h>
2017-01-30 22:13:49 +01:00
#include <c++utilities/conversion/stringbuilder.h>
2015-04-22 19:33:53 +02:00
#include <c++utilities/conversion/conversionexception.h>
#include <c++utilities/io/ansiescapecodes.h>
2016-06-14 22:54:49 +02:00
#include <c++utilities/io/catchiofailure.h>
#include <c++utilities/io/nativefilestream.h>
2015-04-22 19:33:53 +02:00
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
# include <QDir>
# include <qtutilities/misc/conversion.h>
#endif
2015-04-22 19:33:53 +02:00
#include <iostream>
#include <iomanip>
#include <cstring>
#include <algorithm>
2017-02-05 21:04:27 +01:00
#include <memory>
2015-04-22 19:33:53 +02:00
using namespace std;
using namespace ApplicationUtilities;
using namespace ConversionUtilities;
using namespace ChronoUtilities;
using namespace IoUtilities;
2015-04-22 19:33:53 +02:00
using namespace EscapeCodes;
using namespace Settings;
using namespace Media;
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
using namespace Utility;
#endif
2015-04-22 19:33:53 +02:00
namespace Cli {
#define FIELD_NAMES \
"title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
2016-08-03 17:48:53 +02:00
"recorddate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" \
"recordlabel cover composer rating description"
2016-07-04 23:28:11 +02:00
#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" \
2016-08-03 17:48:53 +02:00
"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 &)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
cout << fieldNames;
cout << "\nTag modifier: " << TAG_MODIFIER;
cout << "\nTarget modifier: " << TARGET_MODIFIER << endl;
2015-04-22 19:33:53 +02:00
}
void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
2015-04-22 19:33:53 +02:00
try {
// parse tags
MediaFileInfo inputFileInfo(inputFileArg.values().front());
inputFileInfo.setForceFullParse(validateArg.isPresent());
inputFileInfo.open(true);
inputFileInfo.parseEverything();
2016-08-03 17:48:53 +02:00
(outputFileArg.isPresent() ? cout : cerr) << "Saving file info for \"" << inputFileArg.values().front() << "\" ..." << endl;
2015-04-22 19:33:53 +02:00
NotificationList origNotify;
2016-08-03 17:48:53 +02:00
if(outputFileArg.isPresent()) {
QFile file(fromNativeFileName(outputFileArg.values().front()));
2016-08-03 17:48:53 +02:00
if(file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, origNotify)) && file.flush()) {
cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl;
} else {
cerr << Phrases::Error << "An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << Phrases::EndFlush;
2016-08-03 17:48:53 +02:00
}
2015-04-22 19:33:53 +02:00
} else {
2016-08-03 17:48:53 +02:00
cout << HtmlInfo::generateInfo(inputFileInfo, origNotify).data() << endl;
2015-04-22 19:33:53 +02:00
}
} catch(const Media::Failure &) {
cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::EndFlush;
2016-06-14 22:54:49 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << Phrases::Error << "An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::EndFlush;
2015-04-22 19:33:53 +02:00
}
#else
VAR_UNUSED(inputFileArg);
VAR_UNUSED(outputFileArg);
VAR_UNUSED(validateArg);
cerr << Phrases::Error << "Generating HTML info is only available if built with Qt support." << Phrases::EndFlush;
#endif
2015-04-22 19:33:53 +02:00
}
void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg)
2015-08-09 23:53:45 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
// check whether files have been specified
2016-06-25 23:11:16 +02:00
if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << Phrases::Error << "No files have been specified." << Phrases::End;
2015-08-09 23:53:45 +02:00
return;
}
2015-08-09 23:53:45 +02:00
MediaFileInfo fileInfo;
2016-08-03 17:48:53 +02:00
for(const char *file : filesArg.values()) {
2015-08-09 23:53:45 +02:00
try {
// parse tags
fileInfo.setPath(file);
fileInfo.open(true);
2016-08-03 17:48:53 +02:00
fileInfo.parseContainerFormat();
2015-08-09 23:53:45 +02:00
fileInfo.parseTracks();
fileInfo.parseAttachments();
fileInfo.parseChapters();
// print general/container-related info
cout << "Technical information for \"" << file << "\":\n";
cout << " - " << TextAttribute::Bold << "Container format: " << fileInfo.containerFormatName() << Phrases::End;
if(const auto container = fileInfo.container()) {
size_t segmentIndex = 0;
for(const auto &title : container->titles()) {
if(segmentIndex) {
printProperty("Title", title % " (segment " % ++segmentIndex + ")");
} else {
++segmentIndex;
printProperty("Title", title);
2015-10-14 19:49:48 +02:00
}
}
printProperty("Document type", container->documentType());
printProperty("Read version", container->readVersion());
printProperty("Version", container->version());
printProperty("Document read version", container->doctypeReadVersion());
printProperty("Document version", container->doctypeVersion());
printProperty("Duration", container->duration());
printProperty("Creation time", container->creationTime());
printProperty("Modification time", container->modificationTime());
printProperty("Tag position", container->determineTagPosition());
printProperty("Index position", container->determineIndexPosition());
2015-10-14 19:49:48 +02:00
}
if(fileInfo.paddingSize()) {
printProperty("Padding", dataSizeToString(fileInfo.paddingSize()));
}
// print tracks
const auto tracks = fileInfo.tracks();
if(!tracks.empty()) {
cout << " - " << TextAttribute::Bold << "Tracks: " << fileInfo.technicalSummary() << Phrases::End;
for(const auto *track : tracks) {
printProperty("ID", track->id(), nullptr, true);
printProperty("Name", track->name());
printProperty("Type", track->mediaTypeName());
if(track->language() != "und") {
printProperty("Language", track->language());
2015-08-09 23:53:45 +02:00
}
const char *fmtName = track->formatName(), *fmtAbbr = track->formatAbbreviation();
printProperty("Format", fmtName);
if(strcmp(fmtName, fmtAbbr)) {
printProperty("Abbreviation", fmtAbbr);
}
printProperty("Extensions", track->format().extensionName());
printProperty("Raw format ID", track->formatId());
if(track->size()) {
printProperty("Size", dataSizeToString(track->size(), true));
}
printProperty("Duration", track->duration());
printProperty("FPS", track->fps());
if(track->channelConfigString()) {
printProperty("Channel config", track->channelConfigString());
} else {
printProperty("Channel count", track->channelCount());
}
if(track->extensionChannelConfigString()) {
printProperty("Extension channel config", track->extensionChannelConfigString());
}
printProperty("Bitrate", track->bitrate(), "kbit/s");
printProperty("Bits per sample", track->bitsPerSample());
printProperty("Sampling frequency", track->samplingFrequency(), "Hz");
printProperty("Extension sampling frequency", track->extensionSamplingFrequency(), "Hz");
printProperty(track->mediaType() == MediaType::Video
? "Frame count"
: "Sample count",
track->sampleCount());
printProperty("Creation time", track->creationTime());
printProperty("Modification time", track->modificationTime());
vector<string> labels;
labels.reserve(7);
if(track->isInterlaced()) {
labels.emplace_back("interlaced");
}
if(!track->isEnabled()) {
labels.emplace_back("disabled");
}
if(track->isDefault()) {
labels.emplace_back("default");
}
if(track->isForced()) {
labels.emplace_back("forced");
}
if(track->hasLacing()) {
labels.emplace_back("has lacing");
}
if(track->isEncrypted()) {
labels.emplace_back("encrypted");
}
if(!labels.empty()) {
printProperty("Labeled as", joinStrings(labels, ", "));
}
cout << '\n';
2015-08-09 23:53:45 +02:00
}
} else {
cout << " - File has no (supported) tracks.\n";
2015-08-09 23:53:45 +02:00
}
// print attachments
const auto attachments = fileInfo.attachments();
if(!attachments.empty()) {
cout << " - " << TextAttribute::Bold << "Attachments:" << TextAttribute::Reset << '\n';
for(const auto *attachment : attachments) {
printProperty("ID", attachment->id());
printProperty("Name", attachment->name());
printProperty("MIME-type", attachment->mimeType());
printProperty("Description", attachment->description());
if(attachment->data()) {
printProperty("Size", dataSizeToString(static_cast<uint64>(attachment->data()->size()), true));
2015-08-09 23:53:45 +02:00
}
cout << '\n';
2015-08-09 23:53:45 +02:00
}
}
// print chapters
const auto chapters = fileInfo.chapters();
if(!chapters.empty()) {
cout << " - " << TextAttribute::Bold << "Chapters:" << TextAttribute::Reset << '\n';
for(const auto *chapter : chapters) {
printProperty("ID", chapter->id());
if(!chapter->names().empty()) {
printProperty("Name", static_cast<string>(chapter->names().front()));
}
if(!chapter->startTime().isNegative()) {
printProperty("Start time", chapter->startTime().toString());
}
if(!chapter->endTime().isNegative()) {
printProperty("End time", chapter->endTime().toString());
2015-08-09 23:53:45 +02:00
}
cout << '\n';
2015-08-09 23:53:45 +02:00
}
}
} catch(const Media::Failure &) {
cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush;
2016-06-14 22:54:49 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush;
2015-08-09 23:53:45 +02:00
}
2015-09-23 00:02:06 +02:00
printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent());
2015-08-09 23:53:45 +02:00
cout << endl;
}
}
2016-06-25 23:11:16 +02:00
void displayTagInfo(const Argument &fieldsArg, const Argument &filesArg, const Argument &verboseArg)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
// check whether files have been specified
2016-06-25 23:11:16 +02:00
if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << Phrases::Error << "No files have been specified." << Phrases::End;
2015-04-22 19:33:53 +02:00
return;
}
// parse specified fields
2016-06-25 23:11:16 +02:00
const auto fields = parseFieldDenotations(fieldsArg, true);
2015-04-22 19:33:53 +02:00
MediaFileInfo fileInfo;
2016-08-03 17:48:53 +02:00
for(const char *file : filesArg.values()) {
2015-04-22 19:33:53 +02:00
try {
// parse tags
fileInfo.setPath(file);
fileInfo.open(true);
2016-08-03 17:48:53 +02:00
fileInfo.parseContainerFormat();
2015-04-22 19:33:53 +02:00
fileInfo.parseTags();
cout << "Tag information for \"" << file << "\":\n";
2015-08-09 23:53:45 +02:00
const auto tags = fileInfo.tags();
2016-08-03 17:48:53 +02:00
if(!tags.empty()) {
2015-04-22 19:33:53 +02:00
// iterate through all tags
for(const auto *tag : tags) {
// determine tag type
2016-08-03 17:48:53 +02:00
const TagType tagType = tag->type();
2015-04-22 19:33:53 +02:00
// write tag name and target, eg. MP4/iTunes tag
cout << " - " << TextAttribute::Bold << tagName(tag) << TextAttribute::Reset << '\n';
2015-04-22 19:33:53 +02:00
// iterate through fields specified by the user
if(fields.empty()) {
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
printField(FieldScope(field), tag, tagType, true);
}
} else {
for(const auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) {
printField(denotedScope, tag, tagType, false);
2015-04-22 19:33:53 +02:00
}
}
}
}
} else {
cout << " - File has no (supported) tag information.\n";
2015-04-22 19:33:53 +02:00
}
} catch(const Media::Failure &) {
cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush;
2016-06-14 22:54:49 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush;
2015-04-22 19:33:53 +02:00
}
2015-09-23 00:02:06 +02:00
printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent());
2015-08-09 23:53:45 +02:00
cout << endl;
2015-04-22 19:33:53 +02:00
}
}
2016-06-25 23:11:16 +02:00
void setTagInfo(const SetTagInfoArgs &args)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
// check whether files have been specified
2016-06-25 23:11:16 +02:00
if(!args.filesArg.isPresent() || args.filesArg.values().empty()) {
cerr << Phrases::Error << "No files have been specified." << Phrases::EndFlush;
2015-04-22 19:33:53 +02:00
return;
}
if(args.outputFilesArg.isPresent() && args.outputFilesArg.values().size() != args.filesArg.values().size()) {
cerr << Phrases::Error << "The number of output files does not match the number of input files." << Phrases::EndFlush;
return;
}
// get output files
auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector<const char *>();
auto currentOutputFile = outputFiles.cbegin(), noMoreOutputFiles = outputFiles.cend();
// parse field denotations and check whether there's an operation to be done (changing fields or some other settings)
2016-06-25 23:11:16 +02:00
auto fields = parseFieldDenotations(args.valuesArg, false);
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())
2016-08-05 01:48:36 +02:00
&& (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty())
&& !args.id3v1UsageArg.isPresent()
&& !args.id3v2UsageArg.isPresent()
&& !args.id3v2VersionArg.isPresent()) {
cerr << Phrases::Warning << "No fields/attachments have been specified." << Phrases::End;
2015-04-22 19:33:53 +02:00
}
// determine required targets
vector<TagTarget> requiredTargets;
for(const auto &fieldDenotation : fields) {
const FieldScope &scope = fieldDenotation.first;
2017-06-14 00:09:10 +02:00
if(!scope.isTrack() && find(requiredTargets.cbegin(), requiredTargets.cend(), scope.tagTarget) == requiredTargets.cend()) {
requiredTargets.push_back(scope.tagTarget);
}
}
// determine targets to remove
vector<TagTarget> targetsToRemove;
bool validRemoveTargetsSpecified = false;
for(size_t i = 0, max = args.removeTargetArg.occurrences(); i != max; ++i) {
for(const auto &targetDenotation : args.removeTargetArg.values(i)) {
targetsToRemove.emplace_back();
2016-06-25 23:11:16 +02:00
if(!strcmp(targetDenotation, ",")) {
if(validRemoveTargetsSpecified) {
targetsToRemove.emplace_back();
}
} else if(applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) {
validRemoveTargetsSpecified = true;
} else {
cerr << Phrases::Warning << "The given target specification \"" << targetDenotation << "\" is invalid and will be ignored." << Phrases::End;
}
}
}
// parse ID3v2 version
2015-04-22 19:33:53 +02:00
uint32 id3v2Version = 3;
2015-11-28 00:20:49 +01:00
if(args.id3v2VersionArg.isPresent()) {
2015-04-22 19:33:53 +02:00
try {
2015-11-28 00:20:49 +01:00
id3v2Version = stringToNumber<uint32>(args.id3v2VersionArg.values().front());
2015-04-22 19:33:53 +02:00
if(id3v2Version < 1 || id3v2Version > 4) {
throw ConversionException();
}
2016-06-14 00:52:33 +02:00
} catch (const ConversionException &) {
2015-04-22 19:33:53 +02:00
id3v2Version = 3;
cerr << Phrases::Warning << "The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid and will be ingored." << Phrases::End;
2015-04-22 19:33:53 +02:00
}
}
// parse other settings
2015-11-28 00:20:49 +01:00
const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8);
const TagUsage id3v1Usage = parseUsageDenotation(args.id3v1UsageArg, TagUsage::KeepExisting);
const TagUsage id3v2Usage = parseUsageDenotation(args.id3v2UsageArg, TagUsage::Always);
// setup media file info
2015-11-28 00:20:49 +01:00
MediaFileInfo fileInfo;
fileInfo.setMinPadding(parseUInt64(args.minPaddingArg, 0));
fileInfo.setMaxPadding(parseUInt64(args.maxPaddingArg, 0));
fileInfo.setPreferredPadding(parseUInt64(args.prefPaddingArg, 0));
fileInfo.setTagPosition(parsePositionDenotation(args.tagPosArg, args.tagPosValueArg, ElementPosition::BeforeData));
2015-11-28 00:20:49 +01:00
fileInfo.setForceTagPosition(args.forceTagPosArg.isPresent());
fileInfo.setIndexPosition(parsePositionDenotation(args.indexPosArg, args.indexPosValueArg, ElementPosition::BeforeData));
2015-11-28 00:20:49 +01:00
fileInfo.setForceIndexPosition(args.forceIndexPosArg.isPresent());
fileInfo.setForceRewrite(args.forceRewriteArg.isPresent());
// iterate through all specified files
2015-04-22 19:33:53 +02:00
unsigned int fileIndex = 0;
static const string context("setting tags");
NotificationList notifications;
2016-08-03 17:48:53 +02:00
for(const char *file : args.filesArg.values()) {
2015-04-22 19:33:53 +02:00
try {
// parse tags and tracks (tracks are relevent because track meta-data such as language can be changed as well)
cout << TextAttribute::Bold << "Setting tag information for \"" << file << "\" ..." << Phrases::EndFlush;
notifications.clear();
2015-04-22 19:33:53 +02:00
fileInfo.setPath(file);
2016-08-03 17:48:53 +02:00
fileInfo.parseContainerFormat();
2015-04-22 19:33:53 +02:00
fileInfo.parseTags();
fileInfo.parseTracks();
vector<Tag *> tags;
// remove tags with the specified targets
if(validRemoveTargetsSpecified) {
fileInfo.tags(tags);
for(auto *tag : tags) {
if(find(targetsToRemove.cbegin(), targetsToRemove.cend(), tag->target()) != targetsToRemove.cend()) {
fileInfo.removeTag(tag);
}
}
tags.clear();
}
// create new tags according to settings
2016-08-05 01:48:36 +02:00
fileInfo.createAppropriateTags(args.treatUnknownFilesAsMp3FilesArg.isPresent(), id3v1Usage, id3v2Usage, args.id3InitOnCreateArg.isPresent(), args.id3TransferOnRemovalArg.isPresent(), args.mergeMultipleSuccessiveTagsArg.isPresent(), !args.id3v2VersionArg.isPresent(), id3v2Version, requiredTargets);
auto container = fileInfo.container();
2016-06-25 23:11:16 +02:00
if(args.docTitleArg.isPresent() && !args.docTitleArg.values().empty()) {
2015-12-27 19:49:17 +01:00
if(container && container->supportsTitle()) {
2015-10-14 19:49:48 +02:00
size_t segmentIndex = 0, segmentCount = container->titles().size();
2015-11-28 00:20:49 +01:00
for(const auto &newTitle : args.docTitleArg.values()) {
2015-10-14 19:49:48 +02:00
if(segmentIndex < segmentCount) {
container->setTitle(newTitle, segmentIndex);
} else {
cerr << Phrases::Warning << "The specified document title \"" << newTitle << "\" can not be set because the file has not that many segments." << Phrases::End;
2015-10-14 19:49:48 +02:00
}
++segmentIndex;
}
} else {
cerr << Phrases::Warning << "Setting the document title is not supported for the file." << Phrases::End;
2015-10-14 19:49:48 +02:00
}
}
2017-06-14 00:09:10 +02:00
// select the relevant values for the current file index
for(auto &fieldDenotation : fields) {
FieldValues &denotedValues = fieldDenotation.second;
vector<FieldValue *> &relevantDenotedValues = denotedValues.relevantValues;
denotedValues.relevantValues.clear();
unsigned int currentFileIndex = 0;
for(FieldValue &denotatedValue : denotedValues.allValues) {
if(denotatedValue.fileIndex <= fileIndex) {
if(relevantDenotedValues.empty() || (denotatedValue.fileIndex >= currentFileIndex)) {
if(currentFileIndex != denotatedValue.fileIndex) {
currentFileIndex = denotatedValue.fileIndex;
relevantDenotedValues.clear();
}
relevantDenotedValues.push_back(&denotatedValue);
}
}
}
}
// alter tags
fileInfo.tags(tags);
2017-06-14 00:09:10 +02:00
if(tags.empty()) {
fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context);
} else {
2015-04-22 19:33:53 +02:00
// iterate through all tags
for(auto *tag : tags) {
// clear current values if option is present
2015-11-28 00:20:49 +01:00
if(args.removeOtherFieldsArg.isPresent()) {
2015-04-22 19:33:53 +02:00
tag->removeAllFields();
}
// determine required information for deciding whether specified values match the scope of the current tag
const auto tagType = tag->type();
const bool targetSupported = tag->supportsTarget();
const auto tagTarget = tag->target();
2017-05-18 02:32:51 +02:00
// determine the encoding to store text values
TagTextEncoding usedEncoding = denotedEncoding;
if(!tag->canEncodingBeUsed(denotedEncoding)) {
usedEncoding = tag->proposedTextEncoding();
if(args.encodingArg.isPresent()) {
fileInfo.addNotification(NotificationType::Warning, argsToString("Can't use specified encoding \"", args.encodingArg.values().front(), "\" in ", tagName(tag), " because the tag format/version doesn't support it."), context);
}
}
// iterate through all denoted field values
2017-06-14 00:09:10 +02:00
for(const auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
// decide whether the scope of the denotation matches of the current tag
2017-06-14 00:09:10 +02:00
if(!denotedScope.isTrack() && (denotedScope.tagType == TagType::Unspecified
|| (denotedScope.tagType & tagType) != TagType::Unspecified)
&& (!targetSupported || denotedScope.tagTarget == tagTarget)) {
// convert the values to TagValue
vector<TagValue> convertedValues;
2017-06-14 00:09:10 +02:00
for(const FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) {
// one of the denoted values
if(!relevantDenotedValue->value.empty()) {
if(relevantDenotedValue->type == DenotationType::File) {
2015-04-22 19:33:53 +02:00
try {
// assume the file refers to a picture
MediaFileInfo fileInfo(relevantDenotedValue->value);
2015-04-22 19:33:53 +02:00
fileInfo.open(true);
fileInfo.parseContainerFormat();
2015-08-16 23:41:49 +02:00
auto buff = make_unique<char []>(fileInfo.size());
2015-04-22 19:33:53 +02:00
fileInfo.stream().seekg(0);
fileInfo.stream().read(buff.get(), fileInfo.size());
TagValue value(move(buff), fileInfo.size(), TagDataType::Picture);
value.setMimeType(fileInfo.mimeType());
convertedValues.emplace_back(move(value));
2016-06-14 22:54:49 +02:00
} catch(const Media::Failure &) {
fileInfo.addNotification(NotificationType::Critical, "Unable to parse specified cover file.", context);
2016-06-14 22:54:49 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
fileInfo.addNotification(NotificationType::Critical, "An IO error occured when parsing the specified cover file.", context);
2015-04-22 19:33:53 +02:00
}
} else {
convertedValues.emplace_back(relevantDenotedValue->value, TagTextEncoding::Utf8, usedEncoding);
2015-04-22 19:33:53 +02:00
}
} else {
// if the denoted value is empty, just assign an empty TagValue to remove the field
convertedValues.emplace_back();
2015-04-22 19:33:53 +02:00
}
}
// finally set the values
try {
denotedScope.field.setValues(tag, tagType, convertedValues);
} catch(const ConversionException &e) {
2017-05-18 02:32:51 +02:00
fileInfo.addNotification(NotificationType::Critical, argsToString("Unable to parse denoted field ID \"", denotedScope.field.name(), "\": ", e.what()), context);
}
2015-04-22 19:33:53 +02:00
}
}
}
2017-06-14 00:09:10 +02:00
}
// alter tracks
2017-06-14 00:09:10 +02:00
for(AbstractTrack *track : fileInfo.tracks()) {
for(const auto &fieldDenotation : fields) {
const auto &values = fieldDenotation.second.relevantValues;
if(values.empty()) {
continue;
}
const FieldScope &denotedScope = fieldDenotation.first;
// decide whether the scope of the denotation matches of the current track
if(denotedScope.allTracks || find(denotedScope.trackIds.cbegin(), denotedScope.trackIds.cend(), track->id()) != denotedScope.trackIds.cend()) {
const FieldId &field = denotedScope.field;
const string &value = values.front()->value;
try {
if(field.denotes("name")) {
track->setName(value);
} else if(field.denotes("language")) {
track->setLanguage(value);
} else if(field.denotes("tracknumber")) {
track->setTrackNumber(stringToNumber<uint32>(value));
} else if(field.denotes("enabled")) {
track->setEnabled(stringToBool(value));
} else if(field.denotes("forced")) {
track->setForced(stringToBool(value));
} else if(field.denotes("default")) {
track->setDefault(stringToBool(value));
} else {
fileInfo.addNotification(NotificationType::Critical, argsToString("Denoted track property name \"", field.denotation(), "\" is invalid"), argsToString("setting meta-data of track ", track->id()));
}
} catch(const ConversionException &e) {
fileInfo.addNotification(NotificationType::Critical, argsToString("Unable to parse value for track property \"", field.denotation(), "\": ", e.what()), argsToString("setting meta-data of track ", track->id()));
}
}
}
}
2017-06-14 00:09:10 +02:00
// increment relevant values
for(auto &fieldDenotation : fields) {
for(FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) {
if(!relevantDenotedValue->value.empty() && relevantDenotedValue->type == DenotationType::Increment) {
relevantDenotedValue->value = incremented(relevantDenotedValue->value);
}
}
}
// alter attachments
bool attachmentsModified = false;
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) {
if(container) {
// ignore all existing attachments if argument is specified
2015-11-28 00:20:49 +01:00
if(args.removeExistingAttachmentsArg.isPresent()) {
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
container->attachment(i)->setIgnored(false);
}
attachmentsModified = true;
}
// add/update/remove attachments
AttachmentInfo currentInfo;
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);
}
} else {
fileInfo.addNotification(NotificationType::Critical, "Unable to assign attachments because the container object has not been initialized.", context);
}
} else {
// notification will be added by the file info automatically
}
}
// apply changes
2016-11-19 21:45:52 +01:00
try {
// save parsing notifications because notifications of sub objects like tags, tracks, ... will be gone after applying changes
fileInfo.setSaveFilePath(currentOutputFile != noMoreOutputFiles ? string(*currentOutputFile) : string());
fileInfo.gatherRelatedNotifications(notifications);
fileInfo.invalidateNotifications();
// register handler for logging status
fileInfo.registerCallback(logStatus);
// register interrupt handler
const InterruptHandler handler([&fileInfo] {
fileInfo.tryToAbort();
});
// apply changes and gather notifications
2016-11-19 21:45:52 +01:00
fileInfo.applyChanges();
fileInfo.gatherRelatedNotifications(notifications);
// notify about completion
finalizeLog();
cout << " - Changes have been applied." << endl;
} catch(const Media::OperationAbortedException &) {
finalizeLog();
cerr << Phrases::Warning << "The operation has been aborted." << Phrases::EndFlush;
return;
} catch(const Media::Failure &) {
finalizeLog();
cerr << " - " << Phrases::Error << "Failed to apply changes." << Phrases::EndFlush;
2015-04-22 19:33:53 +02:00
}
} catch(const Media::Failure &) {
finalizeLog();
cerr << " - " << Phrases::Error << "A parsing failure occured when reading/writing the file \"" << file << "\"." << Phrases::EndFlush;
2016-06-25 23:11:16 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
finalizeLog();
cerr << " - " << Phrases::Error << "An IO failure occured when reading/writing the file \"" << file << "\"." << Phrases::EndFlush;
2015-04-22 19:33:53 +02:00
}
2015-11-28 00:20:49 +01:00
printNotifications(notifications, "Notifications:", args.verboseArg.isPresent());
// continue with next file
2015-04-22 19:33:53 +02:00
++fileIndex;
if(currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
2015-04-22 19:33:53 +02:00
}
}
2016-08-03 17:48:53 +02:00
void extractField(const Argument &fieldArg, const Argument &attachmentArg, const Argument &inputFilesArg, const Argument &outputFileArg, const Argument &verboseArg)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
2016-08-03 17:48:53 +02:00
// parse specified field and attachment
const auto fieldDenotations = parseFieldDenotations(fieldArg, true);
AttachmentInfo attachmentInfo;
if(attachmentArg.isPresent()) {
attachmentInfo.parseDenotation(attachmentArg.values().front());
}
if(((fieldDenotations.size() != 1) || (!attachmentInfo.hasId && !attachmentInfo.name))
&& ((fieldDenotations.size() == 1) && (attachmentInfo.hasId || attachmentInfo.name))) {
cerr << Phrases::Error << "Excactly one field or attachment needs to be specified." << Phrases::End;
2015-04-22 19:33:53 +02:00
return;
}
2016-08-03 17:48:53 +02:00
if(!inputFilesArg.isPresent() || inputFilesArg.values().empty()) {
cerr << Phrases::Error << "No files have been specified." << Phrases::End;
2016-08-03 17:48:53 +02:00
return;
}
2015-04-22 19:33:53 +02:00
MediaFileInfo inputFileInfo;
2016-08-03 17:48:53 +02:00
for(const char *file : inputFilesArg.values()) {
try {
// setup media file info
2016-08-03 17:48:53 +02:00
inputFileInfo.setPath(file);
inputFileInfo.open(true);
// extract either tag field or attachment
2016-08-03 17:48:53 +02:00
if(!fieldDenotations.empty()) {
// extract tag field
(outputFileArg.isPresent() ? cout : cerr) << "Extracting field " << fieldArg.values().front() << " of \"" << file << "\" ..." << endl;
inputFileInfo.parseContainerFormat();
inputFileInfo.parseTags();
auto tags = inputFileInfo.tags();
vector<pair<const TagValue *, string> > values;
// iterate through all tags
for(const Tag *tag : tags) {
const TagType tagType = tag->type();
for(const pair<FieldScope, FieldValues> &fieldDenotation : fieldDenotations) {
try {
for(const TagValue *value : fieldDenotation.first.field.values(tag, tagType)) {
values.emplace_back(value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
}
} catch(const ConversionException &e) {
2017-01-30 22:13:49 +01:00
inputFileInfo.addNotification(NotificationType::Critical, "Unable to parse denoted field ID \"" % string(fieldDenotation.first.field.name()) % "\": " + e.what(), "extracting field");
2016-08-03 17:48:53 +02:00
}
}
2015-04-22 19:33:53 +02:00
}
2016-08-03 17:48:53 +02:00
if(values.empty()) {
cerr << " - " << Phrases::Error << "None of the specified files has a (supported) " << fieldArg.values().front() << " field." << Phrases::End;
2016-08-03 17:48:53 +02:00
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(values.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front());
}
for(const auto &value : values) {
NativeFileStream outputFileStream;
2016-08-03 17:48:53 +02:00
outputFileStream.exceptions(ios_base::failbit | ios_base::badbit);
auto path = values.size() > 1 ? joinStrings({outputFilePathWithoutExtension, "-", value.second, outputFileExtension}) : outputFileArg.values().front();
try {
outputFileStream.open(path, ios_base::out | ios_base::binary);
outputFileStream.write(value.first->dataPointer(), value.first->dataSize());
outputFileStream.flush();
cout << " - Value has been saved to \"" << path << "\"." << endl;
2016-08-03 17:48:53 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::End;
2016-08-03 17:48:53 +02:00
}
}
} 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());
}
}
} else {
// extract attachment
auto &logStream = (outputFileArg.isPresent() ? cout : cerr);
logStream << "Extracting attachment with ";
if(attachmentInfo.hasId) {
logStream << "ID " << attachmentInfo.id;
} else {
logStream << "name \"" << attachmentInfo.name << '\"';
}
logStream << " of \"" << file << "\" ..." << endl;
inputFileInfo.parseContainerFormat();
inputFileInfo.parseAttachments();
vector<pair<const AbstractAttachment *, string> > attachments;
// iterate through all attachments
for(const AbstractAttachment *attachment : inputFileInfo.attachments()) {
2016-08-05 01:48:36 +02:00
if((attachmentInfo.hasId && attachment->id() == attachmentInfo.id) || (attachment->name() == attachmentInfo.name)) {
2016-08-03 17:48:53 +02:00
attachments.emplace_back(attachment, joinStrings({attachment->name(), numberToString(attachments.size())}, "-", true));
}
}
if(attachments.empty()) {
cerr << " - " << Phrases::Error << "None of the specified files has a (supported) attachment with the specified ID/name." << Phrases::End;
2016-08-03 17:48:53 +02:00
} else if(outputFileArg.isPresent()) {
string outputFilePathWithoutExtension, outputFileExtension;
if(attachments.size() > 1) {
outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front());
outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front());
}
for(const auto &attachment : attachments) {
NativeFileStream outputFileStream;
2016-08-03 17:48:53 +02:00
outputFileStream.exceptions(ios_base::failbit | ios_base::badbit);
auto path = attachments.size() > 1 ? joinStrings({outputFilePathWithoutExtension, "-", attachment.second, outputFileExtension}) : outputFileArg.values().front();
try {
outputFileStream.open(path, ios_base::out | ios_base::binary);
attachment.first->data()->copyTo(outputFileStream);
outputFileStream.flush();
cout << " - Value has been saved to \"" << path << "\"." << endl;
2016-08-03 17:48:53 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::EndFlush;
2016-08-03 17:48:53 +02:00
}
}
} else {
for(const auto &attachment : attachments) {
attachment.first->data()->copyTo(cout);
}
2015-04-22 19:33:53 +02:00
}
}
2016-08-03 17:48:53 +02:00
} catch(const Media::Failure &) {
cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::End;
2016-08-03 17:48:53 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::End;
2015-04-22 19:33:53 +02:00
}
2016-08-03 17:48:53 +02:00
printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent());
2015-04-22 19:33:53 +02:00
}
}
}