diff --git a/dbquery/dbquery.cpp b/dbquery/dbquery.cpp index 6288611..4977bd3 100644 --- a/dbquery/dbquery.cpp +++ b/dbquery/dbquery.cpp @@ -22,6 +22,8 @@ SongDescription::SongDescription() : cover(nullptr) {} +map QueryResultsModel::m_coverData = map(); + QueryResultsModel::QueryResultsModel(QObject *parent) : QAbstractTableModel(parent), m_resultsAvailable(false), @@ -282,7 +284,7 @@ QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByt m_replies.removeAll(reply); if(reply->error() == QNetworkReply::NoError) { - QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + const QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); if(!redirectionTarget.isNull()) { // there's a redirection available // -> resolve new URL @@ -328,6 +330,35 @@ void HttpResultsModel::abort() } } +void HttpResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row) +{ + QByteArray data; + if(auto *newReply = evaluateReplyResults(reply, data, true)) { + addReply(newReply, bind(&HttpResultsModel::handleCoverReplyFinished, this, newReply, albumId, row)); + } else { + if(!data.isEmpty()) { + parseCoverResults(albumId, row, data); + } + setResultsAvailable(true); + } +} + +void HttpResultsModel::parseCoverResults(const QString &albumId, int row, const QByteArray &data) +{ + // add cover -> determine album ID and row + if(!albumId.isEmpty() && row < m_results.size()) { + if(!data.isEmpty()) { + m_coverData[albumId] = data; + m_results[row].cover = data; + emit coverAvailable(index(row, 0)); + } + } else { + m_errorList << tr("Internal error: context for cover reply invalid"); + setResultsAvailable(true); + } + setFetchingCover(false); +} + } #include "dbquery.moc" diff --git a/dbquery/dbquery.h b/dbquery/dbquery.h index 71fd688..35a7dd4 100644 --- a/dbquery/dbquery.h +++ b/dbquery/dbquery.h @@ -35,6 +35,7 @@ struct SongDescription int32 disk; QByteArray cover; QString lyrics; + QString coverUrl; }; class QueryResultsModel : public QAbstractTableModel @@ -83,6 +84,7 @@ protected: QStringList m_errorList; bool m_resultsAvailable; bool m_fetchingCover; + static std::map m_coverData; }; inline const QList &QueryResultsModel::results() const @@ -119,6 +121,9 @@ protected: virtual void parseInitialResults(const QByteArray &data) = 0; QNetworkReply *evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection = false); + void handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row); + void parseCoverResults(const QString &albumId, int row, const QByteArray &data); + private slots: void handleInitialReplyFinished(); diff --git a/dbquery/lyricswikia.cpp b/dbquery/lyricswikia.cpp index abd3384..ef8bea6 100644 --- a/dbquery/lyricswikia.cpp +++ b/dbquery/lyricswikia.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -29,6 +30,39 @@ LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDe HttpResultsModel(move(initialSongDescription), reply) {} +bool LyricsWikiaResultsModel::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 &) { + if(desc.coverUrl.isEmpty()) { + // request the cover URL + auto *reply = requestAlbumDetails(desc); + addReply(reply, bind(&LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished, this, reply, index.row())); + setFetchingCover(true); + return false; + } else { + // request the cover art + auto *reply = networkAccessManager().get(QNetworkRequest(QUrl(desc.coverUrl))); + addReply(reply, bind(&LyricsWikiaResultsModel::handleCoverReplyFinished, this, reply, desc.albumId, index.row())); + setFetchingCover(true); + return false; + } + } + } else { + m_errorList << tr("Unable to fetch cover: Album ID unknown"); + emit resultsAvailable(); + } + } + return true; +} + bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index) { if(!index.parent().isValid() && index.row() < m_results.size()) { @@ -49,7 +83,7 @@ bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index) void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data) { - // prepare parsing MusicBrainz meta data + // prepare parsing LyricsWikia meta data beginResetModel(); m_results.clear(); QXmlStreamReader xmlReader(data); @@ -101,7 +135,11 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data) } else_skip } for(SongDescription &song : m_results) { + // set the arist which is the same for all results song.artist = artist; + // set the album ID (album is identified by its artist, year and name) + song.albumId = artist % QChar(':') % song.album % QChar('_') % QChar('(') % song.year % QChar(')'); + song.albumId.replace(QChar(' '), QChar('_')); } } else_skip } @@ -131,7 +169,7 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription query.addQueryItem(QStringLiteral("artist"), songDescription.artist); query.addQueryItem(QStringLiteral("title"), songDescription.title); if(!songDescription.album.isEmpty()) { - // specifying album seems to have no effect but also don't hurt + // specifying album seems to have no effect but also doesn't hurt query.addQueryItem(QStringLiteral("album"), songDescription.album); } QUrl url(lyricsWikiaApiUrl()); @@ -140,6 +178,13 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription return Utility::networkAccessManager().get(QNetworkRequest(url)); } +QNetworkReply *LyricsWikiaResultsModel::requestAlbumDetails(const SongDescription &songDescription) +{ + QUrl url(lyricsWikiaApiUrl()); + url.setPath(QStringLiteral("/wiki/") + songDescription.albumId); + return Utility::networkAccessManager().get(QNetworkRequest(url)); +} + void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, int row) { QByteArray data; @@ -237,22 +282,80 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data } SongDescription &assocDesc = m_results[row]; + // convert data to QString + const QString html(data); + // parse lyrics from HTML - QString html(data); - int lyricsStart = html.indexOf("
"); - if(!lyricsStart) { + const int lyricsStart = html.indexOf(QLatin1String("
")); + if(lyricsStart > 0) { m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title); setResultsAvailable(true); return; } - int lyricsEnd = html.indexOf("
", lyricsStart); + const int lyricsEnd = html.indexOf(QLatin1String("
"), lyricsStart); QTextDocument textDoc; textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1)); - assocDesc.lyrics = textDoc.toPlainText(); + emit lyricsAvailable(index(row, 0)); } +void LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished(QNetworkReply *reply, int row) +{ + QByteArray data; + if(auto *newReply = evaluateReplyResults(reply, data, true)) { + addReply(newReply, bind(&LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished, this, newReply, row)); + } else { + parseAlbumDetailsAndFetchCover(row, data); + } +} + +void LyricsWikiaResultsModel::parseAlbumDetailsAndFetchCover(int row, const QByteArray &data) +{ + // check whether the request failed (sufficient error message already emitted in this case) + if(data.isEmpty()) { + setFetchingCover(false); + setResultsAvailable(true); + return; + } + + // find associated result/desc + if(row >= m_results.size()) { + m_errorList << tr("Internal error: context for LyricsWikia page reply invalid"); + setFetchingCover(false); + setResultsAvailable(true); + return; + } + SongDescription &assocDesc = m_results[row]; + + // convert data to QString + const QString html(data); + + // parse cover URL from HTML + const int coverDivStart = html.indexOf(QLatin1String("
56) { + const int coverHrefStart = html.indexOf(QLatin1String("href=\""), coverDivStart) + 6; + if(coverHrefStart > coverDivStart + 6) { + const int coverHrefEnd = html.indexOf(QLatin1String("\""), coverHrefStart); + if(coverHrefEnd > 0) { + assocDesc.coverUrl = html.mid(coverHrefStart, coverHrefEnd - coverHrefStart); + } + } + } + + // handle error case (cover URL not found) + if(assocDesc.coverUrl.isEmpty()) { + m_errorList << tr("Album details requested for %1/%2 do not contain cover").arg(assocDesc.artist, assocDesc.album); + setFetchingCover(false); + setResultsAvailable(true); + return; + } + + // request the cover art + auto *reply = networkAccessManager().get(QNetworkRequest(QUrl(assocDesc.coverUrl))); + addReply(reply, bind(&LyricsWikiaResultsModel::handleCoverReplyFinished, this, reply, assocDesc.albumId, row)); +} + QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription) { // compose URL diff --git a/dbquery/lyricswikia.h b/dbquery/lyricswikia.h index bfb25ae..682947a 100644 --- a/dbquery/lyricswikia.h +++ b/dbquery/lyricswikia.h @@ -13,6 +13,7 @@ class LyricsWikiaResultsModel : public HttpResultsModel public: LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply); + bool fetchCover(const QModelIndex &index); bool fetchLyrics(const QModelIndex &index); protected: @@ -20,11 +21,13 @@ protected: private: QNetworkReply *requestSongDetails(const SongDescription &songDescription); + QNetworkReply *requestAlbumDetails(const SongDescription &songDescription); void handleSongDetailsFinished(QNetworkReply *reply, int row); void parseSongDetails(int row, const QByteArray &data); void handleLyricsReplyFinished(QNetworkReply *reply, int row); void parseLyricsResults(int row, const QByteArray &data); - + void handleAlbumDetailsReplyFinished(QNetworkReply *reply, int row); + void parseAlbumDetailsAndFetchCover(int row, const QByteArray &data); }; } // namespace QtGui diff --git a/dbquery/musicbrainz.cpp b/dbquery/musicbrainz.cpp index d12c7e8..76fbe40 100644 --- a/dbquery/musicbrainz.cpp +++ b/dbquery/musicbrainz.cpp @@ -18,8 +18,6 @@ using namespace Utility; namespace QtGui { -map MusicBrainzResultsModel::m_coverData = map(); - MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) : HttpResultsModel(move(initialSongDescription), reply) {} @@ -155,35 +153,6 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data) endResetModel(); } -void MusicBrainzResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row) -{ - QByteArray data; - if(auto *newReply = evaluateReplyResults(reply, data, true)) { - addReply(newReply, bind(&MusicBrainzResultsModel::handleCoverReplyFinished, this, newReply, albumId, row)); - } else { - if(!data.isEmpty()) { - parseCoverResults(albumId, row, data); - } - setResultsAvailable(true); - } -} - -void MusicBrainzResultsModel::parseCoverResults(const QString &albumId, int row, const QByteArray &data) -{ - // add cover -> determine album ID and row - if(!albumId.isEmpty() && row < m_results.size()) { - if(!data.isEmpty()) { - m_coverData[albumId] = data; - m_results[row].cover = data; - emit coverAvailable(index(row, 0)); - } - } else { - m_errorList << tr("Internal error: context for cover reply invalid"); - setResultsAvailable(true); - } - setFetchingCover(false); -} - QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription) { static const QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/")); diff --git a/dbquery/musicbrainz.h b/dbquery/musicbrainz.h index 14d5a58..60de6c1 100644 --- a/dbquery/musicbrainz.h +++ b/dbquery/musicbrainz.h @@ -26,11 +26,6 @@ protected: void parseInitialResults(const QByteArray &data); private: - void handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row); - void parseCoverResults(const QString &albumId, int row, const QByteArray &data); - -private: - static std::map m_coverData; What m_what; };