From a551c40c4eaa4c0b603bd4436f762ef33db968ad Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 1 Sep 2022 21:48:13 +0100 Subject: [PATCH] Remove Spotify playback support libspotify is dead --- CMakeLists.txt | 35 - dist/windows/clementine.nsi.in | 2 - ext/clementine-spotifyblob/CMakeLists.txt | 79 -- ext/clementine-spotifyblob/main.cpp | 49 - ext/clementine-spotifyblob/mediapipeline.cpp | 169 --- ext/clementine-spotifyblob/mediapipeline.h | 62 - .../spotify_utilities.cpp | 66 - .../spotify_utilities.h | 37 - .../spotify_utilities.mm | 44 - ext/clementine-spotifyblob/spotifyclient.cpp | 1149 ----------------- ext/clementine-spotifyblob/spotifyclient.h | 204 --- ext/clementine-spotifyblob/spotifykey.h | 37 - ext/libclementine-spotifyblob/CMakeLists.txt | 18 - .../blobversion.h.in | 23 - .../spotifymessages.proto | 236 ---- src/CMakeLists.txt | 49 - src/covers/albumcoverloader.cpp | 41 +- src/covers/albumcoverloader.h | 3 - src/engines/gstenginepipeline.cpp | 84 +- src/globalsearch/spotifysearchprovider.cpp | 280 ---- src/globalsearch/spotifysearchprovider.h | 67 - .../spotifywebapisearchprovider.cpp | 97 -- .../spotifywebapisearchprovider.h | 48 - src/internet/core/internetmodel.cpp | 8 - .../spotify/spotifyblobdownloader.cpp | 281 ---- src/internet/spotify/spotifyblobdownloader.h | 70 - src/internet/spotify/spotifyserver.cpp | 321 ----- src/internet/spotify/spotifyserver.h | 112 -- src/internet/spotify/spotifyservice.cpp | 993 -------------- src/internet/spotify/spotifyservice.h | 195 --- src/internet/spotify/spotifysettingspage.cpp | 176 --- src/internet/spotify/spotifysettingspage.h | 60 - src/internet/spotify/spotifysettingspage.ui | 211 --- .../spotifywebapi/spotifywebapiservice.cpp | 198 --- .../spotifywebapi/spotifywebapiservice.h | 60 - 35 files changed, 9 insertions(+), 5555 deletions(-) delete mode 100644 ext/clementine-spotifyblob/CMakeLists.txt delete mode 100644 ext/clementine-spotifyblob/main.cpp delete mode 100644 ext/clementine-spotifyblob/mediapipeline.cpp delete mode 100644 ext/clementine-spotifyblob/mediapipeline.h delete mode 100644 ext/clementine-spotifyblob/spotify_utilities.cpp delete mode 100644 ext/clementine-spotifyblob/spotify_utilities.h delete mode 100644 ext/clementine-spotifyblob/spotify_utilities.mm delete mode 100644 ext/clementine-spotifyblob/spotifyclient.cpp delete mode 100644 ext/clementine-spotifyblob/spotifyclient.h delete mode 100644 ext/clementine-spotifyblob/spotifykey.h delete mode 100644 ext/libclementine-spotifyblob/CMakeLists.txt delete mode 100644 ext/libclementine-spotifyblob/blobversion.h.in delete mode 100644 ext/libclementine-spotifyblob/spotifymessages.proto delete mode 100644 src/globalsearch/spotifysearchprovider.cpp delete mode 100644 src/globalsearch/spotifysearchprovider.h delete mode 100644 src/globalsearch/spotifywebapisearchprovider.cpp delete mode 100644 src/globalsearch/spotifywebapisearchprovider.h delete mode 100644 src/internet/spotify/spotifyblobdownloader.cpp delete mode 100644 src/internet/spotify/spotifyblobdownloader.h delete mode 100644 src/internet/spotify/spotifyserver.cpp delete mode 100644 src/internet/spotify/spotifyserver.h delete mode 100644 src/internet/spotify/spotifyservice.cpp delete mode 100644 src/internet/spotify/spotifyservice.h delete mode 100644 src/internet/spotify/spotifysettingspage.cpp delete mode 100644 src/internet/spotify/spotifysettingspage.h delete mode 100644 src/internet/spotify/spotifysettingspage.ui delete mode 100644 src/internet/spotifywebapi/spotifywebapiservice.cpp delete mode 100644 src/internet/spotifywebapi/spotifywebapiservice.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b2d702707..eb50b239f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,6 @@ find_library(PROTOBUF_STATIC_LIBRARY libprotobuf.a libprotobuf) pkg_check_modules(CDIO libcdio) pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint) -pkg_search_module(CRYPTOPP cryptopp libcrypto++) pkg_check_modules(GIO gio-2.0) pkg_check_modules(GLIB REQUIRED glib-2.0) pkg_check_modules(GOBJECT REQUIRED gobject-2.0) @@ -75,7 +74,6 @@ pkg_check_modules(LIBMTP libmtp>=1.0) pkg_check_modules(LIBMYGPO_QT5 libmygpo-qt5>=1.0.9) pkg_check_modules(LIBPULSE libpulse) pkg_check_modules(LIBXML libxml-2.0) -pkg_check_modules(LIBSPOTIFY libspotify>=12.1.45) pkg_check_modules(TAGLIB taglib) if (WIN32) @@ -167,12 +165,6 @@ endif() if (APPLE) find_library(SPARKLE Sparkle) - find_library(LIBSPOTIFY libspotify) - - if(LIBSPOTIFY_FOUND) - set(LIBSPOTIFY_INCLUDE_DIRS ${LIBSPOTIFY}) - set(LIBSPOTIFY_LIBRARIES ${LIBSPOTIFY}) - endif(LIBSPOTIFY_FOUND) add_subdirectory(3rdparty/SPMediaKeyTap) set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap) @@ -296,19 +288,6 @@ optional_component(UDISKS2 ON "Devices: UDisks2 backend" DEPENDS "D-Bus support" Qt5DBus_FOUND ) -optional_component(SPOTIFY_BLOB ON "Spotify support: non-GPL binary helper" - DEPENDS "protobuf" PROTOBUF_FOUND PROTOBUF_PROTOC_EXECUTABLE - DEPENDS "libspotify" LIBSPOTIFY_FOUND -) - -if (CRYPTOPP_FOUND OR HAVE_SPOTIFY_BLOB) - set(CRYPTOPP_OR_HAVE_SPOTIFY_BLOB ON) -endif() - -optional_component(SPOTIFY ON "Spotify support" - DEPENDS "cryptopp or spotify blob" CRYPTOPP_OR_HAVE_SPOTIFY_BLOB -) - optional_component(MOODBAR ON "Moodbar support" DEPENDS "fftw3" FFTW3_FOUND ) @@ -340,13 +319,6 @@ if (APPLE AND USE_BUNDLE AND NOT USE_BUNDLE_DIR) set(USE_BUNDLE_DIR "../PlugIns") endif() -if(CRYPTOPP_FOUND) - set(HAVE_CRYPTOPP ON) - if(HAVE_SPOTIFY) - set(HAVE_SPOTIFY_DOWNLOADER ON) - endif(HAVE_SPOTIFY) -endif(CRYPTOPP_FOUND) - # Remove GLU and GL from the link line - they're not really required # and don't exist on my mingw toolchain list(REMOVE_ITEM QT_LIBRARIES "-lGLU -lGL") @@ -454,9 +426,6 @@ add_subdirectory(ext/libclementine-common) add_subdirectory(ext/libclementine-tagreader) add_subdirectory(ext/clementine-tagreader) add_subdirectory(ext/libclementine-remote) -if(HAVE_SPOTIFY) - add_subdirectory(ext/libclementine-spotifyblob) -endif(HAVE_SPOTIFY) option(WITH_DEBIAN OFF) if(WITH_DEBIAN) @@ -467,10 +436,6 @@ if(HAVE_BREAKPAD) add_subdirectory(3rdparty/google-breakpad) endif(HAVE_BREAKPAD) -if(HAVE_SPOTIFY_BLOB) - add_subdirectory(ext/clementine-spotifyblob) -endif(HAVE_SPOTIFY_BLOB) - if(HAVE_MOODBAR) add_subdirectory(gst/moodbar) endif() diff --git a/dist/windows/clementine.nsi.in b/dist/windows/clementine.nsi.in index 90c5dfb7f..bda23e6ae 100644 --- a/dist/windows/clementine.nsi.in +++ b/dist/windows/clementine.nsi.in @@ -302,7 +302,6 @@ Section "Clementine" Clementine File "clementine.exe" File "clementine-tagreader.exe" - File "clementine-spotifyblob.exe" File "clementine.ico" File "glew32.dll" File "libcdio-19.dll" @@ -355,7 +354,6 @@ Section "Clementine" Clementine File "libpsl-5.dll" File "libsoup-2.4-1.dll" File "libspeex-1.dll" - File "libspotify.dll" File "libssl-1_1.dll" File "libsqlite3-0.dll" File "libstdc++-6.dll" diff --git a/ext/clementine-spotifyblob/CMakeLists.txt b/ext/clementine-spotifyblob/CMakeLists.txt deleted file mode 100644 index e00aa9ebb..000000000 --- a/ext/clementine-spotifyblob/CMakeLists.txt +++ /dev/null @@ -1,79 +0,0 @@ -include_directories(${LIBSPOTIFY_INCLUDE_DIRS}) -include_directories(${PROTOBUF_INCLUDE_DIRS}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob) -include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob) -include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common) - -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual -Wall -Wno-sign-compare -Wno-deprecated-declarations -Wno-unused-local-typedefs -Wno-unused-private-field -Wno-unknown-warning-option") - -link_directories(${LIBSPOTIFY_LIBRARY_DIRS}) - -set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}) - -set(SOURCES - main.cpp - mediapipeline.cpp - spotifyclient.cpp - spotify_utilities.cpp -) - -set(HEADERS - spotifyclient.h -) - -if(APPLE) - list(APPEND SOURCES spotify_utilities.mm) -endif(APPLE) - -qt5_wrap_cpp(MOC ${HEADERS}) - -if(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE) - set(win32_build_flag WIN32) -endif(WIN32 AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT ENABLE_WIN32_CONSOLE) - -add_executable(clementine-spotifyblob - ${win32_build_flag} - ${SOURCES} - ${MOC} -) - -target_link_libraries(clementine-spotifyblob - ${LIBSPOTIFY_LIBRARIES} ${LIBSPOTIFY_LDFLAGS} - ${QT_QTCORE_LIBRARY} - ${QT_QTNETWORK_LIBRARY} - ${GSTREAMER_BASE_LIBRARIES} - ${GSTREAMER_APP_LIBRARIES} - ${PROTOBUF_STATIC_LIBRARY} - clementine-spotifyblob-messages - libclementine-common -) - -if(APPLE) - target_link_libraries(clementine-spotifyblob - "-framework Foundation" - ) -endif(APPLE) - -if(NOT APPLE) - # macdeploy.py takes care of this on mac - install(TARGETS clementine-spotifyblob - RUNTIME DESTINATION bin - ) -endif(NOT APPLE) - -if(LINUX) - # Versioned name of the blob - if(CMAKE_SIZEOF_VOID_P EQUAL 4) - set(SPOTIFY_BLOB_ARCH 32) - else(CMAKE_SIZEOF_VOID_P EQUAL 4) - set(SPOTIFY_BLOB_ARCH 64) - endif(CMAKE_SIZEOF_VOID_P EQUAL 4) - - install( - FILES ${CMAKE_BINARY_DIR}/clementine-spotifyblob - DESTINATION ${CMAKE_BINARY_DIR}/spotify/version${SPOTIFY_BLOB_VERSION}-${SPOTIFY_BLOB_ARCH}bit/ - RENAME blob - ) -endif(LINUX) diff --git a/ext/clementine-spotifyblob/main.cpp b/ext/clementine-spotifyblob/main.cpp deleted file mode 100644 index 1c40520b6..000000000 --- a/ext/clementine-spotifyblob/main.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#include - -#include -#include - -#include "core/logging.h" -#include "spotifyclient.h" - -int main(int argc, char** argv) { - QCoreApplication a(argc, argv); - QCoreApplication::setApplicationName("Clementine"); - QCoreApplication::setOrganizationName("Clementine"); - QCoreApplication::setOrganizationDomain("clementine-player.org"); - - logging::Init(); - - gst_init(nullptr, nullptr); - - const QStringList arguments(a.arguments()); - - if (arguments.length() != 2) { - qFatal("Usage: %s port", argv[0]); - } - - SpotifyClient client; - client.Init(arguments[1].toInt()); - - return a.exec(); -} diff --git a/ext/clementine-spotifyblob/mediapipeline.cpp b/ext/clementine-spotifyblob/mediapipeline.cpp deleted file mode 100644 index 58b3c388c..000000000 --- a/ext/clementine-spotifyblob/mediapipeline.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#include "mediapipeline.h" - -#include - -#include "core/logging.h" -#include "core/timeconstants.h" - -MediaPipeline::MediaPipeline(int port, quint64 length_msec) - : port_(port), - length_msec_(length_msec), - accepting_data_(true), - pipeline_(nullptr), - appsrc_(nullptr), - byte_rate_(1), - offset_bytes_(0) {} - -MediaPipeline::~MediaPipeline() { - if (pipeline_) { - gst_element_set_state(pipeline_, GST_STATE_NULL); - gst_object_unref(GST_OBJECT(pipeline_)); - } -} - -bool MediaPipeline::Init(int sample_rate, int channels) { - if (is_initialised()) return false; - - pipeline_ = gst_pipeline_new("pipeline"); - - // Create elements - appsrc_ = GST_APP_SRC(gst_element_factory_make("appsrc", nullptr)); - GstElement* gdppay = gst_element_factory_make("gdppay", nullptr); - tcpsink_ = gst_element_factory_make("tcpclientsink", nullptr); - - if (!pipeline_ || !appsrc_ || !tcpsink_) { - if (pipeline_) { - gst_object_unref(GST_OBJECT(pipeline_)); - pipeline_ = nullptr; - } - if (appsrc_) { - gst_object_unref(GST_OBJECT(appsrc_)); - appsrc_ = nullptr; - } - if (gdppay) { - gst_object_unref(GST_OBJECT(gdppay)); - } - if (tcpsink_) { - gst_object_unref(GST_OBJECT(tcpsink_)); - tcpsink_ = nullptr; - } - return false; - } - - // Add elements to the pipeline and link them - gst_bin_add(GST_BIN(pipeline_), GST_ELEMENT(appsrc_)); - gst_bin_add(GST_BIN(pipeline_), gdppay); - gst_bin_add(GST_BIN(pipeline_), tcpsink_); - gst_element_link_many(GST_ELEMENT(appsrc_), gdppay, tcpsink_, nullptr); - - // Set the sink's port - g_object_set(G_OBJECT(tcpsink_), "host", "127.0.0.1", nullptr); - g_object_set(G_OBJECT(tcpsink_), "port", port_, nullptr); - - // Try to send 5 seconds of audio in advance to initially fill Clementine's - // buffer. - g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec), - nullptr); - - // We know the time of each buffer - g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr); - - // Spotify only pushes data to us every 100ms, so keep the appsrc half full - // to prevent tiny stalls. - g_object_set(G_OBJECT(appsrc_), "min-percent", 50, nullptr); - - // Set callbacks for when to start/stop pushing data - GstAppSrcCallbacks callbacks; - callbacks.enough_data = EnoughDataCallback; - callbacks.need_data = NeedDataCallback; - callbacks.seek_data = SeekDataCallback; - - gst_app_src_set_callbacks(appsrc_, &callbacks, this, nullptr); - -#if Q_BYTE_ORDER == Q_BIG_ENDIAN - static const char* format = "S16BE"; -#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN - static const char* format = "S16LE"; -#endif - - // Set caps - GstCaps* caps = gst_caps_new_simple( - "audio/x-raw", "format", G_TYPE_STRING, format, "rate", G_TYPE_INT, - sample_rate, "channels", G_TYPE_INT, channels, "layout", G_TYPE_STRING, - "interleaved", nullptr); - - gst_app_src_set_caps(appsrc_, caps); - gst_caps_unref(caps); - - // Set size - byte_rate_ = quint64(sample_rate) * channels * 2; - const quint64 bytes = byte_rate_ * length_msec_ / 1000; - gst_app_src_set_size(appsrc_, bytes); - - // Ready to go - return gst_element_set_state(pipeline_, GST_STATE_PLAYING) != - GST_STATE_CHANGE_FAILURE; -} - -void MediaPipeline::WriteData(const char* data, qint64 length) { - if (!is_initialised()) return; - - GstBuffer* buffer = gst_buffer_new_allocate(nullptr, length, nullptr); - GstMapInfo map_info; - gst_buffer_map(buffer, &map_info, GST_MAP_WRITE); - - memcpy(map_info.data, data, length); - - gst_buffer_unmap(buffer, &map_info); - - GST_BUFFER_PTS(buffer) = offset_bytes_ * kNsecPerSec / byte_rate_; - GST_BUFFER_DURATION(buffer) = length * kNsecPerSec / byte_rate_; - - offset_bytes_ += length; - - gst_app_src_push_buffer(appsrc_, buffer); -} - -void MediaPipeline::EndStream() { - if (!is_initialised()) return; - - gst_app_src_end_of_stream(appsrc_); -} - -void MediaPipeline::NeedDataCallback(GstAppSrc* src, guint length, void* data) { - MediaPipeline* me = reinterpret_cast(data); - me->accepting_data_ = true; -} - -void MediaPipeline::EnoughDataCallback(GstAppSrc* src, void* data) { - MediaPipeline* me = reinterpret_cast(data); - me->accepting_data_ = false; -} - -gboolean MediaPipeline::SeekDataCallback(GstAppSrc* src, guint64 offset, - void* data) { - // MediaPipeline* me = reinterpret_cast(data); - - qLog(Debug) << "Gstreamer wants seek to" << offset; - return false; -} diff --git a/ext/clementine-spotifyblob/mediapipeline.h b/ext/clementine-spotifyblob/mediapipeline.h deleted file mode 100644 index 32c9b0420..000000000 --- a/ext/clementine-spotifyblob/mediapipeline.h +++ /dev/null @@ -1,62 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#ifndef MEDIAPIPELINE_H -#define MEDIAPIPELINE_H - -#include - -#include -#include - -class MediaPipeline { - public: - MediaPipeline(int port, quint64 length_msec); - ~MediaPipeline(); - - bool is_initialised() const { return pipeline_; } - bool is_accepting_data() const { return accepting_data_; } - bool Init(int sample_rate, int channels); - - void WriteData(const char* data, qint64 length); - void EndStream(); - - private: - static void NeedDataCallback(GstAppSrc* src, guint length, void* data); - static void EnoughDataCallback(GstAppSrc* src, void* data); - static gboolean SeekDataCallback(GstAppSrc* src, guint64 offset, void* data); - - private: - Q_DISABLE_COPY(MediaPipeline) - - const int port_; - const quint64 length_msec_; - - bool accepting_data_; - - GstElement* pipeline_; - GstAppSrc* appsrc_; - GstElement* tcpsink_; - - quint64 byte_rate_; - quint64 offset_bytes_; -}; - -#endif // MEDIAPIPELINE_H diff --git a/ext/clementine-spotifyblob/spotify_utilities.cpp b/ext/clementine-spotifyblob/spotify_utilities.cpp deleted file mode 100644 index cfc077b96..000000000 --- a/ext/clementine-spotifyblob/spotify_utilities.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#include "spotify_utilities.h" - -#include - -#include -#include -#include -#include - -namespace utilities { - -QString GetCacheDirectory() { - QString user_cache = GetUserDataDirectory(); - return user_cache + "/" + QCoreApplication::applicationName() + - "/spotify-cache"; -} - -#ifndef Q_OS_DARWIN // See spotify_utilities.mm for Mac implementation. -QString GetUserDataDirectory() { - const char* xdg_cache_dir = getenv("XDG_CACHE_HOME"); - if (!xdg_cache_dir) { - return QDir::homePath() + "/.config"; - } - return QString::fromLocal8Bit(xdg_cache_dir); -} - -QString GetSettingsDirectory() { - QString ret; - -#ifdef Q_OS_WIN32 - ret = GetUserDataDirectory() + "/" + QCoreApplication::applicationName() + - "/spotify-settings"; -#else - ret = QFileInfo(QSettings().fileName()).absolutePath() + "/spotify-settings"; -#endif // Q_OS_WIN32 - - // Create the directory - QDir dir; - dir.mkpath(ret); - - return ret; -} - -#endif // Q_OS_DARWIN - -} // namespace utilities diff --git a/ext/clementine-spotifyblob/spotify_utilities.h b/ext/clementine-spotifyblob/spotify_utilities.h deleted file mode 100644 index 52f103453..000000000 --- a/ext/clementine-spotifyblob/spotify_utilities.h +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#ifndef SPOTIFY_UTILITIES_H -#define SPOTIFY_UTILITIES_H - -#include - -namespace utilities { - -// Get the path to the current user's data directory for all apps. -QString GetUserDataDirectory(); - -// Get the path for Clementine's cache. -QString GetCacheDirectory(); - -QString GetSettingsDirectory(); -} - -#endif diff --git a/ext/clementine-spotifyblob/spotify_utilities.mm b/ext/clementine-spotifyblob/spotify_utilities.mm deleted file mode 100644 index b88ebcefb..000000000 --- a/ext/clementine-spotifyblob/spotify_utilities.mm +++ /dev/null @@ -1,44 +0,0 @@ -#include "spotify_utilities.h" - -#import -#import -#import - -#import "core/scoped_nsautorelease_pool.h" - -namespace utilities { - -QString GetUserDataDirectory() { - ScopedNSAutoreleasePool pool; - - NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - QString ret; - if ([paths count] > 0) { - NSString* user_path = [paths objectAtIndex:0]; - ret = QString::fromUtf8([user_path UTF8String]); - } else { - ret = "~/Library/Caches"; - } - return ret; -} - -QString GetSettingsDirectory() { - ScopedNSAutoreleasePool pool; - NSArray* paths = - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); - NSString* ret; - if ([paths count] > 0) { - ret = [paths objectAtIndex:0]; - } else { - ret = @"~/Library/Application Support"; - } - ret = [ret stringByAppendingString:@"/Clementine/spotify-settings"]; - - NSFileManager* file_manager = [NSFileManager defaultManager]; - [file_manager createDirectoryAtPath:ret withIntermediateDirectories:YES attributes:nil error:nil]; - - QString path = QString::fromUtf8([ret UTF8String]); - return path; -} - -} // namespace utilities diff --git a/ext/clementine-spotifyblob/spotifyclient.cpp b/ext/clementine-spotifyblob/spotifyclient.cpp deleted file mode 100644 index b8470a6b7..000000000 --- a/ext/clementine-spotifyblob/spotifyclient.cpp +++ /dev/null @@ -1,1149 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#include "spotifyclient.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "core/arraysize.h" -#include "core/logging.h" -#include "core/timeconstants.h" -#include "mediapipeline.h" -#include "spotify_utilities.h" -#include "spotifykey.h" -#include "spotifymessages.pb.h" - -const int SpotifyClient::kSpotifyImageIDSize = 20; -const int SpotifyClient::kWaveHeaderSize = 44; - -SpotifyClient::SpotifyClient(QObject* parent) - : AbstractMessageHandler(nullptr, parent), - api_key_(QByteArray::fromBase64(kSpotifyApiKey)), - protocol_socket_(new QTcpSocket(this)), - session_(nullptr), - events_timer_(new QTimer(this)) { - SetDevice(protocol_socket_); - - memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_)); - memset(&spotify_config_, 0, sizeof(spotify_config_)); - memset(&playlistcontainer_callbacks_, 0, - sizeof(playlistcontainer_callbacks_)); - memset(&get_playlists_callbacks_, 0, sizeof(get_playlists_callbacks_)); - memset(&load_playlist_callbacks_, 0, sizeof(load_playlist_callbacks_)); - - spotify_callbacks_.logged_in = &LoggedInCallback; - spotify_callbacks_.notify_main_thread = &NotifyMainThreadCallback; - spotify_callbacks_.log_message = &LogMessageCallback; - spotify_callbacks_.metadata_updated = &MetadataUpdatedCallback; - spotify_callbacks_.music_delivery = &MusicDeliveryCallback; - spotify_callbacks_.end_of_track = &EndOfTrackCallback; - spotify_callbacks_.streaming_error = &StreamingErrorCallback; - spotify_callbacks_.offline_status_updated = &OfflineStatusUpdatedCallback; - spotify_callbacks_.connection_error = &ConnectionErrorCallback; - spotify_callbacks_.message_to_user = &UserMessageCallback; - spotify_callbacks_.start_playback = &StartPlaybackCallback; - spotify_callbacks_.stop_playback = &StopPlaybackCallback; - - playlistcontainer_callbacks_.container_loaded = - &PlaylistContainerLoadedCallback; - playlistcontainer_callbacks_.playlist_added = &PlaylistAddedCallback; - playlistcontainer_callbacks_.playlist_moved = &PlaylistMovedCallback; - playlistcontainer_callbacks_.playlist_removed = &PlaylistRemovedCallback; - - get_playlists_callbacks_.playlist_state_changed = - &PlaylistStateChangedForGetPlaylists; - - load_playlist_callbacks_.playlist_state_changed = - &PlaylistStateChangedForLoadPlaylist; - - QString cache = utilities::GetCacheDirectory(); - qLog(Debug) << "Using:" << cache << "for Spotify cache"; - QString settings_dir = utilities::GetSettingsDirectory(); - qLog(Debug) << "Using:" << settings_dir << "for Spotify settings"; - - spotify_config_.api_version = SPOTIFY_API_VERSION; // From libspotify/api.h - spotify_config_.cache_location = strdup(cache.toUtf8().constData()); - spotify_config_.settings_location = strdup(settings_dir.toUtf8().constData()); - spotify_config_.application_key = api_key_.constData(); - spotify_config_.application_key_size = api_key_.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(protocol_socket_, SIGNAL(disconnected()), - QCoreApplication::instance(), SLOT(quit())); -} - -SpotifyClient::~SpotifyClient() { - if (session_) { - sp_session_release(session_); - } - - free(const_cast(spotify_config_.cache_location)); - free(const_cast(spotify_config_.settings_location)); -} - -void SpotifyClient::Init(quint16 port) { - qLog(Debug) << "Connecting to port" << port; - - protocol_socket_->connectToHost(QHostAddress::LocalHost, port); -} - -void SpotifyClient::LoggedInCallback(sp_session* session, sp_error error) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - const bool success = error == SP_ERROR_OK; - cpb::spotify::LoginResponse_Error error_code = - cpb::spotify::LoginResponse_Error_Other; - - if (!success) { - qLog(Warning) << "Failed to login" << sp_error_message(error); - } - - switch (error) { - case SP_ERROR_BAD_USERNAME_OR_PASSWORD: - error_code = cpb::spotify::LoginResponse_Error_BadUsernameOrPassword; - break; - case SP_ERROR_USER_BANNED: - error_code = cpb::spotify::LoginResponse_Error_UserBanned; - break; - case SP_ERROR_USER_NEEDS_PREMIUM: - error_code = cpb::spotify::LoginResponse_Error_UserNeedsPremium; - break; - default: - error_code = cpb::spotify::LoginResponse_Error_Other; - break; - } - - me->SendLoginCompleted(success, sp_error_message(error), error_code); - - if (success) { - sp_playlistcontainer_add_callbacks(sp_session_playlistcontainer(session), - &me->playlistcontainer_callbacks_, me); - sp_session_flush_caches(me->session_); - } -} - -void SpotifyClient::NotifyMainThreadCallback(sp_session* session) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - QMetaObject::invokeMethod(me, "ProcessEvents", Qt::QueuedConnection); -} - -void SpotifyClient::ProcessEvents() { - int next_timeout_ms; - sp_session_process_events(session_, &next_timeout_ms); - events_timer_->start(next_timeout_ms); -} - -void SpotifyClient::LogMessageCallback(sp_session* session, const char* data) { - qLog(Debug) << "libspotify:" << QString::fromUtf8(data).trimmed(); -} - -void SpotifyClient::Search(const cpb::spotify::SearchRequest& req) { - sp_search* search = - sp_search_create(session_, req.query().c_str(), 0, req.limit(), 0, - req.limit_album(), 0, 0, // artists - 0, 0, // playlists - SP_SEARCH_STANDARD, &SearchCompleteCallback, this); - - pending_searches_[search] = req; -} - -void SpotifyClient::SearchCompleteCallback(sp_search* result, void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - if (!me->pending_searches_.contains(result)) { - qLog(Warning) << "SearchComplete called with unknown search"; - return; - } - - // If there were any album results then we need to resolve those before - // we can send our response. - const int count = sp_search_num_albums(result); - if (count != 0) { - for (int i = 0; i < count; ++i) { - sp_album* album = sp_search_album(result, i); - sp_albumbrowse* browse = sp_albumbrowse_create( - me->session_, album, &SearchAlbumBrowseComplete, me); - - me->pending_search_album_browse_responses_[browse] = result; - } - return; - } - - me->SendSearchResponse(result); -} - -void SpotifyClient::SearchAlbumBrowseComplete(sp_albumbrowse* result, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - if (!me->pending_search_album_browse_responses_.contains(result)) { - qLog(Warning) << "SearchAlbumBrowseComplete called with unknown result"; - return; - } - - sp_search* search = me->pending_search_album_browse_responses_.take(result); - me->pending_search_album_browses_[search].append(result); - - if (me->pending_search_album_browses_[search].count() >= - sp_search_num_albums(search)) { - me->SendSearchResponse(search); - } -} - -void SpotifyClient::SendSearchResponse(sp_search* result) { - // Take the request out of the queue - cpb::spotify::SearchRequest req = pending_searches_.take(result); - - // Prepare the response - cpb::spotify::Message message; - cpb::spotify::SearchResponse* response = message.mutable_search_response(); - - *response->mutable_request() = req; - - // Check for errors - sp_error error = sp_search_error(result); - if (error != SP_ERROR_OK) { - response->set_error(sp_error_message(error)); - - SendMessage(message); - sp_search_release(result); - return; - } - - // Get the list of tracks from the search - int count = sp_search_num_tracks(result); - for (int i = 0; i < count; ++i) { - sp_track* track = sp_search_track(result, i); - ConvertTrack(track, response->add_result()); - } - - // Get the albums from the search. All these should be resolved by now. - QList browses = pending_search_album_browses_.take(result); - for (sp_albumbrowse* browse : browses) { - sp_album* album = sp_albumbrowse_album(browse); - cpb::spotify::Album* msg = response->add_album(); - - ConvertAlbum(album, msg->mutable_metadata()); - ConvertAlbumBrowse(browse, msg->mutable_metadata()); - - // Add all tracks - const int tracks = sp_albumbrowse_num_tracks(browse); - for (int i = 0; i < tracks; ++i) { - ConvertTrack(sp_albumbrowse_track(browse, i), msg->add_track()); - } - - sp_albumbrowse_release(browse); - } - - // Add other data to the response - response->set_total_tracks(sp_search_total_tracks(result)); - response->set_did_you_mean(sp_search_did_you_mean(result)); - - SendMessage(message); - sp_search_release(result); -} - -void SpotifyClient::MessageArrived(const cpb::spotify::Message& message) { - if (message.has_login_request()) { - Login(message.login_request()); - } else if (message.has_load_playlist_request()) { - LoadPlaylist(message.load_playlist_request()); - } else if (message.has_playback_request()) { - StartPlayback(message.playback_request()); - } else if (message.has_seek_request()) { - Seek(message.seek_request().offset_nsec()); - } else if (message.has_search_request()) { - Search(message.search_request()); - } else if (message.has_image_request()) { - LoadImage(QStringFromStdString(message.image_request().id())); - } else if (message.has_sync_playlist_request()) { - SyncPlaylist(message.sync_playlist_request()); - } else if (message.has_browse_album_request()) { - BrowseAlbum(QStringFromStdString(message.browse_album_request().uri())); - } else if (message.has_set_playback_settings_request()) { - SetPlaybackSettings(message.set_playback_settings_request()); - } else if (message.has_browse_toplist_request()) { - BrowseToplist(message.browse_toplist_request()); - } else if (message.has_pause_request()) { - SetPaused(message.pause_request()); - } else if (message.has_add_tracks_to_playlist()) { - AddTracksToPlaylist(message.add_tracks_to_playlist()); - } else if (message.has_remove_tracks_from_playlist()) { - RemoveTracksFromPlaylist(message.remove_tracks_from_playlist()); - } -} - -void SpotifyClient::SetPlaybackSettings( - const cpb::spotify::PlaybackSettings& req) { - sp_bitrate bitrate = SP_BITRATE_320k; - switch (req.bitrate()) { - case cpb::spotify::Bitrate96k: - bitrate = SP_BITRATE_96k; - break; - case cpb::spotify::Bitrate160k: - bitrate = SP_BITRATE_160k; - break; - case cpb::spotify::Bitrate320k: - bitrate = SP_BITRATE_320k; - break; - } - - qLog(Debug) << "Setting playback settings: bitrate" << bitrate - << "normalisation" << req.volume_normalisation(); - - sp_session_preferred_bitrate(session_, bitrate); - sp_session_preferred_offline_bitrate(session_, bitrate, false); - sp_session_set_volume_normalization(session_, req.volume_normalisation()); -} - -void SpotifyClient::Login(const cpb::spotify::LoginRequest& req) { - sp_error error = sp_session_create(&spotify_config_, &session_); - if (error != SP_ERROR_OK) { - qLog(Warning) << "Failed to create session" << sp_error_message(error); - SendLoginCompleted(false, sp_error_message(error), - cpb::spotify::LoginResponse_Error_Other); - return; - } - - SetPlaybackSettings(req.playback_settings()); - - if (req.password().empty()) { - sp_error error = sp_session_relogin(session_); - if (error != SP_ERROR_OK) { - qLog(Warning) << "Tried to relogin but no stored credentials"; - SendLoginCompleted(false, sp_error_message(error), - cpb::spotify::LoginResponse_Error_ReloginFailed); - } - } else { - sp_session_login(session_, req.username().c_str(), req.password().c_str(), - true, // Remember the password. - nullptr); - } -} - -void SpotifyClient::SendLoginCompleted( - bool success, const QString& error, - cpb::spotify::LoginResponse_Error error_code) { - cpb::spotify::Message message; - - cpb::spotify::LoginResponse* response = message.mutable_login_response(); - response->set_success(success); - response->set_error(DataCommaSizeFromQString(error)); - - if (!success) { - response->set_error_code(error_code); - } - - SendMessage(message); -} - -void SpotifyClient::PlaylistContainerLoadedCallback(sp_playlistcontainer* pc, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - // Install callbacks on all the playlists - sp_playlist_add_callbacks(sp_session_starred_create(me->session_), - &me->get_playlists_callbacks_, me); - const int count = sp_playlistcontainer_num_playlists(pc); - for (int i = 0; i < count; ++i) { - sp_playlist* playlist = sp_playlistcontainer_playlist(pc, i); - sp_playlist_add_callbacks(playlist, &me->get_playlists_callbacks_, me); - } - - me->SendPlaylistList(); -} - -void SpotifyClient::PlaylistAddedCallback(sp_playlistcontainer* pc, - sp_playlist* playlist, int position, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - // Install callbacks on this playlist - sp_playlist_add_callbacks(playlist, &me->get_playlists_callbacks_, me); - - me->SendPlaylistList(); -} - -void SpotifyClient::PlaylistMovedCallback(sp_playlistcontainer* pc, - sp_playlist* playlist, int position, - int new_position, void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - me->SendPlaylistList(); -} - -void SpotifyClient::PlaylistRemovedCallback(sp_playlistcontainer* pc, - sp_playlist* playlist, int position, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - // Remove callbacks from this playlist - sp_playlist_remove_callbacks(playlist, &me->get_playlists_callbacks_, me); - - me->SendPlaylistList(); -} - -void SpotifyClient::SendPlaylistList() { - cpb::spotify::Message message; - cpb::spotify::Playlists* response = message.mutable_playlists_updated(); - - sp_playlistcontainer* container = sp_session_playlistcontainer(session_); - if (!container) { - qLog(Warning) << "sp_session_playlistcontainer returned nullptr"; - return; - } - - const int count = sp_playlistcontainer_num_playlists(container); - - for (int i = 0; i < count; ++i) { - const int type = sp_playlistcontainer_playlist_type(container, i); - sp_playlist* playlist = sp_playlistcontainer_playlist(container, i); - const bool is_loaded = sp_playlist_is_loaded(playlist); - - qLog(Debug) << "Got playlist" << i << is_loaded << type - << sp_playlist_name(playlist); - - if (!is_loaded) { - qLog(Info) << "Playlist is not loaded yet, jump to the next one..."; - continue; - } - - if (type != SP_PLAYLIST_TYPE_PLAYLIST) { - // Just ignore folders for now - continue; - } - - cpb::spotify::Playlists::Playlist* msg = response->add_playlist(); - msg->set_index(i); - msg->set_name(sp_playlist_name(playlist)); - sp_user* playlist_owner = sp_playlist_owner(playlist); - msg->set_is_mine(sp_session_user(session_) == playlist_owner); - msg->set_owner(sp_user_display_name(playlist_owner)); - - sp_playlist_offline_status offline_status = - sp_playlist_get_offline_status(session_, playlist); - const bool is_offline = offline_status == SP_PLAYLIST_OFFLINE_STATUS_YES; - msg->set_is_offline(is_offline); - if (offline_status == SP_PLAYLIST_OFFLINE_STATUS_DOWNLOADING) { - msg->set_download_progress( - sp_playlist_get_offline_download_completed(session_, playlist)); - } else if (offline_status == SP_PLAYLIST_OFFLINE_STATUS_WAITING) { - msg->set_download_progress(0); - } - msg->set_nb_tracks(sp_playlist_num_tracks(playlist)); - // URI - Blugh - char uri[256]; - sp_link* link = sp_link_create_from_playlist(playlist); - sp_link_as_string(link, uri, arraysize(uri)); - sp_link_release(link); - msg->set_uri(uri); - } - - SendMessage(message); -} - -sp_playlist* SpotifyClient::GetPlaylist(cpb::spotify::PlaylistType type, - int user_index) { - sp_playlist* playlist = nullptr; - switch (type) { - case cpb::spotify::Inbox: - playlist = sp_session_inbox_create(session_); - break; - - case cpb::spotify::Starred: - playlist = sp_session_starred_create(session_); - break; - - case cpb::spotify::UserPlaylist: { - sp_playlistcontainer* pc = sp_session_playlistcontainer(session_); - - if (pc && user_index <= sp_playlistcontainer_num_playlists(pc)) { - if (sp_playlistcontainer_playlist_type(pc, user_index) == - SP_PLAYLIST_TYPE_PLAYLIST) { - playlist = sp_playlistcontainer_playlist(pc, user_index); - sp_playlist_add_ref(playlist); - } - } - - break; - } - } - return playlist; -} - -void SpotifyClient::LoadPlaylist(const cpb::spotify::LoadPlaylistRequest& req) { - PendingLoadPlaylist pending_load; - pending_load.request_ = req; - pending_load.playlist_ = GetPlaylist(req.type(), req.user_playlist_index()); - - // A null playlist might mean the user wasn't logged in, or an invalid - // playlist index was requested, so we'd better return an error straight away. - if (!pending_load.playlist_) { - qLog(Warning) << "Invalid playlist requested or not logged in"; - - cpb::spotify::Message message; - cpb::spotify::LoadPlaylistResponse* response = - message.mutable_load_playlist_response(); - *response->mutable_request() = req; - SendMessage(message); - return; - } - - sp_playlist_add_callbacks(pending_load.playlist_, &load_playlist_callbacks_, - this); - pending_load_playlists_ << pending_load; - - PlaylistStateChangedForLoadPlaylist(pending_load.playlist_, this); -} - -void SpotifyClient::SyncPlaylist(const cpb::spotify::SyncPlaylistRequest& req) { - sp_playlist* playlist = - GetPlaylist(req.request().type(), req.request().user_playlist_index()); - - // The playlist should already be loaded. - sp_playlist_set_offline_mode(session_, playlist, req.offline_sync()); -} - -void SpotifyClient::PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - // If the playlist isn't loaded yet we have to wait - if (!sp_playlist_is_loaded(pl)) { - qLog(Debug) << "Playlist isn't loaded yet, waiting"; - return; - } - - // Find this playlist's pending load object - int pending_load_index = -1; - PendingLoadPlaylist* pending_load = nullptr; - for (int i = 0; i < me->pending_load_playlists_.count(); ++i) { - if (me->pending_load_playlists_[i].playlist_ == pl) { - pending_load_index = i; - pending_load = &me->pending_load_playlists_[i]; - break; - } - } - - if (!pending_load) { - qLog(Warning) << "Playlist not found in pending load list"; - return; - } - - // If the playlist was just loaded then get all its tracks and ref them - if (pending_load->tracks_.isEmpty()) { - const int count = sp_playlist_num_tracks(pl); - for (int i = 0; i < count; ++i) { - sp_track* track = sp_playlist_track(pl, i); - sp_track_add_ref(track); - pending_load->tracks_ << track; - } - } - - // If any of the tracks aren't loaded yet we have to wait - for (sp_track* track : pending_load->tracks_) { - if (!sp_track_is_loaded(track)) { - qLog(Debug) << "One or more tracks aren't loaded yet, waiting"; - return; - } - } - - // Everything is loaded so send the response protobuf and unref everything. - cpb::spotify::Message message; - cpb::spotify::LoadPlaylistResponse* response = - message.mutable_load_playlist_response(); - - // For some reason, we receive the starred tracks in reverse order but not - // other playlists. - if (pending_load->request_.type() == cpb::spotify::Starred) { - std::reverse(pending_load->tracks_.begin(), pending_load->tracks_.end()); - } - - *response->mutable_request() = pending_load->request_; - for (sp_track* track : pending_load->tracks_) { - me->ConvertTrack(track, response->add_track()); - sp_track_release(track); - } - me->SendMessage(message); - - // Unref the playlist and remove our callbacks - sp_playlist_remove_callbacks(pl, &me->load_playlist_callbacks_, me); - sp_playlist_release(pl); - - // Remove the pending load object - me->pending_load_playlists_.removeAt(pending_load_index); -} - -void SpotifyClient::PlaylistStateChangedForGetPlaylists(sp_playlist* pl, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - me->SendPlaylistList(); -} - -void SpotifyClient::AddTracksToPlaylist( - const cpb::spotify::AddTracksToPlaylistRequest& req) { - // Get the playlist we want to update - sp_playlist* playlist = - GetPlaylist(req.playlist_type(), req.playlist_index()); - if (!playlist) { - qLog(Error) << "Playlist " << req.playlist_type() << "," - << req.playlist_index() << "not found"; - return; - } - - // Get the tracks we want to add - std::unique_ptr tracks_array( - new sp_track*[req.track_uri_size()]); - for (int i = 0; i < req.track_uri_size(); ++i) { - sp_link* track_link = sp_link_create_from_string(req.track_uri(i).c_str()); - sp_track* track = sp_link_as_track(track_link); - sp_track_add_ref(track); - sp_link_release(track_link); - if (!track) { - qLog(Error) << "Track" << QString::fromStdString(req.track_uri(i)) - << "not found"; - } - tracks_array[i] = track; - } - - // Actually add the tracks to the playlist - if (sp_playlist_add_tracks(playlist, tracks_array.get(), req.track_uri_size(), - 0 /* TODO: don't insert at a hardcoded position */, - session_) != SP_ERROR_OK) { - qLog(Error) << "Error when adding tracks!"; - } - - // Clean everything - for (int i = 0; i < req.track_uri_size(); ++i) { - sp_track_release(tracks_array[i]); - } -} - -void SpotifyClient::RemoveTracksFromPlaylist( - const cpb::spotify::RemoveTracksFromPlaylistRequest& req) { - // Get the playlist we want to update - sp_playlist* playlist = - GetPlaylist(req.playlist_type(), req.playlist_index()); - if (!playlist) { - qLog(Error) << "Playlist " << req.playlist_type() << "," - << req.playlist_index() << "not found"; - return; - } - - // Get the position of the tracks we want to remove - std::unique_ptr tracks_indices_array(new int[req.track_index_size()]); - for (int i = 0; i < req.track_index_size(); ++i) { - tracks_indices_array[i] = req.track_index(i); - } - - // WTF: sp_playlist_remove_tracks indexes start from the end for starred - // playlist, not from the beginning like other playlists: reverse them - if (req.playlist_type() == cpb::spotify::Starred) { - int num_tracks = sp_playlist_num_tracks(playlist); - for (int i = 0; i < req.track_index_size(); i++) { - tracks_indices_array[i] = num_tracks - tracks_indices_array[i] - 1; - } - } - - if (sp_playlist_remove_tracks(playlist, tracks_indices_array.get(), - req.track_index_size()) != SP_ERROR_OK) { - qLog(Error) << "Error when removing tracks!"; - } -} - -void SpotifyClient::ConvertTrack(sp_track* track, cpb::spotify::Track* pb) { - sp_album* album = sp_track_album(track); - - pb->set_starred(sp_track_is_starred(session_, track)); - pb->set_title(sp_track_name(track)); - pb->set_album(sp_album_name(album)); - pb->set_year(sp_album_year(album)); - pb->set_duration_msec(sp_track_duration(track)); - pb->set_popularity(sp_track_popularity(track)); - pb->set_disc(sp_track_disc(track)); - pb->set_track(sp_track_index(track)); - - // Album art - const QByteArray art_id(reinterpret_cast(sp_album_cover( - sp_track_album(track), SP_IMAGE_SIZE_LARGE)), - kSpotifyImageIDSize); - const QString art_id_b64 = QString::fromLatin1(art_id.toBase64()); - pb->set_album_art_id(DataCommaSizeFromQString(art_id_b64)); - - // Artists - for (int i = 0; i < sp_track_num_artists(track); ++i) { - pb->add_artist(sp_artist_name(sp_track_artist(track, i))); - } - - // URI - Blugh - char uri[256]; - sp_link* link = sp_link_create_from_track(track, 0); - sp_link_as_string(link, uri, arraysize(uri)); - sp_link_release(link); - - pb->set_uri(uri); -} - -void SpotifyClient::ConvertAlbum(sp_album* album, cpb::spotify::Track* pb) { - pb->set_album(sp_album_name(album)); - pb->set_year(sp_album_year(album)); - pb->add_artist(sp_artist_name(sp_album_artist(album))); - - // These fields were required in a previous version so need to set them again - // now. - pb->mutable_title(); - pb->set_duration_msec(-1); - pb->set_popularity(-1); - pb->set_disc(-1); - pb->set_track(-1); - pb->set_starred(false); - - // Album art - const QByteArray art_id( - reinterpret_cast(sp_album_cover(album, SP_IMAGE_SIZE_LARGE)), - kSpotifyImageIDSize); - const QString art_id_b64 = QString::fromLatin1(art_id.toBase64()); - pb->set_album_art_id(DataCommaSizeFromQString(art_id_b64)); - - // URI - Blugh - char uri[256]; - sp_link* link = sp_link_create_from_album(album); - sp_link_as_string(link, uri, arraysize(uri)); - sp_link_release(link); - - pb->set_uri(uri); -} - -void SpotifyClient::ConvertAlbumBrowse(sp_albumbrowse* browse, - cpb::spotify::Track* pb) { - pb->set_track(sp_albumbrowse_num_tracks(browse)); -} - -void SpotifyClient::MetadataUpdatedCallback(sp_session* session) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - - for (const PendingLoadPlaylist& load : me->pending_load_playlists_) { - PlaylistStateChangedForLoadPlaylist(load.playlist_, me); - } - for (const PendingPlaybackRequest& playback : - me->pending_playback_requests_) { - me->TryPlaybackAgain(playback); - } -} - -int SpotifyClient::MusicDeliveryCallback(sp_session* session, - const sp_audioformat* format, - const void* frames, int num_frames) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - - if (!me->media_pipeline_) { - return 0; - } - - if (num_frames == 0) { - return 0; - } - - if (!me->media_pipeline_->is_initialised()) { - if (!me->media_pipeline_->Init(format->sample_rate, format->channels)) { - qLog(Warning) << "Failed to intitialise media pipeline"; - sp_session_player_unload(me->session_); - me->media_pipeline_.reset(); - return 0; - } - } - - if (!me->media_pipeline_->is_accepting_data()) { - return 0; - } - - me->media_pipeline_->WriteData(reinterpret_cast(frames), - num_frames * format->channels * 2); - - return num_frames; -} - -void SpotifyClient::EndOfTrackCallback(sp_session* session) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - - me->media_pipeline_.reset(); -} - -void SpotifyClient::StreamingErrorCallback(sp_session* session, - sp_error error) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - - me->media_pipeline_.reset(); - - // Send the error - me->SendPlaybackError(QString::fromUtf8(sp_error_message(error))); -} - -void SpotifyClient::ConnectionErrorCallback(sp_session* session, - sp_error error) { - qLog(Debug) << Q_FUNC_INFO << sp_error_message(error); -} - -void SpotifyClient::UserMessageCallback(sp_session* session, - const char* message) { - qLog(Debug) << Q_FUNC_INFO << message; -} - -void SpotifyClient::StartPlaybackCallback(sp_session* session) { - qLog(Debug) << Q_FUNC_INFO; -} - -void SpotifyClient::StopPlaybackCallback(sp_session* session) { - qLog(Debug) << Q_FUNC_INFO; -} - -void SpotifyClient::OfflineStatusUpdatedCallback(sp_session* session) { - SpotifyClient* me = - reinterpret_cast(sp_session_userdata(session)); - sp_playlistcontainer* container = sp_session_playlistcontainer(session); - if (!container) { - qLog(Warning) << "sp_session_playlistcontainer returned nullptr"; - return; - } - - const int count = sp_playlistcontainer_num_playlists(container); - - for (int i = 0; i < count; ++i) { - const sp_playlist_type type = - sp_playlistcontainer_playlist_type(container, i); - sp_playlist* playlist = sp_playlistcontainer_playlist(container, i); - - if (type != SP_PLAYLIST_TYPE_PLAYLIST) { - // Just ignore folders for now - continue; - } - - int download_progress = me->GetDownloadProgress(playlist); - if (download_progress != -1) { - me->SendDownloadProgress(cpb::spotify::UserPlaylist, i, download_progress); - } - } - - sp_playlist* inbox = sp_session_inbox_create(session); - int download_progress = me->GetDownloadProgress(inbox); - sp_playlist_release(inbox); - - if (download_progress != -1) { - me->SendDownloadProgress(cpb::spotify::Inbox, -1, download_progress); - } - - sp_playlist* starred = sp_session_starred_create(session); - download_progress = me->GetDownloadProgress(starred); - sp_playlist_release(starred); - - if (download_progress != -1) { - me->SendDownloadProgress(cpb::spotify::Starred, -1, download_progress); - } -} - -void SpotifyClient::SendDownloadProgress(cpb::spotify::PlaylistType type, - int index, int download_progress) { - cpb::spotify::Message message; - cpb::spotify::SyncPlaylistProgress* progress = - message.mutable_sync_playlist_progress(); - progress->mutable_request()->set_type(type); - if (index != -1) { - progress->mutable_request()->set_user_playlist_index(index); - } - progress->set_sync_progress(download_progress); - SendMessage(message); -} - -int SpotifyClient::GetDownloadProgress(sp_playlist* playlist) { - sp_playlist_offline_status status = - sp_playlist_get_offline_status(session_, playlist); - switch (status) { - case SP_PLAYLIST_OFFLINE_STATUS_NO: - return -1; - case SP_PLAYLIST_OFFLINE_STATUS_YES: - return 100; - case SP_PLAYLIST_OFFLINE_STATUS_DOWNLOADING: - return sp_playlist_get_offline_download_completed(session_, playlist); - case SP_PLAYLIST_OFFLINE_STATUS_WAITING: - return 0; - } - return -1; -} - -void SpotifyClient::StartPlayback(const cpb::spotify::PlaybackRequest& req) { - // Get a link object from the URI - sp_link* link = sp_link_create_from_string(req.track_uri().c_str()); - if (!link) { - SendPlaybackError("Invalid Spotify URI"); - return; - } - - // Get the track from the link - sp_track* track = sp_link_as_track(link); - if (!track) { - SendPlaybackError("Spotify URI was not a track"); - sp_link_release(link); - return; - } - - PendingPlaybackRequest pending_playback; - pending_playback.request_ = req; - pending_playback.link_ = link; - pending_playback.track_ = track; - - pending_playback_requests_ << pending_playback; - - TryPlaybackAgain(pending_playback); -} - -void SpotifyClient::Seek(qint64 offset_nsec) { - // TODO - qLog(Error) << "TODO seeking"; -} - -void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) { - // If the track was not loaded then we have to come back later - if (!sp_track_is_loaded(req.track_)) { - qLog(Debug) << "Playback track not loaded yet, will try again later"; - return; - } - - // Load the track - sp_error error = sp_session_player_load(session_, req.track_); - if (error != SP_ERROR_OK) { - SendPlaybackError("Spotify playback error: " + - QString::fromUtf8(sp_error_message(error))); - sp_link_release(req.link_); - - // Remove this from the pending list now - pending_playback_requests_.removeAll(req); - return; - } - - // Create the media socket - media_pipeline_.reset(new MediaPipeline(req.request_.media_port(), - sp_track_duration(req.track_))); - - qLog(Info) << "Starting playback of uri" << req.request_.track_uri().c_str() - << "to port" << req.request_.media_port(); - - // Start playback - sp_session_player_play(session_, true); - - sp_link_release(req.link_); - - // Remove this from the pending list now - pending_playback_requests_.removeAll(req); -} - -void SpotifyClient::SendPlaybackError(const QString& error) { - cpb::spotify::Message message; - cpb::spotify::PlaybackError* msg = message.mutable_playback_error(); - - msg->set_error(DataCommaSizeFromQString(error)); - SendMessage(message); -} - -void SpotifyClient::LoadImage(const QString& id_b64) { - QByteArray id = QByteArray::fromBase64(id_b64.toLatin1()); - if (id.length() != kSpotifyImageIDSize) { - qLog(Warning) << "Invalid image ID (did not decode to" - << kSpotifyImageIDSize << "bytes):" << id_b64; - - // Send an error response straight away - cpb::spotify::Message message; - cpb::spotify::ImageResponse* msg = message.mutable_image_response(); - msg->set_id(DataCommaSizeFromQString(id_b64)); - SendMessage(message); - return; - } - - PendingImageRequest pending_load; - pending_load.id_ = id; - pending_load.id_b64_ = id_b64; - pending_load.image_ = - sp_image_create(session_, reinterpret_cast(id.constData())); - pending_image_requests_ << pending_load; - - if (!image_callbacks_registered_[pending_load.image_]) { - sp_image_add_load_callback(pending_load.image_, &ImageLoaded, this); - } - image_callbacks_registered_[pending_load.image_]++; - - TryImageAgain(pending_load.image_); -} - -void SpotifyClient::TryImageAgain(sp_image* image) { - if (!sp_image_is_loaded(image)) { - qLog(Debug) << "Image not loaded, will try again later"; - return; - } - - // Find the pending request for this image - int index = -1; - PendingImageRequest* req = nullptr; - for (int i = 0; i < pending_image_requests_.count(); ++i) { - if (pending_image_requests_[i].image_ == image) { - index = i; - req = &pending_image_requests_[i]; - break; - } - } - - if (index == -1) { - qLog(Warning) << "Image not found in pending load list"; - return; - } - - // Get the image data - size_t size = 0; - const void* data = sp_image_data(image, &size); - - // Send the response - cpb::spotify::Message message; - cpb::spotify::ImageResponse* msg = message.mutable_image_response(); - msg->set_id(DataCommaSizeFromQString(req->id_b64_)); - if (data && size) { - msg->set_data(data, size); - } - SendMessage(message); - - // Free stuff - image_callbacks_registered_[image]--; - - // TODO: memory leak? - // sp_image_remove_load_callback(image, &ImageLoaded, this); - image_callbacks_registered_.remove(image); - - sp_image_release(image); - pending_image_requests_.removeAt(index); -} - -void SpotifyClient::ImageLoaded(sp_image* image, void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - me->TryImageAgain(image); -} - -void SpotifyClient::BrowseAlbum(const QString& uri) { - // Get a link object from the URI - sp_link* link = sp_link_create_from_string(uri.toStdString().c_str()); - if (!link) { - SendPlaybackError("Invalid Album URI"); - return; - } - - // Get the album from the link - sp_album* album = sp_link_as_album(link); - if (!album) { - SendPlaybackError("Spotify URI was not an album"); - sp_link_release(link); - return; - } - - sp_albumbrowse* browse = - sp_albumbrowse_create(session_, album, &AlbumBrowseComplete, this); - pending_album_browses_[browse] = uri; -} - -void SpotifyClient::AlbumBrowseComplete(sp_albumbrowse* result, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - if (!me->pending_album_browses_.contains(result)) return; - - QString uri = me->pending_album_browses_.take(result); - - cpb::spotify::Message message; - cpb::spotify::BrowseAlbumResponse* msg = - message.mutable_browse_album_response(); - - msg->set_uri(DataCommaSizeFromQString(uri)); - - const int count = sp_albumbrowse_num_tracks(result); - for (int i = 0; i < count; ++i) { - me->ConvertTrack(sp_albumbrowse_track(result, i), msg->add_track()); - } - - me->SendMessage(message); - sp_albumbrowse_release(result); -} - -void SpotifyClient::BrowseToplist( - const cpb::spotify::BrowseToplistRequest& req) { - sp_toplistbrowse* browse = sp_toplistbrowse_create( - session_, SP_TOPLIST_TYPE_TRACKS, // TODO: Support albums and artists. - SP_TOPLIST_REGION_EVERYWHERE, // TODO: Support other regions. - nullptr, &ToplistBrowseComplete, this); - pending_toplist_browses_[browse] = req; -} - -void SpotifyClient::SetPaused(const cpb::spotify::PauseRequest& req) { - sp_session_player_play(session_, !req.paused()); -} - -void SpotifyClient::ToplistBrowseComplete(sp_toplistbrowse* result, - void* userdata) { - SpotifyClient* me = reinterpret_cast(userdata); - - qLog(Debug) << "Toplist browse request took:" - << sp_toplistbrowse_backend_request_duration(result) << "ms"; - - if (!me->pending_toplist_browses_.contains(result)) { - return; - } - - const cpb::spotify::BrowseToplistRequest& request = - me->pending_toplist_browses_.take(result); - - cpb::spotify::Message message; - cpb::spotify::BrowseToplistResponse* msg = - message.mutable_browse_toplist_response(); - msg->mutable_request()->CopyFrom(request); - - const int count = sp_toplistbrowse_num_tracks(result); - for (int i = 0; i < count; ++i) { - me->ConvertTrack(sp_toplistbrowse_track(result, i), msg->add_track()); - } - - me->SendMessage(message); - sp_toplistbrowse_release(result); -} - -void SpotifyClient::DeviceClosed() { - AbstractMessageHandler::DeviceClosed(); - - qApp->exit(); -} diff --git a/ext/clementine-spotifyblob/spotifyclient.h b/ext/clementine-spotifyblob/spotifyclient.h deleted file mode 100644 index a496c2864..000000000 --- a/ext/clementine-spotifyblob/spotifyclient.h +++ /dev/null @@ -1,204 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -#ifndef SPOTIFYCLIENT_H -#define SPOTIFYCLIENT_H - -#include "spotifymessages.pb.h" -#include "core/messagehandler.h" - -#include -#include - -#include - -class QTcpSocket; -class QTimer; - -class MediaPipeline; -class ResponseMessage; - -class SpotifyClient : public AbstractMessageHandler { - Q_OBJECT - - public: - SpotifyClient(QObject* parent = nullptr); - ~SpotifyClient(); - - static const int kSpotifyImageIDSize; - static const int kWaveHeaderSize; - - void Init(quint16 port); - - protected: - void MessageArrived(const cpb::spotify::Message& message); - void DeviceClosed(); - - private slots: - void ProcessEvents(); - - private: - void SendLoginCompleted(bool success, const QString& error, - cpb::spotify::LoginResponse_Error error_code); - void SendPlaybackError(const QString& error); - void SendSearchResponse(sp_search* result); - - // Spotify session callbacks. - static void SP_CALLCONV LoggedInCallback(sp_session* session, sp_error error); - static void SP_CALLCONV NotifyMainThreadCallback(sp_session* session); - static void SP_CALLCONV - LogMessageCallback(sp_session* session, const char* data); - static void SP_CALLCONV - SearchCompleteCallback(sp_search* result, void* userdata); - static void SP_CALLCONV MetadataUpdatedCallback(sp_session* session); - static int SP_CALLCONV - MusicDeliveryCallback(sp_session* session, const sp_audioformat* format, - const void* frames, int num_frames); - static void SP_CALLCONV EndOfTrackCallback(sp_session* session); - static void SP_CALLCONV - StreamingErrorCallback(sp_session* session, sp_error error); - static void SP_CALLCONV OfflineStatusUpdatedCallback(sp_session* session); - static void SP_CALLCONV - ConnectionErrorCallback(sp_session* session, sp_error error); - static void SP_CALLCONV - UserMessageCallback(sp_session* session, const char* message); - static void SP_CALLCONV StartPlaybackCallback(sp_session* session); - static void SP_CALLCONV StopPlaybackCallback(sp_session* session); - - // Spotify playlist container callbacks. - static void SP_CALLCONV PlaylistAddedCallback(sp_playlistcontainer* pc, - sp_playlist* playlist, - int position, void* userdata); - static void SP_CALLCONV PlaylistRemovedCallback(sp_playlistcontainer* pc, - sp_playlist* playlist, - int position, void* userdata); - static void SP_CALLCONV - PlaylistMovedCallback(sp_playlistcontainer* pc, sp_playlist* playlist, - int position, int new_position, void* userdata); - static void SP_CALLCONV - PlaylistContainerLoadedCallback(sp_playlistcontainer* pc, void* userdata); - - // Spotify playlist callbacks - when loading the list of playlists - // initially - static void SP_CALLCONV - PlaylistStateChangedForGetPlaylists(sp_playlist* pl, void* userdata); - - // Spotify playlist callbacks - when loading a playlist - static void SP_CALLCONV - PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, void* userdata); - - // Spotify image callbacks. - static void SP_CALLCONV ImageLoaded(sp_image* image, void* userdata); - - // Spotify album browse callbacks. - static void SP_CALLCONV - SearchAlbumBrowseComplete(sp_albumbrowse* result, void* userdata); - static void SP_CALLCONV - AlbumBrowseComplete(sp_albumbrowse* result, void* userdata); - - // Spotify toplist browse callbacks. - static void SP_CALLCONV - ToplistBrowseComplete(sp_toplistbrowse* result, void* userdata); - - // Request handlers. - void Login(const cpb::spotify::LoginRequest& req); - void Search(const cpb::spotify::SearchRequest& req); - void LoadPlaylist(const cpb::spotify::LoadPlaylistRequest& req); - void SyncPlaylist(const cpb::spotify::SyncPlaylistRequest& req); - void AddTracksToPlaylist(const cpb::spotify::AddTracksToPlaylistRequest& req); - void RemoveTracksFromPlaylist( - const cpb::spotify::RemoveTracksFromPlaylistRequest& req); - void StartPlayback(const cpb::spotify::PlaybackRequest& req); - void Seek(qint64 offset_nsec); - void LoadImage(const QString& id_b64); - void BrowseAlbum(const QString& uri); - void BrowseToplist(const cpb::spotify::BrowseToplistRequest& req); - void SetPlaybackSettings(const cpb::spotify::PlaybackSettings& req); - void SetPaused(const cpb::spotify::PauseRequest& req); - - void SendPlaylistList(); - - void ConvertTrack(sp_track* track, cpb::spotify::Track* pb); - void ConvertAlbum(sp_album* album, cpb::spotify::Track* pb); - void ConvertAlbumBrowse(sp_albumbrowse* browse, cpb::spotify::Track* pb); - - // Gets the appropriate sp_playlist* but does not load it. - sp_playlist* GetPlaylist(cpb::spotify::PlaylistType type, int user_index); - - private: - struct PendingLoadPlaylist { - cpb::spotify::LoadPlaylistRequest request_; - sp_playlist* playlist_; - QList tracks_; - bool offline_sync; - }; - - struct PendingPlaybackRequest { - cpb::spotify::PlaybackRequest request_; - sp_link* link_; - sp_track* track_; - - bool operator==(const PendingPlaybackRequest& other) const { - return request_.track_uri() == other.request_.track_uri() && - request_.media_port() == other.request_.media_port(); - } - }; - - struct PendingImageRequest { - QString id_b64_; - QByteArray id_; - sp_image* image_; - }; - - void TryPlaybackAgain(const PendingPlaybackRequest& req); - void TryImageAgain(sp_image* image); - int GetDownloadProgress(sp_playlist* playlist); - void SendDownloadProgress(cpb::spotify::PlaylistType type, int index, - int download_progress); - - QByteArray api_key_; - - QTcpSocket* protocol_socket_; - - sp_session_config spotify_config_; - sp_session_callbacks spotify_callbacks_; - sp_playlistcontainer_callbacks playlistcontainer_callbacks_; - sp_playlist_callbacks get_playlists_callbacks_; - sp_playlist_callbacks load_playlist_callbacks_; - sp_session* session_; - - QTimer* events_timer_; - - QList pending_load_playlists_; - QList pending_playback_requests_; - QList pending_image_requests_; - QMap image_callbacks_registered_; - QMap pending_searches_; - QMap pending_album_browses_; - QMap - pending_toplist_browses_; - - QMap> pending_search_album_browses_; - QMap pending_search_album_browse_responses_; - - QScopedPointer media_pipeline_; -}; - -#endif // SPOTIFYCLIENT_H diff --git a/ext/clementine-spotifyblob/spotifykey.h b/ext/clementine-spotifyblob/spotifykey.h deleted file mode 100644 index 22bedcb38..000000000 --- a/ext/clementine-spotifyblob/spotifykey.h +++ /dev/null @@ -1,37 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -// The Spotify terms of service require that application keys are not -// accessible to third parties. Therefore this application key is heavily -// encrypted here in the source to prevent third parties from viewing it. -// It is most definitely not base64 encoded. - -static const char* kSpotifyApiKey = - "AVlOrvJkKx8T+LEsCk+Kyl24I0MSsjohZAtMFzm2O5Lms1bmAWFWgdZaHkpypzSJPmSd+" - "Wi50wwg" - "JwVCU0sq4Lep1zB4t6Z8h26NK6+z8gmkHVkV9DRPkRgebcUkWTDTflwVPKWF4+" - "gdRjUwprsqBw6O" - "iofRLJzeKaxbmaUGqkSkxVLOiXC9lxylNq6ju7Q7uY8u8XkDUsVM3YIxiWy2+EM7I/" - "lhatzT9xrq" - "rxHe2lg7CzOwF5kuFdwgmi8MQ72xTYXIKnNlOry/" - "hJDlN9lKxkbUBLh+pzbYvO92S2fYKK5PAHvX" - "5+SmSBGbh6dlpHeCGqb8MPdaeZ5I1YxMcDkxa2+tbLA/Muat7gKA9u57TFCtYjun/u/i/" - "ONwdBIQ" - "rePzXZjipO32kYmQAiCkN1p8sgQEcF43QxaVwXGo2X0rRnJf"; diff --git a/ext/libclementine-spotifyblob/CMakeLists.txt b/ext/libclementine-spotifyblob/CMakeLists.txt deleted file mode 100644 index 7a73b1443..000000000 --- a/ext/libclementine-spotifyblob/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -include_directories(${PROTOBUF_INCLUDE_DIRS}) - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/blobversion.h.in - ${CMAKE_CURRENT_BINARY_DIR}/blobversion.h) - -set(MESSAGES - spotifymessages.proto -) - -protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES}) - -add_library(clementine-spotifyblob-messages STATIC - ${PROTO_SOURCES} -) - -target_link_libraries(clementine-spotifyblob-messages - libclementine-common -) diff --git a/ext/libclementine-spotifyblob/blobversion.h.in b/ext/libclementine-spotifyblob/blobversion.h.in deleted file mode 100644 index 0dcf6bb3b..000000000 --- a/ext/libclementine-spotifyblob/blobversion.h.in +++ /dev/null @@ -1,23 +0,0 @@ -/* 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 SPOTIFY_BLOBVERSION_H_IN -#define SPOTIFY_BLOBVERSION_H_IN - -#define SPOTIFY_BLOB_VERSION ${SPOTIFY_BLOB_VERSION} - -#endif // SPOTIFY_BLOBVERSION_H_IN diff --git a/ext/libclementine-spotifyblob/spotifymessages.proto b/ext/libclementine-spotifyblob/spotifymessages.proto deleted file mode 100644 index d89b1eb0c..000000000 --- a/ext/libclementine-spotifyblob/spotifymessages.proto +++ /dev/null @@ -1,236 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. -*/ - -// Note: this file is licensed under the Apache License instead of GPL because -// it is used by the Spotify blob which links against libspotify and is not GPL -// compatible. - -syntax = "proto2"; - -package cpb.spotify; - -message LoginRequest { - required string username = 1; - optional string password = 2; - - optional PlaybackSettings playback_settings = 3; -} - -message LoginResponse { - enum Error { - BadUsernameOrPassword = 1; - UserBanned = 2; - UserNeedsPremium = 3; - Other = 4; - ReloginFailed = 5; - } - - required bool success = 1; - required string error = 2; - optional Error error_code = 3 [default = Other]; -} - -message Playlists { - message Playlist { - required int32 index = 1; - required string name = 2; - required int32 nb_tracks = 3; - required bool is_mine = 4; - required string owner= 5; - required bool is_offline = 6; - required string uri = 7; - // Offline sync progress between 0-100. - optional int32 download_progress = 8; - } - - repeated Playlist playlist = 1; -} - -message Track { - required bool starred = 1; - required string title = 2; - repeated string artist = 3; - required string album = 4; - required int32 duration_msec = 5; - required int32 popularity = 6; - required int32 disc = 7; - required int32 track = 8; - required int32 year = 9; - required string uri = 10; - required string album_art_id = 11; -} - -message Album { - required Track metadata = 1; - repeated Track track = 2; -} - -enum PlaylistType { - Starred = 1; - Inbox = 2; - UserPlaylist = 3; -} - -message LoadPlaylistRequest { - required PlaylistType type = 1; - optional int32 user_playlist_index = 2; -} - -message LoadPlaylistResponse { - required LoadPlaylistRequest request = 1; - repeated Track track = 2; -} - -message SyncPlaylistRequest { - required LoadPlaylistRequest request = 1; - required bool offline_sync = 2; -} - -message SyncPlaylistProgress { - required LoadPlaylistRequest request = 1; - required int32 sync_progress = 2; -} - -message PlaybackRequest { - required string track_uri = 1; - required int32 media_port = 2; -} - -message PlaybackError { - required string error = 1; -} - -message SearchRequest { - required string query = 1; - optional int32 limit = 2 [default = 250]; - optional int32 limit_album = 3 [default = 0]; -} - -message SearchResponse { - required SearchRequest request = 1; - repeated Track result = 2; - optional int32 total_tracks = 3; - optional string did_you_mean = 4; - optional string error = 5; - - // field 6 is deprecated - - repeated Album album = 7; -} - -message ImageRequest { - required string id = 1; -} - -message ImageResponse { - required string id = 1; - optional bytes data = 2; -} - -message BrowseAlbumRequest { - required string uri = 1; -} - -message BrowseAlbumResponse { - required string uri = 1; - repeated Track track = 2; -} - -message BrowseToplistRequest { - enum ToplistType { - Artists = 1; - Albums = 2; - Tracks = 3; - }; - - enum Region { - Everywhere = 1; - User = 2; - }; - - required ToplistType type = 1; - optional Region region = 2 [default=Everywhere]; - // Username to use if region is User. - optional string username = 3; -} - -message BrowseToplistResponse { - required BrowseToplistRequest request = 1; - - repeated Track track = 2; - repeated Album album = 3; -} - -message SeekRequest { - optional int64 offset_nsec = 1; -} - -enum Bitrate { - Bitrate96k = 1; - Bitrate160k = 2; - Bitrate320k = 3; -} - -message PlaybackSettings { - optional Bitrate bitrate = 1 [default = Bitrate320k]; - optional bool volume_normalisation = 2 [default = false]; -} - -message PauseRequest { - optional bool paused = 1 [default = false]; -} - -message AddTracksToPlaylistRequest { - required PlaylistType playlist_type = 1; - optional int64 playlist_index = 2; // Used if playlist_index == UserPlaylist - repeated string track_uri = 3; -} - -message RemoveTracksFromPlaylistRequest { - required PlaylistType playlist_type = 1; - optional int64 playlist_index = 2; // Used if playlist_index == UserPlaylist - repeated int64 track_index = 3; -} - -// NEXT_ID: 25 -message Message { - // Not currently used - optional int32 id = 18; - - optional LoginRequest login_request = 1; - optional LoginResponse login_response = 2; - optional Playlists playlists_updated = 3; - optional LoadPlaylistRequest load_playlist_request = 4; - optional LoadPlaylistResponse load_playlist_response = 5; - optional PlaybackRequest playback_request = 6; - optional PlaybackError playback_error = 7; - optional SearchRequest search_request = 8; - optional SearchResponse search_response = 9; - optional ImageRequest image_request = 10; - optional ImageResponse image_response = 11; - optional SyncPlaylistRequest sync_playlist_request = 12; - optional SyncPlaylistProgress sync_playlist_progress = 13; - optional BrowseAlbumRequest browse_album_request = 14; - optional BrowseAlbumResponse browse_album_response = 15; - optional SeekRequest seek_request = 16; - optional PlaybackSettings set_playback_settings_request = 17; - optional BrowseToplistRequest browse_toplist_request = 19; - optional BrowseToplistResponse browse_toplist_response = 20; - optional PauseRequest pause_request = 21; - // ID 22 unused. - optional AddTracksToPlaylistRequest add_tracks_to_playlist = 23; - optional RemoveTracksFromPlaylistRequest remove_tracks_from_playlist = 24; -} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e1518e1d..9609171ff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,10 +47,6 @@ include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader) include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader) include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-remote) include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-remote) -if(HAVE_SPOTIFY) - include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob) - include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob) -endif(HAVE_SPOTIFY) include(../cmake/ParseArguments.cmake) @@ -885,37 +881,6 @@ optional_source(HAVE_LIBLASTFM internet/lastfm/lastfmsettingspage.ui ) -# Spotify support -optional_source(HAVE_SPOTIFY - SOURCES - internet/spotify/spotifyserver.cpp - internet/spotify/spotifyservice.cpp - internet/spotify/spotifysettingspage.cpp - internet/spotifywebapi/spotifywebapiservice.cpp - globalsearch/spotifysearchprovider.cpp - globalsearch/spotifywebapisearchprovider.cpp - HEADERS - globalsearch/spotifysearchprovider.h - globalsearch/spotifywebapisearchprovider.h - internet/spotify/spotifyserver.h - internet/spotify/spotifyservice.h - internet/spotify/spotifysettingspage.h - internet/spotifywebapi/spotifywebapiservice.h - UI - internet/spotify/spotifysettingspage.ui -) - -if(HAVE_SPOTIFY) - optional_source(HAVE_SPOTIFY_DOWNLOADER - SOURCES - internet/spotify/spotifyblobdownloader.cpp - HEADERS - internet/spotify/spotifyblobdownloader.h - INCLUDE_DIRECTORIES - ${CRYPTOPP_INCLUDE_DIRS} - ) -endif(HAVE_SPOTIFY) - # Platform specific - OS X optional_source(APPLE INCLUDE_DIRECTORIES @@ -1353,17 +1318,6 @@ if(HAVE_BREAKPAD) endif (LINUX) endif(HAVE_BREAKPAD) -if(HAVE_SPOTIFY) - target_link_libraries(clementine_lib clementine-spotifyblob-messages) -endif(HAVE_SPOTIFY) - -if(HAVE_SPOTIFY_DOWNLOADER) - target_link_libraries(clementine_lib - ${CRYPTOPP_LIBRARIES} - ) - link_directories(${CRYPTOPP_LIBRARY_DIRS}) -endif(HAVE_SPOTIFY_DOWNLOADER) - if(HAVE_LIBPULSE) target_link_libraries(clementine_lib ${LIBPULSE_LIBRARIES}) endif() @@ -1451,9 +1405,6 @@ target_link_libraries(clementine ) # macdeploy.py relies on the blob being built first. -if(HAVE_SPOTIFY_BLOB) - add_dependencies(clementine clementine-spotifyblob) -endif(HAVE_SPOTIFY_BLOB) add_dependencies(clementine clementine-tagreader) set_target_properties(clementine PROPERTIES diff --git a/src/covers/albumcoverloader.cpp b/src/covers/albumcoverloader.cpp index d53f7dfde..d5ec5cd5f 100644 --- a/src/covers/albumcoverloader.cpp +++ b/src/covers/albumcoverloader.cpp @@ -34,9 +34,6 @@ #include "core/tagreaderclient.h" #include "core/utilities.h" #include "internet/core/internetmodel.h" -#ifdef HAVE_SPOTIFY -#include "internet/spotify/spotifyservice.h" -#endif AlbumCoverLoader::AlbumCoverLoader(QObject* parent) : QObject(parent), @@ -180,31 +177,7 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage( remote_tasks_.insert(reply, task); return TryLoadResult(true, false, QImage()); - } -#ifdef HAVE_SPOTIFY - else if (filename.toLower().startsWith("spotify://image/")) { - // HACK: we should add generic image URL handlers - SpotifyService* spotify = InternetModel::Service(); - - if (!connected_spotify_) { - connect(spotify, SIGNAL(ImageLoaded(QString, QImage)), - SLOT(SpotifyImageLoaded(QString, QImage))); - connected_spotify_ = true; - } - - QString id = QUrl(filename).path(); - if (id.startsWith('/')) { - id.remove(0, 1); - } - remote_spotify_tasks_.insert(id, task); - - // Need to schedule this in the spotify service's thread - QMetaObject::invokeMethod(spotify, "LoadImage", Qt::QueuedConnection, - Q_ARG(QString, id)); - return TryLoadResult(true, false, QImage()); - } -#endif - else if (filename.isEmpty()) { + } else if (filename.isEmpty()) { // Avoid "QFSFileEngine::open: No file name specified" messages if we know // that the filename is empty return TryLoadResult(false, false, task.options.default_output_image_); @@ -216,18 +189,6 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage( image.isNull() ? task.options.default_output_image_ : image); } -#ifdef HAVE_SPOTIFY -void AlbumCoverLoader::SpotifyImageLoaded(const QString& id, - const QImage& image) { - if (!remote_spotify_tasks_.contains(id)) return; - - Task task = remote_spotify_tasks_.take(id); - QImage scaled = ScaleAndPad(task.options, image); - emit ImageLoaded(task.id, scaled); - emit ImageLoaded(task.id, scaled, image); -} -#endif - void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply* reply) { reply->deleteLater(); diff --git a/src/covers/albumcoverloader.h b/src/covers/albumcoverloader.h index 0fea77fec..36e983c0f 100644 --- a/src/covers/albumcoverloader.h +++ b/src/covers/albumcoverloader.h @@ -67,9 +67,6 @@ class AlbumCoverLoader : public QObject { protected slots: void ProcessTasks(); void RemoteFetchFinished(QNetworkReply* reply); -#ifdef HAVE_SPOTIFY - void SpotifyImageLoaded(const QString& url, const QImage& image); -#endif protected: enum State { diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index b71592cb3..1489a6d9a 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -39,10 +39,6 @@ #include "devices/cddadevice.h" #endif #include "internet/core/internetmodel.h" -#ifdef HAVE_SPOTIFY -#include "internet/spotify/spotifyserver.h" -#include "internet/spotify/spotifyservice.h" -#endif const int GstEnginePipeline::kGstStateTimeoutNanosecs = 10000000; const int GstEnginePipeline::kFaderFudgeMsec = 2000; @@ -171,58 +167,14 @@ QByteArray GstEnginePipeline::GstUriFromUrl(const QUrl& url) { GstElement* GstEnginePipeline::CreateDecodeBinFromUrl(const QUrl& url) { GstElement* new_bin = nullptr; -#ifdef HAVE_SPOTIFY - if (url.scheme() == "spotify") { - new_bin = gst_bin_new("spotify_bin"); - if (!new_bin) return nullptr; - - // Create elements - GstElement* src = engine_->CreateElement("tcpserversrc", new_bin); - if (!src) { - gst_object_unref(GST_OBJECT(new_bin)); - return nullptr; - } - GstElement* gdp = engine_->CreateElement("gdpdepay", new_bin); - if (!gdp) { - gst_object_unref(GST_OBJECT(new_bin)); - return nullptr; - } - - // Pick a port number - const int port = Utilities::PickUnusedPort(); - g_object_set(G_OBJECT(src), "host", "127.0.0.1", nullptr); - g_object_set(G_OBJECT(src), "port", port, nullptr); - - // Link the elements - gst_element_link(src, gdp); - - // Add a ghost pad - GstPad* pad = gst_element_get_static_pad(gdp, "src"); - gst_element_add_pad(GST_ELEMENT(new_bin), gst_ghost_pad_new("src", pad)); - gst_object_unref(GST_OBJECT(pad)); - - // Tell spotify to start sending data to us. - SpotifyServer* spotify_server = - InternetModel::Service()->server(); - // Need to schedule this in the spotify server's thread - QMetaObject::invokeMethod( - spotify_server, "StartPlayback", Qt::QueuedConnection, - Q_ARG(QString, url.toString()), Q_ARG(quint16, port)); - } else { -#endif - QByteArray uri = GstUriFromUrl(url); - new_bin = engine_->CreateElement("uridecodebin"); - if (!new_bin) return nullptr; - g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr); - CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, - this); - CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this); - CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback, - this); -#ifdef HAVE_SPOTIFY - } -#endif - + QByteArray uri = GstUriFromUrl(url); + new_bin = engine_->CreateElement("uridecodebin"); + if (!new_bin) return nullptr; + g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr); + CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, this); + CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this); + CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback, + this); return new_bin; } @@ -1199,26 +1151,6 @@ GstState GstEnginePipeline::state() const { } QFuture GstEnginePipeline::SetState(GstState state) { -#ifdef HAVE_SPOTIFY - if (current_.url_.scheme() == "spotify" && !buffering_) { - const GstState current_state = this->state(); - - if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) { - SpotifyService* spotify = InternetModel::Service(); - - // Need to schedule this in the spotify service's thread - QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection, - Q_ARG(bool, true)); - } else if (state == GST_STATE_PLAYING && - current_state == GST_STATE_PAUSED) { - SpotifyService* spotify = InternetModel::Service(); - - // Need to schedule this in the spotify service's thread - QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection, - Q_ARG(bool, false)); - } - } -#endif return ConcurrentRun::Run( &set_state_threadpool_, &gst_element_set_state, pipeline_, state); } diff --git a/src/globalsearch/spotifysearchprovider.cpp b/src/globalsearch/spotifysearchprovider.cpp deleted file mode 100644 index d61816d57..000000000 --- a/src/globalsearch/spotifysearchprovider.cpp +++ /dev/null @@ -1,280 +0,0 @@ -/* 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 "spotifysearchprovider.h" - -#include -#include - -#include "core/logging.h" -#include "internet/core/internetmodel.h" -#include "internet/spotify/spotifyserver.h" -#include "playlist/songmimedata.h" -#include "ui/iconloader.h" - -namespace { -const int kSearchSongLimit = 5; -const int kSearchAlbumLimit = 20; -} // namespace - -SpotifySearchProvider::SpotifySearchProvider(Application* app, QObject* parent) - : SearchProvider(app, parent), server_(nullptr), service_(nullptr) { - Init("Spotify", "spotify", IconLoader::Load("spotify", IconLoader::Provider), - WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote | - CanShowConfig | CanGiveSuggestions); -} - -SpotifyServer* SpotifySearchProvider::server() { - if (server_) return server_; - - if (!service_) service_ = InternetModel::Service(); - - if (service_->login_state() != SpotifyService::LoginState_LoggedIn) - return nullptr; - - if (!service_->IsBlobInstalled()) return nullptr; - - server_ = service_->server(); - connect(server_, SIGNAL(SearchResults(cpb::spotify::SearchResponse)), - SLOT(SearchFinishedSlot(cpb::spotify::SearchResponse))); - connect(server_, SIGNAL(ImageLoaded(QString, QImage)), - SLOT(ArtLoadedSlot(QString, QImage))); - connect(server_, SIGNAL(destroyed()), SLOT(ServerDestroyed())); - connect(server_, SIGNAL(StarredLoaded(cpb::spotify::LoadPlaylistResponse)), - SLOT(SuggestionsLoaded(cpb::spotify::LoadPlaylistResponse))); - connect(server_, - SIGNAL(ToplistBrowseResults(cpb::spotify::BrowseToplistResponse)), - SLOT(SuggestionsLoaded(cpb::spotify::BrowseToplistResponse))); - - return server_; -} - -void SpotifySearchProvider::ServerDestroyed() { server_ = nullptr; } - -void SpotifySearchProvider::SearchAsync(int id, const QString& query) { - SpotifyServer* s = server(); - if (!s) { - emit SearchFinished(id); - return; - } - - PendingState state; - state.orig_id_ = id; - state.tokens_ = TokenizeQuery(query); - - const QString query_string = state.tokens_.join(" "); - s->Search(query_string, kSearchSongLimit, kSearchAlbumLimit); - queries_[query_string] = state; -} - -void SpotifySearchProvider::SearchFinishedSlot( - const cpb::spotify::SearchResponse& response) { - QString query_string = QString::fromUtf8(response.request().query().c_str()); - QMap::iterator it = queries_.find(query_string); - if (it == queries_.end()) return; - - PendingState state = it.value(); - queries_.erase(it); - - /* Here we clean up Spotify's results for our purposes - * - * Since Spotify doesn't give us an album artist, - * we pick one, so there's a single album artist - * per album to use for sorting. - * - * We also drop any of the single tracks returned - * if they are already represented in an album - * - * This eliminates frequent duplicates from the - * "Top Tracks" results that Spotify sometimes - * returns - */ - QMap album_dedup; - ResultList ret; - - for (int i = 0; i < response.album_size(); ++i) { - const cpb::spotify::Album& album = response.album(i); - - QHash artist_count; - QString majority_artist; - int majority_count = 0; - - /* We go through and find the artist that is - * represented most frequently in the artist - * - * For most albums this will just be one artist, - * but this ensures we have a consistent album artist for - * soundtracks, compilations, contributing artists, etc - */ - for (int j = 0; j < album.track_size(); ++j) { - // Each track can have multiple artists attributed, check them all - for (int k = 0; k < album.track(j).artist_size(); ++k) { - QString artist = QStringFromStdString(album.track(j).artist(k)); - if (artist_count.contains(artist)) { - artist_count[artist]++; - } else { - artist_count[artist] = 1; - } - if (artist_count[artist] > majority_count) { - majority_count = artist_count[artist]; - majority_artist = artist; - } - } - } - - for (int j = 0; j < album.track_size(); ++j) { - // Insert the album/track title into the dedup map - // so we can check tracks against it below - album_dedup.insertMulti(album.track(j).album(), album.track(j).title()); - - Result result(this); - SpotifyService::SongFromProtobuf(album.track(j), &result.metadata_); - - // Just use the album index as an id. - result.metadata_.set_album_id(i); - result.metadata_.set_albumartist(majority_artist); - - ret << result; - } - } - - for (int i = 0; i < response.result_size(); ++i) { - const cpb::spotify::Track& track = response.result(i); - - // Check this track/album against tracks we've already seen - // in the album results, and skip if it's a duplicate - if (album_dedup.contains(track.album()) && - album_dedup.values(track.album()).contains(track.title())) { - continue; - } - - Result result(this); - SpotifyService::SongFromProtobuf(track, &result.metadata_); - - ret << result; - } - - emit ResultsAvailable(state.orig_id_, ret); - emit SearchFinished(state.orig_id_); -} - -void SpotifySearchProvider::LoadArtAsync(int id, const Result& result) { - SpotifyServer* s = server(); - if (!s) { - emit ArtLoaded(id, QImage()); - return; - } - - QString image_id = QUrl(result.metadata_.art_automatic()).path(); - if (image_id.startsWith('/')) image_id.remove(0, 1); - - pending_art_[image_id] = id; - s->LoadImage(image_id); -} - -void SpotifySearchProvider::ArtLoadedSlot(const QString& id, - const QImage& image) { - QMap::iterator it = pending_art_.find(id); - if (it == pending_art_.end()) return; - - const int orig_id = it.value(); - pending_art_.erase(it); - - emit ArtLoaded(orig_id, ScaleAndPad(image)); -} - -bool SpotifySearchProvider::IsLoggedIn() { - if (server()) { - return service_->IsLoggedIn(); - } - return false; -} - -void SpotifySearchProvider::ShowConfig() { - if (service_) { - return service_->ShowConfig(); - } -} - -void SpotifySearchProvider::AddSuggestionFromTrack( - const cpb::spotify::Track& track) { - if (!track.title().empty()) { - suggestions_.insert(QString::fromUtf8(track.title().c_str())); - } - for (int j = 0; j < track.artist_size(); ++j) { - if (!track.artist(j).empty()) { - suggestions_.insert(QString::fromUtf8(track.artist(j).c_str())); - } - } - if (!track.album().empty()) { - suggestions_.insert(QString::fromUtf8(track.album().c_str())); - } -} - -void SpotifySearchProvider::AddSuggestionFromAlbum( - const cpb::spotify::Album& album) { - AddSuggestionFromTrack(album.metadata()); - for (int i = 0; i < album.track_size(); ++i) { - AddSuggestionFromTrack(album.track(i)); - } -} - -void SpotifySearchProvider::SuggestionsLoaded( - const cpb::spotify::LoadPlaylistResponse& playlist) { - for (int i = 0; i < playlist.track_size(); ++i) { - AddSuggestionFromTrack(playlist.track(i)); - } -} - -void SpotifySearchProvider::SuggestionsLoaded( - const cpb::spotify::BrowseToplistResponse& response) { - for (int i = 0; i < response.track_size(); ++i) { - AddSuggestionFromTrack(response.track(i)); - } - for (int i = 0; i < response.album_size(); ++i) { - AddSuggestionFromAlbum(response.album(i)); - } -} - -void SpotifySearchProvider::LoadSuggestions() { - if (!server()) { - return; - } - server()->LoadStarred(); - server()->LoadToplist(); -} - -QStringList SpotifySearchProvider::GetSuggestions(int count) { - if (suggestions_.empty()) { - LoadSuggestions(); - return QStringList(); - } - - QStringList all_suggestions = suggestions_.values(); - - std::mt19937 gen(std::time(0)); - std::uniform_int_distribution<> random(0, all_suggestions.size() - 1); - - QSet candidates; - - const int max = qMin(count, all_suggestions.size()); - while (candidates.size() < max) { - const int index = random(gen); - candidates.insert(all_suggestions[index]); - } - return candidates.toList(); -} diff --git a/src/globalsearch/spotifysearchprovider.h b/src/globalsearch/spotifysearchprovider.h deleted file mode 100644 index 41169194a..000000000 --- a/src/globalsearch/spotifysearchprovider.h +++ /dev/null @@ -1,67 +0,0 @@ -/* 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 SPOTIFYSEARCHPROVIDER_H -#define SPOTIFYSEARCHPROVIDER_H - -#include "internet/spotify/spotifyservice.h" -#include "searchprovider.h" -#include "spotifymessages.pb.h" - -class SpotifyServer; - -class SpotifySearchProvider : public SearchProvider { - Q_OBJECT - - public: - SpotifySearchProvider(Application* app, QObject* parent = nullptr); - - void SearchAsync(int id, const QString& query) override; - void LoadArtAsync(int id, const Result& result) override; - QStringList GetSuggestions(int count) override; - - // SearchProvider - bool IsLoggedIn() override; - void ShowConfig() override; - InternetService* internet_service() override { return service_; } - - private slots: - void ServerDestroyed(); - void SearchFinishedSlot(const cpb::spotify::SearchResponse& response); - void ArtLoadedSlot(const QString& id, const QImage& image); - void SuggestionsLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void SuggestionsLoaded(const cpb::spotify::BrowseToplistResponse& response); - - private: - SpotifyServer* server(); - - void LoadSuggestions(); - void AddSuggestionFromTrack(const cpb::spotify::Track& track); - void AddSuggestionFromAlbum(const cpb::spotify::Album& album); - - private: - SpotifyServer* server_; - SpotifyService* service_; - - QMap queries_; - QMap pending_art_; - QMap pending_tracks_; - - QSet suggestions_; -}; - -#endif // SPOTIFYSEARCHPROVIDER_H diff --git a/src/globalsearch/spotifywebapisearchprovider.cpp b/src/globalsearch/spotifywebapisearchprovider.cpp deleted file mode 100644 index f73cd6391..000000000 --- a/src/globalsearch/spotifywebapisearchprovider.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* This file is part of Clementine. - Copyright 2021, Kenman Tsang - - 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 "spotifywebapisearchprovider.h" - -#include - -#include - -#include "internet/spotifywebapi/spotifywebapiservice.h" -#include "ui/iconloader.h" - -namespace { -static constexpr int kNoRunningSearch = -1; -} - -SpotifyWebApiSearchProvider::SpotifyWebApiSearchProvider( - Application* app, SpotifyWebApiService* parent) - : SearchProvider(app, parent), - parent_{parent}, - last_search_id_{kNoRunningSearch} { - Init("Spotify (Experimential)", "spotify_web_api", - IconLoader::Load("spotify", IconLoader::Provider), - WantsDelayedQueries | WantsSerialisedArtQueries | ArtIsProbablyRemote | - CanGiveSuggestions); - - connect(parent, &SpotifyWebApiService::SearchFinished, this, - &SpotifyWebApiSearchProvider::SearchFinishedSlot); -} - -void SpotifyWebApiSearchProvider::SearchAsync(int id, const QString& query) { - if (last_search_id_ != kNoRunningSearch) { - // Cancel last pending search - emit SearchFinished(last_search_id_); - - // Set the pending query - last_search_id_ = id; - last_query_ = query; - - // And wait for the current search to be completed - return; - } - - last_search_id_ = id; - last_query_ = query; - - parent_->Search(last_search_id_, last_query_); -} - -void SpotifyWebApiSearchProvider::SearchFinishedSlot( - int searchId, const QList& apiResult) { - ResultList ret; - for (auto&& item : apiResult) { - Result result{this}; - result.group_automatically_ = true; - result.metadata_ = item; - ret += result; - } - emit ResultsAvailable(searchId, ret); - emit SearchFinished(searchId); - - // Search again if we have a pending query - if (searchId != last_search_id_) { - parent_->Search(last_search_id_, last_query_); - } else { - last_search_id_ = kNoRunningSearch; - } -} - -void SpotifyWebApiSearchProvider::LoadArtAsync(int id, const Result& result) { - // TODO -} - -void SpotifyWebApiSearchProvider::ShowConfig() {} - -InternetService* SpotifyWebApiSearchProvider::internet_service() { - return parent_; -} - -QStringList SpotifyWebApiSearchProvider::GetSuggestions(int count) { - // TODO - return QStringList{}; -} diff --git a/src/globalsearch/spotifywebapisearchprovider.h b/src/globalsearch/spotifywebapisearchprovider.h deleted file mode 100644 index f59a8d0ed..000000000 --- a/src/globalsearch/spotifywebapisearchprovider.h +++ /dev/null @@ -1,48 +0,0 @@ -/* This file is part of Clementine. - Copyright 2021, Kenman Tsang - - 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 SPOTIFYWEBAPISEARCHPROVIDER_H -#define SPOTIFYWEBAPISEARCHPROVIDER_H - -#include "core/song.h" -#include "searchprovider.h" - -class SpotifyWebApiService; - -class SpotifyWebApiSearchProvider : public SearchProvider { - Q_OBJECT - - public: - SpotifyWebApiSearchProvider(Application* app, SpotifyWebApiService* parent); - - void SearchAsync(int id, const QString& query) override; - void LoadArtAsync(int id, const Result& result) override; - QStringList GetSuggestions(int count) override; - - void ShowConfig() override; - InternetService* internet_service() override; - - private slots: - void SearchFinishedSlot(int id, const QList&); - - private: - SpotifyWebApiService* parent_; - int last_search_id_; - QString last_query_; -}; - -#endif // SPOTIFYWEBAPISEARCHPROVIDER_H diff --git a/src/internet/core/internetmodel.cpp b/src/internet/core/internetmodel.cpp index f6056a3c9..01a08b7e0 100644 --- a/src/internet/core/internetmodel.cpp +++ b/src/internet/core/internetmodel.cpp @@ -60,10 +60,6 @@ #ifdef HAVE_SEAFILE #include "internet/seafile/seafileservice.h" #endif -#ifdef HAVE_SPOTIFY -#include "internet/spotify/spotifyservice.h" -#include "internet/spotifywebapi/spotifywebapiservice.h" -#endif using smart_playlists::Generator; using smart_playlists::GeneratorMimeData; @@ -99,10 +95,6 @@ InternetModel::InternetModel(Application* app, QObject* parent) AddService(new SomaFMService(app, this)); AddService(new IntergalacticFMService(app, this)); AddService(new RadioBrowserService(app, this)); -#ifdef HAVE_SPOTIFY - AddService(new SpotifyService(app, this)); - AddService(new SpotifyWebApiService(app, this)); -#endif AddService(new SubsonicService(app, this)); #ifdef HAVE_BOX AddService(new BoxService(app, this)); diff --git a/src/internet/spotify/spotifyblobdownloader.cpp b/src/internet/spotify/spotifyblobdownloader.cpp deleted file mode 100644 index 6cf540c6b..000000000 --- a/src/internet/spotify/spotifyblobdownloader.cpp +++ /dev/null @@ -1,281 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011-2012, David Sansome - Copyright 2014, Krzysztof Sobiecki - Copyright 2014, John Maguire - - 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 "spotifyblobdownloader.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "core/arraysize.h" -#include "core/logging.h" -#include "core/network.h" -#include "core/utilities.h" -#include "spotifyservice.h" - -#ifdef Q_OS_UNIX -#include -#endif - -#ifdef HAVE_CRYPTOPP -#include -#include - -// Compatibility with cryptocpp >= 6.0.0 -namespace CryptoPP { -typedef unsigned char byte; -} -#endif // HAVE_CRYPTOPP - -const char* SpotifyBlobDownloader::kSignatureSuffix = ".sha512"; - -SpotifyBlobDownloader::SpotifyBlobDownloader(const QString& version, - const QString& path, - QObject* parent) - : QObject(parent), - version_(version), - path_(path), - network_(new NetworkAccessManager(this)), - progress_(new QProgressDialog(tr("Downloading Spotify plugin"), - tr("Cancel"), 0, 0)) { - progress_->setWindowTitle(QCoreApplication::applicationName()); - connect(progress_, SIGNAL(canceled()), SLOT(Cancel())); -} - -SpotifyBlobDownloader::~SpotifyBlobDownloader() { - qDeleteAll(replies_); - replies_.clear(); - - delete progress_; -} - -bool SpotifyBlobDownloader::Prompt() { - QMessageBox::StandardButton ret = QMessageBox::question( - nullptr, tr("Spotify plugin not installed"), - tr("An additional plugin is required to use Spotify in Clementine. " - "Would you like to download and install it now?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - return ret == QMessageBox::Yes; -} - -void SpotifyBlobDownloader::Start() { - qDeleteAll(replies_); - replies_.clear(); - - const QStringList filenames = - QStringList() << "blob" - << "blob" + QString(kSignatureSuffix) - << "libspotify.so.12.1.51" - << "libspotify.so.12.1.51" + QString(kSignatureSuffix); - - for (const QString& filename : filenames) { - const QUrl url(SpotifyService::kBlobDownloadUrl + version_ + "/" + - filename); - qLog(Info) << "Downloading" << url; - - QNetworkRequest req(url); - // This policy will work as long as there isn't a redirect from https to - // http. This is a legacy attribute that should be changed to use - // RedirectPolicyAttribute when Qt 5.9 is the lowest supported version. - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - - QNetworkReply* reply = network_->get(req); - connect(reply, SIGNAL(finished()), SLOT(ReplyFinished())); - connect(reply, SIGNAL(downloadProgress(qint64, qint64)), - SLOT(ReplyProgress())); - - replies_ << reply; - } - - progress_->show(); -} - -void SpotifyBlobDownloader::ReplyFinished() { - QNetworkReply* reply = qobject_cast(sender()); - if (reply->error() != QNetworkReply::NoError) { - // Handle network errors - ShowError(reply->errorString()); - return; - } - - // Is everything finished? - for (QNetworkReply* reply : replies_) { - if (!reply->isFinished()) { - return; - } - } - - // Read files into memory first. - QMap file_data; - QStringList signature_filenames; - - for (QNetworkReply* reply : replies_) { - const QString filename = reply->url().path().section('/', -1, -1); - - if (filename.endsWith(kSignatureSuffix)) { - signature_filenames << filename; - } - - file_data[filename] = reply->readAll(); - } - - if (!CheckSignature(file_data, signature_filenames)) { - qLog(Warning) << "Signature checks failed"; - return; - } - - // Make the destination directory and write the files into it - QDir().mkpath(path_); - - for (const QString& filename : file_data.keys()) { - const QString dest_path = path_ + "/" + filename; - - if (filename.endsWith(kSignatureSuffix)) continue; - - qLog(Info) << "Writing" << dest_path; - - QFile file(dest_path); - if (!file.open(QIODevice::WriteOnly)) { - ShowError("Failed to open " + dest_path + " for writing"); - return; - } - - file.write(file_data[filename]); - file.close(); - file.setPermissions(QFile::Permissions(0x7755)); - -#ifdef Q_OS_UNIX - const int so_pos = filename.lastIndexOf(".so."); - if (so_pos != -1) { - QString link_path = path_ + "/" + filename.left(so_pos + 3); - QStringList version_parts = filename.mid(so_pos + 4).split('.'); - - while (!version_parts.isEmpty()) { - qLog(Debug) << "Linking" << dest_path << "to" << link_path; - int ret = symlink(dest_path.toLocal8Bit().constData(), - link_path.toLocal8Bit().constData()); - - if (ret != 0) { - qLog(Warning) << "Creating symlink failed with return code" << ret; - } - - link_path += "." + version_parts.takeFirst(); - } - } -#endif // Q_OS_UNIX - } - - EmitFinished(); -} - -bool SpotifyBlobDownloader::CheckSignature( - const QMap& file_data, - const QStringList& signature_filenames) { -#ifdef HAVE_CRYPTOPP - QFile public_key_file(":/clementine-spotify-public.pem"); - public_key_file.open(QIODevice::ReadOnly); - const QByteArray public_key_data = ConvertPEMToDER(public_key_file.readAll()); - - try { - CryptoPP::ByteQueue bytes; - bytes.Put( - reinterpret_cast(public_key_data.constData()), - public_key_data.size()); - bytes.MessageEnd(); - - CryptoPP::RSA::PublicKey public_key; - public_key.Load(bytes); - - CryptoPP::RSASS::Verifier verifier( - public_key); - - for (const QString& signature_filename : signature_filenames) { - QString actual_filename = signature_filename; - actual_filename.remove(kSignatureSuffix); - - const bool result = - verifier.VerifyMessage(reinterpret_cast( - file_data[actual_filename].constData()), - file_data[actual_filename].size(), - reinterpret_cast( - file_data[signature_filename].constData()), - file_data[signature_filename].size()); - qLog(Debug) << "Verifying" << actual_filename << "against" - << signature_filename << result; - if (!result) { - ShowError("Invalid signature: " + actual_filename); - return false; - } - } - } catch (std::exception& e) { - // This should only happen if we fail to parse our own key. - qLog(Debug) << "Verifying spotify blob signature failed:" << e.what(); - return false; - } - return true; -#else - return false; -#endif // HAVE_CRYPTOPP -} - -QByteArray SpotifyBlobDownloader::ConvertPEMToDER(const QByteArray& pem) { - QSslKey key(pem, QSsl::Rsa, QSsl::Pem, QSsl::PublicKey); - Q_ASSERT(!key.isNull()); - return key.toDer(); -} - -void SpotifyBlobDownloader::ReplyProgress() { - int progress = 0; - int total = 0; - - for (QNetworkReply* reply : replies_) { - progress += reply->bytesAvailable(); - total += reply->rawHeader("Content-Length").toInt(); - } - - progress_->setMaximum(total); - progress_->setValue(progress); -} - -void SpotifyBlobDownloader::Cancel() { deleteLater(); } - -void SpotifyBlobDownloader::ShowError(const QString& message) { - // Stop any remaining replies before showing the dialog so they don't - // carry on in the background - for (QNetworkReply* reply : replies_) { - disconnect(reply, 0, this, 0); - reply->abort(); - } - - qLog(Warning) << message; - QMessageBox::warning(nullptr, tr("Error downloading Spotify plugin"), message, - QMessageBox::Close); - deleteLater(); -} - -void SpotifyBlobDownloader::EmitFinished() { - emit Finished(); - deleteLater(); -} diff --git a/src/internet/spotify/spotifyblobdownloader.h b/src/internet/spotify/spotifyblobdownloader.h deleted file mode 100644 index d565f62fa..000000000 --- a/src/internet/spotify/spotifyblobdownloader.h +++ /dev/null @@ -1,70 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - Copyright 2014, Krzysztof Sobiecki - Copyright 2014, John Maguire - - 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 INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_ -#define INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_ - -#include -#include - -class QNetworkAccessManager; -class QNetworkReply; -class QProgressDialog; - -class SpotifyBlobDownloader : public QObject { - Q_OBJECT - - public: - SpotifyBlobDownloader(const QString& version, const QString& path, - QObject* parent = nullptr); - ~SpotifyBlobDownloader(); - - static const char* kSignatureSuffix; - - static bool Prompt(); - - void Start(); - - signals: - void Finished(); - - private slots: - void ReplyFinished(); - void ReplyProgress(); - void Cancel(); - - private: - void ShowError(const QString& message); - void EmitFinished(); - - bool CheckSignature(const QMap& file_data, - const QStringList& signature_filenames); - static QByteArray ConvertPEMToDER(const QByteArray& pem); - - private: - QString version_; - QString path_; - - QNetworkAccessManager* network_; - QList replies_; - - QProgressDialog* progress_; -}; - -#endif // INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_ diff --git a/src/internet/spotify/spotifyserver.cpp b/src/internet/spotify/spotifyserver.cpp deleted file mode 100644 index bc600f4d9..000000000 --- a/src/internet/spotify/spotifyserver.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011-2012, 2014, John Maguire - Copyright 2011-2012, 2014, David Sansome - Copyright 2014, Arnaud Bienner - Copyright 2014, pie.or.paj - Copyright 2014, Krzysztof Sobiecki - - 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 -#include -#include -#include - -#include "core/closure.h" -#include "core/logging.h" -#include "spotifymessages.pb.h" - -SpotifyServer::SpotifyServer(QObject* parent) - : AbstractMessageHandler(nullptr, parent), - server_(new QTcpServer(this)), - logged_in_(false) { - 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() { - QTcpSocket* socket = server_->nextPendingConnection(); - SetDevice(socket); - - qLog(Info) << "Connection from port" << socket->peerPort(); - - // Send any login messages that were queued before the client connected - for (const cpb::spotify::Message& message : queued_login_messages_) { - SendOrQueueMessage(message); - } - queued_login_messages_.clear(); - - // Don't take any more connections from clients - disconnect(server_, SIGNAL(newConnection()), this, 0); -} - -void SpotifyServer::SendOrQueueMessage(const cpb::spotify::Message& message) { - const bool is_login_message = message.has_login_request(); - - QList* queue = - is_login_message ? &queued_login_messages_ : &queued_messages_; - - if (!device_ || (!is_login_message && !logged_in_)) { - queue->append(message); - } else { - SendMessage(message); - } -} - -void SpotifyServer::Login(const QString& username, const QString& password, - cpb::spotify::Bitrate bitrate, - bool volume_normalisation) { - cpb::spotify::Message message; - - cpb::spotify::LoginRequest* request = message.mutable_login_request(); - request->set_username(DataCommaSizeFromQString(username)); - if (!password.isEmpty()) { - request->set_password(DataCommaSizeFromQString(password)); - } - request->mutable_playback_settings()->set_bitrate(bitrate); - request->mutable_playback_settings()->set_volume_normalisation( - volume_normalisation); - - SendOrQueueMessage(message); -} - -void SpotifyServer::SetPlaybackSettings(cpb::spotify::Bitrate bitrate, - bool volume_normalisation) { - cpb::spotify::Message message; - - cpb::spotify::PlaybackSettings* request = - message.mutable_set_playback_settings_request(); - request->set_bitrate(bitrate); - request->set_volume_normalisation(volume_normalisation); - - SendOrQueueMessage(message); -} - -void SpotifyServer::MessageArrived(const cpb::spotify::Message& message) { - if (message.has_login_response()) { - const cpb::spotify::LoginResponse& response = message.login_response(); - logged_in_ = response.success(); - - if (response.success()) { - // Send any messages that were queued before the client logged in - for (const cpb::spotify::Message& message : queued_messages_) { - SendOrQueueMessage(message); - } - queued_messages_.clear(); - } - - emit LoginCompleted(response.success(), - QStringFromStdString(response.error()), - response.error_code()); - } else if (message.has_playlists_updated()) { - emit PlaylistsUpdated(message.playlists_updated()); - } else if (message.has_load_playlist_response()) { - const cpb::spotify::LoadPlaylistResponse& response = - message.load_playlist_response(); - - switch (response.request().type()) { - case cpb::spotify::Inbox: - emit InboxLoaded(response); - break; - - case cpb::spotify::Starred: - emit StarredLoaded(response); - break; - - case cpb::spotify::UserPlaylist: - emit UserPlaylistLoaded(response); - break; - } - } else if (message.has_playback_error()) { - emit PlaybackError(QStringFromStdString(message.playback_error().error())); - } else if (message.has_search_response()) { - emit SearchResults(message.search_response()); - } else if (message.has_image_response()) { - const cpb::spotify::ImageResponse& response = message.image_response(); - const QString id = QStringFromStdString(response.id()); - - if (response.has_data()) { - emit ImageLoaded( - id, QImage::fromData( - QByteArray(response.data().data(), response.data().size()))); - } else { - emit ImageLoaded(id, QImage()); - } - } else if (message.has_sync_playlist_progress()) { - emit SyncPlaylistProgress(message.sync_playlist_progress()); - } else if (message.has_browse_album_response()) { - emit AlbumBrowseResults(message.browse_album_response()); - } else if (message.has_browse_toplist_response()) { - emit ToplistBrowseResults(message.browse_toplist_response()); - } -} - -void SpotifyServer::LoadPlaylist(cpb::spotify::PlaylistType type, int index) { - cpb::spotify::Message message; - cpb::spotify::LoadPlaylistRequest* req = - message.mutable_load_playlist_request(); - - req->set_type(type); - if (index != -1) { - req->set_user_playlist_index(index); - } - - SendOrQueueMessage(message); -} - -void SpotifyServer::SyncPlaylist(cpb::spotify::PlaylistType type, int index, - bool offline) { - cpb::spotify::Message message; - cpb::spotify::SyncPlaylistRequest* req = - message.mutable_sync_playlist_request(); - req->mutable_request()->set_type(type); - if (index != -1) { - req->mutable_request()->set_user_playlist_index(index); - } - req->set_offline_sync(offline); - - SendOrQueueMessage(message); -} - -void SpotifyServer::SyncInbox() { SyncPlaylist(cpb::spotify::Inbox, -1, true); } - -void SpotifyServer::SyncStarred() { - SyncPlaylist(cpb::spotify::Starred, -1, true); -} - -void SpotifyServer::SyncUserPlaylist(int index) { - Q_ASSERT(index >= 0); - SyncPlaylist(cpb::spotify::UserPlaylist, index, true); -} - -void SpotifyServer::LoadInbox() { LoadPlaylist(cpb::spotify::Inbox); } - -void SpotifyServer::LoadStarred() { LoadPlaylist(cpb::spotify::Starred); } - -void SpotifyServer::LoadUserPlaylist(int index) { - Q_ASSERT(index >= 0); - LoadPlaylist(cpb::spotify::UserPlaylist, index); -} - -void SpotifyServer::AddSongsToStarred(const QList& songs_urls) { - AddSongsToPlaylist(cpb::spotify::Starred, songs_urls); -} - -void SpotifyServer::AddSongsToUserPlaylist(int playlist_index, - const QList& songs_urls) { - AddSongsToPlaylist(cpb::spotify::UserPlaylist, songs_urls, playlist_index); -} - -void SpotifyServer::AddSongsToPlaylist( - const cpb::spotify::PlaylistType playlist_type, - const QList& songs_urls, int playlist_index) { - cpb::spotify::Message message; - cpb::spotify::AddTracksToPlaylistRequest* req = - message.mutable_add_tracks_to_playlist(); - req->set_playlist_type(playlist_type); - req->set_playlist_index(playlist_index); - for (const QUrl& song_url : songs_urls) { - req->add_track_uri(DataCommaSizeFromQString(song_url.toString())); - } - SendOrQueueMessage(message); -} - -void SpotifyServer::RemoveSongsFromStarred( - const QList& songs_indices_to_remove) { - RemoveSongsFromPlaylist(cpb::spotify::Starred, songs_indices_to_remove); -} - -void SpotifyServer::RemoveSongsFromUserPlaylist( - int playlist_index, const QList& songs_indices_to_remove) { - RemoveSongsFromPlaylist(cpb::spotify::UserPlaylist, songs_indices_to_remove, - playlist_index); -} - -void SpotifyServer::RemoveSongsFromPlaylist( - const cpb::spotify::PlaylistType playlist_type, - const QList& songs_indices_to_remove, int playlist_index) { - cpb::spotify::Message message; - cpb::spotify::RemoveTracksFromPlaylistRequest* req = - message.mutable_remove_tracks_from_playlist(); - req->set_playlist_type(playlist_type); - if (playlist_type == cpb::spotify::UserPlaylist) { - req->set_playlist_index(playlist_index); - } - for (int song_index : songs_indices_to_remove) { - req->add_track_index(song_index); - } - SendOrQueueMessage(message); -} - -void SpotifyServer::StartPlayback(const QString& uri, quint16 port) { - cpb::spotify::Message message; - cpb::spotify::PlaybackRequest* req = message.mutable_playback_request(); - - req->set_track_uri(DataCommaSizeFromQString(uri)); - req->set_media_port(port); - SendOrQueueMessage(message); -} - -void SpotifyServer::Seek(qint64 offset_nsec) { - cpb::spotify::Message message; - cpb::spotify::SeekRequest* req = message.mutable_seek_request(); - - req->set_offset_nsec(offset_nsec); - SendOrQueueMessage(message); -} - -void SpotifyServer::Search(const QString& text, int limit, int limit_album) { - cpb::spotify::Message message; - cpb::spotify::SearchRequest* req = message.mutable_search_request(); - - req->set_query(DataCommaSizeFromQString(text)); - req->set_limit(limit); - req->set_limit_album(limit_album); - SendOrQueueMessage(message); -} - -void SpotifyServer::LoadImage(const QString& id) { - cpb::spotify::Message message; - cpb::spotify::ImageRequest* req = message.mutable_image_request(); - - req->set_id(DataCommaSizeFromQString(id)); - SendOrQueueMessage(message); -} - -void SpotifyServer::AlbumBrowse(const QString& uri) { - cpb::spotify::Message message; - cpb::spotify::BrowseAlbumRequest* req = - message.mutable_browse_album_request(); - - req->set_uri(DataCommaSizeFromQString(uri)); - SendOrQueueMessage(message); -} - -void SpotifyServer::LoadToplist() { - cpb::spotify::Message message; - cpb::spotify::BrowseToplistRequest* req = - message.mutable_browse_toplist_request(); - req->set_type(cpb::spotify::BrowseToplistRequest::Tracks); - req->set_region(cpb::spotify::BrowseToplistRequest::Everywhere); - - SendOrQueueMessage(message); -} - -void SpotifyServer::SetPaused(const bool paused) { - cpb::spotify::Message message; - cpb::spotify::PauseRequest* req = message.mutable_pause_request(); - req->set_paused(paused); - SendOrQueueMessage(message); -} diff --git a/src/internet/spotify/spotifyserver.h b/src/internet/spotify/spotifyserver.h deleted file mode 100644 index 6609d4d78..000000000 --- a/src/internet/spotify/spotifyserver.h +++ /dev/null @@ -1,112 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011-2012, 2014, John Maguire - Copyright 2011-2012, 2014, David Sansome - Copyright 2014, Arnaud Bienner - Copyright 2014, pie.or.paj - Copyright 2014, Krzysztof Sobiecki - - 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 INTERNET_SPOTIFY_SPOTIFYSERVER_H_ -#define INTERNET_SPOTIFY_SPOTIFYSERVER_H_ - -#include -#include - -#include "core/messagehandler.h" -#include "spotifymessages.pb.h" - -class QTcpServer; -class QTcpSocket; - -class SpotifyServer : public AbstractMessageHandler { - Q_OBJECT - - public: - explicit SpotifyServer(QObject* parent = nullptr); - - void Init(); - void Login(const QString& username, const QString& password, - cpb::spotify::Bitrate bitrate, bool volume_normalisation); - - void LoadStarred(); - void SyncStarred(); - void LoadInbox(); - void SyncInbox(); - void LoadUserPlaylist(int index); - void SyncUserPlaylist(int index); - void AddSongsToStarred(const QList& songs_urls); - void AddSongsToUserPlaylist(int playlist_index, - const QList& songs_urls); - void RemoveSongsFromUserPlaylist(int playlist_index, - const QList& songs_indices_to_remove); - void RemoveSongsFromStarred(const QList& songs_indices_to_remove); - void Search(const QString& text, int limit, int limit_album = 0); - void LoadImage(const QString& id); - void AlbumBrowse(const QString& uri); - void SetPlaybackSettings(cpb::spotify::Bitrate bitrate, - bool volume_normalisation); - void LoadToplist(); - void SetPaused(const bool paused); - - int server_port() const; - - public slots: - void StartPlayback(const QString& uri, quint16 port); - void Seek(qint64 offset_nsec); - - signals: - void LoginCompleted(bool success, const QString& error, - cpb::spotify::LoginResponse_Error error_code); - void PlaylistsUpdated(const cpb::spotify::Playlists& playlists); - - void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void PlaybackError(const QString& message); - void SearchResults(const cpb::spotify::SearchResponse& response); - void ImageLoaded(const QString& id, const QImage& image); - void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress); - void AlbumBrowseResults(const cpb::spotify::BrowseAlbumResponse& response); - void ToplistBrowseResults( - const cpb::spotify::BrowseToplistResponse& response); - - protected: - void MessageArrived(const cpb::spotify::Message& message); - - private slots: - void NewConnection(); - - private: - void LoadPlaylist(cpb::spotify::PlaylistType type, int index = -1); - void SyncPlaylist(cpb::spotify::PlaylistType type, int index, bool offline); - void AddSongsToPlaylist(const cpb::spotify::PlaylistType playlist_type, - const QList& songs_urls, - // Used iff type is user_playlist - int playlist_index = -1); - void RemoveSongsFromPlaylist(const cpb::spotify::PlaylistType playlist_type, - const QList& songs_indices_to_remove, - // Used iff type is user_playlist - int playlist_index = -1); - void SendOrQueueMessage(const cpb::spotify::Message& message); - - QTcpServer* server_; - bool logged_in_; - - QList queued_login_messages_; - QList queued_messages_; -}; - -#endif // INTERNET_SPOTIFY_SPOTIFYSERVER_H_ diff --git a/src/internet/spotify/spotifyservice.cpp b/src/internet/spotify/spotifyservice.cpp deleted file mode 100644 index 6c77b8a82..000000000 --- a/src/internet/spotify/spotifyservice.cpp +++ /dev/null @@ -1,993 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011-2014, David Sansome - Copyright 2011, Tyler Rhodes - Copyright 2011-2012, 2014, John Maguire - Copyright 2012, 2014, Arnaud Bienner - Copyright 2014, Chocobozzz - Copyright 2014, pie.or.paj - Copyright 2014, Krzysztof Sobiecki - - 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 "spotifyservice.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "blobversion.h" -#include "config.h" -#include "core/application.h" -#include "core/database.h" -#include "core/logging.h" -#include "core/mergedproxymodel.h" -#include "core/player.h" -#include "core/taskmanager.h" -#include "core/timeconstants.h" -#include "core/utilities.h" -#include "globalsearch/globalsearch.h" -#include "globalsearch/spotifysearchprovider.h" -#include "internet/core/internetmodel.h" -#include "internet/core/searchboxwidget.h" -#include "playlist/playlist.h" -#include "playlist/playlistcontainer.h" -#include "playlist/playlistmanager.h" -#include "spotifyserver.h" -#include "ui/iconloader.h" -#include "widgets/didyoumean.h" - -#ifdef HAVE_SPOTIFY_DOWNLOADER -#include "spotifyblobdownloader.h" -#endif - -Q_DECLARE_METATYPE(QStandardItem*) - -const char* SpotifyService::kServiceName = "Spotify"; -const char* SpotifyService::kSettingsGroup = "Spotify"; -const char* SpotifyService::kBlobDownloadUrl = - "https://spotify.clementine-player.org/"; -const int SpotifyService::kSearchDelayMsec = 400; - -SpotifyService::SpotifyService(Application* app, InternetModel* parent) - : InternetService(kServiceName, app, parent, parent), - server_(nullptr), - blob_process_(nullptr), - root_(nullptr), - search_(nullptr), - starred_(nullptr), - inbox_(nullptr), - toplist_(nullptr), - login_task_id_(0), - playlist_context_menu_(nullptr), - song_context_menu_(nullptr), - playlist_sync_action_(nullptr), - get_url_to_share_playlist_(nullptr), - remove_from_playlist_(nullptr), - search_box_(new SearchBoxWidget(this)), - search_delay_(new QTimer(this)), - login_state_(LoginState_OtherError), - bitrate_(cpb::spotify::Bitrate320k), - volume_normalisation_(false) { -// Build the search path for the binary blob. -// Look for one distributed alongside clementine first, then check in the -// user's home directory for any that have been downloaded. -#if defined(Q_OS_MACOS) && defined(USE_BUNDLE) - system_blob_path_ = QCoreApplication::applicationDirPath() + "/" + - USE_BUNDLE_DIR + "/clementine-spotifyblob"; -#else - system_blob_path_ = QCoreApplication::applicationDirPath() + - "/clementine-spotifyblob" CMAKE_EXECUTABLE_SUFFIX; -#endif - - local_blob_version_ = QString("version%1-%2bit") - .arg(SPOTIFY_BLOB_VERSION) - .arg(sizeof(void*) * 8); - local_blob_path_ = - Utilities::GetConfigPath(Utilities::Path_LocalSpotifyBlob) + "/" + - local_blob_version_ + "/blob"; - - qLog(Debug) << "Spotify system blob path:" << system_blob_path_; - qLog(Debug) << "Spotify local blob path:" << local_blob_path_; - - app_->global_search()->AddProvider(new SpotifySearchProvider(app_, this)); - - search_delay_->setInterval(kSearchDelayMsec); - search_delay_->setSingleShot(true); - connect(search_delay_, SIGNAL(timeout()), SLOT(DoSearch())); - connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(Search(QString))); -} - -SpotifyService::~SpotifyService() { - if (blob_process_ && blob_process_->state() == QProcess::Running) { - qLog(Info) << "Terminating blob process..."; - blob_process_->terminate(); - blob_process_->waitForFinished(1000); - } -} - -QStandardItem* SpotifyService::CreateRootItem() { - root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider), - kServiceName); - root_->setData(true, InternetModel::Role_CanLazyLoad); - return root_; -} - -void SpotifyService::LazyPopulate(QStandardItem* item) { - switch (item->data(InternetModel::Role_Type).toInt()) { - case InternetModel::Type_Service: - EnsureServerCreated(); - break; - - case Type_SearchResults: - break; - - case Type_InboxPlaylist: - EnsureServerCreated(); - server_->LoadInbox(); - break; - - case Type_StarredPlaylist: - EnsureServerCreated(); - server_->LoadStarred(); - break; - - case InternetModel::Type_UserPlaylist: - EnsureServerCreated(); - server_->LoadUserPlaylist(item->data(Role_UserPlaylistIndex).toInt()); - break; - - case Type_Toplist: - EnsureServerCreated(); - server_->LoadToplist(); - break; - - default: - break; - } - - return; -} - -void SpotifyService::Login(const QString& username, const QString& password) { - Logout(); - EnsureServerCreated(username, password); -} - -void SpotifyService::LoginCompleted( - bool success, const QString& error, - cpb::spotify::LoginResponse_Error error_code) { - if (login_task_id_) { - app_->task_manager()->SetTaskFinished(login_task_id_); - login_task_id_ = 0; - } - - if (!success) { - bool show_error_dialog = true; - QString error_copy(error); - - switch (error_code) { - case cpb::spotify::LoginResponse_Error_BadUsernameOrPassword: - login_state_ = LoginState_BadCredentials; - break; - - case cpb::spotify::LoginResponse_Error_UserBanned: - login_state_ = LoginState_Banned; - break; - - case cpb::spotify::LoginResponse_Error_UserNeedsPremium: - login_state_ = LoginState_NoPremium; - break; - - case cpb::spotify::LoginResponse_Error_ReloginFailed: - if (login_state_ == LoginState_LoggedIn) { - // This is the first time the relogin has failed - show a message this - // time only. - error_copy = - tr("You have been logged out of Spotify, please re-enter your " - "password in the Settings dialog."); - } else { - show_error_dialog = false; - } - - login_state_ = LoginState_ReloginFailed; - break; - - default: - login_state_ = LoginState_OtherError; - break; - } - - if (show_error_dialog) { - QMessageBox::warning(nullptr, tr("Spotify login error"), error_copy, - QMessageBox::Close); - } - } else { - login_state_ = LoginState_LoggedIn; - } - - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("login_state", login_state_); - - emit LoginFinished(success); -} - -void SpotifyService::BlobProcessError(QProcess::ProcessError error) { - qLog(Error) << "Spotify blob process failed:" << error; - blob_process_->deleteLater(); - blob_process_ = nullptr; - - if (login_task_id_) { - app_->task_manager()->SetTaskFinished(login_task_id_); - } -} - -void SpotifyService::ReloadSettings() { - QSettings s; - s.beginGroup(kSettingsGroup); - - login_state_ = - LoginState(s.value("login_state", LoginState_OtherError).toInt()); - bitrate_ = static_cast( - s.value("bitrate", cpb::spotify::Bitrate320k).toInt()); - volume_normalisation_ = s.value("volume_normalisation", false).toBool(); - - if (server_ && blob_process_) { - server_->SetPlaybackSettings(bitrate_, volume_normalisation_); - } -} - -void SpotifyService::EnsureServerCreated(const QString& username, - const QString& password) { - if (server_ && blob_process_) { - return; - } - - delete server_; - server_ = new SpotifyServer(this); - - connect( - server_, - SIGNAL(LoginCompleted(bool, QString, cpb::spotify::LoginResponse_Error)), - SLOT(LoginCompleted(bool, QString, cpb::spotify::LoginResponse_Error))); - connect(server_, SIGNAL(PlaylistsUpdated(cpb::spotify::Playlists)), - SLOT(PlaylistsUpdated(cpb::spotify::Playlists))); - connect(server_, SIGNAL(InboxLoaded(cpb::spotify::LoadPlaylistResponse)), - SLOT(InboxLoaded(cpb::spotify::LoadPlaylistResponse))); - connect(server_, SIGNAL(StarredLoaded(cpb::spotify::LoadPlaylistResponse)), - SLOT(StarredLoaded(cpb::spotify::LoadPlaylistResponse))); - connect(server_, - SIGNAL(UserPlaylistLoaded(cpb::spotify::LoadPlaylistResponse)), - SLOT(UserPlaylistLoaded(cpb::spotify::LoadPlaylistResponse))); - connect(server_, SIGNAL(PlaybackError(QString)), - SIGNAL(StreamError(QString))); - connect(server_, SIGNAL(SearchResults(cpb::spotify::SearchResponse)), - SLOT(SearchResults(cpb::spotify::SearchResponse))); - connect(server_, SIGNAL(ImageLoaded(QString, QImage)), - SIGNAL(ImageLoaded(QString, QImage))); - connect(server_, - SIGNAL(SyncPlaylistProgress(cpb::spotify::SyncPlaylistProgress)), - SLOT(SyncPlaylistProgress(cpb::spotify::SyncPlaylistProgress))); - connect(server_, - SIGNAL(ToplistBrowseResults(cpb::spotify::BrowseToplistResponse)), - SLOT(ToplistLoaded(cpb::spotify::BrowseToplistResponse))); - - server_->Init(); - - login_task_id_ = app_->task_manager()->StartTask(tr("Connecting to Spotify")); - - QString login_username = username; - QString login_password = password; - - if (username.isEmpty()) { - QSettings s; - s.beginGroup(kSettingsGroup); - - login_username = s.value("username").toString(); - login_password = QString(); - } - - server_->Login(login_username, login_password, bitrate_, - volume_normalisation_); - - StartBlobProcess(); -} - -void SpotifyService::StartBlobProcess() { - // Try to find an executable to run - QString blob_path; - QProcessEnvironment env(QProcessEnvironment::systemEnvironment()); - - // Look in the system search path first - if (QFile::exists(system_blob_path_)) { - blob_path = system_blob_path_; - } - - // Next look in the local path - if (blob_path.isEmpty()) { - if (QFile::exists(local_blob_path_)) { - blob_path = local_blob_path_; - env.insert("LD_LIBRARY_PATH", QFileInfo(local_blob_path_).path()); - } - } - - if (blob_path.isEmpty()) { - // If the blob still wasn't found then we'll prompt the user to download one - if (login_task_id_) { - app_->task_manager()->SetTaskFinished(login_task_id_); - } - -#ifdef HAVE_SPOTIFY_DOWNLOADER - if (SpotifyBlobDownloader::Prompt()) { - InstallBlob(); - } -#endif - - return; - } - - delete blob_process_; - blob_process_ = new QProcess(this); - blob_process_->setProcessChannelMode(QProcess::ForwardedChannels); - blob_process_->setProcessEnvironment(env); - - connect(blob_process_, SIGNAL(error(QProcess::ProcessError)), - SLOT(BlobProcessError(QProcess::ProcessError))); - - qLog(Info) << "Starting" << blob_path; - blob_process_->start( - blob_path, QStringList() << QString::number(server_->server_port())); -} - -bool SpotifyService::IsBlobInstalled() const { - return QFile::exists(system_blob_path_) || QFile::exists(local_blob_path_); -} - -void SpotifyService::InstallBlob() { -#ifdef HAVE_SPOTIFY_DOWNLOADER - // The downloader deletes itself when it finishes - SpotifyBlobDownloader* downloader = new SpotifyBlobDownloader( - local_blob_version_, QFileInfo(local_blob_path_).path(), this); - connect(downloader, SIGNAL(Finished()), SLOT(BlobDownloadFinished())); - connect(downloader, SIGNAL(Finished()), SIGNAL(BlobStateChanged())); - downloader->Start(); -#endif // HAVE_SPOTIFY_DOWNLOADER -} - -void SpotifyService::BlobDownloadFinished() { EnsureServerCreated(); } - -void SpotifyService::AddCurrentSongToUserPlaylist(QAction* action) { - int playlist_index = action->data().toInt(); - AddSongsToUserPlaylist(playlist_index, QList() << current_song_url_); -} - -void SpotifyService::AddSongsToUserPlaylist(int playlist_index, - const QList& songs_urls) { - EnsureServerCreated(); - server_->AddSongsToUserPlaylist(playlist_index, songs_urls); -} - -void SpotifyService::AddCurrentSongToStarredPlaylist() { - AddSongsToStarred(QList() << current_song_url_); -} - -void SpotifyService::AddSongsToStarred(const QList& songs_urls) { - EnsureMenuCreated(); - server_->AddSongsToStarred(songs_urls); -} - -void SpotifyService::InitSearch() { - search_ = new QStandardItem(IconLoader::Load("edit-find", IconLoader::Base), - tr("Search results")); - search_->setToolTip( - tr("Start typing something on the search box above to " - "fill this search results list")); - search_->setData(Type_SearchResults, InternetModel::Role_Type); - search_->setData(InternetModel::PlayBehaviour_MultipleItems, - InternetModel::Role_PlayBehaviour); - - starred_ = new QStandardItem(IconLoader::Load("star-on", IconLoader::Other), - tr("Starred")); - starred_->setData(Type_StarredPlaylist, InternetModel::Role_Type); - starred_->setData(true, InternetModel::Role_CanLazyLoad); - starred_->setData(InternetModel::PlayBehaviour_MultipleItems, - InternetModel::Role_PlayBehaviour); - starred_->setData(true, InternetModel::Role_CanBeModified); - - inbox_ = new QStandardItem(IconLoader::Load("mail-message", IconLoader::Base), - tr("Inbox")); - inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type); - inbox_->setData(true, InternetModel::Role_CanLazyLoad); - inbox_->setData(InternetModel::PlayBehaviour_MultipleItems, - InternetModel::Role_PlayBehaviour); - - toplist_ = new QStandardItem(QIcon(), tr("Top tracks")); - toplist_->setData(Type_Toplist, InternetModel::Role_Type); - toplist_->setData(true, InternetModel::Role_CanLazyLoad); - toplist_->setData(InternetModel::PlayBehaviour_MultipleItems, - InternetModel::Role_PlayBehaviour); - - root_->appendRow(search_); - root_->appendRow(toplist_); - root_->appendRow(starred_); - root_->appendRow(inbox_); -} - -void SpotifyService::PlaylistsUpdated(const cpb::spotify::Playlists& response) { - if (login_task_id_) { - app_->task_manager()->SetTaskFinished(login_task_id_); - login_task_id_ = 0; - } - - // Create starred and inbox playlists if they're not here already - if (!search_) { - InitSearch(); - } else { - // Always reset starred playlist - // TODO: might be improved by including starred playlist in the response, - // and reloading it only when needed, like other playlists. - starred_->removeRows(0, starred_->rowCount()); - LazyPopulate(starred_); - } - - // Don't do anything if the playlists haven't changed since last time. - if (!DoPlaylistsDiffer(response)) { - qLog(Debug) << "Playlists haven't changed - not updating"; - return; - } - qLog(Debug) << "Playlist have changed: updating"; - - // Remove and recreate the other playlists - for (QStandardItem* item : playlists_) { - item->parent()->removeRow(item->row()); - } - playlists_.clear(); - - for (int i = 0; i < response.playlist_size(); ++i) { - const cpb::spotify::Playlists::Playlist& msg = response.playlist(i); - - QString playlist_title = QStringFromStdString(msg.name()); - if (!msg.is_mine()) { - const std::string& owner = msg.owner(); - playlist_title += - tr(", by ") + QString::fromUtf8(owner.c_str(), owner.size()); - } - QStandardItem* item = new QStandardItem(playlist_title); - item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); - item->setData(true, InternetModel::Role_CanLazyLoad); - item->setData(msg.index(), Role_UserPlaylistIndex); - item->setData(msg.is_mine(), InternetModel::Role_CanBeModified); - item->setData(InternetModel::PlayBehaviour_MultipleItems, - InternetModel::Role_PlayBehaviour); - item->setData(QUrl(QStringFromStdString(msg.uri())), - InternetModel::Role_Url); - - root_->appendRow(item); - playlists_ << item; - - // Preload the playlist items so that drag & drop works immediately. - LazyPopulate(item); - } -} - -bool SpotifyService::DoPlaylistsDiffer( - const cpb::spotify::Playlists& response) const { - if (playlists_.count() != response.playlist_size()) { - return true; - } - - for (int i = 0; i < response.playlist_size(); ++i) { - const cpb::spotify::Playlists::Playlist& msg = response.playlist(i); - const QStandardItem* item = PlaylistBySpotifyIndex(msg.index()); - - if (!item) { - return true; - } - - if (QStringFromStdString(msg.name()) != item->text()) { - return true; - } - - if (msg.nb_tracks() != item->rowCount()) { - return true; - } - } - - return false; -} - -void SpotifyService::InboxLoaded( - const cpb::spotify::LoadPlaylistResponse& response) { - if (inbox_) { - FillPlaylist(inbox_, response); - } -} - -void SpotifyService::StarredLoaded( - const cpb::spotify::LoadPlaylistResponse& response) { - if (starred_) { - FillPlaylist(starred_, response); - } -} - -void SpotifyService::ToplistLoaded( - const cpb::spotify::BrowseToplistResponse& response) { - if (toplist_) { - FillPlaylist(toplist_, response.track()); - } -} - -QStandardItem* SpotifyService::PlaylistBySpotifyIndex(int index) const { - for (QStandardItem* item : playlists_) { - if (item->data(Role_UserPlaylistIndex).toInt() == index) { - return item; - } - } - return nullptr; -} - -void SpotifyService::UserPlaylistLoaded( - const cpb::spotify::LoadPlaylistResponse& response) { - // Find a playlist with this index - QStandardItem* item = - PlaylistBySpotifyIndex(response.request().user_playlist_index()); - if (item) { - FillPlaylist(item, response); - } -} - -void SpotifyService::FillPlaylist( - QStandardItem* item, - const google::protobuf::RepeatedPtrField& tracks) { - if (item->hasChildren()) item->removeRows(0, item->rowCount()); - - for (int i = 0; i < tracks.size(); ++i) { - Song song; - SongFromProtobuf(tracks.Get(i), &song); - - QStandardItem* child = CreateSongItem(song); - - item->appendRow(child); - } -} - -void SpotifyService::FillPlaylist( - QStandardItem* item, const cpb::spotify::LoadPlaylistResponse& response) { - qLog(Debug) << "Filling playlist:" << item->text(); - FillPlaylist(item, response.track()); -} - -void SpotifyService::SongFromProtobuf(const cpb::spotify::Track& track, - Song* song) { - song->set_rating(track.starred() ? 1.0 : 0.0); - song->set_title(QStringFromStdString(track.title())); - song->set_album(QStringFromStdString(track.album())); - song->set_length_nanosec(track.duration_msec() * kNsecPerMsec); - song->set_score(track.popularity()); - song->set_disc(track.disc()); - song->set_track(track.track()); - song->set_year(track.year()); - song->set_url(QUrl(QStringFromStdString(track.uri()))); - song->set_art_automatic("spotify://image/" + - QStringFromStdString(track.album_art_id())); - - QStringList artists; - for (int i = 0; i < track.artist_size(); ++i) { - artists << QStringFromStdString(track.artist(i)); - } - - song->set_artist(artists.join(", ")); - - song->set_filetype(Song::Type_Stream); - song->set_valid(true); - song->set_directory_id(0); - song->set_mtime(0); - song->set_ctime(0); - song->set_filesize(0); -} - -QList SpotifyService::playlistitem_actions(const Song& song) { - // Clear previous actions - while (!playlistitem_actions_.isEmpty()) { - QAction* action = playlistitem_actions_.takeFirst(); - delete action->menu(); - delete action; - } - - QAction* add_to_starred = - new QAction(IconLoader::Load("star-on", IconLoader::Other), - tr("Add to Spotify starred"), this); - connect(add_to_starred, SIGNAL(triggered()), - SLOT(AddCurrentSongToStarredPlaylist())); - playlistitem_actions_.append(add_to_starred); - - // Create a menu with 'add to playlist' actions for each Spotify playlist - QAction* add_to_playlists = - new QAction(IconLoader::Load("list-add", IconLoader::Base), - tr("Add to Spotify playlists"), this); - QMenu* playlists_menu = new QMenu(); - for (const QStandardItem* playlist_item : playlists_) { - if (!playlist_item->data(InternetModel::Role_CanBeModified).toBool()) { - continue; - } - QAction* add_to_playlist = new QAction(playlist_item->text(), this); - add_to_playlist->setData(playlist_item->data(Role_UserPlaylistIndex)); - playlists_menu->addAction(add_to_playlist); - } - connect(playlists_menu, SIGNAL(triggered(QAction*)), - SLOT(AddCurrentSongToUserPlaylist(QAction*))); - add_to_playlists->setMenu(playlists_menu); - playlistitem_actions_.append(add_to_playlists); - - QAction* share_song = - new QAction(tr("Get a URL to share this Spotify song"), this); - connect(share_song, SIGNAL(triggered()), SLOT(GetCurrentSongUrlToShare())); - playlistitem_actions_.append(share_song); - - // Keep in mind the current song URL - current_song_url_ = song.url(); - - return playlistitem_actions_; -} - -PlaylistItem::Options SpotifyService::playlistitem_options() const { - return PlaylistItem::SeekDisabled; -} - -QWidget* SpotifyService::HeaderWidget() const { - if (IsLoggedIn()) return search_box_; - return nullptr; -} - -void SpotifyService::EnsureMenuCreated() { - if (context_menu_) return; - - context_menu_.reset(new QMenu); - context_menu_->addAction(GetNewShowConfigAction()); - - playlist_context_menu_ = new QMenu; - playlist_context_menu_->addActions(GetPlaylistActions()); - playlist_context_menu_->addSeparator(); - playlist_sync_action_ = playlist_context_menu_->addAction( - IconLoader::Load("view-refresh", IconLoader::Base), - tr("Make playlist available offline"), this, SLOT(SyncPlaylist())); - get_url_to_share_playlist_ = playlist_context_menu_->addAction( - tr("Get a URL to share this playlist"), this, - SLOT(GetCurrentPlaylistUrlToShare())); - playlist_context_menu_->addSeparator(); - playlist_context_menu_->addAction(GetNewShowConfigAction()); - - song_context_menu_ = new QMenu; - song_context_menu_->addActions(GetPlaylistActions()); - song_context_menu_->addSeparator(); - remove_from_playlist_ = song_context_menu_->addAction( - IconLoader::Load("list-remove", IconLoader::Base), - tr("Remove from playlist"), this, SLOT(RemoveCurrentFromPlaylist())); - song_context_menu_->addAction(tr("Get a URL to share this Spotify song"), - this, SLOT(GetCurrentSongUrlToShare())); - song_context_menu_->addSeparator(); - song_context_menu_->addAction(GetNewShowConfigAction()); -} - -void SpotifyService::ClearSearchResults() { - if (search_) search_->removeRows(0, search_->rowCount()); -} - -void SpotifyService::SyncPlaylist() { - QStandardItem* item = playlist_sync_action_->data().value(); - Q_ASSERT(item); - - switch (item->data(InternetModel::Role_Type).toInt()) { - case InternetModel::Type_UserPlaylist: { - int index = item->data(Role_UserPlaylistIndex).toInt(); - server_->SyncUserPlaylist(index); - playlist_sync_ids_[index] = - app_->task_manager()->StartTask(tr("Syncing Spotify playlist")); - break; - } - case Type_InboxPlaylist: - server_->SyncInbox(); - inbox_sync_id_ = - app_->task_manager()->StartTask(tr("Syncing Spotify inbox")); - break; - case Type_StarredPlaylist: - server_->SyncStarred(); - starred_sync_id_ = - app_->task_manager()->StartTask(tr("Syncing Spotify starred tracks")); - break; - default: - break; - } -} - -void SpotifyService::Search(const QString& text, bool now) { - EnsureServerCreated(); - - pending_search_ = text; - - // If there is no text (e.g. user cleared search box), we don't need to do a - // real query that will return nothing: we can clear the playlist now - if (text.isEmpty()) { - search_delay_->stop(); - ClearSearchResults(); - return; - } - - if (now) { - search_delay_->stop(); - DoSearch(); - } else { - search_delay_->start(); - } -} - -void SpotifyService::DoSearch() { - if (!pending_search_.isEmpty()) { - server_->Search(pending_search_, 200); - } -} - -void SpotifyService::SearchResults( - const cpb::spotify::SearchResponse& response) { - if (QStringFromStdString(response.request().query()) != pending_search_) { - qLog(Debug) << "Old search result for" - << QStringFromStdString(response.request().query()) - << "expecting" << pending_search_; - return; - } - pending_search_.clear(); - - SongList songs; - for (int i = 0; i < response.result_size(); ++i) { - Song song; - SongFromProtobuf(response.result(i), &song); - songs << song; - } - - qLog(Debug) << "Got" << songs.count() << "results"; - - ClearSearchResults(); - - // Must initialize search pointer if it is nullptr - if (!search_) { - InitSearch(); - } - // Fill results list - for (const Song& song : songs) { - QStandardItem* child = CreateSongItem(song); - search_->appendRow(child); - } - - const QString did_you_mean_suggestion = - QStringFromStdString(response.did_you_mean()); - qLog(Debug) << "Did you mean suggestion: " << did_you_mean_suggestion; - if (!did_you_mean_suggestion.isEmpty()) { - search_box_->did_you_mean()->Show(did_you_mean_suggestion); - } else { - // In case something else was previously displayed - search_box_->did_you_mean()->hide(); - } - - QModelIndex index = model()->merged_model()->mapFromSource(search_->index()); - ScrollToIndex(index); -} - -SpotifyServer* SpotifyService::server() const { - SpotifyService* nonconst_this = const_cast(this); - - if (QThread::currentThread() != thread()) { - metaObject()->invokeMethod(nonconst_this, "EnsureServerCreated", - Qt::BlockingQueuedConnection); - } else { - nonconst_this->EnsureServerCreated(); - } - - return server_; -} - -void SpotifyService::ShowContextMenu(const QPoint& global_pos) { - EnsureMenuCreated(); - QStandardItem* item = model()->itemFromIndex(model()->current_index()); - if (item) { - int type = item->data(InternetModel::Role_Type).toInt(); - if (type == Type_InboxPlaylist || type == Type_StarredPlaylist || - type == InternetModel::Type_UserPlaylist) { - playlist_sync_action_->setData(qVariantFromValue(item)); - playlist_context_menu_->popup(global_pos); - current_playlist_url_ = item->data(InternetModel::Role_Url).toUrl(); - get_url_to_share_playlist_->setVisible(type == - InternetModel::Type_UserPlaylist); - return; - } else if (type == InternetModel::Type_Track) { - current_song_url_ = item->data(InternetModel::Role_Url).toUrl(); - // Is this track contained in a playlist we can modify? - bool is_playlist_modifiable = - item->parent() && - item->parent()->data(InternetModel::Role_CanBeModified).toBool(); - remove_from_playlist_->setVisible(is_playlist_modifiable); - - song_context_menu_->popup(global_pos); - return; - } - } - - context_menu_->popup(global_pos); -} - -void SpotifyService::GetCurrentSongUrlToShare() const { - QString url = current_song_url_.toEncoded(); - // URLs we use can be opened with Spotify application, but I believe it's - // better to give website links instead. - url.replace("spotify:track:", "https://play.spotify.com/track/"); - InternetService::ShowUrlBox(tr("Spotify song's URL"), url); -} - -void SpotifyService::GetCurrentPlaylistUrlToShare() const { - QString url = current_playlist_url_.toEncoded(); - // URLs we use can be opened with Spotify application, but I believe it's - // better to give website links instead. - url.replace(QRegExp("spotify:user:([^:]*):playlist:([^:]*)"), - "https://play.spotify.com/user/\\1/playlist/\\2"); - InternetService::ShowUrlBox(tr("Spotify playlist's URL"), url); -} - -void SpotifyService::DropMimeData(const QMimeData* data, - const QModelIndex& index) { - QModelIndex playlist_root_index = index; - QVariant q_playlist_type = playlist_root_index.data(InternetModel::Role_Type); - if (!q_playlist_type.isValid() || - q_playlist_type.toInt() == InternetModel::Type_Track) { - // In case song was dropped on a playlist item, not on the playlist - // title/root element - playlist_root_index = index.parent(); - q_playlist_type = playlist_root_index.data(InternetModel::Role_Type); - } - - if (!q_playlist_type.isValid()) return; - - int playlist_type = q_playlist_type.toInt(); - if (playlist_type == Type_StarredPlaylist) { - AddSongsToStarred(data->urls()); - } else if (playlist_type == InternetModel::Type_UserPlaylist) { - QVariant q_playlist_index = - playlist_root_index.data(Role_UserPlaylistIndex); - if (!q_playlist_index.isValid()) return; - AddSongsToUserPlaylist(q_playlist_index.toInt(), data->urls()); - } -} - -void SpotifyService::LoadImage(const QString& id) { - EnsureServerCreated(); - server_->LoadImage(id); -} - -void SpotifyService::SetPaused(bool paused) { - EnsureServerCreated(); - server_->SetPaused(paused); -} - -void SpotifyService::SyncPlaylistProgress( - const cpb::spotify::SyncPlaylistProgress& progress) { - qLog(Debug) << "Sync progress:" << progress.sync_progress(); - int task_id = -1; - switch (progress.request().type()) { - case cpb::spotify::Inbox: - task_id = inbox_sync_id_; - break; - case cpb::spotify::Starred: - task_id = starred_sync_id_; - break; - case cpb::spotify::UserPlaylist: { - QMap::const_iterator it = playlist_sync_ids_.constFind( - progress.request().user_playlist_index()); - if (it != playlist_sync_ids_.constEnd()) { - task_id = it.value(); - } - break; - } - default: - break; - } - if (task_id == -1) { - qLog(Warning) << "Received sync progress for unknown playlist"; - return; - } - app_->task_manager()->SetTaskProgress(task_id, progress.sync_progress(), 100); - if (progress.sync_progress() == 100) { - app_->task_manager()->SetTaskFinished(task_id); - if (progress.request().type() == cpb::spotify::UserPlaylist) { - playlist_sync_ids_.remove(task_id); - } - } -} - -QAction* SpotifyService::GetNewShowConfigAction() { - QAction* action = new QAction(IconLoader::Load("configure", IconLoader::Base), - tr("Configure Spotify..."), this); - connect(action, SIGNAL(triggered()), this, SLOT(ShowConfig())); - return action; -} - -void SpotifyService::ShowConfig() { - app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Spotify); -} - -void SpotifyService::RemoveCurrentFromPlaylist() { - const QModelIndexList& indexes(model()->selected_indexes()); - QMap> playlists_songs_indices; - QList starred_songs_indices; - - for (const QModelIndex& index : indexes) { - bool is_starred = false; - if (index.parent().data(InternetModel::Role_Type).toInt() == - Type_StarredPlaylist) { - is_starred = true; - } else if (index.parent().data(InternetModel::Role_Type).toInt() != - InternetModel::Type_UserPlaylist) { - continue; - } - - if (index.data(InternetModel::Role_Type).toInt() != - InternetModel::Type_Track) { - continue; - } - - int song_index = index.row(); - if (is_starred) { - starred_songs_indices << song_index; - } else { - int playlist_index = index.parent().data(Role_UserPlaylistIndex).toInt(); - playlists_songs_indices[playlist_index] << song_index; - } - } - - for (QMap>::const_iterator it = - playlists_songs_indices.constBegin(); - it != playlists_songs_indices.constEnd(); ++it) { - RemoveSongsFromUserPlaylist(it.key(), it.value()); - } - if (!starred_songs_indices.isEmpty()) { - RemoveSongsFromStarred(starred_songs_indices); - } -} - -void SpotifyService::RemoveSongsFromUserPlaylist( - int playlist_index, const QList& songs_indices_to_remove) { - server_->RemoveSongsFromUserPlaylist(playlist_index, songs_indices_to_remove); -} - -void SpotifyService::RemoveSongsFromStarred( - const QList& songs_indices_to_remove) { - server_->RemoveSongsFromStarred(songs_indices_to_remove); -} - -void SpotifyService::Logout() { - delete server_; - delete blob_process_; - server_ = nullptr; - blob_process_ = nullptr; - - login_state_ = LoginState_OtherError; - - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("login_state", login_state_); -} diff --git a/src/internet/spotify/spotifyservice.h b/src/internet/spotify/spotifyservice.h deleted file mode 100644 index 2589d5cf1..000000000 --- a/src/internet/spotify/spotifyservice.h +++ /dev/null @@ -1,195 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, Tyler Rhodes - Copyright 2011-2012, 2014, Arnaud Bienner - Copyright 2011-2012, 2014, John Maguire - Copyright 2011-2012, 2014, David Sansome - Copyright 2014, Krzysztof Sobiecki - - 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 INTERNET_SPOTIFY_SPOTIFYSERVICE_H_ -#define INTERNET_SPOTIFY_SPOTIFYSERVICE_H_ - -#include -#include - -#include "internet/core/internetmodel.h" -#include "internet/core/internetservice.h" -#include "spotifymessages.pb.h" - -class Playlist; -class SearchBoxWidget; -class SpotifyServer; - -class QMenu; - -class SpotifyService : public InternetService { - Q_OBJECT - - public: - SpotifyService(Application* app, InternetModel* parent); - ~SpotifyService(); - - enum Type { - Type_SearchResults = InternetModel::TypeCount, - Type_StarredPlaylist, - Type_InboxPlaylist, - Type_Toplist, - }; - - enum Role { - Role_UserPlaylistIndex = InternetModel::RoleCount, - }; - - // Values are persisted - don't change. - enum LoginState { - LoginState_LoggedIn = 1, - LoginState_Banned = 2, - LoginState_BadCredentials = 3, - LoginState_NoPremium = 4, - LoginState_OtherError = 5, - LoginState_ReloginFailed = 6 - }; - - static const char* kServiceName; - static const char* kSettingsGroup; - static const char* kBlobDownloadUrl; - static const int kSearchDelayMsec; - - void ReloadSettings() override; - - QStandardItem* CreateRootItem() override; - void LazyPopulate(QStandardItem* parent) override; - void ShowContextMenu(const QPoint& global_pos) override; - void DropMimeData(const QMimeData* data, const QModelIndex& index) override; - QList playlistitem_actions(const Song& song) override; - PlaylistItem::Options playlistitem_options() const override; - QWidget* HeaderWidget() const override; - - void Logout(); - void Login(const QString& username, const QString& password); - Q_INVOKABLE void LoadImage(const QString& id); - Q_INVOKABLE void SetPaused(bool paused); - - SpotifyServer* server() const; - - bool IsBlobInstalled() const; - void InstallBlob(); - - // Persisted in the settings and updated on each Login(). - LoginState login_state() const { return login_state_; } - bool IsLoggedIn() const { return login_state_ == LoginState_LoggedIn; } - - bool ConfigRequired() override { return !IsLoggedIn(); } - - static void SongFromProtobuf(const cpb::spotify::Track& track, Song* song); - - signals: - void BlobStateChanged(); - void LoginFinished(bool success); - void ImageLoaded(const QString& id, const QImage& image); - - public slots: - void Search(const QString& text, bool now = false); - void ShowConfig() override; - void RemoveCurrentFromPlaylist(); - - private: - void StartBlobProcess(); - void FillPlaylist( - QStandardItem* item, - const google::protobuf::RepeatedPtrField& tracks); - void FillPlaylist(QStandardItem* item, - const cpb::spotify::LoadPlaylistResponse& response); - void AddSongsToUserPlaylist(int playlist_index, - const QList& songs_urls); - void AddSongsToStarred(const QList& songs_urls); - void EnsureMenuCreated(); - // Create a new "show config" action. The caller is responsible for deleting - // the pointer (or adding it to menu or anything else that will take ownership - // of it) - QAction* GetNewShowConfigAction(); - void InitSearch(); - void ClearSearchResults(); - QStandardItem* PlaylistBySpotifyIndex(int index) const; - bool DoPlaylistsDiffer(const cpb::spotify::Playlists& response) const; - - private slots: - void EnsureServerCreated(const QString& username = QString(), - const QString& password = QString()); - void BlobProcessError(QProcess::ProcessError error); - void LoginCompleted(bool success, const QString& error, - cpb::spotify::LoginResponse_Error error_code); - void AddCurrentSongToUserPlaylist(QAction* action); - void AddCurrentSongToStarredPlaylist(); - void RemoveSongsFromUserPlaylist(int playlist_index, - const QList& songs_indices_to_remove); - void RemoveSongsFromStarred(const QList& songs_indices_to_remove); - void PlaylistsUpdated(const cpb::spotify::Playlists& response); - void InboxLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void StarredLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void UserPlaylistLoaded(const cpb::spotify::LoadPlaylistResponse& response); - void SearchResults(const cpb::spotify::SearchResponse& response); - void SyncPlaylistProgress(const cpb::spotify::SyncPlaylistProgress& progress); - void ToplistLoaded(const cpb::spotify::BrowseToplistResponse& response); - void GetCurrentSongUrlToShare() const; - void GetCurrentPlaylistUrlToShare() const; - - void DoSearch(); - - void SyncPlaylist(); - void BlobDownloadFinished(); - - private: - SpotifyServer* server_; - - QString system_blob_path_; - QString local_blob_version_; - QString local_blob_path_; - QProcess* blob_process_; - - QStandardItem* root_; - QStandardItem* search_; - QStandardItem* starred_; - QStandardItem* inbox_; - QStandardItem* toplist_; - QList playlists_; - - int login_task_id_; - QString pending_search_; - - QMenu* playlist_context_menu_; - QMenu* song_context_menu_; - QAction* playlist_sync_action_; - QAction* get_url_to_share_playlist_; - QList playlistitem_actions_; - QAction* remove_from_playlist_; - QUrl current_song_url_; - QUrl current_playlist_url_; - - SearchBoxWidget* search_box_; - - QTimer* search_delay_; - - int inbox_sync_id_; - int starred_sync_id_; - QMap playlist_sync_ids_; - - LoginState login_state_; - cpb::spotify::Bitrate bitrate_; - bool volume_normalisation_; -}; - -#endif // INTERNET_SPOTIFY_SPOTIFYSERVICE_H_ diff --git a/src/internet/spotify/spotifysettingspage.cpp b/src/internet/spotify/spotifysettingspage.cpp deleted file mode 100644 index 4d3505fb4..000000000 --- a/src/internet/spotify/spotifysettingspage.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, Andrea Decorte - Copyright 2011-2013, David Sansome - Copyright 2014, Krzysztof Sobiecki - Copyright 2014, John Maguire - - 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 "spotifysettingspage.h" - -#include -#include -#include -#include -#include - -#include "config.h" -#include "core/network.h" -#include "internet/core/internetmodel.h" -#include "spotifymessages.pb.h" -#include "spotifyservice.h" -#include "ui/iconloader.h" -#include "ui_spotifysettingspage.h" - -SpotifySettingsPage::SpotifySettingsPage(SettingsDialog* dialog) - : SettingsPage(dialog), - ui_(new Ui_SpotifySettingsPage), - service_(InternetModel::Service()), - validated_(false) { - ui_->setupUi(this); - - setWindowIcon(IconLoader::Load("spotify", IconLoader::Provider)); - - QFont bold_font(font()); - bold_font.setBold(true); - ui_->blob_status->setFont(bold_font); - - connect(ui_->download_blob, SIGNAL(clicked()), SLOT(DownloadBlob())); - connect(ui_->login, SIGNAL(clicked()), SLOT(Login())); - connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout())); - connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login())); - - connect(service_, SIGNAL(LoginFinished(bool)), SLOT(LoginFinished(bool))); - connect(service_, SIGNAL(BlobStateChanged()), SLOT(BlobStateChanged())); - - ui_->login_state->AddCredentialField(ui_->username); - ui_->login_state->AddCredentialField(ui_->password); - ui_->login_state->AddCredentialGroup(ui_->account_group); - - ui_->bitrate->addItem("96 " + tr("kbps"), cpb::spotify::Bitrate96k); - ui_->bitrate->addItem("160 " + tr("kbps"), cpb::spotify::Bitrate160k); - ui_->bitrate->addItem("320 " + tr("kbps"), cpb::spotify::Bitrate320k); - - BlobStateChanged(); -} - -SpotifySettingsPage::~SpotifySettingsPage() { delete ui_; } - -void SpotifySettingsPage::BlobStateChanged() { - const bool installed = service_->IsBlobInstalled(); - - ui_->account_group->setEnabled(installed); - ui_->blob_status->setText(installed ? tr("Installed") : tr("Not installed")); - -#ifdef HAVE_SPOTIFY_DOWNLOADER - ui_->download_blob->setEnabled(!installed); -#else - ui_->download_blob->hide(); -#endif -} - -void SpotifySettingsPage::DownloadBlob() { service_->InstallBlob(); } - -void SpotifySettingsPage::Login() { - if (!service_->IsBlobInstalled()) { - return; - } - - if (ui_->username->text() == original_username_ && - ui_->password->text() == original_password_ && - service_->login_state() == SpotifyService::LoginState_LoggedIn) { - return; - } - - ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); - service_->Login(ui_->username->text(), ui_->password->text()); -} - -void SpotifySettingsPage::Load() { - QSettings s; - s.beginGroup(SpotifyService::kSettingsGroup); - - original_username_ = s.value("username").toString(); - - ui_->username->setText(original_username_); - validated_ = false; - - ui_->bitrate->setCurrentIndex(ui_->bitrate->findData( - s.value("bitrate", cpb::spotify::Bitrate320k).toInt())); - ui_->volume_normalisation->setChecked( - s.value("volume_normalisation", false).toBool()); - - UpdateLoginState(); -} - -void SpotifySettingsPage::Save() { - QSettings s; - s.beginGroup(SpotifyService::kSettingsGroup); - - s.setValue("username", ui_->username->text()); - s.setValue("password", ui_->password->text()); - - s.setValue("bitrate", - ui_->bitrate->itemData(ui_->bitrate->currentIndex()).toInt()); - s.setValue("volume_normalisation", ui_->volume_normalisation->isChecked()); -} - -void SpotifySettingsPage::LoginFinished(bool success) { - validated_ = success; - - Save(); - UpdateLoginState(); -} - -void SpotifySettingsPage::UpdateLoginState() { - const bool logged_in = - service_->login_state() == SpotifyService::LoginState_LoggedIn; - - ui_->login_state->SetLoggedIn( - logged_in ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut, - ui_->username->text()); - ui_->login_state->SetAccountTypeVisible(!logged_in); - - switch (service_->login_state()) { - case SpotifyService::LoginState_NoPremium: - ui_->login_state->SetAccountTypeText( - tr("You do not have a Spotify Premium account.")); - break; - - case SpotifyService::LoginState_Banned: - case SpotifyService::LoginState_BadCredentials: - ui_->login_state->SetAccountTypeText( - tr("Your username or password was incorrect.")); - break; - - case SpotifyService::LoginState_ReloginFailed: - ui_->login_state->SetAccountTypeText( - tr("You have been logged out of Spotify, please re-enter your " - "password.")); - break; - - default: - ui_->login_state->SetAccountTypeText( - tr("A Spotify Premium account is required.")); - break; - } -} - -void SpotifySettingsPage::Logout() { - service_->Logout(); - UpdateLoginState(); - - ui_->username->clear(); -} diff --git a/src/internet/spotify/spotifysettingspage.h b/src/internet/spotify/spotifysettingspage.h deleted file mode 100644 index 59cc247d1..000000000 --- a/src/internet/spotify/spotifysettingspage.h +++ /dev/null @@ -1,60 +0,0 @@ -/* This file is part of Clementine. - Copyright 2011, David Sansome - Copyright 2014, Krzysztof Sobiecki - Copyright 2014, John Maguire - - 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 INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_ -#define INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_ - -#include "ui/settingspage.h" - -class NetworkAccessManager; -class Ui_SpotifySettingsPage; -class SpotifyService; - -class SpotifySettingsPage : public SettingsPage { - Q_OBJECT - - public: - explicit SpotifySettingsPage(SettingsDialog* dialog); - ~SpotifySettingsPage(); - - void Load(); - void Save(); - - public slots: - void BlobStateChanged(); - void DownloadBlob(); - - private slots: - void Login(); - void LoginFinished(bool success); - void Logout(); - - private: - void UpdateLoginState(); - - private: - Ui_SpotifySettingsPage* ui_; - SpotifyService* service_; - - bool validated_; - QString original_username_; - QString original_password_; -}; - -#endif // INTERNET_SPOTIFY_SPOTIFYSETTINGSPAGE_H_ diff --git a/src/internet/spotify/spotifysettingspage.ui b/src/internet/spotify/spotifysettingspage.ui deleted file mode 100644 index f56dfa5d5..000000000 --- a/src/internet/spotify/spotifysettingspage.ui +++ /dev/null @@ -1,211 +0,0 @@ - - - SpotifySettingsPage - - - - 0 - 0 - 545 - 458 - - - - Spotify - - - - - - - - - Account details - - - - - - true - - - - 0 - - - - - Username - - - - - - - - - - Password - - - - - - - QLineEdit::Password - - - - - - - Login - - - - - - - - - - - - - Spotify plugin - - - - - - For licensing reasons Spotify support is in a separate plugin. - - - - - - - - - Plugin status: - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Download... - - - - - - - - - - - - Preferences - - - - - - Preferred bitrate - - - - - - - - - - Use volume normalisation - - - - - - - - - - Qt::Vertical - - - - 20 - 30 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 64 - 64 - - - - - 64 - 64 - - - - :/spotify-attribution.png - - - - - - - - - - LoginStateWidget - QWidget -
widgets/loginstatewidget.h
- 1 -
-
- - - - -
diff --git a/src/internet/spotifywebapi/spotifywebapiservice.cpp b/src/internet/spotifywebapi/spotifywebapiservice.cpp deleted file mode 100644 index 4bd08101f..000000000 --- a/src/internet/spotifywebapi/spotifywebapiservice.cpp +++ /dev/null @@ -1,198 +0,0 @@ -/* This file is part of Clementine. - Copyright 2021, Kenman Tsang - - 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 "spotifywebapiservice.h" - -#include -#include -#include -#include -#include - -#include "3rdparty/qtiocompressor/qtiocompressor.h" -#include "core/application.h" -#include "core/network.h" -#include "core/timeconstants.h" -#include "globalsearch/globalsearch.h" -#include "globalsearch/spotifywebapisearchprovider.h" -#include "ui/iconloader.h" - -namespace { -static constexpr const char* kServiceName = "SpotifyWebApi"; -static constexpr const char* kGetAccessTokenUrl = - "https://open.spotify.com/" - "get_access_token?reason=transport&productType=web_player"; -static constexpr const char* kSearchUrl = - "https://api.spotify.com/v1/search?q=%1&type=track&limit=50"; - -template -inline QJsonValue Get(QJsonValue obj, Args&&... args) { - std::array names = { - std::forward(args)...}; - for (auto&& name : names) { - Q_ASSERT(obj.isObject()); - obj = obj.toObject()[name]; - } - return obj; -} - -template -inline QJsonValue Get(const QJsonDocument& obj, Args&&... args) { - return Get(obj.object(), std::forward(args)...); -} - -QString concat(const QJsonArray& array, const char* name) { - QStringList ret; - for (auto&& item : array) { - ret << Get(item, name).toString(); - } - return ret.join(", "); -} - -} // namespace - -SpotifyWebApiService::SpotifyWebApiService(Application* app, - InternetModel* parent) - : InternetService(kServiceName, app, parent, parent), - network_(new NetworkAccessManager{this}), - token_expiration_ms_{0} { - app_->global_search()->AddProvider( - new SpotifyWebApiSearchProvider(app_, this)); -} - -SpotifyWebApiService::~SpotifyWebApiService() {} - -QStandardItem* SpotifyWebApiService::CreateRootItem() { - root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider), - kServiceName); - return root_; -} - -void SpotifyWebApiService::LazyPopulate(QStandardItem* item) {} - -void SpotifyWebApiService::Search(int searchId, QString queryStr) { - if (QDateTime::currentDateTime().toMSecsSinceEpoch() >= - token_expiration_ms_) { - QNetworkRequest request{QUrl{kGetAccessTokenUrl}}; - request.setRawHeader("Accept-Encoding", "gzip"); - - QNetworkReply* reply = network_->get(request); - connect(reply, &QNetworkReply::finished, [=]() { - reply->deleteLater(); - - OnTokenReady(ParseJsonReplyWithGzip(reply), searchId, queryStr); - }); - } else { - OnReadyToSearch(searchId, queryStr); - } -} - -void SpotifyWebApiService::OnTokenReady(const QJsonDocument& json, int searchId, - QString queryStr) { - if (!json.isEmpty()) { - token_ = Get(json, "accessToken").toString(); - token_expiration_ms_ = static_cast( - Get(json, "accessTokenExpirationTimestampMs").toDouble()); - - qLog(Debug) << "Spotify API Token:" << token_; - - OnReadyToSearch(searchId, queryStr); - } -} - -void SpotifyWebApiService::OnReadyToSearch(int searchId, QString queryStr) { - qLog(Debug) << "Spotify API Searching: " << queryStr; - - QNetworkRequest request{ - QUrl{QString(kSearchUrl).arg(queryStr.toHtmlEscaped())}}; - - request.setRawHeader("Accept-Encoding", "gzip"); - request.setRawHeader("Authorization", ("Bearer " + token_).toUtf8()); - - QNetworkReply* reply = network_->get(request); - connect(reply, &QNetworkReply::finished, [=] { - reply->deleteLater(); - - BuildResultList(ParseJsonReplyWithGzip(reply), searchId); - }); -} - -void SpotifyWebApiService::BuildResultList(const QJsonDocument& json, - int searchId) { - QList result; - - for (auto&& item : Get(json, "tracks", "items").toArray()) { - Song song; - - song.set_albumartist( - concat(Get(item, "album", "artists").toArray(), "name")); - song.set_album(Get(item, "album", "name").toString()); - song.set_artist(concat(Get(item, "artists").toArray(), "name")); - song.set_disc(Get(item, "disc_number").toInt()); - song.set_length_nanosec(Get(item, "duration_ms").toInt() * kNsecPerMsec); - song.set_title(Get(item, "name").toString()); - song.set_track(Get(item, "track_number").toInt()); - song.set_url(QUrl{Get(item, "uri").toString()}); - song.set_filetype(Song::Type_Stream); - song.set_valid(true); - song.set_directory_id(0); - song.set_mtime(0); - song.set_ctime(0); - song.set_filesize(0); - - result += song; - } - - emit SearchFinished(searchId, result); -} - -QJsonDocument SpotifyWebApiService::ParseJsonReplyWithGzip( - QNetworkReply* reply) { - if (reply->error() != QNetworkReply::NoError) { - app_->AddError(tr("%1 request failed:\n%2") - .arg(kServiceName) - .arg(reply->errorString())); - return QJsonDocument(); - } - - QByteArray output; - if (reply->hasRawHeader("content-encoding") && - reply->rawHeader("content-encoding") == "gzip") { - QtIOCompressor gzip(reply); - gzip.setStreamFormat(QtIOCompressor::GzipFormat); - if (!gzip.open(QIODevice::ReadOnly)) { - app_->AddError(tr("%1 failed to decode as gzip stream:\n%2") - .arg(kServiceName) - .arg(gzip.errorString())); - return QJsonDocument(); - } - output = gzip.readAll(); - } else { - output = reply->readAll(); - } - - QJsonParseError error; - QJsonDocument document = QJsonDocument::fromJson(output, &error); - if (error.error != QJsonParseError::NoError) { - app_->AddError(tr("Failed to parse %1 response:\n%2") - .arg(kServiceName) - .arg(error.errorString())); - return QJsonDocument(); - } - - return document; -} diff --git a/src/internet/spotifywebapi/spotifywebapiservice.h b/src/internet/spotifywebapi/spotifywebapiservice.h deleted file mode 100644 index 04e97bb13..000000000 --- a/src/internet/spotifywebapi/spotifywebapiservice.h +++ /dev/null @@ -1,60 +0,0 @@ -/* This file is part of Clementine. - Copyright 2021, Kenman Tsang - - 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 SPOTIFYWEBAPISERVICE_H -#define SPOTIFYWEBAPISERVICE_H - -#include - -#include "internet/core/internetmodel.h" -#include "internet/core/internetservice.h" - -class NetworkAccessManager; - -class SpotifyWebApiService : public InternetService { - Q_OBJECT - - public: - SpotifyWebApiService(Application* app, InternetModel* parent); - ~SpotifyWebApiService(); - - QStandardItem* CreateRootItem() override; - void LazyPopulate(QStandardItem* parent) override; - - public: - void Search(int searchId, QString queryStr); - - private: - void OnTokenReady(const QJsonDocument&, int searchId, QString queryStr); - void OnReadyToSearch(int searchId, QString queryStr); - void BuildResultList(const QJsonDocument&, int searchId); - - signals: - void SearchFinished(int searchId, const QList&); - - private: - QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply); - - private: - QStandardItem* root_; - NetworkAccessManager* network_; - - QString token_; - qint64 token_expiration_ms_; -}; - -#endif // SPOTIFYWEBAPISERVICE_H