Allow adding cover via JavaScript
This commit is contained in:
parent
7063f1bf03
commit
c4f7d195a0
11
cli/helper.h
11
cli/helper.h
|
@ -54,6 +54,17 @@ const std::vector<std::string_view> &id3v2CoverTypeNames();
|
||||||
CoverType id3v2CoverType(std::string_view coverName);
|
CoverType id3v2CoverType(std::string_view coverName);
|
||||||
std::string_view id3v2CoverName(CoverType coverType);
|
std::string_view id3v2CoverName(CoverType coverType);
|
||||||
|
|
||||||
|
template <class TagType>
|
||||||
|
bool fieldPredicate(CoverType coverType, std::optional<std::string_view> description,
|
||||||
|
const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
|
||||||
|
{
|
||||||
|
const auto &[fieldId, field] = pair;
|
||||||
|
const auto typeMatches
|
||||||
|
= field.isTypeInfoAssigned() ? (field.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType)) : (coverType == 0);
|
||||||
|
const auto descMatches = !description.has_value() || field.value().description() == description.value();
|
||||||
|
return typeMatches && descMatches;
|
||||||
|
}
|
||||||
|
|
||||||
class FieldId {
|
class FieldId {
|
||||||
friend struct std::hash<FieldId>;
|
friend struct std::hash<FieldId>;
|
||||||
|
|
||||||
|
|
|
@ -448,17 +448,6 @@ struct Id3v2Cover {
|
||||||
std::optional<std::string_view> description;
|
std::optional<std::string_view> description;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class TagType>
|
|
||||||
bool fieldPredicate(CoverType coverType, std::optional<std::string_view> description,
|
|
||||||
const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
|
|
||||||
{
|
|
||||||
const auto &[fieldId, field] = pair;
|
|
||||||
const auto typeMatches
|
|
||||||
= field.isTypeInfoAssigned() ? (field.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType)) : (coverType == 0);
|
|
||||||
const auto descMatches = !description.has_value() || field.value().description() == description.value();
|
|
||||||
return typeMatches && descMatches;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<Id3v2Cover> &&values)
|
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<Id3v2Cover> &&values)
|
||||||
{
|
{
|
||||||
auto &fields = tag->fields();
|
auto &fields = tag->fields();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "./mediafileinfoobject.h"
|
#include "./mediafileinfoobject.h"
|
||||||
#include "./fieldmapping.h"
|
#include "./fieldmapping.h"
|
||||||
|
#include "./helper.h"
|
||||||
|
|
||||||
#include "../application/knownfieldmodel.h"
|
#include "../application/knownfieldmodel.h"
|
||||||
#include "../dbquery/dbquery.h"
|
#include "../dbquery/dbquery.h"
|
||||||
|
@ -7,10 +8,13 @@
|
||||||
|
|
||||||
#include <tagparser/abstracttrack.h>
|
#include <tagparser/abstracttrack.h>
|
||||||
#include <tagparser/exceptions.h>
|
#include <tagparser/exceptions.h>
|
||||||
|
#include <tagparser/id3/id3v2tag.h>
|
||||||
#include <tagparser/mediafileinfo.h>
|
#include <tagparser/mediafileinfo.h>
|
||||||
#include <tagparser/progressfeedback.h>
|
#include <tagparser/progressfeedback.h>
|
||||||
|
#include <tagparser/signature.h>
|
||||||
#include <tagparser/tag.h>
|
#include <tagparser/tag.h>
|
||||||
#include <tagparser/tagvalue.h>
|
#include <tagparser/tagvalue.h>
|
||||||
|
#include <tagparser/vorbis/vorbiscomment.h>
|
||||||
|
|
||||||
#include <qtutilities/misc/conversion.h>
|
#include <qtutilities/misc/conversion.h>
|
||||||
|
|
||||||
|
@ -19,9 +23,12 @@
|
||||||
|
|
||||||
#include <qtutilities/misc/compat.h>
|
#include <qtutilities/misc/compat.h>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QByteArray>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QImage>
|
||||||
#include <QJSEngine>
|
#include <QJSEngine>
|
||||||
#include <QJSValueIterator>
|
#include <QJSValueIterator>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
@ -147,6 +154,24 @@ QJSValue UtilityObject::queryTekstowo(const QJSValue &songDescription)
|
||||||
return m_engine->newQObject(QtGui::queryTekstowo(makeSongDescription(songDescription)));
|
return m_engine->newQObject(QtGui::queryTekstowo(makeSongDescription(songDescription)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray UtilityObject::convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format)
|
||||||
|
{
|
||||||
|
auto image = QImage::fromData(imageData);
|
||||||
|
if (image.isNull()) {
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
if (!maxSize.isNull() && (image.width() > maxSize.width() || image.height() > maxSize.height())) {
|
||||||
|
image = image.scaled(maxSize.width(), maxSize.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
if (image.isNull()) {
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto newData = QByteArray();
|
||||||
|
auto buffer = QBuffer(&newData);
|
||||||
|
auto res = buffer.open(QIODevice::WriteOnly) && image.save(&buffer, format.isEmpty() ? "JPEG" : format.toUtf8().data());
|
||||||
|
return res ? newData : imageData;
|
||||||
|
}
|
||||||
|
|
||||||
static QString propertyString(const QJSValue &obj, const QString &propertyName)
|
static QString propertyString(const QJSValue &obj, const QString &propertyName)
|
||||||
{
|
{
|
||||||
const auto val = obj.property(propertyName);
|
const auto val = obj.property(propertyName);
|
||||||
|
@ -293,12 +318,21 @@ bool TagValueObject::isInitial() const
|
||||||
|
|
||||||
TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const
|
TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const
|
||||||
{
|
{
|
||||||
|
auto res = TagParser::TagValue();
|
||||||
if (m_content.isUndefined() || m_content.isNull()) {
|
if (m_content.isUndefined() || m_content.isNull()) {
|
||||||
return TagParser::TagValue();
|
return res;
|
||||||
}
|
}
|
||||||
const auto str = m_content.toString();
|
if (const auto variant = m_content.toVariant(); variant.userType() == QMetaType::QByteArray) {
|
||||||
return TagParser::TagValue(reinterpret_cast<const char *>(str.utf16()), static_cast<std::size_t>(str.size()) * (sizeof(ushort) / sizeof(char)),
|
const auto bytes = variant.toByteArray();
|
||||||
nativeUtf16Encoding, encoding);
|
const auto container = TagParser::parseSignature(bytes.data(), static_cast<std::size_t>(bytes.size()));
|
||||||
|
res.assignData(bytes.data(), static_cast<std::size_t>(bytes.size()), TagParser::TagDataType::Binary);
|
||||||
|
res.setMimeType(TagParser::containerMimeType(container));
|
||||||
|
} else {
|
||||||
|
const auto str = m_content.toString();
|
||||||
|
res.assignText(reinterpret_cast<const char *>(str.utf16()), static_cast<std::size_t>(str.size()) * (sizeof(ushort) / sizeof(char)),
|
||||||
|
nativeUtf16Encoding, encoding);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TagValueObject::flagChange()
|
void TagValueObject::flagChange()
|
||||||
|
@ -415,6 +449,40 @@ QJSValue &TagObject::fields()
|
||||||
return m_fields;
|
return m_fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// \brief Sets the first of the specified \a values as front-cover with no description replacing any existing cover values.
|
||||||
|
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<TagParser::TagValue> &&values)
|
||||||
|
{
|
||||||
|
auto &fields = tag->fields();
|
||||||
|
const auto id = tag->fieldId(TagParser::KnownField::Cover);
|
||||||
|
const auto range = fields.equal_range(id);
|
||||||
|
const auto first = range.first;
|
||||||
|
|
||||||
|
constexpr auto coverType = CoverType(3); // assume front cover
|
||||||
|
constexpr auto description = std::optional<std::string_view>(); // assume no description
|
||||||
|
|
||||||
|
for (auto &tagValue : values) {
|
||||||
|
// check whether there is already a tag value with the current type and description
|
||||||
|
auto pair = std::find_if(first, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, std::placeholders::_1));
|
||||||
|
if (pair != range.second) {
|
||||||
|
// there is already a tag value with the current type and description
|
||||||
|
// -> update this value
|
||||||
|
pair->second.setValue(tagValue);
|
||||||
|
// check whether there are more values with the current type and description
|
||||||
|
while ((pair = std::find_if(++pair, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, std::placeholders::_1)))
|
||||||
|
!= range.second) {
|
||||||
|
// -> remove these values as we only support one value of a type/description in the same tag
|
||||||
|
pair->second.setValue(TagParser::TagValue());
|
||||||
|
}
|
||||||
|
} else if (!tagValue.isEmpty()) {
|
||||||
|
using FieldType = typename TagType::FieldType;
|
||||||
|
auto newField = FieldType(id, tagValue);
|
||||||
|
newField.setTypeInfo(static_cast<typename FieldType::TypeInfoType>(coverType));
|
||||||
|
fields.insert(std::pair(id, std::move(newField)));
|
||||||
|
}
|
||||||
|
break; // allow only setting one value for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TagObject::applyChanges()
|
void TagObject::applyChanges()
|
||||||
{
|
{
|
||||||
auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag
|
auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag
|
||||||
|
@ -460,7 +528,7 @@ void TagObject::applyChanges()
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto &value = values.emplace_back(tagValueObj->toTagValue(encoding));
|
auto &value = values.emplace_back(tagValueObj->toTagValue(encoding));
|
||||||
m_diag.emplace_back(TagParser::DiagLevel::Debug,
|
m_diag.emplace_back(TagParser::DiagLevel::Debug,
|
||||||
value.isNull()
|
value.isNull()
|
||||||
? CppUtilities::argsToString(" - delete ", propertyName.toStdString(), '[', i, ']')
|
? CppUtilities::argsToString(" - delete ", propertyName.toStdString(), '[', i, ']')
|
||||||
|
@ -474,6 +542,18 @@ void TagObject::applyChanges()
|
||||||
printJsValue(tagValueObj->initialContent()), "' to '", printJsValue(tagValueObj->content()), '\''))),
|
printJsValue(tagValueObj->initialContent()), "' to '", printJsValue(tagValueObj->content()), '\''))),
|
||||||
std::string());
|
std::string());
|
||||||
}
|
}
|
||||||
|
// assign cover values of ID3v2/VorbisComment tags as front-cover with no description
|
||||||
|
if (field == TagParser::KnownField::Cover && !values.empty()) {
|
||||||
|
switch (m_tag.type()) {
|
||||||
|
case TagParser::TagType::Id3v2Tag:
|
||||||
|
setId3v2CoverValues(static_cast<TagParser::Id3v2Tag *>(&m_tag), std::move(values));
|
||||||
|
continue;
|
||||||
|
case TagParser::TagType::VorbisComment:
|
||||||
|
setId3v2CoverValues(static_cast<TagParser::VorbisComment *>(&m_tag), std::move(values));
|
||||||
|
continue;
|
||||||
|
default:;
|
||||||
|
}
|
||||||
|
}
|
||||||
m_tag.setValues(field, values);
|
m_tag.setValues(field, values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ public Q_SLOTS:
|
||||||
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
|
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
|
||||||
QJSValue queryTekstowo(const QJSValue &songDescription);
|
QJSValue queryTekstowo(const QJSValue &songDescription);
|
||||||
|
|
||||||
|
QByteArray convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QtGui::SongDescription makeSongDescription(const QJSValue &obj);
|
static QtGui::SongDescription makeSongDescription(const QJSValue &obj);
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,79 @@
|
||||||
const cache = {};
|
const lyricsCache = {};
|
||||||
|
const coverCache = {};
|
||||||
|
const albumColumn = 1;
|
||||||
|
|
||||||
|
export function queryLyrics(searchCriteria) {
|
||||||
|
return cacheValue(lyricsCache, searchCriteria.title + "_" + searchCriteria.artist, () => {
|
||||||
|
return queryLyricsFromProviders(["Tekstowo", "MakeItPersonal"], searchCriteria)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryCover(searchCriteria) {
|
||||||
|
return cacheValue(coverCache, searchCriteria.album + "_" + searchCriteria.artist, () => {
|
||||||
|
return queryCoverFromProvider("MusicBrainz", searchCriteria);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheValue(cache, key, generator) {
|
||||||
|
const cachedValue = cache[key];
|
||||||
|
return cachedValue ? cachedValue : cache[key] = generator();
|
||||||
|
}
|
||||||
|
|
||||||
function waitFor(signal) {
|
function waitFor(signal) {
|
||||||
signal.connect(() => { utility.exit(); });
|
signal.connect(() => { utility.exit(); });
|
||||||
utility.exec();
|
utility.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryProvider(provider, searchCriteria) {
|
function queryLyricsFromProvider(provider, searchCriteria) {
|
||||||
const lyricsModel = utility["query" + provider](searchCriteria);
|
const model = utility["query" + provider](searchCriteria);
|
||||||
if (!lyricsModel.areResultsAvailable) {
|
if (!model.areResultsAvailable) {
|
||||||
waitFor(lyricsModel.resultsAvailable);
|
waitFor(model.resultsAvailable);
|
||||||
}
|
}
|
||||||
if (!lyricsModel.fetchLyrics(lyricsModel.index(0, 0))) {
|
if (!model.fetchLyrics(model.index(0, 0))) {
|
||||||
waitFor(lyricsModel.lyricsAvailable);
|
waitFor(model.lyricsAvailable);
|
||||||
}
|
}
|
||||||
const lyrics = lyricsModel.lyricsValue(lyricsModel.index(0, 0));
|
const lyrics = model.lyricsValue(model.index(0, 0));
|
||||||
if (lyrics && lyrics.startsWith("Bots have beat this API")) {
|
if (lyrics && lyrics.startsWith("Bots have beat this API")) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return lyrics;
|
return lyrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryProviders(providers, searchCriteria) {
|
function queryLyricsFromProviders(providers, searchCriteria) {
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
const res = queryProvider(provider, searchCriteria);
|
const res = queryLyricsFromProvider(provider, searchCriteria);
|
||||||
if (res) {
|
if (res) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryLyrics(searchCriteria) {
|
function queryCoverFromProvider(provider, searchCriteria) {
|
||||||
const cacheKey = searchCriteria.title + "_" + searchCriteria.artist;
|
const context = searchCriteria.album + " from " + searchCriteria.artist;
|
||||||
const cachedValue = cache[cacheKey];
|
const model = utility["query" + provider](searchCriteria);
|
||||||
return cachedValue
|
if (!model.areResultsAvailable) {
|
||||||
? cachedValue
|
waitFor(model.resultsAvailable);
|
||||||
: cache[cacheKey] = queryProviders(["Tekstowo", "MakeItPersonal"], searchCriteria);
|
}
|
||||||
|
const albumUpper = searchCriteria.album.toUpperCase();
|
||||||
|
utility.diag("debug", model.rowCount(), "rows");
|
||||||
|
let row = 0, rowCount = model.rowCount();
|
||||||
|
for (; row != rowCount; ++row) {
|
||||||
|
const album = model.data(model.index(row, albumColumn));
|
||||||
|
if (album && album.toUpperCase() === albumUpper) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row === rowCount) {
|
||||||
|
utility.diag("debug", "unable to find meta-data on " + provider, context);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!model.fetchCover(model.index(row, 0))) {
|
||||||
|
waitFor(model.coverAvailable);
|
||||||
|
}
|
||||||
|
let cover = model.coverValue(model.index(row, 0));
|
||||||
|
if (cover instanceof ArrayBuffer) {
|
||||||
|
utility.diag("debug", "found cover", context);
|
||||||
|
cover = utility.convertImage(cover, Qt.size(512, 512), "JPEG");
|
||||||
|
}
|
||||||
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,18 @@ function addLyrics(file, tag) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addCover(file, tag) {
|
||||||
|
const fields = tag.fields;
|
||||||
|
if (fields.cover.length) {
|
||||||
|
return; // skip if already assigned
|
||||||
|
}
|
||||||
|
const firstAlbum = fields.album?.[0]?.content?.replace(/ \(.*\)/, '');
|
||||||
|
const firstArtist = fields.artist?.[0]?.content;
|
||||||
|
if (firstAlbum && firstArtist) {
|
||||||
|
fields.cover = metadatasearch.queryCover({album: firstAlbum, artist: firstArtist});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addMiscFields(file, tag) {
|
function addMiscFields(file, tag) {
|
||||||
// assume the number of disks is always one for now
|
// assume the number of disks is always one for now
|
||||||
const fields = tag.fields;
|
const fields = tag.fields;
|
||||||
|
@ -102,4 +114,5 @@ function changeTagFields(file, tag) {
|
||||||
addTotalNumberOfTracks(file, tag);
|
addTotalNumberOfTracks(file, tag);
|
||||||
addMiscFields(file, tag);
|
addMiscFields(file, tag);
|
||||||
addLyrics(file, tag);
|
addLyrics(file, tag);
|
||||||
|
addCover(file, tag);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue