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
|
||||
# disable new ABI (can't catch ios_base::failure with new ABI)
|
||||
# enable code for debugging
|
||||
add_definitions(
|
||||
-DGUI_QTWIDGETS
|
||||
-DMODEL_UNDO_SUPPORT
|
||||
|
@ -240,6 +241,10 @@ add_definitions(
|
|||
${JS_DEFINITION}
|
||||
${WEBVIEW_DEFINITION}
|
||||
)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
add_definitions(-DDEBUG_BUILD)
|
||||
message(STATUS "Debug build enabled.")
|
||||
endif()
|
||||
|
||||
# enable moc, uic and rcc
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
|
|
@ -236,7 +236,8 @@ KnownFieldModel &dbQueryFields()
|
|||
<< KnownFieldModel::mkItem(KnownField::Album)
|
||||
<< KnownFieldModel::mkItem(KnownField::Album)
|
||||
<< KnownFieldModel::mkItem(KnownField::Year)
|
||||
<< KnownFieldModel::mkItem(KnownField::Genre));
|
||||
<< KnownFieldModel::mkItem(KnownField::Genre)
|
||||
<< KnownFieldModel::mkItem(KnownField::Cover, Qt::Unchecked));
|
||||
return v;
|
||||
}
|
||||
|
||||
|
@ -246,6 +247,12 @@ QString &musicBrainzUrl()
|
|||
return v;
|
||||
}
|
||||
|
||||
QString &coverArtArchiveUrl()
|
||||
{
|
||||
static QString v;
|
||||
return v;
|
||||
}
|
||||
|
||||
// renaming files dialog
|
||||
int &scriptSource()
|
||||
{
|
||||
|
@ -395,6 +402,7 @@ void restore()
|
|||
dbQueryOverride() = settings.value(QStringLiteral("override"), true).toBool();
|
||||
dbQueryFields().restore(settings, QStringLiteral("fields"));
|
||||
musicBrainzUrl() = settings.value(QStringLiteral("musicbrainzurl")).toString();
|
||||
coverArtArchiveUrl() = settings.value(QStringLiteral("coverartarchiveurl")).toString();
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup(QStringLiteral("renamedlg"));
|
||||
|
@ -471,6 +479,7 @@ void save()
|
|||
settings.setValue(QStringLiteral("override"), dbQueryOverride());
|
||||
dbQueryFields().save(settings, QStringLiteral("fields"));
|
||||
settings.setValue(QStringLiteral("musicbrainzurl"), musicBrainzUrl());
|
||||
settings.setValue(QStringLiteral("coverartarchiveurl"), coverArtArchiveUrl());
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup(QStringLiteral("renamedlg"));
|
||||
|
|
|
@ -96,6 +96,7 @@ bool &dbQueryWidgetShown();
|
|||
bool &dbQueryOverride();
|
||||
KnownFieldModel &dbQueryFields();
|
||||
QString &musicBrainzUrl();
|
||||
QString &coverArtArchiveUrl();
|
||||
|
||||
// rename files dialog
|
||||
int &scriptSource();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# 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
|
||||
* artist by name
|
||||
|
@ -22,7 +23,10 @@ Available at ```http://musicbrainz.org/ws/2```
|
|||
```
|
||||
/release?query="$album_name"
|
||||
```
|
||||
* cover art: TODO
|
||||
* cover art by album ID
|
||||
```
|
||||
/release/$album_id/front-500
|
||||
```
|
||||
* lyrics: TODO
|
||||
|
||||
|
||||
|
@ -32,3 +36,6 @@ Available at ```http://musicbrainz.org/ws/2```
|
|||
## See also
|
||||
* [Web Service](https://wiki.musicbrainz.org/Development/JSON_Web_Service)
|
||||
* [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 <QUrlQuery>
|
||||
#include <QStringBuilder>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QMessageBox>
|
||||
|
||||
#define xmlReader m_reader
|
||||
#include <qtutilities/misc/xmlparsermacros.h>
|
||||
#include <map>
|
||||
#ifdef DEBUG_BUILD
|
||||
# include <iostream>
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace Utility;
|
||||
using namespace Media;
|
||||
|
||||
|
@ -23,25 +28,33 @@ namespace QtGui {
|
|||
SongDescription::SongDescription() :
|
||||
track(0),
|
||||
totalTracks(0),
|
||||
disk(0)
|
||||
disk(0),
|
||||
cover(nullptr)
|
||||
{}
|
||||
|
||||
QueryResultsModel::QueryResultsModel(QObject *parent) :
|
||||
QAbstractTableModel(parent),
|
||||
m_resultsAvailable(false)
|
||||
m_resultsAvailable(false),
|
||||
m_fetchingCover(false)
|
||||
{}
|
||||
|
||||
void QueryResultsModel::setResultsAvailable(bool resultsAvailable)
|
||||
{
|
||||
if(resultsAvailable != m_resultsAvailable) {
|
||||
if(m_resultsAvailable = resultsAvailable) {
|
||||
emit this->resultsAvailable();
|
||||
}
|
||||
if(m_resultsAvailable = resultsAvailable) {
|
||||
emit this->resultsAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
void QueryResultsModel::setFetchingCover(bool fetchingCover)
|
||||
{
|
||||
m_fetchingCover = fetchingCover;
|
||||
}
|
||||
|
||||
#define returnValue(field) return qstringToTagValue(res.field, TagTextEncoding::Utf16LittleEndian)
|
||||
|
||||
void QueryResultsModel::abort()
|
||||
{}
|
||||
|
||||
TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
||||
{
|
||||
if(row < m_results.size()) {
|
||||
|
@ -63,6 +76,11 @@ TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
|||
return TagValue(res.track);
|
||||
case KnownField::TotalParts:
|
||||
return TagValue(res.totalTracks);
|
||||
case KnownField::Cover:
|
||||
if(!res.cover.isEmpty()) {
|
||||
return TagValue(res.cover.data(), res.cover.size(), TagDataType::Picture);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
@ -167,43 +185,216 @@ int QueryResultsModel::columnCount(const QModelIndex &parent) const
|
|||
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
|
||||
public:
|
||||
MusicBrainzResultsModel(QNetworkReply *reply);
|
||||
~MusicBrainzResultsModel();
|
||||
~HttpResultsModel();
|
||||
void addReply(QNetworkReply *reply);
|
||||
void abort();
|
||||
|
||||
protected:
|
||||
HttpResultsModel(QNetworkReply *reply);
|
||||
virtual void parseResults(QNetworkReply *reply, const QByteArray &data) = 0;
|
||||
|
||||
private slots:
|
||||
void parseResults();
|
||||
void handleReplyFinished();
|
||||
|
||||
private:
|
||||
QNetworkReply *m_reply;
|
||||
QXmlStreamReader m_reader;
|
||||
protected:
|
||||
QList<QNetworkReply *> m_replies;
|
||||
};
|
||||
|
||||
MusicBrainzResultsModel::MusicBrainzResultsModel(QNetworkReply *reply) :
|
||||
m_reply(reply)
|
||||
HttpResultsModel::HttpResultsModel(QNetworkReply *reply)
|
||||
{
|
||||
connect(reply, &QNetworkReply::finished, this, &MusicBrainzResultsModel::parseResults);
|
||||
m_reader.setDevice(m_reply);
|
||||
addReply(reply);
|
||||
}
|
||||
|
||||
MusicBrainzResultsModel::~MusicBrainzResultsModel()
|
||||
HttpResultsModel::~HttpResultsModel()
|
||||
{
|
||||
if(m_reply) {
|
||||
m_reply->abort();
|
||||
delete m_reply;
|
||||
qDeleteAll(m_replies);
|
||||
}
|
||||
|
||||
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
|
||||
switch(m_reply->error()) {
|
||||
case QNetworkReply::NoError:
|
||||
class MusicBrainzResultsModel : public HttpResultsModel
|
||||
{
|
||||
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
|
||||
children {
|
||||
iftag("metadata") {
|
||||
|
@ -232,6 +423,9 @@ void MusicBrainzResultsModel::parseResults()
|
|||
} eliftag("release-list") {
|
||||
children {
|
||||
iftag("release") {
|
||||
if(currentDescription.albumId.isEmpty()) {
|
||||
currentDescription.albumId = attribute("id").toString();
|
||||
}
|
||||
children {
|
||||
iftag("title") {
|
||||
currentDescription.album = text;
|
||||
|
@ -285,6 +479,8 @@ void MusicBrainzResultsModel::parseResults()
|
|||
} else_skip
|
||||
}
|
||||
|
||||
#include <qtutilities/misc/undefxmlparsermacros.h>
|
||||
|
||||
// check for parsing errors
|
||||
switch(m_reader.error()) {
|
||||
case QXmlStreamReader::NoError:
|
||||
|
@ -293,25 +489,15 @@ void MusicBrainzResultsModel::parseResults()
|
|||
default:
|
||||
m_errorList << m_reader.errorString();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
m_errorList << m_reply->errorString();
|
||||
// promote changes
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
// delete reply
|
||||
m_reply->deleteLater();
|
||||
m_reader.setDevice(m_reply = nullptr);
|
||||
|
||||
// promote changes
|
||||
endResetModel();
|
||||
setResultsAvailable(true);
|
||||
}
|
||||
|
||||
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
||||
{
|
||||
static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording"));
|
||||
// TODO: make this configurable
|
||||
static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
|
||||
|
||||
// compose parts
|
||||
QStringList parts;
|
||||
|
@ -330,7 +516,7 @@ QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
|||
}
|
||||
|
||||
// compose URL
|
||||
QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording")));
|
||||
QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording/")));
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND ")));
|
||||
url.setQuery(query);
|
||||
|
@ -339,6 +525,12 @@ QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription)
|
|||
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"
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
#include <QAbstractTableModel>
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QNetworkReply)
|
||||
|
||||
namespace Media {
|
||||
class TagValue;
|
||||
DECLARE_ENUM(KnownField, unsigned int)
|
||||
|
@ -18,12 +20,14 @@ struct SongDescription
|
|||
|
||||
QString title;
|
||||
QString album;
|
||||
QString albumId;
|
||||
QString artist;
|
||||
QString year;
|
||||
QString genre;
|
||||
unsigned int track;
|
||||
unsigned int totalTracks;
|
||||
unsigned int disk;
|
||||
QByteArray cover;
|
||||
};
|
||||
|
||||
class QueryResultsModel : public QAbstractTableModel
|
||||
|
@ -44,6 +48,7 @@ public:
|
|||
const QList<SongDescription> &results() const;
|
||||
const QStringList &errorList() const;
|
||||
bool areResultsAvailable() const;
|
||||
bool isFetchingCover() const;
|
||||
Media::TagValue fieldValue(int row, Media::KnownField knownField) const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const;
|
||||
|
@ -51,17 +56,23 @@ public:
|
|||
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
|
||||
int rowCount(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:
|
||||
void resultsAvailable();
|
||||
void coverAvailable(const QModelIndex &index);
|
||||
|
||||
protected:
|
||||
QueryResultsModel(QObject *parent = nullptr);
|
||||
void setResultsAvailable(bool resultsAvailable);
|
||||
void setFetchingCover(bool fetchingCover);
|
||||
|
||||
QList<SongDescription> m_results;
|
||||
QStringList m_errorList;
|
||||
bool m_resultsAvailable;
|
||||
bool m_fetchingCover;
|
||||
};
|
||||
|
||||
inline const QList<SongDescription> &QueryResultsModel::results() const
|
||||
|
@ -79,7 +90,13 @@ inline bool QueryResultsModel::areResultsAvailable() const
|
|||
return m_resultsAvailable;
|
||||
}
|
||||
|
||||
inline bool QueryResultsModel::isFetchingCover() const
|
||||
{
|
||||
return m_fetchingCover;
|
||||
}
|
||||
|
||||
QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription);
|
||||
QNetworkReply *queryCoverArtArchive(const QString &albumId);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
#include <c++utilities/conversion/conversionexception.h>
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QDialog>
|
||||
#include <QGraphicsView>
|
||||
#include <QGraphicsItem>
|
||||
|
||||
using namespace ConversionUtilities;
|
||||
using namespace Dialogs;
|
||||
|
@ -30,7 +34,8 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
|
|||
QWidget(parent),
|
||||
m_ui(new Ui::DbQueryWidget),
|
||||
m_tagEditorWidget(tagEditorWidget),
|
||||
m_model(nullptr)
|
||||
m_model(nullptr),
|
||||
m_coverIndex(-1)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
#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->applyPushButton, &QPushButton::clicked, this, &DbQueryWidget::applyResults);
|
||||
connect(m_tagEditorWidget, &TagEditorWidget::fileStatusChange, this, &DbQueryWidget::fileStatusChanged);
|
||||
connect(m_ui->resultsTreeView, &QTreeView::customContextMenuRequested, this, &DbQueryWidget::showResultsContextMenu);
|
||||
}
|
||||
|
||||
DbQueryWidget::~DbQueryWidget()
|
||||
|
@ -115,7 +121,7 @@ void DbQueryWidget::startSearch()
|
|||
|
||||
// show status
|
||||
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
|
||||
m_ui->notificationLabel->setText(tr("Retrieving ..."));
|
||||
m_ui->notificationLabel->setText(tr("Retrieving meta data ..."));
|
||||
setStatus(false);
|
||||
|
||||
// get song description
|
||||
|
@ -128,20 +134,26 @@ void DbQueryWidget::startSearch()
|
|||
// do actual query
|
||||
m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(desc));
|
||||
connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults);
|
||||
connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex);
|
||||
}
|
||||
|
||||
void DbQueryWidget::abortSearch()
|
||||
{
|
||||
if(m_model && !m_model->areResultsAvailable()) {
|
||||
// delete model to abort search
|
||||
m_ui->resultsTreeView->setModel(nullptr);
|
||||
delete m_model;
|
||||
m_model = nullptr;
|
||||
if(m_model) {
|
||||
if(m_model->isFetchingCover()) {
|
||||
// call abort to abort fetching cover
|
||||
m_model->abort();
|
||||
} else if(!m_model->areResultsAvailable()) {
|
||||
// delete model to abort search
|
||||
m_ui->resultsTreeView->setModel(nullptr);
|
||||
delete m_model;
|
||||
m_model = nullptr;
|
||||
|
||||
// update status
|
||||
m_ui->notificationLabel->setNotificationType(NotificationType::Information);
|
||||
m_ui->notificationLabel->setText(tr("Aborted"));
|
||||
setStatus(true);
|
||||
// update status
|
||||
m_ui->notificationLabel->setNotificationType(NotificationType::Information);
|
||||
m_ui->notificationLabel->setText(tr("Aborted"));
|
||||
setStatus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +191,7 @@ void DbQueryWidget::setStatus(bool 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());
|
||||
insertSearchTermsFromTagEdit(m_tagEditorWidget->activeTagEdit());
|
||||
|
@ -187,17 +199,47 @@ void DbQueryWidget::fileStatusChanged(bool , bool hasTags)
|
|||
|
||||
void DbQueryWidget::applyResults()
|
||||
{
|
||||
// check whether model, tag edit and current selection exist
|
||||
if(m_model) {
|
||||
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
|
||||
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
|
||||
const QModelIndexList selection = selectionModel->selection().indexes();
|
||||
if(!selection.isEmpty()) {
|
||||
// determine previous value handling
|
||||
PreviousValueHandling previousValueHandling = m_ui->overrideCheckBox->isChecked()
|
||||
? PreviousValueHandling::Update : PreviousValueHandling::Keep;
|
||||
|
||||
// loop through all fields
|
||||
for(const ChecklistItem &item : Settings::dbQueryFields().items()) {
|
||||
if(item.isChecked()) {
|
||||
// field should be used
|
||||
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)
|
||||
{
|
||||
if(obj = m_ui->searchGroupBox) {
|
||||
|
|
|
@ -40,6 +40,10 @@ private slots:
|
|||
void setStatus(bool aborted);
|
||||
void fileStatusChanged(bool opened, bool hasTags);
|
||||
void applyResults();
|
||||
void showResultsContextMenu();
|
||||
void fetchAndShowCoverForSelection();
|
||||
void showCover(const QByteArray &data);
|
||||
void showCoverFromIndex(const QModelIndex &index);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
@ -48,6 +52,7 @@ private:
|
|||
std::unique_ptr<Ui::DbQueryWidget> m_ui;
|
||||
TagEditorWidget *m_tagEditorWidget;
|
||||
QueryResultsModel *m_model;
|
||||
int m_coverIndex;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
<ui version="4.0">
|
||||
<class>QtGui::DbQueryWidget</class>
|
||||
<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">
|
||||
<string>MusicBrainz search</string>
|
||||
</property>
|
||||
|
@ -140,6 +148,9 @@
|
|||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="fieldsGroupBox">
|
||||
<property name="sizePolicy">
|
||||
|
|
|
@ -23,6 +23,20 @@
|
|||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
|
|
|
@ -65,6 +65,23 @@ PicturePreviewSelection::PicturePreviewSelection(Tag *tag, KnownField field, QWi
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -42,6 +42,7 @@ public:
|
|||
|
||||
public slots:
|
||||
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 clear();
|
||||
|
|
|
@ -263,6 +263,7 @@ bool EditorDbQueryOptionsPage::apply()
|
|||
{
|
||||
if(hasBeenShown()) {
|
||||
Settings::musicBrainzUrl() = ui()->musicBrainzUrlLineEdit->text();
|
||||
Settings::coverArtArchiveUrl() = ui()->coverArtArchiveUrlLineEdit->text();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -271,6 +272,7 @@ void EditorDbQueryOptionsPage::reset()
|
|||
{
|
||||
if(hasBeenShown()) {
|
||||
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)
|
||||
{
|
||||
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 previousValueHandling Specifies how to deal with the previous value.
|
||||
* \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)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue