Implement downloading cover from LyricWiki
This commit is contained in:
parent
7400534d20
commit
15fc9b5f66
|
@ -22,6 +22,8 @@ SongDescription::SongDescription() :
|
||||||
cover(nullptr)
|
cover(nullptr)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
map<QString, QByteArray> QueryResultsModel::m_coverData = map<QString, QByteArray>();
|
||||||
|
|
||||||
QueryResultsModel::QueryResultsModel(QObject *parent) :
|
QueryResultsModel::QueryResultsModel(QObject *parent) :
|
||||||
QAbstractTableModel(parent),
|
QAbstractTableModel(parent),
|
||||||
m_resultsAvailable(false),
|
m_resultsAvailable(false),
|
||||||
|
@ -282,7 +284,7 @@ QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByt
|
||||||
m_replies.removeAll(reply);
|
m_replies.removeAll(reply);
|
||||||
|
|
||||||
if(reply->error() == QNetworkReply::NoError) {
|
if(reply->error() == QNetworkReply::NoError) {
|
||||||
QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
const QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||||
if(!redirectionTarget.isNull()) {
|
if(!redirectionTarget.isNull()) {
|
||||||
// there's a redirection available
|
// there's a redirection available
|
||||||
// -> resolve new URL
|
// -> 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"
|
#include "dbquery.moc"
|
||||||
|
|
|
@ -35,6 +35,7 @@ struct SongDescription
|
||||||
int32 disk;
|
int32 disk;
|
||||||
QByteArray cover;
|
QByteArray cover;
|
||||||
QString lyrics;
|
QString lyrics;
|
||||||
|
QString coverUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
class QueryResultsModel : public QAbstractTableModel
|
class QueryResultsModel : public QAbstractTableModel
|
||||||
|
@ -83,6 +84,7 @@ protected:
|
||||||
QStringList m_errorList;
|
QStringList m_errorList;
|
||||||
bool m_resultsAvailable;
|
bool m_resultsAvailable;
|
||||||
bool m_fetchingCover;
|
bool m_fetchingCover;
|
||||||
|
static std::map<QString, QByteArray> m_coverData;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline const QList<SongDescription> &QueryResultsModel::results() const
|
inline const QList<SongDescription> &QueryResultsModel::results() const
|
||||||
|
@ -119,6 +121,9 @@ protected:
|
||||||
virtual void parseInitialResults(const QByteArray &data) = 0;
|
virtual void parseInitialResults(const QByteArray &data) = 0;
|
||||||
QNetworkReply *evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection = false);
|
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:
|
private slots:
|
||||||
void handleInitialReplyFinished();
|
void handleInitialReplyFinished();
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
@ -29,6 +30,39 @@ LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDe
|
||||||
HttpResultsModel(move(initialSongDescription), reply)
|
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)
|
bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
if(!index.parent().isValid() && index.row() < m_results.size()) {
|
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)
|
void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
|
||||||
{
|
{
|
||||||
// prepare parsing MusicBrainz meta data
|
// prepare parsing LyricsWikia meta data
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_results.clear();
|
m_results.clear();
|
||||||
QXmlStreamReader xmlReader(data);
|
QXmlStreamReader xmlReader(data);
|
||||||
|
@ -101,7 +135,11 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
|
||||||
} else_skip
|
} else_skip
|
||||||
}
|
}
|
||||||
for(SongDescription &song : m_results) {
|
for(SongDescription &song : m_results) {
|
||||||
|
// set the arist which is the same for all results
|
||||||
song.artist = artist;
|
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
|
} else_skip
|
||||||
}
|
}
|
||||||
|
@ -131,7 +169,7 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription
|
||||||
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
|
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
|
||||||
query.addQueryItem(QStringLiteral("title"), songDescription.title);
|
query.addQueryItem(QStringLiteral("title"), songDescription.title);
|
||||||
if(!songDescription.album.isEmpty()) {
|
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);
|
query.addQueryItem(QStringLiteral("album"), songDescription.album);
|
||||||
}
|
}
|
||||||
QUrl url(lyricsWikiaApiUrl());
|
QUrl url(lyricsWikiaApiUrl());
|
||||||
|
@ -140,6 +178,13 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription
|
||||||
return Utility::networkAccessManager().get(QNetworkRequest(url));
|
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)
|
void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, int row)
|
||||||
{
|
{
|
||||||
QByteArray data;
|
QByteArray data;
|
||||||
|
@ -237,22 +282,80 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data
|
||||||
}
|
}
|
||||||
SongDescription &assocDesc = m_results[row];
|
SongDescription &assocDesc = m_results[row];
|
||||||
|
|
||||||
|
// convert data to QString
|
||||||
|
const QString html(data);
|
||||||
|
|
||||||
// parse lyrics from HTML
|
// parse lyrics from HTML
|
||||||
QString html(data);
|
const int lyricsStart = html.indexOf(QLatin1String("<div class='lyricbox'>"));
|
||||||
int lyricsStart = html.indexOf("<div class='lyricbox'>");
|
if(lyricsStart > 0) {
|
||||||
if(!lyricsStart) {
|
|
||||||
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
|
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
|
||||||
setResultsAvailable(true);
|
setResultsAvailable(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int lyricsEnd = html.indexOf("<div class='lyricsbreak'></div>", lyricsStart);
|
const int lyricsEnd = html.indexOf(QLatin1String("<div class='lyricsbreak'></div>"), lyricsStart);
|
||||||
QTextDocument textDoc;
|
QTextDocument textDoc;
|
||||||
textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1));
|
textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1));
|
||||||
|
|
||||||
assocDesc.lyrics = textDoc.toPlainText();
|
assocDesc.lyrics = textDoc.toPlainText();
|
||||||
|
|
||||||
emit lyricsAvailable(index(row, 0));
|
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)
|
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription)
|
||||||
{
|
{
|
||||||
// compose URL
|
// compose URL
|
||||||
|
|
|
@ -13,6 +13,7 @@ class LyricsWikiaResultsModel : public HttpResultsModel
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
|
LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
|
||||||
|
bool fetchCover(const QModelIndex &index);
|
||||||
bool fetchLyrics(const QModelIndex &index);
|
bool fetchLyrics(const QModelIndex &index);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -20,11 +21,13 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkReply *requestSongDetails(const SongDescription &songDescription);
|
QNetworkReply *requestSongDetails(const SongDescription &songDescription);
|
||||||
|
QNetworkReply *requestAlbumDetails(const SongDescription &songDescription);
|
||||||
void handleSongDetailsFinished(QNetworkReply *reply, int row);
|
void handleSongDetailsFinished(QNetworkReply *reply, int row);
|
||||||
void parseSongDetails(int row, const QByteArray &data);
|
void parseSongDetails(int row, const QByteArray &data);
|
||||||
void handleLyricsReplyFinished(QNetworkReply *reply, int row);
|
void handleLyricsReplyFinished(QNetworkReply *reply, int row);
|
||||||
void parseLyricsResults(int row, const QByteArray &data);
|
void parseLyricsResults(int row, const QByteArray &data);
|
||||||
|
void handleAlbumDetailsReplyFinished(QNetworkReply *reply, int row);
|
||||||
|
void parseAlbumDetailsAndFetchCover(int row, const QByteArray &data);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QtGui
|
} // namespace QtGui
|
||||||
|
|
|
@ -18,8 +18,6 @@ using namespace Utility;
|
||||||
|
|
||||||
namespace QtGui {
|
namespace QtGui {
|
||||||
|
|
||||||
map<QString, QByteArray> MusicBrainzResultsModel::m_coverData = map<QString, QByteArray>();
|
|
||||||
|
|
||||||
MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) :
|
MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) :
|
||||||
HttpResultsModel(move(initialSongDescription), reply)
|
HttpResultsModel(move(initialSongDescription), reply)
|
||||||
{}
|
{}
|
||||||
|
@ -155,35 +153,6 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
|
||||||
endResetModel();
|
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)
|
QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
|
||||||
{
|
{
|
||||||
static const QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
|
static const QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
|
||||||
|
|
|
@ -26,11 +26,6 @@ protected:
|
||||||
void parseInitialResults(const QByteArray &data);
|
void parseInitialResults(const QByteArray &data);
|
||||||
|
|
||||||
private:
|
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;
|
What m_what;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue