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);
|
||||
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 {
|
||||
friend struct std::hash<FieldId>;
|
||||
|
||||
|
|
|
@ -448,17 +448,6 @@ struct Id3v2Cover {
|
|||
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)
|
||||
{
|
||||
auto &fields = tag->fields();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "./mediafileinfoobject.h"
|
||||
#include "./fieldmapping.h"
|
||||
#include "./helper.h"
|
||||
|
||||
#include "../application/knownfieldmodel.h"
|
||||
#include "../dbquery/dbquery.h"
|
||||
|
@ -7,10 +8,13 @@
|
|||
|
||||
#include <tagparser/abstracttrack.h>
|
||||
#include <tagparser/exceptions.h>
|
||||
#include <tagparser/id3/id3v2tag.h>
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
#include <tagparser/progressfeedback.h>
|
||||
#include <tagparser/signature.h>
|
||||
#include <tagparser/tag.h>
|
||||
#include <tagparser/tagvalue.h>
|
||||
#include <tagparser/vorbis/vorbiscomment.h>
|
||||
|
||||
#include <qtutilities/misc/conversion.h>
|
||||
|
||||
|
@ -19,9 +23,12 @@
|
|||
|
||||
#include <qtutilities/misc/compat.h>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QHash>
|
||||
#include <QImage>
|
||||
#include <QJSEngine>
|
||||
#include <QJSValueIterator>
|
||||
#include <QRegularExpression>
|
||||
|
@ -147,6 +154,24 @@ QJSValue UtilityObject::queryTekstowo(const QJSValue &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)
|
||||
{
|
||||
const auto val = obj.property(propertyName);
|
||||
|
@ -293,12 +318,21 @@ bool TagValueObject::isInitial() const
|
|||
|
||||
TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const
|
||||
{
|
||||
auto res = TagParser::TagValue();
|
||||
if (m_content.isUndefined() || m_content.isNull()) {
|
||||
return TagParser::TagValue();
|
||||
return res;
|
||||
}
|
||||
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);
|
||||
if (const auto variant = m_content.toVariant(); variant.userType() == QMetaType::QByteArray) {
|
||||
const auto bytes = variant.toByteArray();
|
||||
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()
|
||||
|
@ -415,6 +449,40 @@ QJSValue &TagObject::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()
|
||||
{
|
||||
auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag
|
||||
|
@ -460,7 +528,7 @@ void TagObject::applyChanges()
|
|||
}
|
||||
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,
|
||||
value.isNull()
|
||||
? CppUtilities::argsToString(" - delete ", propertyName.toStdString(), '[', i, ']')
|
||||
|
@ -474,6 +542,18 @@ void TagObject::applyChanges()
|
|||
printJsValue(tagValueObj->initialContent()), "' to '", printJsValue(tagValueObj->content()), '\''))),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ public Q_SLOTS:
|
|||
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
|
||||
QJSValue queryTekstowo(const QJSValue &songDescription);
|
||||
|
||||
QByteArray convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format);
|
||||
|
||||
private:
|
||||
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) {
|
||||
signal.connect(() => { utility.exit(); });
|
||||
utility.exec();
|
||||
}
|
||||
|
||||
function queryProvider(provider, searchCriteria) {
|
||||
const lyricsModel = utility["query" + provider](searchCriteria);
|
||||
if (!lyricsModel.areResultsAvailable) {
|
||||
waitFor(lyricsModel.resultsAvailable);
|
||||
function queryLyricsFromProvider(provider, searchCriteria) {
|
||||
const model = utility["query" + provider](searchCriteria);
|
||||
if (!model.areResultsAvailable) {
|
||||
waitFor(model.resultsAvailable);
|
||||
}
|
||||
if (!lyricsModel.fetchLyrics(lyricsModel.index(0, 0))) {
|
||||
waitFor(lyricsModel.lyricsAvailable);
|
||||
if (!model.fetchLyrics(model.index(0, 0))) {
|
||||
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")) {
|
||||
return undefined;
|
||||
}
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
function queryProviders(providers, searchCriteria) {
|
||||
function queryLyricsFromProviders(providers, searchCriteria) {
|
||||
for (const provider of providers) {
|
||||
const res = queryProvider(provider, searchCriteria);
|
||||
const res = queryLyricsFromProvider(provider, searchCriteria);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function queryLyrics(searchCriteria) {
|
||||
const cacheKey = searchCriteria.title + "_" + searchCriteria.artist;
|
||||
const cachedValue = cache[cacheKey];
|
||||
return cachedValue
|
||||
? cachedValue
|
||||
: cache[cacheKey] = queryProviders(["Tekstowo", "MakeItPersonal"], searchCriteria);
|
||||
function queryCoverFromProvider(provider, searchCriteria) {
|
||||
const context = searchCriteria.album + " from " + searchCriteria.artist;
|
||||
const model = utility["query" + provider](searchCriteria);
|
||||
if (!model.areResultsAvailable) {
|
||||
waitFor(model.resultsAvailable);
|
||||
}
|
||||
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) {
|
||||
// assume the number of disks is always one for now
|
||||
const fields = tag.fields;
|
||||
|
@ -102,4 +114,5 @@ function changeTagFields(file, tag) {
|
|||
addTotalNumberOfTracks(file, tag);
|
||||
addMiscFields(file, tag);
|
||||
addLyrics(file, tag);
|
||||
addCover(file, tag);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue