Add a DBus interface onto the global search engine, and add a KDE krunner plugin that performs global searches in Clementine.

This is optional and is not compiled unless you have the plasma developer packages installed.
This commit is contained in:
David Sansome 2011-10-16 21:57:53 +01:00
parent 348faef9e1
commit 9a739a3346
30 changed files with 936 additions and 88 deletions

View File

@ -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)

View File

@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 2.6)
add_definitions(-DQT_STATICPLUGIN)
# Source files
set(SQLITE-SOURCES
qsql_sqlite.cpp

View File

@ -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"

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/clementineplasmarunner">
<file>nocover.png</file>
</qresource>
</RCC>

View File

@ -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})

View File

@ -0,0 +1,295 @@
/* This file is part of Clementine.
Copyright 2011, 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 "clementinerunner.h"
#include "globalsearchinterface.h"
#include <QDBusConnection>
#include <QMutexLocker>
#include <QtDebug>
#include <unistd.h>
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<GlobalSearchServiceResult>();
qDBusRegisterMetaType<GlobalSearchServiceResultList>();
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("<query>", 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<int> 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)

View File

@ -0,0 +1,83 @@
/* This file is part of Clementine.
Copyright 2011, 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/>.
*/
#ifndef CLEMENTINERUNNER_H
#define CLEMENTINERUNNER_H
#include "src/globalsearch/common.h"
#include <QSemaphore>
#include <Plasma/AbstractRunner>
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<int, PendingQuery*> 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

View File

@ -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

View File

@ -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)

View File

@ -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_);
}

View File

@ -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

View File

@ -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

View File

@ -18,6 +18,7 @@
#include "fmpsparser.h"
#include "song.h"
#include "core/logging.h"
#include "core/mpris_common.h"
#include <algorithm>
@ -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);
}
}

View File

@ -27,6 +27,7 @@
#include <QSqlQuery>
#include <QString>
#include <QUrl>
#include <QVariantMap>
#include <xiphcomment.h>
@ -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_; }

View File

@ -0,0 +1,32 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name='org.clementineplayer.GlobalSearch'>
<method name='StartSearch'>
<arg type='i' name='id' direction='out' />
<arg type='s' name='query' direction='in' />
<arg type='b' name='prefetch_art' direction='in' />
</method>
<method name='CancelSearch'>
<arg type='i' name='id' direction='in' />
</method>
<signal name='ResultsAvailable'>
<arg type='i' name='id' />
<arg type='a(ibsiiissssbi)' name='results' />
<annotation name="com.trolltech.QtDBus.QtTypeName.In1" value="GlobalSearchServiceResultList" />
</signal>
<signal name='SearchFinished'>
<arg type='i' name='id' />
</signal>
<signal name='ArtLoaded'>
<arg type='i' name='result_id' />
<arg type='ay' name='image_data' />
<annotation name="com.trolltech.QtDBus.QtTypeName.Out1" value="QByteArray" />
</signal>
</interface>
</node>

View File

@ -0,0 +1,66 @@
/* This file is part of Clementine.
Copyright 2011, 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 "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<globalsearch::Type>(type);
result.match_quality_ = static_cast<globalsearch::MatchQuality>(match_quality);
return arg;
}
#endif // HAVE_DBUS

92
src/globalsearch/common.h Normal file
View File

@ -0,0 +1,92 @@
/* This file is part of Clementine.
Copyright 2011, 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/>.
*/
#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 <QDBusArgument>
#endif
#include <QIcon>
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<GlobalSearchServiceResult> 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

View File

@ -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())

View File

@ -0,0 +1,129 @@
/* This file is part of Clementine.
Copyright 2011, 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 "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<GlobalSearchServiceResult>();
qDBusRegisterMetaType<GlobalSearchServiceResultList>();
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<int, int>::iterator it = prefetching_art_.find(id);
if (it == prefetching_art_.end())
return;
const int result_id = prefetching_art_.take(id);
QMap<int, RecentResult>::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());
}

View File

@ -0,0 +1,74 @@
/* This file is part of Clementine.
Copyright 2011, 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/>.
*/
#ifndef GLOBALSEARCHSERVICE_H
#define GLOBALSEARCHSERVICE_H
#include <QDBusArgument>
#include <QObject>
#include <QSet>
#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<int, PendingSearch> pending_searches_;
// Result ids
QMap<int, RecentResult> recent_results_;
QMap<int, int> prefetching_art_; // LoadArt id -> result id
int next_result_id_;
};
#endif // GLOBALSEARCHSERVICE_H

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}

View File

@ -23,6 +23,7 @@
#include <QObject>
#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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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 <QDBusArgument>
#include <QDBusConnection>
#include <QImage>
@ -220,6 +221,7 @@ int main(int argc, char *argv[]) {
qRegisterMetaTypeStreamOperators<QMap<int, int> >("ColumnAlignmentMap");
qRegisterMetaType<QNetworkCookie>("QNetworkCookie");
qRegisterMetaType<QList<QNetworkCookie> >("QList<QNetworkCookie>");
qRegisterMetaType<SearchProvider::Result>("SearchProvider::Result");
qRegisterMetaType<SearchProvider::ResultList>("SearchProvider::ResultList");
qRegisterMetaType<GstBuffer*>("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