#include "./helper.h" #include "./fieldmapping.h" #include "./mainfeatures.h" #include #include #include #include #include #include #include #include #include #include #if !defined(PLATFORM_WINDOWS) || defined(PLATFORM_MINGW) // for write(STDOUT_FILENO, data, size) - not sure what to use with MSVC #include #endif #include #include #include using namespace std; using namespace std::placeholders; using namespace CppUtilities; using namespace CppUtilities::EscapeCodes; using namespace TagParser; using namespace Settings; namespace Cli { const std::vector &id3v2CoverTypeNames() { static const auto t = std::vector{ "other"sv, "file-icon"sv, "other-file-icon"sv, "front-cover"sv, "back-cover"sv, "leaflet-page"sv, "media"sv, "lead-performer"sv, "artist"sv, "conductor"sv, "band"sv, "composer"sv, "lyricist"sv, "recording-location"sv, "during-recording"sv, "during-performance"sv, "movie-screen-capture"sv, "bright-colored-fish"sv, "illustration"sv, "artist-logotype"sv, "publisher"sv }; return t; } CoverType id3v2CoverType(std::string_view coverName) { static const auto mapping = [] { const auto &names = id3v2CoverTypeNames(); auto map = std::map(); auto index = CoverType(); for (const auto name : names) { map[name] = index++; } return map; }(); if (const auto i = mapping.find(coverName); i != mapping.end()) { return i->second; } else { return invalidCoverType; } } std::string_view id3v2CoverName(CoverType coverType) { const auto &names = id3v2CoverTypeNames(); return coverType < names.size() ? names[coverType] : "?"sv; } CppUtilities::TimeSpanOutputFormat timeSpanOutputFormat = TimeSpanOutputFormat::WithMeasures; /*! * \class InterruptHandler * \brief The InterruptHandler class allows to register an interrupt handler within a scope. * \remarks Only one instance of the class can exist at a time. */ std::function InterruptHandler::s_handler; bool InterruptHandler::s_handlerRegistered = false; /*! * \brief Registers the specified \a handler for SIGINT and SIGTERM as long as this object is alive. * \remarks The specified \a handler should only call functions which are permitted to be used in signal handlers * (eg. use POSIX write() instead of std::cout). * \throws Throws std::runtime_error when attempting to create a 2nd instance. */ InterruptHandler::InterruptHandler(std::function &&handler) { // set handler function or throw if an instance has already been created if (s_handler) { throw runtime_error("Only one instance of InterruptHandler can exist at a time."); } s_handler = std::move(handler); // register handler if not registered yet if (!s_handlerRegistered) { s_handlerRegistered = true; signal(SIGINT, &InterruptHandler::handler); signal(SIGTERM, &InterruptHandler::handler); } } /*! * \brief Unregisters the handler. */ InterruptHandler::~InterruptHandler() { s_handler = function(); } /*! * \brief Internal handler method. */ void InterruptHandler::handler(int signum) { // just exit if no custom handler has been defined if (!s_handler) { _exit(signum); } // finalize log and print warning, prevent using std::cout which could lead to undefined behaviour if (!logLineFinalized) { logLineFinalized = true; #if !defined(PLATFORM_WINDOWS) || defined(PLATFORM_MINGW) write(STDOUT_FILENO, "\n", 1); #endif } #if !defined(PLATFORM_WINDOWS) || defined(PLATFORM_MINGW) if (EscapeCodes::enabled) { write(STDOUT_FILENO, "\033[1;33mWarning:\033[0m \033[1mSignal received, trying to abort ongoing process ...\033[0m\n", 82); } else { write(STDOUT_FILENO, "Warning: Signal received, trying to abort ongoing process ...\n", 63); } #endif // call custom handler s_handler(); } string incremented(const string &str, unsigned int toIncrement) { string res; res.reserve(str.size()); unsigned int value = 0; bool hasValue = false; for (const char &c : str) { if (toIncrement && c >= '0' && c <= '9') { value = value * 10 + static_cast(c - '0'); hasValue = true; } else { if (hasValue) { res += numberToString(value + 1); hasValue = false; --toIncrement; } res += c; } } if (hasValue) { res += numberToString(value + 1); } return res; } void printDiagMessages(const Diagnostics &diag, const char *head, bool beVerbose, const CppUtilities::Argument *pedanticArg) { if (diag.empty()) { return; } // set exit code to failure if there are diag messages considered bad enough auto minLevel = beVerbose ? DiagLevel::Information : DiagLevel::Warning; auto badExitLevel = DiagLevel::Fatal; if (pedanticArg && pedanticArg->isPresent()) { const auto &values = pedanticArg->values(); if (values.empty() || values.front() == "error"sv || values.front() == "critical"sv) { badExitLevel = DiagLevel::Critical; } else if (values.front() == "warning"sv) { badExitLevel = DiagLevel::Warning; } else if (values.front() == "info"sv) { badExitLevel = minLevel = DiagLevel::Information; } else { badExitLevel = minLevel = DiagLevel::Debug; } } // set exit code if there are severe enough messages and check whether there's something to print auto hasAnythingToPrint = false; for (const auto &message : diag) { if (message.level() >= badExitLevel) { exitCode = EXIT_PARSING_FAILURE; } if (message.level() >= minLevel) { hasAnythingToPrint = true; } if (exitCode != EXIT_SUCCESS && hasAnythingToPrint) { break; } } // print diag messages if there's anything to print if (!hasAnythingToPrint) { return; } if (head) { cerr << " - " << head << endl; } for (const auto &message : diag) { if (message.level() < minLevel) { continue; } switch (message.level()) { case DiagLevel::Debug: cerr << " Debug "; break; case DiagLevel::Information: cerr << " Information "; break; case DiagLevel::Warning: setStyle(cerr, Color::Yellow, ColorContext::Foreground, TextAttribute::Bold); setStyle(cerr, TextAttribute::Reset); setStyle(cerr, TextAttribute::Bold); cerr << " Warning "; setStyle(cerr, TextAttribute::Reset); break; case DiagLevel::Critical: setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold); setStyle(cerr, TextAttribute::Reset); setStyle(cerr, TextAttribute::Bold); cerr << " Error "; setStyle(cerr, TextAttribute::Reset); break; case DiagLevel::Fatal: setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold); setStyle(cerr, TextAttribute::Reset); setStyle(cerr, TextAttribute::Bold); cerr << " Fatal "; setStyle(cerr, TextAttribute::Reset); break; default:; } cerr << message.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " "; if (!message.context().empty()) { cerr << message.context() << ": "; } cerr << message.message() << '\n'; } } void printProperty(const char *propName, std::string_view value, const char *suffix, Indentation indentation) { if (value.empty()) { return; } const auto propLen(strlen(propName)); cout << indentation << propName << Indentation(static_cast(propLen < 30 ? 30 - propLen : 1)) << value; if (suffix) { cout << ' ' << suffix; } cout << '\n'; } void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix, Indentation indentation) { const char *pos = nullptr; 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); } } static void printFieldName(std::string_view fieldName) { cout << " " << fieldName; // also write padding constexpr auto defaultIndent = 18; if (fieldName.size() >= defaultIndent) { // write at least one space cout << ' '; return; } for (auto i = fieldName.size(); i < defaultIndent; ++i) { cout << ' '; } } static void printTagValue(const TagValue &value) { switch (value.type()) { case TagDataType::Binary: case TagDataType::Picture: { const auto type = !value.mimeType().empty() ? std::string_view(value.mimeType()) : std::string_view("data"); std::cout << "can't display " << type << " as string (use --extract)"; break; } default: std::cout << value.toDisplayString(); } cout << '\n'; } static void printDescription(const TagValue &value) { if (value.description().empty()) { return; } printFieldName(" description:"); if (value.descriptionEncoding() == TagTextEncoding::Utf8) { cout << value.description(); } else { auto tempValue = TagValue(); tempValue.setDescription(value.description(), value.descriptionEncoding()); tempValue.convertDescriptionEncoding(TagTextEncoding::Utf8); cout << tempValue.description(); } cout << '\n'; } template static void printId3v2CoverValues(TagType *tag) { const auto &fields = tag->fields(); const auto id = tag->fieldId(KnownField::Cover); for (auto range = fields.equal_range(id); range.first != range.second; ++range.first) { const auto &field = range.first->second; printFieldName(argsToString("Cover (", id3v2CoverName(static_cast(field.typeInfo())), ")")); printTagValue(field.value()); printDescription(field.value()); } } void printField(const FieldScope &scope, const Tag *tag, TagType tagType, bool skipEmpty) { const auto fieldName = std::string_view(scope.field.name()); try { if (scope.field.knownFieldForTag(tag, tagType) == KnownField::Cover) { if (tagType == TagType::Id3v2Tag) { printId3v2CoverValues(static_cast(tag)); return; } else if (tagType == TagType::VorbisComment) { printId3v2CoverValues(static_cast(tag)); return; } } // parse field denotation const auto &values = scope.field.values(tag, tagType); // skip values if format-specific field ID used which is not applicable for the current tag if (!values.second) { return; } // skip empty values (unless prevented) if (skipEmpty && values.first.empty()) { return; } // print empty value (if not prevented) if (values.first.empty()) { printFieldName(fieldName); cout << "none\n"; return; } // print values for (const auto &value : values.first) { printFieldName(fieldName); printTagValue(*value); printDescription(*value); } } catch (const ConversionException &e) { // handle conversion error which might happen when parsing field denotation printFieldName(fieldName); cout << "unable to parse - " << e.what() << '\n'; } } template void printNativeFields(const Tag *tag) { const auto *const concreteTag = static_cast(tag); for (const auto &field : concreteTag->fields()) { // skip all fields which are supported anyways if (concreteTag->knownField(field.first) != KnownField::Invalid) { continue; } const auto fieldId(ConcreteTag::FieldType::fieldIdToString(field.first)); printFieldName(fieldId); printTagValue(field.second.value()); } } void printNativeFields(const Tag *tag) { switch (tag->type()) { case TagType::Id3v2Tag: printNativeFields(tag); break; case TagType::Mp4Tag: printNativeFields(tag); break; case TagType::MatroskaTag: printNativeFields(tag); break; case TagType::VorbisComment: case TagType::OggVorbisComment: printNativeFields(tag); break; default:; } } TimeSpanOutputFormat parseTimeSpanOutputFormat(const Argument &timeSpanFormatArg, TimeSpanOutputFormat defaultFormat) { if (timeSpanFormatArg.isPresent()) { const auto &val = timeSpanFormatArg.values().front(); if (!strcmp(val, "measures")) { return TimeSpanOutputFormat::WithMeasures; } else if (!strcmp(val, "colons")) { return TimeSpanOutputFormat::Normal; } else if (!strcmp(val, "seconds")) { return TimeSpanOutputFormat::TotalSeconds; } else { cerr << Phrases::Error << "The specified time span format \"" << val << "\" is invalid." << Phrases::End << "note: Valid formats are measures, colons and seconds." << endl; exit(-1); } } return defaultFormat; } TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage) { if (usageArg.isPresent()) { const auto &val = usageArg.values().front(); if (!strcmp(val, "never")) { return TagUsage::Never; } else if (!strcmp(val, "keepexisting")) { return TagUsage::KeepExisting; } else if (!strcmp(val, "always")) { return TagUsage::Always; } else { cerr << Phrases::Error << "The specified tag usage \"" << val << "\" is invalid." << Phrases::End << "note: Valid values are never, keepexisting and always." << endl; exit(-1); } } return defaultUsage; } TagTextEncoding parseEncodingDenotation(const Argument &encodingArg, TagTextEncoding defaultEncoding) { if (encodingArg.isPresent()) { const auto &val = encodingArg.values().front(); if (!strcmp(val, "utf8")) { return TagTextEncoding::Utf8; } else if (!strcmp(val, "latin1")) { return TagTextEncoding::Latin1; } else if (!strcmp(val, "utf16be")) { return TagTextEncoding::Utf16BigEndian; } else if (!strcmp(val, "utf16le")) { return TagTextEncoding::Utf16LittleEndian; } else if (!strcmp(val, "auto")) { } else { cerr << Phrases::Error << "The specified encoding \"" << val << "\" is invalid." << Phrases::End << "note: Valid encodings are utf8, latin1, utf16be, utf16le and auto." << endl; exit(-1); } } return defaultEncoding; } ElementPosition parsePositionDenotation(const Argument &posArg, const Argument &valueArg, ElementPosition defaultPos) { if (posArg.isPresent()) { const char *val = valueArg.values(0).front(); if (!strcmp(val, "front")) { return ElementPosition::BeforeData; } else if (!strcmp(val, "back")) { return ElementPosition::AfterData; } else if (!strcmp(val, "keep")) { return ElementPosition::Keep; } else { cerr << Phrases::Error << "The specified position \"" << val << "\" is invalid." << Phrases::End << "note: Valid positions are front, back and keep." << endl; exit(-1); } } return defaultPos; } std::uint64_t parseUInt64(const Argument &arg, std::uint64_t defaultValue) { if (arg.isPresent()) { try { if (*arg.values().front() == '0' && *(arg.values().front() + 1) == 'x') { return stringToNumber(arg.values().front() + 2, 16); } else { return stringToNumber(arg.values().front()); } } catch (const ConversionException &) { cerr << Phrases::Error << "The specified value \"" << arg.values().front() << "\" is no valid unsigned integer." << Phrases::EndFlush; exit(-1); } } return defaultValue; } TagTarget::IdContainerType parseIds(std::string_view concatenatedIds) { const auto splittedIds = splitStringSimple(concatenatedIds, ","); auto convertedIds = TagTarget::IdContainerType(); convertedIds.reserve(splittedIds.size()); for (const auto &id : splittedIds) { if (id.empty()) { continue; } try { convertedIds.push_back(stringToNumber(id)); } catch (const ConversionException &) { cerr << Phrases::Error << "The specified ID \"" << id << "\" is invalid." << Phrases::End << "note: IDs must be unsigned integers." << endl; exit(-1); } } return convertedIds; } bool applyTargetConfiguration(TagTarget &target, std::string_view configStr) { if (!configStr.empty()) { if (configStr.compare(0, 13, "target-level=") == 0) { try { target.setLevel(stringToNumber(configStr.substr(13))); } catch (const ConversionException &) { cerr << Phrases::Error << "The specified target level \"" << configStr.substr(13) << "\" is invalid." << Phrases::End << "note: The target level must be an unsigned integer." << endl; exit(-1); } } else if (configStr.compare(0, 17, "target-levelname=") == 0) { target.setLevelName(std::string(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(19)); } else if (configStr.compare(0, 13, "target-reset=") == 0) { if (*(configStr.data() + 13)) { cerr << Phrases::Error << "Invalid assignment " << (configStr.data() + 13) << " for target-reset." << Phrases::EndFlush; exit(-1); } target.clear(); } else if (configStr == "target-reset") { target.clear(); } else { return false; } return true; } else { return false; } } FieldDenotations parseFieldDenotations(const Argument &fieldsArg, bool readOnly) { auto fields = FieldDenotations(); if (!fieldsArg.isPresent()) { return fields; } const std::vector &fieldDenotations = fieldsArg.values(); auto scope = FieldScope(); for (std::string_view fieldDenotationString : fieldDenotations) { // check for tag or target specifier if (startsWith(fieldDenotationString, "tag=")) { const auto tagTypeString = fieldDenotationString.substr(4); if (tagTypeString.empty()) { cerr << Phrases::Error << "The \"tag\"-specifier has been used with no value(s)." << Phrases::End << "note: Possible values are id3,id3v1,id3v2,itunes,vorbis,matroska and all." << endl; std::exit(-1); } auto tagType = TagType::Unspecified; for (const auto &part : splitStringSimple>(tagTypeString, ",")) { if (part.empty()) { continue; } 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 | TagType::OggVorbisComment; } else if (part == "matroska") { tagType |= TagType::MatroskaTag; } else if (part == "all" || part == "any") { tagType = TagType::Unspecified; break; } else { cerr << Phrases::Error << "The value \"" << part << " for the \"tag\"-specifier is invalid." << Phrases::End << "note: Possible values are id3,id3v1,id3v2,itunes,vorbis,matroska and all." << endl; std::exit(-1); } } scope.tagType = tagType; scope.allTracks = false; scope.trackIds.clear(); continue; } else if (applyTargetConfiguration(scope.tagTarget, fieldDenotationString)) { continue; } else if (fieldDenotationString == "target-matching=exact") { scope.exactTargetMatching = true; continue; } else if (fieldDenotationString == "target-matching=relaxed") { scope.exactTargetMatching = false; continue; } else if (startsWith(fieldDenotationString, "track-id=")) { const auto parts = splitStringSimple>(fieldDenotationString.substr(9), ","); auto allTracks = false; auto trackIds = vector(); trackIds.reserve(parts.size()); for (const auto &part : parts) { if (part.empty()) { continue; } if (part == "all" || part == "any") { allTracks = true; break; } try { trackIds.emplace_back(stringToNumber(part)); } catch (const ConversionException &) { cerr << Phrases::Error << "The value provided with the \"track\"-specifier is invalid." << Phrases::End << "note: It must be a comma-separated list of track IDs." << endl; std::exit(-1); } } scope.allTracks = allTracks; scope.trackIds = std::move(trackIds); continue; } // check whether field name starts with + indicating an additional value auto additionalValue = !fieldDenotationString.empty() && fieldDenotationString.front() == '+'; if (additionalValue) { fieldDenotationString = fieldDenotationString.substr(1); } // read field name const auto equationPos = fieldDenotationString.find('='); auto fieldNameLen = equationPos != std::string_view::npos ? equationPos : fieldDenotationString.size(); // field name might denote increment ("+") or path disclosure (">") auto type = DenotationType::Normal; if (fieldNameLen && equationPos != std::string_view::npos && equationPos) { switch (fieldDenotationString[equationPos - 1]) { case '+': type = DenotationType::Increment; --fieldNameLen; break; case '>': type = DenotationType::File; --fieldNameLen; break; default:; } } // field name might specify a file index auto fileIndex = 0u, mult = 1u; for (auto digitPos = fieldNameLen - 1; fieldNameLen && isDigit(fieldDenotationString[digitPos]); --fieldNameLen, --digitPos, mult *= 10) { fileIndex += static_cast(fieldDenotationString[digitPos] - '0') * mult; } if (!fieldNameLen) { cerr << Phrases::Error << "The field denotation \"" << fieldDenotationString << "\" has no field name." << Phrases::EndFlush; exit(-1); } // parse the denoted field ID const auto fieldName = fieldDenotationString.substr(0, fieldNameLen); try { if (scope.isTrack()) { scope.field = FieldId::fromTrackDenotation(fieldName); } else { scope.field = FieldId::fromTagDenotation(fieldName); } } catch (const ConversionException &e) { // unable to parse field ID denotation -> discard the field denotation cerr << Phrases::Error << "The field denotation \"" << fieldName << "\" could not be parsed: " << e.what() << Phrases::EndFlush; std::exit(-1); } // read cover always from file if (scope.field.knownField() == KnownField::Cover) { type = DenotationType::File; } // add field denotation scope auto &fieldValues = fields[scope]; // add value to the scope (if present) if (equationPos != std::string_view::npos) { if (readOnly) { cerr << Phrases::Error << "A value has been specified for \"" << fieldName << "\"." << Phrases::End << "note: This is only possible when the \"set\"-operation is used." << endl; std::exit(-1); } else { // file index might have been specified explicitly // 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.allValues.emplace_back(type, mult == 1 ? (fieldValues.allValues.empty() ? 0 : fieldValues.allValues.back().fileIndex + (additionalValue ? 0 : 1)) : fileIndex, fieldDenotationString.substr(equationPos + 1)); } } if (additionalValue && readOnly) { cerr << Phrases::Error << "Indication of an additional value for \"" << fieldName << "\" is invalid." << Phrases::End << "note: This is only possible when the \"set\"-operation is used." << endl; std::exit(-1); } } return fields; } template static std::pair, bool> valuesForNativeField(std::string_view idString, const Tag *tag, TagType tagType) { auto res = make_pair, bool>({}, false); if (!(tagType & tagTypeMask)) { return res; } res.first = static_cast(tag)->values(ConcreteTag::FieldType::fieldIdFromString(idString)); res.second = true; return res; } template static bool setValuesForNativeField(std::string_view idString, Tag *tag, TagType tagType, const std::vector &values) { if (!(tagType & tagTypeMask)) { return false; } return static_cast(tag)->setValues(ConcreteTag::FieldType::fieldIdFromString(idString), values); } template static KnownField knownFieldForNativeField(std::string_view idString, const Tag *tag, TagType tagType) { if (!(tagType & tagTypeMask)) { return KnownField::Invalid; } try { return static_cast(tag)->knownField(ConcreteTag::FieldType::fieldIdFromString(idString)); } catch (const ConversionException &) { return KnownField::Invalid; } } inline FieldId::FieldId(std::string_view nativeField, GetValuesForNativeFieldType &&valuesForNativeField, SetValuesForNativeFieldType &&setValuesForNativeField, KnownFieldForNativeFieldType &&knownFieldForNativeField) : m_knownField(KnownField::Invalid) , m_nativeField(nativeField) , m_valuesForNativeField(std::move(valuesForNativeField)) , m_setValuesForNativeField(std::move(setValuesForNativeField)) , m_knownFieldForNativeField(std::move(knownFieldForNativeField)) { } /// \remarks This wrapper is required because specifying c'tor template args is not possible. template FieldId FieldId::fromNativeField(std::string_view nativeFieldId) { return FieldId(nativeFieldId, std::bind(&valuesForNativeField, nativeFieldId, _1, _2), std::bind(&setValuesForNativeField, nativeFieldId, _1, _2, _3), std::bind(&knownFieldForNativeField, nativeFieldId, _1, _2)); } FieldId FieldId::fromTagDenotation(std::string_view denotation) { // check for native, format-specific denotation if (startsWith(denotation, "mkv:")) { return FieldId::fromNativeField(denotation.substr(4)); } else if (startsWith(denotation, "mp4:")) { return FieldId::fromNativeField(denotation.substr(4)); } else if (startsWith(denotation, "vorbis:")) { return FieldId::fromNativeField(denotation.substr(7)); } else if (startsWith(denotation, "id3:")) { return FieldId::fromNativeField(denotation.substr(4)); } else if (startsWith(denotation, "generic:")) { // allow prefix 'generic:' for consistency denotation = denotation.substr(8); } // determine KnownField for generic denotation const auto knownField(FieldMapping::knownField(denotation.data(), denotation.size())); if (knownField == KnownField::Invalid) { throw ConversionException("generic field name is unknown"); } return FieldId(knownField, nullptr, 0); } FieldId FieldId::fromTrackDenotation(std::string_view denotation) { return FieldId(KnownField::Invalid, denotation.data(), denotation.size()); } std::pair, bool> FieldId::values(const Tag *tag, TagType tagType) const { auto res = make_pair, bool>({}, false); if (!m_nativeField.empty()) { res = m_valuesForNativeField(tag, tagType); } else { res.first = tag->values(m_knownField); res.second = true; } return res; } bool FieldId::setValues(Tag *tag, TagType tagType, const std::vector &values) const { if (!m_nativeField.empty()) { return m_setValuesForNativeField(tag, tagType, values); } else { return tag->setValues(m_knownField, values); } } KnownField FieldId::knownFieldForTag(const Tag *tag, TagType tagType) const { if (!m_nativeField.empty()) { return m_knownFieldForNativeField(tag, tagType); } else { return m_knownField; } } string tagName(const Tag *tag) { stringstream ss; const TagType tagType = tag->type(); // write tag name and target, eg. MP4/iTunes tag ss << tag->typeName(); if (tagType == TagType::Id3v2Tag) { // version only interesting for ID3v2 tags? ss << " (version " << tag->version() << ')'; } if (tagType == TagType::MatroskaTag || !tag->target().isEmpty()) { ss << " targeting \"" << tag->targetString() << '\"'; } return ss.str(); } bool stringToBool(const string &str) { if (str == "yes" || str == "true" || str == "on" || str == "1") { return true; } else if (str == "no" || str == "false" || str == "off" || str == "0") { return false; } throw ConversionException(argsToString('\"', str, "\" is not yes or no")); } bool logLineFinalized = true; static string lastStep; void logNextStep(const AbortableProgressFeedback &progress) { // finalize previous step if (!logLineFinalized) { cout << "\r - [100%] " << lastStep << endl; logLineFinalized = true; } // print line for next step lastStep = progress.step(); cout << "\r - [" << setw(3) << static_cast(progress.stepPercentage()) << "%] " << lastStep << flush; logLineFinalized = false; } void logStepPercentage(const TagParser::AbortableProgressFeedback &progress) { cout << "\r - [" << setw(3) << static_cast(progress.stepPercentage()) << "%] " << lastStep << flush; } void finalizeLog() { if (logLineFinalized) { return; } cout << "\r - [100%] " << lastStep << '\n'; logLineFinalized = true; lastStep.clear(); } } // namespace Cli