tageditor/cli/mainfeatures.cpp

1467 lines
67 KiB
C++
Raw Normal View History

2015-09-06 20:20:00 +02:00
#include "./mainfeatures.h"
2015-09-06 15:41:17 +02:00
2015-09-06 20:20:00 +02:00
#include "../application/knownfieldmodel.h"
#if defined(GUI_QTWIDGETS) || defined(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>
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>
#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>
2015-04-22 19:33:53 +02:00
#include <c++utilities/misc/memory.h>
#if defined(GUI_QTWIDGETS) || defined(GUI_QTQUICK)
# include <QDir>
#endif
2015-04-22 19:33:53 +02:00
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
#include <vector>
2015-04-22 19:33:53 +02:00
using namespace std;
using namespace ApplicationUtilities;
using namespace ConversionUtilities;
using namespace ChronoUtilities;
using namespace EscapeCodes;
using namespace Settings;
using namespace Media;
#if defined(GUI_QTWIDGETS) || defined(GUI_QTQUICK)
using namespace Utility;
#endif
2015-04-22 19:33:53 +02:00
namespace Cli {
// define enums, operators and structs to handle specified field denotations
2015-04-22 19:33:53 +02:00
enum class DenotationType
{
Normal,
Increment,
File
};
inline TagType operator| (TagType lhs, TagType rhs)
{
return static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
}
inline TagType operator& (TagType lhs, TagType rhs)
{
return static_cast<TagType>(static_cast<unsigned int>(lhs) & static_cast<unsigned int>(rhs));
}
2015-04-22 19:33:53 +02:00
inline TagType &operator|= (TagType &lhs, TagType rhs)
{
return lhs = static_cast<TagType>(static_cast<unsigned int>(lhs) | static_cast<unsigned int>(rhs));
}
struct FieldScope
2015-04-22 19:33:53 +02:00
{
FieldScope(KnownField field = KnownField::Invalid, TagType tagType = TagType::Unspecified, TagTarget tagTarget = TagTarget());
bool operator ==(const FieldScope &other) const;
KnownField field;
2015-04-22 19:33:53 +02:00
TagType tagType;
TagTarget tagTarget;
};
FieldScope::FieldScope(KnownField field, TagType tagType, TagTarget tagTarget) :
field(field),
tagType(tagType),
tagTarget(tagTarget)
2015-04-22 19:33:53 +02:00
{}
bool FieldScope::operator ==(const FieldScope &other) const
{
return field == other.field && tagType == other.tagType && tagTarget == other.tagTarget;
}
struct FieldValue
{
FieldValue(DenotationType type, unsigned int fileIndex, const char *value);
DenotationType type;
unsigned int fileIndex;
string value;
};
inline FieldValue::FieldValue(DenotationType type, unsigned int fileIndex, const char *value) :
type(type),
fileIndex(fileIndex),
value(value)
{}
}
namespace std {
using namespace Cli;
template <> struct hash<KnownField>
{
std::size_t operator()(const KnownField &ids) const
{
using type = typename std::underlying_type<KnownField>::type;
return std::hash<type>()(static_cast<type>(ids));
}
};
template <> struct hash<TagType>
{
std::size_t operator()(const TagType &ids) const
{
using type = typename std::underlying_type<TagType>::type;
return std::hash<type>()(static_cast<type>(ids));
}
};
template <> struct hash<TagTarget::IdContainerType>
{
std::size_t operator()(const TagTarget::IdContainerType &ids) const
{
using std::hash;
auto seed = ids.size();
for(auto id : ids) {
seed ^= id + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
return seed;
}
};
template <> struct hash<TagTarget>
{
std::size_t operator()(const TagTarget& target) const
{
using std::hash;
return ((hash<uint64>()(target.level())
^ (hash<TagTarget::IdContainerType>()(target.tracks()) << 1)) >> 1)
^ (hash<TagTarget::IdContainerType>()(target.attachments()) << 1);
}
};
template <> struct hash<FieldScope>
{
std::size_t operator()(const FieldScope& scope) const
{
using std::hash;
return ((hash<KnownField>()(scope.field)
^ (hash<TagType>()(scope.tagType) << 1)) >> 1)
^ (hash<TagTarget>()(scope.tagTarget) << 1);
}
};
}
namespace Cli {
typedef vector<FieldValue> FieldValues;
typedef unordered_map<FieldScope, FieldValues> FieldDenotations;
constexpr bool isDigit(char c)
2015-04-22 19:33:53 +02:00
{
return c >= '0' && c <= '9';
}
string incremented(const string &str, unsigned int toIncrement = 1)
2015-04-22 19:33:53 +02:00
{
string res;
2015-04-22 19:33:53 +02:00
res.reserve(str.size());
unsigned int value = 0;
bool hasValue = false;
for(const char &c : str) {
if(toIncrement && c >= '0' && c <= '9') {
value = value * 10 + static_cast<unsigned int>(c - '0');
2015-04-22 19:33:53 +02:00
hasValue = true;
} else {
if(hasValue) {
res += numberToString(value + 1);
2015-04-22 19:33:53 +02:00
hasValue = false;
--toIncrement;
}
res += c;
2015-04-22 19:33:53 +02:00
}
}
if(hasValue) {
res += numberToString(value + 1);
}
2015-04-22 19:33:53 +02:00
return res;
}
void printNotifications(NotificationList &notifications, const char *head = nullptr, bool beVerbose = false)
2015-04-22 19:33:53 +02:00
{
2015-09-23 00:02:06 +02:00
if(!beVerbose) {
for(const auto &notification : notifications) {
switch(notification.type()) {
case NotificationType::Debug:
case NotificationType::Information:
break;
default:
goto printNotifications;
}
}
return;
}
2015-04-22 19:33:53 +02:00
if(!notifications.empty()) {
printNotifications:
2015-04-22 19:33:53 +02:00
if(head) {
cout << head << endl;
}
Notification::sortByTime(notifications);
2015-09-23 00:02:06 +02:00
for(const auto &notification : notifications) {
2015-04-22 19:33:53 +02:00
switch(notification.type()) {
case NotificationType::Debug:
2015-09-23 00:02:06 +02:00
if(beVerbose) {
cout << "Debug ";
break;
} else {
continue;
2015-09-23 00:02:06 +02:00
}
case NotificationType::Information:
if(beVerbose) {
cout << "Information ";
break;
} else {
continue;
2015-09-23 00:02:06 +02:00
}
2015-04-22 19:33:53 +02:00
case NotificationType::Warning:
cout << "Warning ";
break;
2015-09-23 00:02:06 +02:00
case NotificationType::Critical:
cout << "Error ";
break;
2015-04-22 19:33:53 +02:00
default:
2015-09-23 00:02:06 +02:00
;
2015-04-22 19:33:53 +02:00
}
cout << notification.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
cout << notification.context() << ": ";
cout << notification.message() << endl;
}
}
}
void printNotifications(const MediaFileInfo &fileInfo, const char *head = nullptr, bool beVerbose = false)
{
NotificationList notifications;
fileInfo.gatherRelatedNotifications(notifications);
printNotifications(notifications, head, beVerbose);
}
#define FIELD_NAMES "title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" \
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 &occurrence)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
VAR_UNUSED(occurrence)
2016-08-03 17:48:53 +02:00
cout << fieldNames;
cout << "\nTag modifier: " << TAG_MODIFIER;
cout << "\nTarget modifier: " << TARGET_MODIFIER << endl;
2015-04-22 19:33:53 +02:00
}
TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage)
{
if(usageArg.isPresent()) {
const auto &val = usageArg.values().front();
2016-06-14 00:52:33 +02:00
if(!strcmp(val, "never")) {
2015-04-22 19:33:53 +02:00
return TagUsage::Never;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "keepexisting")) {
2015-04-22 19:33:53 +02:00
return TagUsage::KeepExisting;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "always")) {
2015-04-22 19:33:53 +02:00
return TagUsage::Always;
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified tag usage \"" << val << "\" is invalid and will be ignored." << endl;
2015-04-22 19:33:53 +02:00
}
}
return defaultUsage;
}
TagTextEncoding parseEncodingDenotation(const Argument &encodingArg, TagTextEncoding defaultEncoding)
{
if(encodingArg.isPresent()) {
const auto &val = encodingArg.values().front();
2016-06-14 00:52:33 +02:00
if(!strcmp(val, "utf8")) {
2015-04-22 19:33:53 +02:00
return TagTextEncoding::Utf8;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "latin1")) {
2015-04-22 19:33:53 +02:00
return TagTextEncoding::Latin1;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "utf16be")) {
2015-04-22 19:33:53 +02:00
return TagTextEncoding::Utf16BigEndian;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "utf16le")) {
2015-04-22 19:33:53 +02:00
return TagTextEncoding::Utf16LittleEndian;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "auto")) {
2015-04-22 19:33:53 +02:00
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified encoding \"" << val << "\" is invalid and will be ignored." << endl;
2015-04-22 19:33:53 +02:00
}
}
return defaultEncoding;
}
ElementPosition parsePositionDenotation(const Argument &posArg, const Argument &valueArg, ElementPosition defaultPos)
2015-11-28 00:20:49 +01:00
{
if(posArg.isPresent()) {
const char *val = valueArg.values(0).front();
2016-06-14 00:52:33 +02:00
if(!strcmp(val, "front")) {
2015-11-28 00:20:49 +01:00
return ElementPosition::BeforeData;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "back")) {
2015-11-28 00:20:49 +01:00
return ElementPosition::AfterData;
2016-06-14 00:52:33 +02:00
} else if(!strcmp(val, "keep")) {
2015-11-28 00:20:49 +01:00
return ElementPosition::Keep;
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified position \"" << val << "\" is invalid and will be ignored." << endl;
2015-11-28 00:20:49 +01:00
}
}
return defaultPos;
}
uint64 parseUInt64(const Argument &arg, uint64 defaultValue)
{
if(arg.isPresent()) {
try {
2016-06-14 00:52:33 +02:00
if(*arg.values().front() == '0' && *(arg.values().front() + 1) == 'x') {
return stringToNumber<uint64>(arg.values().front() + 2, 16);
2015-11-28 00:20:49 +01:00
} else {
return stringToNumber<uint64>(arg.values().front());
2015-11-28 00:20:49 +01:00
}
} catch(const ConversionException &) {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified value \"" << arg.values().front() << "\" is no valid unsigned integer and will be ignored." << endl;
2015-11-28 00:20:49 +01:00
}
}
return defaultValue;
}
2015-04-22 19:33:53 +02:00
TagTarget::IdContainerType parseIds(const std::string &concatenatedIds)
{
auto splittedIds = splitString(concatenatedIds, ",", EmptyPartsTreat::Omit);
TagTarget::IdContainerType convertedIds;
convertedIds.reserve(splittedIds.size());
for(const auto &id : splittedIds) {
try {
convertedIds.push_back(stringToNumber<TagTarget::IdType>(id));
2015-11-28 00:20:49 +01:00
} catch(const ConversionException &) {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified ID \"" << id << "\" is invalid and will be ignored." << endl;
2015-04-22 19:33:53 +02:00
}
}
return convertedIds;
}
bool applyTargetConfiguration(TagTarget &target, const std::string &configStr)
{
if(!configStr.empty()) {
if(configStr.compare(0, 13, "target-level=") == 0) {
try {
target.setLevel(stringToNumber<uint64>(configStr.substr(13)));
2015-11-28 00:20:49 +01:00
} catch (const ConversionException &) {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified target level \"" << configStr.substr(13) << "\" is invalid and will be ignored." << endl;
}
} else if(configStr.compare(0, 17, "target-levelname=") == 0) {
target.setLevelName(configStr.substr(17));
} else if(configStr.compare(0, 14, "target-tracks=") == 0) {
target.tracks() = parseIds(configStr.substr(14));
} else if(configStr.compare(0, 16, "target-chapters=") == 0) {
target.chapters() = parseIds(configStr.substr(16));
} else if(configStr.compare(0, 16, "target-editions=") == 0) {
target.editions() = parseIds(configStr.substr(16));
} else if(configStr.compare(0, 19, "target-attachments=") == 0) {
target.attachments() = parseIds(configStr.substr(17));
} else if(configStr.compare(0, 13, "target-reset=") == 0) {
if(*(configStr.data() + 13)) {
cerr << "Warning: Invalid assignment " << (configStr.data() + 13) << " for target-reset will be ignored." << endl;
}
target.clear();
} else if(configStr == "target-reset") {
target.clear();
} else {
return false;
}
return true;
2015-04-22 19:33:53 +02:00
} else {
return false;
}
}
FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly)
2015-04-22 19:33:53 +02:00
{
FieldDenotations fields;
2016-06-25 23:11:16 +02:00
if(fieldsArg.isPresent()) {
const vector<const char *> &fieldDenotations = fieldsArg.values();
FieldScope scope;
2016-06-25 23:11:16 +02:00
for(const char *fieldDenotationString : fieldDenotations) {
// check for tag or target specifier
const auto fieldDenotationLen = strlen(fieldDenotationString);
if(!strncmp(fieldDenotationString, "tag=", 4)) {
2016-06-25 23:11:16 +02:00
if(fieldDenotationLen == 4) {
cerr << "Warning: The \"tag\"-specifier has been used with no value(s) and hence is ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
} else {
TagType tagType = TagType::Unspecified;
for(const auto &part : splitString(fieldDenotationString + 4, ",", EmptyPartsTreat::Omit)) {
if(part == "id3v1") {
tagType |= TagType::Id3v1Tag;
} else if(part == "id3v2") {
tagType |= TagType::Id3v2Tag;
} else if(part == "id3") {
tagType |= TagType::Id3v1Tag | TagType::Id3v2Tag;
} else if(part == "itunes" || part == "mp4") {
tagType |= TagType::Mp4Tag;
} else if(part == "vorbis") {
tagType |= TagType::VorbisComment;
} else if(part == "matroska") {
tagType |= TagType::MatroskaTag;
} else if(part == "all" || part == "any") {
tagType = TagType::Unspecified;
break;
} else {
cerr << "Warning: The value provided with the \"tag\"-specifier is invalid and will be ignored. Possible values are: id3,id3v1,id3v2,itunes,vorbis,matroska,all" << endl;
tagType = scope.tagType;
2016-06-25 23:11:16 +02:00
break;
}
2015-04-22 19:33:53 +02:00
}
scope.tagType = tagType;
2016-06-25 23:11:16 +02:00
break;
2015-04-22 19:33:53 +02:00
}
} else if(applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) {
2016-06-25 23:11:16 +02:00
continue;
2015-04-22 19:33:53 +02:00
}
// check whether field name starts with + indicating an additional value
bool additionalValue = *fieldDenotationString == '+';
if(additionalValue) {
++fieldDenotationString;
}
2016-06-25 23:11:16 +02:00
// read field name
const auto equationPos = strchr(fieldDenotationString, '=');
size_t fieldNameLen = equationPos ? static_cast<size_t>(equationPos - fieldDenotationString) : strlen(fieldDenotationString);
// field name might denote increment ("+") or path disclosure (">")
DenotationType type = DenotationType::Normal;
if(fieldNameLen && equationPos) {
switch(*(equationPos - 1)) {
case '+':
type = DenotationType::Increment;
--fieldNameLen;
break;
case '>':
type = DenotationType::File;
--fieldNameLen;
break;
default:
;
}
2015-04-22 19:33:53 +02:00
}
2016-06-25 23:11:16 +02:00
// field name might specify a file index
unsigned int fileIndex = 0, mult = 1;
for(const char *digitPos = fieldDenotationString + fieldNameLen - 1; fieldNameLen && isDigit(*digitPos); --fieldNameLen, --digitPos, mult *= 10) {
fileIndex += static_cast<unsigned int>(*digitPos - '0') * mult;
}
if(!fieldNameLen) {
cerr << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl;
continue;
}
// parse the denoted filed
if(!strncmp(fieldDenotationString, "title", fieldNameLen)) {
scope.field = KnownField::Title;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "album", fieldNameLen)) {
scope.field = KnownField::Album;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "artist", fieldNameLen)) {
scope.field = KnownField::Artist;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "genre", fieldNameLen)) {
scope.field = KnownField::Genre;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "year", fieldNameLen)) {
scope.field = KnownField::Year;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "comment", fieldNameLen)) {
scope.field = KnownField::Comment;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "bpm", fieldNameLen)) {
scope.field = KnownField::Bpm;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "bps", fieldNameLen)) {
scope.field = KnownField::Bps;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "lyricist", fieldNameLen)) {
scope.field = KnownField::Lyricist;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "track", fieldNameLen)) {
scope.field = KnownField::TrackPosition;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "disk", fieldNameLen)) {
scope.field = KnownField::DiskPosition;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "part", fieldNameLen)) {
scope.field = KnownField::PartNumber;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "totalparts", fieldNameLen)) {
scope.field = KnownField::TotalParts;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "encoder", fieldNameLen)) {
scope.field = KnownField::Encoder;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "recorddate", fieldNameLen)) {
scope.field = KnownField::RecordDate;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "performers", fieldNameLen)) {
scope.field = KnownField::Performers;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "duration", fieldNameLen)) {
scope.field = KnownField::Length;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "language", fieldNameLen)) {
scope.field = KnownField::Language;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "encodersettings", fieldNameLen)) {
scope.field = KnownField::EncoderSettings;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "lyrics", fieldNameLen)) {
scope.field = KnownField::Lyrics;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "synchronizedlyrics", fieldNameLen)) {
scope.field = KnownField::SynchronizedLyrics;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "grouping", fieldNameLen)) {
scope.field = KnownField::Grouping;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "recordlabel", fieldNameLen)) {
scope.field = KnownField::RecordLabel;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "cover", fieldNameLen)) {
scope.field = KnownField::Cover;
2016-06-25 23:11:16 +02:00
type = DenotationType::File; // read cover always from file
} else if(!strncmp(fieldDenotationString, "composer", fieldNameLen)) {
scope.field = KnownField::Composer;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "rating", fieldNameLen)) {
scope.field = KnownField::Rating;
2016-06-25 23:11:16 +02:00
} else if(!strncmp(fieldDenotationString, "description", fieldNameLen)) {
scope.field = KnownField::Description;
2015-04-22 19:33:53 +02:00
} else {
2016-06-25 23:11:16 +02:00
// no "KnownField" value matching -> discard the field denotation
cerr << "Warning: The field name \"" << string(fieldDenotationString, fieldNameLen) << "\" is unknown and will be ingored." << endl;
continue;
}
// add field denotation scope
auto &fieldValues = fields[scope];
// add value to the scope (if present)
2016-06-25 23:11:16 +02:00
if(equationPos) {
if(readOnly) {
cerr << "Warning: Specified value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
} else {
// file index might have been specified explicitely
// if not (mult == 1) use the index of the last value and increase it by one if the value is not an additional one
// if there are no previous values, just use the index 0
fieldValues.emplace_back(FieldValue(type, mult == 1 ? (fieldValues.empty() ? 0 : fieldValues.back().fileIndex + (additionalValue ? 0 : 1)) : fileIndex, (equationPos + 1)));
2016-06-25 23:11:16 +02:00
}
2015-04-22 19:33:53 +02:00
}
if(additionalValue && readOnly) {
cerr << "Warning: Indication of an additional value for \"" << string(fieldDenotationString, fieldNameLen) << "\" will be ignored." << endl;
}
2015-04-22 19:33:53 +02:00
}
}
return fields;
2015-04-22 19:33:53 +02:00
}
enum class AttachmentAction {
Add,
Update,
Remove
};
class AttachmentInfo
{
public:
AttachmentInfo();
void parseDenotation(const char *denotation);
void apply(AbstractContainer *container);
void apply(AbstractAttachment *attachment);
void reset();
bool next(AbstractContainer *container);
AttachmentAction action;
uint64 id;
bool hasId;
const char *path;
const char *name;
const char *mime;
const char *desc;
};
AttachmentInfo::AttachmentInfo() :
action(AttachmentAction::Add),
id(0),
hasId(false),
path(nullptr),
name(nullptr),
mime(nullptr),
desc(nullptr)
{}
void AttachmentInfo::parseDenotation(const char *denotation)
{
if(!strncmp(denotation, "id=", 3)) {
try {
id = stringToNumber<uint64, string>(denotation + 3);
hasId = true;
} catch(const ConversionException &) {
cerr << "The specified attachment ID \"" << (denotation + 3) << "\" is invalid.";
}
} else if(!strncmp(denotation, "path=", 5)) {
path = denotation + 5;
} else if(!strncmp(denotation, "name=", 5)) {
name = denotation + 5;
} else if(!strncmp(denotation, "mime=", 5)) {
mime = denotation + 5;
} else if(!strncmp(denotation, "desc=", 5)) {
desc = denotation + 5;
} else {
cerr << "The attachment specification \"" << denotation << "\" is invalid and will be ignored.";
}
}
void AttachmentInfo::apply(AbstractContainer *container)
{
static const string context("applying specified attachments");
AbstractAttachment *attachment = nullptr;
bool attachmentFound = false;
switch(action) {
case AttachmentAction::Add:
if(!path || !name) {
cerr << "Argument --update-argument specified but no name/path provided." << endl;
return;
}
apply(container->createAttachment());
break;
case AttachmentAction::Update:
if(hasId) {
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->id() == id) {
apply(attachment);
attachmentFound = true;
}
}
if(!attachmentFound) {
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be updated.", context);
}
} else if(name) {
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->name() == name) {
apply(attachment);
attachmentFound = true;
}
}
if(!attachmentFound) {
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be updated.", context);
}
} else {
cerr << "Argument --update-argument specified but no ID/name provided." << endl;
}
break;
case AttachmentAction::Remove:
if(hasId) {
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->id() == id) {
attachment->setIgnored(true);
attachmentFound = true;
}
}
if(!attachmentFound) {
container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be removed.", context);
}
} else if(name) {
for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) {
attachment = container->attachment(i);
if(attachment->name() == name) {
attachment->setIgnored(true);
attachmentFound = true;
}
}
if(!attachmentFound) {
container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + string(name) + "\" does not exist and hence can't be removed.", context);
}
} else {
cerr << "Argument --remove-argument specified but no ID/name provided." << endl;
}
break;
}
}
void AttachmentInfo::apply(AbstractAttachment *attachment)
{
if(hasId) {
attachment->setId(id);
}
if(path) {
attachment->setFile(path);
}
if(name) {
attachment->setName(name);
}
if(mime) {
attachment->setMimeType(mime);
}
if(desc) {
attachment->setDescription(desc);
}
}
void AttachmentInfo::reset()
{
action = AttachmentAction::Add;
id = 0;
hasId = false;
path = name = mime = desc = nullptr;
}
bool AttachmentInfo::next(AbstractContainer *container)
{
if(!id && !path && !name && !mime && !desc) {
// skip empty attachment infos
return false;
}
apply(container);
reset();
return true;
}
void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg)
2015-04-22 19:33:53 +02:00
{
2015-09-01 20:20:15 +02:00
CMD_UTILS_START_CONSOLE;
#if defined(GUI_QTWIDGETS) || defined(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(QString::fromLocal8Bit(outputFileArg.values().front()));
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 << "Error: An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << endl;
}
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
}
2016-06-14 22:54:49 +02:00
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl;
2015-04-22 19:33:53 +02:00
}
#else
VAR_UNUSED(inputFileArg);
VAR_UNUSED(outputFileArg);
VAR_UNUSED(validateArg);
cerr << "Error: Generating HTML info is only available if built with Qt support." << endl;
#endif
2015-04-22 19:33:53 +02:00
}
2016-10-02 21:55:12 +02:00
void printProperty(const char *propName, const char *value, const char *suffix = nullptr, Indentation indentation = 4)
2015-08-09 23:53:45 +02:00
{
if(*value) {
2016-10-02 21:55:12 +02:00
std::cout << indentation << propName << Indentation(30 - strlen(propName)) << value;
2015-08-09 23:53:45 +02:00
if(suffix) {
2016-10-02 21:55:12 +02:00
std::cout << ' ' << suffix;
2015-08-09 23:53:45 +02:00
}
2016-10-02 21:55:12 +02:00
std::cout << '\n';
2015-08-09 23:53:45 +02:00
}
}
2016-10-02 21:55:12 +02:00
void printProperty(const char *propName, const string &value, const char *suffix = nullptr, Indentation indentation = 4)
2015-08-09 23:53:45 +02:00
{
2016-10-02 21:55:12 +02:00
printProperty(propName, value.data(), suffix, indentation);
2015-08-09 23:53:45 +02:00
}
2016-10-02 21:55:12 +02:00
void printProperty(const char *propName, TimeSpan timeSpan, const char *suffix = nullptr, Indentation indentation = 4)
2015-10-14 19:49:48 +02:00
{
if(!timeSpan.isNull()) {
2016-10-02 21:55:12 +02:00
printProperty(propName, timeSpan.toString(TimeSpanOutputFormat::WithMeasures), suffix, indentation);
2015-10-14 19:49:48 +02:00
}
}
2016-10-02 21:55:12 +02:00
void printProperty(const char *propName, DateTime dateTime, const char *suffix = nullptr, Indentation indentation = 4)
2015-10-14 19:49:48 +02:00
{
if(!dateTime.isNull()) {
2016-10-02 21:55:12 +02:00
printProperty(propName, dateTime.toString(), suffix, indentation);
2015-10-14 19:49:48 +02:00
}
}
2016-11-16 19:34:40 +01:00
void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix = nullptr, Indentation indentation = 4)
{
const char *pos;
switch(elementPosition) {
case ElementPosition::BeforeData:
pos = "before data";
break;
case ElementPosition::AfterData:
pos = "after data";
break;
case ElementPosition::Keep:
pos = nullptr;
}
if(pos) {
printProperty(propName, pos, suffix, indentation);
}
}
2015-08-09 23:53:45 +02:00
template<typename intType>
2016-10-02 21:55:12 +02:00
void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, Indentation indentation = 4)
2015-08-09 23:53:45 +02:00
{
if(value != 0 || force) {
2016-10-02 21:55:12 +02:00
printProperty(propName, numberToString<intType>(value), suffix, indentation);
2015-08-09 23:53:45 +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;
2016-06-25 23:11:16 +02:00
if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl;
2015-08-09 23:53:45 +02:00
return;
}
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();
cout << "Technical information for \"" << file << "\":" << endl;
cout << " Container format: " << fileInfo.containerFormatName() << endl;
2015-10-14 19:49:48 +02:00
{
if(const auto container = fileInfo.container()) {
size_t segmentIndex = 0;
for(const auto &title : container->titles()) {
if(segmentIndex) {
printProperty("Title", title + " (segment " + numberToString(++segmentIndex) + ")");
} else {
++segmentIndex;
printProperty("Title", title);
}
}
printProperty("Document type", container->documentType());
printProperty("Read version", container->readVersion());
printProperty("Version", container->version());
printProperty("Document read version", container->doctypeReadVersion());
printProperty("Document version", container->doctypeVersion());
printProperty("Duration", container->duration());
printProperty("Creation time", container->creationTime());
printProperty("Modification time", container->modificationTime());
2016-11-16 19:34:40 +01:00
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()));
}
}
2015-08-09 23:53:45 +02:00
{ // tracks
const auto tracks = fileInfo.tracks();
if(!tracks.empty()) {
cout << " Tracks:" << endl;
for(const auto *track : tracks) {
printProperty("ID", track->id(), nullptr, true);
printProperty("Name", track->name());
printProperty("Type", track->mediaTypeName());
2015-09-24 01:15:48 +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());
2015-08-09 23:53:45 +02:00
printProperty("Raw format ID", track->formatId());
if(track->size()) {
printProperty("Size", dataSizeToString(track->size(), true));
}
2015-10-14 19:49:48 +02:00
printProperty("Duration", track->duration());
2015-08-09 23:53:45 +02:00
printProperty("FPS", track->fps());
if(track->channelConfigString()) {
printProperty("Channel config", track->channelConfigString());
} else {
printProperty("Channel count", track->channelCount());
}
2015-09-24 01:15:48 +02:00
if(track->extensionChannelConfigString()) {
printProperty("Extension channel config", track->extensionChannelConfigString());
}
2015-08-09 23:53:45 +02:00
printProperty("Bitrate", track->bitrate(), "kbit/s");
printProperty("Bits per sample", track->bitsPerSample());
2015-09-24 01:15:48 +02:00
printProperty("Sampling frequency", track->samplingFrequency(), "Hz");
2015-08-09 23:53:45 +02:00
printProperty("Extension sampling frequency", track->extensionSamplingFrequency(), "Hz");
printProperty("Sample count", track->sampleCount());
2015-10-14 19:49:48 +02:00
printProperty("Creation time", track->creationTime());
printProperty("Modification time", track->modificationTime());
2015-08-09 23:53:45 +02:00
cout << endl;
}
} else {
cout << " File has no (supported) tracks." << endl;
}
}
{ // attachments
const auto attachments = fileInfo.attachments();
if(!attachments.empty()) {
cout << "Attachments:" << endl;
2015-08-09 23:53:45 +02:00
for(const auto *attachment : attachments) {
printProperty("ID", attachment->id());
printProperty("Name", attachment->name());
printProperty("MIME-type", attachment->mimeType());
printProperty("Description", attachment->description());
if(attachment->data()) {
printProperty("Size", dataSizeToString(attachment->data()->size(), true));
}
cout << endl;
}
}
}
{ // chapters
const auto chapters = fileInfo.chapters();
if(!chapters.empty()) {
cout << "Chapters:" << endl;
for(const auto *chapter : chapters) {
2015-08-09 23:53:45 +02:00
printProperty("ID", chapter->id());
if(!chapter->names().empty()) {
printProperty("Name", static_cast<string>(chapter->names().front()));
}
if(!chapter->startTime().isNegative()) {
2015-08-09 23:53:45 +02:00
printProperty("Start time", chapter->startTime().toString());
}
if(!chapter->endTime().isNegative()) {
2015-08-09 23:53:45 +02:00
printProperty("End time", chapter->endTime().toString());
}
cout << endl;
}
}
}
2016-06-14 22:54:49 +02:00
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl;
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;
2016-06-25 23:11:16 +02:00
if(!filesArg.isPresent() || filesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl;
2015-04-22 19:33:53 +02:00
return;
}
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 << "\":" << endl;
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 << tag->typeName();
2016-08-05 01:48:36 +02:00
if(tagType == TagType::Id3v2Tag) {
// version only interesting for ID3v2 tags?
cout << " (version " << tag->version() << ')';
}
if(tagType == TagType::MatroskaTag || !tag->target().isEmpty()) {
2016-08-03 17:48:53 +02:00
cout << " targeting \"" << tag->targetString() << '\"';
2015-04-22 19:33:53 +02:00
}
cout << endl;
// iterate through fields specified by the user
if(fields.empty()) {
for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) {
2016-08-03 17:48:53 +02:00
const auto &values = tag->values(field);
if(!values.empty()) {
const char *fieldName = KnownFieldModel::fieldName(field);
2016-08-03 17:48:53 +02:00
const auto fieldNameLen = strlen(fieldName);
for(const auto &value : values) {
// write field name
cout << ' ' << fieldName;
// write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
2015-04-22 19:33:53 +02:00
}
2016-08-03 17:48:53 +02:00
// write value
try {
const auto textValue = value->toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
cout << textValue;
}
} catch(const ConversionException &) {
cout << "conversion error";
}
cout << endl;
2015-04-22 19:33:53 +02:00
}
}
}
} else {
for(const auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
if(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) {
2016-08-03 17:48:53 +02:00
const auto &values = tag->values(denotedScope.field);
const char *fieldName = KnownFieldModel::fieldName(denotedScope.field);
2016-08-03 17:48:53 +02:00
const auto fieldNameLen = strlen(fieldName);
if(values.empty()) {
// write field name
const char *fieldName = KnownFieldModel::fieldName(denotedScope.field);
cout << ' ' << fieldName;
// write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
}
cout << "none";
} else {
2016-08-03 17:48:53 +02:00
for(const auto &value : values) {
// write field name
const char *fieldName = KnownFieldModel::fieldName(denotedScope.field);
cout << ' ' << fieldName;
// write padding
for(auto i = fieldNameLen; i < 18; ++i) {
cout << ' ';
}
2016-08-03 17:48:53 +02:00
// write value
try {
const auto textValue = value->toString(TagTextEncoding::Utf8);
if(textValue.empty()) {
cout << "can't display here (see --extract)";
} else {
cout << textValue;
}
} catch(const ConversionException &) {
cout << "conversion error";
}
cout << endl;
}
}
2015-04-22 19:33:53 +02:00
}
}
}
}
} else {
cout << " File has no (supported) tag information." << endl;
}
2016-06-14 22:54:49 +02:00
} catch(const ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl;
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;
2016-06-25 23:11:16 +02:00
if(!args.filesArg.isPresent() || args.filesArg.values().empty()) {
2016-06-14 22:54:49 +02:00
cerr << "Error: No files have been specified." << endl;
2015-04-22 19:33:53 +02:00
return;
}
if(args.outputFilesArg.isPresent() && args.outputFilesArg.values().size() != args.filesArg.values().size()) {
cerr << "Error: The number of output files does not match the number of input files." << endl;
return;
}
auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector<const char *>();
auto currentOutputFile = outputFiles.cbegin(), noMoreOutputFiles = outputFiles.cend();
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 << "Warning: No fields/attachments have been specified." << endl;
2015-04-22 19:33:53 +02:00
}
// determine required targets
vector<TagTarget> requiredTargets;
for(const auto &fieldDenotation : fields) {
const FieldScope &scope = fieldDenotation.first;
if(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 << "Warning: The given target specification \"" << targetDenotation << "\" is invalid and will be ignored." << endl;
}
}
}
// parse other settings
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;
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid and will be ingored." << endl;
2015-04-22 19:33:53 +02:00
}
}
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);
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
cout << "Setting tag information for \"" << file << "\" ..." << endl;
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();
2015-10-14 19:49:48 +02:00
bool docTitleModified = false;
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);
docTitleModified = true;
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: The specified document title \"" << newTitle << "\" can not be set because the file has not that many segments." << endl;
2015-10-14 19:49:48 +02:00
}
++segmentIndex;
}
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: Setting the document title is not supported for the file." << endl;
2015-10-14 19:49:48 +02:00
}
}
fileInfo.tags(tags);
if(!tags.empty()) {
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();
// iterate through all denoted field values
for(auto &fieldDenotation : fields) {
const FieldScope &denotedScope = fieldDenotation.first;
FieldValues &denotedValues = fieldDenotation.second;
// decide whether the scope of the denotation matches of the current tag
if((denotedScope.tagType == TagType::Unspecified
|| (denotedScope.tagType & tagType) != TagType::Unspecified)
&& (!targetSupported || denotedScope.tagTarget == tagTarget)) {
// select the relevant values for the current file index
vector<FieldValue *> relevantDenotedValues;
unsigned int currentFileIndex = 0;
for(FieldValue &denotatedValue : denotedValues) {
if(denotatedValue.fileIndex <= fileIndex) {
if(relevantDenotedValues.empty() || (denotatedValue.fileIndex >= currentFileIndex)) {
if(currentFileIndex != denotatedValue.fileIndex) {
currentFileIndex = denotatedValue.fileIndex;
relevantDenotedValues.clear();
}
relevantDenotedValues.push_back(&denotatedValue);
2015-04-22 19:33:53 +02:00
}
}
}
// convert the values to TagValue
vector<TagValue> convertedValues;
for(FieldValue *relevantDenotedValue : relevantDenotedValues) {
// 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 {
TagTextEncoding usedEncoding = denotedEncoding;
if(!tag->canEncodingBeUsed(denotedEncoding)) {
usedEncoding = tag->proposedTextEncoding();
}
convertedValues.emplace_back(relevantDenotedValue->value, TagTextEncoding::Utf8, usedEncoding);
if(relevantDenotedValue->type == DenotationType::Increment && tag == tags.back()) {
relevantDenotedValue->value = incremented(relevantDenotedValue->value);
}
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
tag->setValues(denotedScope.field, convertedValues);
2015-04-22 19:33:53 +02:00
}
}
}
} else {
fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context);
}
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
}
}
2015-10-14 19:49:48 +02:00
if(!tags.empty() || docTitleModified || attachmentsModified) {
2015-04-22 19:33:53 +02: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();
2015-04-22 19:33:53 +02:00
fileInfo.applyChanges();
fileInfo.gatherRelatedNotifications(notifications);
2015-04-22 19:33:53 +02:00
cout << "Changes have been applied." << endl;
} catch(const ApplicationUtilities::Failure &) {
2016-06-14 22:54:49 +02:00
cerr << "Error: Failed to apply changes." << endl;
2015-04-22 19:33:53 +02:00
}
2015-10-14 19:49:48 +02:00
} else {
2016-06-14 22:54:49 +02:00
cerr << "Warning: No changed to be applied." << endl;
2015-04-22 19:33:53 +02:00
}
} catch(const ApplicationUtilities::Failure &) {
2016-06-14 22:54:49 +02:00
cerr << "Error: A parsing failure occured when reading/writing the file \"" << file << "\"." << endl;
2016-06-25 23:11:16 +02:00
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading/writing the file \"" << file << "\"." << endl;
2015-04-22 19:33:53 +02:00
}
2015-11-28 00:20:49 +01:00
printNotifications(notifications, "Notifications:", args.verboseArg.isPresent());
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 << "Error: Excactly one field or attachment needs to be specified." << endl;
2015-04-22 19:33:53 +02:00
return;
}
2016-08-03 17:48:53 +02:00
if(!inputFilesArg.isPresent() || inputFilesArg.values().empty()) {
cerr << "Error: No files have been specified." << endl;
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 {
// parse tags
inputFileInfo.setPath(file);
inputFileInfo.open(true);
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) {
for(const auto &fieldDenotation : fieldDenotations) {
const auto &value = tag->value(fieldDenotation.first.field);
if(!value.isEmpty()) {
values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-", true));
}
}
2015-04-22 19:33:53 +02:00
}
2016-08-03 17:48:53 +02:00
if(values.empty()) {
cerr << " None of the specified files has a (supported) " << fieldArg.values().front() << " field." << endl;
} 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) {
fstream outputFileStream;
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;
} catch(...) {
::IoUtilities::catchIoFailure();
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());
}
}
} 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 << " None of the specified files has a (supported) attachment with the specified ID/name." << endl;
} 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) {
fstream outputFileStream;
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;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO error occured when writing the file \"" << path << "\"." << endl;
}
}
} 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 ApplicationUtilities::Failure &) {
cerr << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl;
} catch(...) {
::IoUtilities::catchIoFailure();
cerr << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl;
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
}
}
}