Extend script API; allow copying tags from other files
This commit is contained in:
parent
e1e979f9f5
commit
88989ff986
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue