Extend script API; allow copying tags from other files

This commit is contained in:
Martchus 2023-12-30 03:06:58 +01:00
parent e1e979f9f5
commit 88989ff986
6 changed files with 134 additions and 15 deletions

View File

@ -372,7 +372,8 @@ Here are some Bash examples which illustrate getting and setting tag information
- The script needs to export a `main()` function. This function is invoked for every file and
passed an object representing the current file as first argument.
- Checkout the file `testfiles/set-tags.js` in this repository for an example that applies basic
fixes and tries to fetch lyrics and cover art.
fixes and tries to fetch lyrics and cover art when according settings are passed (e.g.
`--script-settings addCover=1 addLyrics=1`).
- For debugging, the option `--pedantic debug` is very useful. You may also add
`--script-settings dryRun=1` and check for that setting within the script as shown in the
mentioned example script.
@ -400,6 +401,11 @@ Here are some Bash examples which illustrate getting and setting tag information
file without modifying it by returning a falsy value). If the specified path is relative, it
is interpreted relative to current directory of the file (and *not* to the current working
directory of the tag editor).
- It is also possible to open another file via `utility.openFile(path)`. This makes it possible
to copy tags over from another file, e.g. to insert tags back from original files that have
been lost when converting to a different format. The mentioned example script `set-tags.js`
also demonstrates this for covers and lyrics when according script settings are passed (e.g.
`--script-settings addCover=1 originalDir=… originalExt=…`).
##### Further useful commands
* Let the tag editor return with a non-zero exit code even if only non-fatal problems have been encountered

View File

