#include "./mainfeatures.h" #include "../application/knownfieldmodel.h" #include "../misc/utility.h" #include "../misc/htmlinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace ApplicationUtilities; using namespace ConversionUtilities; using namespace ChronoUtilities; using namespace EscapeCodes; using namespace Utility; using namespace Settings; using namespace Media; namespace Cli { enum class DenotationType { Normal, Increment, File }; inline TagType operator| (TagType lhs, TagType rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } inline TagType &operator|= (TagType &lhs, TagType rhs) { return lhs = static_cast(static_cast(lhs) | static_cast(rhs)); } struct FieldDenotation { FieldDenotation(KnownField field); KnownField field; DenotationType type; TagType tagType; TagTarget tagTarget; std::vector > values; }; FieldDenotation::FieldDenotation(KnownField field) : field(field), type(DenotationType::Normal), tagType(TagType::Unspecified) {} inline bool isDigit(char c) { return c >= '0' && c <= '9'; } QString incremented(const QString &str, unsigned int toIncrement = 1) { QString res; res.reserve(str.size()); unsigned int value = 0; bool hasValue = false; for(const QChar &c : str) { if(toIncrement && c.isDigit()) { value = value * 10 + static_cast(c.digitValue()); hasValue = true; } else { if(hasValue) { res.append(QString::number(value + 1)); hasValue = false; --toIncrement; } res.append(c); } } return res; } void printNotifications(NotificationList ¬ifications, const char *head = nullptr, bool beVerbose = false) { if(!beVerbose) { for(const auto ¬ification : notifications) { switch(notification.type()) { case NotificationType::Debug: case NotificationType::Information: break; default: goto printNotifications; } } return; } if(!notifications.empty()) { printNotifications: if(head) { cout << head << endl; } Notification::sortByTime(notifications); for(const auto ¬ification : notifications) { switch(notification.type()) { case NotificationType::Debug: if(beVerbose) { cout << "Debug "; break; } else { continue; } case NotificationType::Information: if(beVerbose) { cout << "Information "; break; } else { continue; } case NotificationType::Warning: cout << "Warning "; break; case NotificationType::Critical: cout << "Error "; break; default: ; } 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); } void printFieldNames(const StringVector ¶meterValues) { CMD_UTILS_START_CONSOLE; VAR_UNUSED(parameterValues) cout << "title album artist genre year comment bpm bps lyricist track disk part totalparts encoder\n" "recorddate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" "recordlabel cover composer rating description" << endl; } void removeBackupFiles(const StringVector ¶meterValues, const Argument &recursiveArg) { CMD_UTILS_START_CONSOLE; QDir dir(QString::fromStdString(parameterValues.at(0))); QStringList affectedFiles; int filesFound = Utility::removeBackupFiles(dir, affectedFiles, &cout, recursiveArg.isPresent()); cout << affectedFiles.size() << " of " << filesFound << " backup files have been removed." << endl; } TagUsage parseUsageDenotation(const Argument &usageArg, TagUsage defaultUsage) { if(usageArg.isPresent()) { const auto &val = usageArg.values().front(); if(val == "never") { return TagUsage::Never; } else if(val == "keepexisting") { return TagUsage::KeepExisting; } else if(val == "always") { return TagUsage::Always; } else { cout << "Warning: The specified tag usage \"" << val << "\" is invalid and will be ignored." << endl; } } return defaultUsage; } TagTextEncoding parseEncodingDenotation(const Argument &encodingArg, TagTextEncoding defaultEncoding) { if(encodingArg.isPresent()) { const auto &val = encodingArg.values().front(); if(val == "utf8") { return TagTextEncoding::Utf8; } else if(val == "latin1") { return TagTextEncoding::Latin1; } else if(val == "utf16be") { return TagTextEncoding::Utf16BigEndian; } else if(val == "utf16le") { return TagTextEncoding::Utf16LittleEndian; } else if(val == "auto") { } else { cout << "Warning: The specified encoding \"" << val << "\" is invalid and will be ignored." << endl; } } return defaultEncoding; } ElementPosition parsePositionDenotation(const Argument &posArg, ElementPosition defaultPos) { if(posArg.isPresent()) { const auto &val = posArg.values().front(); if(val == "front") { return ElementPosition::BeforeData; } else if(val == "back") { return ElementPosition::AfterData; } else if(val == "keep") { return ElementPosition::Keep; } else { cout << "Warning: The specified position \"" << val << "\" is invalid and will be ignored." << endl; } } return defaultPos; } uint64 parseUInt64(const Argument &arg, uint64 defaultValue) { if(arg.isPresent()) { try { if(startsWith(arg.values().front(), "0x")) { return stringToNumber(arg.values().front().substr(2), 16); } else { return stringToNumber(arg.values().front()); } } catch(const ConversionException &) { cout << "Warning: The specified value \"" << arg.values().front() << "\" is no valid unsigned integer and will be ignored." << endl; } } return defaultValue; } 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(id)); } catch(const ConversionException &) { cout << "Warning: The specified ID \"" << id << "\" is invalid and will be ignored." << endl; } } 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(configStr.substr(13))); } catch (const ConversionException &) { cout << "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, 17, "target-attachments=") == 0) { target.attachments() = parseIds(configStr.substr(17)); } else if(configStr == "target-reset") { target.clear(); } else { return false; } return true; } else { return false; } } vector parseFieldDenotations(const StringVector &fieldDenotations, bool readOnly) { vector fields; fields.reserve(fieldDenotations.size()); TagType currentTagType = TagType::Unspecified; TagTarget currentTagTarget; for(const string &fieldDenotationString : fieldDenotations) { // check for tag or target specifier if(strncmp(fieldDenotationString.c_str(), "tag:", 4) == 0) { if(fieldDenotationString.size() == 4) { cout << "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.substr(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 { cout << "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 = currentTagType; break; } } currentTagType = tagType; break; } } else if(applyTargetConfiguration(currentTagTarget, fieldDenotationString)) { continue; } // read field name auto equationPos = fieldDenotationString.find('='); auto fieldName = equationPos != string::npos ? fieldDenotationString.substr(0, equationPos) : fieldDenotationString; // field name might denote increment ("+") or path disclosure (">") auto fieldNamePos = fieldName.size(); DenotationType type = DenotationType::Normal; if(fieldNamePos) { switch(fieldName.at(--fieldNamePos)) { case '+': type = DenotationType::Increment; --fieldNamePos; break; case '>': type = DenotationType::File; --fieldNamePos; break; default: ; } } // field name might specify a file index unsigned int fileIndex = 0, mult = 1; for(; fieldNamePos != static_cast(-1) && isDigit(fieldName.at(fieldNamePos)); --fieldNamePos, mult *= 10) { fileIndex += static_cast(fieldName.at(fieldNamePos) - '0') * mult; } if(fieldNamePos == static_cast(-1)) { cout << "Warning: Ignoring field denotation \"" << fieldDenotationString << "\" because no field name has been specified." << endl; continue; } else if(++fieldNamePos < fieldName.size()) { fieldName = fieldName.substr(0, fieldNamePos); } // parse the denoted filed KnownField field; if(fieldName == "title") { field = KnownField::Title; } else if(fieldName == "album") { field = KnownField::Album; } else if(fieldName == "artist") { field = KnownField::Artist; } else if(fieldName == "genre") { field = KnownField::Genre; } else if(fieldName == "year") { field = KnownField::Year; } else if(fieldName == "comment") { field = KnownField::Comment; } else if(fieldName == "bpm") { field = KnownField::Bpm; } else if(fieldName == "bps") { field = KnownField::Bps; } else if(fieldName == "lyricist") { field = KnownField::Lyricist; } else if(fieldName == "track") { field = KnownField::TrackPosition; } else if(fieldName == "disk") { field = KnownField::DiskPosition; } else if(fieldName == "part") { field = KnownField::PartNumber; } else if(fieldName == "totalparts") { field = KnownField::TotalParts; } else if(fieldName == "encoder") { field = KnownField::Encoder; } else if(fieldName == "recorddate") { field = KnownField::RecordDate; } else if(fieldName == "performers") { field = KnownField::Performers; } else if(fieldName == "duration") { field = KnownField::Length; } else if(fieldName == "language") { field = KnownField::Language; } else if(fieldName == "encodersettings") { field = KnownField::EncoderSettings; } else if(fieldName == "lyrics") { field = KnownField::Lyrics; } else if(fieldName == "synchronizedlyrics") { field = KnownField::SynchronizedLyrics; } else if(fieldName == "grouping") { field = KnownField::Grouping; } else if(fieldName == "recordlabel") { field = KnownField::RecordLabel; } else if(fieldName == "cover") { field = KnownField::Cover; type = DenotationType::File; // read cover always from file } else if(fieldName == "composer") { field = KnownField::Composer; } else if(fieldName == "rating") { field = KnownField::Rating; } else if(fieldName == "description") { field = KnownField::Description; } else { // no "KnownField" value matching -> discard the field denotation cout << "The field name \"" << fieldName << "\" is unknown and will be ingored." << endl; continue; } // add field denotation with parsed values fields.emplace_back(field); FieldDenotation &fieldDenotation = fields.back(); fieldDenotation.type = type; fieldDenotation.tagType = currentTagType; fieldDenotation.tagTarget = currentTagTarget; if(equationPos != string::npos) { if(readOnly) { cout << "Warning: Specified value for \"" << fieldName << "\" will be ignored." << endl; } else { fieldDenotation.values.emplace_back(make_pair(mult == 1 ? fieldDenotation.values.size() : fileIndex, QString::fromLocal8Bit(fieldDenotationString.data() + equationPos + 1))); } } } return fields; } enum class AttachmentAction { Add, UpdateById, UpdateByName, RemoveById, RemoveByName }; class AttachmentInfo { public: AttachmentInfo(); void apply(AbstractContainer *container); void apply(AbstractAttachment *attachment); void reset(); bool next(AbstractContainer *container); AttachmentAction action; uint64 id; string path; string name; string mime; string desc; }; AttachmentInfo::AttachmentInfo() : action(AttachmentAction::Add), id(0) {} 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.empty() || name.empty()) { container->addNotification(NotificationType::Critical, "No name or path specified for new attachment to be added.", context); return; } apply(container->createAttachment()); break; case AttachmentAction::UpdateById: for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) { attachment = container->attachment(i); if(attachment->id() == id) { apply(attachment); attachmentFound = true; } } if(!attachmentFound == true) { container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be updated.", context); } break; case AttachmentAction::UpdateByName: for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) { attachment = container->attachment(i); if(attachment->name() == name) { apply(attachment); attachmentFound = true; } } if(!attachmentFound == true) { container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + name + "\" does not exist and hence can't be updated.", context); } break; case AttachmentAction::RemoveById: for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) { attachment = container->attachment(i); if(attachment->id() == id) { attachment->setIgnored(true); attachmentFound = true; } } if(!attachmentFound == true) { container->addNotification(NotificationType::Critical, "Attachment with the specified ID \"" + numberToString(id) + "\" does not exist and hence can't be removed.", context); } break; case AttachmentAction::RemoveByName: for(size_t i = 0, count = container->attachmentCount(); i < count; ++i) { attachment = container->attachment(i); if(attachment->name() == name) { attachment->setIgnored(true); attachmentFound = true; } } if(!attachmentFound == true) { container->addNotification(NotificationType::Critical, "Attachment with the specified name \"" + name + "\" does not exist and hence can't be removed.", context); } break; } } void AttachmentInfo::apply(AbstractAttachment *attachment) { if(id) { attachment->setId(id); } if(!path.empty()) { attachment->setFile(path); } if(!name.empty()) { attachment->setName(name); } if(!mime.empty()) { attachment->setMimeType(mime); } if(!desc.empty()) { attachment->setDescription(desc); } } void AttachmentInfo::reset() { action = AttachmentAction::Add; id = 0; path.clear(); name.clear(); mime.clear(); desc.clear(); } bool AttachmentInfo::next(AbstractContainer *container) { if(!id && path.empty() && name.empty() && mime.empty() && desc.empty()) { // skip empty attachment infos return false; } apply(container); reset(); return true; } void generateFileInfo(const StringVector ¶meterValues, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg) { CMD_UTILS_START_CONSOLE; VAR_UNUSED(parameterValues) try { // parse tags MediaFileInfo inputFileInfo(inputFileArg.values().front()); inputFileInfo.setForceFullParse(validateArg.isPresent()); inputFileInfo.open(true); inputFileInfo.parseEverything(); cout << "Saving file info of \"" << inputFileArg.values().front() << "\" ..." << endl; NotificationList origNotify; QFile file(QString::fromLocal8Bit(outputFileArg.values().front().c_str())); 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 { cout << "Error: An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << endl; } } catch(ios_base::failure &) { cout << "Error: An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; } catch(ApplicationUtilities::Failure &) { cout << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; } } void printProperty(const char *propName, const char *value, const char *suffix = nullptr, size_t intention = 4) { if(*value) { for(; intention; --intention) { cout << ' '; } cout << propName; for(intention = strlen(propName); intention < 30; ++intention) { cout << ' '; } cout << value; if(suffix) { cout << ' ' << suffix; } cout << endl; } } void printProperty(const char *propName, const string &value, const char *suffix = nullptr, size_t intention = 4) { printProperty(propName, value.data(), suffix, intention); } void printProperty(const char *propName, TimeSpan timeSpan, const char *suffix = nullptr, size_t intention = 4) { if(!timeSpan.isNull()) { printProperty(propName, timeSpan.toString(TimeSpanOutputFormat::WithMeasures), suffix, intention); } } void printProperty(const char *propName, DateTime dateTime, const char *suffix = nullptr, size_t intention = 4) { if(!dateTime.isNull()) { printProperty(propName, dateTime.toString(), suffix, intention); } } template void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, size_t intention = 4) { if(value != 0 || force) { printProperty(propName, numberToString(value), suffix, intention); } } void displayFileInfo(const StringVector &, const Argument &filesArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; if(!filesArg.valueCount()) { cout << "Error: No files have been specified." << endl; return; } MediaFileInfo fileInfo; for(const auto &file : filesArg.values()) { try { // parse tags fileInfo.setPath(file); fileInfo.open(true); fileInfo.parseTracks(); fileInfo.parseAttachments(); fileInfo.parseChapters(); cout << "Technical information for \"" << file << "\":" << endl; cout << " Container format: " << fileInfo.containerFormatName() << endl; { 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()); } if(fileInfo.paddingSize()) { printProperty("Padding", dataSizeToString(fileInfo.paddingSize())); } } { // 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()); 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("Sample count", track->sampleCount()); printProperty("Creation time", track->creationTime()); printProperty("Modification time", track->modificationTime()); cout << endl; } } else { cout << " File has no (supported) tracks." << endl; } } { // attachments const auto attachments = fileInfo.attachments(); if(!attachments.empty()) { for(const auto *attachment : attachments) { printProperty("ID", attachment->id()); printProperty("Name", attachment->name()); printProperty("MIME-type", attachment->mimeType()); printProperty("Label", attachment->label()); printProperty("Description", attachment->description()); if(attachment->data()) { printProperty("Size", dataSizeToString(attachment->data()->size(), true)); } cout << endl; } } } { // chapters const auto chapters = fileInfo.chapters(); if(!chapters.empty()) { for(const auto *chapter : chapters) { printProperty("ID", chapter->id()); if(!chapter->names().empty()) { printProperty("Name", static_cast(chapter->names().front())); } if(!chapter->startTime().isNull()) { printProperty("Start time", chapter->startTime().toString()); } if(!chapter->endTime().isNull()) { printProperty("End time", chapter->endTime().toString()); } cout << endl; } } } } catch(ios_base::failure &) { cout << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl; } catch(ApplicationUtilities::Failure &) { cout << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl; } printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent()); cout << endl; } } void displayTagInfo(const StringVector ¶meterValues, const Argument &filesArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; if(!filesArg.valueCount()) { cout << "Error: No files have been specified." << endl; return; } const auto fields = parseFieldDenotations(parameterValues, true); MediaFileInfo fileInfo; for(const auto &file : filesArg.values()) { try { // parse tags fileInfo.setPath(file); fileInfo.open(true); fileInfo.parseTags(); cout << "Tag information for \"" << file << "\":" << endl; const auto tags = fileInfo.tags(); if(tags.size()) { // iterate through all tags for(const auto *tag : tags) { // determine tag type TagType tagType = tag->type(); // write tag name and target, eg. MP4/iTunes tag cout << tag->typeName(); if(!tag->target().isEmpty()) { cout << " targeting \"" << tag->targetString() << "\""; } cout << endl; // iterate through fields specified by the user if(fields.empty()) { for(auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) { const auto &value = tag->value(field); if(!value.isEmpty()) { // write field name const char *fieldName = KnownFieldModel::fieldName(field); cout << ' ' << fieldName; // write padding for(auto i = strlen(fieldName); i < 18; ++i) { cout << ' '; } // write value try { const auto textValue = tagValueToQString(value); if(textValue.isEmpty()) { cout << "can't display here (see --extract)"; } else { cout << textValue.toLocal8Bit().data(); } } catch(ConversionException &) { cout << "conversion error"; } cout << endl; } } } else { for(const FieldDenotation &fieldDenotation : fields) { const auto &value = tag->value(fieldDenotation.field); if(fieldDenotation.tagType == TagType::Unspecified || (fieldDenotation.tagType | tagType) != TagType::Unspecified) { // write field name const char *fieldName = KnownFieldModel::fieldName(fieldDenotation.field); cout << ' ' << fieldName; // write padding for(auto i = strlen(fieldName); i < 18; ++i) { cout << ' '; } // write value if(value.isEmpty()) { cout << "none"; } else { try { const auto textValue = tagValueToQString(value); if(textValue.isEmpty()) { cout << "can't display here (see --extract)"; } else { cout << textValue.toLocal8Bit().data(); } } catch(ConversionException &) { cout << "conversion error"; } } cout << endl; } } } } } else { cout << " File has no (supported) tag information." << endl; } } catch(ios_base::failure &) { cout << "Error: An IO failure occured when reading the file \"" << file << "\"." << endl; } catch(ApplicationUtilities::Failure &) { cout << "Error: A parsing failure occured when reading the file \"" << file << "\"." << endl; } printNotifications(fileInfo, "Parsing notifications:", verboseArg.isPresent()); cout << endl; } } void setTagInfo(const StringVector ¶meterValues, const SetTagInfoArgs &args) { CMD_UTILS_START_CONSOLE; if(!args.setTagInfoArg.valueCount()) { cout << "Error: No files have been specified." << endl; return; } auto fields = parseFieldDenotations(parameterValues, false); if(fields.empty() && args.attachmentsArg.values().empty() && args.docTitleArg.values().empty()) { cout << "Error: No fields/attachments have been specified." << endl; return; } // determine required targets vector requiredTargets; for(const FieldDenotation &fieldDenotation : fields) { if(find(requiredTargets.cbegin(), requiredTargets.cend(), fieldDenotation.tagTarget) == requiredTargets.cend()) { requiredTargets.push_back(fieldDenotation.tagTarget); } } // determine targets to remove vector targetsToRemove; targetsToRemove.emplace_back(); bool validRemoveTargetsSpecified = false; for(const auto &targetDenotation : args.removeTargetsArg.values()) { if(targetDenotation == ",") { if(validRemoveTargetsSpecified) { targetsToRemove.emplace_back(); } } else if(applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) { validRemoveTargetsSpecified = true; } else { cout << "Warning: The given target specification \"" << targetDenotation << "\" is invalid and will be ignored." << endl; } } // parse other settings uint32 id3v2Version = 3; if(args.id3v2VersionArg.isPresent()) { try { id3v2Version = stringToNumber(args.id3v2VersionArg.values().front()); if(id3v2Version < 1 || id3v2Version > 4) { throw ConversionException(); } } catch (ConversionException &) { id3v2Version = 3; cout << "Warning: The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid and will be ingored." << endl; } } 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, ElementPosition::BeforeData)); fileInfo.setForceTagPosition(args.forceTagPosArg.isPresent()); fileInfo.setIndexPosition(parsePositionDenotation(args.indexPosArg, ElementPosition::BeforeData)); fileInfo.setForceIndexPosition(args.forceIndexPosArg.isPresent()); fileInfo.setForceRewrite(args.forceRewriteArg.isPresent()); // iterate through all specified files unsigned int fileIndex = 0; static const string context("setting tags"); NotificationList notifications; for(const auto &file : args.filesArg.values()) { try { // parse tags cout << "Setting tag information for \"" << file << "\" ..." << endl; notifications.clear(); fileInfo.setPath(file); fileInfo.parseTags(); fileInfo.parseTracks(); vector 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 fileInfo.createAppropriateTags(args.treatUnknownFilesAsMp3FilesArg.isPresent(), id3v1Usage, id3v2Usage, args.mergeMultipleSuccessiveTagsArg.isPresent(), !args.id3v2VersionArg.isPresent(), id3v2Version, requiredTargets); auto container = fileInfo.container(); bool docTitleModified = false; if(!args.docTitleArg.values().empty()) { if(container && container->supportsTitle()) { size_t segmentIndex = 0, segmentCount = container->titles().size(); for(const auto &newTitle : args.docTitleArg.values()) { if(segmentIndex < segmentCount) { container->setTitle(newTitle, segmentIndex); docTitleModified = true; } else { cout << "Warning: The specified document title \"" << newTitle << "\" can not be set because the file has not that many segments." << endl; } ++segmentIndex; } } else { cout << "Warning: Setting the document title is not supported for the file." << endl; } } fileInfo.tags(tags); if(!tags.empty()) { // iterate through all tags for(auto *tag : tags) { if(args.removeOtherFieldsArg.isPresent()) { tag->removeAllFields(); } auto tagType = tag->type(); bool targetSupported = tag->supportsTarget(); auto tagTarget = tag->target(); for(FieldDenotation &fieldDenotation : fields) { if((fieldDenotation.tagType == TagType::Unspecified || (fieldDenotation.tagType | tagType) != TagType::Unspecified) && (!targetSupported || fieldDenotation.tagTarget == tagTarget)) { pair *selectedDenotatedValue = nullptr; for(auto &someDenotatedValue : fieldDenotation.values) { if(someDenotatedValue.first <= fileIndex) { if(!selectedDenotatedValue || (someDenotatedValue.first > selectedDenotatedValue->first)) { selectedDenotatedValue = &someDenotatedValue; } } } if(selectedDenotatedValue) { if(fieldDenotation.type == DenotationType::File) { if(selectedDenotatedValue->second.isEmpty()) { tag->setValue(fieldDenotation.field, TagValue()); } else { try { MediaFileInfo fileInfo(selectedDenotatedValue->second.toLocal8Bit().constData()); fileInfo.open(true); fileInfo.parseContainerFormat(); auto buff = make_unique(fileInfo.size()); fileInfo.stream().seekg(0); fileInfo.stream().read(buff.get(), fileInfo.size()); TagValue value(move(buff), fileInfo.size(), TagDataType::Picture); value.setMimeType(fileInfo.mimeType()); tag->setValue(fieldDenotation.field, move(value)); } catch (ios_base::failure &) { fileInfo.addNotification(NotificationType::Critical, "An IO error occured when parsing the specified cover file.", context); } catch (Media::Failure &) { fileInfo.addNotification(NotificationType::Critical, "Unable to parse specified cover file.", context); } } } else { TagTextEncoding usedEncoding = denotedEncoding; if(!tag->canEncodingBeUsed(denotedEncoding)) { usedEncoding = tag->proposedTextEncoding(); } tag->setValue(fieldDenotation.field, qstringToTagValue(selectedDenotatedValue->second, usedEncoding)); if(fieldDenotation.type == DenotationType::Increment && tag == tags.back()) { selectedDenotatedValue->second = incremented(selectedDenotatedValue->second); } } } } } } } else { fileInfo.addNotification(NotificationType::Critical, "Can not create appropriate tags for file.", context); } bool attachmentsModified = false; if(args.attachmentsArg.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 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 explicitely AttachmentInfo currentInfo; for(const auto &value : args.attachmentsArg.values()) { const auto *data = value.data(); if(value == ",") { attachmentsModified |= currentInfo.next(container); } else if(value == "add") { currentInfo.action = AttachmentAction::Add; } else if(value == "update-by-id") { currentInfo.action = AttachmentAction::UpdateById; } else if(value == "update-by-name") { currentInfo.action = AttachmentAction::UpdateByName; } else if(value == "remove-by-id") { currentInfo.action = AttachmentAction::RemoveById; } else if(value == "remove-by-name") { currentInfo.action = AttachmentAction::RemoveByName; } else if(!strncmp(data, "id=", 3)) { try { currentInfo.id = stringToNumber(data + 3); } catch(const ConversionException &) { container->addNotification(NotificationType::Warning, "The specified attachment ID \"" + string(data + 3) + "\" is invalid.", context); } } else if(!strncmp(data, "path=", 5)) { currentInfo.path = data + 5; } else if(!strncmp(data, "name=", 5)) { currentInfo.name = data + 5; } else if(!strncmp(data, "mime=", 5)) { currentInfo.mime = data + 5; } else if(!strncmp(data, "desc=", 5)) { currentInfo.desc = data + 5; } else { container->addNotification(NotificationType::Warning, "The attachment specification \"" + value + "\" is invalid and will be ignored.", context); } } 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 } } if(!tags.empty() || docTitleModified || attachmentsModified) { try { // save parsing notifications because notifications of sub objects like tags, tracks, ... will be gone after applying changes fileInfo.gatherRelatedNotifications(notifications); fileInfo.invalidateNotifications(); fileInfo.applyChanges(); fileInfo.gatherRelatedNotifications(notifications); cout << "Changes have been applied." << endl; } catch(const ApplicationUtilities::Failure &) { cout << "Error: Failed to apply changes." << endl; } } else { cout << "Warning: No changed to be applied." << endl; } } catch(const ios_base::failure &) { cout << "Error: An IO failure occured when reading/writing the file \"" << file << "\"." << endl; } catch(const ApplicationUtilities::Failure &) { cout << "Error: A parsing failure occured when reading/writing the file \"" << file << "\"." << endl; } printNotifications(notifications, "Notifications:", args.verboseArg.isPresent()); ++fileIndex; } } void extractField(const StringVector ¶meterValues, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; const auto fields = parseFieldDenotations(parameterValues, true); if(fields.size() != 1) { cout << "Error: Excactly one field needs to be specified." << endl; return; } MediaFileInfo inputFileInfo; try { // parse tags inputFileInfo.setPath(inputFileArg.values().front()); inputFileInfo.open(true); inputFileInfo.parseTags(); cout << "Extracting " << parameterValues.front() << " of \"" << inputFileArg.values().front() << "\" ..." << endl; auto tags = inputFileInfo.tags(); vector > values; // iterate through all tags for(const Tag *tag : tags) { for(const auto &fieldDenotation : fields) { const auto &value = tag->value(fieldDenotation.field); if(!value.isEmpty()) { values.emplace_back(&value, joinStrings({tag->typeName(), numberToString(values.size())}, "-")); } } } if(values.empty()) { cout << "File has no (supported) " << parameterValues.front() << " field." << endl; } else { 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(ios_base::failure &) { cout << "An IO error occured when writing the file \"" << path << "\"." << endl; } } } } catch(ios_base::failure &) { cout << "Error: An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; } catch(ApplicationUtilities::Failure &) { cout << "Error: A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << endl; } printNotifications(inputFileInfo, "Parsing notifications:", verboseArg.isPresent()); } }