Replace echonest artist images with spotify #5416
This commit is contained in:
parent
0a4eafafcd
commit
aeb493c016
|
@ -302,7 +302,6 @@ set(SOURCES
|
||||||
songinfo/collapsibleinfoheader.cpp
|
songinfo/collapsibleinfoheader.cpp
|
||||||
songinfo/collapsibleinfopane.cpp
|
songinfo/collapsibleinfopane.cpp
|
||||||
songinfo/echonestbiographies.cpp
|
songinfo/echonestbiographies.cpp
|
||||||
songinfo/echonestimages.cpp
|
|
||||||
songinfo/songinfobase.cpp
|
songinfo/songinfobase.cpp
|
||||||
songinfo/songinfofetcher.cpp
|
songinfo/songinfofetcher.cpp
|
||||||
songinfo/songinfoprovider.cpp
|
songinfo/songinfoprovider.cpp
|
||||||
|
@ -312,6 +311,7 @@ set(SOURCES
|
||||||
songinfo/songkickconcerts.cpp
|
songinfo/songkickconcerts.cpp
|
||||||
songinfo/songkickconcertwidget.cpp
|
songinfo/songkickconcertwidget.cpp
|
||||||
songinfo/songplaystats.cpp
|
songinfo/songplaystats.cpp
|
||||||
|
songinfo/spotifyimages.cpp
|
||||||
songinfo/taglyricsinfoprovider.cpp
|
songinfo/taglyricsinfoprovider.cpp
|
||||||
songinfo/ultimatelyricslyric.cpp
|
songinfo/ultimatelyricslyric.cpp
|
||||||
songinfo/ultimatelyricsprovider.cpp
|
songinfo/ultimatelyricsprovider.cpp
|
||||||
|
@ -594,7 +594,6 @@ set(HEADERS
|
||||||
songinfo/collapsibleinfoheader.h
|
songinfo/collapsibleinfoheader.h
|
||||||
songinfo/collapsibleinfopane.h
|
songinfo/collapsibleinfopane.h
|
||||||
songinfo/echonestbiographies.h
|
songinfo/echonestbiographies.h
|
||||||
songinfo/echonestimages.h
|
|
||||||
songinfo/songinfobase.h
|
songinfo/songinfobase.h
|
||||||
songinfo/songinfofetcher.h
|
songinfo/songinfofetcher.h
|
||||||
songinfo/songinfoprovider.h
|
songinfo/songinfoprovider.h
|
||||||
|
@ -604,6 +603,7 @@ set(HEADERS
|
||||||
songinfo/songkickconcerts.h
|
songinfo/songkickconcerts.h
|
||||||
songinfo/songkickconcertwidget.h
|
songinfo/songkickconcertwidget.h
|
||||||
songinfo/songplaystats.h
|
songinfo/songplaystats.h
|
||||||
|
songinfo/spotifyimages.h
|
||||||
songinfo/taglyricsinfoprovider.h
|
songinfo/taglyricsinfoprovider.h
|
||||||
songinfo/ultimatelyricslyric.h
|
songinfo/ultimatelyricslyric.h
|
||||||
songinfo/ultimatelyricsprovider.h
|
songinfo/ultimatelyricsprovider.h
|
||||||
|
|
|
@ -16,10 +16,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "artistinfoview.h"
|
#include "artistinfoview.h"
|
||||||
#include "echonestbiographies.h"
|
|
||||||
#include "echonestimages.h"
|
#include "songinfo/echonestbiographies.h"
|
||||||
#include "songinfofetcher.h"
|
#include "songinfo/songinfofetcher.h"
|
||||||
#include "songkickconcerts.h"
|
#include "songinfo/songkickconcerts.h"
|
||||||
|
#include "songinfo/spotifyimages.h"
|
||||||
#include "widgets/prettyimageview.h"
|
#include "widgets/prettyimageview.h"
|
||||||
|
|
||||||
#ifdef HAVE_LIBLASTFM
|
#ifdef HAVE_LIBLASTFM
|
||||||
|
@ -29,8 +30,8 @@
|
||||||
|
|
||||||
ArtistInfoView::ArtistInfoView(QWidget* parent) : SongInfoBase(parent) {
|
ArtistInfoView::ArtistInfoView(QWidget* parent) : SongInfoBase(parent) {
|
||||||
fetcher_->AddProvider(new EchoNestBiographies);
|
fetcher_->AddProvider(new EchoNestBiographies);
|
||||||
fetcher_->AddProvider(new EchoNestImages);
|
|
||||||
fetcher_->AddProvider(new SongkickConcerts);
|
fetcher_->AddProvider(new SongkickConcerts);
|
||||||
|
fetcher_->AddProvider(new SpotifyImages);
|
||||||
#ifdef HAVE_LIBLASTFM
|
#ifdef HAVE_LIBLASTFM
|
||||||
fetcher_->AddProvider(new EchoNestSimilarArtists);
|
fetcher_->AddProvider(new EchoNestSimilarArtists);
|
||||||
fetcher_->AddProvider(new EchoNestTags);
|
fetcher_->AddProvider(new EchoNestTags);
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
/* This file is part of Clementine.
|
|
||||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
|
||||||
|
|
||||||
Clementine is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Clementine is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "echonestimages.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include <echonest/Artist.h>
|
|
||||||
#include <qjson/parser.h>
|
|
||||||
|
|
||||||
#include "core/closure.h"
|
|
||||||
#include "core/logging.h"
|
|
||||||
#include "core/network.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
static const char* kSpotifyBucket = "spotify";
|
|
||||||
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
|
|
||||||
}
|
|
||||||
|
|
||||||
EchoNestImages::EchoNestImages() : network_(new NetworkAccessManager) {}
|
|
||||||
|
|
||||||
EchoNestImages::~EchoNestImages() {}
|
|
||||||
|
|
||||||
void EchoNestImages::FetchInfo(int id, const Song& metadata) {
|
|
||||||
Echonest::Artist artist;
|
|
||||||
artist.setName(metadata.artist());
|
|
||||||
|
|
||||||
// Search for images directly on echonest.
|
|
||||||
// This is currently a bit limited as most results are for last.fm urls that
|
|
||||||
// no longer work.
|
|
||||||
QNetworkReply* reply = artist.fetchImages();
|
|
||||||
RegisterReply(reply, id);
|
|
||||||
NewClosure(reply, SIGNAL(finished()), this,
|
|
||||||
SLOT(RequestFinished(QNetworkReply*, int, Echonest::Artist)),
|
|
||||||
reply, id, artist);
|
|
||||||
|
|
||||||
// Also look up the artist id for the spotify API so we can directly request
|
|
||||||
// images from there too.
|
|
||||||
Echonest::Artist::SearchParams params;
|
|
||||||
params.push_back(
|
|
||||||
qMakePair(Echonest::Artist::Name, QVariant(metadata.artist())));
|
|
||||||
QNetworkReply* rosetta_reply = Echonest::Artist::search(
|
|
||||||
params,
|
|
||||||
Echonest::ArtistInformation(Echonest::ArtistInformation::NoInformation,
|
|
||||||
QStringList() << kSpotifyBucket));
|
|
||||||
RegisterReply(rosetta_reply, id);
|
|
||||||
NewClosure(rosetta_reply, SIGNAL(finished()), this,
|
|
||||||
SLOT(IdsFound(QNetworkReply*, int)), rosetta_reply, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EchoNestImages::RequestFinished(QNetworkReply* reply, int id,
|
|
||||||
Echonest::Artist artist) {
|
|
||||||
reply->deleteLater();
|
|
||||||
try {
|
|
||||||
artist.parseProfile(reply);
|
|
||||||
} catch (Echonest::ParseError e) {
|
|
||||||
qLog(Warning) << "Error parsing echonest reply:" << e.errorType()
|
|
||||||
<< e.what();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const Echonest::ArtistImage& image : artist.images()) {
|
|
||||||
// Echonest still sends these broken URLs for last.fm.
|
|
||||||
if (image.url().authority() != "userserve-ak.last.fm") {
|
|
||||||
emit ImageReady(id, image.url());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EchoNestImages::IdsFound(QNetworkReply* reply, int request_id) {
|
|
||||||
reply->deleteLater();
|
|
||||||
try {
|
|
||||||
Echonest::Artists artists = Echonest::Artist::parseSearch(reply);
|
|
||||||
if (artists.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Echonest::ForeignIds& foreign_ids = artists.first().foreignIds();
|
|
||||||
for (const Echonest::ForeignId& id : foreign_ids) {
|
|
||||||
if (id.catalog.contains("spotify")) {
|
|
||||||
DoSpotifyImageRequest(id.foreign_id, request_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Echonest::ParseError e) {
|
|
||||||
qLog(Warning) << "Error parsing echonest reply:" << e.errorType()
|
|
||||||
<< e.what();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EchoNestImages::DoSpotifyImageRequest(const QString& id, int request_id) {
|
|
||||||
QString artist_id = id.split(":").last();
|
|
||||||
QUrl url(QString(kSpotifyArtistUrl).arg(artist_id));
|
|
||||||
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
|
||||||
RegisterReply(reply, request_id);
|
|
||||||
NewClosure(reply, SIGNAL(finished()), [this, reply, request_id]() {
|
|
||||||
reply->deleteLater();
|
|
||||||
QJson::Parser parser;
|
|
||||||
QVariantMap result = parser.parse(reply).toMap();
|
|
||||||
QVariantList images = result["images"].toList();
|
|
||||||
QList<QPair<QUrl, QSize>> image_urls;
|
|
||||||
for (const QVariant& image : images) {
|
|
||||||
QVariantMap image_result = image.toMap();
|
|
||||||
image_urls.append(qMakePair(image_result["url"].toUrl(),
|
|
||||||
QSize(image_result["width"].toInt(),
|
|
||||||
image_result["height"].toInt())));
|
|
||||||
}
|
|
||||||
// All the images are the same just different sizes; just pick the largest.
|
|
||||||
std::sort(image_urls.begin(), image_urls.end(),
|
|
||||||
[](const QPair<QUrl, QSize>& a,
|
|
||||||
const QPair<QUrl, QSize>& b) {
|
|
||||||
// Sorted by area ascending.
|
|
||||||
return (a.second.height() * a.second.width()) <
|
|
||||||
(b.second.height() * b.second.width());
|
|
||||||
});
|
|
||||||
if (!image_urls.isEmpty()) {
|
|
||||||
emit ImageReady(request_id, image_urls.last().first);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keeps track of replies and emits Finished() when all replies associated with
|
|
||||||
// a request are finished with.
|
|
||||||
void EchoNestImages::RegisterReply(QNetworkReply* reply, int id) {
|
|
||||||
replies_.insert(id, reply);
|
|
||||||
NewClosure(reply, SIGNAL(destroyed()), [this, reply, id]() {
|
|
||||||
replies_.remove(id, reply);
|
|
||||||
if (!replies_.contains(id)) {
|
|
||||||
emit Finished(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
#include "spotifyimages.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <qjson/parser.h>
|
||||||
|
|
||||||
|
#include <QPair>
|
||||||
|
|
||||||
|
#include "core/closure.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/network.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
static const char* kSpotifySearchUrl = "https://api.spotify.com/v1/search";
|
||||||
|
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString ExtractSpotifyId(const QString& spotify_uri) {
|
||||||
|
return spotify_uri.split(':')[2];
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SpotifyImages::SpotifyImages()
|
||||||
|
: network_(new NetworkAccessManager) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotifyImages::~SpotifyImages() {}
|
||||||
|
|
||||||
|
void SpotifyImages::FetchInfo(int id, const Song& metadata) {
|
||||||
|
if (metadata.artist().isEmpty()) {
|
||||||
|
emit Finished(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch artist id.
|
||||||
|
QUrl search_url(kSpotifySearchUrl);
|
||||||
|
search_url.addQueryItem("q", metadata.artist());
|
||||||
|
search_url.addQueryItem("type", "artist");
|
||||||
|
search_url.addQueryItem("limit", "1");
|
||||||
|
|
||||||
|
qLog(Debug) << "Fetching artist:" << search_url;
|
||||||
|
|
||||||
|
QNetworkRequest request(search_url);
|
||||||
|
QNetworkReply* reply = network_->get(request);
|
||||||
|
NewClosure(reply, SIGNAL(finished()), [this, id, reply]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
QJson::Parser parser;
|
||||||
|
QVariantMap result = parser.parse(reply).toMap();
|
||||||
|
QVariantMap artists = result["artists"].toMap();
|
||||||
|
if (artists.isEmpty()) {
|
||||||
|
emit Finished(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantList items = artists["items"].toList();
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
emit Finished(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap artist = items.first().toMap();
|
||||||
|
QString spotify_uri = artist["uri"].toString();
|
||||||
|
|
||||||
|
FetchImagesForArtist(id, ExtractSpotifyId(spotify_uri));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpotifyImages::FetchImagesForArtist(int id, const QString& spotify_id) {
|
||||||
|
QUrl artist_url(QString(kSpotifyArtistUrl).arg(spotify_id));
|
||||||
|
qLog(Debug) << "Fetching images for artist:" << artist_url;
|
||||||
|
QNetworkRequest request(artist_url);
|
||||||
|
QNetworkReply* reply = network_->get(request);
|
||||||
|
NewClosure(reply, SIGNAL(finished()), [this, id, reply]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
QJson::Parser parser;
|
||||||
|
QVariantMap result = parser.parse(reply).toMap();
|
||||||
|
QVariantList images = result["images"].toList();
|
||||||
|
QList<QPair<QUrl, QSize>> image_candidates;
|
||||||
|
for (QVariant i : images) {
|
||||||
|
QVariantMap image = i.toMap();
|
||||||
|
int height = image["height"].toInt();
|
||||||
|
int width = image["width"].toInt();
|
||||||
|
QUrl url = image["url"].toUrl();
|
||||||
|
image_candidates.append(qMakePair(url, QSize(width, height)));
|
||||||
|
}
|
||||||
|
QPair<QUrl, QSize> winner = *std::max_element(
|
||||||
|
image_candidates.begin(),
|
||||||
|
image_candidates.end(),
|
||||||
|
[](const QPair<QUrl, QSize>& a, const QPair<QUrl, QSize>& b) {
|
||||||
|
return (a.second.height() * a.second.width()) < (b.second.height() * b.second.width());
|
||||||
|
});
|
||||||
|
emit ImageReady(id, winner.first);
|
||||||
|
emit Finished(id);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/* This file is part of Clementine.
|
/* This file is part of Clementine.
|
||||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
Copyright 2016, John Maguire <john.maguire@gmail.com>
|
||||||
|
|
||||||
Clementine is free software: you can redistribute it and/or modify
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,38 +15,27 @@
|
||||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef ECHONESTIMAGES_H
|
#ifndef SPOTIFYIMAGES_H
|
||||||
#define ECHONESTIMAGES_H
|
#define SPOTIFYIMAGES_H
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <QMultiMap>
|
|
||||||
|
|
||||||
#include <echonest/Artist.h>
|
|
||||||
|
|
||||||
#include "songinfo/songinfoprovider.h"
|
#include "songinfo/songinfoprovider.h"
|
||||||
|
|
||||||
class NetworkAccessManager;
|
class NetworkAccessManager;
|
||||||
class QNetworkReply;
|
|
||||||
|
|
||||||
class EchoNestImages : public SongInfoProvider {
|
class SpotifyImages : public SongInfoProvider {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EchoNestImages();
|
SpotifyImages();
|
||||||
virtual ~EchoNestImages();
|
~SpotifyImages();
|
||||||
void FetchInfo(int id, const Song& metadata);
|
|
||||||
|
|
||||||
private slots:
|
void FetchInfo(int id, const Song& metadata) override;
|
||||||
void RequestFinished(QNetworkReply*, int id, Echonest::Artist artist);
|
|
||||||
void IdsFound(QNetworkReply* reply, int id);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void DoSpotifyImageRequest(const QString& id, int request_id);
|
void FetchImagesForArtist(int id, const QString& spotify_id);
|
||||||
|
|
||||||
void RegisterReply(QNetworkReply* reply, int id);
|
|
||||||
QMultiMap<int, QNetworkReply*> replies_;
|
|
||||||
std::unique_ptr<NetworkAccessManager> network_;
|
std::unique_ptr<NetworkAccessManager> network_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ECHONESTIMAGES_H
|
#endif // SPOTIFYIMAGES_H
|
Loading…
Reference in New Issue