diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2612682d..321be2d6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -71,6 +71,11 @@ if (BUILD_SNIPPETS)
add_subdirectory(snippets)
endif()
+option(BUILD_SPOTIFY "Build the extension" ON)
+if (BUILD_SPOTIFY)
+ add_subdirectory(spotify)
+endif()
+
option(BUILD_SSH "Build the extension" ON)
if (BUILD_SSH)
add_subdirectory(ssh)
diff --git a/spotify/CMakeLists.txt b/spotify/CMakeLists.txt
new file mode 100644
index 00000000..52a97846
--- /dev/null
+++ b/spotify/CMakeLists.txt
@@ -0,0 +1,21 @@
+cmake_minimum_required(VERSION 3.1.3)
+
+project(spotify)
+
+file(GLOB_RECURSE SRC src/* metadata.json)
+
+find_package(Qt5 5.5.0 REQUIRED COMPONENTS Network Widgets)
+
+add_library(${PROJECT_NAME} SHARED ${SRC})
+
+target_include_directories(${PROJECT_NAME} PRIVATE src/)
+
+target_link_libraries(${PROJECT_NAME}
+ PRIVATE
+ Qt5::Network
+ Qt5::Widgets
+ albert::lib
+ xdg
+)
+
+install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/albert/plugins)
diff --git a/spotify/README.md b/spotify/README.md
new file mode 100644
index 00000000..535c6259
--- /dev/null
+++ b/spotify/README.md
@@ -0,0 +1,60 @@
+# Spotify extension
+
+The Spotify extension for Albert launcher allows you to search
+tracks on Spotify and play them immediately or add them to the
+queue. It also allows you to choose the Spotify client, where
+to play the track.
+
+The extension uses the Spotify Web API.
+
+For the proper
+functionality of extension, **Spotify premium is required**.
+
+
+
+## Web API connection
+
+### 1. Get your Client ID and Client Secret
+
+Visit: https://developer.spotify.com/dashboard/applications and log
+in with your Spotify account.
+
+Click on the button **Create an app**
+and fill the form. You can use for example name "Albert" and
+description "Spotify extension for Albert launcher".
+
+Once you
+click on **Create**, your new application window will appear. You
+can copy your **Client ID** and show **Client Secret**.
+Both are 32-character strings.
+
+Click on **Edit settings** and add new **Redirect URI**. It doesn't
+have to exist. In this example, I will use: `https://nonexistent-uri.net/`
+
+### 2. Get `code` parameter
+
+Open your browser and visit: https://accounts.spotify.com/cs/authorize?response_type=code&client_id=[[client_id]]&scope=user-modify-playback-state%20user-read-playback-state&redirect_uri=https://nonexistent-uri.net/
+
+You have to replace `[[client_id]]` with your actual **Client ID**.
+
+When you press enter, you will get redirected to
+`https://nonexistent-uri.net/` with `code` in URL parameters.
+Copy that string and note it down for the next usage.
+
+### 3. Get your Refresh Token
+
+I will use `curl` for this last step. Replace or export all variables and run this command:
+
+```
+curl -d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET -d grant_type=authorization_code -d code=$CODE -d redirect_uri=https://nonexistent-uri.net/ https://accounts.spotify.com/api/token
+```
+
+Use your Client ID, Client Secret and `code` from the previous step.
+
+It will send POST request and return JSON in the answer.
+You can finally get your **Refresh Token**.
+
+
+
+The whole process is also similarly described
+[here](https://benwiz.com/blog/create-spotify-refresh-token/).
\ No newline at end of file
diff --git a/spotify/metadata.json b/spotify/metadata.json
new file mode 100644
index 00000000..bc8db2d3
--- /dev/null
+++ b/spotify/metadata.json
@@ -0,0 +1,9 @@
+{
+ "id" : "org.albert.extension.spotify",
+ "name" : "Spotify",
+ "version" : "1.0",
+ "platform" : "All",
+ "group" : "Extensions",
+ "author" : "Ivo Šmerek",
+ "dependencies" : []
+}
diff --git a/spotify/src/configwidget.cpp b/spotify/src/configwidget.cpp
new file mode 100644
index 00000000..9f308b21
--- /dev/null
+++ b/spotify/src/configwidget.cpp
@@ -0,0 +1,15 @@
+// Copyright (C) 2014-2018 Manuel Schneider
+
+#include "configwidget.h"
+
+/** ***************************************************************************/
+Spotify::ConfigWidget::ConfigWidget(QWidget *parent) : QWidget(parent) {
+ ui.setupUi(this);
+}
+
+
+
+/** ***************************************************************************/
+Spotify::ConfigWidget::~ConfigWidget() {
+
+}
diff --git a/spotify/src/configwidget.h b/spotify/src/configwidget.h
new file mode 100644
index 00000000..5fdd3ea0
--- /dev/null
+++ b/spotify/src/configwidget.h
@@ -0,0 +1,16 @@
+// Copyright (C) 2014-2018 Manuel Schneider
+
+#pragma once
+#include
+#include "ui_configwidget.h"
+
+namespace Spotify {
+class ConfigWidget final : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ConfigWidget(QWidget *parent = nullptr);
+ ~ConfigWidget();
+ Ui::ConfigWidget ui;
+};
+}
diff --git a/spotify/src/configwidget.ui b/spotify/src/configwidget.ui
new file mode 100644
index 00000000..027160f4
--- /dev/null
+++ b/spotify/src/configwidget.ui
@@ -0,0 +1,227 @@
+
+
+ Spotify::ConfigWidget
+
+
+
+ 0
+ 0
+ 448
+ 325
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ <html><head/><body><p>Web API connection</p></body></html>
+
+
+
+ -
+
+
-
+
+
+ Client ID:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Client Secret:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -
+
+
+ Refresh Token:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
-
+
+
+ Test connection
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600;">User preferences</span></p></body></html>
+
+
+
+ -
+
+
-
+
+
+ Number of results:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
-
+
+
+ 1
+
+
+ 10
+
+
+ 5
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Spotify executable:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+
+
+ spotify
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Allow explicit content:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Cache directory:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ /tmp/albert-spotify-covers
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+ lineEdit_client_id
+ lineEdit_client_secret
+ lineEdit_refresh_token
+
+
+
+
diff --git a/spotify/src/device.h b/spotify/src/device.h
new file mode 100644
index 00000000..07c29cfb
--- /dev/null
+++ b/spotify/src/device.h
@@ -0,0 +1,18 @@
+// Copyright (C) 2020-2021 Ivo Šmerek
+
+#include
+
+namespace Spotify {
+
+ class Device {
+
+ public:
+ Device() = default;
+ ~Device() = default;
+
+ QString id;
+ QString name;
+ QString type;
+ bool isActive = false;
+ };
+}
diff --git a/spotify/src/extension.cpp b/spotify/src/extension.cpp
new file mode 100644
index 00000000..fa18f853
--- /dev/null
+++ b/spotify/src/extension.cpp
@@ -0,0 +1,295 @@
+// Copyright (C) 2014-2021 Manuel Schneider, Ivo Šmerek
+
+#include
+#include
+#include "albert/util/standardactions.h"
+#include "albert/util/standarditem.h"
+#include "configwidget.h"
+#include "extension.h"
+#include "spotifyWebAPI.h"
+Q_LOGGING_CATEGORY(qlc, "spotify")
+#define DEBG qCDebug(qlc,).noquote()
+#define INFO qCInfo(qlc,).noquote()
+#define WARN qCWarning(qlc,).noquote()
+#define CRIT qCCritical(qlc,).noquote()
+using namespace Core;
+using namespace std;
+
+class Spotify::Private
+{
+public:
+ QPointer widget;
+ QString clientId;
+ QString clientSecret;
+ QString refreshToken;
+ QString spotifyExecutable;
+ QString cacheDirectory;
+ bool explicitState = true;
+ int numberOfResults = 5;
+ SpotifyWebAPI *api = nullptr;
+};
+
+
+/** ***************************************************************************/
+Spotify::Extension::Extension()
+ : Core::Extension("org.albert.extension.spotify"), // Must match the id in metadata
+ Core::QueryHandler(Core::Plugin::id()),
+ d(new Private) {
+
+ registerQueryHandler(this);
+
+ d->api = new SpotifyWebAPI(this);
+
+ d->clientId = settings().value("client_id").toString();
+ d->clientSecret = settings().value("client_secret").toString();
+ d->refreshToken = settings().value("refresh_token").toString();
+ d->explicitState = settings().value("explicit_state").toBool();
+ d->numberOfResults = settings().value("number_or_results").toInt();
+ d->spotifyExecutable = settings().value("spotify_executable").toString();
+ d->cacheDirectory = settings().value("cache_directory").toString();
+
+ if (d->numberOfResults == 0) {
+ d->numberOfResults = 5;
+ }
+
+ if (d->spotifyExecutable.isEmpty()) {
+ d->spotifyExecutable = SPOTIFY_EXECUTABLE;
+ }
+
+ if (d->cacheDirectory.isEmpty()) {
+ d->cacheDirectory = CACHE_DIRECTORY;
+ }
+}
+
+
+
+/** ***************************************************************************/
+Spotify::Extension::~Extension() = default;
+
+
+
+/** ***************************************************************************/
+QWidget *Spotify::Extension::widget(QWidget *parent) {
+ if (d->widget.isNull()) {
+ d->widget = new ConfigWidget(parent);
+ }
+
+ // Initialize the content and connect the signals
+
+ d->widget->ui.lineEdit_client_id->setText(d->clientId);
+ connect(d->widget->ui.lineEdit_client_id, &QLineEdit::textEdited, [this](const QString &s){
+ d->clientId = s;
+ settings().setValue("client_id", s);
+ });
+
+ d->widget->ui.lineEdit_client_secret->setText(d->clientSecret);
+ connect(d->widget->ui.lineEdit_client_secret, &QLineEdit::textEdited, [this](const QString &s){
+ d->clientSecret = s;
+ settings().setValue("client_secret", s);
+ });
+
+ d->widget->ui.lineEdit_refresh_token->setText(d->refreshToken);
+ connect(d->widget->ui.lineEdit_refresh_token, &QLineEdit::textEdited, [this](const QString &s){
+ d->refreshToken = s;
+ settings().setValue("refresh_token", s);
+ });
+
+ d->widget->ui.checkBox_explicit->setCheckState(d->explicitState ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
+ connect(d->widget->ui.checkBox_explicit, &QCheckBox::stateChanged, [this](const int s){
+ d->explicitState = s;
+ settings().setValue("explicit_state", s);
+ });
+
+ d->widget->ui.spinBox_number_of_results->setValue(d->numberOfResults);
+ connect(d->widget->ui.spinBox_number_of_results, &QSpinBox::textChanged, [this](const QString &s){
+ d->numberOfResults = s.toInt();
+ settings().setValue("number_or_results", s);
+ });
+
+ if (d->spotifyExecutable != SPOTIFY_EXECUTABLE) {
+ d->widget->ui.lineEdit_spotify_executable->setText(d->spotifyExecutable);
+ }
+ d->widget->ui.lineEdit_spotify_executable->setPlaceholderText(SPOTIFY_EXECUTABLE);
+ connect(d->widget->ui.lineEdit_spotify_executable, &QLineEdit::textEdited, [this](const QString &s){
+ if (s.isEmpty()) {
+ d->spotifyExecutable = SPOTIFY_EXECUTABLE;
+ } else {
+ d->spotifyExecutable = s;
+ }
+ settings().setValue("spotify_executable", s);
+ });
+
+ if (d->cacheDirectory != CACHE_DIRECTORY) {
+ d->widget->ui.lineEdit_cache_directory->setText(d->cacheDirectory);
+ }
+ d->widget->ui.lineEdit_cache_directory->setPlaceholderText(CACHE_DIRECTORY);
+ connect(d->widget->ui.lineEdit_cache_directory, &QLineEdit::textEdited, [this](const QString &s){
+ if (s.isEmpty()) {
+ d->cacheDirectory = CACHE_DIRECTORY;
+ } else {
+ d->cacheDirectory = s;
+ }
+ settings().setValue("cache_directory", s);
+ });
+
+ // Bind "Test connection" button
+
+ connect(d->widget->ui.pushButton_test_connection, &QPushButton::clicked, [this](){
+ d->api->setConnection(d->clientId, d->clientSecret, d->refreshToken);
+ d->api->manager = new QNetworkAccessManager();
+
+ bool status = d->api->testConnection();
+
+ QString message = "Everything is set up correctly.";
+ if (!status) {
+ message = QString("Spotify Web API returns: \"%1\"\nPlease, check all input fields.")
+ .arg(d->api->lastErrorMessage);
+ if (d->api->lastErrorMessage.isEmpty()) {
+ message = "Can't get an answer from the server.\nPlease, check your internet connection.";
+ }
+ }
+
+ auto messageBox = new QMessageBox();
+ messageBox->setWindowTitle(status ? "Success" : "API error");
+ messageBox->setText(message);
+ messageBox->setIcon(status ? QMessageBox::Information : QMessageBox::Critical);
+ messageBox->exec();
+ });
+
+ return d->widget;
+}
+
+
+
+/** ***************************************************************************/
+void Spotify::Extension::setupSession() {
+ d->api->setConnection(d->clientId, d->clientSecret, d->refreshToken);
+
+ if(!QDir(d->cacheDirectory).exists()) {
+ QDir().mkdir(d->cacheDirectory);
+ }
+}
+
+
+
+/** ***************************************************************************/
+void Spotify::Extension::teardownSession() {
+
+}
+
+
+
+/** ***************************************************************************/
+void Spotify::Extension::handleQuery(Core::Query * query) const {
+ if (query->string().trimmed().isEmpty())
+ return;
+
+ d->api->manager = new QNetworkAccessManager();
+
+ // If there is no internet connection, make one alerting item to let the user know.
+ if (!d->api->testInternetConnection()) {
+ DEBG << "No internet connection!";
+
+ auto result = makeStdItem("no-internet", "", "Can't get an answer from the server.");
+ result->setSubtext("Please, check your internet connection.");
+
+ query->addMatch(move(result), UINT_MAX);
+ return;
+ }
+
+ // If the access token expires, try to refresh it or alert the user what is wrong.
+ if (d->api->expired()) {
+ DEBG << "Token expired. Refreshing";
+ if (!d->api->refreshToken()) {
+ auto result = makeStdItem("wrong-credentials", "", "Wrong credentials");
+ result->setSubtext(d->api->lastErrorMessage + ". Please, check extension settings.");
+
+ query->addMatch(move(result), UINT_MAX);
+ return;
+ }
+ }
+
+ // Search for tracks on Spotify using the query.
+ auto results = d->api->searchTracks(query->string(), d->numberOfResults);
+
+ // Get available Spotify devices.
+ auto *devices = d->api->getDevices();
+
+ for (const auto& track : results) {
+ // Deal with explicit tracks according to user setting.
+ if (track.isExplicit && !d->explicitState) {
+ continue;
+ }
+
+ auto filename = QString("%1/%2.jpeg").arg(d->cacheDirectory, track.albumId);
+
+ // Download cover image of the album.
+ d->api->downloadImage(track.imageUrl, filename);
+
+ // Create a standard item with a track name in title and album with artists in subtext.
+ auto result = makeStdItem(track.id, filename, track.name);
+ result->setSubtext(QString("%1 (%2)").arg(track.albumName, track.artists));
+
+ // First default action with intelligent device chooser.
+ auto playTrack = makeFuncAction("Play this track on Spotify", [this, track, devices]()
+ {
+ // Check if the last-used device is still available.
+ bool lastDeviceConfirmed = false;
+ QString lastDevice = settings().value("last_device").toString();
+ if (!lastDevice.isEmpty() || !devices->isEmpty()) {
+ for (const auto& device : *devices) {
+ if (device.id == lastDevice) {
+ lastDeviceConfirmed = true;
+ break;
+ }
+ }
+ }
+
+ if (d->api->activeDevice) {
+ // If available, use an active device and play the track.
+ // TODO: Maybe let user choose in setting if prefer active or last-used device.
+ d->api->play(track.uri, d->api->activeDevice->id);
+ settings().setValue("last_device", d->api->activeDevice->id);
+ } else if (lastDeviceConfirmed) {
+ // If there is not an active device, use last-used one.
+ d->api->play(track.uri, lastDevice);
+ } else if (!devices->isEmpty()) {
+ // Use the first available device.
+ d->api->play(track.uri, devices[0][0].id);
+ settings().setValue("last_device", devices[0][0].id);
+ } else {
+ // Run local Spotify client, wait until it loads, and play the track.
+ makeProcAction("Run Spotify", QStringList() << d->spotifyExecutable)->activate();
+ d->api->waitForDeviceAndPlay(track.uri, 10);
+ }
+ });
+
+ // Action to add track to the Spotify queue.
+ auto addToQueue = makeFuncAction("Add to the Spotify queue", [this, track]()
+ {
+ d->api->addItemToQueue(track.uri);
+ });
+
+ result->addAction(playTrack);
+ result->addAction(addToQueue);
+
+ // For each device except active create action to transfer Spotify playback to this device.
+ for (const auto& device : *devices) {
+ if (device.isActive) continue;
+
+ auto action = makeFuncAction(QString("Play on %1 (%2)").arg(device.type, device.name), [this, track, device]()
+ {
+ d->api->play(track.uri, device.id);
+ settings().setValue("last_device", device.id);
+ });
+
+ result->addAction(action);
+ }
+
+ query->addMatch(move(result), UINT_MAX);
+ }
+}
+
+QueryHandler::ExecutionType Spotify::Extension::executionType() const {
+ return QueryHandler::ExecutionType::Realtime;
+}
diff --git a/spotify/src/extension.h b/spotify/src/extension.h
new file mode 100644
index 00000000..3bd81150
--- /dev/null
+++ b/spotify/src/extension.h
@@ -0,0 +1,37 @@
+// Copyright (C) 2014-2021 Manuel Schneider, Ivo Šmerek
+
+#pragma once
+#include
+#include "albert/extension.h"
+#include "albert/queryhandler.h"
+Q_DECLARE_LOGGING_CATEGORY(qlc)
+
+namespace Spotify {
+
+class Private;
+
+class Extension final :
+ public Core::Extension,
+ public Core::QueryHandler
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID ALBERT_EXTENSION_IID FILE "metadata.json")
+
+public:
+ Extension();
+ ~Extension() override;
+
+ QString name() const override { return "Spotify"; }
+ QWidget *widget(QWidget *parent = nullptr) override;
+ QStringList triggers() const override { return {"spotify ", "play "}; }
+ void setupSession() override;
+ void teardownSession() override;
+ void handleQuery(Core::Query * query) const override;
+ ExecutionType executionType() const override;
+
+private:
+ std::unique_ptr d;
+ QString SPOTIFY_EXECUTABLE = "spotify";
+ QString CACHE_DIRECTORY = "/tmp/albert-spotify-covers";
+};
+}
diff --git a/spotify/src/spotifyWebAPI.cpp b/spotify/src/spotifyWebAPI.cpp
new file mode 100644
index 00000000..7a77bcf5
--- /dev/null
+++ b/spotify/src/spotifyWebAPI.cpp
@@ -0,0 +1,296 @@
+// Copyright (C) 2020-2021 Ivo Šmerek
+
+#include
+#include
+
+#include "spotifyWebAPI.h"
+
+namespace Spotify {
+
+ SpotifyWebAPI::SpotifyWebAPI(QObject* parent) {
+ manager = new QNetworkAccessManager(this);
+ setParent(parent);
+ }
+
+ SpotifyWebAPI::~SpotifyWebAPI() {
+ delete manager;
+ }
+
+ QJsonObject SpotifyWebAPI::answerToJson_(const QString& answer) {
+ QJsonDocument doc = QJsonDocument::fromJson(answer.toUtf8());
+ QJsonObject jsonObject = doc.object();
+ return jsonObject;
+ }
+
+ void SpotifyWebAPI::waitForSignal_(const QObject *sender, const char *signal) {
+ QEventLoop loop;
+ connect(sender, signal, &loop, SLOT(quit()));
+ loop.exec();
+ }
+
+ QString SpotifyWebAPI::waitForDevice_(QString uri, int timeout) {
+ int counter = 0;
+ while (counter < timeout) {
+ QThread::sleep(1);
+ auto device = getFirstDeviceId();
+ if (!device.isEmpty()) {
+ emit deviceReady(std::move(uri), device);
+ return device;
+ }
+ counter++;
+ }
+
+ return "";
+ }
+
+ QNetworkRequest SpotifyWebAPI::buildRequest_(const QUrl& url) {
+ auto request = new QNetworkRequest(url);
+ auto header = QString("Bearer ") + accessToken_;
+ request->setRawHeader(QByteArray("Authorization"), header.toUtf8());
+ request->setRawHeader(QByteArray("Accept"), "application/json");
+ request->setHeader(QNetworkRequest::ContentTypeHeader, QVariant(QString("application/json")));
+ return *request;
+ }
+
+ bool SpotifyWebAPI::testInternetConnection() {
+ try {
+ auto *url = new QUrl(TOKEN_URL);
+ QNetworkRequest request(*url);
+
+ if (manager->thread() != QThread::currentThread()) {
+ return false;
+ }
+ QNetworkReply *reply = manager->get(request);
+
+ waitForSignal_(reply, SIGNAL(finished()));
+ return reply->bytesAvailable();
+ } catch (...) {
+ return false;
+ }
+ }
+
+ void SpotifyWebAPI::setConnection(QString clientId, QString clientSecret, QString refreshToken) {
+ clientId_ = std::move(clientId);
+ clientSecret_ = std::move(clientSecret);
+ refreshToken_ = std::move(refreshToken);
+ expirationTime_ = QDateTime::currentDateTime();
+ }
+
+ bool SpotifyWebAPI::refreshToken() {
+ auto url = QUrl(TOKEN_URL);
+ QNetworkRequest request(url);
+
+ auto hash = QString("%1:%2").arg(clientId_, clientSecret_).toUtf8().toBase64();
+ auto header = QString("Basic ").append(hash);
+ request.setRawHeader(QByteArray("Authorization"), header.toUtf8());
+ request.setHeader(QNetworkRequest::ContentTypeHeader,
+ QVariant(QString("application/x-www-form-urlencoded")));
+
+ QByteArray postData = QString("grant_type=refresh_token&refresh_token=%1").arg(refreshToken_).toLocal8Bit();
+
+ QString savedToken = accessToken_;
+
+ if (manager->thread() != QThread::currentThread()) {
+ return false;
+ }
+ QNetworkReply *reply = manager->post(request, postData);
+
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ QString answer = reply->readAll();
+ QJsonObject jsonVariant = answerToJson_(answer);
+
+ accessToken_ = "";
+
+ if (!jsonVariant["access_token"].isUndefined()) {
+ accessToken_ = jsonVariant["access_token"].toString();
+ expirationTime_ = QDateTime::currentDateTime().addSecs(jsonVariant["expires_in"].toInt());
+ }
+
+ if (!jsonVariant["error_description"].isUndefined()) {
+ lastErrorMessage = jsonVariant["error_description"].toString();
+ } else {
+ lastErrorMessage = jsonVariant["error"].toString();
+ }
+ });
+
+ waitForSignal_(reply, SIGNAL(finished()));
+
+ return !accessToken_.isEmpty() && savedToken != accessToken_;
+ }
+
+ bool SpotifyWebAPI::testConnection() {
+ return refreshToken();
+ }
+
+ bool SpotifyWebAPI::expired() {
+ return QDateTime::currentDateTime() > expirationTime_;
+ }
+
+ QVector