diff --git a/3rdparty/libprojectm/CMakeLists.txt b/3rdparty/libprojectm/CMakeLists.txt index 98d7b4468..f543f0041 100644 --- a/3rdparty/libprojectm/CMakeLists.txt +++ b/3rdparty/libprojectm/CMakeLists.txt @@ -5,6 +5,7 @@ SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "The projectM core library.") include(CheckCXXCompilerFlag) cmake_policy(SET CMP0005 OLD) +cmake_policy(SET CMP0017 OLD) set(USE_DEVIL OFF) diff --git a/3rdparty/qsqlite/CMakeLists.txt b/3rdparty/qsqlite/CMakeLists.txt index d391b6602..d6d174042 100644 --- a/3rdparty/qsqlite/CMakeLists.txt +++ b/3rdparty/qsqlite/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 2.6) +add_definitions(-DQT_STATICPLUGIN) + # Source files set(SQLITE-SOURCES qsql_sqlite.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e1ffbca1d..1f8c7676b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,7 +279,6 @@ endif(ENABLE_REMOTE AND HAVE_GNUTLS AND HAVE_PJSIP) set(HAVE_STATIC_SQLITE ${STATIC_SQLITE}) if(STATIC_SQLITE) message(STATUS "Building static qsqlite plugin") - add_definitions(-DQT_STATICPLUGIN) add_subdirectory(3rdparty/qsqlite) include_directories("3rdparty/qsqlite") endif(STATIC_SQLITE) @@ -381,6 +380,13 @@ if(HAVE_SPOTIFY_BLOB) add_subdirectory(spotifyblob/blob) endif(HAVE_SPOTIFY_BLOB) +# This goes after everything else because KDE fucks everything else up with its +# cmake includes. +find_package(KDE4 4.3.60) +if(KDE4_PLASMA_LIBS) + add_subdirectory(plasmarunner) +endif(KDE4_PLASMA_LIBS) + # Uninstall support configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" diff --git a/data/clementineplasmarunner.qrc b/data/clementineplasmarunner.qrc new file mode 100644 index 000000000..a4ff71d9c --- /dev/null +++ b/data/clementineplasmarunner.qrc @@ -0,0 +1,5 @@ + + + nocover.png + + diff --git a/plasmarunner/CMakeLists.txt b/plasmarunner/CMakeLists.txt new file mode 100644 index 000000000..a0f993328 --- /dev/null +++ b/plasmarunner/CMakeLists.txt @@ -0,0 +1,33 @@ +include(KDE4Defaults) +include(MacroLibrary) + +add_definitions(${KDE4_DEFINITIONS}) +include_directories(${CMAKE_SOURCE_DIR}) +include_directories(${CMAKE_BINARY_DIR}/src) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${KDE4_INCLUDES}) + +set(SOURCES + clementinerunner.cpp + ../src/globalsearch/common.cpp +) + +set(RESOURCES + ../data/clementineplasmarunner.qrc +) + +list(APPEND QT_DBUSXML2CPP_EXECUTABLE -i src/globalsearch/common.h) + +qt4_add_dbus_interface(SOURCES + ${CMAKE_SOURCE_DIR}/src/dbus/org.clementineplayer.GlobalSearch.xml + globalsearchinterface) +qt4_add_resources(QRC ${RESOURCES}) +kde4_add_plugin(plasma_runner_clementine ${SOURCES} ${QRC}) + +target_link_libraries(plasma_runner_clementine ${KDE4_PLASMA_LIBS}) + +# Install the library and .desktop file +install(TARGETS plasma_runner_clementine DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES plasma-runner-clementine.desktop DESTINATION ${SERVICES_INSTALL_DIR}) + diff --git a/plasmarunner/clementinerunner.cpp b/plasmarunner/clementinerunner.cpp new file mode 100644 index 000000000..30f085e24 --- /dev/null +++ b/plasmarunner/clementinerunner.cpp @@ -0,0 +1,295 @@ +/* This file is part of Clementine. + Copyright 2011, 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 "clementinerunner.h" +#include "globalsearchinterface.h" + +#include +#include +#include + +#include + +const char* ClementineRunner::kDbusService = "org.mpris.clementine"; +const char* ClementineRunner::kDbusPath = "/GlobalSearch"; + + +ClementineRunner::ClementineRunner(QObject* parent, const QVariantList& args) + : Plasma::AbstractRunner(parent, args), + interface_(NULL) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + Q_INIT_RESOURCE(clementineplasmarunner); + + nocover_ = QIcon(":/clementineplasmarunner/nocover.png"); + + setIgnoredTypes(Plasma::RunnerContext::NetworkLocation | + Plasma::RunnerContext::FileSystem); + setSpeed(SlowSpeed); + setPriority(LowPriority); + setHasRunOptions(true); + + Plasma::RunnerSyntax syntax = Plasma::RunnerSyntax("", tr("Search music")); + syntax.addExampleQuery("foo fighters"); + addSyntax(syntax); + + QDBusConnection bus = QDBusConnection::sessionBus(); + bus.connect( + kDbusService, kDbusPath, + OrgClementineplayerGlobalSearchInterface::staticInterfaceName(), + "SearchFinished", "i", this, SLOT(SearchFinished(int))); + bus.connect( + kDbusService, kDbusPath, + OrgClementineplayerGlobalSearchInterface::staticInterfaceName(), + "ResultsAvailable", "ia(ibsiiissssbi)", + this, SLOT(ResultsAvailable(int,GlobalSearchServiceResultList))); + bus.connect( + kDbusService, kDbusPath, + OrgClementineplayerGlobalSearchInterface::staticInterfaceName(), + "ArtLoaded", "iay", this, SLOT(ArtLoaded(int,QByteArray))); +} + +void ClementineRunner::match(Plasma::RunnerContext& context) { + + if (context.query().length() < 3) + return; + + if (!interface_) { + interface_ = new OrgClementineplayerGlobalSearchInterface( + kDbusService, kDbusPath, QDBusConnection::sessionBus()); + } + + int id = 0; + PendingQuery query; + query.results_mutex_.lock(); + + // Start the search + { + QMutexLocker l(&pending_mutex_); + QDBusPendingReply reply = interface_->StartSearch(context.query(), true); + reply.waitForFinished(); + + if (!reply.isValid()) { + qDebug() << reply.error(); + return; + } + id = reply.value(); + + pending_queries_[id] = &query; + } + + query.results_mutex_.unlock(); + + // Wait for results to arrive + forever { + query.results_semaphore_.acquire(); + + // If the user closed the query then stop early. + if (!context.isValid()) + break; + + // Take the next result on the list + GlobalSearchServiceResult result; + { + QMutexLocker l(&query.results_mutex_); + if (query.results_.isEmpty() && query.finished_) + break; + + result = query.results_.takeFirst(); + } + + // Create a match for the result. + Plasma::QueryMatch match(this); + FillMatch(result, &match); + + // Emit the match + context.addMatch(QString(), match); + } + + // Remove the pending search + { + QMutexLocker l(&pending_mutex_); + pending_queries_.remove(id); + } +} + +void ClementineRunner::FillMatch(const GlobalSearchServiceResult& result, + Plasma::QueryMatch* match) const { + QString line[2]; + + switch (result.type_) { + case globalsearch::Type_Track: + case globalsearch::Type_Stream: { + line[0] = result.title_; + + if (!result.artist_.isEmpty()) { + line[1] += result.artist_; + } else if (!result.album_artist_.isEmpty()) { + line[1] += result.album_artist_; + } + + if (!result.album_.isEmpty()) { + line[1] += " - " + result.album_; + } + + if (result.track_ > 0) { + line[1] += " - " + tr("track %1").arg(result.track_); + } + + break; + } + + case globalsearch::Type_Album: { + if (!result.album_artist_.isEmpty()) + line[0] += result.album_artist_; + else if (result.is_compilation_) + line[0] += tr("Various Artists"); + else if (!result.artist_.isEmpty()) + line[0] += result.artist_; + else + line[0] += tr("Unknown"); + + // Dash + line[0] += " - "; + + // Album + if (result.album_.isEmpty()) + line[0] += tr("Unknown"); + else + line[0] += result.album_; + + if (result.album_size_ > 1) + line[1] = tr("Album with %1 tracks").arg(result.album_size_); + + break; + } + } + + match->setType(Plasma::QueryMatch::CompletionMatch); + match->setText(line[0]); + match->setSubtext(line[1]); + match->setRelevance(ResultRelevance(result)); + + if (!result.image_.isNull()) { + match->setIcon(result.image_); + } else { + match->setIcon(nocover_); + } +} + +void ClementineRunner::run(const Plasma::RunnerContext& context, const Plasma::QueryMatch& match) { + // TODO + qDebug() << __PRETTY_FUNCTION__; +} + +qreal ClementineRunner::ResultRelevance(const GlobalSearchServiceResult& result) const { + // Plasma sorts results by a qreal that we provide. We have to do some dumb + // maths to create a qreal for the result such that it will produce the same + // sorting as you'd get in clementine. + + // Clementine sorts by: + // provider + // match quality + // type + // title (for tracks and streams) + // artist/album (for albums) + + qreal ret = + 10000 * qreal(result.provider_order_) / 10.0 + + 100 * qreal(globalsearch::Quality_None + 1 - result.match_quality_) + + 10 * qreal(globalsearch::Type_Album + 1 - result.type_); + + return ret; +} + +void ClementineRunner::ResultsAvailable(int id, GlobalSearchServiceResultList results) { + // Lock the mutex and add the results to the list. + QMutexLocker l(&pending_mutex_); + PendingMap::iterator it = pending_queries_.find(id); + if (it == pending_queries_.end()) + return; + + (*it)->provider_order_ ++; + + QMutexLocker result_l(&(*it)->results_mutex_); + foreach (GlobalSearchServiceResult result, results) { + result.provider_order_ = (*it)->provider_order_; + + if (result.art_on_the_way_) { + // Brace yourselves, art is coming. + (*it)->results_waiting_for_art_ << result; + } else { + (*it)->results_.append(results); + (*it)->results_semaphore_.release(); + } + } +} + +void ClementineRunner::SearchFinished(int id) { + QMutexLocker l(&pending_mutex_); + PendingMap::iterator it = pending_queries_.find(id); + if (it == pending_queries_.end()) + return; + + (*it)->finished_signal_emitted_ = true; + + if ((*it)->results_waiting_for_art_.isEmpty()) { + QMutexLocker result_l(&(*it)->results_mutex_); + (*it)->finished_ = true; + (*it)->results_semaphore_.release(); + } +} + +void ClementineRunner::ArtLoaded(int result_id, const QByteArray& image_data) { + QMutexLocker l(&pending_mutex_); + + // Find a query that has a result with this ID waiting for art + foreach (PendingQuery* query, pending_queries_.values()) { + GlobalSearchServiceResultList::iterator it; + for (it = query->results_waiting_for_art_.begin() ; + it != query->results_waiting_for_art_.end() ; ++it) { + if (it->result_id_ == result_id) { + break; + } + } + + if (it == query->results_waiting_for_art_.end()) + continue; + + // The API is stupid so we have to create the QIcon here on the GUI thread. + if (!image_data.isEmpty()) { + QImage image; + if (image.loadFromData(image_data)) { + it->image_ = QIcon(QPixmap::fromImage(image)); + } + } + + // Add the art to this result and remote it from the waiting list + QMutexLocker result_l(&query->results_mutex_); + query->results_ << *it; + query->results_waiting_for_art_.erase(it); + query->finished_ = + query->finished_signal_emitted_ && + query->results_waiting_for_art_.isEmpty(); + query->results_semaphore_.release(query->finished_ ? 2 : 1); + break; + } +} + +K_EXPORT_PLASMA_RUNNER(clementine, ClementineRunner) diff --git a/plasmarunner/clementinerunner.h b/plasmarunner/clementinerunner.h new file mode 100644 index 000000000..0775cbbb8 --- /dev/null +++ b/plasmarunner/clementinerunner.h @@ -0,0 +1,83 @@ +/* This file is part of Clementine. + Copyright 2011, 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 CLEMENTINERUNNER_H +#define CLEMENTINERUNNER_H + +#include "src/globalsearch/common.h" + +#include + +#include + +class OrgClementineplayerGlobalSearchInterface; + +class ClementineRunner : public Plasma::AbstractRunner { + Q_OBJECT + +public: + ClementineRunner(QObject* parent, const QVariantList& args); + + static const char* kDbusService; + static const char* kDbusPath; + + void match(Plasma::RunnerContext& context); + void run(const Plasma::RunnerContext& context, const Plasma::QueryMatch& match); + +private slots: + void ResultsAvailable(int id, GlobalSearchServiceResultList results); + void SearchFinished(int id); + void ArtLoaded(int result_id, const QByteArray& image_data); + +private: + struct PendingQuery { + PendingQuery() + : provider_order_(0), + finished_signal_emitted_(false), + finished_(false) + {} + + // The main thread is the only one to access these variables. + int provider_order_; + GlobalSearchServiceResultList results_waiting_for_art_; + bool finished_signal_emitted_; + + // This list contains results that are finished and waiting to be processed + // by the match() thread. results_mutex_ locks results_ and + // results_semaphore_ is released once for each result. + GlobalSearchServiceResultList results_; + QMutex results_mutex_; + QSemaphore results_semaphore_; + bool finished_; + }; + typedef QMap PendingMap; + + qreal ResultRelevance(const GlobalSearchServiceResult& result) const; + void FillMatch(const GlobalSearchServiceResult& result, + Plasma::QueryMatch* match) const; + +private: + OrgClementineplayerGlobalSearchInterface* interface_; + + QIcon nocover_; + + // pending_mutex_ locks any access to the PendingMap. + QMutex pending_mutex_; + PendingMap pending_queries_; +}; + +#endif // CLEMENTINERUNNER_H diff --git a/plasmarunner/plasma-runner-clementine.desktop b/plasmarunner/plasma-runner-clementine.desktop new file mode 100644 index 000000000..eddf71016 --- /dev/null +++ b/plasmarunner/plasma-runner-clementine.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +Name=Clementine +Icon=clementine +Comment=Clementine global music search +Type=Service +ServiceTypes=Plasma/Runner + +X-KDE-Library=plasma_runner_clementine +X-KDE-PluginInfo-Author=David Sansome +X-KDE-PluginInfo-Email=me@davidsansome.com +X-KDE-PluginInfo-Name=clementine +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=http://www.clementine-player.org/ +X-KDE-PluginInfo-Category=Audio +X-KDE-PluginInfo-Depends= +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7c4416ba2..4b12d41fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -116,6 +116,7 @@ set(SOURCES engines/gstenginepipeline.cpp engines/gstelementdeleter.cpp + globalsearch/common.cpp globalsearch/digitallyimportedsearchprovider.cpp globalsearch/globalsearch.cpp globalsearch/globalsearchitemdelegate.cpp @@ -779,7 +780,6 @@ if(HAVE_DBUS) dbus/org.gnome.SettingsDaemon.MediaKeys.xml dbus/gnomesettingsdaemon) - # DeviceKit DBUS interfaces if(HAVE_DEVICEKIT) qt4_add_dbus_interface(SOURCES @@ -790,6 +790,16 @@ if(HAVE_DBUS) dbus/udisksdevice) endif(HAVE_DEVICEKIT) + # Global search interface + qt4_add_dbus_adaptor(SOURCES + dbus/org.clementineplayer.GlobalSearch.xml + globalsearch/globalsearchservice.h GlobalSearchService + globalsearch/globalsearchadaptor) + + # Global search source + list(APPEND SOURCES globalsearch/globalsearchservice.cpp) + list(APPEND HEADERS globalsearch/globalsearchservice.h) + # MPRIS source list(APPEND SOURCES core/mpris.cpp core/mpris1.cpp core/mpris2.cpp) list(APPEND HEADERS core/mpris.h core/mpris1.h core/mpris2.h) diff --git a/src/core/mpris2.cpp b/src/core/mpris2.cpp index 6154de3e9..c36d06641 100644 --- a/src/core/mpris2.cpp +++ b/src/core/mpris2.cpp @@ -305,25 +305,11 @@ void Mpris2::CurrentSongChanged(const Song& song) { // ... and we add the cover information later, when it's available. void Mpris2::ArtLoaded(const Song& song, const QString& art_uri) { last_metadata_ = QVariantMap(); + song.ToXesam(&last_metadata_); using mpris::AddMetadata; AddMetadata("mpris:trackid", current_track_id(), &last_metadata_); - AddMetadata("xesam:url", song.url().toString(), &last_metadata_); - AddMetadata("xesam:title", song.PrettyTitle(), &last_metadata_); - AddMetadataAsList("xesam:artist", song.artist(), &last_metadata_); - AddMetadata("xesam:album", song.album(), &last_metadata_); - AddMetadataAsList("xesam:albumArtist", song.albumartist(), &last_metadata_); - AddMetadata("mpris:length", song.length_nanosec() / kNsecPerUsec, &last_metadata_); - AddMetadata("xesam:trackNumber", song.track(), &last_metadata_); - AddMetadataAsList("xesam:genre", song.genre(), &last_metadata_); - AddMetadata("xesam:discNumber", song.disc(), &last_metadata_); - AddMetadataAsList("xesam:comment", song.comment(), &last_metadata_); - AddMetadata("xesam:contentCreated", AsMPRISDateTimeType(song.ctime()), &last_metadata_); - AddMetadata("xesam:lastUsed", AsMPRISDateTimeType(song.lastplayed()), &last_metadata_); - AddMetadata("xesam:audioBPM", song.bpm(), &last_metadata_); - AddMetadataAsList("xesam:composer", song.composer(), &last_metadata_); - AddMetadata("xesam:useCount", song.playcount(), &last_metadata_); - AddMetadata("xesam:autoRating", song.score(), &last_metadata_); + if (song.rating() != -1.0) { AddMetadata("rating", song.rating() * 5, &last_metadata_); } diff --git a/src/core/mpris2.h b/src/core/mpris2.h index 445db8b2d..3d7f33e36 100644 --- a/src/core/mpris2.h +++ b/src/core/mpris2.h @@ -175,10 +175,6 @@ private: Mpris1* mpris1_; }; -inline QString AsMPRISDateTimeType(uint time) { - return time != -1 ? QDateTime::fromTime_t(time).toString(Qt::ISODate) : ""; -} - } // namespace mpris #endif diff --git a/src/core/mpris_common.h b/src/core/mpris_common.h index 361aaff65..218c3e3fc 100644 --- a/src/core/mpris_common.h +++ b/src/core/mpris_common.h @@ -41,6 +41,10 @@ inline void AddMetadata(const QString& key, const QDateTime& metadata, QVariantM if (metadata.isValid()) (*map)[key] = metadata; } +inline QString AsMPRISDateTimeType(uint time) { + return time != -1 ? QDateTime::fromTime_t(time).toString(Qt::ISODate) : ""; +} + } // namespace mpris #endif // MPRIS_COMMON_H diff --git a/src/core/song.cpp b/src/core/song.cpp index 993343d77..2e1f58628 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -18,6 +18,7 @@ #include "fmpsparser.h" #include "song.h" #include "core/logging.h" +#include "core/mpris_common.h" #include @@ -1357,3 +1358,29 @@ bool Song::IsOnSameAlbum(const Song& other) const { return album() == other.album() && artist() == other.artist(); } + +void Song::ToXesam(QVariantMap* map) const { + using mpris::AddMetadata; + using mpris::AddMetadataAsList; + using mpris::AsMPRISDateTimeType; + + AddMetadata("xesam:url", url().toString(), map); + AddMetadata("xesam:title", PrettyTitle(), map); + AddMetadataAsList("xesam:artist", artist(), map); + AddMetadata("xesam:album", album(), map); + AddMetadataAsList("xesam:albumArtist", albumartist(), map); + AddMetadata("mpris:length", length_nanosec() / kNsecPerUsec, map); + AddMetadata("xesam:trackNumber", track(), map); + AddMetadataAsList("xesam:genre", genre(), map); + AddMetadata("xesam:discNumber", disc(), map); + AddMetadataAsList("xesam:comment", comment(), map); + AddMetadata("xesam:contentCreated", AsMPRISDateTimeType(ctime()), map); + AddMetadata("xesam:lastUsed", AsMPRISDateTimeType(lastplayed()), map); + AddMetadata("xesam:audioBPM", bpm(), map); + AddMetadataAsList("xesam:composer", composer(), map); + AddMetadata("xesam:useCount", playcount(), map); + AddMetadata("xesam:autoRating", score(), map); + if (rating() != -1.0) { + AddMetadata("xesam:userRating", rating(), map); + } +} diff --git a/src/core/song.h b/src/core/song.h index 0374b0e42..ee3396261 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -63,6 +64,7 @@ namespace TagLib { } } + class FileRefFactory { public: virtual ~FileRefFactory() {} @@ -161,6 +163,7 @@ class Song { #ifdef HAVE_LIBLASTFM void ToLastFM(lastfm::Track* track) const; #endif + void ToXesam(QVariantMap* map) const; // Simple accessors bool is_valid() const { return d->valid_; } diff --git a/src/dbus/org.clementineplayer.GlobalSearch.xml b/src/dbus/org.clementineplayer.GlobalSearch.xml new file mode 100644 index 000000000..13fb04241 --- /dev/null +++ b/src/dbus/org.clementineplayer.GlobalSearch.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/globalsearch/common.cpp b/src/globalsearch/common.cpp new file mode 100644 index 000000000..df7711d50 --- /dev/null +++ b/src/globalsearch/common.cpp @@ -0,0 +1,66 @@ +/* This file is part of Clementine. + Copyright 2011, 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 "common.h" + +#ifdef HAVE_DBUS + +QDBusArgument& operator <<(QDBusArgument& arg, const GlobalSearchServiceResult& result) { + arg.beginStructure(); + arg << result.result_id_ + << result.art_on_the_way_ + << result.provider_name_ + << result.type_ + << result.match_quality_ + << result.album_size_ + << result.title_ + << result.artist_ + << result.album_ + << result.album_artist_ + << result.is_compilation_ + << result.track_; + arg.endStructure(); + + return arg; +} + +const QDBusArgument& operator >>(const QDBusArgument& arg, GlobalSearchServiceResult& result) { + int type; + int match_quality; + + arg.beginStructure(); + arg >> result.result_id_ + >> result.art_on_the_way_ + >> result.provider_name_ + >> type + >> match_quality + >> result.album_size_ + >> result.title_ + >> result.artist_ + >> result.album_ + >> result.album_artist_ + >> result.is_compilation_ + >> result.track_; + arg.endStructure(); + + result.type_ = static_cast(type); + result.match_quality_ = static_cast(match_quality); + + return arg; +} + +#endif // HAVE_DBUS diff --git a/src/globalsearch/common.h b/src/globalsearch/common.h new file mode 100644 index 000000000..ce9d2fdaa --- /dev/null +++ b/src/globalsearch/common.h @@ -0,0 +1,92 @@ +/* This file is part of Clementine. + Copyright 2011, 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 GLOBALSEARCHSERVICERESULT_H +#define GLOBALSEARCHSERVICERESULT_H + +// This file contains definitions that are shared between Clementine and any +// external applications that need to do global searches over DBus. + +#include "config.h" + +#ifdef HAVE_DBUS +# include +#endif + +#include + +namespace globalsearch { + +// The order of types here is the order they'll appear in the UI. +enum Type { + Type_Track = 0, + Type_Stream, + Type_Album +}; + +enum MatchQuality { + // A token in the search string matched at the beginning of the song + // metadata. + Quality_AtStart = 0, + + // A token matched somewhere else. + Quality_Middle, + + Quality_None +}; + +} // namespace globalsearch + + +#ifdef HAVE_DBUS + +struct GlobalSearchServiceResult { + // When adding new fields to this struct remember to update the dbus signature + // which is duplicated in the xml specification and in clementinerunner.cpp + + int result_id_; + bool art_on_the_way_; + + QString provider_name_; + globalsearch::Type type_; + globalsearch::MatchQuality match_quality_; + + int album_size_; + + QString title_; + QString artist_; + QString album_; + QString album_artist_; + bool is_compilation_; + int track_; + + // Not included in the dbus emission. + QIcon image_; + int provider_order_; +}; +typedef QList GlobalSearchServiceResultList; + +Q_DECLARE_METATYPE(GlobalSearchServiceResult) +Q_DECLARE_METATYPE(GlobalSearchServiceResultList) + +QDBusArgument& operator <<(QDBusArgument& arg, const GlobalSearchServiceResult& result); +const QDBusArgument& operator >>(const QDBusArgument& arg, GlobalSearchServiceResult& result); + +#endif // HAVE_DBUS + + +#endif // GLOBALSEARCHSERVICERESULT_H diff --git a/src/globalsearch/globalsearchitemdelegate.cpp b/src/globalsearch/globalsearchitemdelegate.cpp index d737022b5..ded793dd9 100644 --- a/src/globalsearch/globalsearchitemdelegate.cpp +++ b/src/globalsearch/globalsearchitemdelegate.cpp @@ -88,14 +88,14 @@ void GlobalSearchItemDelegate::paint(QPainter* p, QString count; switch (result.type_) { - case SearchProvider::Result::Type_Track: + case globalsearch::Type_Track: break; - case SearchProvider::Result::Type_Stream: + case globalsearch::Type_Stream: count = QString::fromUtf8("∞"); break; - case SearchProvider::Result::Type_Album: + case globalsearch::Type_Album: if (result.album_size_ <= 0) count = "-"; else @@ -130,8 +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_Stream: { + case globalsearch::Type_Track: + case globalsearch::Type_Stream: { // Title line_1 += m.title() + " "; @@ -153,7 +153,7 @@ void GlobalSearchItemDelegate::paint(QPainter* p, break; } - case SearchProvider::Result::Type_Album: { + case globalsearch::Type_Album: { // Line 1 is Artist - Album // Artist if (!m.albumartist().isEmpty()) diff --git a/src/globalsearch/globalsearchservice.cpp b/src/globalsearch/globalsearchservice.cpp new file mode 100644 index 000000000..3ab6b7e0d --- /dev/null +++ b/src/globalsearch/globalsearchservice.cpp @@ -0,0 +1,129 @@ +/* This file is part of Clementine. + Copyright 2011, 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 "globalsearch.h" +#include "globalsearchservice.h" +#include "globalsearch/globalsearchadaptor.h" +#include "core/logging.h" +#include "core/mpris_common.h" + + +GlobalSearchService::GlobalSearchService(GlobalSearch* engine, QObject* parent) + : QObject(parent), + engine_(engine) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + new GlobalSearchAdaptor(this); + QDBusConnection::sessionBus().registerObject("/GlobalSearch", this); + + connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)), + this, SLOT(ResultsAvailableSlot(int,SearchProvider::ResultList)), + Qt::QueuedConnection); + connect(engine_, SIGNAL(SearchFinished(int)), + this, SLOT(SearchFinishedSlot(int)), + Qt::QueuedConnection); + connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), + this, SLOT(ArtLoadedSlot(int,QPixmap)), + Qt::QueuedConnection); +} + +int GlobalSearchService::StartSearch(const QString& query, bool prefetch_art) { + PendingSearch pending_search; + pending_search.prefetch_art_ = prefetch_art; + + const int id = engine_->SearchAsync(query); + pending_searches_[id] = pending_search; + return id; +} + +void GlobalSearchService::CancelSearch(int id) { + if (!pending_searches_.contains(id)) + return; + + engine_->CancelSearch(id); + pending_searches_.remove(id); +} + +void GlobalSearchService::ResultsAvailableSlot(int id, const SearchProvider::ResultList& results) { + if (!pending_searches_.contains(id)) + return; + + const PendingSearch& pending = pending_searches_[id]; + + GlobalSearchServiceResultList ret; + foreach (const SearchProvider::Result& result, results) { + const int result_id = next_result_id_ ++; + + RecentResult& recent = recent_results_[result_id]; + recent.result_.art_on_the_way_ = false; + + // Prefetch art if it was requested + if (pending.prefetch_art_ && !result.provider_->art_is_probably_remote()) { + const int art_id = engine_->LoadArtAsync(result); + prefetching_art_[art_id] = result_id; + recent.result_.art_on_the_way_ = true; + } + + // Build the result to send back + recent.result_.result_id_ = result_id; + recent.result_.provider_name_ = result.provider_->name(); + recent.result_.type_ = result.type_; + recent.result_.match_quality_ = result.match_quality_; + + recent.result_.album_size_ = result.album_size_; + + recent.result_.title_ = result.metadata_.title(); + recent.result_.artist_ = result.metadata_.artist(); + recent.result_.album_ = result.metadata_.album(); + recent.result_.album_artist_ = result.metadata_.albumartist(); + recent.result_.is_compilation_ = result.metadata_.is_compilation(); + recent.result_.track_ = result.metadata_.track(); + + ret << recent.result_; + } + + emit ResultsAvailable(id, ret); +} + +void GlobalSearchService::SearchFinishedSlot(int id) { + if (!pending_searches_.contains(id)) + return; + + emit SearchFinished(id); + pending_searches_.remove(id); +} + +void GlobalSearchService::ArtLoadedSlot(int id, const QPixmap& pixmap) { + QMap::iterator it = prefetching_art_.find(id); + if (it == prefetching_art_.end()) + return; + + const int result_id = prefetching_art_.take(id); + QMap::iterator it2 = recent_results_.find(result_id); + if (it2 == recent_results_.end()) + return; + + // Encode the pixmap as a png + QBuffer buf; + buf.open(QIODevice::WriteOnly); + pixmap.toImage().save(&buf, "PNG"); + buf.close(); + + emit ArtLoaded(result_id, buf.data()); +} diff --git a/src/globalsearch/globalsearchservice.h b/src/globalsearch/globalsearchservice.h new file mode 100644 index 000000000..9d9171ec7 --- /dev/null +++ b/src/globalsearch/globalsearchservice.h @@ -0,0 +1,74 @@ +/* This file is part of Clementine. + Copyright 2011, 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 GLOBALSEARCHSERVICE_H +#define GLOBALSEARCHSERVICE_H + +#include +#include +#include + +#include "common.h" +#include "searchprovider.h" + +class GlobalSearch; + +class QEventLoop; + + +class GlobalSearchService : public QObject { + Q_OBJECT + +public: + GlobalSearchService(GlobalSearch* engine, QObject* parent = 0); + +public slots: + int StartSearch(const QString& query, bool prefetch_art); + void CancelSearch(int id); + +signals: + void ResultsAvailable(int id, const GlobalSearchServiceResultList& results); + void SearchFinished(int id); + void ArtLoaded(int result_id, const QByteArray& image_data); + +private slots: + void ResultsAvailableSlot(int id, const SearchProvider::ResultList& results); + void SearchFinishedSlot(int id); + void ArtLoadedSlot(int id, const QPixmap& pixmap); + +private: + struct PendingSearch { + bool prefetch_art_; + }; + + struct RecentResult { + GlobalSearchServiceResult result_; + }; + + GlobalSearch* engine_; + + // GlobalSearch request ids + QMap pending_searches_; + + // Result ids + QMap recent_results_; + QMap prefetching_art_; // LoadArt id -> result id + + int next_result_id_; +}; + +#endif // GLOBALSEARCHSERVICE_H diff --git a/src/globalsearch/globalsearchsortmodel.cpp b/src/globalsearch/globalsearchsortmodel.cpp index f50c5fefb..eb79022ae 100644 --- a/src/globalsearch/globalsearchsortmodel.cpp +++ b/src/globalsearch/globalsearchsortmodel.cpp @@ -56,11 +56,11 @@ bool GlobalSearchSortModel::lessThan(const QModelIndex& left, const QModelIndex& // Failing that, compare title, artist and album switch (r1.type_) { - case SearchProvider::Result::Type_Track: - case SearchProvider::Result::Type_Stream: + case globalsearch::Type_Track: + case globalsearch::Type_Stream: CompareString(title); // fallthrough - case SearchProvider::Result::Type_Album: + case globalsearch::Type_Album: CompareString(artist); CompareString(album); break; diff --git a/src/globalsearch/globalsearchwidget.cpp b/src/globalsearch/globalsearchwidget.cpp index f0b0829a2..e9b25d173 100644 --- a/src/globalsearch/globalsearchwidget.cpp +++ b/src/globalsearch/globalsearchwidget.cpp @@ -616,15 +616,15 @@ GlobalSearchWidget::CombineAction GlobalSearchWidget::CanCombineResults( (QString::compare(r1.metadata_.field(), r2.metadata_.field(), Qt::CaseInsensitive) != 0) switch (r1.type_) { - case SearchProvider::Result::Type_Track: + case globalsearch::Type_Track: if (StringsDiffer(title)) return CannotCombine; // fallthrough - case SearchProvider::Result::Type_Album: + case globalsearch::Type_Album: if (StringsDiffer(album) || StringsDiffer(artist)) return CannotCombine; break; - case SearchProvider::Result::Type_Stream: + case globalsearch::Type_Stream: if (StringsDiffer(url().toString)) return CannotCombine; break; diff --git a/src/globalsearch/groovesharksearchprovider.cpp b/src/globalsearch/groovesharksearchprovider.cpp index 2a4238809..99df96aa3 100644 --- a/src/globalsearch/groovesharksearchprovider.cpp +++ b/src/globalsearch/groovesharksearchprovider.cpp @@ -67,9 +67,9 @@ void GroovesharkSearchProvider::SearchDone(int id, const SongList& songs) { ResultList ret; foreach (const Song& song, songs) { Result result(this); - result.type_ = Result::Type_Track; + result.type_ = globalsearch::Type_Track; result.metadata_ = song; - result.match_quality_ = Result::Quality_AtStart; + result.match_quality_ = globalsearch::Quality_AtStart; ret << result; } @@ -84,8 +84,8 @@ void GroovesharkSearchProvider::AlbumSearchResult(int id, const SongList& songs) ResultList ret; foreach (const Song& s, songs) { Result result(this); - result.type_ = Result::Type_Album; - result.match_quality_ = Result::Quality_AtStart; + result.type_ = globalsearch::Type_Album; + result.match_quality_ = globalsearch::Quality_AtStart; result.metadata_ = s; ret << result; @@ -120,7 +120,7 @@ void GroovesharkSearchProvider::LoadTracksAsync(int id, const Result& result) { SongList ret; switch (result.type_) { - case Result::Type_Track: { + case globalsearch::Type_Track: { ret << result.metadata_; SortSongs(&ret); @@ -131,7 +131,7 @@ void GroovesharkSearchProvider::LoadTracksAsync(int id, const Result& result) { break; } - case Result::Type_Album: + case globalsearch::Type_Album: FetchAlbum(id, result); break; diff --git a/src/globalsearch/librarysearchprovider.cpp b/src/globalsearch/librarysearchprovider.cpp index dad24f589..d088959df 100644 --- a/src/globalsearch/librarysearchprovider.cpp +++ b/src/globalsearch/librarysearchprovider.cpp @@ -74,13 +74,13 @@ SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString& album_key.prepend(song.artist()); } - Result::MatchQuality quality = MatchQuality(tokens, song.title()); + globalsearch::MatchQuality quality = MatchQuality(tokens, song.title()); - if (quality != Result::Quality_None) { + if (quality != globalsearch::Quality_None) { // If the query matched in the song title then we're interested in this // as an individual song. Result result(this); - result.type_ = Result::Type_Track; + result.type_ = globalsearch::Type_Track; result.metadata_ = song; result.match_quality_ = quality; ret << result; @@ -96,7 +96,7 @@ SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString& // song title. foreach (const QString& key, albums_with_non_track_matches) { Result result(this); - result.type_ = Result::Type_Album; + result.type_ = globalsearch::Type_Album; result.metadata_ = albums.value(key); result.album_size_ = albums.count(key); result.match_quality_ = @@ -131,12 +131,12 @@ void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) { SongList ret; switch (result.type_) { - case Result::Type_Track: + case globalsearch::Type_Track: // This is really easy - we just emit the track again. ret << result.metadata_; break; - case Result::Type_Album: { + case globalsearch::Type_Album: { // Find all the songs in this album. LibraryQuery query; query.SetColumnSpec("ROWID, " + Song::kColumnSpec); diff --git a/src/globalsearch/searchprovider.cpp b/src/globalsearch/searchprovider.cpp index 042e78101..a5d30d846 100644 --- a/src/globalsearch/searchprovider.cpp +++ b/src/globalsearch/searchprovider.cpp @@ -55,16 +55,16 @@ QStringList SearchProvider::TokenizeQuery(const QString& query) { return tokens; } -SearchProvider::Result::MatchQuality SearchProvider::MatchQuality( +globalsearch::MatchQuality SearchProvider::MatchQuality( const QStringList& tokens, const QString& string) { - Result::MatchQuality ret = Result::Quality_None; + globalsearch::MatchQuality ret = globalsearch::Quality_None; foreach (const QString& token, tokens) { const int index = string.indexOf(token, 0, Qt::CaseInsensitive); if (index == 0) { - return Result::Quality_AtStart; + return globalsearch::Quality_AtStart; } else if (index != -1) { - ret = Result::Quality_Middle; + ret = globalsearch::Quality_Middle; } } diff --git a/src/globalsearch/searchprovider.h b/src/globalsearch/searchprovider.h index a0a4262bb..f7cf7ea90 100644 --- a/src/globalsearch/searchprovider.h +++ b/src/globalsearch/searchprovider.h @@ -23,6 +23,7 @@ #include #include "core/song.h" +#include "globalsearch/common.h" class MimeData; @@ -39,31 +40,13 @@ public: Result(SearchProvider* provider = 0) : provider_(provider), album_size_(0) {} - // The order of types here is the order they'll appear in the UI. - enum Type { - Type_Track = 0, - Type_Stream, - Type_Album - }; - - enum MatchQuality { - // A token in the search string matched at the beginning of the song - // metadata. - Quality_AtStart = 0, - - // A token matched somewhere else. - Quality_Middle, - - Quality_None - }; - // This must be set by the provider using the constructor. SearchProvider* provider_; // These must be set explicitly by the provider. - Type type_; + globalsearch::Type type_; + globalsearch::MatchQuality match_quality_; Song metadata_; - MatchQuality match_quality_; // How many songs in the album - valid only if type == Type_Album. int album_size_; @@ -132,7 +115,7 @@ protected: // useful for figuring out whether you got a result because it matched in // the song title or the artist/album name. static QStringList TokenizeQuery(const QString& query); - static Result::MatchQuality MatchQuality(const QStringList& tokens, const QString& string); + static globalsearch::MatchQuality MatchQuality(const QStringList& tokens, const QString& string); // Sorts a list of songs by disc, then by track. static void SortSongs(SongList* list); diff --git a/src/globalsearch/simplesearchprovider.cpp b/src/globalsearch/simplesearchprovider.cpp index ebc412478..c9dd18bb4 100644 --- a/src/globalsearch/simplesearchprovider.cpp +++ b/src/globalsearch/simplesearchprovider.cpp @@ -68,12 +68,12 @@ SearchProvider::ResultList SimpleSearchProvider::Search(int id, const QString& q QMutexLocker l(&items_mutex_); foreach (const Item& item, items_) { Result result(this); - result.type_ = Result::Type_Stream; - result.match_quality_ = Result::Quality_None; + result.type_ = globalsearch::Type_Stream; + result.match_quality_ = globalsearch::Quality_None; foreach (const QString& token, tokens) { if (item.keyword_.startsWith(token, Qt::CaseInsensitive)) { - result.match_quality_ = Result::Quality_AtStart; + result.match_quality_ = globalsearch::Quality_AtStart; continue; } @@ -88,17 +88,17 @@ SearchProvider::ResultList SimpleSearchProvider::Search(int id, const QString& q if (matched_safe_word) continue; - result.match_quality_ = Result::Quality_None; + result.match_quality_ = globalsearch::Quality_None; break; } - result.match_quality_ = qMin(result.match_quality_, Result::Quality_Middle); + result.match_quality_ = qMin(result.match_quality_, globalsearch::Quality_Middle); } - if (result.match_quality_ == Result::Quality_Middle) { + if (result.match_quality_ == globalsearch::Quality_Middle) { result.match_quality_ = MatchQuality(tokens, item.metadata_.title()); } - if (result.match_quality_ != Result::Quality_None) { + if (result.match_quality_ != globalsearch::Quality_None) { result.metadata_ = item.metadata_; ret << result; } diff --git a/src/globalsearch/spotifysearchprovider.cpp b/src/globalsearch/spotifysearchprovider.cpp index 8b3c8e86a..ee0be977c 100644 --- a/src/globalsearch/spotifysearchprovider.cpp +++ b/src/globalsearch/spotifysearchprovider.cpp @@ -88,7 +88,7 @@ void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse& const spotify_pb::Track& track = response.result(i); Result result(this); - result.type_ = Result::Type_Track; + result.type_ = globalsearch::Type_Track; SpotifyService::SongFromProtobuf(track, &result.metadata_); result.match_quality_ = MatchQuality(state.tokens_, result.metadata_.title()); @@ -99,7 +99,7 @@ void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse& const spotify_pb::Album& album = response.album(i); Result result(this); - result.type_ = Result::Type_Album; + result.type_ = globalsearch::Type_Album; SpotifyService::SongFromProtobuf(album.metadata(), &result.metadata_); result.match_quality_ = qMin(MatchQuality(state.tokens_, result.metadata_.album()), @@ -147,14 +147,14 @@ void SpotifySearchProvider::ArtLoadedSlot(const QString& id, const QImage& image void SpotifySearchProvider::LoadTracksAsync(int id, const Result& result) { switch (result.type_) { - case Result::Type_Track: { + case globalsearch::Type_Track: { SongMimeData* mime_data = new SongMimeData; mime_data->songs = SongList() << result.metadata_; emit TracksLoaded(id, mime_data); break; } - case Result::Type_Album: { + case globalsearch::Type_Album: { SpotifyServer* s = server(); if (!s) { emit TracksLoaded(id, NULL); diff --git a/src/globalsearch/tooltipresultwidget.cpp b/src/globalsearch/tooltipresultwidget.cpp index c5124b0ad..a399bd5f7 100644 --- a/src/globalsearch/tooltipresultwidget.cpp +++ b/src/globalsearch/tooltipresultwidget.cpp @@ -58,11 +58,11 @@ QSize TooltipResultWidget::CalculateSizeHint() const { bold_metrics_.width(TitleText()) + kBorder); switch (result_.type_) { - case SearchProvider::Result::Type_Track: - case SearchProvider::Result::Type_Stream: + case globalsearch::Type_Track: + case globalsearch::Type_Stream: break; - case SearchProvider::Result::Type_Album: + case globalsearch::Type_Album: if (result_.album_songs_.isEmpty()) break; @@ -119,11 +119,11 @@ void TooltipResultWidget::paintEvent(QPaintEvent*) { y += kLineHeight; switch (result_.type_) { - case SearchProvider::Result::Type_Track: - case SearchProvider::Result::Type_Stream: + case globalsearch::Type_Track: + case globalsearch::Type_Stream: break; - case SearchProvider::Result::Type_Album: + case globalsearch::Type_Album: if (result_.album_songs_.isEmpty()) break; diff --git a/src/main.cpp b/src/main.cpp index c443308d5..43a064478 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,6 +93,7 @@ using boost::scoped_ptr; #include "core/mpris.h" #include "core/mpris2.h" #include "dbus/metatypes.h" + #include "globalsearch/globalsearchservice.h" #include #include #include @@ -220,6 +221,7 @@ int main(int argc, char *argv[]) { qRegisterMetaTypeStreamOperators >("ColumnAlignmentMap"); qRegisterMetaType("QNetworkCookie"); qRegisterMetaType >("QList"); + qRegisterMetaType("SearchProvider::Result"); qRegisterMetaType("SearchProvider::ResultList"); qRegisterMetaType("GstBuffer*"); @@ -438,6 +440,8 @@ int main(int argc, char *argv[]) { QObject::connect(&playlists, SIGNAL(CurrentSongChanged(Song)), &art_loader, SLOT(LoadArt(Song))); QObject::connect(&art_loader, SIGNAL(ThumbnailLoaded(Song, QString, QImage)), &osd, SLOT(CoverArtPathReady(Song, QString))); + + GlobalSearchService global_search_service(&global_search); #endif // Window