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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + 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 +
widgets/busyindicator.h
+
+
+ + +
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_;