Implement downloading cover from LyricWiki

This commit is contained in:
Martchus 2017-07-30 20:30:50 +02:00
parent 7400534d20
commit 15fc9b5f66
6 changed files with 151 additions and 45 deletions

View File

@ -22,6 +22,8 @@ SongDescription::SongDescription() :
cover(nullptr)
{}
map<QString, QByteArray> QueryResultsModel::m_coverData = map<QString, QByteArray>();
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"

View File

@ -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<QString, QByteArray> m_coverData;
};
inline const QList<SongDescription> &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();

View File

@ -8,6 +8,7 @@
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QTextDocument>
#include <QStringBuilder>
#include <functional>
@ -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("<div class='lyricbox'>");
if(!lyricsStart) {
const int lyricsStart = html.indexOf(QLatin1String("<div class='lyricbox'>"));
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("<div class='lyricsbreak'></div>", lyricsStart);
const int lyricsEnd = html.indexOf(QLatin1String("<div class='lyricsbreak'></div>"), 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("<div class=\"plainlinks\" style=\"clear:right; float:right;")) + 56;
if(coverDivStart > 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

View File

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

View File

@ -18,8 +18,6 @@ using namespace Utility;
namespace QtGui {
map<QString, QByteArray> MusicBrainzResultsModel::m_coverData = map<QString, QByteArray>();
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/"));

View File

@ -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<QString, QByteArray> m_coverData;
What m_what;
};