allow querying cover from Cover Art Archive

This commit is contained in:
Martchus 2016-03-06 17:52:33 +01:00
parent 0adb77fd58
commit 70b94fa5fa
14 changed files with 461 additions and 60 deletions

View File

@ -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)

View File

@ -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"));

View File

@ -96,6 +96,7 @@ bool &dbQueryWidgetShown();
bool &dbQueryOverride();
KnownFieldModel &dbQueryFields();
QString &musicBrainzUrl();
QString &coverArtArchiveUrl();
// rename files dialog
int &scriptSource();

View File

@ -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

View File

@ -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"

View File

@ -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);
}

View File

@ -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) {

View File

@ -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;
};
}

View File

@ -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">

View File

@ -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>

View File

@ -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 &currentValue = 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.
*/

View File

@ -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();

View File

@ -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());
}
}

View File

@ -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)
{