diff --git a/CMakeLists.txt b/CMakeLists.txt index b9db710..1f10d71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/application/settings.cpp b/application/settings.cpp index 6da68d9..08cedb8 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -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")); diff --git a/application/settings.h b/application/settings.h index a0d4efa..0767d55 100644 --- a/application/settings.h +++ b/application/settings.h @@ -96,6 +96,7 @@ bool &dbQueryWidgetShown(); bool &dbQueryOverride(); KnownFieldModel &dbQueryFields(); QString &musicBrainzUrl(); +QString &coverArtArchiveUrl(); // rename files dialog int &scriptSource(); diff --git a/dbquery/api.md b/dbquery/api.md index 055886c..4ec6e15 100644 --- a/dbquery/api.md +++ b/dbquery/api.md @@ -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 diff --git a/dbquery/dbquery.cpp b/dbquery/dbquery.cpp index e15a536..3bb3cab 100644 --- a/dbquery/dbquery.cpp +++ b/dbquery/dbquery.cpp @@ -11,10 +11,15 @@ #include #include #include +#include +#include -#define xmlReader m_reader -#include +#include +#ifdef DEBUG_BUILD +# include +#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 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(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("

Do you want to redirect form %1 to %2?

").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 m_coverData; + QXmlStreamReader m_reader; + What m_what; +}; + +map MusicBrainzResultsModel::m_coverData = map(); + +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 + // 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 + // 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" diff --git a/dbquery/dbquery.h b/dbquery/dbquery.h index 870346e..84a6563 100644 --- a/dbquery/dbquery.h +++ b/dbquery/dbquery.h @@ -5,6 +5,8 @@ #include +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 &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 m_results; QStringList m_errorList; bool m_resultsAvailable; + bool m_fetchingCover; }; inline const QList &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); } diff --git a/gui/dbquerywidget.cpp b/gui/dbquerywidget.cpp index 33ead19..6e6fc3b 100644 --- a/gui/dbquerywidget.cpp +++ b/gui/dbquerywidget.cpp @@ -16,6 +16,10 @@ #include #include +#include +#include +#include +#include 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(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) { diff --git a/gui/dbquerywidget.h b/gui/dbquerywidget.h index 1466eb5..0c36b81 100644 --- a/gui/dbquerywidget.h +++ b/gui/dbquerywidget.h @@ -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 m_ui; TagEditorWidget *m_tagEditorWidget; QueryResultsModel *m_model; + int m_coverIndex; }; } diff --git a/gui/dbquerywidget.ui b/gui/dbquerywidget.ui index eb8ccb5..34fc42a 100644 --- a/gui/dbquerywidget.ui +++ b/gui/dbquerywidget.ui @@ -2,6 +2,14 @@ QtGui::DbQueryWidget + + + 0 + 0 + 731 + 603 + + MusicBrainz search @@ -140,6 +148,9 @@ 0 + + Qt::CustomContextMenu + diff --git a/gui/editordbqueryoptionpage.ui b/gui/editordbqueryoptionpage.ui index f3a07a8..1aba7b1 100644 --- a/gui/editordbqueryoptionpage.ui +++ b/gui/editordbqueryoptionpage.ui @@ -23,6 +23,20 @@ + + + + Cover Art Archive URL + + + + + + + http://coverartarchive.org + + + diff --git a/gui/picturepreviewselection.cpp b/gui/picturepreviewselection.cpp index 640cb5c..4f4cd5d 100644 --- a/gui/picturepreviewselection.cpp +++ b/gui/picturepreviewselection.cpp @@ -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(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. */ diff --git a/gui/picturepreviewselection.h b/gui/picturepreviewselection.h index 70cd4d1..9e6e1f8 100644 --- a/gui/picturepreviewselection.h +++ b/gui/picturepreviewselection.h @@ -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(); diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index 5ca3b52..df22a3c 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -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()); } } diff --git a/gui/tagfieldedit.cpp b/gui/tagfieldedit.cpp index c2d3edc..14c8cd4 100644 --- a/gui/tagfieldedit.cpp +++ b/gui/tagfieldedit.cpp @@ -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) {