From 958cd9960c84e907ae8361e8223766f65323496c Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 9 Oct 2016 19:44:06 +0200 Subject: [PATCH] Allow querying LyricsWikia for metadata Querying lyrics has not been implemented yet --- CMakeLists.txt | 4 + README.md | 6 +- application/settings.cpp | 6 + application/settings.h | 1 + dbquery/dbquery.cpp | 266 +++---------------------------- dbquery/dbquery.h | 23 ++- dbquery/lyricswikia.cpp | 115 ++++++++++++++ dbquery/lyricswikia.h | 27 ++++ dbquery/musicbrainz.cpp | 209 ++++++++++++++++++++++++ dbquery/musicbrainz.h | 39 +++++ gui/dbquerywidget.cpp | 49 +++++- gui/dbquerywidget.h | 3 +- gui/dbquerywidget.ui | 89 +++++++---- gui/mainwindow.cpp | 10 +- gui/mainwindow.h | 2 +- gui/mainwindow.ui | 15 +- gui/notificationlabel.cpp | 30 ++-- gui/tageditorwidget.ui | 20 ++- translations/tageditor_de_DE.ts | 270 ++++++++++++++++++-------------- translations/tageditor_en_US.ts | 264 +++++++++++++++++-------------- 20 files changed, 902 insertions(+), 546 deletions(-) create mode 100644 dbquery/lyricswikia.cpp create mode 100644 dbquery/lyricswikia.h create mode 100644 dbquery/musicbrainz.cpp create mode 100644 dbquery/musicbrainz.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0266f37..2b5ff3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,8 @@ set(WIDGETS_HEADER_FILES gui/tageditorwidget.h gui/fileinfomodel.h dbquery/dbquery.h + dbquery/musicbrainz.h + dbquery/lyricswikia.h gui/dbquerywidget.h misc/networkaccessmanager.h renamingutility/filesystemitem.h @@ -89,6 +91,8 @@ set(WIDGETS_SRC_FILES gui/tageditorwidget.cpp gui/fileinfomodel.cpp dbquery/dbquery.cpp + dbquery/musicbrainz.cpp + dbquery/lyricswikia.cpp gui/dbquerywidget.cpp misc/networkaccessmanager.cpp renamingutility/filesystemitem.cpp diff --git a/README.md b/README.md index e2cdad1..c588cef 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,9 @@ There is also a tool to rename files using the tag information stored in the fil by a small JavaScript which can be customized. An example script is provided. Before any actual changes are made, you will see a preview with the generated file names. -#### MusicBrainz search -The tag editor also features a MusicBrainz and Cover Art Archive search which can be opened with *F10*. However, this feature is still experimental. +#### MusicBrainz, Cover Art Archive and LyricaWiki search +The tag editor also features a MusicBrainz, Cover Art Archive and LyricaWiki search which can be opened with *F10*. +However, this feature is still experimental. ### CLI #### Usage @@ -194,3 +195,4 @@ To build without GUI, add the following parameters to the CMake call: - Large file information is not shown when using Qt WebEngine - It is recommend you to create backups before editing because I can not test whether the library works with all kind of files (when forcing rewrite a backup is always created) - underlying library: Matroska files composed of more than one segment aren't tested yet and might not work. +- Also note the issue tracker on GitHub diff --git a/application/settings.cpp b/application/settings.cpp index 0a38679..ba3c156 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -281,6 +281,12 @@ QString &coverArtArchiveUrl() return v; } +QString &lyricsWikiaUrl() +{ + static QString v; + return v; +} + // renaming files dialog int &scriptSource() { diff --git a/application/settings.h b/application/settings.h index 5db1986..957de5b 100644 --- a/application/settings.h +++ b/application/settings.h @@ -110,6 +110,7 @@ bool &dbQueryOverride(); KnownFieldModel &dbQueryFields(); QString &musicBrainzUrl(); QString &coverArtArchiveUrl(); +QString &lyricsWikiaUrl(); // rename files dialog int &scriptSource(); diff --git a/dbquery/dbquery.cpp b/dbquery/dbquery.cpp index 73d7161..5adc8ea 100644 --- a/dbquery/dbquery.cpp +++ b/dbquery/dbquery.cpp @@ -2,20 +2,14 @@ #include "../misc/utility.h" #include "../misc/networkaccessmanager.h" -#include "../application/settings.h" #include #include #include #include -#include -#include -#include -#include #include -#include #ifdef DEBUG_BUILD # include #endif @@ -215,40 +209,37 @@ bool QueryResultsModel::fetchCover(const QModelIndex &) return false; } -class HttpResultsModel : public QueryResultsModel -{ - Q_OBJECT -public: - ~HttpResultsModel(); - void addReply(QNetworkReply *reply); - void abort(); - -protected: - HttpResultsModel(QNetworkReply *reply); - virtual void parseResults(QNetworkReply *reply, const QByteArray &data) = 0; - -private slots: - void handleReplyFinished(); - -protected: - QList m_replies; -}; - -HttpResultsModel::HttpResultsModel(QNetworkReply *reply) +/*! + * \brief Constructs a new HttpResultsModel for the specified \a reply. + * \remarks The model takes ownership over the specified \a reply. + */ +HttpResultsModel::HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) : + m_initialDescription(initialSongDescription) { addReply(reply); } +/*! + * \brief Deletes all associated replies. + */ HttpResultsModel::~HttpResultsModel() { qDeleteAll(m_replies); } +/*! + * \brief Helper to copy a property from one QObject to another. + * \remarks Used to transfer reply properties to new reply in case a second reply is required. + */ void copyProperty(const char *property, const QObject *from, QObject *to) { to->setProperty(property, from->property(property)); } +/*! + * \brief Evaluates request results. + * \remarks Calls parseResults() if the requested data is available. Handles errors/redirections otherwise. + */ void HttpResultsModel::handleReplyFinished() { auto *reply = static_cast(sender()); @@ -296,6 +287,10 @@ void HttpResultsModel::handleReplyFinished() setResultsAvailable(true); } +/*! + * \brief Adds a reply. + * \remarks Called within c'tor and handleReplyFinished() in case of redirection. Might be called when subclassing to do further requests. + */ void HttpResultsModel::addReply(QNetworkReply *reply) { m_replies << reply; @@ -305,6 +300,9 @@ void HttpResultsModel::addReply(QNetworkReply *reply) connect(reply, &QNetworkReply::finished, this, &HttpResultsModel::handleReplyFinished); } +/*! + * \brief Aborts all ongoing requests and causes error "Aborted by user" if requests where ongoing. + */ void HttpResultsModel::abort() { if(!m_replies.isEmpty()) { @@ -316,222 +314,6 @@ void HttpResultsModel::abort() } } - -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); - - // parse XML tree -#define xmlReader m_reader -#include - children { - iftag("metadata") { - children { - iftag("recording-list") { - children { - iftag("recording") { - SongDescription currentDescription; - children { - iftag("title") { - currentDescription.title = text; - } eliftag("artist-credit") { - children { - iftag("name-credit") { - children { - iftag("artist") { - children { - iftag("name") { - currentDescription.artist = text; - } else_skip - } - } else_skip - } - } else_skip - } - } eliftag("release-list") { - children { - iftag("release") { - if(currentDescription.albumId.isEmpty()) { - currentDescription.albumId = attribute("id").toString(); - } - children { - iftag("title") { - currentDescription.album = text; - } eliftag("date") { - currentDescription.year = text; - } eliftag("medium-list") { - children { - iftag("medium") { - children { - iftag("position") { - currentDescription.disk = text.toUInt(); - } eliftag("track-list") { - currentDescription.totalTracks = attribute("count").toUInt(); - children { - iftag("track") { - children { - iftag("number") { - currentDescription.track = text.toUInt(); - } else_skip - } - } else_skip - } - } else_skip - } - } else_skip - } - } else_skip - } - } else_skip - } - } eliftag("tag-list") { - children { - iftag("tag") { - children { - iftag("name") { - if(!currentDescription.genre.isEmpty()) { - currentDescription.genre.append(QLatin1Char(' ')); - } - currentDescription.genre.append(text); - } else_skip - } - } else_skip - } - } else_skip - } - m_results << currentDescription; - } else_skip - } - } else_skip - } - } else_skip - } -#include - - // check for parsing errors - switch(m_reader.error()) { - case QXmlStreamReader::NoError: - case QXmlStreamReader::PrematureEndOfDocumentError: - break; - default: - m_errorList << m_reader.errorString(); - } - - // promote changes - endResetModel(); - } -} - -QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription) -{ - static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/")); - - // compose parts - QStringList parts; - parts.reserve(4); - if(!songDescription.title.isEmpty()) { - parts << QChar('\"') % songDescription.title % QChar('\"'); - } - if(!songDescription.artist.isEmpty()) { - parts << QStringLiteral("artist:\"") % songDescription.artist % QChar('\"'); - } - if(!songDescription.album.isEmpty()) { - parts << QStringLiteral("release:\"") % songDescription.album % QChar('\"'); - } - if(songDescription.track) { - parts << QStringLiteral("number:") + QString::number(songDescription.track); - } - - // compose URL - QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording/"))); - QUrlQuery query; - query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND "))); - url.setQuery(query); - - // make request - 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 cb888cf..57ca5fa 100644 --- a/dbquery/dbquery.h +++ b/dbquery/dbquery.h @@ -95,7 +95,28 @@ inline bool QueryResultsModel::isFetchingCover() const return m_fetchingCover; } -QueryResultsModel *queryMusicBrainz(const SongDescription &songDescription); +class HttpResultsModel : public QueryResultsModel +{ + Q_OBJECT +public: + ~HttpResultsModel(); + void abort(); + +protected: + HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply); + void addReply(QNetworkReply *reply); + virtual void parseResults(QNetworkReply *reply, const QByteArray &data) = 0; + +private slots: + void handleReplyFinished(); + +protected: + QList m_replies; + const SongDescription m_initialDescription; +}; + +QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription); +QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription); QNetworkReply *queryCoverArtArchive(const QString &albumId); } diff --git a/dbquery/lyricswikia.cpp b/dbquery/lyricswikia.cpp new file mode 100644 index 0000000..b3c3ee1 --- /dev/null +++ b/dbquery/lyricswikia.cpp @@ -0,0 +1,115 @@ +#include "./lyricswikia.h" + +#include "../misc/networkaccessmanager.h" +#include "../application/settings.h" + +#include +#include +#include + +using namespace std; +using namespace Utility; + +namespace QtGui { + +LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) : + HttpResultsModel(move(initialSongDescription), reply) +{} + +void LyricsWikiaResultsModel::parseResults(QNetworkReply *reply, const QByteArray &data) +{ + Q_UNUSED(reply) + + // prepare parsing MusicBrainz meta data + beginResetModel(); + m_results.clear(); + m_reader.addData(data); + + // parse XML tree +#define xmlReader m_reader +#include + children { + iftag("getArtistResponse") { + QString artist; + children { + iftag("artist") { + artist = text; + } eliftag("albums") { + children { + iftag("albumResult") { + QString album, year; + QList songs; + children { + iftag("album") { + album = text; + } eliftag("year") { + year = text; + } eliftag("songs") { + children { + iftag("item") { + songs << SongDescription(); + songs.back().title = text; + songs.back().track = static_cast(songs.size()); + } else_skip + } + } else_skip + // need to filter results manually because the filtering provided by Lyrica Wiki API doesn't work + if((m_initialDescription.album.isEmpty() || m_initialDescription.album == album) + && (m_initialDescription.year.isEmpty() || m_initialDescription.year == year) + && (!m_initialDescription.totalTracks || m_initialDescription.totalTracks == static_cast(songs.size()))) { + for(SongDescription &song : songs) { + if((m_initialDescription.title.isEmpty() || m_initialDescription.title == song.title) + && (!m_initialDescription.track || m_initialDescription.track == static_cast(songs.size()))) { + song.album = album; + song.year = year; + song.totalTracks = static_cast(songs.size()); + m_results << move(song); + } + } + } + } + } else_skip + } + } else_skip + } + for(SongDescription &song : m_results) { + song.artist = artist; + } + } else_skip + } +#include + + // check for parsing errors + switch(m_reader.error()) { + case QXmlStreamReader::NoError: + case QXmlStreamReader::PrematureEndOfDocumentError: + break; + default: + m_errorList << m_reader.errorString(); + } + + // promote changes + endResetModel(); +} + +QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription) +{ + static QString defaultUrl(QStringLiteral("https://lyrics.wikia.com/api.php")); + + // compose URL + QUrl url(Settings::lyricsWikiaUrl().isEmpty() ? defaultUrl : Settings::lyricsWikiaUrl()); + QUrlQuery query; + query.addQueryItem(QStringLiteral("func"), QStringLiteral("getArtist")); + query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml")); + query.addQueryItem(QStringLiteral("fixXML"), QString()); + query.addQueryItem(QStringLiteral("artist"), songDescription.artist); + url.setQuery(query); + + // NOTE: Only getArtist seems to work, so artist must be specified and filtering must + // be done manually when parsing results. + + // make request + return new LyricsWikiaResultsModel(move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url))); +} + +} // namespace QtGui diff --git a/dbquery/lyricswikia.h b/dbquery/lyricswikia.h new file mode 100644 index 0000000..443fe2e --- /dev/null +++ b/dbquery/lyricswikia.h @@ -0,0 +1,27 @@ +#ifndef QTGUI_LYRICSWIKIA_H +#define QTGUI_LYRICSWIKIA_H + +#include "./dbquery.h" + +#include + +namespace QtGui { + +class LyricsWikiaResultsModel : public HttpResultsModel +{ + Q_OBJECT + +public: + LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply); + +protected: + void parseResults(QNetworkReply *reply, const QByteArray &data); + +private: + QXmlStreamReader m_reader; + +}; + +} // namespace QtGui + +#endif // QTGUI_LYRICSWIKIA_H diff --git a/dbquery/musicbrainz.cpp b/dbquery/musicbrainz.cpp new file mode 100644 index 0000000..9d55ff0 --- /dev/null +++ b/dbquery/musicbrainz.cpp @@ -0,0 +1,209 @@ +#include "./musicbrainz.h" + +#include "../misc/utility.h" +#include "../misc/networkaccessmanager.h" +#include "../application/settings.h" + +#include +#include +#include +#include + +using namespace std; +using namespace Utility; + +namespace QtGui { + +map MusicBrainzResultsModel::m_coverData = map(); + +MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) : + HttpResultsModel(move(initialSongDescription), 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); + + // parse XML tree +#define xmlReader m_reader +#include + children { + iftag("metadata") { + children { + iftag("recording-list") { + children { + iftag("recording") { + SongDescription currentDescription; + children { + iftag("title") { + currentDescription.title = text; + } eliftag("artist-credit") { + children { + iftag("name-credit") { + children { + iftag("artist") { + children { + iftag("name") { + currentDescription.artist = text; + } else_skip + } + } else_skip + } + } else_skip + } + } eliftag("release-list") { + children { + iftag("release") { + if(currentDescription.albumId.isEmpty()) { + currentDescription.albumId = attribute("id").toString(); + } + children { + iftag("title") { + currentDescription.album = text; + } eliftag("date") { + currentDescription.year = text; + } eliftag("medium-list") { + children { + iftag("medium") { + children { + iftag("position") { + currentDescription.disk = text.toUInt(); + } eliftag("track-list") { + currentDescription.totalTracks = attribute("count").toUInt(); + children { + iftag("track") { + children { + iftag("number") { + currentDescription.track = text.toUInt(); + } else_skip + } + } else_skip + } + } else_skip + } + } else_skip + } + } else_skip + } + } else_skip + } + } eliftag("tag-list") { + children { + iftag("tag") { + children { + iftag("name") { + if(!currentDescription.genre.isEmpty()) { + currentDescription.genre.append(QLatin1Char(' ')); + } + currentDescription.genre.append(text); + } else_skip + } + } else_skip + } + } else_skip + } + m_results << currentDescription; + } else_skip + } + } else_skip + } + } else_skip + } +#include + + // check for parsing errors + switch(m_reader.error()) { + case QXmlStreamReader::NoError: + case QXmlStreamReader::PrematureEndOfDocumentError: + break; + default: + m_errorList << m_reader.errorString(); + } + + // promote changes + endResetModel(); + } +} + +QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription) +{ + static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/")); + + // compose parts + QStringList parts; + parts.reserve(4); + if(!songDescription.title.isEmpty()) { + parts << QChar('\"') % songDescription.title % QChar('\"'); + } + if(!songDescription.artist.isEmpty()) { + parts << QStringLiteral("artist:\"") % songDescription.artist % QChar('\"'); + } + if(!songDescription.album.isEmpty()) { + parts << QStringLiteral("release:\"") % songDescription.album % QChar('\"'); + } + if(songDescription.track) { + parts << QStringLiteral("number:") + QString::number(songDescription.track); + } + + // compose URL + QUrl url(Settings::musicBrainzUrl().isEmpty() ? defaultMusicBrainzUrl : (Settings::musicBrainzUrl() + QStringLiteral("/recording/"))); + QUrlQuery query; + query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND "))); + url.setQuery(query); + + // make request + return new MusicBrainzResultsModel(move(songDescription), 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")))); +} + +} // namespace QtGui diff --git a/dbquery/musicbrainz.h b/dbquery/musicbrainz.h new file mode 100644 index 0000000..43e5310 --- /dev/null +++ b/dbquery/musicbrainz.h @@ -0,0 +1,39 @@ +#ifndef QTGUI_MUSICBRAINZ_H +#define QTGUI_MUSICBRAINZ_H + +#include "./dbquery.h" + +#include + +#include + +QT_FORWARD_DECLARE_CLASS(QNetworkRequest) + +namespace QtGui { + +class MusicBrainzResultsModel : public HttpResultsModel +{ + Q_OBJECT +private: + enum What { + MusicBrainzMetaData, + CoverArt + }; + +public: + MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply); + bool fetchCover(const QModelIndex &index); + static QNetworkRequest coverRequest(const QString &albumId); + +protected: + void parseResults(QNetworkReply *reply, const QByteArray &data); + +private: + static std::map m_coverData; + QXmlStreamReader m_reader; + What m_what; +}; + +} // namespace QtGui + +#endif // QTGUI_MUSICBRAINZ_H diff --git a/gui/dbquerywidget.cpp b/gui/dbquerywidget.cpp index 8222c9c..b180799 100644 --- a/gui/dbquerywidget.cpp +++ b/gui/dbquerywidget.cpp @@ -45,7 +45,7 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent) setStyleSheet(QStringLiteral("QGroupBox { color: palette(text); background-color: palette(base); }")); #endif - m_ui->notificationLabel->setText(tr("Search hasn't been started.")); + m_ui->notificationLabel->setText(tr("Search hasn't been started")); m_ui->searchGroupBox->installEventFilter(this); // initialize buttons @@ -74,7 +74,8 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent) // connect signals and slots connect(m_ui->abortPushButton, &QPushButton::clicked, this, &DbQueryWidget::abortSearch); - connect(m_ui->startPushButton, &QPushButton::clicked, this, &DbQueryWidget::startSearch); + connect(m_ui->searchMusicBrainzPushButton, &QPushButton::clicked, this, &DbQueryWidget::searchMusicBrainz); + connect(m_ui->searchLyricsWikiaPushButton, &QPushButton::clicked, this, &DbQueryWidget::searchLyricsWikia); connect(m_ui->applyPushButton, &QPushButton::clicked, this, &DbQueryWidget::applyResults); connect(m_tagEditorWidget, &TagEditorWidget::fileStatusChanged, this, &DbQueryWidget::fileStatusChanged); connect(m_ui->resultsTreeView, &QTreeView::customContextMenuRequested, this, &DbQueryWidget::showResultsContextMenu); @@ -116,9 +117,9 @@ void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit) } } -void DbQueryWidget::startSearch() +void DbQueryWidget::searchMusicBrainz() { - // check whether enought search terms are supplied + // check whether enough search terms are supplied if(m_ui->titleLineEdit->text().isEmpty() && m_ui->albumLineEdit->text().isEmpty() && m_ui->artistLineEdit->text().isEmpty()) { m_ui->notificationLabel->setNotificationType(NotificationType::Critical); m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied")); @@ -142,7 +143,38 @@ void DbQueryWidget::startSearch() desc.track = m_ui->trackSpinBox->value(); // do actual query - m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(desc)); + m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(std::move(desc))); + connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults); + connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex); +} + +void DbQueryWidget::searchLyricsWikia() +{ + // check whether enough search terms are supplied + if(m_ui->artistLineEdit->text().isEmpty()) { + m_ui->notificationLabel->setNotificationType(NotificationType::Critical); + m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied")); + return; + } + + // delete current model + m_ui->resultsTreeView->setModel(nullptr); + delete m_model; + + // show status + m_ui->notificationLabel->setNotificationType(NotificationType::Progress); + m_ui->notificationLabel->setText(tr("Retrieving meta data ...")); + setStatus(false); + + // get song description + SongDescription desc; + desc.title = m_ui->titleLineEdit->text(); + desc.album = m_ui->albumLineEdit->text(); + desc.artist = m_ui->artistLineEdit->text(); + desc.track = static_cast(m_ui->trackSpinBox->value()); + + // do actual query + m_ui->resultsTreeView->setModel(m_model = queryLyricsWikia(std::move(desc))); connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults); connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex); } @@ -197,7 +229,8 @@ void DbQueryWidget::showResults() void DbQueryWidget::setStatus(bool aborted) { m_ui->abortPushButton->setVisible(!aborted); - m_ui->startPushButton->setVisible(aborted); + m_ui->searchMusicBrainzPushButton->setVisible(aborted); + m_ui->searchLyricsWikiaPushButton->setVisible(aborted); m_ui->applyPushButton->setVisible(aborted); } @@ -271,7 +304,7 @@ void DbQueryWidget::showResultsContextMenu() if(!selection.isEmpty()) { QMenu contextMenu; if(m_ui->applyPushButton->isEnabled()) { - contextMenu.addAction(m_ui->applyPushButton->icon(), m_ui->applyPushButton->text(), this, SLOT(applyResults())); + contextMenu.addAction(m_ui->applyPushButton->icon(), tr("Use selected row"), this, SLOT(applyResults())); } if(m_model && m_model->areResultsAvailable()) { contextMenu.addAction(QIcon::fromTheme(QStringLiteral("view-preview")), tr("Show cover"), this, SLOT(fetchAndShowCoverForSelection())); @@ -353,7 +386,7 @@ bool DbQueryWidget::eventFilter(QObject *obj, QEvent *event) case QEvent::KeyRelease: switch(static_cast(event)->key()) { case Qt::Key_Return: - startSearch(); + searchMusicBrainz(); break; default: ; diff --git a/gui/dbquerywidget.h b/gui/dbquerywidget.h index 1897c9a..bdf0b66 100644 --- a/gui/dbquerywidget.h +++ b/gui/dbquerywidget.h @@ -34,7 +34,8 @@ public: void insertSearchTermsFromTagEdit(TagEdit *tagEdit); public slots: - void startSearch(); + void searchMusicBrainz(); + void searchLyricsWikia(); void abortSearch(); void applyResults(); void insertSearchTermsFromActiveTagEdit(); diff --git a/gui/dbquerywidget.ui b/gui/dbquerywidget.ui index e8ebc3e..74fd1dc 100644 --- a/gui/dbquerywidget.ui +++ b/gui/dbquerywidget.ui @@ -7,11 +7,11 @@ 0 0 731 - 603 + 619 - MusicBrainz search + MusicBrainz/LyricsWikia search @@ -31,6 +31,12 @@ + + + 0 + 3 + + 1 @@ -142,12 +148,6 @@ Qt::Vertical - - - 0 - 0 - - Qt::CustomContextMenu @@ -237,29 +237,22 @@ - - - 325 - 16777215 - + + + 0 + 0 + - - - - Qt::Horizontal - - - - 433 - 20 - - - - + + + 0 + 0 + + 23 @@ -276,15 +269,46 @@ + + + 0 + 0 + + Abort - + + + + 0 + 0 + + - Search + Search +MusicBrainz + + + + .. + + + + + + + + 0 + 0 + + + + Search +LyricWikia @@ -297,11 +321,18 @@ false + + + 0 + 0 + + Inserts the selected result into the current tag (doesn't save anything) - Apply results + Use selected +row diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 094dbe2..d833afa 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -110,7 +110,7 @@ MainWindow::MainWindow(QWidget *parent) : // dbquery dock widget if(Settings::dbQueryWidgetShown()) { - showDbQueryWidget(); + toggleDbQueryWidget(); } else { // ensure the dock widget is invisible m_ui->dbQueryDockWidget->setVisible(false); @@ -122,7 +122,7 @@ MainWindow::MainWindow(QWidget *parent) : // connect signals and slots, install event filter // menu: application connect(m_ui->actionSettings, &QAction::triggered, this, &MainWindow::showSettingsDlg); - connect(m_ui->actionOpen_MusicBrainz_search, &QAction::triggered, this, &MainWindow::showDbQueryWidget); + connect(m_ui->actionOpen_MusicBrainz_search, &QAction::triggered, this, &MainWindow::toggleDbQueryWidget); connect(m_ui->lockLayout, &QAction::triggered, this, &MainWindow::toggleLayoutLocked); connect(m_ui->actionQuit, &QAction::triggered, this, &MainWindow::close); // menu: file @@ -337,14 +337,14 @@ void MainWindow::spawnExternalPlayer() } /*! - * \brief Shows the database query widget. + * \brief Toggles visibility of the database query widget. */ -void MainWindow::showDbQueryWidget() +void MainWindow::toggleDbQueryWidget() { if(!m_dbQueryWidget) { m_ui->dbQueryDockWidget->setWidget(m_dbQueryWidget = new DbQueryWidget(m_ui->tagEditorWidget, this)); } - m_ui->dbQueryDockWidget->setVisible(true); + m_ui->dbQueryDockWidget->setVisible(m_ui->dbQueryDockWidget->isHidden()); } /*! diff --git a/gui/mainwindow.h b/gui/mainwindow.h index 52cd9f1..2b98c1a 100644 --- a/gui/mainwindow.h +++ b/gui/mainwindow.h @@ -77,7 +77,7 @@ private slots: void showAboutDlg(); void showRenameFilesDlg(); void spawnExternalPlayer(); - void showDbQueryWidget(); + void toggleDbQueryWidget(); private: QMutex &fileOperationMutex(); diff --git a/gui/mainwindow.ui b/gui/mainwindow.ui index 2ca0115..84e8b67 100644 --- a/gui/mainwindow.ui +++ b/gui/mainwindow.ui @@ -102,12 +102,12 @@ QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - F&ile selection + File sele&ction 1 - + 2 @@ -152,12 +152,12 @@ false - &MusicBrains search (beta) + &MusicBrains/LyricsWikia search 1 - + @@ -333,7 +333,7 @@ .. - &Open MusicBrainz search + &Toggle MusicBrainz/LyricsWikia search F10 @@ -356,10 +356,11 @@ - + + .. - Lock layout + &Lock layout diff --git a/gui/notificationlabel.cpp b/gui/notificationlabel.cpp index 5cebe31..f60e654 100644 --- a/gui/notificationlabel.cpp +++ b/gui/notificationlabel.cpp @@ -156,30 +156,36 @@ void NotificationLabel::drawProgressIndicator(QPainter &painter, QRect rect, con QSize NotificationLabel::sizeHint() const { - return minimumSizeHint(); -} - -QSize NotificationLabel::minimumSizeHint() const -{ - QFontMetrics fm(font()); + const QFontMetrics fm(fontMetrics()); QSize size = fm.size(0, m_text); if(size.height() < m_minIconSize) { size.setHeight(m_minIconSize); } - int iconSize = size.height() > m_maxIconSize ? m_maxIconSize : size.height(); + const int iconSize = size.height() > m_maxIconSize ? m_maxIconSize : size.height(); size.setWidth(iconSize + 5 + size.width()); return size; } +QSize NotificationLabel::minimumSizeHint() const +{ + return QSize(m_minIconSize, m_minIconSize); +} + void NotificationLabel::setText(const QString &text) { m_text = text; updateGeometry(); update(textRect()); + if(toolTip().isEmpty()) { + setToolTip(text); + } } void NotificationLabel::clearText() { + if(toolTip() == m_text) { + toolTip().clear(); + } m_text.clear(); updateGeometry(); update(textRect()); @@ -187,17 +193,21 @@ void NotificationLabel::clearText() void NotificationLabel::appendLine(const QString &line) { + const bool updateTooltip = toolTip().isEmpty() || toolTip() == m_text; if(m_text.isEmpty()) { m_text = line; } else { - if(!m_text.startsWith("•")) { - m_text.insert(0, "• "); + if(!m_text.startsWith(QStringLiteral("•"))) { + m_text.insert(0, QStringLiteral("• ")); } - m_text.append("\n• "); + m_text.append(QStringLiteral("\n• ")); m_text.append(line); } updateGeometry(); update(textRect()); + if(updateTooltip) { + setToolTip(m_text); + } } void NotificationLabel::setNotificationSubject(NotificationSubject value) diff --git a/gui/tageditorwidget.ui b/gui/tageditorwidget.ui index bdc5721..05066d3 100644 --- a/gui/tageditorwidget.ui +++ b/gui/tageditorwidget.ui @@ -6,7 +6,7 @@ 0 0 - 634 + 702 203 @@ -34,7 +34,14 @@ - + + + + 0 + 0 + + + @@ -195,7 +202,14 @@ the file reverting all unsaved changings. - + + + + 0 + 0 + + + diff --git a/translations/tageditor_de_DE.ts b/translations/tageditor_de_DE.ts index 81075f9..b1acfd4 100644 --- a/translations/tageditor_de_DE.ts +++ b/translations/tageditor_de_DE.ts @@ -649,34 +649,34 @@ QtGui::DbQueryWidget - MusicBrainz search + MusicBrainz/LyricsWikia search - + Search &criteria - + Song - - - - + + + + ? - + Album - + Artist @@ -691,28 +691,36 @@ - + Abort - - Search + + Search +MusicBrainz - + + Search +LyricWikia + + + + Inserts the selected result into the current tag (doesn't save anything) - - Apply results + + Use selected +row - Search hasn't been started. + Search hasn't been started @@ -726,27 +734,29 @@ - + + Insufficient search criteria supplied - + + Retrieving meta data ... - + Aborted - + No results available - + %1 result(s) available @@ -754,22 +764,27 @@ - + Retrieving cover art to be applied ... - + + Use selected row + + + + Show cover - + Retrieving cover art ... - + Cover - %1 @@ -953,7 +968,7 @@ Remarks - + To avoid unnecessary copying this directory should be on the same partition as the files you want to edit. @@ -1550,12 +1565,12 @@ another position would prevent rewriting the entire file - + Minimum padding must be less or equal than maximum padding. - + These options might be ignored if not supported by either the format or the implementation. @@ -1563,22 +1578,22 @@ another position would prevent rewriting the entire file QtGui::HttpResultsModel - + <p>Do you want to redirect form <i>%1</i> to <i>%2</i>?</p> - + Search - + Redirection to: - + Aborted by user. @@ -1731,112 +1746,112 @@ another position would prevent rewriting the entire file - - F&ile selection - - - - - &MusicBrains search (beta) - - - - + Save &entered tags - + &Delete all tags - + Ctrl+D - + &Close - + &Select next file - + &Rename files using tags - + &Open - + &Save file information as HTML document - + Ctrl+H - + Select &next file and save current - + F3 - + &About - + &Quit - + &Settings - + &Play (external player) - + Ctrl+E - - &Open MusicBrainz search + + &Lock layout - + F10 - - Save (entered tags) as ... + + &Toggle MusicBrainz/LyricsWikia search - + + Save (entered tags) &as ... + + + + + Lock layout + + + + Ctrl+Shift+S @@ -1846,47 +1861,57 @@ another position would prevent rewriting the entire file - + Ctrl+S - + Ctrl+O - + F8 - + Ctrl+Q - + + File sele&ction + + + + Select next file - + + &MusicBrains/LyricsWikia search + + + + F6 - + F2 - + &Reload (reverts all changes!) - + F5 @@ -1897,58 +1922,63 @@ another position would prevent rewriting the entire file - + No file opened. - + A tag editing utility supporting ID3, MP4 (iTunes style), Vorbis and Matroska tags. - + Unable to show the next file because it can't be found anymore. - + Open file - - + Save changes as - - + Save file information - - + No file is opened. - + Unable to save file information because the current process hasn't been finished yet. - + Unable to write to file. %1 - + + Unlock layout + + + + Unable to open file. - + No file information available. @@ -1956,12 +1986,12 @@ another position would prevent rewriting the entire file QtGui::MusicBrainzResultsModel - + Unable to fetch cover: Album ID is unknown. - + Cover reply is invalid (internal error). @@ -2285,42 +2315,42 @@ another position would prevent rewriting the entire file QtGui::QueryResultsModel - + Song title - + Album - + Artist - + Year - + Track - + Total tracks - + Genre - + Fetching the cover is not implemented for the selected provider. @@ -2571,17 +2601,17 @@ Error in line %1: %3 - + Tag processing - + Editor - + File browser @@ -2613,12 +2643,12 @@ Error in line %1: %3 - + Document title - + Let you choose whether the values of the previously opened file should be cleared when opening the next file. @@ -2627,12 +2657,12 @@ tagging multiple files of the same album. - + Keep previous values - + Let you enable or disable the automatic creation or removal of tags (according to the settings) when loading a file. @@ -2641,99 +2671,99 @@ You can also create or remove tags manually. - + Tag management - + Restores the original values read from the file reverting all unsaved changings. - + Restore - + Clears all values. - + Clear - + Aborts the saving process. The tageditor will try to restore the original file from the backup. - + Abort - + Save - + all entered values - + Delete - + all tags from the file - + Open next file - + and save current before - + Close - + the file and discard changings - + No, disable this feature - + Yes, but only if both files are in the same directory - + Yes, regardless where the files are stored - + Manage tags automatically when loading file diff --git a/translations/tageditor_en_US.ts b/translations/tageditor_en_US.ts index 01ba157..d9dbe24 100644 --- a/translations/tageditor_en_US.ts +++ b/translations/tageditor_en_US.ts @@ -649,7 +649,7 @@ QtGui::DbQueryWidget - Search hasn't been started. + Search hasn't been started @@ -663,27 +663,29 @@ - + + Insufficient search criteria supplied - + + Retrieving meta data ... - + Aborted - + No results available - + %1 result(s) available %1 result available @@ -691,55 +693,60 @@ - + Retrieving cover art to be applied ... - + + Use selected row + + + + Show cover - + Retrieving cover art ... - + Cover - %1 - MusicBrainz search + MusicBrainz/LyricsWikia search - + Search &criteria - + Song - - - - + + + + ? - + Album - + Artist @@ -754,23 +761,31 @@ - + Abort - - Search + + Search +MusicBrainz - + + Search +LyricWikia + + + + Inserts the selected result into the current tag (doesn't save anything) - - Apply results + + Use selected +row @@ -953,7 +968,7 @@ Remarks - + To avoid unnecessary copying this directory should be on the same partition as the files you want to edit. @@ -1550,12 +1565,12 @@ another position would prevent rewriting the entire file - + Minimum padding must be less or equal than maximum padding. - + These options might be ignored if not supported by either the format or the implementation. @@ -1563,22 +1578,22 @@ another position would prevent rewriting the entire file QtGui::HttpResultsModel - + <p>Do you want to redirect form <i>%1</i> to <i>%2</i>?</p> - + Search - + Redirection to: - + Aborted by user. @@ -1742,213 +1757,228 @@ another position would prevent rewriting the entire file - + + File sele&ction + + + + Select next file - - F&ile selection + + &MusicBrains/LyricsWikia search - - &MusicBrains search (beta) - - - - + Save &entered tags - + Ctrl+S - + &Delete all tags - + Ctrl+D - + &Close - + Ctrl+Q - + &Select next file - + F6 - + &Rename files using tags - + F2 - + &Open - + Ctrl+O - + &Save file information as HTML document - + Ctrl+H - + Select &next file and save current - + F3 - + &About - + &Quit - + &Settings - + F8 - + &Reload (reverts all changes!) - + F5 - + &Play (external player) - + Ctrl+E - - &Open MusicBrainz search + + &Lock layout - + F10 - - Save (entered tags) as ... + + &Toggle MusicBrainz/LyricsWikia search - + + Save (entered tags) &as ... + + + + + Lock layout + + + + Ctrl+Shift+S - + + Unlock layout + + + + No file opened. - + A tag editing utility supporting ID3, MP4 (iTunes style), Vorbis and Matroska tags. - + Unable to show the next file because it can't be found anymore. - + Open file - - + Save changes as - - + Save file information - - + Unable to write to file. %1 - + Unable to open file. - + No file information available. - + No file is opened. - + Unable to save file information because the current process hasn't been finished yet. @@ -1956,12 +1986,12 @@ another position would prevent rewriting the entire file QtGui::MusicBrainzResultsModel - + Unable to fetch cover: Album ID is unknown. - + Cover reply is invalid (internal error). @@ -2285,42 +2315,42 @@ another position would prevent rewriting the entire file QtGui::QueryResultsModel - + Song title - + Album - + Artist - + Year - + Track - + Total tracks - + Genre - + Fetching the cover is not implemented for the selected provider. @@ -2571,17 +2601,17 @@ Error in line %1: %3 - + Tag processing - + Editor - + File browser @@ -2914,12 +2944,12 @@ Error in line %1: %3 - + Document title - + Let you choose whether the values of the previously opened file should be cleared when opening the next file. @@ -2928,12 +2958,12 @@ tagging multiple files of the same album. - + Keep previous values - + Let you enable or disable the automatic creation or removal of tags (according to the settings) when loading a file. @@ -2942,99 +2972,99 @@ You can also create or remove tags manually. - + Tag management - + Restores the original values read from the file reverting all unsaved changings. - + Restore - + Clears all values. - + Clear - + Aborts the saving process. The tageditor will try to restore the original file from the backup. - + Abort - + Save - + all entered values - + Delete - + all tags from the file - + Open next file - + and save current before - + Close - + the file and discard changings - + No, disable this feature - + Yes, but only if both files are in the same directory - + Yes, regardless where the files are stored - + Manage tags automatically when loading file