Allow tag processing via JavaScript
This commit is contained in:
parent
857917c23d
commit
22953ad0da
|
@ -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)
|
||||
|
|
32
README.md
32
README.md
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -48,6 +48,7 @@ struct SetTagInfoArgs {
|
|||
CppUtilities::ConfigValueArgument backupDirArg;
|
||||
CppUtilities::ConfigValueArgument layoutOnlyArg;
|
||||
CppUtilities::ConfigValueArgument preserveModificationTimeArg;
|
||||
CppUtilities::ConfigValueArgument jsArg;
|
||||
CppUtilities::OperationArgument setTagInfoArg;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
Loading…
Reference in New Issue