Allow tag processing via JavaScript

This commit is contained in:
Martchus 2023-07-05 01:47:26 +02:00
parent 857917c23d
commit 22953ad0da
10 changed files with 753 additions and 17 deletions

View File

@ -115,6 +115,7 @@ set(WIDGETS_UI_FILES
set(TEST_HEADER_FILES)
set(TEST_SRC_FILES tests/cli.cpp)
set(EXCLUDED_FILES cli/mediafileinfoobject.h cli/mediafileinfoobject.cpp)
set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
@ -234,13 +235,21 @@ endif ()
# add Qt modules which can not be detected automatically
list(APPEND ADDITIONAL_QT_MODULES Concurrent Network)
# include modules to apply configuration
# configure usage of Qt
if (WIDGETS_GUI OR QUICK_GUI)
include(QtGuiConfig)
include(QtJsProviderConfig)
include(QtWebViewProviderConfig)
include(QtConfig)
endif ()
# configure JavaScript processing for the CLI
if (JS_PROVIDER STREQUAL Qml)
list(APPEND HEADER_FILES cli/mediafileinfoobject.h)
list(APPEND SRC_FILES cli/mediafileinfoobject.cpp)
endif ()
# include modules to apply configuration
include(WindowsResources)
include(TestTarget)
include(AppTarget)

View File

@ -344,6 +344,38 @@ Here are some Bash examples which illustrate getting and setting tag information
- This is only supported by the tag formats ID3v2 and Vorbis Comment. The type and description are ignored
when dealing with a different format.
* Sets fields by running a script to compute changes dynamically:
```
tageditor set --pedantic debug --java-script path/to/script.js -f foo.mp3
```
- This feature is still experimental. The script API is still very limited and subject to change.
- The script needs to be ECMAScript as supported by the Qt framework.
- This feature requires the tag editor to be configured with Qt QML as JavaScript provider at
compile time. Checkout the build instructions under "Building with Qt GUI" for details.
- It needs to export a `main()` function. This function gets executed for every file and passed
and object representing this file as first argument.
- The option `--pedantic debug` is not required but useful for debugging.
- Checkout the file `testfiles/set-tags.js` directory in this repository for a basic example.
- Common tag fields are exposed as object properties as shown in the mentioned example.
- Only properties for fields that are supported by the tag are added to the "fields" object.
- Adding properties of unsupported fields manually does not work; those will just be ignored.
- The content of fields that are absent in the tag is set to `undefined`. You may also set
the content of fields to `undefined` to delete them (`null` works as well).
- The content of binary fields is exposed as `ArrayBuffer`. Use must also use an `ArrayBuffer`
to set the value of binary fields such as the cover.
- The content of other fields is mostly exposed as `String` or `Number`. Use must also use
these types to set the value of those fields. The string-representation of the assigned
content will then be converted automatically to what's needed internally.
- The `utility` object exposes useful methods, e.g. for logging and controlling the event loop.
- Checkout the file `testfiles/http.js` in this repository for an example of using XHR and
controlling the event loop.
- The script is executed before any other modifications are applied. So if you also specify
values as usual (via `--values`) then these values override values changes by the script.
- The script runs so far before tags are added/removed (according to options like
`--id3v1-usage`). This may change in future versions. A JavaScript API to deal with such
changes still needs to be implemented.
##### Further useful commands
* Let the tag editor return with a non-zero exit code even if only non-fatal problems have been encountered
* when saving a file:

View File

@ -83,6 +83,7 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
, backupDirArg("temp-dir", '\0', "specifies the directory for temporary/backup files", { "path" })
, layoutOnlyArg("layout-only", 'l', "confirms layout-only changes")
, preserveModificationTimeArg("preserve-modification-time", '\0', "preserves the file's modification time")
, jsArg("java-script", 'j', "modifies tag fields via the specified JavaScript", { "path" })
, setTagInfoArg("set", 's', "sets the specified tag information and attachments")
{
docTitleArg.setRequiredValueCount(Argument::varValueCount);
@ -120,6 +121,7 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
valuesArg.setPreDefinedCompletionValues(Cli::fieldNamesForSet);
valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
outputFilesArg.setRequiredValueCount(Argument::varValueCount);
jsArg.setValueCompletionBehavior(ValueCompletionBehavior::Files);
setTagInfoArg.setCallback(std::bind(Cli::setTagInfo, std::cref(*this)));
setTagInfoArg.setExample(PROJECT_NAME
" set title=\"Title of \"{1st,2nd,3rd}\" file\" title=\"Title of \"{4..16}\"th file\" album=\"The Album\" -f /some/dir/*.m4a\n" PROJECT_NAME
@ -135,7 +137,7 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
&id3v2UsageArg, &id3InitOnCreateArg, &id3TransferOnRemovalArg, &mergeMultipleSuccessiveTagsArg, &id3v2VersionArg, &encodingArg,
&removeTargetArg, &addAttachmentArg, &updateAttachmentArg, &removeAttachmentArg, &removeExistingAttachmentsArg, &minPaddingArg,
&maxPaddingArg, &prefPaddingArg, &tagPosArg, &indexPosArg, &forceRewriteArg, &backupDirArg, &layoutOnlyArg, &preserveModificationTimeArg,
&verboseArg, &pedanticArg, &quietArg, &outputFilesArg });
&jsArg, &verboseArg, &pedanticArg, &quietArg, &outputFilesArg });
}
} // namespace Cli