@ -59,6 +59,8 @@ constexpr auto nativeUtf16Encoding = TagParser::TagTextEncoding::
#endif
;
const std::string UtilityObject::s_defaultContext = std::string("executing JavaScript");
UtilityObject::UtilityObject(QJSEngine *engine)
: QObject(engine)
, m_engine(engine)
@ -86,9 +88,8 @@ void UtilityObject::diag(const QString &level, const QString &message, const QSt
{ 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());
context.isEmpty() ? (m_context ? *m_context : s_defaultContext) : context.toStdString());
}
int UtilityObject::exec()
@ -127,6 +128,25 @@ QJSValue UtilityObject::readFile(const QString &path)
return QJSValue();
}
QJSValue UtilityObject::openFile(const QString &path)
{
if (!m_diag) {
return QJSValue();
}
auto mediaFileInfo = std::make_unique<TagParser::MediaFileInfo>(path.toStdString());
auto feedback = TagParser::AbortableProgressFeedback();
try {
mediaFileInfo->open(true);
mediaFileInfo->parseEverything(*m_diag, feedback);
} catch (const std::exception &e) {
m_diag->emplace_back(TagParser::DiagLevel::Critical, CppUtilities::argsToString("Unable to open \"", mediaFileInfo->path(), "\": ", e.what()),
m_context ? *m_context : s_defaultContext);
return QJSValue();
}
auto mediaFileInfoObj = new MediaFileInfoObject(std::move(mediaFileInfo), *m_diag, m_engine, false, m_engine);
return m_engine->newQObject(mediaFileInfoObj);
}
QString UtilityObject::formatName(const QString &str) const
{
return Utility::formatName(str);
@ -571,6 +591,13 @@ MediaFileInfoObject::MediaFileInfoObject(
{
}
MediaFileInfoObject::MediaFileInfoObject(
std::unique_ptr<TagParser::MediaFileInfo> &&mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent)
: MediaFileInfoObject(*mediaFileInfo.get(), diag, engine, quiet, parent)
{
m_f_owned = std::move(mediaFileInfo);
}
MediaFileInfoObject::~MediaFileInfoObject()
{
}
@ -580,11 +607,21 @@ QString MediaFileInfoObject::path() const
return QString::fromStdString(m_f.path());
}
bool MediaFileInfoObject::isPathRelative() const
{
return QFileInfo(path()).isRelative();
}
QString MediaFileInfoObject::name() const
{
return QString::fromStdString(m_f.fileName());
}
QString MediaFileInfoObject::nameWithoutExtension() const
{
return QString::fromStdString(m_f.fileName(true));
}
QString MediaFileInfoObject::extension() const
{
return QString::fromStdString(m_f.extension());

View File

@ -54,6 +54,7 @@ public Q_SLOTS:
QJSValue readEnvironmentVariable(const QString &variable, const QJSValue &defaultValue = QJSValue()) const;
QJSValue readDirectory(const QString &path);
QJSValue readFile(const QString &path);
QJSValue openFile(const QString &path);
QString formatName(const QString &str) const;
QString fixUmlauts(const QString &str) const;
@ -63,13 +64,14 @@ public Q_SLOTS:
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
QJSValue queryTekstowo(const QJSValue &songDescription);
QByteArray convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format);
QByteArray convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format = QString());
private:
static QtGui::SongDescription makeSongDescription(const QJSValue &obj);
QJSEngine *m_engine;
const std::string *m_context;
static const std::string s_defaultContext;
TagParser::Diagnostics *m_diag;
};
@ -186,7 +188,9 @@ inline TagParser::Tag &TagObject::tag()
class MediaFileInfoObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString path READ path)
Q_PROPERTY(bool pathRelative READ isPathRelative)
Q_PROPERTY(QString name READ name)
Q_PROPERTY(QString nameWithoutExtension READ nameWithoutExtension)
Q_PROPERTY(QString extension READ extension)
Q_PROPERTY(QString containingDirectory READ containingDirectory)
Q_PROPERTY(QString savePath READ savePath WRITE setSavePath)
@ -195,11 +199,15 @@ class MediaFileInfoObject : public QObject {
public:
explicit MediaFileInfoObject(
TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent = nullptr);
explicit MediaFileInfoObject(std::unique_ptr<TagParser::MediaFileInfo> &&mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine,
bool quiet, QObject *parent = nullptr);
~MediaFileInfoObject() override;
TagParser::MediaFileInfo &fileInfo();
QString path() const;
bool isPathRelative() const;
QString name() const;
QString nameWithoutExtension() const;
QString extension() const;
QString containingDirectory() const;
QString savePath() const;
@ -212,6 +220,7 @@ public Q_SLOTS:
private:
TagParser::MediaFileInfo &m_f;
std::unique_ptr<TagParser::MediaFileInfo> m_f_owned;
TagParser::Diagnostics &m_diag;
QJSEngine *m_engine;
QList<TagObject *> m_tags;

View File

@ -1,3 +1,5 @@
const fileCache = {};
export function isString(value) {
return typeof(value) === "string" || value instanceof String;
}
@ -13,10 +15,49 @@ export function logTagInfo(file, tag) {
utility.diag("debug", Object.keys(fields).sort().join(", "), "supported fields");
// log 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 + ")");
for (const [key, values] of Object.entries(fields)) {
for (const value of values) {
const content = value.content;
if (content !== undefined && content !== null && !(content instanceof ArrayBuffer)) {
utility.diag("debug", content, key + " (" + value.type + ")");
}
}
}
}
export function readFieldContents(file, fieldName) {
const values = [];
for (const tag of file.tags) {
if (tag.type === "ID3v1 tag") {
// due to its limitations ID3v1 tags may have truncated contents; so just ignore them
// for the sake of this script
continue;
}
for (const value of tag.fields[fieldName]) {
const content = value.content;
if (content !== undefined && content !== null) {
values.push(content);
}
}
if (values.length) {
// just return the contents from the first tag that has any for now
break;
}
}
return values;
}
export function cacheValue(cache, key, generator) {
const cachedValue = cache[key];
return cachedValue ? cachedValue : (cache[key] = generator());
}
export function openOriginalFile(file) {
const originalDir = settings.originalDir;
const originalExt = settings.originalExt || file.extension;
if (originalDir && file.pathRelative) {
const name = file.nameWithoutExtension;
const path = [originalDir, file.containingDirectory, name + originalExt].join("/");
return cacheValue(fileCache, path, () => utility.openFile(path));
}
}

View File

@ -1,26 +1,23 @@
import * as helpers from "helpers.js"
const lyricsCache = {};
const coverCache = {};
const albumColumn = 1;
export function queryLyrics(searchCriteria) {
return cacheValue(lyricsCache, searchCriteria.title + "_" + searchCriteria.artist, () => {
return helpers.cacheValue(lyricsCache, searchCriteria.title + "_" + searchCriteria.artist, () => {
utility.log(" - Querying lyrics for '" + searchCriteria.title + "' from '" + searchCriteria.artist + "' ...");
return queryLyricsFromProviders(["Tekstowo", "MakeItPersonal"], searchCriteria)
});
}
export function queryCover(searchCriteria) {
return cacheValue(coverCache, searchCriteria.album + "_" + searchCriteria.artist, () => {
return helpers.cacheValue(coverCache, searchCriteria.album + "_" + searchCriteria.artist, () => {
utility.log(" - Querying cover art for '" + searchCriteria.album + "' from '" + searchCriteria.artist + "' ...");
return queryCoverFromProvider("MusicBrainz", searchCriteria);
});
}
function cacheValue(cache, key, generator) {
const cachedValue = cache[key];
return cachedValue ? cachedValue : (cache[key] = generator());
}
function waitFor(signal) {
signal.connect(() => { utility.exit(); });
utility.exec();

View File

@ -57,11 +57,29 @@ function addTotalNumberOfTracks(file, tag) {
}
}
function addFieldFromOriginalFile(file, tag, fieldName) {
const originalFile = helpers.openOriginalFile(file);
if (!originalFile) {
return false;
}
utility.diag("debug", "Trying to take over " + fieldName + " from \"" + originalFile.path + "\".");
const contents = helpers.readFieldContents(originalFile, fieldName);
if (!contents.length) {
utility.diag("debug", "No " + fieldName + " found in original file.");
return false;
}
tag.fields[fieldName] = contents;
return true;
}
function addLyrics(file, tag) {
const fields = tag.fields;
if (fields.lyrics.length) {
return; // skip if already assigned
}
if (addFieldFromOriginalFile(file, tag, "lyrics")) {
return; // skip fetching via meta-data search if lyrics could be taken over from original file
}
const firstTitle = fields.title?.[0]?.content;
const firstArtist = fields.artist?.[0]?.content;
if (firstTitle && firstArtist) {
@ -74,6 +92,17 @@ function addCover(file, tag) {
if (fields.cover.length) {
return; // skip if already assigned
}
if (addFieldFromOriginalFile(file, tag, "cover")) {
// ensure the cover's resolution is below a certain size to avoid bloating the file
const convertedCovers = [];
const maxSizeInt = parseInt(settings.coverMaxSize || 512);
const maxSize = Qt.size(maxSizeInt, maxSizeInt);
for (const cover of fields.cover) {
convertedCovers.push(utility.convertImage(cover, maxSize));
}
fields.cover = convertedCovers;
return; // skip fetching via meta-data search if cover could be taken over from original file
}
const firstAlbum = fields.album?.[0]?.content?.replace(/ \(.*\)/, '');
const firstArtist = fields.artist?.[0]?.content;
if (firstAlbum && firstArtist) {