From cc20d90a7a91ead8f077ec91acb3d36585b24ce3 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sat, 24 Sep 2011 17:01:18 +0100 Subject: [PATCH] Last.fm global search provider --- src/CMakeLists.txt | 2 + src/globalsearch/globalsearchitemdelegate.cpp | 7 +- src/globalsearch/globalsearchsortmodel.cpp | 1 + src/globalsearch/globalsearchwidget.cpp | 10 +- src/globalsearch/lastfmsearchprovider.cpp | 148 ++++++++++++++++++ src/globalsearch/lastfmsearchprovider.h | 56 +++++++ src/globalsearch/librarysearchprovider.cpp | 5 +- src/globalsearch/searchprovider.h | 3 +- src/globalsearch/spotifysearchprovider.cpp | 3 + src/globalsearch/tooltipresultwidget.cpp | 2 + src/internet/lastfmservice.cpp | 86 +++++++--- src/internet/lastfmservice.h | 9 ++ src/main.cpp | 1 + 13 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 src/globalsearch/lastfmsearchprovider.cpp create mode 100644 src/globalsearch/lastfmsearchprovider.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d3036ecdc..41fff6cab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -659,6 +659,7 @@ endif(ENABLE_VISUALISATIONS) if(HAVE_LIBLASTFM) list(APPEND SOURCES covers/lastfmcoverprovider.cpp + globalsearch/lastfmsearchprovider.cpp internet/fixlastfm.cpp internet/lastfmservice.cpp internet/lastfmsettingspage.cpp @@ -672,6 +673,7 @@ if(HAVE_LIBLASTFM) ) list(APPEND HEADERS covers/lastfmcoverprovider.h + globalsearch/lastfmsearchprovider.h internet/lastfmservice.h internet/lastfmsettingspage.h internet/lastfmstationdialog.h diff --git a/src/globalsearch/globalsearchitemdelegate.cpp b/src/globalsearch/globalsearchitemdelegate.cpp index 846bc9a20..d737022b5 100644 --- a/src/globalsearch/globalsearchitemdelegate.cpp +++ b/src/globalsearch/globalsearchitemdelegate.cpp @@ -91,6 +91,10 @@ void GlobalSearchItemDelegate::paint(QPainter* p, case SearchProvider::Result::Type_Track: break; + case SearchProvider::Result::Type_Stream: + count = QString::fromUtf8("∞"); + break; + case SearchProvider::Result::Type_Album: if (result.album_size_ <= 0) count = "-"; @@ -126,7 +130,8 @@ void GlobalSearchItemDelegate::paint(QPainter* p, // The text we draw depends on the type of result. switch (result.type_) { - case SearchProvider::Result::Type_Track: { + case SearchProvider::Result::Type_Track: + case SearchProvider::Result::Type_Stream: { // Title line_1 += m.title() + " "; diff --git a/src/globalsearch/globalsearchsortmodel.cpp b/src/globalsearch/globalsearchsortmodel.cpp index 88fea56d2..696a26b76 100644 --- a/src/globalsearch/globalsearchsortmodel.cpp +++ b/src/globalsearch/globalsearchsortmodel.cpp @@ -50,6 +50,7 @@ bool GlobalSearchSortModel::lessThan(const QModelIndex& left, const QModelIndex& // Then compare title, artist and album switch (r1.type_) { case SearchProvider::Result::Type_Track: + case SearchProvider::Result::Type_Stream: CompareString(title); // fallthrough case SearchProvider::Result::Type_Album: diff --git a/src/globalsearch/globalsearchwidget.cpp b/src/globalsearch/globalsearchwidget.cpp index f02d8ad83..7ec3be84c 100644 --- a/src/globalsearch/globalsearchwidget.cpp +++ b/src/globalsearch/globalsearchwidget.cpp @@ -112,8 +112,12 @@ GlobalSearchWidget::~GlobalSearchWidget() { void GlobalSearchWidget::Init(GlobalSearch* engine) { engine_ = engine; + + // These have to be queued connections because they may get emitted before + // our call to Search() (or whatever) returns and we add the ID to the map. connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)), - SLOT(AddResults(int,SearchProvider::ResultList))); + SLOT(AddResults(int,SearchProvider::ResultList)), + Qt::QueuedConnection); connect(engine_, SIGNAL(SearchFinished(int)), SLOT(SearchFinished(int)), Qt::QueuedConnection); connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)), @@ -555,6 +559,10 @@ GlobalSearchWidget::CombineAction GlobalSearchWidget::CanCombineResults( if (StringsDiffer(album) || StringsDiffer(artist)) return CannotCombine; break; + case SearchProvider::Result::Type_Stream: + if (StringsDiffer(url().toString)) + return CannotCombine; + break; } #undef StringsDiffer diff --git a/src/globalsearch/lastfmsearchprovider.cpp b/src/globalsearch/lastfmsearchprovider.cpp new file mode 100644 index 000000000..fac71a84f --- /dev/null +++ b/src/globalsearch/lastfmsearchprovider.cpp @@ -0,0 +1,148 @@ +/* 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 "lastfmsearchprovider.h" +#include "core/logging.h" +#include "internet/lastfmservice.h" +#include "playlist/songmimedata.h" + +const int LastFMSearchProvider::kResultLimit = 6; + + +LastFMSearchProvider::LastFMSearchProvider(LastFMService* service, QObject* parent) + : BlockingSearchProvider(parent), + service_(service) { + Init("Last.fm", "lastfm", QIcon(":last.fm/as.png"), false, true); + icon_ = ScaleAndPad(QImage(":last.fm/as.png")); + + connect(service, SIGNAL(SavedItemsChanged()), SLOT(RecreateItems())); + RecreateItems(); +} + +SearchProvider::ResultList LastFMSearchProvider::Search(int id, const QString& query) { + ResultList ret; + const QStringList tokens = TokenizeQuery(query); + + QMutexLocker l(&items_mutex_); + foreach (const Item& item, items_) { + Result result(this); + result.type_ = Result::Type_Stream; + result.match_quality_ = Result::Quality_None; + + foreach (const QString& token, tokens) { + if (item.keyword_.startsWith(token, Qt::CaseInsensitive)) { + result.match_quality_ = Result::Quality_AtStart; + result.metadata_ = item.metadata_; + break; // Next item + } + + Result::MatchQuality quality = MatchQuality(tokens, item.metadata_.title()); + if (quality == Result::Quality_None) + continue; + + result.match_quality_ = qMin(quality, result.match_quality_); + result.metadata_ = item.metadata_; + } + + if (result.match_quality_ != Result::Quality_None) { + ret << result; + } + + if (ret.count() >= kResultLimit) + break; + } + + return ret; +} + +void LastFMSearchProvider::LoadArtAsync(int id, const Result& result) { + // TODO: Maybe we should try to get user pictures for friends? + + emit ArtLoaded(id, icon_); +} + +void LastFMSearchProvider::LoadTracksAsync(int id, const Result& result) { + Song metadata = result.metadata_; + metadata.set_filetype(Song::Type_Stream); + + SongMimeData* mime_data = new SongMimeData; + mime_data->songs = SongList() << metadata; + + emit TracksLoaded(id, mime_data); +} + +void LastFMSearchProvider::RecreateItems() { + QList items; + Item item; + + item.keyword_ = "recommended"; + item.metadata_.set_title(tr("My Last.fm Recommended Radio")); + item.metadata_.set_url(QUrl("lastfm://user/USERNAME/recommended")); + items << item; + + item.keyword_ = "radio"; + item.metadata_.set_title(tr("My Last.fm Library")); + item.metadata_.set_url(QUrl("lastfm://user/USERNAME/library")); + items << item; + + item.keyword_ = "mix"; + item.metadata_.set_title(tr("My Last.fm Mix Radio")); + item.metadata_.set_url(QUrl("lastfm://user/USERNAME/mix")); + items << item; + + item.keyword_ = "neighborhood"; + item.metadata_.set_title(tr("My Last.fm Neighborhood")); + item.metadata_.set_url(QUrl("lastfm://user/USERNAME/neighbours")); + items << item; + + const QStringList artists = service_->SavedArtistRadioNames(); + const QStringList tags = service_->SavedTagRadioNames(); + const QStringList friends = service_->FriendNames(); + + foreach (const QString& name, artists) { + item.keyword_ = name; + item.metadata_.set_title(tr(LastFMService::kTitleArtist).arg(name)); + item.metadata_.set_url(QUrl(QString(LastFMService::kUrlArtist).arg(name))); + items << item; + } + + foreach (const QString& name, tags) { + item.keyword_ = name; + item.metadata_.set_title(tr(LastFMService::kTitleTag).arg(name)); + item.metadata_.set_url(QUrl(QString(LastFMService::kUrlTag).arg(name))); + items << item; + } + + foreach (const QString& name, friends) { + item.keyword_ = name; + + item.metadata_.set_title(tr("Last.fm Radio Station - %1").arg(name)); + item.metadata_.set_url(QUrl("lastfm://user/" + name + "/library")); + items << item; + + item.metadata_.set_title(tr("Last.fm Mix Radio - %1").arg(name)); + item.metadata_.set_url(QUrl("lastfm://user/" + name + "/mix")); + items << item; + + item.metadata_.set_title(tr("Last.fm Neighbor Radio - %1").arg(name)); + item.metadata_.set_url(QUrl("lastfm://user/" + name + "/neighbours")); + items << item; + } + + QMutexLocker l(&items_mutex_); + items_ = items; +} diff --git a/src/globalsearch/lastfmsearchprovider.h b/src/globalsearch/lastfmsearchprovider.h new file mode 100644 index 000000000..6027f4e95 --- /dev/null +++ b/src/globalsearch/lastfmsearchprovider.h @@ -0,0 +1,56 @@ +/* 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 . +*/ + +#ifndef LASTFMSEARCHPROVIDER_H +#define LASTFMSEARCHPROVIDER_H + +#include "searchprovider.h" + +class LastFMService; + +class LastFMSearchProvider : public BlockingSearchProvider { + Q_OBJECT + +public: + LastFMSearchProvider(LastFMService* service, QObject* parent); + + static const int kResultLimit; + + void LoadArtAsync(int id, const Result& result); + void LoadTracksAsync(int id, const Result& result); + +protected: + ResultList Search(int id, const QString& query); + +private slots: + void RecreateItems(); + +private: + LastFMService* service_; + + QImage icon_; + + struct Item { + QString keyword_; + Song metadata_; + }; + + QMutex items_mutex_; + QList items_; +}; + +#endif // LASTFMSEARCHPROVIDER_H diff --git a/src/globalsearch/librarysearchprovider.cpp b/src/globalsearch/librarysearchprovider.cpp index e01ac2a4e..770ff4539 100644 --- a/src/globalsearch/librarysearchprovider.cpp +++ b/src/globalsearch/librarysearchprovider.cpp @@ -136,7 +136,7 @@ void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) { ret << result.metadata_; break; - case Result::Type_Album: { + case Result::Type_Album: { // Find all the songs in this album. LibraryQuery query; query.SetColumnSpec("ROWID, " + Song::kColumnSpec); @@ -156,6 +156,9 @@ void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) { ret << song; } } + + default: + break; } SortSongs(&ret); diff --git a/src/globalsearch/searchprovider.h b/src/globalsearch/searchprovider.h index 3d0ec838a..0d5912965 100644 --- a/src/globalsearch/searchprovider.h +++ b/src/globalsearch/searchprovider.h @@ -42,6 +42,7 @@ public: // The order of types here is the order they'll appear in the UI. enum Type { Type_Track = 0, + Type_Stream, Type_Album }; @@ -113,7 +114,7 @@ protected: // Sorts a list of songs by disc, then by track. static void SortSongs(SongList* list); - // Subclasses must call this from their constructor + // Subclasses must call this from their constructors. void Init(const QString& name, const QString& id, const QIcon& icon, bool delay_searches, bool serialised_art); diff --git a/src/globalsearch/spotifysearchprovider.cpp b/src/globalsearch/spotifysearchprovider.cpp index 0ad47bb4f..814ab6599 100644 --- a/src/globalsearch/spotifysearchprovider.cpp +++ b/src/globalsearch/spotifysearchprovider.cpp @@ -166,6 +166,9 @@ void SpotifySearchProvider::LoadTracksAsync(int id, const Result& result) { s->AlbumBrowse(uri); break; } + + default: + break; } } diff --git a/src/globalsearch/tooltipresultwidget.cpp b/src/globalsearch/tooltipresultwidget.cpp index 71743c121..c5124b0ad 100644 --- a/src/globalsearch/tooltipresultwidget.cpp +++ b/src/globalsearch/tooltipresultwidget.cpp @@ -59,6 +59,7 @@ QSize TooltipResultWidget::CalculateSizeHint() const { switch (result_.type_) { case SearchProvider::Result::Type_Track: + case SearchProvider::Result::Type_Stream: break; case SearchProvider::Result::Type_Album: @@ -119,6 +120,7 @@ void TooltipResultWidget::paintEvent(QPaintEvent*) { switch (result_.type_) { case SearchProvider::Result::Type_Track: + case SearchProvider::Result::Type_Stream: break; case SearchProvider::Result::Type_Album: diff --git a/src/internet/lastfmservice.cpp b/src/internet/lastfmservice.cpp index 1ce54d9a4..c8ca814aa 100644 --- a/src/internet/lastfmservice.cpp +++ b/src/internet/lastfmservice.cpp @@ -20,6 +20,8 @@ #include "lastfmurlhandler.h" #include "internetmodel.h" #include "internetplaylistitem.h" +#include "globalsearch/globalsearch.h" +#include "globalsearch/lastfmsearchprovider.h" #include "core/logging.h" #include "core/player.h" #include "core/song.h" @@ -79,6 +81,7 @@ LastFMService::LastFMService(InternetModel* parent) initial_tune_(false), tune_task_id_(0), scrobbling_enabled_(false), + root_item_(NULL), artist_list_(NULL), tag_list_(NULL), custom_list_(NULL), @@ -111,6 +114,8 @@ LastFMService::LastFMService(InternetModel* parent) model()->player()->RegisterUrlHandler(url_handler_); model()->cover_providers()->AddProvider(new LastFmCoverProvider(this)); + + model()->global_search()->AddProvider(new LastFMSearchProvider(this, this)); } LastFMService::~LastFMService() { @@ -126,8 +131,8 @@ void LastFMService::ReloadSettings() { buttons_visible_ = settings.value("ShowLoveBanButtons", true).toBool(); scrobble_button_visible_ = settings.value("ShowScrobbleButton", true).toBool(); - friend_names_ = settings.value("FriendNames").toStringList(); last_refreshed_friends_ = settings.value("LastRefreshedFriends").toDateTime(); + friend_names_ = settings.value("FriendNames").toStringList(); //avoid emitting signal if it's not changed if(scrobbling_enabled_old != scrobbling_enabled_) @@ -151,9 +156,9 @@ bool LastFMService::IsSubscriber() const { } QStandardItem* LastFMService::CreateRootItem() { - QStandardItem* item = new QStandardItem(QIcon(":last.fm/as.png"), kServiceName); - item->setData(true, InternetModel::Role_CanLazyLoad); - return item; + root_item_ = new QStandardItem(QIcon(":last.fm/as.png"), kServiceName); + root_item_->setData(true, InternetModel::Role_CanLazyLoad); + return root_item_; } void LastFMService::LazyPopulate(QStandardItem* parent) { @@ -281,6 +286,7 @@ void LastFMService::SignOut() { QSettings settings; settings.beginGroup(kSettingsGroup); + settings.setValue("Username", QString()); settings.setValue("Session", QString()); settings.setValue("FriendNames", friend_names_); @@ -589,13 +595,55 @@ bool LastFMService::IsFriendsListStale() const { kFriendsCacheDurationSecs; } +QStringList LastFMService::FriendNames() { + // Update the list for next time, in the main thread. + if (IsFriendsListStale()) + metaObject()->invokeMethod(this, "RefreshFriends", Qt::QueuedConnection); + + QSettings s; + s.beginGroup(LastFMService::kSettingsGroup); + return s.value("FriendNames").toStringList(); +} + +static QStringList SavedArtistOrTagRadioNames(const QString& name) { + QStringList ret; + + QSettings s; + s.beginGroup(LastFMService::kSettingsGroup); + int count = s.beginReadArray(name); + for (int i=0 ; isetData(false, InternetModel::Role_CanLazyLoad); + LazyPopulate(root_item_); + } if (!force && !IsFriendsListStale()) { PopulateFriendsList(); @@ -635,6 +683,7 @@ void LastFMService::RefreshFriendsFinished() { } last_refreshed_friends_ = QDateTime::currentDateTime(); + friend_names_ = QStringList(); foreach (const lastfm::User& f, friends) { friend_names_ << f.name(); @@ -646,6 +695,8 @@ void LastFMService::RefreshFriendsFinished() { s.setValue("LastRefreshedFriends", last_refreshed_friends_); PopulateFriendsList(); + + emit SavedItemsChanged(); } void LastFMService::PopulateFriendsList() { @@ -756,6 +807,8 @@ void LastFMService::AddArtistOrTag(const QString& name, emit AddItemToPlaylist(item->index(), AddMode_Append); SaveList(name, list); + + emit SavedItemsChanged(); } void LastFMService::SaveList(const QString& name, QStandardItem* list) const { @@ -774,35 +827,30 @@ void LastFMService::RestoreList(const QString& name, const QString& url_pattern, const QString& title_pattern, const QIcon& icon, QStandardItem* parent) { - QSettings settings; - settings.beginGroup(kSettingsGroup); - if (parent->hasChildren()) parent->removeRows(0, parent->rowCount()); - int count = settings.beginReadArray(name); - for (int i=0 ; isetData(QVariant::fromValue(song), InternetModel::Role_SongMetadata); item->setData(InternetModel::PlayBehaviour_SingleItem, InternetModel::Role_PlayBehaviour); parent->appendRow(item); } - settings.endArray(); } void LastFMService::Remove() { diff --git a/src/internet/lastfmservice.h b/src/internet/lastfmservice.h index 0483a03b2..6ca061a36 100644 --- a/src/internet/lastfmservice.h +++ b/src/internet/lastfmservice.h @@ -113,6 +113,11 @@ class LastFMService : public InternetService { bool IsFriendsListStale() const; + // Thread safe + QStringList FriendNames(); + QStringList SavedArtistRadioNames() const; + QStringList SavedTagRadioNames() const; + public slots: void NowPlaying(const Song& song); void Scrobble(); @@ -130,6 +135,8 @@ class LastFMService : public InternetService { void UpdatedSubscriberStatus(bool is_subscriber); void ScrobbledRadioStream(); + void SavedItemsChanged(); + protected: QModelIndex GetCurrentIndex(); @@ -146,6 +153,7 @@ class LastFMService : public InternetService { void AddTagRadio(); void AddCustomRadio(); void ForceRefreshFriends(); + void RefreshFriends(); void Remove(); // Radio tuner. @@ -208,6 +216,7 @@ class LastFMService : public InternetService { bool buttons_visible_; bool scrobble_button_visible_; + QStandardItem* root_item_; QStandardItem* artist_list_; QStandardItem* tag_list_; QStandardItem* custom_list_; diff --git a/src/main.cpp b/src/main.cpp index eafecae85..baaeda10e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -220,6 +220,7 @@ int main(int argc, char *argv[]) { qRegisterMetaTypeStreamOperators >("ColumnAlignmentMap"); qRegisterMetaType("QNetworkCookie"); qRegisterMetaType >("QList"); + qRegisterMetaType("SearchProvider::ResultList"); qRegisterMetaType("GstBuffer*"); qRegisterMetaType("GstElement*");