Remove Spotify playback support

libspotify is dead
This commit is contained in:
John Maguire 2022-09-01 21:48:13 +01:00
parent 99029ed643
commit a551c40c4e
35 changed files with 9 additions and 5555 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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