#include "./mainfeatures.h" #include "./attachmentinfo.h" #include "./helper.h" #ifdef TAGEDITOR_JSON_EXPORT #include "./json.h" #endif #include "../application/knownfieldmodel.h" #if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK) #include "../misc/htmlinfo.h" #include "../misc/utility.h" #endif #include "resources/config.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef TAGEDITOR_JSON_EXPORT #include #endif #include #include #include #include #include #include #include #if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK) #include #include #endif #ifdef TAGEDITOR_JSON_EXPORT #include #include #include #endif #include #include #include #include #include #include using namespace std; using namespace CppUtilities; using namespace CppUtilities::EscapeCodes; using namespace Settings; using namespace TagParser; #if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK) using namespace QtUtilities; #endif namespace Cli { #define FIELD_NAMES \ "title album artist genre recorddate comment bpm bps lyricist track disk part totalparts encoder\n" \ " releasedate performers duration language encodersettings lyrics synchronizedlyrics grouping\n" \ " recordlabel cover composer rating description vendor albumartist" #define TRACK_ATTRIBUTE_NAMES "name tracknumber enabled=yes enabled=no forced=yes forced=no default=yes default=no" #define TAG_MODIFIER "tag=id3v1 tag=id3v2 tag=id3 tag=itunes tag=vorbis tag=matroska tag=all" #define TRACK_MODIFIER "track-id= track=all" #define TARGET_MODIFIER \ "target-level target-levelname target-tracks target-tracks\n" \ " target-chapters target-editions target-attachments target-reset" const char *const fieldNames = FIELD_NAMES; const char *const fieldNamesForSet = TAG_MODIFIER " " FIELD_NAMES " " TRACK_MODIFIER " " TRACK_ATTRIBUTE_NAMES " " TARGET_MODIFIER; void printFieldNames(const ArgumentOccurrence &) { CMD_UTILS_START_CONSOLE; cout << "Field and track attribute names allow referring to a field or track attribute in a format-independent way.\n" " - Field names:\n " FIELD_NAMES "\n" " - Track attribute names: " TRACK_ATTRIBUTE_NAMES "\n\n" "Modifier specify to which tags and tracks the subsequent values should be applied.\n" " - Tag modifier: " TAG_MODIFIER "\n" " - Track modifier: track=id1,id2,id3,... track=all\n" " - Target modifier:\n " TARGET_MODIFIER "\n" "ID3v2 cover types:\n" << joinStrings, std::string>(id3v2CoverTypeNames(), "\n"sv, false, " - "sv, ""sv) << '\n' << flush; } void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg, const Argument &outputFileArg, const Argument &validateArg) { CMD_UTILS_START_CONSOLE; #if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK) try { // parse tags auto inputFileInfo = MediaFileInfo(std::string(inputFileArg.values().front())); inputFileInfo.setForceFullParse(validateArg.isPresent()); inputFileInfo.open(true); auto diag = Diagnostics(); auto progress = AbortableProgressFeedback(); // FIXME: actually use the progress object inputFileInfo.parseEverything(diag, progress); // generate and save info auto diagReparsing = Diagnostics(); (outputFileArg.isPresent() ? cout : cerr) << "Saving file info for \"" << inputFileArg.values().front() << "\" ..." << endl; if (!outputFileArg.isPresent()) { cout << HtmlInfo::generateInfo(inputFileInfo, diag, diagReparsing).data() << endl; return; } auto file = QFile(fromNativeFileName(outputFileArg.values().front())); if (file.open(QFile::WriteOnly) && file.write(HtmlInfo::generateInfo(inputFileInfo, diag, diagReparsing)) && file.flush()) { cout << "File information has been saved to \"" << outputFileArg.values().front() << "\"." << endl; } else { cerr << Phrases::Error << "An IO error occured when writing the file \"" << outputFileArg.values().front() << "\"." << Phrases::EndFlush; } } catch (const TagParser::Failure &) { cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::EndFlush; } catch (const std::ios_base::failure &) { cerr << Phrases::Error << "An IO failure occured when reading the file \"" << inputFileArg.values().front() << "\"." << Phrases::EndFlush; } #else CPP_UTILITIES_UNUSED(inputFileArg); CPP_UTILITIES_UNUSED(outputFileArg); CPP_UTILITIES_UNUSED(validateArg); cerr << Phrases::Error << "Generating HTML info is only available if built with Qt support." << Phrases::EndFlush; #endif } void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; // check whether files have been specified if (!filesArg.isPresent() || filesArg.values().empty()) { cerr << Phrases::Error << "No files have been specified." << Phrases::End; exit(-1); } MediaFileInfo fileInfo; for (const char *file : filesArg.values()) { Diagnostics diag; AbortableProgressFeedback progress; // FIXME: actually use the progress object try { // parse tags fileInfo.setPath(std::string(file)); fileInfo.open(true); fileInfo.parseContainerFormat(diag, progress); fileInfo.parseEverything(diag, progress); // print general/container-related info cout << "Technical information for \"" << file << "\":\n"; cout << " - " << TextAttribute::Bold << "Container format: " << fileInfo.containerFormatName() << Phrases::End; printProperty("Size", dataSizeToString(fileInfo.size())); if (const auto mimeType = fileInfo.mimeType(); !mimeType.empty()) { printProperty("Mime-type", mimeType); } const auto duration = fileInfo.duration(); if (!duration.isNull()) { printProperty("Duration", duration); printProperty("Overall avg. bitrate", bitrateToString(fileInfo.overallAverageBitrate())); } if (const auto container = fileInfo.container()) { size_t segmentIndex = 0; for (const auto &title : container->titles()) { if (segmentIndex) { printProperty("Title", title % " (segment " % ++segmentIndex + ")"); } else { ++segmentIndex; printProperty("Title", title); } } 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("Creation time", container->creationTime()); printProperty("Modification time", container->modificationTime()); printProperty("Tag position", container->determineTagPosition(diag)); printProperty("Index position", container->determineIndexPosition(diag)); } if (fileInfo.paddingSize()) { printProperty("Padding", dataSizeToString(fileInfo.paddingSize())); } // print tracks const auto tracks = fileInfo.tracks(); if (!tracks.empty()) { cout << " - " << TextAttribute::Bold << "Tracks: " << fileInfo.technicalSummary() << Phrases::End; for (const auto *track : tracks) { printProperty("ID", track->id(), nullptr, true); printProperty("Name", track->name()); printProperty("Type", track->mediaTypeName()); if (const auto &language = track->locale().fullOrSomeAbbreviatedName(); !language.empty()) { printProperty("Language", language); } const auto fmtName = track->formatName(), fmtAbbr = track->formatAbbreviation(); printProperty("Format", fmtName); if (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->pixelSize().isNull()) { printProperty("Pixel size", track->pixelSize().toString()); } if (!track->displaySize().isNull()) { printProperty("Display size", track->displaySize().toString()); } if (track->pixelAspectRatio().isValid()) { printProperty("Pixel Aspect Ratio", track->pixelAspectRatio().toString()); } if (const auto cc = track->channelConfigString(); !cc.empty()) { printProperty("Channel config", cc); } else { printProperty("Channel count", track->channelCount()); } if (const auto ecc = track->extensionChannelConfigString(); !ecc.empty()) { printProperty("Extension channel config", ecc); } if (track->bitrate() > 0.0) { printProperty("Bitrate", bitrateToString(track->bitrate())); } printProperty("Bits per sample", track->bitsPerSample()); printProperty("Sampling frequency", track->samplingFrequency(), "Hz"); printProperty("Extension sampling frequency", track->extensionSamplingFrequency(), "Hz"); printProperty(track->mediaType() == MediaType::Video ? "Frame count" : "Sample count", track->sampleCount()); printProperty("Creation time", track->creationTime()); printProperty("Modification time", track->modificationTime()); vector labels; labels.reserve(7); if (track->isInterlaced()) { labels.emplace_back("interlaced"); } if (!track->isEnabled()) { labels.emplace_back("disabled"); } if (track->isDefault()) { labels.emplace_back("default"); } if (track->isForced()) { labels.emplace_back("forced"); } if (track->hasLacing()) { labels.emplace_back("has lacing"); } if (track->isEncrypted()) { labels.emplace_back("encrypted"); } if (!labels.empty()) { printProperty("Labeled as", joinStrings(labels, ", ")); } cout << '\n'; } } else { cout << " - File has no (supported) tracks.\n"; } // print attachments const auto attachments = fileInfo.attachments(); if (!attachments.empty()) { cout << " - " << TextAttribute::Bold << "Attachments:" << TextAttribute::Reset << '\n'; for (const auto *attachment : attachments) { printProperty("ID", attachment->id()); printProperty("Name", attachment->name()); printProperty("MIME-type", attachment->mimeType()); printProperty("Description", attachment->description()); if (attachment->data()) { printProperty("Size", dataSizeToString(static_cast(attachment->data()->size()), true)); } cout << '\n'; } } // print chapters const auto chapters = fileInfo.chapters(); if (!chapters.empty()) { cout << " - " << TextAttribute::Bold << "Chapters:" << TextAttribute::Reset << '\n'; for (const auto *chapter : chapters) { printProperty("ID", chapter->id()); if (!chapter->names().empty()) { printProperty("Name", static_cast(chapter->names().front())); } if (!chapter->startTime().isNegative()) { printProperty("Start time", chapter->startTime().toString()); } if (!chapter->endTime().isNegative()) { printProperty("End time", chapter->endTime().toString()); } cout << '\n'; } } } catch (const TagParser::Failure &) { cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; } catch (const std::ios_base::failure &) { cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"" << Phrases::EndFlush; } printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent()); cout << endl; } } void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedArg, const Argument &filesArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; // check whether files have been specified if (!filesArg.isPresent() || filesArg.values().empty()) { cerr << Phrases::Error << "No files have been specified." << Phrases::End; exit(-1); } // parse specified fields const auto fields = parseFieldDenotations(fieldsArg, true); MediaFileInfo fileInfo; for (const char *file : filesArg.values()) { Diagnostics diag; AbortableProgressFeedback progress; // FIXME: actually use the progress object try { // parse tags fileInfo.setPath(std::string(file)); fileInfo.open(true); fileInfo.parseContainerFormat(diag, progress); fileInfo.parseTags(diag, progress); cout << "Tag information for \"" << file << "\":\n"; const auto tags = fileInfo.tags(); if (tags.empty()) { cout << " - File has no (supported) tag information.\n"; continue; } // iterate through all tags for (const auto *tag : tags) { // determine tag type const TagType tagType = tag->type(); // write tag name and target, eg. MP4/iTunes tag cout << " - " << TextAttribute::Bold << tagName(tag) << TextAttribute::Reset << '\n'; // iterate through fields specified by the user if (fields.empty()) { for (auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) { printField(FieldScope(field), tag, tagType, true); } if (showUnsupportedArg.isPresent()) { printNativeFields(tag); } } else { for (const auto &fieldDenotation : fields) { const FieldScope &denotedScope = fieldDenotation.first; if (denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType | tagType) != TagType::Unspecified) { printField(denotedScope, tag, tagType, false); } } } } } catch (const TagParser::Failure &) { cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; } catch (const std::ios_base::failure &) { cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; } printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent()); cout << endl; } } template bool fieldPredicate(CoverType coverType, const std::pair &pair) { return pair.second.isTypeInfoAssigned() ? (pair.second.typeInfo() == static_cast(coverType)) : (coverType == 0); } template static void setId3v2CoverValues(TagType *tag, std::vector> &&values) { auto &fields = tag->fields(); const auto id = tag->fieldId(KnownField::Cover); const auto range = fields.equal_range(id); const auto first = range.first; for (auto &[tagValue, coverType] : values) { // check whether there is already a tag value with the current index/type auto pair = find_if(first, range.second, std::bind(fieldPredicate, coverType, placeholders::_1)); if (pair != range.second) { // there is already a tag value with the current index/type // -> update this value pair->second.setValue(tagValue); // check whether there are more values with the current index/type assigned while ((pair = find_if(++pair, range.second, std::bind(fieldPredicate, coverType, placeholders::_1))) != range.second) { // -> remove these values as we only support one value of a type in the same tag pair->second.setValue(TagValue()); } } else if (!tagValue.isEmpty()) { using FieldType = typename TagType::FieldType; auto newField = FieldType(id, tagValue); newField.setTypeInfo(static_cast(coverType)); fields.insert(std::pair(id, std::move(newField))); } } } void setTagInfo(const SetTagInfoArgs &args) { CMD_UTILS_START_CONSOLE; // check whether files have been specified if (!args.filesArg.isPresent() || args.filesArg.values().empty()) { cerr << Phrases::Error << "No files have been specified." << Phrases::EndFlush; exit(-1); } if (args.outputFilesArg.isPresent() && args.outputFilesArg.values().size() != args.filesArg.values().size()) { cerr << Phrases::Error << "The number of output files does not match the number of input files." << Phrases::EndFlush; exit(-1); } // get output files auto &outputFiles = args.outputFilesArg.isPresent() ? args.outputFilesArg.values() : vector(); auto currentOutputFile = outputFiles.cbegin(), noMoreOutputFiles = outputFiles.cend(); // parse field denotations and check whether there's an operation to be done (changing fields or some other settings) 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()) && (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty()) && !args.id3v1UsageArg.isPresent() && !args.id3v2UsageArg.isPresent() && !args.id3v2VersionArg.isPresent()) { if (!args.layoutOnlyArg.isPresent()) { cerr << Phrases::Error << "No fields/attachments have been specified." << Phrases::End << "note: This is usually a mistake. Use --layout-only to prevent this error and apply file layout options only." << endl; exit(-1); } } else if (args.layoutOnlyArg.isPresent()) { cerr << Phrases::Error << "Fields/attachments and --layout-only have been specified." << Phrases::End << "note: Don't use --layout-only if you actually want to alter tag information or attachments." << endl; exit(-1); } TagCreationSettings settings; settings.flags = TagCreationFlags::None; // determine required targets for (const auto &fieldDenotation : fields) { const FieldScope &scope = fieldDenotation.first; if (!scope.isTrack() && find(settings.requiredTargets.cbegin(), settings.requiredTargets.cend(), scope.tagTarget) == settings.requiredTargets.cend()) { settings.requiredTargets.push_back(scope.tagTarget); } } // determine targets to remove vector 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(); if (!strcmp(targetDenotation, ",")) { if (validRemoveTargetsSpecified) { targetsToRemove.emplace_back(); } } else if (applyTargetConfiguration(targetsToRemove.back(), targetDenotation)) { validRemoveTargetsSpecified = true; } else { cerr << Phrases::Error << "The given target specification \"" << targetDenotation << "\" is invalid." << Phrases::EndFlush; exit(-1); } } } // parse ID3v2 version if (args.id3v2VersionArg.isPresent()) { try { settings.id3v2MajorVersion = stringToNumber(args.id3v2VersionArg.values().front()); if (settings.id3v2MajorVersion < 1 || settings.id3v2MajorVersion > 4) { throw ConversionException(); } } catch (const ConversionException &) { settings.id3v2MajorVersion = 3; cerr << Phrases::Error << "The specified ID3v2 version \"" << args.id3v2VersionArg.values().front() << "\" is invalid." << Phrases::End << "note: Valid versions are 1, 2, 3 and 4." << endl; exit(-1); } } // parse flags if (args.treatUnknownFilesAsMp3FilesArg.isPresent()) { settings.flags |= TagCreationFlags::TreatUnknownFilesAsMp3Files; } if (args.id3InitOnCreateArg.isPresent()) { settings.flags |= TagCreationFlags::Id3InitOnCreate; } if (args.id3TransferOnRemovalArg.isPresent()) { settings.flags |= TagCreationFlags::Id3TransferValuesOnRemoval; } if (args.mergeMultipleSuccessiveTagsArg.isPresent()) { settings.flags |= TagCreationFlags::MergeMultipleSuccessiveId3v2Tags; } if (!args.id3v2VersionArg.isPresent()) { settings.flags |= TagCreationFlags::KeepExistingId3v2Version; } // parse other settings const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8); settings.id3v1usage = parseUsageDenotation(args.id3v1UsageArg, TagUsage::KeepExisting); settings.id3v2usage = parseUsageDenotation(args.id3v2UsageArg, TagUsage::Always); // setup media file info 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)); fileInfo.setForceTagPosition(args.forceTagPosArg.isPresent()); fileInfo.setIndexPosition(parsePositionDenotation(args.indexPosArg, args.indexPosValueArg, ElementPosition::BeforeData)); fileInfo.setForceIndexPosition(args.forceIndexPosArg.isPresent()); fileInfo.setForceRewrite(args.forceRewriteArg.isPresent()); fileInfo.setWritingApplication(APP_NAME " v" APP_VERSION); // set backup path if (args.backupDirArg.isPresent()) { fileInfo.setBackupDirectory(std::string(args.backupDirArg.values().front())); } // iterate through all specified files unsigned int fileIndex = 0; static string context("setting tags"); for (const char *file : args.filesArg.values()) { Diagnostics diag; AbortableProgressFeedback parsingProgress; // FIXME: actually use the progress object try { // parse tags and tracks (tracks are relevent because track meta-data such as language can be changed as well) cout << TextAttribute::Bold << "Setting tag information for \"" << file << "\" ..." << Phrases::EndFlush; fileInfo.setPath(std::string(file)); fileInfo.parseContainerFormat(diag, parsingProgress); fileInfo.parseTags(diag, parsingProgress); fileInfo.parseTracks(diag, parsingProgress); fileInfo.parseAttachments(diag, parsingProgress); 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(settings); auto container = fileInfo.container(); if (args.docTitleArg.isPresent() && !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); } else { diag.emplace_back(DiagLevel::Warning, argsToString( "The specified document title \"", newTitle, "\" can not be set because the file has not that many segments."), context); } ++segmentIndex; } } else { diag.emplace_back(DiagLevel::Warning, "Setting the document title is not supported for the file.", context); } } // select the relevant values for the current file index for (auto &fieldDenotation : fields) { FieldValues &denotedValues = fieldDenotation.second; vector &relevantDenotedValues = denotedValues.relevantValues; denotedValues.relevantValues.clear(); unsigned int currentFileIndex = 0; for (FieldValue &denotatedValue : denotedValues.allValues) { if ((denotatedValue.fileIndex <= fileIndex) && (relevantDenotedValues.empty() || (denotatedValue.fileIndex >= currentFileIndex))) { if (currentFileIndex != denotatedValue.fileIndex) { currentFileIndex = denotatedValue.fileIndex; relevantDenotedValues.clear(); } relevantDenotedValues.push_back(&denotatedValue); } } } // alter tags fileInfo.tags(tags); if (tags.empty()) { diag.emplace_back(DiagLevel::Critical, "Can not create appropriate tags for file.", context); } else { // iterate through all tags for (auto *tag : tags) { // clear current values if option is present if (args.removeOtherFieldsArg.isPresent()) { 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(); // determine the encoding to store text values TagTextEncoding usedEncoding = denotedEncoding; if (!tag->canEncodingBeUsed(denotedEncoding)) { usedEncoding = tag->proposedTextEncoding(); if (args.encodingArg.isPresent()) { diag.emplace_back(DiagLevel::Warning, argsToString("Can't use specified encoding \"", args.encodingArg.values().front(), "\" in ", tagName(tag), " because the tag format/version doesn't support it."), context); } } // iterate through all denoted field values for (const auto &fieldDenotation : fields) { const FieldScope &denotedScope = fieldDenotation.first; // skip values which scope does not match the current tag if (denotedScope.isTrack() || !(denotedScope.tagType == TagType::Unspecified || (denotedScope.tagType & tagType)) || !(!targetSupported || denotedScope.tagTarget == tagTarget)) { continue; } // convert the values to TagValue auto convertedValues = std::vector(); auto convertedValuesWithCoverType = std::vector>(); for (const FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) { // assign an empty TagValue to remove the field if denoted value is empty if (relevantDenotedValue->value.empty()) { convertedValues.emplace_back(); continue; } // add text value if (relevantDenotedValue->type != DenotationType::File) { convertedValues.emplace_back(relevantDenotedValue->value, TagTextEncoding::Utf8, usedEncoding); continue; } // add value from file const auto parts = splitStringSimple>(relevantDenotedValue->value, ":", 3); const auto path = parts.empty() ? std::string_view() : parts.front(); try { // assume the file refers to a picture auto value = TagValue(); if (!path.empty()) { auto coverFileInfo = MediaFileInfo(path); auto coverDiag = Diagnostics(); auto coverProgress = AbortableProgressFeedback(); // FIXME: actually use the progress object coverFileInfo.open(true); coverFileInfo.parseContainerFormat(coverDiag, coverProgress); auto buff = make_unique(coverFileInfo.size()); coverFileInfo.stream().seekg(static_cast(coverFileInfo.containerOffset())); coverFileInfo.stream().read(buff.get(), static_cast(coverFileInfo.size())); value = TagValue(std::move(buff), coverFileInfo.size(), TagDataType::Picture); value.setMimeType(coverFileInfo.mimeType()); } if (parts.size() > 2) { value.setDescription(parts[2], TagTextEncoding::Utf8); } if (parts.size() > 1 && denotedScope.field.knownFieldForTag(tag, tagType) == KnownField::Cover && (tagType == TagType::Id3v2Tag || tagType == TagType::VorbisComment)) { const auto coverType = id3v2CoverType(parts[1]); if (coverType == invalidCoverType) { diag.emplace_back(DiagLevel::Warning, argsToString("Specified cover type \"", parts[1], "\" is invalid. Ignoring the specified field/value."), context); } else { convertedValuesWithCoverType.emplace_back(std::pair(std::move(value), coverType)); } } else { if (parts.size() > 1) { diag.emplace_back(DiagLevel::Warning, argsToString("Ignoring cover type \"", parts[1], "\" for ", tag->typeName(), ". It is only supported by the cover field and the tag formats ID3v2 and Vorbis Comment."), context); } convertedValues.emplace_back(std::move(value)); } } catch (const TagParser::Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to parse specified cover file.", context); } catch (const std::ios_base::failure &e) { diag.emplace_back(DiagLevel::Critical, argsToString("An IO error occured when parsing the specified cover file: ", e.what()), context); } } // finally set the values try { if (!convertedValues.empty() || convertedValuesWithCoverType.empty()) { denotedScope.field.setValues(tag, tagType, convertedValues); } } catch (const ConversionException &e) { diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse denoted field ID \"", denotedScope.field.name(), "\": ", e.what()), context); } if (!convertedValuesWithCoverType.empty()) { switch (tagType) { case TagType::Id3v2Tag: setId3v2CoverValues(static_cast(tag), std::move(convertedValuesWithCoverType)); break; case TagType::VorbisComment: setId3v2CoverValues(static_cast(tag), std::move(convertedValuesWithCoverType)); break; default:; } } } } } // alter tracks for (AbstractTrack *track : fileInfo.tracks()) { for (const auto &fieldDenotation : fields) { // skip empty values const auto &values = fieldDenotation.second.relevantValues; if (values.empty()) { continue; } // skip values which scope does not match the current track const FieldScope &denotedScope = fieldDenotation.first; if (!denotedScope.allTracks && find(denotedScope.trackIds.cbegin(), denotedScope.trackIds.cend(), track->id()) == denotedScope.trackIds.cend()) { continue; } const FieldId &field = denotedScope.field; const string &value = values.front()->value; try { if (field.denotes("name")) { track->setName(value); } else if (field.denotes("language")) { track->setLocale(Locale(std::string_view(value), LocaleFormat::Unknown)); } else if (field.denotes("tracknumber")) { track->setTrackNumber(stringToNumber(value)); } else if (field.denotes("enabled")) { track->setEnabled(stringToBool(value)); } else if (field.denotes("forced")) { track->setForced(stringToBool(value)); } else if (field.denotes("default")) { track->setDefault(stringToBool(value)); } else { diag.emplace_back(DiagLevel::Critical, argsToString("Denoted track property name \"", field.denotation(), "\" is invalid"), argsToString("setting meta-data of track ", track->id())); } } catch (const ConversionException &e) { diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse value for track property \"", field.denotation(), "\": ", e.what()), argsToString("setting meta-data of track ", track->id())); } } } // increment relevant values for (auto &fieldDenotation : fields) { for (FieldValue *relevantDenotedValue : fieldDenotation.second.relevantValues) { if (!relevantDenotedValue->value.empty() && relevantDenotedValue->type == DenotationType::Increment) { relevantDenotedValue->value = incremented(relevantDenotedValue->value); } } } // alter attachments bool attachmentsModified = false; if (args.addAttachmentArg.isPresent() || args.updateAttachmentArg.isPresent() || args.removeAttachmentArg.isPresent() || args.removeExistingAttachmentsArg.isPresent()) { static const string attachmentsContext("setting attachments"); fileInfo.parseAttachments(diag, parsingProgress); if (fileInfo.attachmentsParsingStatus() == ParsingStatus::Ok && 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 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, diag); } 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, diag); } 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, diag); } } else { diag.emplace_back(DiagLevel::Critical, "Unable to assign attachments because the container object has not been initialized.", attachmentsContext); } } // apply changes fileInfo.setSaveFilePath(currentOutputFile != noMoreOutputFiles ? string(*currentOutputFile) : string()); try { // create handler for progress updates and aborting AbortableProgressFeedback applyProgress(logNextStep, logStepPercentage); const InterruptHandler handler(bind(&AbortableProgressFeedback::tryToAbort, ref(applyProgress))); // apply changes fileInfo.applyChanges(diag, applyProgress); // notify about completion finalizeLog(); cout << " - Changes have been applied." << endl; } catch (const TagParser::OperationAbortedException &) { finalizeLog(); cerr << Phrases::Warning << "The operation has been aborted." << Phrases::EndFlush; return; } catch (const TagParser::Failure &) { finalizeLog(); cerr << " - " << Phrases::Error << "Failed to apply changes." << Phrases::EndFlush; } } catch (const TagParser::Failure &) { finalizeLog(); cerr << " - " << Phrases::Error << "A parsing failure occured when reading/writing the file \"" << file << "\"." << Phrases::EndFlush; } catch (const std::ios_base::failure &) { finalizeLog(); cerr << " - " << Phrases::Error << "An IO failure occured when reading/writing the file \"" << file << "\"." << Phrases::EndFlush; } printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent()); // continue with next file ++fileIndex; if (currentOutputFile != noMoreOutputFiles) { ++currentOutputFile; } } } void extractField( const Argument &fieldArg, const Argument &attachmentArg, const Argument &inputFilesArg, const Argument &outputFileArg, const Argument &verboseArg) { CMD_UTILS_START_CONSOLE; // parse specified field and attachment const auto fieldDenotations = parseFieldDenotations(fieldArg, true); AttachmentInfo attachmentInfo; if (attachmentArg.isPresent()) { attachmentInfo.parseDenotation(attachmentArg.values().front()); } if (((fieldDenotations.size() != 1) || (!attachmentInfo.hasId && !attachmentInfo.name)) && ((fieldDenotations.size() == 1) && (attachmentInfo.hasId || attachmentInfo.name))) { cerr << Phrases::Error << "Excactly one field or attachment needs to be specified." << Phrases::EndFlush; exit(-1); } if (!inputFilesArg.isPresent() || inputFilesArg.values().empty()) { cerr << Phrases::Error << "No files have been specified." << Phrases::EndFlush; exit(-1); } MediaFileInfo inputFileInfo; for (const char *file : inputFilesArg.values()) { Diagnostics diag; AbortableProgressFeedback progress; // FIXME: actually use the progress object try { // setup media file info inputFileInfo.setPath(std::string(file)); inputFileInfo.open(true); // extract either tag field or attachment if (!fieldDenotations.empty()) { // extract tag field (outputFileArg.isPresent() ? cout : cerr) << "Extracting field " << fieldArg.values().front() << " of \"" << file << "\" ..." << endl; inputFileInfo.parseContainerFormat(diag, progress); inputFileInfo.parseTags(diag, progress); auto tags = inputFileInfo.tags(); vector> values; // iterate through all tags for (const Tag *tag : tags) { const TagType tagType = tag->type(); for (const auto &fieldDenotation : fieldDenotations) { try { const auto valuesForField = fieldDenotation.first.field.values(tag, tagType); // skip if field ID is format specific and not relevant for the current format if (!valuesForField.second) { continue; } for (const TagValue *value : valuesForField.first) { values.emplace_back(value, joinStrings({ std::string(tag->typeName()), numberToString(values.size()) }, "-", true)); } } catch (const ConversionException &e) { diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse denoted field ID \"", fieldDenotation.first.field.name(), "\": ", e.what()), "extracting field"); } } } if (values.empty()) { cerr << " - " << Phrases::Error << "None of the specified files has a (supported) " << fieldArg.values().front() << " field." << Phrases::End; } else if (outputFileArg.isPresent()) { string outputFilePathWithoutExtension, outputFileExtension; if (values.size() > 1) { outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front()); outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front()); } for (const auto &value : values) { NativeFileStream outputFileStream; 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(), static_cast(value.first->dataSize())); outputFileStream.flush(); cout << " - Value has been saved to \"" << path << "\"." << endl; } catch (const std::ios_base::failure &) { cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::End; } } } else { // write data to stdout if no output file has been specified for (const auto &value : values) { cout.write(value.first->dataPointer(), static_cast(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(diag, progress); inputFileInfo.parseAttachments(diag, progress); vector> attachments; // iterate through all attachments for (const AbstractAttachment *attachment : inputFileInfo.attachments()) { if ((attachmentInfo.hasId && attachment->id() == attachmentInfo.id) || (attachment->name() == attachmentInfo.name)) { attachments.emplace_back(attachment, joinStrings({ attachment->name(), numberToString(attachments.size()) }, "-", true)); } } if (attachments.empty()) { cerr << " - " << Phrases::Error << "None of the specified files has a (supported) attachment with the specified ID/name." << Phrases::End; } else if (outputFileArg.isPresent()) { string outputFilePathWithoutExtension, outputFileExtension; if (attachments.size() > 1) { outputFilePathWithoutExtension = BasicFileInfo::pathWithoutExtension(outputFileArg.values().front()); outputFileExtension = BasicFileInfo::extension(outputFileArg.values().front()); } for (const auto &attachment : attachments) { NativeFileStream outputFileStream; 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 (const std::ios_base::failure &) { cerr << " - " << Phrases::Error << "An IO error occured when writing the file \"" << path << "\"." << Phrases::EndFlush; } } } else { for (const auto &attachment : attachments) { attachment.first->data()->copyTo(cout); } } } } catch (const TagParser::Failure &) { cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::End; } catch (const std::ios_base::failure &) { cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::End; } printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent()); } } void exportToJson(const ArgumentOccurrence &, const Argument &filesArg, const Argument &prettyArg) { CMD_UTILS_START_CONSOLE; #ifdef TAGEDITOR_JSON_EXPORT // check whether files have been specified if (!filesArg.isPresent() || filesArg.values().empty()) { cerr << Phrases::Error << "No files have been specified." << Phrases::End; exit(-1); } RAPIDJSON_NAMESPACE::Document document(RAPIDJSON_NAMESPACE::kArrayType); std::vector jsonData; MediaFileInfo fileInfo; // gather tags for each file Diagnostics diag; // FIXME: actually use diag object AbortableProgressFeedback progress; // FIXME: actually use the progress object for (const char *file : filesArg.values()) { try { // parse tags fileInfo.setPath(std::string(file)); fileInfo.open(true); fileInfo.parseContainerFormat(diag, progress); fileInfo.parseTags(diag, progress); fileInfo.parseTracks(diag, progress); jsonData.emplace_back(fileInfo, document.GetAllocator()); } catch (const TagParser::Failure &) { cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; } catch (const std::ios_base::failure &) { cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; } } // TODO: serialize diag messages // print the gathered data as JSON document ReflectiveRapidJSON::JsonReflector::push(jsonData, document, document.GetAllocator()); RAPIDJSON_NAMESPACE::OStreamWrapper osw(cout); if (prettyArg.isPresent()) { RAPIDJSON_NAMESPACE::PrettyWriter writer(osw); document.Accept(writer); } else { RAPIDJSON_NAMESPACE::Writer writer(osw); document.Accept(writer); } cout << endl; #else CPP_UTILITIES_UNUSED(filesArg); CPP_UTILITIES_UNUSED(prettyArg); cerr << Phrases::Error << "JSON export has not been enabled when building the tag editor." << Phrases::EndFlush; #endif } void applyGeneralConfig(const Argument &timeSapnFormatArg) { timeSpanOutputFormat = parseTimeSpanOutputFormat(timeSapnFormatArg, TimeSpanOutputFormat::WithMeasures); } } // namespace Cli