Add JSON export

This commit is contained in:
Martchus 2018-01-15 00:14:53 +01:00
parent f9e3f8426e
commit 01e57c86d6
9 changed files with 364 additions and 38 deletions

View File

@ -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)

View File

@ -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);

64
cli/fieldmapping.cpp Normal file
View File

@ -0,0 +1,64 @@
#include "./fieldmapping.h"
#include <tagparser/tag.h>
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;
}
}
}

19
cli/fieldmapping.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef CLI_FIELDMAPPING
#define CLI_FIELDMAPPING
#include <cstddef>
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

View File

@ -1,4 +1,5 @@
#include "./helper.h"
#include "./fieldmapping.h"
#include <tagparser/mediafileinfo.h>
#include <tagparser/matroska/matroskatag.h>
@ -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)

129
cli/json.cpp Normal file
View File

@ -0,0 +1,129 @@
#include "./json.h"
#include "./fieldmapping.h"
#include <reflective-rapidjson/lib/json/reflector-chronoutilities.h>
#include <tagparser/mediafileinfo.h>
#include <tagparser/tag.h>
#include <c++utilities/conversion/stringconversion.h>
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<Cli::Json::TagValue::ValueAllowedToMove>(
const Cli::Json::TagValue::ValueAllowedToMove &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &)
{
value = const_cast<Cli::Json::TagValue::ValueAllowedToMove &>(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<const byte *>(tagValue.dataPointer()), static_cast<uint32>(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<const byte *>(tagValue.dataPointer()), static_cast<uint32>(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<TagValue> 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"

50
cli/json.h Normal file
View File

@ -0,0 +1,50 @@
#ifndef CLI_JSON
#define CLI_JSON
#include <reflective-rapidjson/lib/json/serializable.h>
#include <c++utilities/chrono/timespan.h>
#include <unordered_map>
namespace Media {
class MediaFileInfo;
class Tag;
class TagValue;
}
namespace Cli {
namespace Json {
struct TagValue : ReflectiveRapidJSON::JsonSerializable<TagValue> {
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> {
TagInfo(const Media::Tag &tag, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator);
const char *format = nullptr;
std::string target;
std::unordered_map<std::string, std::vector<TagValue>> fields;
};
struct FileInfo : ReflectiveRapidJSON::JsonSerializable<FileInfo> {
FileInfo(const Media::MediaFileInfo &mediaFileInfo, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator);
std::string fileName;
std::size_t size;
const char *mimeType;
std::vector<TagInfo> tags;
std::string formatSummary;
ChronoUtilities::TimeSpan duration;
};
}
}
#endif // CLI_JSON

View File

@ -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 <tagparser/abstractattachment.h>
#include <tagparser/abstractchapter.h>
#include <reflective-rapidjson/lib/json/reflector.h>
#include <c++utilities/application/failure.h>
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/conversion/stringconversion.h>
@ -29,6 +34,11 @@
# include <qtutilities/misc/conversion.h>
#endif
#ifdef TAGEDITOR_JSON_EXPORT
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/writer.h>
#endif
#include <iostream>
#include <iomanip>
#include <cstring>
@ -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<Json::FileInfo> 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<RAPIDJSON_NAMESPACE::OStreamWrapper> 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);

View File

@ -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);
}