mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-21 23:43:58 +01:00
Search song from Spotify via web api
This commit is contained in:
parent
98dd3e48a6
commit
6982b47819
@ -1,4 +1,4 @@
|
||||
include_directories(${SPOTIFY_INCLUDE_DIRS})
|
||||
include_directories(${LIBSPOTIFY_INCLUDE_DIRS})
|
||||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
@ -8,7 +8,7 @@ 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(${SPOTIFY_LIBRARY_DIRS})
|
||||
link_directories(${LIBSPOTIFY_LIBRARY_DIRS})
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
|
@ -888,12 +888,16 @@ optional_source(HAVE_SPOTIFY
|
||||
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
|
||||
)
|
||||
|
97
src/globalsearch/spotifywebapisearchprovider.cpp
Normal file
97
src/globalsearch/spotifywebapisearchprovider.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
/* 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{};
|
||||
}
|
48
src/globalsearch/spotifywebapisearchprovider.h
Normal file
48
src/globalsearch/spotifywebapisearchprovider.h
Normal file
@ -0,0 +1,48 @@
|
||||
/* 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
|
@ -62,6 +62,7 @@
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
#include "internet/spotify/spotifyservice.h"
|
||||
#include "internet/spotifywebapi/spotifywebapiservice.h"
|
||||
#endif
|
||||
|
||||
using smart_playlists::Generator;
|
||||
@ -100,6 +101,7 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
||||
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
|
||||
|
198
src/internet/spotifywebapi/spotifywebapiservice.cpp
Normal file
198
src/internet/spotifywebapi/spotifywebapiservice.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
/* 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;
|
||||
}
|
60
src/internet/spotifywebapi/spotifywebapiservice.h
Normal file
60
src/internet/spotifywebapi/spotifywebapiservice.h
Normal file
@ -0,0 +1,60 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2021, Kenman Tsang <kentsangkm@pm.me>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SPOTIFYWEBAPISERVICE_H
|
||||
#define SPOTIFYWEBAPISERVICE_H
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "internet/core/internetservice.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
class SpotifyWebApiService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpotifyWebApiService(Application* app, InternetModel* parent);
|
||||
~SpotifyWebApiService();
|
||||
|
||||
QStandardItem* CreateRootItem() override;
|
||||
void LazyPopulate(QStandardItem* parent) override;
|
||||
|
||||
public:
|
||||
void Search(int searchId, QString queryStr);
|
||||
|
||||
private:
|
||||
void OnTokenReady(const QJsonDocument&, int searchId, QString queryStr);
|
||||
void OnReadyToSearch(int searchId, QString queryStr);
|
||||
void BuildResultList(const QJsonDocument&, int searchId);
|
||||
|
||||
signals:
|
||||
void SearchFinished(int searchId, const QList<Song>&);
|
||||
|
||||
private:
|
||||
QJsonDocument ParseJsonReplyWithGzip(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QStandardItem* root_;
|
||||
NetworkAccessManager* network_;
|
||||
|
||||
QString token_;
|
||||
qint64 token_expiration_ms_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYWEBAPISERVICE_H
|
Loading…
Reference in New Issue
Block a user