/* This file is part of Clementine. Copyright 2010, David Sansome 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 . */ #include "spotifysearchprovider.h" #include #include #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(); 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::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 album_dedup; ResultList ret; for (int i = 0; i < response.album_size(); ++i) { const cpb::spotify::Album& album = response.album(i); QHash 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::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 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(); }