View File

@ -222,17 +222,25 @@ void printDiagMessages(const Diagnostics &diag, const char *head, bool beVerbose
setStyle(cerr, TextAttribute::Reset);
break;
case DiagLevel::Critical:
case DiagLevel::Fatal:
setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold);
setStyle(cerr, TextAttribute::Reset);
setStyle(cerr, TextAttribute::Bold);
cerr << " Error ";
setStyle(cerr, TextAttribute::Reset);
break;
case DiagLevel::Fatal:
setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold);
setStyle(cerr, TextAttribute::Reset);
setStyle(cerr, TextAttribute::Bold);
cerr << " Fatal ";
setStyle(cerr, TextAttribute::Reset);
break;
default:;
}
cerr << message.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
cerr << message.context() << ": ";
if (!message.context().empty()) {
cerr << message.context() << ": ";
}
cerr << message.message() << '\n';
}
}

View File

@ -6,6 +6,13 @@
#endif
#include "../application/knownfieldmodel.h"
// includes for JavaScript support of set operation
#ifdef TAGEDITOR_USE_JSENGINE
#include "./mediafileinfoobject.h"
#endif
// includes for generating HTML info
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
#include "../misc/htmlinfo.h"
#include "../misc/utility.h"
@ -39,6 +46,16 @@
#include <c++utilities/io/path.h>
#include <c++utilities/misc/parseerror.h>
// includes for JavaScript support of set operation
#ifdef TAGEDITOR_USE_JSENGINE
#include <QCoreApplication>
#include <QFile>
#include <QJSValue>
#include <QQmlEngine>
#include <QTextStream>
#endif
// includes for generating HTML info
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
#include <QDir>
#include <qtutilities/misc/conversion.h>
@ -471,6 +488,110 @@ template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vect
}
}
#ifdef TAGEDITOR_USE_JSENGINE
class JavaScriptProcessor {
public:
explicit JavaScriptProcessor(const SetTagInfoArgs &args);
JavaScriptProcessor(const JavaScriptProcessor &) = delete;
QJSValue callMain(MediaFileInfo &mediaFileInfo, Diagnostics &diag);
private:
static void addWarnings(Diagnostics &diag, const std::string &context, const QList<QQmlError> &warnings);
int argc;
QCoreApplication app;
Diagnostics diag;
QQmlEngine engine; // not using QJSEngine as otherwise XMLHttpRequest is not available
QJSValue module, main;
UtilityObject *utility;
};
/*!
* \brief Initializes JavaScript processing for the specified \a args.
* \remarks
* - Comes with its own QCoreApplication. Only for use within the CLI parts of the app!
* - Exits the app on fatal errors.
* - Logs status/problems directly in accordance with other parts of the CLI.
*/
JavaScriptProcessor::JavaScriptProcessor(const SetTagInfoArgs &args)
: argc(0)
, app(argc, nullptr)
, utility(new UtilityObject(&engine))
{
// print status message
const auto jsPath = args.jsArg.firstValue();
if (!jsPath) {
return;
}
if (!args.quietArg.isPresent()) {
std::cout << TextAttribute::Bold << "Loading JavaScript file \"" << jsPath << "\" ..." << Phrases::EndFlush;
}
// print warnings later via the usual helper function for consistent formatting
engine.setOutputWarningsToStandardError(false);
QObject::connect(&engine, &QQmlEngine::warnings, &engine,
[this, context = std::string("loading JavaScript")](const auto &warnings) { addWarnings(diag, context, warnings); });
// assign utility object and load specified JavaScript file as module
engine.globalObject().setProperty(QStringLiteral("utility"), engine.newQObject(utility));
module = engine.importModule(QString::fromUtf8(jsPath));
if (module.isError()) {
std::cerr << Phrases::Error << "Unable to load the specified JavaScript file \"" << jsPath << "\":" << Phrases::End;
std::cerr << "Uncaught exception at line " << module.property(QStringLiteral("lineNumber")).toInt() << ':' << ' '
<< module.toString().toStdString() << '\n';
std::exit(EXIT_FAILURE);
}
main = module.property(QStringLiteral("main"));
if (!main.isCallable()) {
std::cerr << Phrases::Error << "The specified JavaScript file \"" << jsPath << "\" does not export a main() function." << Phrases::End;
std::exit(EXIT_FAILURE);
}
// print warnings
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
diag.clear();
}
/*!
* \brief Calls the JavaScript's main() function for the specified \a mediaFileInfo populating \a diag.
* \returns Returns what the main() function has returned.
*/
QJSValue JavaScriptProcessor::callMain(MediaFileInfo &mediaFileInfo, Diagnostics &diag)
{
auto fileInfoObject = MediaFileInfoObject(mediaFileInfo, diag, &engine);
auto fileInfoObjectValue = engine.newQObject(&fileInfoObject);
auto context = argsToString("executing JavaScript for ", mediaFileInfo.fileName());
utility->setDiag(&context, &diag);
QObject::connect(
&engine, &QQmlEngine::warnings, &fileInfoObject, [&diag, &context](const auto &warnings) { addWarnings(diag, context, warnings); });
diag.emplace_back(DiagLevel::Information, "entering main() function", context);
auto res = main.call(QJSValueList({ fileInfoObjectValue }));
if (res.isError()) {
diag.emplace_back(DiagLevel::Fatal,
argsToString(res.toString().toStdString(), " at line ", res.property(QStringLiteral("lineNumber")).toInt(), '.'), context);
} else if (!res.isUndefined()) {
diag.emplace_back(DiagLevel::Information, argsToString("done with return value: ", res.toString().toStdString()), context);
} else {
diag.emplace_back(DiagLevel::Debug, "done without return value", context);
}
return res;
}
/*!
* \brief Adds the \a warnings to the specified \a diag object with the specified \a context.
*/
void JavaScriptProcessor::addWarnings(Diagnostics &diag, const string &context, const QList<QQmlError> &warnings)
{
for (const auto &warning : warnings) {
diag.emplace_back(DiagLevel::Warning, warning.toString().toStdString(), context);
}
}
#endif
/*!
* \brief Implements the "set"-operation of the CLI.
*/
void setTagInfo(const SetTagInfoArgs &args)
{
CMD_UTILS_START_CONSOLE;
@ -496,7 +617,7 @@ void setTagInfo(const SetTagInfoArgs &args)
&& (!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()) {
&& !args.id3v2VersionArg.isPresent() && !args.jsArg.isPresent()) {
if (!args.layoutOnlyArg.isPresent()) {
std::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;
@ -556,12 +677,13 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// parse other settings
const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8);
const auto 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;
auto fileInfo = MediaFileInfo();
auto tags = std::vector<Tag *>();
fileInfo.setMinPadding(parseUInt64(args.minPaddingArg, 0));
fileInfo.setMaxPadding(parseUInt64(args.maxPaddingArg, 0));
fileInfo.setPreferredPadding(parseUInt64(args.prefPaddingArg, 0));
@ -577,10 +699,27 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.setBackupDirectory(std::string(args.backupDirArg.values().front()));
}
// initialize JavaScript processing if --java-script argument is present
#ifdef TAGEDITOR_USE_JSENGINE
auto js = args.jsArg.isPresent() ? std::make_unique<JavaScriptProcessor>(args) : std::unique_ptr<JavaScriptProcessor>();
#else
if (args.jsArg.isPresent()) {
std::cerr << Phrases::Error << "A JavaScript has been specified but support for this has been disabled at compile-time." << Phrases::EndFlush;
std::exit(EXIT_FAILURE);
}
#endif
// iterate through all specified files
const auto quiet = args.quietArg.isPresent();
auto fileIndex = 0u;
static auto context = std::string("setting tags");
const auto continueWithNextFile = [&](Diagnostics &diag) {
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
++fileIndex;
if (currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
};
for (const char *file : args.filesArg.values()) {
auto diag = Diagnostics();
auto parsingProgress = AbortableProgressFeedback(); // FIXME: actually use the progress object
@ -595,16 +734,36 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.parseTracks(diag, parsingProgress);
fileInfo.parseAttachments(diag, parsingProgress);
// process tag fields via the specified JavaScript
#ifdef TAGEDITOR_USE_JSENGINE
if (js) {
const auto res = js->callMain(fileInfo, diag);
if (res.isError() || diag.has(DiagLevel::Fatal)) {
if (!quiet) {
std::cout << " - Skipping file due to fatal error when executing JavaScript.\n";
}
continueWithNextFile(diag);
continue;
}
if (!res.isUndefined() && !res.toBool()) {
if (!quiet) {
std::cout << " - Skipping file because JavaScript returned a falsy value other than undefined.\n";
}
continueWithNextFile(diag);
continue;
}
}
#endif
// remove tags with the specified targets
auto tags = std::vector<Tag *>();
if (!targetsToRemove.empty()) {
tags.clear();
fileInfo.tags(tags);
for (auto *const tag : tags) {
if (find(targetsToRemove.cbegin(), targetsToRemove.cend(), tag->target()) != targetsToRemove.cend()) {
fileInfo.removeTag(tag);
}
}
tags.clear();
}
// select the relevant values for the current file index
@ -670,6 +829,7 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// alter tags
tags.clear();
fileInfo.tags(tags);
if (tags.empty()) {
diag.emplace_back(DiagLevel::Critical, "Can not create appropriate tags for file.", context);
@ -958,14 +1118,7 @@ void setTagInfo(const SetTagInfoArgs &args)
<< Phrases::EndFlush;
exitCode = EXIT_IO_FAILURE;
}
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
// continue with next file
++fileIndex;
if (currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
continueWithNextFile(diag);
}
}
@ -1221,4 +1374,5 @@ void applyGeneralConfig(const Argument &timeSapnFormatArg)
{
timeSpanOutputFormat = parseTimeSpanOutputFormat(timeSapnFormatArg, TimeSpanOutputFormat::WithMeasures);
}
} // namespace Cli

View File

@ -48,6 +48,7 @@ struct SetTagInfoArgs {
CppUtilities::ConfigValueArgument backupDirArg;
CppUtilities::ConfigValueArgument layoutOnlyArg;
CppUtilities::ConfigValueArgument preserveModificationTimeArg;
CppUtilities::ConfigValueArgument jsArg;
CppUtilities::OperationArgument setTagInfoArg;
};

319
cli/mediafileinfoobject.cpp Normal file
View File

@ -0,0 +1,319 @@
#include "./mediafileinfoobject.h"
#include "./fieldmapping.h"
#include "../application/knownfieldmodel.h"
#include "../misc/utility.h"
#include <tagparser/abstracttrack.h>
#include <tagparser/exceptions.h>
#include <tagparser/mediafileinfo.h>
#include <tagparser/progressfeedback.h>
#include <tagparser/tag.h>
#include <tagparser/tagvalue.h>
#include <qtutilities/misc/conversion.h>
#include <c++utilities/conversion/binaryconversion.h>
#include <c++utilities/conversion/conversionexception.h>
#include <qtutilities/misc/compat.h>
#include <QCoreApplication>
#include <QHash>
#include <QJSEngine>
#include <QJSValueIterator>
#include <QRegularExpression>
#include <iostream>
#include <limits>
namespace Cli {
constexpr auto nativeUtf16Encoding = TagParser::TagTextEncoding::
#if defined(CONVERSION_UTILITIES_BYTE_ORDER_LITTLE_ENDIAN)
Utf16LittleEndian
#else
Utf16BigEndian
#endif
;
UtilityObject::UtilityObject(QJSEngine *engine)
: QObject(engine)
, m_context(nullptr)
, m_diag(nullptr)
{
}
void UtilityObject::log(const QString &message)
{
std::cout << message.toStdString() << std::endl;
}
void UtilityObject::diag(const QString &level, const QString &message, const QString &context)
{
if (!m_diag) {
return;
}
static const auto mapping = QHash<QString, TagParser::DiagLevel>({
{ QStringLiteral("fatal"), TagParser::DiagLevel::Fatal },
{ QStringLiteral("critical"), TagParser::DiagLevel::Critical },
{ QStringLiteral("error"), TagParser::DiagLevel::Critical },
{ QStringLiteral("warning"), TagParser::DiagLevel::Warning },
{ QStringLiteral("info"), TagParser::DiagLevel::Information },
{ QStringLiteral("information"), TagParser::DiagLevel::Information },
{ QStringLiteral("debug"), TagParser::DiagLevel::Debug },
});
static const auto defaultContext = std::string("executing JavaScript");
m_diag->emplace_back(mapping.value(level.toLower(), TagParser::DiagLevel::Debug), message.toStdString(),
context.isEmpty() ? (m_context ? *m_context : defaultContext) : context.toStdString());
}
int UtilityObject::exec()
{
return QCoreApplication::exec();
}
void UtilityObject::exit(int retcode)
{
QCoreApplication::exit(retcode);
}
QString UtilityObject::readEnvironmentVariable(const QString &variable, const QString &defaultValue) const
{
return qEnvironmentVariable(variable.toUtf8().data(), defaultValue);
}
QString UtilityObject::formatName(const QString &str) const
{
return Utility::formatName(str);
}
QString UtilityObject::fixUmlauts(const QString &str) const
{
return Utility::fixUmlauts(str);
}
TagValueObject::TagValueObject(const TagParser::TagValue &value, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_type(QString::fromUtf8(TagParser::tagDataTypeString(value.type())))
, m_initial(true)
{
switch (value.type()) {
case TagParser::TagDataType::Undefined:
break;
case TagParser::TagDataType::Text:
case TagParser::TagDataType::PositionInSet:
case TagParser::TagDataType::Popularity:
case TagParser::TagDataType::DateTime:
case TagParser::TagDataType::DateTimeExpression:
case TagParser::TagDataType::TimeSpan:
m_content = Utility::tagValueToQString(value);
break;
case TagParser::TagDataType::Integer:
m_content = value.toInteger();
break;
case TagParser::TagDataType::UnsignedInteger:
if (auto v = value.toUnsignedInteger(); v < std::numeric_limits<uint>::max()) {
m_content = QJSValue(static_cast<uint>(v));
} else {
m_content = QString::number(v);
}
break;
case TagParser::TagDataType::Binary:
case TagParser::TagDataType::Picture:
m_content = engine->toScriptValue(QByteArray(value.dataPointer(), Utility::sizeToInt(value.dataSize())));
break;
default:
m_content = QJSValue::NullValue;
}
}
TagValueObject::~TagValueObject()
{
}
const QString &TagValueObject::type() const
{
return m_type;
}
const QJSValue &TagValueObject::content() const
{
return m_content;
}
const QJSValue &TagValueObject::initialContent() const
{
return m_initial ? m_content : m_initialContent;
}
void TagValueObject::setContent(const QJSValue &content)
{
if (m_initial) {
m_initialContent = m_content;
m_initial = false;
}
m_content = content;
}
bool TagValueObject::isInitial() const
{
return m_initial;
}
TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const
{
if (m_content.isUndefined() || m_content.isNull()) {
return TagParser::TagValue();
}
const auto str = m_content.toString();
return TagParser::TagValue(reinterpret_cast<const char *>(str.utf16()), static_cast<std::size_t>(str.size()) * (sizeof(ushort) / sizeof(char)),
nativeUtf16Encoding, encoding);
}
void TagValueObject::restore()
{
if (!m_initial) {
m_content = m_initialContent;
m_initial = true;
}
}
void TagValueObject::clear()
{
setContent(QJSValue());
}
TagObject::TagObject(TagParser::Tag &tag, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_tag(tag)
, m_diag(diag)
, m_engine(engine)
{
}
TagObject::~TagObject()
{
}
QString TagObject::type() const
{
return Utility::qstr(m_tag.typeName());
}
QJSValue &TagObject::fields()
{
if (!m_fields.isUndefined()) {
return m_fields;
}
static const auto fieldRegex = QRegularExpression(QStringLiteral("\\s(\\w)"));
m_fields = m_engine->newObject();
for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) {
if (!m_tag.supportsField(field)) {
continue;
}
if (const auto propertyName = propertyNameForField(field); !propertyName.isEmpty()) {
m_fields.setProperty(propertyName, m_engine->newQObject(new TagValueObject(m_tag.value(field), m_engine, this)));
}
}
return m_fields;
}
QString TagObject::propertyNameForField(TagParser::KnownField field)
{
static const auto reverseMapping = [] {
auto reverse = QHash<TagParser::KnownField, QString>();
for (const auto &mapping : FieldMapping::mapping()) {
auto d = QString::fromUtf8(mapping.knownDenotation);
if (d.isUpper()) {
d = d.toLower(); // turn abbreviations into just lower case
} else {
d.front() = d.front().toLower();
}
reverse[mapping.knownField] = std::move(d);
}
return reverse;
}();
return reverseMapping.value(field, QString());
}
void TagObject::applyChanges()
{
auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag
? CppUtilities::argsToString(m_tag.typeName(), " targeting ", m_tag.targetString())
: std::string(m_tag.typeName());
m_diag.emplace_back(TagParser::DiagLevel::Debug, "applying changes", std::move(context));
if (m_fields.isUndefined()) {
return;
}
const auto encoding = m_tag.proposedTextEncoding();
for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) {
if (!m_tag.supportsField(field)) {
continue;
}
const auto propertyName = propertyNameForField(field);
if (propertyName.isEmpty()) {
continue;
}
auto propertyValue = m_fields.property(propertyName);
auto fieldDisplayName = Settings::KnownFieldModel::fieldName(field);
if (const auto *const tagValueObj = qobject_cast<const TagValueObject *>(propertyValue.toQObject())) {
if (!tagValueObj->isInitial()) {
auto value = tagValueObj->toTagValue(encoding);
m_diag.emplace_back(TagParser::DiagLevel::Debug,
value.isNull()
? CppUtilities::argsToString(" - delete '", fieldDisplayName, '\'')
: CppUtilities::argsToString(" - change '", fieldDisplayName, "' from '",
tagValueObj->initialContent().toString().toStdString(), "' to '", tagValueObj->content().toString().toStdString(), '\''),
std::string());
m_tag.setValue(field, std::move(value));
}
} else {
m_engine->throwError(QJSValue::TypeError, QStringLiteral("invalid value assigned to field ") + propertyName);
}
}
}
MediaFileInfoObject::MediaFileInfoObject(TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_f(mediaFileInfo)
, m_diag(diag)
, m_engine(engine)
{
}
MediaFileInfoObject::~MediaFileInfoObject()
{
}
QString MediaFileInfoObject::currentPath() const
{
return QString::fromStdString(m_f.path());
}
QString MediaFileInfoObject::currentName() const
{
return QString::fromStdString(m_f.fileName());
}
QList<Cli::TagObject *> &MediaFileInfoObject::tags()
{
if (!m_tags.isEmpty()) {
return m_tags;
}
auto tags = m_f.tags();
m_tags.reserve(Utility::sizeToInt(tags.size()));
for (auto *const tag : tags) {
m_tags << new TagObject(*tag, m_diag, m_engine, this);
}
return m_tags;
}
void MediaFileInfoObject::applyChanges()
{
for (auto *const tag : m_tags) {
tag->applyChanges();
}
}
} // namespace Cli

152
cli/mediafileinfoobject.h Normal file
View File

@ -0,0 +1,152 @@
#ifndef CLI_MEDIA_FILE_INFO_OBJECT_H
#define CLI_MEDIA_FILE_INFO_OBJECT_H
#include <QJSValue>
#include <QObject>
QT_FORWARD_DECLARE_CLASS(QJSEngine)
namespace TagParser {
class Diagnostics;
class MediaFileInfo;
class Tag;
class TagValue;
enum class KnownField : unsigned int;
enum class TagTextEncoding : unsigned int;
} // namespace TagParser
namespace Cli {
/*!
* \brief The UtilityObject class wraps useful functions of Qt, TagParser and the Utility namespace for use within QML.
*/
class UtilityObject : public QObject {
Q_OBJECT
public:
explicit UtilityObject(QJSEngine *engine);
void setDiag(const std::string *context, TagParser::Diagnostics *diag);
public Q_SLOTS:
void log(const QString &message);
void diag(const QString &level, const QString &message, const QString &context = QString());
int exec();
void exit(int retcode);
QString readEnvironmentVariable(const QString &variable, const QString &defaultValue = QString()) const;
QString formatName(const QString &str) const;
QString fixUmlauts(const QString &str) const;
private:
const std::string *m_context;
TagParser::Diagnostics *m_diag;
};
inline void UtilityObject::setDiag(const std::string *context, TagParser::Diagnostics *diag)
{
m_context = context;
m_diag = diag;
}
/*!
* \brief The TagValueObject class wraps a TagParser::TagValue for use within QML.
*/
class TagValueObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString type READ type)
Q_PROPERTY(QJSValue content READ content WRITE setContent)
Q_PROPERTY(QJSValue initialContent READ initialContent)
Q_PROPERTY(bool initial READ isInitial)
public:
explicit TagValueObject(const TagParser::TagValue &value, QJSEngine *engine, QObject *parent);
~TagValueObject() override;
const QString &type() const;
const QJSValue &content() const;
void setContent(const QJSValue &content);
const QJSValue &initialContent() const;
bool isInitial() const;
TagParser::TagValue toTagValue(TagParser::TagTextEncoding encoding) const;
public Q_SLOTS:
void restore();
void clear();
private:
QString m_type;
QJSValue m_content;
QJSValue m_initialContent;
bool m_initial;
};
/*!
* \brief The TagObject class wraps a TagParser::Tag for use within QML.
*/
class TagObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString type READ type)
Q_PROPERTY(QJSValue fields READ fields)
public:
explicit TagObject(TagParser::Tag &tag, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent);
~TagObject() override;
TagParser::Tag &tag();
QString type() const;
QJSValue &fields();
public Q_SLOTS:
void applyChanges();
private:
static QString propertyNameForField(TagParser::KnownField field);
TagParser::Tag &m_tag;
TagParser::Diagnostics &m_diag;
QJSEngine *m_engine;
QString m_type;
QJSValue m_fields;
};
inline TagParser::Tag &TagObject::tag()
{
return m_tag;
}
/*!
* \brief The MediaFileInfoObject class wraps a TagParser::MediaFileInfo for use within QML.
*/
class MediaFileInfoObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString currentPath READ currentPath)
Q_PROPERTY(QString currentName READ currentName)
Q_PROPERTY(QList<TagObject *> tags READ tags)
public:
explicit MediaFileInfoObject(TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent = nullptr);
~MediaFileInfoObject() override;
TagParser::MediaFileInfo &fileInfo();
QString currentPath() const;
QString currentName() const;
QList<TagObject *> &tags();
public Q_SLOTS:
void applyChanges();
private:
TagParser::MediaFileInfo &m_f;
TagParser::Diagnostics &m_diag;
QJSEngine *m_engine;
QList<TagObject *> m_tags;
};
inline TagParser::MediaFileInfo &MediaFileInfoObject::fileInfo()
{
return m_f;
}
} // namespace Cli
#endif // CLI_MEDIA_FILE_INFO_OBJECT_H

