Add internet tabs view and tidal favorites (#167)
This commit is contained in:
212
src/tidal/tidalbaserequest.cpp
Normal file
212
src/tidal/tidalbaserequest.cpp
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalbaserequest.h"
|
||||
|
||||
const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1";
|
||||
const char *TidalBaseRequest::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng==";
|
||||
|
||||
TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) :
|
||||
QObject(parent),
|
||||
service_(service),
|
||||
network_(network)
|
||||
{}
|
||||
|
||||
TidalBaseRequest::~TidalBaseRequest() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, 0, nullptr, 0);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const QList<Param> ¶ms_provided) {
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
typedef QList<EncodedParam> EncodedParamList;
|
||||
|
||||
ParamList params = ParamList() << params_provided
|
||||
<< Param("sessionId", session_id())
|
||||
<< Param("countryCode", country_code());
|
||||
|
||||
QStringList query_items;
|
||||
QUrlQuery url_query;
|
||||
for (const Param& param : params) {
|
||||
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
query_items << QString(encoded_param.first + "=" + encoded_param.second);
|
||||
url_query.addQueryItem(encoded_param.first, encoded_param.second);
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/") + ressource_name);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
|
||||
//qLog(Debug) << "Tidal: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error, const bool send_login) {
|
||||
|
||||
if (replies_.contains(reply)) {
|
||||
replies_.removeAll(reply);
|
||||
reply->deleteLater();
|
||||
}
|
||||
else {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "userMessage" - then use that instead.
|
||||
data = reply->readAll();
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
int status = 0;
|
||||
int sub_status = 0;
|
||||
QString failure_reason;
|
||||
if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
|
||||
status = json_obj["status"].toInt();
|
||||
sub_status = json_obj["subStatus"].toInt();
|
||||
QString user_message = json_obj["userMessage"].toString();
|
||||
failure_reason = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
|
||||
}
|
||||
}
|
||||
if (failure_reason.isEmpty()) {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
if (status == 401 && sub_status == 6001) { // User does not have a valid session
|
||||
emit service_->Logout();
|
||||
if (send_login && login_attempts() < max_login_attempts() && !token().isEmpty() && !username().isEmpty() && !password().isEmpty()) {
|
||||
qLog(Error) << "Tidal:" << failure_reason;
|
||||
qLog(Info) << "Tidal:" << "Attempting to login.";
|
||||
NeedLogin();
|
||||
emit service_->Login();
|
||||
}
|
||||
else {
|
||||
error = Error(failure_reason);
|
||||
}
|
||||
}
|
||||
else { // Fail
|
||||
error = Error(failure_reason);
|
||||
}
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
error = Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
error = Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
error = Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
error = Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonValue TidalBaseRequest::ExtractItems(QByteArray &data, QString &error) {
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data, error);
|
||||
if (json_obj.isEmpty()) return QJsonValue();
|
||||
return ExtractItems(json_obj, error);
|
||||
|
||||
}
|
||||
|
||||
QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) {
|
||||
|
||||
if (!json_obj.contains("items")) {
|
||||
error = Error("Json reply is missing items.", json_obj);
|
||||
return QJsonArray();
|
||||
}
|
||||
QJsonValue json_items = json_obj["items"];
|
||||
return json_items;
|
||||
|
||||
}
|
||||
|
||||
QString TidalBaseRequest::Error(QString error, QVariant debug) {
|
||||
|
||||
qLog(Error) << "Tidal:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
return error;
|
||||
|
||||
}
|
||||
111
src/tidal/tidalbaserequest.h
Normal file
111
src/tidal/tidalbaserequest.h
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALBASEREQUEST_H
|
||||
#define TIDALBASEREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "internet/internetservices.h"
|
||||
#include "internet/internetservice.h"
|
||||
#include "internet/internetsearch.h"
|
||||
#include "tidalservice.h"
|
||||
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class TidalUrlHandler;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
|
||||
class TidalBaseRequest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
enum QueryType {
|
||||
QueryType_None,
|
||||
QueryType_Artists,
|
||||
QueryType_Albums,
|
||||
QueryType_Songs,
|
||||
QueryType_SearchArtists,
|
||||
QueryType_SearchAlbums,
|
||||
QueryType_SearchSongs,
|
||||
QueryType_StreamURL,
|
||||
};
|
||||
|
||||
TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent);
|
||||
~TidalBaseRequest();
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> ¶ms_provided);
|
||||
QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool send_login);
|
||||
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QJsonObject &json_obj, QString &error);
|
||||
|
||||
QString Error(QString error, QVariant debug = QVariant());
|
||||
|
||||
QString token() { return service_->token(); }
|
||||
QString username() { return service_->username(); }
|
||||
QString password() { return service_->password(); }
|
||||
QString quality() { return service_->quality(); }
|
||||
int artistssearchlimit() { return service_->artistssearchlimit(); }
|
||||
int albumssearchlimit() { return service_->albumssearchlimit(); }
|
||||
int songssearchlimit() { return service_->songssearchlimit(); }
|
||||
bool fetchalbums() { return service_->fetchalbums(); }
|
||||
QString coversize() { return service_->coversize(); }
|
||||
|
||||
QString session_id() { return service_->session_id(); }
|
||||
quint64 user_id() { return service_->user_id(); }
|
||||
QString country_code() { return service_->country_code(); }
|
||||
|
||||
bool authenticated() { return service_->authenticated(); }
|
||||
bool need_login() { return need_login(); }
|
||||
bool login_sent() { return service_->login_sent(); }
|
||||
int max_login_attempts() { return service_->max_login_attempts(); }
|
||||
int login_attempts() { return service_->login_attempts(); }
|
||||
|
||||
virtual void NeedLogin() = 0;
|
||||
|
||||
private:
|
||||
|
||||
static const char *kApiUrl;
|
||||
static const char *kApiTokenB64;
|
||||
|
||||
TidalService *service_;
|
||||
NetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALBASEREQUEST_H
|
||||
821
src/tidal/tidalrequest.cpp
Normal file
821
src/tidal/tidalrequest.cpp
Normal file
@@ -0,0 +1,821 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalurlhandler.h"
|
||||
#include "tidalrequest.h"
|
||||
|
||||
const char *TidalRequest::kResourcesUrl = "http://resources.tidal.com";
|
||||
|
||||
TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent)
|
||||
: TidalBaseRequest(service, network, parent),
|
||||
service_(service),
|
||||
url_handler_(url_handler),
|
||||
network_(network),
|
||||
type_(type),
|
||||
artist_query_(false),
|
||||
artist_albums_requested_(0),
|
||||
artist_albums_received_(0),
|
||||
album_songs_requested_(0),
|
||||
album_songs_received_(0),
|
||||
album_covers_requested_(0),
|
||||
album_covers_received_(0),
|
||||
need_login_(false),
|
||||
no_match_(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
TidalRequest::~TidalRequest() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, 0, nullptr, 0);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::LoginComplete(bool success, QString error) {
|
||||
|
||||
if (!need_login_) return;
|
||||
need_login_ = false;
|
||||
|
||||
if (!success) {
|
||||
Error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
Process();
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::Process() {
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
service_->TryLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type_) {
|
||||
case QueryType::QueryType_Artists:
|
||||
GetArtists();
|
||||
break;
|
||||
case QueryType::QueryType_Albums:
|
||||
GetAlbums();
|
||||
break;
|
||||
case QueryType::QueryType_Songs:
|
||||
GetSongs();
|
||||
break;
|
||||
case QueryType::QueryType_SearchArtists:
|
||||
SendArtistsSearch();
|
||||
break;
|
||||
case QueryType::QueryType_SearchAlbums:
|
||||
SendAlbumsSearch();
|
||||
break;
|
||||
case QueryType::QueryType_SearchSongs:
|
||||
SendSongsSearch();
|
||||
break;
|
||||
default:
|
||||
Error("Invalid query type.");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::Search(const int search_id, const QString &search_text) {
|
||||
search_id_ = search_id;
|
||||
search_text_ = search_text;
|
||||
}
|
||||
|
||||
void TidalRequest::GetArtists() {
|
||||
|
||||
emit UpdateStatus(tr("Retrieving artists..."));
|
||||
|
||||
artist_query_ = true;
|
||||
|
||||
ParamList parameters;
|
||||
QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/artists").arg(service_->user_id()), parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReceived(QNetworkReply*)), reply);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetAlbums() {
|
||||
|
||||
emit UpdateStatus(tr("Retrieving albums..."));
|
||||
|
||||
type_ = QueryType_Albums;
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ParamList parameters;
|
||||
QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/albums").arg(service_->user_id()), parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetSongs() {
|
||||
|
||||
emit UpdateStatus(tr("Retrieving songs..."));
|
||||
|
||||
type_ = QueryType_Songs;
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ParamList parameters;
|
||||
QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/tracks").arg(service_->user_id()), parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int)), reply, 0);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::SendArtistsSearch() {
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
artist_query_ = true;
|
||||
|
||||
ParamList parameters;
|
||||
parameters << Param("query", search_text_);
|
||||
parameters << Param("limit", QString::number(service_->artistssearchlimit()));
|
||||
QNetworkReply *reply = CreateRequest("search/artists", parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReceived(QNetworkReply*)), reply);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::SendAlbumsSearch() {
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ParamList parameters;
|
||||
parameters << Param("query", search_text_);
|
||||
parameters << Param("limit", QString::number(service_->albumssearchlimit()));
|
||||
QNetworkReply *reply = CreateRequest("search/albums", parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::SendSongsSearch() {
|
||||
|
||||
if (!service_->authenticated()) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ParamList parameters;
|
||||
parameters << Param("query", search_text_);
|
||||
parameters << Param("limit", QString::number(service_->songssearchlimit()));
|
||||
QNetworkReply *reply = CreateRequest("search/tracks", parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::ArtistsReceived(QNetworkReply *reply) {
|
||||
|
||||
QString error;
|
||||
QByteArray data = GetReplyData(reply, error, true);
|
||||
if (data.isEmpty()) {
|
||||
artist_query_ = false;
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue json_value = ExtractItems(data, error);
|
||||
if (!json_value.isArray()) {
|
||||
artist_query_ = false;
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
QJsonArray json_items = json_value.toArray();
|
||||
if (json_items.isEmpty()) { // Empty array means no match
|
||||
artist_query_ = false;
|
||||
no_match_ = true;
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &value : json_items) {
|
||||
if (!value.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
|
||||
if (json_obj.contains("item")) {
|
||||
QJsonValue json_item = json_obj["item"];
|
||||
if (!json_item.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
|
||||
qLog(Debug) << json_item;
|
||||
continue;
|
||||
}
|
||||
json_obj = json_item.toObject();
|
||||
}
|
||||
|
||||
if (!json_obj.contains("id") || !json_obj.contains("name")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item missing id or album.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
|
||||
int artist_id = json_obj["id"].toInt();
|
||||
if (requests_artist_albums_.contains(artist_id)) continue;
|
||||
requests_artist_albums_.append(artist_id);
|
||||
GetArtistAlbums(artist_id);
|
||||
artist_albums_requested_++;
|
||||
if (artist_albums_requested_ >= service_->artistssearchlimit()) break;
|
||||
|
||||
}
|
||||
|
||||
if (artist_albums_requested_ > 0) {
|
||||
if (artist_albums_requested_ == 1) emit UpdateStatus(tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_));
|
||||
else emit UpdateStatus(tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_));
|
||||
emit ProgressSetMaximum(artist_albums_requested_);
|
||||
emit UpdateProgress(0);
|
||||
}
|
||||
else {
|
||||
artist_query_ = false;
|
||||
}
|
||||
|
||||
CheckFinish();
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetArtistAlbums(const int artist_id, const int offset) {
|
||||
|
||||
ParamList parameters;
|
||||
if (offset > 0) parameters << Param("offset", QString::number(offset));
|
||||
QNetworkReply *reply = CreateRequest(QString("artists/%1/albums").arg(artist_id), parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, artist_id, offset);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::AlbumsReceived(QNetworkReply *reply, const int artist_id, const int offset_requested) {
|
||||
|
||||
QString error;
|
||||
QByteArray data = GetReplyData(reply, error, (artist_id == 0));
|
||||
|
||||
if (artist_query_) {
|
||||
if (!requests_artist_albums_.contains(artist_id)) return;
|
||||
artist_albums_received_++;
|
||||
emit UpdateProgress(artist_albums_received_);
|
||||
}
|
||||
|
||||
if (data.isEmpty()) {
|
||||
AlbumsFinished(artist_id, offset_requested);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data, error);
|
||||
if (json_obj.isEmpty()) {
|
||||
AlbumsFinished(artist_id, offset_requested);
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = 0;
|
||||
int total_albums = 0;
|
||||
if (artist_query_) { // This was a list of albums by artist
|
||||
if (!json_obj.contains("limit") ||
|
||||
!json_obj.contains("offset") ||
|
||||
!json_obj.contains("totalNumberOfItems") ||
|
||||
!json_obj.contains("items")) {
|
||||
AlbumsFinished(artist_id, offset_requested);
|
||||
Error("Json object missing values.", json_obj);
|
||||
return;
|
||||
}
|
||||
limit = json_obj["limit"].toInt();
|
||||
int offset = json_obj["offset"].toInt();
|
||||
total_albums = json_obj["totalNumberOfItems"].toInt();
|
||||
if (offset != offset_requested) {
|
||||
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
|
||||
Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonValue json_value = ExtractItems(json_obj, error);
|
||||
if (!json_value.isArray()) {
|
||||
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
|
||||
return;
|
||||
}
|
||||
QJsonArray json_items = json_value.toArray();
|
||||
if (json_items.isEmpty()) {
|
||||
if (!artist_query_) no_match_ = true;
|
||||
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
|
||||
return;
|
||||
}
|
||||
|
||||
int albums = 0;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
++albums;
|
||||
if (!value.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
|
||||
if (json_obj.contains("item")) {
|
||||
QJsonValue json_item = json_obj["item"];
|
||||
if (!json_item.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
|
||||
qLog(Debug) << json_item;
|
||||
continue;
|
||||
}
|
||||
json_obj = json_item.toObject();
|
||||
}
|
||||
|
||||
int album_id = 0;
|
||||
QString album;
|
||||
if (json_obj.contains("type")) { // This was a albums request or search
|
||||
if (!json_obj.contains("id") || !json_obj.contains("title")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
album_id = json_obj["id"].toInt();
|
||||
album = json_obj["title"].toString();
|
||||
}
|
||||
else if (json_obj.contains("album")) { // This was a tracks request or search
|
||||
if (!service_->fetchalbums()) {
|
||||
Song song;
|
||||
ParseSong(song, 0, value);
|
||||
songs_ << song;
|
||||
continue;
|
||||
}
|
||||
QJsonValue json_value_album = json_obj["album"];
|
||||
if (!json_value_album.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item album is not a object.";
|
||||
qLog(Debug) << json_value_album;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_album = json_value_album.toObject();
|
||||
if (!json_album.contains("id") || !json_album.contains("title")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item album is missing ID or title.";
|
||||
qLog(Debug) << json_album;
|
||||
continue;
|
||||
}
|
||||
album_id = json_album["id"].toInt();
|
||||
album = json_album["title"].toString();
|
||||
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item missing type or album.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (requests_album_songs_.contains(album_id)) continue;
|
||||
|
||||
if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item missing artist, title or audioQuality.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
QJsonValue json_value_artist = json_obj["artist"];
|
||||
if (!json_value_artist.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item artist is not a object.";
|
||||
qLog(Debug) << json_value_artist;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_artist = json_value_artist.toObject();
|
||||
if (!json_artist.contains("name")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item artist missing name.";
|
||||
qLog(Debug) << json_artist;
|
||||
continue;
|
||||
}
|
||||
QString artist = json_artist["name"].toString();
|
||||
|
||||
QString quality = json_obj["audioQuality"].toString();
|
||||
QString copyright = json_obj["copyright"].toString();
|
||||
|
||||
//qLog(Debug) << "Tidal:" << artist << album << quality << copyright;
|
||||
|
||||
requests_album_songs_.insert(album_id, artist);
|
||||
album_songs_requested_++;
|
||||
if (album_songs_requested_ >= service_->albumssearchlimit()) break;
|
||||
}
|
||||
|
||||
AlbumsFinished(artist_id, offset_requested, total_albums, limit, albums);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums, const int limit, const int albums) {
|
||||
|
||||
if (artist_query_) { // This is a artist search.
|
||||
if (albums > limit) {
|
||||
Error("Albums returned does not match limit returned!");
|
||||
}
|
||||
int offset_next = offset_requested + albums;
|
||||
if (album_songs_requested_ < service_->albumssearchlimit() && offset_next < total_albums) {
|
||||
GetArtistAlbums(artist_id, offset_next);
|
||||
artist_albums_requested_++;
|
||||
}
|
||||
else if (artist_albums_received_ >= artist_albums_requested_) { // Artist search is finished.
|
||||
artist_query_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!artist_query_) {
|
||||
// Get songs for the albums.
|
||||
QHashIterator<int, QString> i(requests_album_songs_);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
GetAlbumSongs(i.key());
|
||||
}
|
||||
|
||||
if (album_songs_requested_ > 0) {
|
||||
if (album_songs_requested_ == 1) emit UpdateStatus(tr("Retrieving songs for %1 album...").arg(album_songs_requested_));
|
||||
else emit UpdateStatus(tr("Retrieving songs for %1 albums...").arg(album_songs_requested_));
|
||||
emit ProgressSetMaximum(album_songs_requested_);
|
||||
emit UpdateProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
CheckFinish();
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetAlbumSongs(const int album_id) {
|
||||
|
||||
ParamList parameters;
|
||||
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int)), reply, album_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::SongsReceived(QNetworkReply *reply, const int album_id) {
|
||||
|
||||
QString error;
|
||||
QByteArray data = GetReplyData(reply, error, false);
|
||||
|
||||
QString album_artist;
|
||||
if (album_id != 0) {
|
||||
if (!requests_album_songs_.contains(album_id)) return;
|
||||
album_artist = requests_album_songs_[album_id];
|
||||
}
|
||||
|
||||
album_songs_received_++;
|
||||
if (!artist_query_) {
|
||||
emit UpdateProgress(album_songs_received_);
|
||||
}
|
||||
|
||||
if (data.isEmpty()) {
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue json_value = ExtractItems(data, error);
|
||||
if (!json_value.isArray()) {
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray json_items = json_value.toArray();
|
||||
if (json_items.isEmpty()) {
|
||||
no_match_ = true;
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
bool compilation = false;
|
||||
bool multidisc = false;
|
||||
SongList songs;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
Song song;
|
||||
ParseSong(song, album_id, value, album_artist);
|
||||
if (!song.is_valid()) continue;
|
||||
if (song.disc() >= 2) multidisc = true;
|
||||
if (song.is_compilation()) compilation = true;
|
||||
songs << song;
|
||||
}
|
||||
|
||||
for (Song &song : songs) {
|
||||
if (compilation) song.set_compilation_detected(true);
|
||||
if (multidisc) {
|
||||
QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc()));
|
||||
song.set_album(album_full);
|
||||
}
|
||||
songs_ << song;
|
||||
|
||||
}
|
||||
|
||||
if (service_->cache_album_covers() && artist_albums_requested_ <= artist_albums_received_ && album_songs_requested_ <= album_songs_received_) {
|
||||
GetAlbumCovers();
|
||||
}
|
||||
|
||||
CheckFinish();
|
||||
|
||||
}
|
||||
|
||||
int TidalRequest::ParseSong(Song &song, const int album_id_requested, const QJsonValue &value, QString album_artist) {
|
||||
|
||||
if (!value.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track is not a object.";
|
||||
qLog(Debug) << value;
|
||||
return -1;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
|
||||
if (
|
||||
!json_obj.contains("album") ||
|
||||
!json_obj.contains("allowStreaming") ||
|
||||
!json_obj.contains("artist") ||
|
||||
!json_obj.contains("artists") ||
|
||||
!json_obj.contains("audioQuality") ||
|
||||
!json_obj.contains("duration") ||
|
||||
!json_obj.contains("id") ||
|
||||
!json_obj.contains("streamReady") ||
|
||||
!json_obj.contains("title") ||
|
||||
!json_obj.contains("trackNumber") ||
|
||||
!json_obj.contains("url") ||
|
||||
!json_obj.contains("volumeNumber") ||
|
||||
!json_obj.contains("copyright")
|
||||
) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values.";
|
||||
qLog(Debug) << json_obj;
|
||||
return -1;
|
||||
}
|
||||
|
||||
QJsonValue json_value_artist = json_obj["artist"];
|
||||
QJsonValue json_value_album = json_obj["album"];
|
||||
QJsonValue json_duration = json_obj["duration"];
|
||||
QJsonArray json_artists = json_obj["artists"].toArray();
|
||||
|
||||
int song_id = json_obj["id"].toInt();
|
||||
|
||||
QString title = json_obj["title"].toString();
|
||||
QString urlstr = json_obj["url"].toString();
|
||||
int track = json_obj["trackNumber"].toInt();
|
||||
int disc = json_obj["volumeNumber"].toInt();
|
||||
bool allow_streaming = json_obj["allowStreaming"].toBool();
|
||||
bool stream_ready = json_obj["streamReady"].toBool();
|
||||
QString copyright = json_obj["copyright"].toString();
|
||||
|
||||
if (!json_value_artist.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object.";
|
||||
qLog(Debug) << json_value_artist;
|
||||
return -1;
|
||||
}
|
||||
QJsonObject json_artist = json_value_artist.toObject();
|
||||
if (!json_artist.contains("name")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track artist is missing name.";
|
||||
qLog(Debug) << json_artist;
|
||||
return -1;
|
||||
}
|
||||
QString artist = json_artist["name"].toString();
|
||||
|
||||
if (!json_value_album.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track album is not a object.";
|
||||
qLog(Debug) << json_value_album;
|
||||
return -1;
|
||||
}
|
||||
QJsonObject json_album = json_value_album.toObject();
|
||||
if (!json_album.contains("id") || !json_album.contains("title") || !json_album.contains("cover")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track album is missing id, title or cover.";
|
||||
qLog(Debug) << json_album;
|
||||
return -1;
|
||||
}
|
||||
int album_id = json_album["id"].toInt();
|
||||
if (album_id_requested != 0 && album_id_requested != album_id) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track album id is wrong.";
|
||||
qLog(Debug) << json_album;
|
||||
return -1;
|
||||
}
|
||||
QString album = json_album["title"].toString();
|
||||
QString cover = json_album["cover"].toString();
|
||||
|
||||
if (!allow_streaming) {
|
||||
qLog(Error) << "Tidal: Song" << artist << album << title << "is not allowStreaming";
|
||||
}
|
||||
|
||||
if (!stream_ready) {
|
||||
qLog(Error) << "Tidal: Song" << artist << album << title << "is not streamReady.";
|
||||
}
|
||||
|
||||
QUrl url;
|
||||
url.setScheme(url_handler_->scheme());
|
||||
url.setPath(QString::number(song_id));
|
||||
|
||||
QVariant q_duration = json_duration.toVariant();
|
||||
quint64 duration = 0;
|
||||
if (q_duration.isValid() && (q_duration.type() == QVariant::Int || q_duration.type() == QVariant::Double)) {
|
||||
duration = q_duration.toInt() * kNsecPerSec;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Tidal: Invalid duration for song.";
|
||||
qLog(Debug) << json_duration;
|
||||
return -1;
|
||||
}
|
||||
|
||||
cover = cover.replace("-", "/");
|
||||
QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(service_->coversize()));
|
||||
|
||||
title.remove(Song::kTitleRemoveMisc);
|
||||
|
||||
//qLog(Debug) << "id" << song_id << "track" << track << "disc" << disc << "title" << title << "album" << album << "album artist" << album_artist << "artist" << artist << cover << allow_streaming << url;
|
||||
|
||||
song.set_source(Song::Source_Tidal);
|
||||
song.set_album_id(album_id);
|
||||
if (album_artist != artist) song.set_albumartist(album_artist);
|
||||
song.set_album(album);
|
||||
song.set_artist(artist);
|
||||
song.set_title(title);
|
||||
song.set_track(track);
|
||||
song.set_disc(disc);
|
||||
song.set_url(url);
|
||||
song.set_length_nanosec(duration);
|
||||
song.set_art_automatic(cover_url.toEncoded());
|
||||
song.set_comment(copyright);
|
||||
song.set_directory_id(0);
|
||||
song.set_filetype(Song::FileType_Stream);
|
||||
song.set_filesize(0);
|
||||
song.set_mtime(0);
|
||||
song.set_ctime(0);
|
||||
song.set_valid(true);
|
||||
|
||||
return song_id;
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetAlbumCovers() {
|
||||
|
||||
for (Song &song : songs_) {
|
||||
GetAlbumCover(song);
|
||||
}
|
||||
|
||||
if (album_covers_requested_ == 1) emit UpdateStatus(tr("Retrieving album cover for %1 album...").arg(album_covers_requested_));
|
||||
else emit UpdateStatus(tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_));
|
||||
emit ProgressSetMaximum(album_covers_requested_);
|
||||
emit UpdateProgress(0);
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::GetAlbumCover(Song &song) {
|
||||
|
||||
if (requests_album_covers_.contains(song.album_id())) {
|
||||
requests_album_covers_.insertMulti(song.album_id(), &song);
|
||||
return;
|
||||
}
|
||||
|
||||
album_covers_requested_++;
|
||||
requests_album_covers_.insertMulti(song.album_id(), &song);
|
||||
|
||||
QUrl url(song.art_automatic());
|
||||
QNetworkRequest req(url);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, int, QUrl)), reply, song.album_id(), url);
|
||||
replies_ << reply;
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, int album_id, QUrl url) {
|
||||
|
||||
if (replies_.contains(reply)) {
|
||||
replies_.removeAll(reply);
|
||||
reply->deleteLater();
|
||||
}
|
||||
else {
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!requests_album_covers_.contains(album_id)) {
|
||||
CheckFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
album_covers_received_++;
|
||||
emit UpdateProgress(album_covers_received_);
|
||||
|
||||
QString error;
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
requests_album_covers_.remove(album_id);
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
error = Error(QString("Received empty image data for %1").arg(url.toString()));
|
||||
requests_album_covers_.remove(album_id);
|
||||
return;
|
||||
}
|
||||
|
||||
QImage image;
|
||||
if (image.loadFromData(data)) {
|
||||
|
||||
QDir dir;
|
||||
if (dir.mkpath(service_->CoverCacheDir())) {
|
||||
QString filename(service_->CoverCacheDir() + "/" + QString::number(album_id) + "-" + url.fileName());
|
||||
if (image.save(filename, "JPG")) {
|
||||
while (requests_album_covers_.contains(album_id)) {
|
||||
Song *song = requests_album_covers_.take(album_id);
|
||||
song->set_art_automatic(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
error = Error(QString("Error decoding image data from %1").arg(url.toString()));
|
||||
}
|
||||
|
||||
CheckFinish();
|
||||
|
||||
}
|
||||
|
||||
void TidalRequest::CheckFinish() {
|
||||
|
||||
if (!need_login_ &&
|
||||
!artist_query_ &&
|
||||
artist_albums_requested_ <= artist_albums_received_ &&
|
||||
album_songs_requested_ <= album_songs_received_ &&
|
||||
album_covers_requested_ <= album_covers_received_
|
||||
) {
|
||||
if (songs_.isEmpty()) {
|
||||
if (IsSearch()) {
|
||||
if (no_match_) emit ErrorSignal(search_id_, tr("No match"));
|
||||
else if (errors_.isEmpty()) emit ErrorSignal(search_id_, tr("Unknown error"));
|
||||
else emit ErrorSignal(search_id_, errors_);
|
||||
}
|
||||
else {
|
||||
if (no_match_) emit Results(songs_);
|
||||
else if (errors_.isEmpty()) emit ErrorSignal(tr("Unknown error"));
|
||||
else emit ErrorSignal(errors_);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (IsSearch()) {
|
||||
emit SearchResults(search_id_, songs_);
|
||||
}
|
||||
else {
|
||||
emit Results(songs_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString TidalRequest::Error(QString error, QVariant debug) {
|
||||
|
||||
qLog(Error) << "Tidal:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
if (!error.isEmpty()) {
|
||||
errors_ += error;
|
||||
errors_ += "<br />";
|
||||
}
|
||||
CheckFinish();
|
||||
|
||||
return error;
|
||||
|
||||
}
|
||||
134
src/tidal/tidalrequest.h
Normal file
134
src/tidal/tidalrequest.h
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALREQUEST_H
|
||||
#define TIDALREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QMultiMap>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "tidalbaserequest.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
class TidalService;
|
||||
class TidalUrlHandler;
|
||||
|
||||
class TidalRequest : public TidalBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
TidalRequest(TidalService *service, TidalUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent);
|
||||
~TidalRequest();
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
void Process();
|
||||
void NeedLogin() { need_login_ = true; }
|
||||
void Search(const int search_id, const QString &search_text);
|
||||
void SendArtistsSearch();
|
||||
void SendAlbumsSearch();
|
||||
void SendSongsSearch();
|
||||
|
||||
signals:
|
||||
void Login();
|
||||
void Login(const QString &username, const QString &password, const QString &token);
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
void Results(SongList songs);
|
||||
void SearchResults(int id, SongList songs);
|
||||
void ErrorSignal(QString message);
|
||||
void ErrorSignal(int id, QString message);
|
||||
void UpdateStatus(QString text);
|
||||
void ProgressSetMaximum(int max);
|
||||
void UpdateProgress(int max);
|
||||
void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
|
||||
|
||||
public slots:
|
||||
void GetArtists();
|
||||
void GetAlbums();
|
||||
void GetSongs();
|
||||
|
||||
private slots:
|
||||
void LoginComplete(bool success, QString error = QString());
|
||||
void ArtistsReceived(QNetworkReply *reply);
|
||||
void AlbumsReceived(QNetworkReply *reply, const int artist_id, const int offset_requested = 0);
|
||||
void AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums = 0, const int limit = 0, const int albums = 0);
|
||||
void SongsReceived(QNetworkReply *reply, int album_id);
|
||||
void AlbumCoverReceived(QNetworkReply *reply, int album_id, QUrl url);
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
const bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); }
|
||||
void SendSearch();
|
||||
void GetArtistAlbums(const int artist_id, const int offset = 0);
|
||||
void GetAlbumSongs(const int album_id);
|
||||
void GetSongs(const int album_id);
|
||||
int ParseSong(Song &song, const int album_id_requested, const QJsonValue &value, QString album_artist = QString());
|
||||
void GetAlbumCovers();
|
||||
void GetAlbumCover(Song &song);
|
||||
void CheckFinish();
|
||||
QString LoginError(QString error, QVariant debug = QVariant());
|
||||
QString Error(QString error, QVariant debug = QVariant());
|
||||
|
||||
static const char *kResourcesUrl;
|
||||
|
||||
TidalService *service_;
|
||||
TidalUrlHandler *url_handler_;
|
||||
NetworkAccessManager *network_;
|
||||
|
||||
QueryType type_;
|
||||
bool artist_query_;
|
||||
|
||||
int search_id_;
|
||||
QString search_text_;
|
||||
QList<int> requests_artist_albums_;
|
||||
QHash<int, QString> requests_album_songs_;
|
||||
QMultiMap<int, Song*> requests_album_covers_;
|
||||
int artist_albums_requested_;
|
||||
int artist_albums_received_;
|
||||
int album_songs_requested_;
|
||||
int album_songs_received_;
|
||||
int album_covers_requested_;
|
||||
int album_covers_received_;
|
||||
SongList songs_;
|
||||
QString errors_;
|
||||
bool need_login_;
|
||||
bool no_match_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALREQUEST_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,27 +22,31 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "internet/internetservices.h"
|
||||
#include "internet/internetservice.h"
|
||||
#include "internet/internetsearch.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class TidalUrlHandler;
|
||||
class TidalRequest;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
class TidalService : public InternetService {
|
||||
Q_OBJECT
|
||||
@@ -54,81 +58,120 @@ class TidalService : public InternetService {
|
||||
static const Song::Source kSource;
|
||||
|
||||
void ReloadSettings();
|
||||
QString CoverCacheDir();
|
||||
|
||||
void Logout();
|
||||
int Search(const QString &query, InternetSearch::SearchType type);
|
||||
void CancelSearch();
|
||||
|
||||
const bool login_sent() { return login_sent_; }
|
||||
const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); }
|
||||
const int max_login_attempts() { return kLoginAttempts; }
|
||||
|
||||
QString token() { return token_; }
|
||||
QString username() { return username_; }
|
||||
QString password() { return password_; }
|
||||
QString quality() { return quality_; }
|
||||
int search_delay() { return search_delay_; }
|
||||
int artistssearchlimit() { return artistssearchlimit_; }
|
||||
int albumssearchlimit() { return albumssearchlimit_; }
|
||||
int songssearchlimit() { return songssearchlimit_; }
|
||||
bool fetchalbums() { return fetchalbums_; }
|
||||
QString coversize() { return coversize_; }
|
||||
bool cache_album_covers() { return cache_album_covers_; }
|
||||
|
||||
QString session_id() { return session_id_; }
|
||||
quint64 user_id() { return user_id_; }
|
||||
QString country_code() { return country_code_; }
|
||||
|
||||
const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); }
|
||||
const bool login_sent() { return login_sent_; }
|
||||
const bool login_attempts() { return login_attempts_; }
|
||||
|
||||
void GetStreamURL(const QUrl &url);
|
||||
|
||||
CollectionBackend *artists_collection_backend() { return artists_collection_backend_; }
|
||||
CollectionBackend *albums_collection_backend() { return albums_collection_backend_; }
|
||||
CollectionBackend *songs_collection_backend() { return songs_collection_backend_; }
|
||||
|
||||
CollectionModel *artists_collection_model() { return artists_collection_model_; }
|
||||
CollectionModel *albums_collection_model() { return albums_collection_model_; }
|
||||
CollectionModel *songs_collection_model() { return songs_collection_model_; }
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model() { return artists_collection_sort_model_; }
|
||||
QSortFilterProxyModel *albums_collection_sort_model() { return albums_collection_sort_model_; }
|
||||
QSortFilterProxyModel *songs_collection_sort_model() { return songs_collection_sort_model_; }
|
||||
|
||||
enum QueryType {
|
||||
QueryType_Artists,
|
||||
QueryType_Albums,
|
||||
QueryType_Songs,
|
||||
QueryType_SearchArtists,
|
||||
QueryType_SearchAlbums,
|
||||
QueryType_SearchSongs,
|
||||
};
|
||||
|
||||
signals:
|
||||
void Login();
|
||||
void Login(const QString &username, const QString &password, const QString &token);
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
void SearchResults(int id, SongList songs);
|
||||
void SearchError(int id, QString message);
|
||||
void UpdateStatus(QString text);
|
||||
void ProgressSetMaximum(int max);
|
||||
void UpdateProgress(int max);
|
||||
void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
|
||||
|
||||
public slots:
|
||||
void ShowConfig();
|
||||
void TryLogin();
|
||||
void SendLogin(const QString &username, const QString &password, const QString &token);
|
||||
void GetArtists();
|
||||
void GetAlbums();
|
||||
void GetSongs();
|
||||
|
||||
private slots:
|
||||
void SendLogin();
|
||||
void HandleAuthReply(QNetworkReply *reply);
|
||||
void ResetLoginAttempts();
|
||||
void StartSearch();
|
||||
void ArtistsReceived(QNetworkReply *reply, int search_id);
|
||||
void AlbumsReceived(QNetworkReply *reply, int search_id, int artist_id, int offset_requested = 0);
|
||||
void AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums = 0, const int limit = 0, const int albums = 0);
|
||||
void SongsReceived(QNetworkReply *reply, int search_id, int album_id);
|
||||
void StreamURLReceived(QNetworkReply *reply, const int song_id, const QUrl original_url);
|
||||
void UpdateArtists(SongList songs);
|
||||
void UpdateAlbums(SongList songs);
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
void ClearSearch();
|
||||
void LoadSessionID();
|
||||
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> ¶ms);
|
||||
QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool sendlogin = false);
|
||||
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QJsonObject &json_obj, QString &error);
|
||||
void SendSearch();
|
||||
void SendArtistsSearch();
|
||||
void SendAlbumsSearch();
|
||||
void SendSongsSearch();
|
||||
void GetAlbums(const int artist_id, const int offset = 0);
|
||||
void GetSongs(const int album_id);
|
||||
Song ParseSong(const int album_id_requested, const QJsonValue &value, QString album_artist = QString());
|
||||
void CheckFinish();
|
||||
QString LoginError(QString error, QVariant debug = QVariant());
|
||||
QString Error(QString error, QVariant debug = QVariant());
|
||||
|
||||
static const char *kApiUrl;
|
||||
static const char *kAuthUrl;
|
||||
static const char *kResourcesUrl;
|
||||
static const char *kApiTokenB64;
|
||||
static const int kLoginAttempts;
|
||||
static const int kTimeResetLoginAttempts;
|
||||
|
||||
static const char *kArtistsSongsTable;
|
||||
static const char *kAlbumsSongsTable;
|
||||
static const char *kSongsTable;
|
||||
|
||||
static const char *kArtistsSongsFtsTable;
|
||||
static const char *kAlbumsSongsFtsTable;
|
||||
static const char *kSongsFtsTable;
|
||||
|
||||
Application *app_;
|
||||
NetworkAccessManager *network_;
|
||||
TidalUrlHandler *url_handler_;
|
||||
|
||||
CollectionBackend *artists_collection_backend_;
|
||||
CollectionBackend *albums_collection_backend_;
|
||||
CollectionBackend *songs_collection_backend_;
|
||||
|
||||
CollectionModel *artists_collection_model_;
|
||||
CollectionModel *albums_collection_model_;
|
||||
CollectionModel *songs_collection_model_;
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model_;
|
||||
QSortFilterProxyModel *albums_collection_sort_model_;
|
||||
QSortFilterProxyModel *songs_collection_sort_model_;
|
||||
|
||||
QTimer *timer_search_delay_;
|
||||
QTimer *timer_login_attempt_;
|
||||
|
||||
std::shared_ptr<TidalRequest> artists_request_;
|
||||
std::shared_ptr<TidalRequest> albums_request_;
|
||||
std::shared_ptr<TidalRequest> songs_request_;
|
||||
std::shared_ptr<TidalRequest> search_request_;
|
||||
|
||||
QString token_;
|
||||
QString username_;
|
||||
QString password_;
|
||||
@@ -139,6 +182,8 @@ class TidalService : public InternetService {
|
||||
int songssearchlimit_;
|
||||
bool fetchalbums_;
|
||||
QString coversize_;
|
||||
bool cache_album_covers_;
|
||||
|
||||
QString session_id_;
|
||||
quint64 user_id_;
|
||||
QString country_code_;
|
||||
@@ -150,20 +195,8 @@ class TidalService : public InternetService {
|
||||
|
||||
int search_id_;
|
||||
QString search_text_;
|
||||
bool artist_search_;
|
||||
QList<int> requests_artist_albums_;
|
||||
QHash<int, QString> requests_album_songs_;
|
||||
QHash<int, QUrl> requests_stream_url_;
|
||||
QList<QUrl> queue_stream_url_;
|
||||
int artist_albums_requested_;
|
||||
int artist_albums_received_;
|
||||
int album_songs_requested_;
|
||||
int album_songs_received_;
|
||||
SongList songs_;
|
||||
QString search_error_;
|
||||
bool login_sent_;
|
||||
int login_attempts_;
|
||||
QUrl stream_request_url_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
145
src/tidal/tidalstreamurlrequest.cpp
Normal file
145
src/tidal/tidalstreamurlrequest.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalbaserequest.h"
|
||||
#include "tidalstreamurlrequest.h"
|
||||
|
||||
TidalStreamURLRequest::TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent)
|
||||
: TidalBaseRequest(service, network, parent),
|
||||
reply_(nullptr),
|
||||
original_url_(original_url),
|
||||
song_id_(original_url.path().toInt()),
|
||||
tries_(0),
|
||||
need_login_(false) {}
|
||||
|
||||
TidalStreamURLRequest::~TidalStreamURLRequest() {
|
||||
Cancel();
|
||||
}
|
||||
|
||||
void TidalStreamURLRequest::LoginComplete(bool success, QString error) {
|
||||
|
||||
if (!need_login_) return;
|
||||
need_login_ = false;
|
||||
|
||||
if (!success) {
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
|
||||
return;
|
||||
}
|
||||
|
||||
Process();
|
||||
|
||||
}
|
||||
|
||||
void TidalStreamURLRequest::Process() {
|
||||
|
||||
if (!authenticated()) {
|
||||
need_login_ = true;
|
||||
emit TryLogin();
|
||||
return;
|
||||
}
|
||||
GetStreamURL();
|
||||
|
||||
}
|
||||
|
||||
void TidalStreamURLRequest::Cancel() {
|
||||
|
||||
if (reply_) {
|
||||
if (reply_->isRunning()) {
|
||||
reply_->abort();
|
||||
}
|
||||
reply_->deleteLater();
|
||||
reply_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalStreamURLRequest::GetStreamURL() {
|
||||
|
||||
++tries_;
|
||||
|
||||
ParamList parameters;
|
||||
parameters << Param("soundQuality", quality());
|
||||
|
||||
if (reply_) {
|
||||
Cancel();
|
||||
}
|
||||
reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), parameters);
|
||||
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
|
||||
|
||||
}
|
||||
|
||||
void TidalStreamURLRequest::StreamURLReceived() {
|
||||
|
||||
if (!reply_) return;
|
||||
disconnect(reply_, 0, nullptr, 0);
|
||||
reply_->deleteLater();
|
||||
|
||||
QString error;
|
||||
|
||||
QByteArray data = GetReplyData(reply_, error, true);
|
||||
if (data.isEmpty()) {
|
||||
reply_ = nullptr;
|
||||
if (!authenticated() && login_sent() && tries_ <= 1) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data, error);
|
||||
if (json_obj.isEmpty()) {
|
||||
reply_ = nullptr;
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("url") || !json_obj.contains("codec")) {
|
||||
reply_ = nullptr;
|
||||
error = Error("Invalid Json reply, stream missing url or codec.", json_obj);
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl new_url(json_obj["url"].toString());
|
||||
QString codec(json_obj["codec"].toString().toLower());
|
||||
Song::FileType filetype(Song::FiletypeByExtension(codec));
|
||||
if (filetype == Song::FileType_Unknown) {
|
||||
qLog(Debug) << "Tidal: Unknown codec" << codec;
|
||||
filetype = Song::FileType_Stream;
|
||||
}
|
||||
|
||||
emit StreamURLFinished(original_url_, new_url, filetype, QString());
|
||||
|
||||
reply_ = nullptr;
|
||||
deleteLater();
|
||||
|
||||
}
|
||||
68
src/tidal/tidalstreamurlrequest.h
Normal file
68
src/tidal/tidalstreamurlrequest.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSTREAMURLREQUEST_H
|
||||
#define TIDALSTREAMURLREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "tidalbaserequest.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class TidalService;
|
||||
|
||||
class TidalStreamURLRequest : public TidalBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent);
|
||||
~TidalStreamURLRequest();
|
||||
|
||||
void GetStreamURL();
|
||||
void Process();
|
||||
void NeedLogin() { need_login_ = true; }
|
||||
void Cancel();
|
||||
|
||||
QUrl original_url() { return original_url_; }
|
||||
int song_id() { return song_id_; }
|
||||
bool need_login() { return need_login_; }
|
||||
|
||||
signals:
|
||||
void TryLogin();
|
||||
void StreamURLFinished(const QUrl original_url, const QUrl stream_url, const Song::FileType, QString error = QString());
|
||||
|
||||
private slots:
|
||||
void LoginComplete(bool success, QString error = QString());
|
||||
void StreamURLReceived();
|
||||
|
||||
private:
|
||||
QNetworkReply *reply_;
|
||||
QUrl original_url_;
|
||||
int song_id_;
|
||||
int tries_;
|
||||
bool need_login_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALSTREAMURLREQUEST_H
|
||||
Reference in New Issue
Block a user