parent
99029ed643
commit
a551c40c4e
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -1,49 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <gst/gst.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QStringList>
|
||||
|
||||
#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();
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <cstring>
|
||||
|
||||
#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<MediaPipeline*>(data);
|
||||
me->accepting_data_ = true;
|
||||
}
|
||||
|
||||
void MediaPipeline::EnoughDataCallback(GstAppSrc* src, void* data) {
|
||||
MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
|
||||
me->accepting_data_ = false;
|
||||
}
|
||||
|
||||
gboolean MediaPipeline::SeekDataCallback(GstAppSrc* src, guint64 offset,
|
||||
void* data) {
|
||||
// MediaPipeline* me = reinterpret_cast<MediaPipeline*>(data);
|
||||
|
||||
qLog(Debug) << "Gstreamer wants seek to" << offset;
|
||||
return false;
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <QtGlobal>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsrc.h>
|
||||
|
||||
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
|
|
@ -1,66 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <stdlib.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
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
|
|
@ -1,37 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <QString>
|
||||
|
||||
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
|
|
@ -1,44 +0,0 @@
|
|||
#include "spotify_utilities.h"
|
||||
|
||||
#import <Foundation/NSAutoreleasePool.h>
|
||||
#import <Foundation/NSFileManager.h>
|
||||
#import <Foundation/NSPathUtilities.h>
|
||||
|
||||
#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
|
File diff suppressed because it is too large
Load Diff
|
@ -1,204 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <QMap>
|
||||
#include <QObject>
|
||||
|
||||
#include <libspotify/api.h>
|
||||
|
||||
class QTcpSocket;
|
||||
class QTimer;
|
||||
|
||||
class MediaPipeline;
|
||||
class ResponseMessage;
|
||||
|
||||
class SpotifyClient : public AbstractMessageHandler<cpb::spotify::Message> {
|
||||
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<sp_track*> 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<PendingLoadPlaylist> pending_load_playlists_;
|
||||
QList<PendingPlaybackRequest> pending_playback_requests_;
|
||||
QList<PendingImageRequest> pending_image_requests_;
|
||||
QMap<sp_image*, int> image_callbacks_registered_;
|
||||
QMap<sp_search*, cpb::spotify::SearchRequest> pending_searches_;
|
||||
QMap<sp_albumbrowse*, QString> pending_album_browses_;
|
||||
QMap<sp_toplistbrowse*, cpb::spotify::BrowseToplistRequest>
|
||||
pending_toplist_browses_;
|
||||
|
||||
QMap<sp_search*, QList<sp_albumbrowse*>> pending_search_album_browses_;
|
||||
QMap<sp_albumbrowse*, sp_search*> pending_search_album_browse_responses_;
|
||||
|
||||
QScopedPointer<MediaPipeline> media_pipeline_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYCLIENT_H
|
|
@ -1,37 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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";
|
|
@ -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
|
||||
)
|
|
@ -1,23 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFY_BLOBVERSION_H_IN
|
||||
#define SPOTIFY_BLOBVERSION_H_IN
|
||||
|
||||
#define SPOTIFY_BLOB_VERSION ${SPOTIFY_BLOB_VERSION}
|
||||
|
||||
#endif // SPOTIFY_BLOBVERSION_H_IN
|
|
@ -1,236 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<SpotifyService>();
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<SpotifyService>()->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<GstStateChangeReturn> 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<SpotifyService>();
|
||||
|
||||
// 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<SpotifyService>();
|
||||
|
||||
// Need to schedule this in the spotify service's thread
|
||||
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
|
||||
Q_ARG(bool, false));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
|
||||
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
||||
}
|
||||
|
|
|
@ -1,280 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifysearchprovider.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
|
||||
#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<SpotifyService>();
|
||||
|
||||
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<QString, PendingState>::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<std::string, std::string> album_dedup;
|
||||
ResultList ret;
|
||||
|
||||
for (int i = 0; i < response.album_size(); ++i) {
|
||||
const cpb::spotify::Album& album = response.album(i);
|
||||
|
||||
QHash<QString, int> 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<QString, int>::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<QString> 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();
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef 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<QString, PendingState> queries_;
|
||||
QMap<QString, int> pending_art_;
|
||||
QMap<QString, int> pending_tracks_;
|
||||
|
||||
QSet<QString> suggestions_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYSEARCHPROVIDER_H
|
|
@ -1,97 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifywebapisearchprovider.h"
|
||||
|
||||
#include <qurl.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#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<Song>& 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{};
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef 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<Song>&);
|
||||
|
||||
private:
|
||||
SpotifyWebApiService* parent_;
|
||||
int last_search_id_;
|
||||
QString last_query_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYWEBAPISEARCHPROVIDER_H
|
|
@ -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));
|
||||
|
|
|
@ -1,281 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2012, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifyblobdownloader.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QProgressDialog>
|
||||
#include <QSslKey>
|
||||
|
||||
#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 <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CRYPTOPP
|
||||
#include <cryptopp/pkcspad.h>
|
||||
#include <cryptopp/rsa.h>
|
||||
|
||||
// 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<QNetworkReply*>(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<QString, QByteArray> 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<QString, QByteArray>& 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<const CryptoPP::byte*>(public_key_data.constData()),
|
||||
public_key_data.size());
|
||||
bytes.MessageEnd();
|
||||
|
||||
CryptoPP::RSA::PublicKey public_key;
|
||||
public_key.Load(bytes);
|
||||
|
||||
CryptoPP::RSASS<CryptoPP::PKCS1v15, CryptoPP::SHA512>::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<const CryptoPP::byte*>(
|
||||
file_data[actual_filename].constData()),
|
||||
file_data[actual_filename].size(),
|
||||
reinterpret_cast<const CryptoPP::byte*>(
|
||||
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();
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
|
||||
#define INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
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<QString, QByteArray>& file_data,
|
||||
const QStringList& signature_filenames);
|
||||
static QByteArray ConvertPEMToDER(const QByteArray& pem);
|
||||
|
||||
private:
|
||||
QString version_;
|
||||
QString path_;
|
||||
|
||||
QNetworkAccessManager* network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
QProgressDialog* progress_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_SPOTIFY_SPOTIFYBLOBDOWNLOADER_H_
|
|
@ -1,321 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifyserver.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
SpotifyServer::SpotifyServer(QObject* parent)
|
||||
: AbstractMessageHandler<cpb::spotify::Message>(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<cpb::spotify::Message>* 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<QUrl>& songs_urls) {
|
||||
AddSongsToPlaylist(cpb::spotify::Starred, songs_urls);
|
||||
}
|
||||
|
||||
void SpotifyServer::AddSongsToUserPlaylist(int playlist_index,
|
||||
const QList<QUrl>& songs_urls) {
|
||||
AddSongsToPlaylist(cpb::spotify::UserPlaylist, songs_urls, playlist_index);
|
||||
}
|
||||
|
||||
void SpotifyServer::AddSongsToPlaylist(
|
||||
const cpb::spotify::PlaylistType playlist_type,
|
||||
const QList<QUrl>& 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<int>& songs_indices_to_remove) {
|
||||
RemoveSongsFromPlaylist(cpb::spotify::Starred, songs_indices_to_remove);
|
||||
}
|
||||
|
||||
void SpotifyServer::RemoveSongsFromUserPlaylist(
|
||||
int playlist_index, const QList<int>& 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<int>& 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);
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVER_H_
|
||||
#define INTERNET_SPOTIFY_SPOTIFYSERVER_H_
|
||||
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
|
||||
#include "core/messagehandler.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
class QTcpServer;
|
||||
class QTcpSocket;
|
||||
|
||||
class SpotifyServer : public AbstractMessageHandler<cpb::spotify::Message> {
|
||||
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<QUrl>& songs_urls);
|
||||
void AddSongsToUserPlaylist(int playlist_index,
|
||||
const QList<QUrl>& songs_urls);
|
||||
void RemoveSongsFromUserPlaylist(int playlist_index,
|
||||
const QList<int>& songs_indices_to_remove);
|
||||
void RemoveSongsFromStarred(const QList<int>& 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<QUrl>& songs_urls,
|
||||
// Used iff type is user_playlist
|
||||
int playlist_index = -1);
|
||||
void RemoveSongsFromPlaylist(const cpb::spotify::PlaylistType playlist_type,
|
||||
const QList<int>& 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<cpb::spotify::Message> queued_login_messages_;
|
||||
QList<cpb::spotify::Message> queued_messages_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_SPOTIFY_SPOTIFYSERVER_H_
|
|
@ -1,993 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011-2014, David Sansome <me@davidsansome.com>
|
||||
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
|
||||
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
Copyright 2014, Chocobozzz <florian.bigard@gmail.com>
|
||||
Copyright 2014, pie.or.paj <pie.or.paj@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifyservice.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMimeData>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
|
||||
#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<cpb::spotify::Bitrate>(
|
||||
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<QUrl>() << current_song_url_);
|
||||
}
|
||||
|
||||
void SpotifyService::AddSongsToUserPlaylist(int playlist_index,
|
||||
const QList<QUrl>& songs_urls) {
|
||||
EnsureServerCreated();
|
||||
server_->AddSongsToUserPlaylist(playlist_index, songs_urls);
|
||||
}
|
||||
|
||||
void SpotifyService::AddCurrentSongToStarredPlaylist() {
|
||||
AddSongsToStarred(QList<QUrl>() << current_song_url_);
|
||||
}
|
||||
|
||||
void SpotifyService::AddSongsToStarred(const QList<QUrl>& 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<cpb::spotify::Track>& 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<QAction*> 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<QStandardItem*>();
|
||||
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<SpotifyService*>(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<int, int>::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<int, QList<int>> playlists_songs_indices;
|
||||
QList<int> 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<int, QList<int>>::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<int>& songs_indices_to_remove) {
|
||||
server_->RemoveSongsFromUserPlaylist(playlist_index, songs_indices_to_remove);
|
||||
}
|
||||
|
||||
void SpotifyService::RemoveSongsFromStarred(
|
||||
const QList<int>& 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_);
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, Tyler Rhodes <tyler.s.rhodes@gmail.com>
|
||||
Copyright 2011-2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
Copyright 2011-2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2011-2012, 2014, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
||||
#define INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
||||
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
||||
#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<QAction*> 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<cpb::spotify::Track>& tracks);
|
||||
void FillPlaylist(QStandardItem* item,
|
||||
const cpb::spotify::LoadPlaylistResponse& response);
|
||||
void AddSongsToUserPlaylist(int playlist_index,
|
||||
const QList<QUrl>& songs_urls);
|
||||
void AddSongsToStarred(const QList<QUrl>& 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<int>& songs_indices_to_remove);
|
||||
void RemoveSongsFromStarred(const QList<int>& 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<QStandardItem*> 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<QAction*> 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<int, int> playlist_sync_ids_;
|
||||
|
||||
LoginState login_state_;
|
||||
cpb::spotify::Bitrate bitrate_;
|
||||
bool volume_normalisation_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_SPOTIFY_SPOTIFYSERVICE_H_
|
|
@ -1,176 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, Andrea Decorte <adecorte@gmail.com>
|
||||
Copyright 2011-2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifysettingspage.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<SpotifyService>()),
|
||||
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();
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef 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_
|
|
@ -1,211 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SpotifySettingsPage</class>
|
||||
<widget class="QWidget" name="SpotifySettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>545</width>
|
||||
<height>458</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Spotify</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="account_group">
|
||||
<property name="title">
|
||||
<string>Account details</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QWidget" name="login_container" native="true">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="username_label">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="password_label">
|
||||
<property name="text">
|
||||
<string>Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Spotify plugin</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>For licensing reasons Spotify support is in a separate plugin.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Plugin status:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="blob_status"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="download_blob">
|
||||
<property name="text">
|
||||
<string>Download...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Preferred bitrate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="bitrate"/>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="volume_normalisation">
|
||||
<property name="text">
|
||||
<string>Use volume normalisation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../../../data/data.qrc">:/spotify-attribution.png</pixmap>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LoginStateWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/loginstatewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -1,198 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spotifywebapiservice.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
#include <utility>
|
||||
|
||||
#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 <typename... Args>
|
||||
inline QJsonValue Get(QJsonValue obj, Args&&... args) {
|
||||
std::array<const char*, sizeof...(Args)> names = {
|
||||
std::forward<Args>(args)...};
|
||||
for (auto&& name : names) {
|
||||
Q_ASSERT(obj.isObject());
|
||||
obj = obj.toObject()[name];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline QJsonValue Get(const QJsonDocument& obj, Args&&... args) {
|
||||
return Get(obj.object(), std::forward<Args>(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<qint64>(
|
||||
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<Song> 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;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYWEBAPISERVICE_H
|
||||
#define SPOTIFYWEBAPISERVICE_H
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#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<Song>&);
|
||||
|
||||
private:
|
||||
QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QStandardItem* root_;
|
||||
NetworkAccessManager* network_;
|
||||
|
||||
QString token_;
|
||||
qint64 token_expiration_ms_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYWEBAPISERVICE_H
|
Loading…
Reference in New Issue