allow querying cover from Cover Art Archive
This commit is contained in:
parent
0adb77fd58
commit
70b94fa5fa
|
@ -233,6 +233,7 @@ endif()
|
||||||
|
|
||||||
# enable Qt Widgets GUI
|
# enable Qt Widgets GUI
|
||||||
# disable new ABI (can't catch ios_base::failure with new ABI)
|
# disable new ABI (can't catch ios_base::failure with new ABI)
|
||||||
|
# enable code for debugging
|
||||||
add_definitions(
|
add_definitions(
|
||||||
-DGUI_QTWIDGETS
|
-DGUI_QTWIDGETS
|
||||||
-DMODEL_UNDO_SUPPORT
|
-DMODEL_UNDO_SUPPORT
|
||||||
|
@ -240,6 +241,10 @@ add_definitions(
|
||||||
${JS_DEFINITION}
|
${JS_DEFINITION}
|
||||||
${WEBVIEW_DEFINITION}
|
${WEBVIEW_DEFINITION}
|
||||||
)
|
)
|
||||||
|
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
add_definitions(-DDEBUG_BUILD)
|
||||||
|
message(STATUS "Debug build enabled.")
|
||||||
|
endif()
|
||||||
|
|
||||||
# enable moc, uic and rcc
|
# enable moc, uic and rcc
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
|
@ -236,7 +236,8 @@ KnownFieldModel &dbQueryFields()
|
||||||
<< KnownFieldModel::mkItem(KnownField::Album)
|
<< KnownFieldModel::mkItem(KnownField::Album)
|
||||||
<< KnownFieldModel::mkItem(KnownField::Album)
|
<< KnownFieldModel::mkItem(KnownField::Album)
|
||||||
<< KnownFieldModel::mkItem(KnownField::Year)
|
<< KnownFieldModel::mkItem(KnownField::Year)
|
||||||
<< KnownFieldModel::mkItem(KnownField::Genre));
|
<< KnownFieldModel::mkItem(KnownField::Genre)
|
||||||
|
<< KnownFieldModel::mkItem(KnownField::Cover, Qt::Unchecked));
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,6 +247,12 @@ QString &musicBrainzUrl()
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString &coverArtArchiveUrl()
|
||||||
|
{
|
||||||
|
static QString v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
// renaming files dialog
|
// renaming files dialog
|
||||||
int &scriptSource()
|
int &scriptSource()
|
||||||
{
|
{
|
||||||
|
@ -395,6 +402,7 @@ void restore()
|
||||||
dbQueryOverride() = settings.value(QStringLiteral("override"), true).toBool();
|
dbQueryOverride() = settings.value(QStringLiteral("override"), true).toBool();
|
||||||
dbQueryFields().restore(settings, QStringLiteral("fields"));
|
dbQueryFields().restore(settings, QStringLiteral("fields"));
|
||||||
musicBrainzUrl() = settings.value(QStringLiteral("musicbrainzurl")).toString();
|
musicBrainzUrl() = settings.value(QStringLiteral("musicbrainzurl")).toString();
|
||||||
|
coverArtArchiveUrl() = settings.value(QStringLiteral("coverartarchiveurl")).toString();
|
||||||
settings.endGroup();
|
settings.endGroup();
|
||||||
|
|
||||||
settings.beginGroup(QStringLiteral("renamedlg"));
|
settings.beginGroup(QStringLiteral("renamedlg"));
|
||||||
|
@ -471,6 +479,7 @@ void save()
|
||||||
settings.setValue(QStringLiteral("override"), dbQueryOverride());
|
settings.setValue(QStringLiteral("override"), dbQueryOverride());
|
||||||
dbQueryFields().save(settings, QStringLiteral("fields"));
|
dbQueryFields().save(settings, QStringLiteral("fields"));
|
||||||
settings.setValue(QStringLiteral("musicbrainzurl"), musicBrainzUrl());
|
settings.setValue(QStringLiteral("musicbrainzurl"), musicBrainzUrl());
|
||||||
|
settings.setValue(QStringLiteral("coverartarchiveurl"), coverArtArchiveUrl());
|
||||||
settings.endGroup();
|
settings.endGroup();
|
||||||
|
|
||||||
settings.beginGroup(QStringLiteral("renamedlg"));
|
settings.beginGroup(QStringLiteral("renamedlg"));
|
||||||
|
|
|
@ -96,6 +96,7 @@ bool &dbQueryWidgetShown();
|
||||||
bool &dbQueryOverride();
|
bool &dbQueryOverride();
|
||||||
KnownFieldModel &dbQueryFields();
|
KnownFieldModel &dbQueryFields();
|
||||||
QString &musicBrainzUrl();
|
QString &musicBrainzUrl();
|
||||||
|
QString &coverArtArchiveUrl();
|
||||||
|
|
||||||
// rename files dialog
|
// rename files dialog
|
||||||
int &scriptSource();
|
int &scriptSource();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# MusicBrainz API
|
# MusicBrainz API
|
||||||
Available at ```http://musicbrainz.org/ws/2```
|
* Regular meta data is available at ```http://musicbrainz.org/ws/2``` and ```http://mb.videolan.org/ws/2```.
|
||||||
|
* Cover art is available at ```http://coverartarchive.org```.
|
||||||
|
|
||||||
## Most useful queries
|
## Most useful queries
|
||||||
* artist by name
|
* artist by name
|
||||||
|
@ -22,7 +23,10 @@ Available at ```http://musicbrainz.org/ws/2```
|
||||||
```
|
```
|
||||||
/release?query="$album_name"
|
/release?query="$album_name"
|
||||||
```
|
```
|
||||||
* cover art: TODO
|
* cover art by album ID
|
||||||
|
```
|
||||||
|
/release/$album_id/front-500
|
||||||
|
```
|
||||||
* lyrics: TODO
|
* lyrics: TODO
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,3 +36,6 @@ Available at ```http://musicbrainz.org/ws/2```
|
||||||
## See also
|
## See also
|
||||||
* [Web Service](https://wiki.musicbrainz.org/Development/JSON_Web_Service)
|
* [Web Service](https://wiki.musicbrainz.org/Development/JSON_Web_Service)
|
||||||
* [Web Service/Search](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search)
|
* [Web Service/Search](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search)
|
||||||
|
|
||||||
|
# Other sources for meta data
|
||||||
|
TODO
|
||||||
|
|
|
@ -11,10 +11,15 @@
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
#define xmlReader m_reader
|
#include <map>
|
||||||
#include <qtutilities/misc/xmlparsermacros.h>
|
#ifdef DEBUG_BUILD
|
||||||
|
# include <iostream>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
using namespace Utility;
|
using namespace Utility;
|
||||||
using namespace Media;
|
using namespace Media;
|
||||||
|
|
||||||
|
@ -23,25 +28,33 @@ namespace QtGui {
|
||||||
SongDescription::SongDescription() :
|
SongDescription::SongDescription() :
|
||||||
track(0),
|
track(0),
|
||||||
totalTracks(0),
|
totalTracks(0),
|
||||||
disk(0)
|
disk(0),
|
||||||
|
cover(nullptr)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
QueryResultsModel::QueryResultsModel(QObject *parent) :
|
QueryResultsModel::QueryResultsModel(QObject *parent) :
|
||||||
QAbstractTableModel(parent),
|
QAbstractTableModel(parent),
|
||||||
m_resultsAvailable(false)
|
m_resultsAvailable(false),
|
||||||
|
m_fetchingCover(false)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void QueryResultsModel::setResultsAvailable(bool resultsAvailable)
|
void QueryResultsModel::setResultsAvailable(bool resultsAvailable)
|
||||||
{
|
{
|
||||||
if(resultsAvailable != m_resultsAvailable) {
|
if(m_resultsAvailable = resultsAvailable) {
|
||||||
if(m_resultsAvailable = resultsAvailable) {
|
emit this->resultsAvailable();
|
||||||
emit this->resultsAvailable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QueryResultsModel::setFetchingCover(bool fetchingCover)
|
||||||
|
{
|
||||||
|
m_fetchingCover = fetchingCover;
|
||||||
|
}
|
||||||
|
|
||||||
#define returnValue(field) return qstringToTagValue(res.field, TagTextEncoding::Utf16LittleEndian)
|
#define returnValue(field) return qstringToTagValue(res.field, TagTextEncoding::Utf16LittleEndian)
|
||||||
|
|
||||||
|
void QueryResultsModel::abort()
|
||||||
|
{}
|
||||||
|
|
||||||
TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
||||||
{
|
{
|
||||||
if(row < m_results.size()) {
|
if(row < m_results.size()) {
|
||||||
|
@ -63,6 +76,11 @@ TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
||||||
return TagValue(res.track);
|
return TagValue(res.track);
|
||||||
case KnownField::TotalParts:
|
case KnownField::TotalParts:
|
||||||
return TagValue(res.totalTracks);
|
return TagValue(res.totalTracks);
|
||||||
|
case KnownField::Cover:
|
||||||
|
if(!res.cover.isEmpty()) {
|
||||||
|
return TagValue(res.cover.data(), res.cover.size(), TagDataType::Picture);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
@ -167,43 +185,216 @@ int QueryResultsModel::columnCount(const QModelIndex &parent) const
|
||||||
return parent.isValid() ? 0 : (TotalTracksCol + 1);
|
return parent.isValid() ? 0 : (TotalTracksCol + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MusicBrainzResultsModel : public QueryResultsModel
|
const QByteArray *QueryResultsModel::cover(const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
if(!index.parent().isValid() && index.row() < m_results.size()) {
|
||||||
|
const QByteArray &cover = m_results.at(index.row()).cover;
|
||||||
|
if(!cover.isEmpty()) {
|
||||||
|
return &cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Fetches the cover the specified \a index.
|
||||||
|
* \returns True if the cover is immidiately available; false is the cover is fetched asynchronously.
|
||||||
|
*
|
||||||
|
* If the cover is fetched asynchronously the coverAvailable() signal is emitted, when the cover
|
||||||
|
* is available.
|
||||||
|
*
|
||||||
|
* The resultsAvailable() signal is emitted if errors have been added to errorList().
|
||||||
|
*/
|
||||||
|
bool QueryResultsModel::fetchCover(const QModelIndex &)
|
||||||
|
{
|
||||||
|
m_errorList << tr("Fetching the cover is not implemented for the selected provider.");
|
||||||
|
emit resultsAvailable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpResultsModel : public QueryResultsModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
MusicBrainzResultsModel(QNetworkReply *reply);
|
~HttpResultsModel();
|
||||||
~MusicBrainzResultsModel();
|
void addReply(QNetworkReply *reply);
|
||||||
|
void abort();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HttpResultsModel(QNetworkReply *reply);
|
||||||
|
virtual void parseResults(QNetworkReply *reply, const QByteArray &data) = 0;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void parseResults();
|
void handleReplyFinished();
|
||||||
|
|
||||||
private:
|
protected:
|
||||||
QNetworkReply *m_reply;
|
QList<QNetworkReply *> m_replies;
|
||||||
QXmlStreamReader m_reader;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MusicBrainzResultsModel::MusicBrainzResultsModel(QNetworkReply *reply) :
|
HttpResultsModel::HttpResultsModel(QNetworkReply *reply)
|
||||||
m_reply(reply)
|
|
||||||
{
|
{
|
||||||
connect(reply, &QNetworkReply::finished, this, &MusicBrainzResultsModel::parseResults);
|
addReply(reply);
|
||||||
m_reader.setDevice(m_reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicBrainzResultsModel::~MusicBrainzResultsModel()
|
HttpResultsModel::~HttpResultsModel()
|
||||||
{
|
{
|
||||||
if(m_reply) {
|
qDeleteAll(m_replies);
|
||||||
m_reply->abort();
|
}
|
||||||
delete m_reply;
|
|
||||||
|
void copyProperty(const char *property, const QObject *from, QObject *to)
|
||||||
|
{
|
||||||
|
to->setProperty(property, from->property(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResultsModel::handleReplyFinished()
|
||||||
|
{
|
||||||
|
auto *reply = static_cast<QNetworkReply *>(sender());
|
||||||
|
if(reply->error() == QNetworkReply::NoError) {
|
||||||
|
QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||||
|
if(!redirectionTarget.isNull()) {
|
||||||
|
// there's a redirection available
|
||||||
|
// -> resolve new URL
|
||||||
|
const QUrl newUrl = reply->url().resolved(redirectionTarget.toUrl());
|
||||||
|
// -> always follow when retrieving cover art, otherwise ask user
|
||||||
|
bool follow = reply->property("coverArt").toBool();
|
||||||
|
if(!follow) {
|
||||||
|
const QString message = tr("<p>Do you want to redirect form <i>%1</i> to <i>%2</i>?</p>").arg(
|
||||||
|
reply->url().toString(), newUrl.toString());
|
||||||
|
follow = QMessageBox::question(nullptr, tr("Search"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
|
||||||
|
}
|
||||||
|
if(follow) {
|
||||||
|
QNetworkReply *newReply = networkAccessManager().get(QNetworkRequest(newUrl));
|
||||||
|
// retain possible assigned dynamic properties (TODO: implement it in a better way)
|
||||||
|
copyProperty("coverArt", reply, newReply);
|
||||||
|
copyProperty("albumId", reply, newReply);
|
||||||
|
copyProperty("row", reply, newReply);
|
||||||
|
addReply(newReply);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
m_errorList << tr("Redirection to: ") + newUrl.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
#ifdef DEBUG_BUILD
|
||||||
|
std::cerr << "Results from HTTP query:" << std::endl;
|
||||||
|
std::cerr << data.data() << std::endl;
|
||||||
|
#endif
|
||||||
|
parseResults(reply, data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_errorList << reply->errorString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete reply
|
||||||
|
reply->deleteLater();
|
||||||
|
m_replies.removeAll(reply);
|
||||||
|
|
||||||
|
// update status, emit resultsAvailable()
|
||||||
|
setResultsAvailable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResultsModel::addReply(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
m_replies << reply;
|
||||||
|
#ifdef DEBUG_BUILD
|
||||||
|
std::cerr << "HTTP query: " << reply->url().toString().toLocal8Bit().data() << std::endl;
|
||||||
|
#endif
|
||||||
|
connect(reply, &QNetworkReply::finished, this, &HttpResultsModel::handleReplyFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResultsModel::abort()
|
||||||
|
{
|
||||||
|
if(!m_replies.isEmpty()) {
|
||||||
|
qDeleteAll(m_replies);
|
||||||
|
m_replies.clear();
|
||||||
|
// must update status manually because handleReplyFinished() won't be called anymore
|
||||||
|
m_errorList << tr("Aborted by user.");
|
||||||
|
setResultsAvailable(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicBrainzResultsModel::parseResults()
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
|
|
||||||
// check for network error
|
class MusicBrainzResultsModel : public HttpResultsModel
|
||||||
switch(m_reply->error()) {
|
{
|
||||||
case QNetworkReply::NoError:
|
Q_OBJECT
|
||||||
|
private:
|
||||||
|
enum What {
|
||||||
|
MusicBrainzMetaData,
|
||||||
|
CoverArt
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
MusicBrainzResultsModel(QNetworkReply *reply);
|
||||||
|
bool fetchCover(const QModelIndex &index);
|
||||||
|
static QNetworkRequest coverRequest(const QString &albumId);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void parseResults(QNetworkReply *reply, const QByteArray &data);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static map<QString, QByteArray> m_coverData;
|
||||||
|
QXmlStreamReader m_reader;
|
||||||
|
What m_what;
|
||||||
|
};
|
||||||
|
|
||||||
|
map<QString, QByteArray> MusicBrainzResultsModel::m_coverData = map<QString, QByteArray>();
|
||||||
|
|
||||||
|
MusicBrainzResultsModel::MusicBrainzResultsModel(QNetworkReply *reply) :
|
||||||
|
HttpResultsModel(reply)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
if(!index.parent().isValid() && index.row() < m_results.size()) {
|
||||||
|
SongDescription &desc = m_results[index.row()];
|
||||||
|
if(!desc.cover.isEmpty()) {
|
||||||
|
// cover is already available -> nothing to do
|
||||||
|
} else if(!desc.albumId.isEmpty()) {
|
||||||
|
try {
|
||||||
|
// the item belongs to an album which cover has already been fetched
|
||||||
|
desc.cover = m_coverData.at(desc.albumId);
|
||||||
|
} catch(const out_of_range &) {
|
||||||
|
// request the cover art
|
||||||
|
QNetworkReply *reply = queryCoverArtArchive(desc.albumId);
|
||||||
|
addReply(reply);
|
||||||
|
reply->setProperty("coverArt", true);
|
||||||
|
reply->setProperty("albumId", desc.albumId);
|
||||||
|
reply->setProperty("row", index.row());
|
||||||
|
setFetchingCover(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_errorList << tr("Unable to fetch cover: Album ID is unknown.");
|
||||||
|
emit resultsAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MusicBrainzResultsModel::parseResults(QNetworkReply *reply, const QByteArray &data)
|
||||||
|
{
|
||||||
|
if(reply->property("coverArt").toBool()) {
|
||||||
|
// add cover -> determine album ID and row
|
||||||
|
bool ok;
|
||||||
|
QString albumId = reply->property("albumId").toString();
|
||||||
|
int row = reply->property("row").toInt(&ok);
|
||||||
|
if(!albumId.isEmpty() && ok && row < m_results.size()) {
|
||||||
|
m_coverData[albumId] = data;
|
||||||
|
m_results[row].cover = data;
|
||||||
|
emit coverAvailable(index(row, 0));
|
||||||
|
} else {
|
||||||
|
m_errorList << tr("Cover reply is invalid (internal error).");
|
||||||
|
}
|
||||||
|
setFetchingCover(false);
|
||||||
|
} else {
|
||||||
|
// prepare parsing MusicBrainz meta data
|
||||||
|
beginResetModel();
|
||||||
|
m_results.clear();
|
||||||
|
m_reader.addData(data);
|
||||||
|
|
||||||
|
#define xmlReader m_reader
|
||||||
|
#include <qtutilities/misc/xmlparsermacros.h>
|
||||||
|
|
||||||
// parse XML tree
|
// parse XML tree
|
||||||
children {
|
children {
|
||||||
iftag("metadata") {
|
iftag("metadata") {
|
||||||
|
@ -232,6 +423,9 @@ void MusicBrainzResultsModel::parseResults()
|
||||||
} eliftag("release-list") {
|
} eliftag("release-list") {
|
||||||
children {
|
children {
|
||||||
iftag("release") {
|
iftag("release") {
|
||||||
|
if(currentDescription.albumId.isEmpty()) {
|
||||||
|
currentDescription.albumId = attribute("id").toString();
|
||||||
|
}
|
||||||
children {
|
children {
|
||||||
iftag("title") {
|
iftag("title") {
|
||||||
currentDescription.album = text;
|
currentDescription.album = text;
|
||||||
|
@ -285,6 +479,8 @@ void MusicBrainzResultsModel::parseResults()
|
||||||
} else_skip
|
} else_skip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <qtutilities/misc/undefxmlparsermacros.h>
|
||||||
|
|
||||||
// check for parsing errors
|
// check for parsing errors
|
||||||
switch(m_reader.error()) {
|
switch(m_reader.error()) {
|
||||||
case QXmlStreamReader::NoError:
|
case QXmlStreamReader::NoError:
|
||||||
|
@ -293,25 +489,15 @@ void MusicBrainzResultsModel::parseResults()
|
||||||
default:
|
default:
|
||||||
m_errorList << m_reader.errorString();
|
m_errorList << m_reader.errorString();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
// promote changes
|
||||||
m_errorList << m_reply->errorString();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete reply
|
|
||||||
m_reply->deleteLater();
|
|
||||||
m_reader.setDevice(m_reply = nullptr);
|
|
||||||
|
|
||||||
// promote changes
|
|
||||||
endResetModel();
|
|
||||||
setResultsAvailable(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
||||||
{
|
{
|
||||||
static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording"));
|
static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
|
||||||
// TODO: make this configurable
|
|
||||||
|
|
||||||
// compose parts
|
// compose parts
|
||||||
QStringList parts;
|
QStringList parts;
|
||||||
|
@ -330,7 +516,7 @@ QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose URL
|
// compose URL
|
||||||
QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording")));
|
QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording/")));
|
||||||
QUrlQuery query;
|
QUrlQuery query;
|
||||||
query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND ")));
|
query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND ")));
|
||||||
url.setQuery(query);
|
url.setQuery(query);
|
||||||
|
@ -339,6 +525,12 @@ QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
||||||
return new MusicBrainzResultsModel(Utility::networkAccessManager().get(QNetworkRequest(url)));
|
return new MusicBrainzResultsModel(Utility::networkAccessManager().get(QNetworkRequest(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QNetworkReply *queryCoverArtArchive(const QString &albumId)
|
||||||
|
{
|
||||||
|
static QString defaultArchiveUrl(QStringLiteral("https://coverartarchive.org"));
|
||||||
|
return networkAccessManager().get(QNetworkRequest(QUrl((Settings::coverArtArchiveUrl().isEmpty() ? defaultArchiveUrl : Settings::coverArtArchiveUrl()) % QStringLiteral("/release/") % albumId % QStringLiteral("/front"))));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "dbquery.moc"
|
#include "dbquery.moc"
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
#include <QAbstractTableModel>
|
#include <QAbstractTableModel>
|
||||||
|
|
||||||
|
QT_FORWARD_DECLARE_CLASS(QNetworkReply)
|
||||||
|
|
||||||
namespace Media {
|
namespace Media {
|
||||||
class TagValue;
|
class TagValue;
|
||||||
DECLARE_ENUM(KnownField, unsigned int)
|
DECLARE_ENUM(KnownField, unsigned int)
|
||||||
|
@ -18,12 +20,14 @@ struct SongDescription
|
||||||
|
|
||||||
QString title;
|
QString title;
|
||||||
QString album;
|
QString album;
|
||||||
|
QString albumId;
|
||||||
QString artist;
|
QString artist;
|
||||||
QString year;
|
QString year;
|
||||||
QString genre;
|
QString genre;
|
||||||
unsigned int track;
|
unsigned int track;
|
||||||
unsigned int totalTracks;
|
unsigned int totalTracks;
|
||||||
unsigned int disk;
|
unsigned int disk;
|
||||||
|
QByteArray cover;
|
||||||
};
|
};
|
||||||
|
|
||||||
class QueryResultsModel : public QAbstractTableModel
|
class QueryResultsModel : public QAbstractTableModel
|
||||||
|
@ -44,6 +48,7 @@ public:
|
||||||
const QList<SongDescription> &results() const;
|
const QList<SongDescription> &results() const;
|
||||||
const QStringList &errorList() const;
|
const QStringList &errorList() const;
|
||||||
bool areResultsAvailable() const;
|
bool areResultsAvailable() const;
|
||||||
|
bool isFetchingCover() const;
|
||||||
Media::TagValue fieldValue(int row, Media::KnownField knownField) const;
|
Media::TagValue fieldValue(int row, Media::KnownField knownField) const;
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role) const;
|
QVariant data(const QModelIndex &index, int role) const;
|
||||||
|
@ -51,17 +56,23 @@ public:
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
|
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
|
||||||
int rowCount(const QModelIndex &parent) const;
|
int rowCount(const QModelIndex &parent) const;
|
||||||
int columnCount(const QModelIndex &parent) const;
|
int columnCount(const QModelIndex &parent) const;
|
||||||
|
const QByteArray *cover(const QModelIndex &index) const;
|
||||||
|
virtual bool fetchCover(const QModelIndex &index);
|
||||||
|
virtual void abort();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void resultsAvailable();
|
void resultsAvailable();
|
||||||
|
void coverAvailable(const QModelIndex &index);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QueryResultsModel(QObject *parent = nullptr);
|
QueryResultsModel(QObject *parent = nullptr);
|
||||||
void setResultsAvailable(bool resultsAvailable);
|
void setResultsAvailable(bool resultsAvailable);
|
||||||
|
void setFetchingCover(bool fetchingCover);
|
||||||
|
|
||||||
QList<SongDescription> m_results;
|
QList<SongDescription> m_results;
|
||||||
QStringList m_errorList;
|
QStringList m_errorList;
|
||||||
bool m_resultsAvailable;
|
bool m_resultsAvailable;
|
||||||
|
bool m_fetchingCover;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline const QList<SongDescription> &QueryResultsModel::results() const
|
inline const QList<SongDescription> &QueryResultsModel::results() const
|
||||||
|
@ -79,7 +90,13 @@ inline bool QueryResultsModel::areResultsAvailable() const
|
||||||
return m_resultsAvailable;
|
return m_resultsAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool QueryResultsModel::isFetchingCover() const
|
||||||
|
{
|
||||||
|
return m_fetchingCover;
|
||||||
|
}
|
||||||
|
|
||||||
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription);
|
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription);
|
||||||
|
QNetworkReply *queryCoverArtArchive(const QString &albumId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
#include <c++utilities/conversion/conversionexception.h>
|
#include <c++utilities/conversion/conversionexception.h>
|
||||||
|
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include <QGraphicsItem>
|
||||||
|
|
||||||
using namespace ConversionUtilities;
|
using namespace ConversionUtilities;
|
||||||
using namespace Dialogs;
|
using namespace Dialogs;
|
||||||
|
@ -30,7 +34,8 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
|
||||||
QWidget(parent),
|
QWidget(parent),
|
||||||
m_ui(new Ui::DbQueryWidget),
|
m_ui(new Ui::DbQueryWidget),
|
||||||
m_tagEditorWidget(tagEditorWidget),
|
m_tagEditorWidget(tagEditorWidget),
|
||||||
m_model(nullptr)
|
m_model(nullptr),
|
||||||
|
m_coverIndex(-1)
|
||||||
{
|
{
|
||||||
m_ui->setupUi(this);
|
m_ui->setupUi(this);
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
|
@ -61,6 +66,7 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
|
||||||
connect(m_ui->startPushButton, &QPushButton::clicked, this, &DbQueryWidget::startSearch);
|
connect(m_ui->startPushButton, &QPushButton::clicked, this, &DbQueryWidget::startSearch);
|
||||||
connect(m_ui->applyPushButton, &QPushButton::clicked, this, &DbQueryWidget::applyResults);
|
connect(m_ui->applyPushButton, &QPushButton::clicked, this, &DbQueryWidget::applyResults);
|
||||||
connect(m_tagEditorWidget, &TagEditorWidget::fileStatusChange, this, &DbQueryWidget::fileStatusChanged);
|
connect(m_tagEditorWidget, &TagEditorWidget::fileStatusChange, this, &DbQueryWidget::fileStatusChanged);
|
||||||
|
connect(m_ui->resultsTreeView, &QTreeView::customContextMenuRequested, this, &DbQueryWidget::showResultsContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
DbQueryWidget::~DbQueryWidget()
|
DbQueryWidget::~DbQueryWidget()
|
||||||
|
@ -115,7 +121,7 @@ void DbQueryWidget::startSearch()
|
||||||
|
|
||||||
// show status
|
// show status
|
||||||
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
|
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
|
||||||
m_ui->notificationLabel->setText(tr("Retrieving ..."));
|
m_ui->notificationLabel->setText(tr("Retrieving meta data ..."));
|
||||||
setStatus(false);
|
setStatus(false);
|
||||||
|
|
||||||
// get song description
|
// get song description
|
||||||
|
@ -128,20 +134,26 @@ void DbQueryWidget::startSearch()
|
||||||
// do actual query
|
// do actual query
|
||||||
m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(desc));
|
m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(desc));
|
||||||
connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults);
|
connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults);
|
||||||
|
connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DbQueryWidget::abortSearch()
|
void DbQueryWidget::abortSearch()
|
||||||
{
|
{
|
||||||
if(m_model && !m_model->areResultsAvailable()) {
|
if(m_model) {
|
||||||
// delete model to abort search
|
if(m_model->isFetchingCover()) {
|
||||||
m_ui->resultsTreeView->setModel(nullptr);
|
// call abort to abort fetching cover
|
||||||
delete m_model;
|
m_model->abort();
|
||||||
m_model = nullptr;
|
} else if(!m_model->areResultsAvailable()) {
|
||||||
|
// delete model to abort search
|
||||||
|
m_ui->resultsTreeView->setModel(nullptr);
|
||||||
|
delete m_model;
|
||||||
|
m_model = nullptr;
|
||||||
|
|
||||||
// update status
|
// update status
|
||||||
m_ui->notificationLabel->setNotificationType(NotificationType::Information);
|
m_ui->notificationLabel->setNotificationType(NotificationType::Information);
|
||||||
m_ui->notificationLabel->setText(tr("Aborted"));
|
m_ui->notificationLabel->setText(tr("Aborted"));
|
||||||
setStatus(true);
|
setStatus(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +191,7 @@ void DbQueryWidget::setStatus(bool aborted)
|
||||||
m_ui->applyPushButton->setVisible(aborted);
|
m_ui->applyPushButton->setVisible(aborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DbQueryWidget::fileStatusChanged(bool , bool hasTags)
|
void DbQueryWidget::fileStatusChanged(bool, bool hasTags)
|
||||||
{
|
{
|
||||||
m_ui->applyPushButton->setEnabled(hasTags && m_ui->resultsTreeView->selectionModel() && m_ui->resultsTreeView->selectionModel()->hasSelection());
|
m_ui->applyPushButton->setEnabled(hasTags && m_ui->resultsTreeView->selectionModel() && m_ui->resultsTreeView->selectionModel()->hasSelection());
|
||||||
insertSearchTermsFromTagEdit(m_tagEditorWidget->activeTagEdit());
|
insertSearchTermsFromTagEdit(m_tagEditorWidget->activeTagEdit());
|
||||||
|
@ -187,17 +199,47 @@ void DbQueryWidget::fileStatusChanged(bool , bool hasTags)
|
||||||
|
|
||||||
void DbQueryWidget::applyResults()
|
void DbQueryWidget::applyResults()
|
||||||
{
|
{
|
||||||
|
// check whether model, tag edit and current selection exist
|
||||||
if(m_model) {
|
if(m_model) {
|
||||||
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
|
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
|
||||||
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
|
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
|
||||||
const QModelIndexList selection = selectionModel->selection().indexes();
|
const QModelIndexList selection = selectionModel->selection().indexes();
|
||||||
if(!selection.isEmpty()) {
|
if(!selection.isEmpty()) {
|
||||||
|
// determine previous value handling
|
||||||
PreviousValueHandling previousValueHandling = m_ui->overrideCheckBox->isChecked()
|
PreviousValueHandling previousValueHandling = m_ui->overrideCheckBox->isChecked()
|
||||||
? PreviousValueHandling::Update : PreviousValueHandling::Keep;
|
? PreviousValueHandling::Update : PreviousValueHandling::Keep;
|
||||||
|
|
||||||
|
// loop through all fields
|
||||||
for(const ChecklistItem &item : Settings::dbQueryFields().items()) {
|
for(const ChecklistItem &item : Settings::dbQueryFields().items()) {
|
||||||
if(item.isChecked()) {
|
if(item.isChecked()) {
|
||||||
|
// field should be used
|
||||||
const auto field = static_cast<KnownField>(item.id().toInt());
|
const auto field = static_cast<KnownField>(item.id().toInt());
|
||||||
tagEdit->setValue(field, m_model->fieldValue(selection.front().row(), field), previousValueHandling);
|
int row = selection.front().row();
|
||||||
|
TagValue value = m_model->fieldValue(row, field);
|
||||||
|
|
||||||
|
if(value.isEmpty() && field == KnownField::Cover) {
|
||||||
|
// cover value is empty -> cover still needs to be fetched
|
||||||
|
if(m_model->fetchCover(selection.front())) {
|
||||||
|
// cover is available now
|
||||||
|
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
|
||||||
|
} else {
|
||||||
|
// cover is fetched asynchronously
|
||||||
|
// -> show status
|
||||||
|
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
|
||||||
|
m_ui->notificationLabel->setText(tr("Retrieving cover art to be applied ..."));
|
||||||
|
setStatus(false);
|
||||||
|
// -> apply cover when available
|
||||||
|
connect(m_model, &QueryResultsModel::coverAvailable, [this, row, previousValueHandling](const QModelIndex &index) {
|
||||||
|
if(row == index.row()) {
|
||||||
|
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
|
||||||
|
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tagEdit->setValue(field, value, previousValueHandling);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,6 +248,80 @@ void DbQueryWidget::applyResults()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DbQueryWidget::showResultsContextMenu()
|
||||||
|
{
|
||||||
|
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
|
||||||
|
const QModelIndexList selection = selectionModel->selection().indexes();
|
||||||
|
if(!selection.isEmpty()) {
|
||||||
|
QMenu contextMenu;
|
||||||
|
if(m_ui->applyPushButton->isEnabled()) {
|
||||||
|
contextMenu.addAction(m_ui->applyPushButton->icon(), m_ui->applyPushButton->text(), this, SLOT(applyResults()));
|
||||||
|
}
|
||||||
|
if(m_model && m_model->areResultsAvailable()) {
|
||||||
|
contextMenu.addAction(QIcon::fromTheme(QStringLiteral("view-preview")), tr("Show cover"), this, SLOT(fetchAndShowCoverForSelection()));
|
||||||
|
}
|
||||||
|
contextMenu.exec(QCursor::pos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbQueryWidget::fetchAndShowCoverForSelection()
|
||||||
|
{
|
||||||
|
if(m_model) {
|
||||||
|
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
|
||||||
|
const QModelIndexList selection = selectionModel->selection().indexes();
|
||||||
|
if(!selection.isEmpty()) {
|
||||||
|
const QModelIndex &selectedIndex = selection.at(0);
|
||||||
|
if(const QByteArray *cover = m_model->cover(selectedIndex)) {
|
||||||
|
showCover(*cover);
|
||||||
|
} else {
|
||||||
|
if(m_model->fetchCover(selectedIndex)) {
|
||||||
|
if(const QByteArray *cover = m_model->cover(selectedIndex)) {
|
||||||
|
showCover(*cover);
|
||||||
|
} else {
|
||||||
|
// cover couldn't be fetched
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cover is fetched asynchronously
|
||||||
|
// -> memorize index to be shown
|
||||||
|
m_coverIndex = selectedIndex.row();
|
||||||
|
// -> show status
|
||||||
|
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
|
||||||
|
m_ui->notificationLabel->setText(tr("Retrieving cover art ..."));
|
||||||
|
setStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbQueryWidget::showCover(const QByteArray &data)
|
||||||
|
{
|
||||||
|
// show cover
|
||||||
|
QDialog dlg;
|
||||||
|
dlg.setWindowFlags(Qt::Tool);
|
||||||
|
dlg.setWindowTitle(tr("Cover - %1").arg(QApplication::applicationName()));
|
||||||
|
QBoxLayout layout(QBoxLayout::Up);
|
||||||
|
layout.setMargin(0);
|
||||||
|
QGraphicsView view(&dlg);
|
||||||
|
QGraphicsScene scene;
|
||||||
|
layout.addWidget(&view);
|
||||||
|
scene.addItem(new QGraphicsPixmapItem(QPixmap::fromImage(QImage::fromData(data))));
|
||||||
|
view.setScene(&scene);
|
||||||
|
view.show();
|
||||||
|
dlg.setLayout(&layout);
|
||||||
|
dlg.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbQueryWidget::showCoverFromIndex(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
if(m_coverIndex == index.row()) {
|
||||||
|
m_coverIndex = -1;
|
||||||
|
showCover(*m_model->cover(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool DbQueryWidget::eventFilter(QObject *obj, QEvent *event)
|
bool DbQueryWidget::eventFilter(QObject *obj, QEvent *event)
|
||||||
{
|
{
|
||||||
if(obj = m_ui->searchGroupBox) {
|
if(obj = m_ui->searchGroupBox) {
|
||||||
|
|
|
@ -40,6 +40,10 @@ private slots:
|
||||||
void setStatus(bool aborted);
|
void setStatus(bool aborted);
|
||||||
void fileStatusChanged(bool opened, bool hasTags);
|
void fileStatusChanged(bool opened, bool hasTags);
|
||||||
void applyResults();
|
void applyResults();
|
||||||
|
void showResultsContextMenu();
|
||||||
|
void fetchAndShowCoverForSelection();
|
||||||
|
void showCover(const QByteArray &data);
|
||||||
|
void showCoverFromIndex(const QModelIndex &index);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject *obj, QEvent *event);
|
bool eventFilter(QObject *obj, QEvent *event);
|
||||||
|
@ -48,6 +52,7 @@ private:
|
||||||
std::unique_ptr<Ui::DbQueryWidget> m_ui;
|
std::unique_ptr<Ui::DbQueryWidget> m_ui;
|
||||||
TagEditorWidget *m_tagEditorWidget;
|
TagEditorWidget *m_tagEditorWidget;
|
||||||
QueryResultsModel *m_model;
|
QueryResultsModel *m_model;
|
||||||
|
int m_coverIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>QtGui::DbQueryWidget</class>
|
<class>QtGui::DbQueryWidget</class>
|
||||||
<widget class="QWidget" name="QtGui::DbQueryWidget">
|
<widget class="QWidget" name="QtGui::DbQueryWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>731</width>
|
||||||
|
<height>603</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>MusicBrainz search</string>
|
<string>MusicBrainz search</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -140,6 +148,9 @@
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="contextMenuPolicy">
|
||||||
|
<enum>Qt::CustomContextMenu</enum>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QGroupBox" name="fieldsGroupBox">
|
<widget class="QGroupBox" name="fieldsGroupBox">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
|
|
|
@ -23,6 +23,20 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="coverArtArchiveUrlLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cover Art Archive URL</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="Widgets::ClearLineEdit" name="coverArtArchiveUrlLineEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>http://coverartarchive.org</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
|
|
@ -65,6 +65,23 @@ PicturePreviewSelection::PicturePreviewSelection(Tag *tag, KnownField field, QWi
|
||||||
PicturePreviewSelection::~PicturePreviewSelection()
|
PicturePreviewSelection::~PicturePreviewSelection()
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Sets the \a value of the current tag field manually using the given \a previousValueHandling.
|
||||||
|
*/
|
||||||
|
void PicturePreviewSelection::setValue(const TagValue &value, PreviousValueHandling previousValueHandling)
|
||||||
|
{
|
||||||
|
if(m_currentTypeIndex < static_cast<unsigned int>(m_values.count())) {
|
||||||
|
TagValue ¤tValue = m_values[m_currentTypeIndex];
|
||||||
|
if(previousValueHandling == PreviousValueHandling::Clear || !value.isEmpty()) {
|
||||||
|
if(previousValueHandling != PreviousValueHandling::Keep || currentValue.isEmpty()) {
|
||||||
|
currentValue = value; // TODO: move(value);
|
||||||
|
emit pictureChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePreview(m_currentTypeIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Defines the predicate to get relevant fields.
|
* \brief Defines the predicate to get relevant fields.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -42,6 +42,7 @@ public:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setTagField(Media::Tag *tag, Media::KnownField field, PreviousValueHandling previousValueHandling = PreviousValueHandling::Clear);
|
void setTagField(Media::Tag *tag, Media::KnownField field, PreviousValueHandling previousValueHandling = PreviousValueHandling::Clear);
|
||||||
|
void setValue(const Media::TagValue &value, PreviousValueHandling previousValueHandling = PreviousValueHandling::Clear);
|
||||||
|
|
||||||
void apply();
|
void apply();
|
||||||
void clear();
|
void clear();
|
||||||
|
|
|
@ -263,6 +263,7 @@ bool EditorDbQueryOptionsPage::apply()
|
||||||
{
|
{
|
||||||
if(hasBeenShown()) {
|
if(hasBeenShown()) {
|
||||||
Settings::musicBrainzUrl() = ui()->musicBrainzUrlLineEdit->text();
|
Settings::musicBrainzUrl() = ui()->musicBrainzUrlLineEdit->text();
|
||||||
|
Settings::coverArtArchiveUrl() = ui()->coverArtArchiveUrlLineEdit->text();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -271,6 +272,7 @@ void EditorDbQueryOptionsPage::reset()
|
||||||
{
|
{
|
||||||
if(hasBeenShown()) {
|
if(hasBeenShown()) {
|
||||||
ui()->musicBrainzUrlLineEdit->setText(Settings::musicBrainzUrl());
|
ui()->musicBrainzUrlLineEdit->setText(Settings::musicBrainzUrl());
|
||||||
|
ui()->coverArtArchiveUrlLineEdit->setText(Settings::coverArtArchiveUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,7 +166,10 @@ TagValue TagFieldEdit::value(TagTextEncoding encoding, bool includeDescription)
|
||||||
bool TagFieldEdit::setValue(const TagValue &value, PreviousValueHandling previousValueHandling)
|
bool TagFieldEdit::setValue(const TagValue &value, PreviousValueHandling previousValueHandling)
|
||||||
{
|
{
|
||||||
updateValue(value, previousValueHandling, false);
|
updateValue(value, previousValueHandling, false);
|
||||||
return m_pictureSelection == nullptr;
|
if(m_pictureSelection) {
|
||||||
|
m_pictureSelection->setValue(value, previousValueHandling);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -694,6 +697,7 @@ void TagFieldEdit::updateValue(Tag *tag, PreviousValueHandling previousValueHand
|
||||||
* \param value Specifies the new value.
|
* \param value Specifies the new value.
|
||||||
* \param previousValueHandling Specifies how to deal with the previous value.
|
* \param previousValueHandling Specifies how to deal with the previous value.
|
||||||
* \param updateRestoreButton Specifies whether the "restore button" should be updated.
|
* \param updateRestoreButton Specifies whether the "restore button" should be updated.
|
||||||
|
* \remarks Does not update the picture preview selection.
|
||||||
*/
|
*/
|
||||||
void TagFieldEdit::updateValue(const TagValue &value, PreviousValueHandling previousValueHandling, bool updateRestoreButton)
|
void TagFieldEdit::updateValue(const TagValue &value, PreviousValueHandling previousValueHandling, bool updateRestoreButton)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue