Clementine-audio-player-Mac.../src/globalsearch/spotifysearchprovider.cpp

281 lines
8.5 KiB
C++

/* 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 "spotifysearchprovider.h"
#include <ctime>
#include <random>
#include "core/logging.h"
#include "internet/core/internetmodel.h"
#include "internet/spotify/spotifyserver.h"
#include "playlist/songmimedata.h"
#include "ui/iconloader.h"
namespace {
const int kSearchSongLimit = 5;
const int kSearchAlbumLimit = 20;
} // namespace
SpotifySearchProvider::SpotifySearchProvider(Application* app, QObject* parent)
: SearchProvider(app, parent), server_(nullptr), service_(nullptr) {
Init("Spotify", "spotify", IconLoader::Load("spotify", IconLoader::Provider),
WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote |
CanShowConfig | CanGiveSuggestions);
}
SpotifyServer* SpotifySearchProvider::server() {
if (server_) return server_;
if (!service_) service_ = InternetModel::Service<SpotifyService>();
if (service_->login_state() != SpotifyService::LoginState_LoggedIn)
return nullptr;
if (!service_->IsBlobInstalled()) return nullptr;
server_ = service_->server();
connect(server_, SIGNAL(SearchResults(cpb::spotify::SearchResponse)),
SLOT(SearchFinishedSlot(cpb::spotify::SearchResponse)));
connect(server_, SIGNAL(ImageLoaded(QString, QImage)),
SLOT(ArtLoadedSlot(QString, QImage)));
connect(server_, SIGNAL(destroyed()), SLOT(ServerDestroyed()));
connect(server_, SIGNAL(StarredLoaded(cpb::spotify::LoadPlaylistResponse)),
SLOT(SuggestionsLoaded(cpb::spotify::LoadPlaylistResponse)));
connect(server_,
SIGNAL(ToplistBrowseResults(cpb::spotify::BrowseToplistResponse)),
SLOT(SuggestionsLoaded(cpb::spotify::BrowseToplistResponse)));
return server_;
}
void SpotifySearchProvider::ServerDestroyed() { server_ = nullptr; }
void SpotifySearchProvider::SearchAsync(int id, const QString& query) {
SpotifyServer* s = server();
if (!s) {
emit SearchFinished(id);
return;
}
PendingState state;
state.orig_id_ = id;
state.tokens_ = TokenizeQuery(query);
const QString query_string = state.tokens_.join(" ");
s->Search(query_string, kSearchSongLimit, kSearchAlbumLimit);
queries_[query_string] = state;
}
void SpotifySearchProvider::SearchFinishedSlot(
const cpb::spotify::SearchResponse& response) {
QString query_string = QString::fromUtf8(response.request().query().c_str());
QMap<QString, PendingState>::iterator it = queries_.find(query_string);
if (it == queries_.end()) return;
PendingState state = it.value();
queries_.erase(it);
/* Here we clean up Spotify's results for our purposes
*
* Since Spotify doesn't give us an album artist,
* we pick one, so there's a single album artist
* per album to use for sorting.
*
* We also drop any of the single tracks returned
* if they are already represented in an album
*
* This eliminates frequent duplicates from the
* "Top Tracks" results that Spotify sometimes
* returns
*/
QMap<std::string, std::string> album_dedup;
ResultList ret;
for (int i = 0; i < response.album_size(); ++i) {
const cpb::spotify::Album& album = response.album(i);
QHash<QString, int> artist_count;
QString majority_artist;
int majority_count = 0;
/* We go through and find the artist that is
* represented most frequently in the artist
*
* For most albums this will just be one artist,
* but this ensures we have a consistent album artist for
* soundtracks, compilations, contributing artists, etc
*/
for (int j = 0; j < album.track_size(); ++j) {
// Each track can have multiple artists attributed, check them all
for (int k = 0; k < album.track(j).artist_size(); ++k) {
QString artist = QStringFromStdString(album.track(j).artist(k));
if (artist_count.contains(artist)) {
artist_count[artist]++;
} else {
artist_count[artist] = 1;
}
if (artist_count[artist] > majority_count) {
majority_count = artist_count[artist];
majority_artist = artist;
}
}
}
for (int j = 0; j < album.track_size(); ++j) {
// Insert the album/track title into the dedup map
// so we can check tracks against it below
album_dedup.insertMulti(album.track(j).album(), album.track(j).title());
Result result(this);
SpotifyService::SongFromProtobuf(album.track(j), &result.metadata_);
// Just use the album index as an id.
result.metadata_.set_album_id(i);
result.metadata_.set_albumartist(majority_artist);
ret << result;
}
}
for (int i = 0; i < response.result_size(); ++i) {
const cpb::spotify::Track& track = response.result(i);
// Check this track/album against tracks we've already seen
// in the album results, and skip if it's a duplicate
if (album_dedup.contains(track.album()) &&
album_dedup.values(track.album()).contains(track.title())) {
continue;
}
Result result(this);
SpotifyService::SongFromProtobuf(track, &result.metadata_);
ret << result;
}
emit ResultsAvailable(state.orig_id_, ret);
emit SearchFinished(state.orig_id_);
}
void SpotifySearchProvider::LoadArtAsync(int id, const Result& result) {
SpotifyServer* s = server();
if (!s) {
emit ArtLoaded(id, QImage());
return;
}
QString image_id = QUrl(result.metadata_.art_automatic()).path();
if (image_id.startsWith('/')) image_id.remove(0, 1);
pending_art_[image_id] = id;
s->LoadImage(image_id);
}
void SpotifySearchProvider::ArtLoadedSlot(const QString& id,
const QImage& image) {
QMap<QString, int>::iterator it = pending_art_.find(id);
if (it == pending_art_.end()) return;
const int orig_id = it.value();
pending_art_.erase(it);
emit ArtLoaded(orig_id, ScaleAndPad(image));
}
bool SpotifySearchProvider::IsLoggedIn() {
if (server()) {
return service_->IsLoggedIn();
}
return false;
}
void SpotifySearchProvider::ShowConfig() {
if (service_) {
return service_->ShowConfig();
}
}
void SpotifySearchProvider::AddSuggestionFromTrack(
const cpb::spotify::Track& track) {
if (!track.title().empty()) {
suggestions_.insert(QString::fromUtf8(track.title().c_str()));
}
for (int j = 0; j < track.artist_size(); ++j) {
if (!track.artist(j).empty()) {
suggestions_.insert(QString::fromUtf8(track.artist(j).c_str()));
}
}
if (!track.album().empty()) {
suggestions_.insert(QString::fromUtf8(track.album().c_str()));
}
}
void SpotifySearchProvider::AddSuggestionFromAlbum(
const cpb::spotify::Album& album) {
AddSuggestionFromTrack(album.metadata());
for (int i = 0; i < album.track_size(); ++i) {
AddSuggestionFromTrack(album.track(i));
}
}
void SpotifySearchProvider::SuggestionsLoaded(
const cpb::spotify::LoadPlaylistResponse& playlist) {
for (int i = 0; i < playlist.track_size(); ++i) {
AddSuggestionFromTrack(playlist.track(i));
}
}
void SpotifySearchProvider::SuggestionsLoaded(
const cpb::spotify::BrowseToplistResponse& response) {
for (int i = 0; i < response.track_size(); ++i) {
AddSuggestionFromTrack(response.track(i));
}
for (int i = 0; i < response.album_size(); ++i) {
AddSuggestionFromAlbum(response.album(i));
}
}
void SpotifySearchProvider::LoadSuggestions() {
if (!server()) {
return;
}
server()->LoadStarred();
server()->LoadToplist();
}
QStringList SpotifySearchProvider::GetSuggestions(int count) {
if (suggestions_.empty()) {
LoadSuggestions();
return QStringList();
}
QStringList all_suggestions = suggestions_.values();
std::mt19937 gen(std::time(0));
std::uniform_int_distribution<> random(0, all_suggestions.size() - 1);
QSet<QString> candidates;
const int max = qMin(count, all_suggestions.size());
while (candidates.size() < max) {
const int index = random(gen);
candidates.insert(all_suggestions[index]);
}
return candidates.toList();
}