Add JSON export
This commit is contained in:
parent
f9e3f8426e
commit
01e57c86d6
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue