From 01e57c86d6ebc88e9dcd88514516e6dd8bbc50f1 Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 15 Jan 2018 00:14:53 +0100 Subject: [PATCH] Add JSON export --- CMakeLists.txt | 34 ++++++++++++ application/main.cpp | 7 ++- cli/fieldmapping.cpp | 64 +++++++++++++++++++++ cli/fieldmapping.h | 19 +++++++ cli/helper.cpp | 42 ++------------ cli/json.cpp | 129 +++++++++++++++++++++++++++++++++++++++++++ cli/json.h | 50 +++++++++++++++++ cli/mainfeatures.cpp | 56 +++++++++++++++++++ cli/mainfeatures.h | 1 + 9 files changed, 364 insertions(+), 38 deletions(-) create mode 100644 cli/fieldmapping.cpp create mode 100644 cli/fieldmapping.h create mode 100644 cli/json.cpp create mode 100644 cli/json.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 792477d..ee7f2cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ set(META_NO_TIDY ON) # add project files set(HEADER_FILES cli/attachmentinfo.h + cli/fieldmapping.h cli/helper.h cli/mainfeatures.h application/knownfieldmodel.h @@ -25,6 +26,7 @@ set(HEADER_FILES set(SRC_FILES application/main.cpp cli/attachmentinfo.cpp + cli/fieldmapping.cpp cli/helper.cpp cli/mainfeatures.cpp application/knownfieldmodel.cpp @@ -219,6 +221,38 @@ find_package(tagparser 6.4.0 REQUIRED) use_tag_parser() list(APPEND TEST_LIBRARIES ${TAG_PARSER_SHARED_LIB}) +# enable experimental JSON export +option(ENABLE_JSON_EXPORT "enable JSON export" OFF) +if(ENABLE_JSON_EXPORT) + # find reflective-rapidjson + find_package(reflective_rapidjson REQUIRED) + use_reflective_rapidjson() + + # add additional source files + list(APPEND HEADER_FILES + cli/json.h + ) + list(APPEND SRC_FILES + cli/json.cpp + ) + + # add generator invocation + include(ReflectionGenerator) + add_reflection_generator_invocation( + INPUT_FILES + cli/json.h + GENERATORS + json + OUTPUT_LISTS + HEADER_FILES + CLANG_OPTIONS_FROM_TARGETS + tageditor + ) + + # add compile definitions + list(APPEND META_PRIVATE_COMPILE_DEFINITIONS ${META_PROJECT_VARNAME_UPPER}_JSON_EXPORT) +endif() + # add Qt modules which can currently not be detected automatically list(APPEND ADDITIONAL_QT_MODULES Concurrent Network) diff --git a/application/main.cpp b/application/main.cpp index f576d10..c87467b 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -219,6 +219,11 @@ int main(int argc, char *argv[]) extractFieldArg.setSubArguments({&fieldArg, &attachmentArg, &fileArg, &outputFileArg, &verboseArg}); extractFieldArg.setDenotesOperation(true); extractFieldArg.setCallback(std::bind(Cli::extractField, std::cref(fieldArg), std::cref(attachmentArg), std::cref(fileArg), std::cref(outputFileArg), std::cref(verboseArg))); + // export to JSON + Argument exportArg("export", 'j', "exports the tag information for the specified files to JSON"); + exportArg.setSubArguments({&filesArg}); + exportArg.setDenotesOperation(true); + exportArg.setCallback(std::bind(Cli::exportToJson, _1, std::cref(filesArg))); // file info Argument validateArg("validate", 'c', "validates the file integrity as accurately as possible; the structure of the file will be parsed completely"); validateArg.setCombinable(true); @@ -233,7 +238,7 @@ int main(int argc, char *argv[]) qtConfigArgs.qtWidgetsGuiArg().setAbbreviation('\0'); qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&defaultFileArg); qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&renamingUtilityArg); - parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayFileInfoArg, &displayTagInfoArg, &setTagInfoArgs.setTagInfoArg, &extractFieldArg, &genInfoArg, &timeSpanFormatArg, &noColorArg, &helpArg}); + parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayFileInfoArg, &displayTagInfoArg, &setTagInfoArgs.setTagInfoArg, &extractFieldArg, &exportArg, &genInfoArg, &timeSpanFormatArg, &noColorArg, &helpArg}); // parse given arguments parser.parseArgsExt(argc, argv, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::ExitOnFailure); diff --git a/cli/fieldmapping.cpp b/cli/fieldmapping.cpp new file mode 100644 index 0000000..73f3a32 --- /dev/null +++ b/cli/fieldmapping.cpp @@ -0,0 +1,64 @@ +#include "./fieldmapping.h" + +#include + +using namespace Media; + +namespace Cli { +namespace FieldMapping { + +static constexpr struct { + const char *knownDenotation; + KnownField knownField; +} fieldMapping[] = { + {"title", KnownField::Title}, + {"album", KnownField::Album}, + {"artist", KnownField::Artist}, + {"genre", KnownField::Genre}, + {"year", KnownField::Year}, + {"comment", KnownField::Comment}, + {"bpm", KnownField::Bpm}, + {"bps", KnownField::Bps}, + {"lyricist", KnownField::Lyricist}, + {"track", KnownField::TrackPosition}, + {"disk", KnownField::DiskPosition}, + {"part", KnownField::PartNumber}, + {"totalparts", KnownField::TotalParts}, + {"encoder", KnownField::Encoder}, + {"recorddate", KnownField::RecordDate}, + {"performers", KnownField::Performers}, + {"duration", KnownField::Length}, + {"language", KnownField::Language}, + {"encodersettings", KnownField::EncoderSettings}, + {"lyrics", KnownField::Lyrics}, + {"synchronizedlyrics", KnownField::SynchronizedLyrics}, + {"grouping", KnownField::Grouping}, + {"recordlabel", KnownField::RecordLabel}, + {"cover", KnownField::Cover}, + {"composer", KnownField::Composer}, + {"rating", KnownField::Rating}, + {"description", KnownField::Description}, +}; + +const char *fieldDenotation(Media::KnownField knownField) +{ + for(const auto &mapping : fieldMapping) { + if(mapping.knownField == knownField) { + return mapping.knownDenotation; + } + } + return nullptr; +} + +Media::KnownField knownField(const char *fieldDenotation, std::size_t fieldDenotationSize) +{ + for(const auto &mapping : fieldMapping) { + if(!strncmp(fieldDenotation, mapping.knownDenotation, fieldDenotationSize)) { + return mapping.knownField; + } + } + return KnownField::Invalid; +} + +} +} diff --git a/cli/fieldmapping.h b/cli/fieldmapping.h new file mode 100644 index 0000000..3544c33 --- /dev/null +++ b/cli/fieldmapping.h @@ -0,0 +1,19 @@ +#ifndef CLI_FIELDMAPPING +#define CLI_FIELDMAPPING + +#include + +namespace Media { +enum class KnownField : unsigned int; +} + +namespace Cli { +namespace FieldMapping { + +const char *fieldDenotation(Media::KnownField knownField); +Media::KnownField knownField(const char *fieldDenotation, std::size_t fieldDenotationSize); + +} +} + +#endif // CLI_FIELDMAPPING diff --git a/cli/helper.cpp b/cli/helper.cpp index 1761536..597b4f0 100644 --- a/cli/helper.cpp +++ b/cli/helper.cpp @@ -1,4 +1,5 @@ #include "./helper.h" +#include "./fieldmapping.h" #include #include @@ -598,44 +599,11 @@ FieldId FieldId::fromTagDenotation(const char *denotation, size_t denotationSize } // determine KnownField for generic denotation - static const struct { - const char *knownDenotation; - KnownField knownField; - } fieldMapping[] = { - {"title", KnownField::Title}, - {"album", KnownField::Album}, - {"artist", KnownField::Artist}, - {"genre", KnownField::Genre}, - {"year", KnownField::Year}, - {"comment", KnownField::Comment}, - {"bpm", KnownField::Bpm}, - {"bps", KnownField::Bps}, - {"lyricist", KnownField::Lyricist}, - {"track", KnownField::TrackPosition}, - {"disk", KnownField::DiskPosition}, - {"part", KnownField::PartNumber}, - {"totalparts", KnownField::TotalParts}, - {"encoder", KnownField::Encoder}, - {"recorddate", KnownField::RecordDate}, - {"performers", KnownField::Performers}, - {"duration", KnownField::Length}, - {"language", KnownField::Language}, - {"encodersettings", KnownField::EncoderSettings}, - {"lyrics", KnownField::Lyrics}, - {"synchronizedlyrics", KnownField::SynchronizedLyrics}, - {"grouping", KnownField::Grouping}, - {"recordlabel", KnownField::RecordLabel}, - {"cover", KnownField::Cover}, - {"composer", KnownField::Composer}, - {"rating", KnownField::Rating}, - {"description", KnownField::Description}, - }; - for(const auto &mapping : fieldMapping) { - if(!strncmp(denotation, mapping.knownDenotation, denotationSize)) { - return FieldId(mapping.knownField, nullptr, 0); - } + const auto knownField(FieldMapping::knownField(denotation, denotationSize)); + if (knownField == KnownField::Invalid) { + throw ConversionException("generic field name is unknown"); } - throw ConversionException("generic field name is unknown"); + return FieldId(knownField, nullptr, 0); } FieldId FieldId::fromTrackDenotation(const char *denotation, size_t denotationSize) diff --git a/cli/json.cpp b/cli/json.cpp new file mode 100644 index 0000000..d497e5f --- /dev/null +++ b/cli/json.cpp @@ -0,0 +1,129 @@ +#include "./json.h" +#include "./fieldmapping.h" + +#include + +#include +#include + +#include + +using namespace std; +using namespace Media; + +namespace ReflectiveRapidJSON { +namespace JsonReflector { + +/*! + * \brief Allows moving a RAPIDJSON_NAMESPACE::Value wrapped as Cli::Json::TagValue::ValueAllowedToMove for serialization. + * \remarks The const-cast and move operation are ok in this particular case, since the object to be serialized + * is not const and not used anymore after serialization. + * \todo Add an overload for &&reflectable to Reflective RapidJSON. + */ +template <> +inline void push( + const Cli::Json::TagValue::ValueAllowedToMove &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &) +{ + value = const_cast(reflectable).Move(); +} + +} // namespace JsonReflector +} // namespace ReflectiveRapidJSON + +namespace Cli { +namespace Json { + +TagValue::TagValue(const Media::TagValue &tagValue, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) + : mimeType(tagValue.mimeType()) +{ + if (tagValue.isEmpty()) { + value.SetNull(); + return; + } + try { + switch(tagValue.type()) { + case TagDataType::Text: + case TagDataType::StandardGenreIndex: + ReflectiveRapidJSON::JsonReflector::push(tagValue.toString(TagTextEncoding::Utf8), value, allocator); + kind = "text"; + break; + case TagDataType::Integer: + ReflectiveRapidJSON::JsonReflector::push(tagValue.toInteger(), value, allocator); + kind = "integer"; + break; + case TagDataType::PositionInSet: { + const auto position(tagValue.toPositionInSet()); + value.SetObject(); + if (position.position()) { + value.AddMember("position", RAPIDJSON_NAMESPACE::Value(position.position()), allocator); + } + if (position.total()) { + value.AddMember("total", RAPIDJSON_NAMESPACE::Value(position.total()), allocator); + } + kind = "position"; + break; + } + case TagDataType::TimeSpan: + ReflectiveRapidJSON::JsonReflector::push(tagValue.toTimeSpan(), value, allocator); + break; + case TagDataType::DateTime: + ReflectiveRapidJSON::JsonReflector::push(tagValue.toDateTime(), value, allocator); + break; + case TagDataType::Picture: + if (tagValue.dataSize() > (1024 * 1024)) { + throw ConversionUtilities::ConversionException("size is too big"); + } + ReflectiveRapidJSON::JsonReflector::push(ConversionUtilities::encodeBase64(reinterpret_cast(tagValue.dataPointer()), static_cast(tagValue.dataSize())), value, allocator); + kind = "picture"; + break; + case TagDataType::Binary: + if (tagValue.dataSize() > (1024 * 1024)) { + throw ConversionUtilities::ConversionException("size is too big"); + } + ReflectiveRapidJSON::JsonReflector::push(ConversionUtilities::encodeBase64(reinterpret_cast(tagValue.dataPointer()), static_cast(tagValue.dataSize())), value, allocator); + kind = "binary"; + break; + default: + value.SetNull(); + } + } catch (const ConversionUtilities::ConversionException &e) { + ReflectiveRapidJSON::JsonReflector::push(e.what(), value, allocator); + kind = "error"; + } +} + +TagInfo::TagInfo(const Tag &tag, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) + : format(tag.typeName()) + , target(tag.targetString()) +{ + for (auto field = firstKnownField; field != KnownField::Invalid; field = nextKnownField(field)) { + const auto &tagValues(tag.values(field)); + if (tagValues.empty()) { + continue; + } + std::vector valueObjects; + valueObjects.reserve(tagValues.size()); + for (const auto *tagValue : tagValues) { + valueObjects.emplace_back(*tagValue, allocator); + } + fields.insert(make_pair(FieldMapping::fieldDenotation(field), move(valueObjects))); + } +} + +FileInfo::FileInfo(const Media::MediaFileInfo &mediaFileInfo, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) + : fileName(mediaFileInfo.fileName()) + , size(mediaFileInfo.size()) + , mimeType(mediaFileInfo.mimeType()) + , formatSummary(mediaFileInfo.technicalSummary()) + , duration(mediaFileInfo.duration()) +{ + for (const Tag *tag : mediaFileInfo.tags()) { + tags.emplace_back(*tag, allocator); + } +} + +} + +} + +#include "reflection/json.h" diff --git a/cli/json.h b/cli/json.h new file mode 100644 index 0000000..63a2d7d --- /dev/null +++ b/cli/json.h @@ -0,0 +1,50 @@ +#ifndef CLI_JSON +#define CLI_JSON + +#include + +#include + +#include + +namespace Media { +class MediaFileInfo; +class Tag; +class TagValue; +} + +namespace Cli { +namespace Json { + +struct TagValue : ReflectiveRapidJSON::JsonSerializable { + TagValue(const Media::TagValue &tagValue, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator); + + const char *kind = "undefined"; + const std::string mimeType; + struct ValueAllowedToMove : RAPIDJSON_NAMESPACE::Value { + } value; +}; + +struct TagInfo : ReflectiveRapidJSON::JsonSerializable { + TagInfo(const Media::Tag &tag, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator); + + const char *format = nullptr; + std::string target; + std::unordered_map> fields; +}; + +struct FileInfo : ReflectiveRapidJSON::JsonSerializable { + FileInfo(const Media::MediaFileInfo &mediaFileInfo, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator); + + std::string fileName; + std::size_t size; + const char *mimeType; + std::vector tags; + std::string formatSummary; + ChronoUtilities::TimeSpan duration; +}; + +} +} + +#endif // CLI_JSON diff --git a/cli/mainfeatures.cpp b/cli/mainfeatures.cpp index 1d9fe16..3e816dd 100644 --- a/cli/mainfeatures.cpp +++ b/cli/mainfeatures.cpp @@ -1,6 +1,9 @@ #include "./mainfeatures.h" #include "./helper.h" #include "./attachmentinfo.h" +#ifdef TAGEDITOR_JSON_EXPORT +#include "./json.h" +#endif #include "../application/knownfieldmodel.h" #if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK) @@ -15,6 +18,8 @@ #include #include +#include + #include #include #include @@ -29,6 +34,11 @@ # include #endif +#ifdef TAGEDITOR_JSON_EXPORT +#include +#include +#endif + #include #include #include @@ -843,6 +853,52 @@ void extractField(const Argument &fieldArg, const Argument &attachmentArg, const } } +void exportToJson(const ArgumentOccurrence &, const Argument &filesArg) +{ + 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; + return; + } + + RAPIDJSON_NAMESPACE::Document document(RAPIDJSON_NAMESPACE::kArrayType); + std::vector jsonData; + MediaFileInfo fileInfo; + + // gather tags for each file + for(const char *file : filesArg.values()) { + try { + // parse tags + fileInfo.setPath(file); + fileInfo.open(true); + fileInfo.parseContainerFormat(); + fileInfo.parseTags(); + fileInfo.parseTracks(); + jsonData.emplace_back(fileInfo, document.GetAllocator()); + } catch(const Media::Failure &) { + cerr << Phrases::Error << "A parsing failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; + } catch(...) { + ::IoUtilities::catchIoFailure(); + cerr << Phrases::Error << "An IO failure occured when reading the file \"" << file << "\"." << Phrases::EndFlush; + } + } + + // print the gathered data as JSON document + ReflectiveRapidJSON::JsonReflector::push(jsonData, document, document.GetAllocator()); + RAPIDJSON_NAMESPACE::OStreamWrapper osw(cout); + RAPIDJSON_NAMESPACE::Writer writer(osw); + document.Accept(writer); + cout << endl; + +#else + VAR_UNUSED(filesArg); + 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); diff --git a/cli/mainfeatures.h b/cli/mainfeatures.h index fdbc994..fbcb77e 100644 --- a/cli/mainfeatures.h +++ b/cli/mainfeatures.h @@ -53,6 +53,7 @@ void generateFileInfo(const ApplicationUtilities::ArgumentOccurrence &, const Ap void displayTagInfo(const ApplicationUtilities::Argument &fieldsArg, const ApplicationUtilities::Argument &filesArg, const ApplicationUtilities::Argument &verboseArg); void setTagInfo(const Cli::SetTagInfoArgs &args); void extractField(const ApplicationUtilities::Argument &fieldArg, const ApplicationUtilities::Argument &attachmentArg, const ApplicationUtilities::Argument &inputFilesArg, const ApplicationUtilities::Argument &outputFileArg, const ApplicationUtilities::Argument &verboseArg); +void exportToJson(const ApplicationUtilities::ArgumentOccurrence &, const ApplicationUtilities::Argument &filesArg); }