23
testfiles/http.js Normal file
View File

@ -0,0 +1,23 @@
export function query(method, url) {
let request = new XMLHttpRequest();
let result = null;
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
utility.exit(0);
result = {
status: request.status,
headers: request.getAllResponseHeaders(),
contentType: request.responseType,
content: request.response
};
}
};
request.open(method, url);
request.send();
utility.exec();
return result;
}
export function get(url) {
return query("GET", url);
}

36
testfiles/set-tags.js Normal file
View File

@ -0,0 +1,36 @@
// import another module as an example how imports work
import * as http from "http.js"
export function main(file) {
// iterate though all tags of the file to change fields in all of them
for (const tag of file.tags) {
changeTagFields(tag);
}
// submit changes from the JavaScript-context to the tag editor application; does not save changes to disk yet
file.applyChanges();
// return a falsy value to skip the file after all
return false;
}
function changeTagFields(tag) {
// log supported fields
const fields = tag.fields;
utility.diag("debug", tag.type, "tag");
utility.diag("debug", Object.keys(fields).join(", "), "supported fields");
// log tag type and fields for debugging purposes
for (const [key, value] of Object.entries(fields)) {
const content = value.content;
if (content !== undefined && content != null && !(content instanceof ArrayBuffer)) {
utility.diag("debug", content, key + " (" + value.type + ")");
}
}
// change some fields
fields.title.content = "foo";
fields.artist.content = "bar";
fields.track.content = "4/17";
fields.comment.clear();
}