diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1ac6a3873..71e0dfb4d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -46,6 +46,7 @@ find_package(OpenGL REQUIRED)
find_package(Boost REQUIRED)
find_package(Gettext REQUIRED)
find_package(PkgConfig REQUIRED)
+find_package(Protobuf)
pkg_check_modules(TAGLIB REQUIRED taglib>=1.6)
pkg_check_modules(GSTREAMER gstreamer-0.10)
@@ -61,6 +62,7 @@ pkg_check_modules(USBMUXD libusbmuxd)
pkg_check_modules(LIBMTP libmtp>=1.0)
pkg_check_modules(INDICATEQT indicate-qt)
pkg_check_modules(ARCHIVE libarchive)
+pkg_check_modules(SPOTIFY libspotify)
if (WIN32)
find_package(ZLIB REQUIRED)
@@ -103,11 +105,13 @@ find_path(LASTFM_INCLUDE_DIRS lastfm/ws.h)
if (APPLE)
find_library(GROWL Growl)
+
option(ENABLE_SPARKLE "Sparkle updating" ON)
find_library(SPARKLE Sparkle)
if (ENABLE_SPARKLE AND SPARKLE)
set(HAVE_SPARKLE ON)
endif (ENABLE_SPARKLE AND SPARKLE)
+
# Uses Darwin kernel version.
# 9.8.0 -> 10.5/Leopard
# 10.4.0 -> 10.6/Snow Leopard
@@ -177,6 +181,7 @@ option(ENABLE_SCRIPTING_ARCHIVES "Enable support for loading scripts from archiv
option(ENABLE_SCRIPTING_PYTHON "Enable python scripting" ON)
option(ENABLE_REMOTE "Enable support for using remote controls with Clementine" OFF)
option(ENABLE_BREAKPAD "Enable crash reporting" OFF)
+option(ENABLE_SPOTIFY "Enable spotify support" OFF)
if(WIN32)
option(ENABLE_WIN32_CONSOLE "Show the windows console even outside Debug mode" OFF)
@@ -222,6 +227,11 @@ if(ENABLE_BREAKPAD)
set(HAVE_BREAKPAD ON)
endif(ENABLE_BREAKPAD)
+if(ENABLE_SPOTIFY AND SPOTIFY_FOUND AND PROTOBUF_FOUND)
+ set(HAVE_SPOTIFY ON)
+endif(ENABLE_SPOTIFY AND SPOTIFY_FOUND AND PROTOBUF_FOUND)
+
+
if(ENABLE_VISUALISATIONS)
# When/if upstream accepts our patches then these options can be used to link
# to system installed projectM instead.
@@ -357,6 +367,10 @@ if(HAVE_BREAKPAD)
add_subdirectory(3rdparty/google-breakpad)
endif(HAVE_BREAKPAD)
+if(HAVE_SPOTIFY)
+ add_subdirectory(spotifyblob)
+endif(HAVE_SPOTIFY)
+
# Uninstall support
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in"
@@ -382,4 +396,5 @@ summary_add("Scripting support: loading archives" HAVE_LIBARCHIVE)
summary_add("Scripting support: Python" HAVE_SCRIPTING_PYTHON)
summary_add("(Mac OS X) Sparkle integration" HAVE_SPARKLE)
summary_add("(unstable) Remote control support" HAVE_REMOTE)
+summary_add("(unstable) Spotify support" HAVE_SPOTIFY)
summary_show()
diff --git a/data/data.qrc b/data/data.qrc
index 480fdf780..dcc4d6732 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -49,6 +49,7 @@
providers/magnatune.png
schema/schema-8.sql
schema/schema-9.sql
+ icons/svg/spotify.svg
icons/22x22/application-exit.png
icons/22x22/applications-internet.png
icons/22x22/configure.png
diff --git a/data/icons/svg/spotify.svg b/data/icons/svg/spotify.svg
new file mode 100644
index 000000000..6f4f92c21
--- /dev/null
+++ b/data/icons/svg/spotify.svg
@@ -0,0 +1,285 @@
+
+
+
diff --git a/spotifyblob/CMakeLists.txt b/spotifyblob/CMakeLists.txt
new file mode 100644
index 000000000..e31caf998
--- /dev/null
+++ b/spotifyblob/CMakeLists.txt
@@ -0,0 +1,51 @@
+include_directories(${SPOTIFY_INCLUDE_DIRS})
+include_directories(${PROTOBUF_INCLUDE_DIRS})
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
+include_directories(${CMAKE_SOURCE_DIR}/src)
+
+link_directories(${SPOTIFY_LIBRARY_DIRS})
+
+set(EXECUTABLE_OUTPUT_PATH ..)
+
+
+set(SOURCES
+ main.cpp
+ spotifyblob.cpp
+ spotifyclient.cpp
+
+ ../src/core/logging.cpp
+)
+
+set(HEADERS
+ spotifyblob.h
+ spotifyclient.h
+)
+
+set(MESSAGES
+ messages.proto
+)
+
+qt4_wrap_cpp(MOC ${HEADERS})
+protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
+
+
+add_library(clementine-spotifyblob-messages STATIC
+ ${PROTO_SOURCES}
+)
+
+target_link_libraries(clementine-spotifyblob-messages
+ ${PROTOBUF_LIBRARY}
+ ${CMAKE_THREAD_LIBS_INIT}
+)
+
+
+add_executable(clementine-spotifyblob
+ ${SOURCES}
+ ${MOC}
+)
+
+target_link_libraries(clementine-spotifyblob
+ ${SPOTIFY_LIBRARIES}
+ ${QT_LIBRARIES}
+ clementine-spotifyblob-messages
+)
diff --git a/spotifyblob/main.cpp b/spotifyblob/main.cpp
new file mode 100644
index 000000000..a1dec90f8
--- /dev/null
+++ b/spotifyblob/main.cpp
@@ -0,0 +1,25 @@
+#include
+#include
+
+#include "spotifyblob.h"
+#include "spotifyclient.h"
+#include "core/logging.h"
+
+int main(int argc, char** argv) {
+ QCoreApplication a(argc, argv);
+
+ logging::Init();
+
+ const QStringList arguments(a.arguments());
+
+ if (arguments.length() != 2) {
+ qFatal("Usage: %s port", argv[0]);
+ }
+
+ SpotifyClient client;
+ SpotifyBlob blob(&client);
+
+ client.Init(arguments[1].toInt());
+
+ return a.exec();
+}
diff --git a/spotifyblob/messages.proto b/spotifyblob/messages.proto
new file mode 100644
index 000000000..a30a46b37
--- /dev/null
+++ b/spotifyblob/messages.proto
@@ -0,0 +1,17 @@
+message LoginRequest {
+ required string username = 1;
+ required string password = 2;
+}
+
+message LoginResponse {
+ required bool success = 1;
+ required string error = 2;
+}
+
+message RequestMessage {
+ optional LoginRequest login_request = 1;
+}
+
+message ResponseMessage {
+ optional LoginResponse login_response = 1;
+}
diff --git a/spotifyblob/spotify.cpp b/spotifyblob/spotify.cpp
new file mode 100644
index 000000000..e69de29bb
diff --git a/spotifyblob/spotify.h b/spotifyblob/spotify.h
new file mode 100644
index 000000000..e69de29bb
diff --git a/spotifyblob/spotifyblob.cpp b/spotifyblob/spotifyblob.cpp
new file mode 100644
index 000000000..ad5a470e4
--- /dev/null
+++ b/spotifyblob/spotifyblob.cpp
@@ -0,0 +1,124 @@
+#include "spotifyblob.h"
+#include "spotifyclient.h"
+#include "spotifykey.h"
+
+#include
+#include
+#include
+
+SpotifyBlob::SpotifyBlob(SpotifyClient* client, QObject* parent)
+ : QObject(parent),
+ client_(client),
+ session_(NULL),
+ events_timer_(new QTimer(this)) {
+ memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_));
+ memset(&spotify_config_, 0, sizeof(spotify_config_));
+
+ spotify_callbacks_.logged_in = &LoggedIn;
+ spotify_callbacks_.notify_main_thread = &NotifyMainThread;
+ spotify_callbacks_.log_message = &LogMessage;
+
+ spotify_config_.api_version = SPOTIFY_API_VERSION; // From libspotify/api.h
+ spotify_config_.cache_location = strdup(QDir::tempPath().toLocal8Bit().constData());
+ spotify_config_.settings_location = strdup(QDir::tempPath().toLocal8Bit().constData());
+ spotify_config_.application_key = g_appkey;
+ spotify_config_.application_key_size = g_appkey_size;
+ spotify_config_.callbacks = &spotify_callbacks_;
+ spotify_config_.userdata = this;
+ spotify_config_.user_agent = "Clementine Player";
+
+ events_timer_->setSingleShot(true);
+ connect(events_timer_, SIGNAL(timeout()), SLOT(ProcessEvents()));
+
+ connect(client, SIGNAL(Login(QString,QString)), SLOT(Login(QString,QString)));
+}
+
+SpotifyBlob::~SpotifyBlob() {
+ if (session_) {
+ sp_session_release(session_);
+ }
+
+ free(const_cast(spotify_config_.cache_location));
+ free(const_cast(spotify_config_.settings_location));
+}
+
+void SpotifyBlob::Login(const QString& username, const QString& password) {
+ sp_error error = sp_session_create(&spotify_config_, &session_);
+ if (error != SP_ERROR_OK) {
+ qWarning() << "Failed to create session" << sp_error_message(error);
+ client_->LoginCompleted(false, sp_error_message(error));
+ return;
+ }
+
+ sp_session_login(session_, username.toUtf8().constData(), password.toUtf8().constData());
+}
+
+void SpotifyBlob::LoggedIn(sp_session* session, sp_error error) {
+ qDebug() << Q_FUNC_INFO;
+ SpotifyBlob* me = reinterpret_cast(sp_session_userdata(session));
+ me->LoggedIn(error);
+}
+
+void SpotifyBlob::LoggedIn(sp_error error) {
+ if (error != SP_ERROR_OK) {
+ qWarning() << "Failed to login" << sp_error_message(error);
+ }
+
+ client_->LoginCompleted(error == SP_ERROR_OK, sp_error_message(error));
+}
+
+void SpotifyBlob::NotifyMainThread(sp_session* session) {
+ qDebug() << Q_FUNC_INFO;
+ SpotifyBlob* me = reinterpret_cast(sp_session_userdata(session));
+ me->Notify();
+}
+
+// Called by spotify from an internal thread to notify us that its events need processing.
+void SpotifyBlob::Notify() {
+ metaObject()->invokeMethod(this, "ProcessEvents", Qt::QueuedConnection);
+}
+
+void SpotifyBlob::ProcessEvents() {
+ qDebug() << Q_FUNC_INFO;
+ int next_timeout_ms;
+ sp_session_process_events(session_, &next_timeout_ms);
+ qDebug() << next_timeout_ms << events_timer_;
+ events_timer_->start(next_timeout_ms);
+ qDebug() << "Started";
+}
+
+void SpotifyBlob::LogMessage(sp_session* session, const char* data) {
+ qDebug() << Q_FUNC_INFO;
+}
+
+void SpotifyBlob::Search(const QString& query) {
+ sp_search_create(
+ session_,
+ query.toUtf8().constData(),
+ 0, // track offset
+ 10, // track count
+ 0, // album offset
+ 10, // album count
+ 0, // artist offset
+ 10, // artist count
+ &SearchComplete,
+ this);
+}
+
+void SpotifyBlob::SearchComplete(sp_search* result, void* userdata) {
+ sp_error error = sp_search_error(result);
+ if (error != SP_ERROR_OK) {
+ qWarning() << "Search failed";
+ sp_search_release(result);
+ return;
+ }
+
+ int artists = sp_search_num_artists(result);
+ for (int i = 0; i < artists; ++i) {
+ sp_artist* artist = sp_search_artist(result, i);
+ qDebug() << "Found artist:" << sp_artist_name(artist);
+ }
+
+ sp_search_release(result);
+}
+
diff --git a/spotifyblob/spotifyblob.h b/spotifyblob/spotifyblob.h
new file mode 100644
index 000000000..cbf053bc3
--- /dev/null
+++ b/spotifyblob/spotifyblob.h
@@ -0,0 +1,44 @@
+#ifndef SPOTIFY_H
+#define SPOTIFY_H
+
+#include
+
+#include
+
+class SpotifyClient;
+
+class QTimer;
+
+class SpotifyBlob : public QObject {
+ Q_OBJECT
+
+public:
+ SpotifyBlob(SpotifyClient* client, QObject* parent = 0);
+ ~SpotifyBlob();
+
+private slots:
+ void ProcessEvents();
+ void Login(const QString& username, const QString& password);
+ void Search(const QString& query);
+
+private:
+ // Spotify callbacks.
+ static void LoggedIn(sp_session* session, sp_error error);
+ static void NotifyMainThread(sp_session* session);
+ static void LogMessage(sp_session* session, const char* data);
+
+ static void SearchComplete(sp_search* result, void* userdata);
+
+ void Notify();
+ void LoggedIn(sp_error error);
+
+ SpotifyClient* client_;
+
+ sp_session_config spotify_config_;
+ sp_session_callbacks spotify_callbacks_;
+ sp_session* session_;
+
+ QTimer* events_timer_;
+};
+
+#endif // SPOTIFY_H
diff --git a/spotifyblob/spotifyclient.cpp b/spotifyblob/spotifyclient.cpp
new file mode 100644
index 000000000..96546d923
--- /dev/null
+++ b/spotifyblob/spotifyclient.cpp
@@ -0,0 +1,90 @@
+/* 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 "messages.pb.h"
+#include "spotifyclient.h"
+#include "core/logging.h"
+
+#include
+#include
+
+#include
+
+SpotifyClient::SpotifyClient(QObject* parent)
+ : QObject(parent),
+ socket_(new QTcpSocket(this))
+{
+ connect(socket_, SIGNAL(readyRead()), SLOT(SocketReadyRead()));
+}
+
+void SpotifyClient::Init(quint16 port) {
+ qLog(Debug) << "port" << port;
+
+ socket_->connectToHost(QHostAddress::LocalHost, port);
+}
+
+void SpotifyClient::SocketReadyRead() {
+ QDataStream s(socket_);
+
+ quint32 length = 0;
+ s >> length;
+
+ if (length == 0) {
+ return;
+ }
+
+ boost::scoped_array data(new char[length]);
+ s.readRawData(data.get(), length);
+
+ RequestMessage message;
+ if (!message.ParseFromArray(data.get(), length)) {
+ qLog(Error) << "Malformed protobuf message";
+ socket_->deleteLater();
+ socket_ = NULL;
+ return;
+ }
+
+ qLog(Debug) << message.DebugString().c_str();
+
+ if (message.has_login_request()) {
+ const LoginRequest& r = message.login_request();
+ emit Login(QString::fromUtf8(r.username().data(), r.username().length()),
+ QString::fromUtf8(r.password().data(), r.password().length()));
+ }
+}
+
+void SpotifyClient::SendMessage(const ResponseMessage& message) {
+ qLog(Debug) << message.DebugString().c_str();
+
+ std::string data(message.SerializeAsString());
+
+ QDataStream s(socket_);
+ s << quint32(data.length());
+ s.writeRawData(data.data(), data.length());
+}
+
+void SpotifyClient::LoginCompleted(bool success, const QString& error) {
+ const QByteArray error_bytes(error.toUtf8());
+
+ ResponseMessage message;
+
+ LoginResponse* response = message.mutable_login_response();
+ response->set_success(success);
+ response->set_error(error_bytes.constData(), error_bytes.length());
+
+ SendMessage(message);
+}
diff --git a/spotifyblob/spotifyclient.h b/spotifyblob/spotifyclient.h
new file mode 100644
index 000000000..e25ac21e4
--- /dev/null
+++ b/spotifyblob/spotifyclient.h
@@ -0,0 +1,32 @@
+#ifndef SPOTIFYCLIENT_H
+#define SPOTIFYCLIENT_H
+
+#include
+
+class QTcpSocket;
+
+class ResponseMessage;
+
+class SpotifyClient : public QObject {
+ Q_OBJECT
+
+public:
+ SpotifyClient(QObject* parent = 0);
+
+ void Init(quint16 port);
+
+ void LoginCompleted(bool success, const QString& error);
+
+signals:
+ void Login(const QString& username, const QString& password);
+
+private slots:
+ void SocketReadyRead();
+
+private:
+ void SendMessage(const ResponseMessage& message);
+
+ QTcpSocket* socket_;
+};
+
+#endif // SPOTIFYCLIENT_H
diff --git a/spotifyblob/spotifykey.h b/spotifyblob/spotifykey.h
new file mode 100644
index 000000000..13d2285dc
--- /dev/null
+++ b/spotifyblob/spotifykey.h
@@ -0,0 +1,28 @@
+#include
+#include
+
+const uint8_t g_appkey[] = {
+ 0x01, 0x59, 0x4E, 0xAE, 0xF2, 0x64, 0x2B, 0x1F, 0x13, 0xF8, 0xB1, 0x2C, 0x0A, 0x4F, 0x8A, 0xCA,
+ 0x5D, 0xB8, 0x23, 0x43, 0x12, 0xB2, 0x3A, 0x21, 0x64, 0x0B, 0x4C, 0x17, 0x39, 0xB6, 0x3B, 0x92,
+ 0xE6, 0xB3, 0x56, 0xE6, 0x01, 0x61, 0x56, 0x81, 0xD6, 0x5A, 0x1E, 0x4A, 0x72, 0xA7, 0x34, 0x89,
+ 0x3E, 0x64, 0x9D, 0xF9, 0x68, 0xB9, 0xD3, 0x0C, 0x20, 0x27, 0x05, 0x42, 0x53, 0x4B, 0x2A, 0xE0,
+ 0xB7, 0xA9, 0xD7, 0x30, 0x78, 0xB7, 0xA6, 0x7C, 0x87, 0x6E, 0x8D, 0x2B, 0xAF, 0xB3, 0xF2, 0x09,
+ 0xA4, 0x1D, 0x59, 0x15, 0xF4, 0x34, 0x4F, 0x91, 0x18, 0x1E, 0x6D, 0xC5, 0x24, 0x59, 0x30, 0xD3,
+ 0x7E, 0x5C, 0x15, 0x3C, 0xA5, 0x85, 0xE3, 0xE8, 0x1D, 0x46, 0x35, 0x30, 0xA6, 0xBB, 0x2A, 0x07,
+ 0x0E, 0x8E, 0x8A, 0x87, 0xD1, 0x2C, 0x9C, 0xDE, 0x29, 0xAC, 0x5B, 0x99, 0xA5, 0x06, 0xAA, 0x44,
+ 0xA4, 0xC5, 0x52, 0xCE, 0x89, 0x70, 0xBD, 0x97, 0x1C, 0xA5, 0x36, 0xAE, 0xA3, 0xBB, 0xB4, 0x3B,
+ 0xB9, 0x8F, 0x2E, 0xF1, 0x79, 0x03, 0x52, 0xC5, 0x4C, 0xDD, 0x82, 0x31, 0x89, 0x6C, 0xB6, 0xF8,
+ 0x43, 0x3B, 0x23, 0xF9, 0x61, 0x6A, 0xDC, 0xD3, 0xF7, 0x1A, 0xEA, 0xAF, 0x11, 0xDE, 0xDA, 0x58,
+ 0x3B, 0x0B, 0x33, 0xB0, 0x17, 0x99, 0x2E, 0x15, 0xDC, 0x20, 0x9A, 0x2F, 0x0C, 0x43, 0xBD, 0xB1,
+ 0x4D, 0x85, 0xC8, 0x2A, 0x73, 0x65, 0x3A, 0xBC, 0xBF, 0x84, 0x90, 0xE5, 0x37, 0xD9, 0x4A, 0xC6,
+ 0x46, 0xD4, 0x04, 0xB8, 0x7E, 0xA7, 0x36, 0xD8, 0xBC, 0xEF, 0x76, 0x4B, 0x67, 0xD8, 0x28, 0xAE,
+ 0x4F, 0x00, 0x7B, 0xD7, 0xE7, 0xE4, 0xA6, 0x48, 0x11, 0x9B, 0x87, 0xA7, 0x65, 0xA4, 0x77, 0x82,
+ 0x1A, 0xA6, 0xFC, 0x30, 0xF7, 0x5A, 0x79, 0x9E, 0x48, 0xD5, 0x8C, 0x4C, 0x70, 0x39, 0x31, 0x6B,
+ 0x6F, 0xAD, 0x6C, 0xB0, 0x3F, 0x32, 0xE6, 0xAD, 0xEE, 0x02, 0x80, 0xF6, 0xEE, 0x7B, 0x4C, 0x50,
+ 0xAD, 0x62, 0x3B, 0xA7, 0xFE, 0xEF, 0xE2, 0xFC, 0xE3, 0x70, 0x74, 0x12, 0x10, 0xAD, 0xE3, 0xF3,
+ 0x5D, 0x98, 0xE2, 0xA4, 0xED, 0xF6, 0x91, 0x89, 0x90, 0x02, 0x20, 0xA4, 0x37, 0x5A, 0x7C, 0xB2,
+ 0x04, 0x04, 0x70, 0x5E, 0x37, 0x43, 0x16, 0x95, 0xC1, 0x71, 0xA8, 0xD9, 0x7D, 0x2B, 0x46, 0x72,
+ 0x5F,
+};
+
+const size_t g_appkey_size = sizeof(g_appkey);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ab1952fc2..0b6679746 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -613,6 +613,21 @@ if(HAVE_LIBLASTFM)
)
endif(HAVE_LIBLASTFM)
+if(HAVE_SPOTIFY)
+ list(APPEND SOURCES
+ radio/spotifyconfig.cpp
+ radio/spotifyserver.cpp
+ radio/spotifyservice.cpp
+ )
+ list(APPEND HEADERS
+ radio/spotifyconfig.h
+ radio/spotifyserver.h
+ radio/spotifyservice.h
+ )
+ list(APPEND UI
+ radio/spotifyconfig.ui)
+endif(HAVE_SPOTIFY)
+
if(APPLE)
list(APPEND HEADERS core/macglobalshortcutbackend.h)
list(APPEND HEADERS devices/macdevicelister.h)
@@ -995,6 +1010,10 @@ if(HAVE_BREAKPAD)
endif (LINUX)
endif(HAVE_BREAKPAD)
+if(HAVE_SPOTIFY)
+ target_link_libraries(clementine_lib clementine-spotifyblob-messages)
+endif(HAVE_SPOTIFY)
+
if (APPLE)
target_link_libraries(clementine_lib
${GROWL}
diff --git a/src/config.h.in b/src/config.h.in
index c7c765f59..3eba0d2e3 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -33,6 +33,7 @@
#cmakedefine HAVE_REMOTE
#cmakedefine HAVE_SCRIPTING_PYTHON
#cmakedefine HAVE_SPARKLE
+#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_STATIC_SQLITE
#cmakedefine HAVE_WIIMOTEDEV
#cmakedefine LEOPARD
diff --git a/src/core/logging.cpp b/src/core/logging.cpp
index 48d8b6370..a5be61eae 100644
--- a/src/core/logging.cpp
+++ b/src/core/logging.cpp
@@ -1,18 +1,17 @@
/* This file is part of Clementine.
- Copyright 2010, David Sansome
+ 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.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- 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.
+ http://www.apache.org/licenses/LICENSE-2.0
- You should have received a copy of the GNU General Public License
- along with Clementine. If not, see .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
*/
#include
@@ -37,8 +36,7 @@ static const int kMessageHandlerMagicLength = strlen(kMessageHandlerMagic);
static QtMsgHandler sOriginalMessageHandler = NULL;
-static void GLog(const gchar* domain, GLogLevelFlags level,
- const gchar* message, gpointer user_data) {
+void GLog(const char* domain, int level, const char* message, void* user_data) {
switch (level) {
case G_LOG_FLAG_RECURSION:
case G_LOG_FLAG_FATAL:
@@ -84,9 +82,6 @@ void Init() {
sClassLevels = new QMap();
sNullDevice = new NullDevice;
- // Glib integration
- g_log_set_default_handler(&GLog, NULL);
-
// Catch other messages from Qt
if (!sOriginalMessageHandler) {
sOriginalMessageHandler = qInstallMsgHandler(MessageHandler);
diff --git a/src/core/logging.h b/src/core/logging.h
index 80705cb04..66dc3e2db 100644
--- a/src/core/logging.h
+++ b/src/core/logging.h
@@ -1,18 +1,17 @@
/* This file is part of Clementine.
- Copyright 2010, David Sansome
+ 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.
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- 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.
+ http://www.apache.org/licenses/LICENSE-2.0
- You should have received a copy of the GNU General Public License
- along with Clementine. If not, see .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
*/
#ifndef LOGGING_H
@@ -46,6 +45,8 @@ namespace logging {
QDebug CreateLogger(Level level, const char* pretty_function, int line);
+ void GLog(const char* domain, int level, const char* message, void* user_data);
+
extern const char* kDefaultLogLevels;
}
diff --git a/src/main.cpp b/src/main.cpp
index 85a86dde1..85466b047 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -314,6 +314,7 @@ int main(int argc, char *argv[]) {
// Initialise logging
logging::Init();
logging::SetLevels(options.log_levels());
+ g_log_set_default_handler(reinterpret_cast(&logging::GLog), NULL);
QtSingleApplication a(argc, argv);
#ifdef Q_OS_DARWIN
diff --git a/src/radio/radiomodel.cpp b/src/radio/radiomodel.cpp
index f2149ca1b..4e13a701c 100644
--- a/src/radio/radiomodel.cpp
+++ b/src/radio/radiomodel.cpp
@@ -29,6 +29,9 @@
#ifdef HAVE_LIBLASTFM
#include "lastfmservice.h"
#endif
+#ifdef HAVE_SPOTIFY
+ #include "spotifyservice.h"
+#endif
#include
#include
@@ -51,6 +54,9 @@ RadioModel::RadioModel(BackgroundThread* db_thread,
#ifdef HAVE_LIBLASTFM
AddService(new LastFMService(this));
+#endif
+#ifdef HAVE_SPOTIFY
+ AddService(new SpotifyService(this));
#endif
AddService(new SomaFMService(this));
AddService(new MagnatuneService(this));
@@ -70,6 +76,7 @@ void RadioModel::AddService(RadioService *service) {
root->setData(QVariant::fromValue(service), Role_Service);
invisibleRootItem()->appendRow(root);
+ qDebug() << "Adding:" << service->name();
sServices->insert(service->name(), service);
connect(service, SIGNAL(AsyncLoadFinished(PlaylistItem::SpecialLoadResult)), SIGNAL(AsyncLoadFinished(PlaylistItem::SpecialLoadResult)));
diff --git a/src/radio/spotifyconfig.cpp b/src/radio/spotifyconfig.cpp
new file mode 100644
index 000000000..c0225f687
--- /dev/null
+++ b/src/radio/spotifyconfig.cpp
@@ -0,0 +1,80 @@
+/* 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 "spotifyconfig.h"
+
+#include "core/network.h"
+#include "spotifyservice.h"
+#include "radiomodel.h"
+#include "ui_spotifyconfig.h"
+
+#include
+#include
+#include
+#include
+#include
+
+SpotifyConfig::SpotifyConfig(QWidget *parent)
+ : QWidget(parent),
+ network_(new NetworkAccessManager(this)),
+ ui_(new Ui_SpotifyConfig),
+ service_(RadioModel::Service()),
+ needs_validation_(true)
+{
+ ui_->setupUi(this);
+ ui_->busy->hide();
+ connect(service_, SIGNAL(LoginFinished(bool)), SLOT(LoginFinished(bool)));
+}
+
+SpotifyConfig::~SpotifyConfig() {
+ delete ui_;
+}
+
+bool SpotifyConfig::NeedsValidation() const {
+ // FIXME
+ return needs_validation_;
+}
+
+void SpotifyConfig::Validate() {
+ ui_->busy->show();
+ service_->Login(ui_->username->text(), ui_->password->text());
+}
+
+void SpotifyConfig::Load() {
+ QSettings s;
+ s.beginGroup(SpotifyService::kSettingsGroup);
+
+ ui_->username->setText(s.value("username").toString());
+ ui_->password->setText(s.value("password").toString());
+}
+
+void SpotifyConfig::Save() {
+ QSettings s;
+ s.beginGroup(SpotifyService::kSettingsGroup);
+
+ s.setValue("username", ui_->username->text());
+ s.setValue("password", ui_->password->text());
+
+ RadioModel::Service()->ReloadSettings();
+}
+
+void SpotifyConfig::LoginFinished(bool success) {
+ qDebug() << Q_FUNC_INFO << success;
+ needs_validation_ = !success;
+ ui_->busy->hide();
+ emit ValidationComplete(success);
+}
diff --git a/src/radio/spotifyconfig.h b/src/radio/spotifyconfig.h
new file mode 100644
index 000000000..0bbb3f8c4
--- /dev/null
+++ b/src/radio/spotifyconfig.h
@@ -0,0 +1,57 @@
+/* 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 SPOTIFYCONFIG_H
+#define SPOTIFYCONFIG_H
+
+#include
+
+class QAuthenticator;
+class QNetworkReply;
+
+class NetworkAccessManager;
+class Ui_SpotifyConfig;
+class SpotifyService;
+
+class SpotifyConfig : public QWidget {
+ Q_OBJECT
+public:
+ SpotifyConfig(QWidget* parent = 0);
+ ~SpotifyConfig();
+
+ bool NeedsValidation() const;
+ void Validate();
+
+signals:
+ void ValidationComplete(bool success);
+
+public slots:
+ void Load();
+ void Save();
+
+private slots:
+ void LoginFinished(bool success);
+
+private:
+ NetworkAccessManager* network_;
+ Ui_SpotifyConfig* ui_;
+ SpotifyService* service_;
+
+ bool needs_validation_;
+};
+
+#endif // SPOTIFYCONFIG_H
diff --git a/src/radio/spotifyconfig.ui b/src/radio/spotifyconfig.ui
new file mode 100644
index 000000000..3631f903a
--- /dev/null
+++ b/src/radio/spotifyconfig.ui
@@ -0,0 +1,125 @@
+
+
+ SpotifyConfig
+
+
+
+ 0
+ 0
+ 448
+ 310
+
+
+
+ Form
+
+
+ -
+
+
+ Account details
+
+
+
-
+
+
+ A Spotify Premium account is required.
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+
+
-
+
+
+ Username
+
+
+
+ -
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+ -
+
+
+
+ 0
+
+
-
+
+
+ Authenticating...
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ BusyIndicator
+ QLabel
+
+
+
+
+
+
diff --git a/src/radio/spotifyserver.cpp b/src/radio/spotifyserver.cpp
new file mode 100644
index 000000000..296ad8d8c
--- /dev/null
+++ b/src/radio/spotifyserver.cpp
@@ -0,0 +1,108 @@
+/* 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 "spotifyserver.h"
+#include "core/logging.h"
+
+#include "spotifyblob/messages.pb.h"
+
+#include
+#include
+
+#include
+
+SpotifyServer::SpotifyServer(QObject* parent)
+ : QObject(parent),
+ server_(new QTcpServer(this)),
+ protocol_socket_(NULL)
+{
+ connect(server_, SIGNAL(newConnection()), SLOT(NewConnection()));
+}
+
+void SpotifyServer::Init() {
+ if (!server_->listen(QHostAddress::LocalHost)) {
+ qLog(Error) << "Couldn't open server socket" << server_->errorString();
+ }
+}
+
+int SpotifyServer::server_port() const {
+ return server_->serverPort();
+}
+
+void SpotifyServer::NewConnection() {
+ protocol_socket_ = server_->nextPendingConnection();
+ connect(protocol_socket_, SIGNAL(readyRead()), SLOT(ProtocolSocketReadyRead()));
+
+ qLog(Info) << "Connection from port" << protocol_socket_->peerPort();
+
+ emit ClientConnected();
+}
+
+void SpotifyServer::Login(const QString& username, const QString& password) {
+ const QByteArray username_bytes(username.toUtf8());
+ const QByteArray password_bytes(password.toUtf8());
+
+ RequestMessage message;
+
+ LoginRequest* request = message.mutable_login_request();
+ request->set_username(username_bytes.constData(), username_bytes.length());
+ request->set_password(password_bytes.constData(), password_bytes.length());
+
+ SendMessage(message);
+}
+
+void SpotifyServer::SendMessage(const RequestMessage& message) {
+ qLog(Debug) << message.DebugString().c_str();
+
+ std::string data(message.SerializeAsString());
+
+ QDataStream s(protocol_socket_);
+ s << quint32(data.length());
+ s.writeRawData(data.data(), data.length());
+}
+
+void SpotifyServer::ProtocolSocketReadyRead() {
+ QDataStream s(protocol_socket_);
+
+ quint32 length = 0;
+ s >> length;
+
+ if (length == 0) {
+ return;
+ }
+
+ boost::scoped_array data(new char[length]);
+ s.readRawData(data.get(), length);
+
+ ResponseMessage message;
+ if (!message.ParseFromArray(data.get(), length)) {
+ qLog(Error) << "Malformed protobuf message";
+ protocol_socket_->deleteLater();
+ protocol_socket_ = NULL;
+ return;
+ }
+
+ qLog(Debug) << message.DebugString().c_str();
+
+ if (message.has_login_response()) {
+ const LoginResponse& response = message.login_response();
+ if (!response.success()) {
+ qLog(Info) << QString::fromUtf8(response.error().data(), response.error().size());
+ }
+ emit LoginCompleted(response.success());
+ }
+}
diff --git a/src/radio/spotifyserver.h b/src/radio/spotifyserver.h
new file mode 100644
index 000000000..d901207ea
--- /dev/null
+++ b/src/radio/spotifyserver.h
@@ -0,0 +1,54 @@
+/* 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 SPOTIFYSERVER_H
+#define SPOTIFYSERVER_H
+
+#include
+
+class RequestMessage;
+
+class QTcpServer;
+class QTcpSocket;
+
+class SpotifyServer : public QObject {
+ Q_OBJECT
+
+public:
+ SpotifyServer(QObject* parent = 0);
+
+ void Init();
+ void Login(const QString& username, const QString& password);
+
+ int server_port() const;
+
+signals:
+ void ClientConnected();
+ void LoginCompleted(bool success);
+
+private slots:
+ void NewConnection();
+ void ProtocolSocketReadyRead();
+
+private:
+ void SendMessage(const RequestMessage& message);
+
+ QTcpServer* server_;
+ QTcpSocket* protocol_socket_;
+};
+
+#endif // SPOTIFYSERVER_H
diff --git a/src/radio/spotifyservice.cpp b/src/radio/spotifyservice.cpp
new file mode 100644
index 000000000..7fbcd4705
--- /dev/null
+++ b/src/radio/spotifyservice.cpp
@@ -0,0 +1,72 @@
+#include "core/logging.h"
+#include "radiomodel.h"
+#include "spotifyserver.h"
+#include "spotifyservice.h"
+
+#include
+#include
+
+const char* SpotifyService::kServiceName = "Spotify";
+const char* SpotifyService::kSettingsGroup = "Spotify";
+
+SpotifyService::SpotifyService(RadioModel* parent)
+ : RadioService(kServiceName, parent),
+ server_(NULL),
+ blob_process_(NULL) {
+ blob_path_ = QCoreApplication::applicationFilePath() + "-spotifyblob";
+}
+
+SpotifyService::~SpotifyService() {
+}
+
+QStandardItem* SpotifyService::CreateRootItem() {
+ QStandardItem* item = new QStandardItem(QIcon(":icons/svg/spotify.svg"), kServiceName);
+ item->setData(true, RadioModel::Role_CanLazyLoad);
+ return item;
+}
+
+void SpotifyService::LazyPopulate(QStandardItem* parent) {
+ return;
+}
+
+QModelIndex SpotifyService::GetCurrentIndex() {
+ return QModelIndex();
+}
+
+void SpotifyService::Login(const QString& username, const QString& password) {
+ qLog(Debug) << Q_FUNC_INFO;
+ delete server_;
+ delete blob_process_;
+
+ server_ = new SpotifyServer(this);
+ blob_process_ = new QProcess(this);
+ blob_process_->setProcessChannelMode(QProcess::ForwardedChannels);
+
+ connect(server_, SIGNAL(ClientConnected()), SLOT(ClientConnected()));
+ connect(server_, SIGNAL(LoginCompleted(bool)), SLOT(LoginCompleted(bool)));
+
+ connect(blob_process_,
+ SIGNAL(error(QProcess::ProcessError)),
+ SLOT(BlobProcessError(QProcess::ProcessError)));
+
+ pending_username_ = username;
+ pending_password_ = password;
+
+ server_->Init();
+ blob_process_->start(
+ blob_path_, QStringList() << QString::number(server_->server_port()));
+}
+
+void SpotifyService::ClientConnected() {
+ qLog(Debug) << "ClientConnected";
+
+ server_->Login(pending_username_, pending_password_);
+}
+
+void SpotifyService::LoginCompleted(bool success) {
+ emit LoginFinished(success);
+}
+
+void SpotifyService::BlobProcessError(QProcess::ProcessError error) {
+ qLog(Error) << "Failed to start blob process:" << error;
+}
diff --git a/src/radio/spotifyservice.h b/src/radio/spotifyservice.h
new file mode 100644
index 000000000..a790e7b26
--- /dev/null
+++ b/src/radio/spotifyservice.h
@@ -0,0 +1,48 @@
+#ifndef SPOTIFYSERVICE_H
+#define SPOTIFYSERVICE_H
+
+#include "radioservice.h"
+
+#include
+#include
+
+class SpotifyServer;
+
+class SpotifyService : public RadioService {
+ Q_OBJECT
+
+public:
+ explicit SpotifyService(RadioModel* parent);
+ virtual ~SpotifyService();
+
+ virtual QStandardItem* CreateRootItem();
+ virtual void LazyPopulate(QStandardItem* parent);
+
+ void Login(const QString& username, const QString& password);
+ void Search(const QString& query);
+
+ static const char* kServiceName;
+ static const char* kSettingsGroup;
+
+signals:
+ void LoginFinished(bool success);
+
+protected:
+ virtual QModelIndex GetCurrentIndex();
+
+private slots:
+ void ClientConnected();
+ void LoginCompleted(bool success);
+ void BlobProcessError(QProcess::ProcessError error);
+
+private:
+ SpotifyServer* server_;
+
+ QString blob_path_;
+ QProcess* blob_process_;
+
+ QString pending_username_;
+ QString pending_password_;
+};
+
+#endif
diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp
index b40a01f6e..8f72826d1 100644
--- a/src/ui/settingsdialog.cpp
+++ b/src/ui/settingsdialog.cpp
@@ -45,6 +45,10 @@
# include "remote/remoteconfig.h"
#endif
+#ifdef HAVE_SPOTIFY
+# include "radio/spotifyconfig.h"
+#endif
+
#include
#include
#include
@@ -118,6 +122,21 @@ SettingsDialog::SettingsDialog(BackgroundStreams* streams, QWidget* parent)
connect(lastfm_config_, SIGNAL(ValidationComplete(bool)), SLOT(ValidationComplete(bool)));
#endif
+#ifdef HAVE_SPOTIFY
+ ui_->list->insertItem(Page_Spotify, tr("Spotify"));
+ ui_->list->item(Page_Spotify)->setIcon(QIcon(":/icons/svg/spotify.svg"));
+
+ QWidget* spotify_page = new QWidget;
+ QVBoxLayout* spotify_layout = new QVBoxLayout;
+ spotify_layout->setContentsMargins(0, 0, 0, 0);
+ spotify_config_ = new SpotifyConfig;
+ spotify_layout->addWidget(spotify_config_);
+ spotify_page->setLayout(spotify_layout);
+
+ ui_->stacked_widget->insertWidget(Page_Spotify, spotify_page);
+ connect(spotify_config_, SIGNAL(ValidationComplete(bool)), SLOT(ValidationComplete(bool)));
+#endif
+
#ifdef HAVE_REMOTE
ui_->list->insertItem(Page_Remote, tr("Remote Control"));
ui_->list->item(Page_Remote)->setIcon(IconLoader::Load("network-server"));
@@ -312,6 +331,16 @@ void SettingsDialog::accept() {
}
#endif
+#ifdef HAVE_SPOTIFY
+ if (spotify_config_->NeedsValidation()) {
+ spotify_config_->Validate();
+ ui_->buttonBox->setEnabled(false);
+ return;
+ } else {
+ spotify_config_->Save();
+ }
+#endif
+
if (ui_->magnatune->NeedsValidation()) {
ui_->magnatune->Validate();
ui_->buttonBox->setEnabled(false);
@@ -524,6 +553,10 @@ void SettingsDialog::showEvent(QShowEvent*) {
remote_config_->Load();
#endif
+#ifdef HAVE_SPOTIFY
+ spotify_config_->Load();
+#endif
+
// Magnatune
ui_->magnatune->Load();
diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h
index 32840fcd3..6da02d46c 100644
--- a/src/ui/settingsdialog.h
+++ b/src/ui/settingsdialog.h
@@ -44,6 +44,9 @@ class Ui_SettingsDialog;
#ifdef HAVE_REMOTE
class RemoteConfig;
#endif
+#ifdef HAVE_SPOTIFY
+ class SpotifyConfig;
+#endif
class GstEngine;
@@ -63,6 +66,9 @@ class SettingsDialog : public QDialog {
Page_Library,
#ifdef HAVE_LIBLASTFM
Page_Lastfm,
+#endif
+#ifdef HAVE_SPOTIFY
+ Page_Spotify,
#endif
Page_Magnatune,
Page_BackgroundStreams,
@@ -126,6 +132,9 @@ class SettingsDialog : public QDialog {
#endif
#ifdef HAVE_REMOTE
RemoteConfig* remote_config_;
+#endif
+#ifdef HAVE_SPOTIFY
+ SpotifyConfig* spotify_config_;
#endif
const GstEngine* gst_engine_;