1 /*
2 * Strawberry Music Player
3 * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
4 *
5 * Strawberry is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * Strawberry is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 #include "config.h"
21
22 #include <memory>
23 #include <chrono>
24
25 #include <QObject>
26 #include <QDesktopServices>
27 #include <QCryptographicHash>
28 #include <QByteArray>
29 #include <QPair>
30 #include <QList>
31 #include <QMap>
32 #include <QString>
33 #include <QChar>
34 #include <QUrl>
35 #include <QUrlQuery>
36 #include <QNetworkRequest>
37 #include <QNetworkReply>
38 #include <QSslError>
39 #include <QTimer>
40 #include <QJsonValue>
41 #include <QJsonDocument>
42 #include <QJsonObject>
43 #include <QSettings>
44 #include <QSortFilterProxyModel>
45 #include <QtDebug>
46
47 #include "core/application.h"
48 #include "core/player.h"
49 #include "core/logging.h"
50 #include "core/networkaccessmanager.h"
51 #include "core/database.h"
52 #include "core/song.h"
53 #include "core/utilities.h"
54 #include "core/timeconstants.h"
55 #include "internet/internetsearchview.h"
56 #include "collection/collectionbackend.h"
57 #include "collection/collectionmodel.h"
58 #include "tidalservice.h"
59 #include "tidalurlhandler.h"
60 #include "tidalbaserequest.h"
61 #include "tidalrequest.h"
62 #include "tidalfavoriterequest.h"
63 #include "tidalstreamurlrequest.h"
64 #include "settings/settingsdialog.h"
65 #include "settings/tidalsettingspage.h"
66
67 const Song::Source TidalService::kSource = Song::Source_Tidal;
68 const char *TidalService::kOAuthUrl = "https://login.tidal.com/authorize";
69 const char *TidalService::kOAuthAccessTokenUrl = "https://login.tidal.com/oauth2/token";
70 const char *TidalService::kOAuthRedirectUrl = "tidal://login/auth";
71 const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username";
72 const char *TidalService::kApiUrl = "https://api.tidalhifi.com/v1";
73 const char *TidalService::kResourcesUrl = "https://resources.tidal.com";
74 const int TidalService::kLoginAttempts = 2;
75 const int TidalService::kTimeResetLoginAttempts = 60000;
76
77 const char *TidalService::kArtistsSongsTable = "tidal_artists_songs";
78 const char *TidalService::kAlbumsSongsTable = "tidal_albums_songs";
79 const char *TidalService::kSongsTable = "tidal_songs";
80
81 const char *TidalService::kArtistsSongsFtsTable = "tidal_artists_songs_fts";
82 const char *TidalService::kAlbumsSongsFtsTable = "tidal_albums_songs_fts";
83 const char *TidalService::kSongsFtsTable = "tidal_songs_fts";
84
85 using namespace std::chrono_literals;
86
TidalService(Application * app,QObject * parent)87 TidalService::TidalService(Application *app, QObject *parent)
88 : InternetService(Song::Source_Tidal, "Tidal", "tidal", TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, app, parent),
89 app_(app),
90 network_(new NetworkAccessManager(this)),
91 url_handler_(new TidalUrlHandler(app, this)),
92 artists_collection_backend_(nullptr),
93 albums_collection_backend_(nullptr),
94 songs_collection_backend_(nullptr),
95 artists_collection_model_(nullptr),
96 albums_collection_model_(nullptr),
97 songs_collection_model_(nullptr),
98 artists_collection_sort_model_(new QSortFilterProxyModel(this)),
99 albums_collection_sort_model_(new QSortFilterProxyModel(this)),
100 songs_collection_sort_model_(new QSortFilterProxyModel(this)),
101 timer_search_delay_(new QTimer(this)),
102 timer_login_attempt_(new QTimer(this)),
103 timer_refresh_login_(new QTimer(this)),
104 favorite_request_(new TidalFavoriteRequest(this, network_, this)),
105 enabled_(false),
106 oauth_(false),
107 user_id_(0),
108 artistssearchlimit_(1),
109 albumssearchlimit_(1),
110 songssearchlimit_(1),
111 fetchalbums_(true),
112 download_album_covers_(true),
113 stream_url_method_(TidalSettingsPage::StreamUrlMethod_StreamUrl),
114 album_explicit_(false),
115 expires_in_(0),
116 login_time_(0),
117 pending_search_id_(0),
118 next_pending_search_id_(1),
119 pending_search_type_(InternetSearchView::SearchType_Artists),
120 search_id_(0),
121 login_sent_(false),
122 login_attempts_(0),
123 next_stream_url_request_id_(0) {
124
125 app->player()->RegisterUrlHandler(url_handler_);
126
127 // Backends
128
129 artists_collection_backend_ = new CollectionBackend();
130 artists_collection_backend_->moveToThread(app_->database()->thread());
131 artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source_Tidal, kArtistsSongsTable, kArtistsSongsFtsTable);
132
133 albums_collection_backend_ = new CollectionBackend();
134 albums_collection_backend_->moveToThread(app_->database()->thread());
135 albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source_Tidal, kAlbumsSongsTable, kAlbumsSongsFtsTable);
136
137 songs_collection_backend_ = new CollectionBackend();
138 songs_collection_backend_->moveToThread(app_->database()->thread());
139 songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source_Tidal, kSongsTable, kSongsFtsTable);
140
141 artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
142 albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
143 songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
144
145 artists_collection_sort_model_->setSourceModel(artists_collection_model_);
146 artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
147 artists_collection_sort_model_->setDynamicSortFilter(true);
148 artists_collection_sort_model_->setSortLocaleAware(true);
149 artists_collection_sort_model_->sort(0);
150
151 albums_collection_sort_model_->setSourceModel(albums_collection_model_);
152 albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
153 albums_collection_sort_model_->setDynamicSortFilter(true);
154 albums_collection_sort_model_->setSortLocaleAware(true);
155 albums_collection_sort_model_->sort(0);
156
157 songs_collection_sort_model_->setSourceModel(songs_collection_model_);
158 songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
159 songs_collection_sort_model_->setDynamicSortFilter(true);
160 songs_collection_sort_model_->setSortLocaleAware(true);
161 songs_collection_sort_model_->sort(0);
162
163 // Search
164
165 timer_search_delay_->setSingleShot(true);
166 QObject::connect(timer_search_delay_, &QTimer::timeout, this, &TidalService::StartSearch);
167
168 timer_login_attempt_->setSingleShot(true);
169 timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
170 QObject::connect(timer_login_attempt_, &QTimer::timeout, this, &TidalService::ResetLoginAttempts);
171
172 timer_refresh_login_->setSingleShot(true);
173 QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &TidalService::RequestNewAccessToken);
174
175 QObject::connect(this, &TidalService::RequestLogin, this, &TidalService::SendLogin);
176 QObject::connect(this, &TidalService::LoginWithCredentials, this, &TidalService::SendLoginWithCredentials);
177
178 QObject::connect(this, &TidalService::AddArtists, favorite_request_, &TidalFavoriteRequest::AddArtists);
179 QObject::connect(this, &TidalService::AddAlbums, favorite_request_, &TidalFavoriteRequest::AddAlbums);
180 QObject::connect(this, &TidalService::AddSongs, favorite_request_, &TidalFavoriteRequest::AddSongs);
181
182 QObject::connect(this, &TidalService::RemoveArtists, favorite_request_, &TidalFavoriteRequest::RemoveArtists);
183 QObject::connect(this, &TidalService::RemoveAlbums, favorite_request_, &TidalFavoriteRequest::RemoveAlbums);
184 QObject::connect(this, QOverload<SongList>::of(&TidalService::RemoveSongs), favorite_request_, QOverload<const SongList&>::of(&TidalFavoriteRequest::RemoveSongs));
185 QObject::connect(this, QOverload<SongMap>::of(&TidalService::RemoveSongs), favorite_request_, QOverload<const SongMap&>::of(&TidalFavoriteRequest::RemoveSongs));
186
187 QObject::connect(favorite_request_, &TidalFavoriteRequest::RequestLogin, this, &TidalService::SendLogin);
188
189 QObject::connect(favorite_request_, &TidalFavoriteRequest::ArtistsAdded, artists_collection_backend_, &CollectionBackend::AddOrUpdateSongs);
190 QObject::connect(favorite_request_, &TidalFavoriteRequest::AlbumsAdded, albums_collection_backend_, &CollectionBackend::AddOrUpdateSongs);
191 QObject::connect(favorite_request_, &TidalFavoriteRequest::SongsAdded, songs_collection_backend_, &CollectionBackend::AddOrUpdateSongs);
192
193 QObject::connect(favorite_request_, &TidalFavoriteRequest::ArtistsRemoved, artists_collection_backend_, &CollectionBackend::DeleteSongs);
194 QObject::connect(favorite_request_, &TidalFavoriteRequest::AlbumsRemoved, albums_collection_backend_, &CollectionBackend::DeleteSongs);
195 QObject::connect(favorite_request_, &TidalFavoriteRequest::SongsRemoved, songs_collection_backend_, &CollectionBackend::DeleteSongs);
196
197 TidalService::ReloadSettings();
198 LoadSession();
199
200 }
201
~TidalService()202 TidalService::~TidalService() {
203
204 while (!replies_.isEmpty()) {
205 QNetworkReply *reply = replies_.takeFirst();
206 QObject::disconnect(reply, nullptr, this, nullptr);
207 reply->abort();
208 reply->deleteLater();
209 }
210
211 while (!stream_url_requests_.isEmpty()) {
212 std::shared_ptr<TidalStreamURLRequest> stream_url_req = stream_url_requests_.take(stream_url_requests_.firstKey());
213 QObject::disconnect(stream_url_req.get(), nullptr, this, nullptr);
214 stream_url_req->deleteLater();
215 }
216
217 artists_collection_backend_->deleteLater();
218 albums_collection_backend_->deleteLater();
219 songs_collection_backend_->deleteLater();
220
221 }
222
Exit()223 void TidalService::Exit() {
224
225 wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_;
226
227 QObject::connect(artists_collection_backend_, &CollectionBackend::ExitFinished, this, &TidalService::ExitReceived);
228 QObject::connect(albums_collection_backend_, &CollectionBackend::ExitFinished, this, &TidalService::ExitReceived);
229 QObject::connect(songs_collection_backend_, &CollectionBackend::ExitFinished, this, &TidalService::ExitReceived);
230
231 artists_collection_backend_->ExitAsync();
232 albums_collection_backend_->ExitAsync();
233 songs_collection_backend_->ExitAsync();
234
235 }
236
ExitReceived()237 void TidalService::ExitReceived() {
238
239 QObject *obj = sender();
240 QObject::disconnect(obj, nullptr, this, nullptr);
241 qLog(Debug) << obj << "successfully exited.";
242 wait_for_exit_.removeAll(obj);
243 if (wait_for_exit_.isEmpty()) emit ExitFinished();
244
245 }
246
ShowConfig()247 void TidalService::ShowConfig() {
248 app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
249 }
250
LoadSession()251 void TidalService::LoadSession() {
252
253 QSettings s;
254 s.beginGroup(TidalSettingsPage::kSettingsGroup);
255 user_id_ = s.value("user_id").toInt();
256 country_code_ = s.value("country_code", "US").toString();
257 access_token_ = s.value("access_token").toString();
258 refresh_token_ = s.value("refresh_token").toString();
259 session_id_ = s.value("session_id").toString();
260 expires_in_ = s.value("expires_in").toLongLong();
261 login_time_ = s.value("login_time").toLongLong();
262 s.endGroup();
263
264 if (!refresh_token_.isEmpty()) {
265 qint64 time = expires_in_ - (QDateTime::currentDateTime().toSecsSinceEpoch() - login_time_);
266 if (time <= 0) {
267 timer_refresh_login_->setInterval(200ms);
268 }
269 else {
270 timer_refresh_login_->setInterval(static_cast<int>(time * kMsecPerSec));
271 }
272 timer_refresh_login_->start();
273 }
274
275 }
276
ReloadSettings()277 void TidalService::ReloadSettings() {
278
279 QSettings s;
280 s.beginGroup(TidalSettingsPage::kSettingsGroup);
281
282 enabled_ = s.value("enabled", false).toBool();
283 oauth_ = s.value("oauth", true).toBool();
284 client_id_ = s.value("client_id").toString();
285 api_token_ = s.value("api_token").toString();
286
287 username_ = s.value("username").toString();
288 QByteArray password = s.value("password").toByteArray();
289 if (password.isEmpty()) password_.clear();
290 else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
291
292 quality_ = s.value("quality", "LOSSLESS").toString();
293 quint64 search_delay = s.value("searchdelay", 1500).toInt();
294 artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt();
295 albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt();
296 songssearchlimit_ = s.value("songssearchlimit", 10).toInt();
297 fetchalbums_ = s.value("fetchalbums", false).toBool();
298 coversize_ = s.value("coversize", "640x640").toString();
299 download_album_covers_ = s.value("downloadalbumcovers", true).toBool();
300 stream_url_method_ = static_cast<TidalSettingsPage::StreamUrlMethod>(s.value("streamurl").toInt());
301 album_explicit_ = s.value("album_explicit").toBool();
302
303 s.endGroup();
304
305 timer_search_delay_->setInterval(search_delay);
306
307 }
308
StartAuthorization(const QString & client_id)309 void TidalService::StartAuthorization(const QString &client_id) {
310
311 client_id_ = client_id;
312 code_verifier_ = Utilities::CryptographicRandomString(44);
313 code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
314
315 if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
316 code_challenge_.chop(1);
317 }
318
319 const ParamList params = ParamList() << Param("response_type", "code")
320 << Param("code_challenge", code_challenge_)
321 << Param("code_challenge_method", "S256")
322 << Param("redirect_uri", kOAuthRedirectUrl)
323 << Param("client_id", client_id_)
324 << Param("scope", "r_usr w_usr");
325
326 QUrlQuery url_query;
327 for (const Param ¶m : params) {
328 url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
329 }
330
331 QUrl url = QUrl(kOAuthUrl);
332 url.setQuery(url_query);
333 QDesktopServices::openUrl(url);
334
335 }
336
AuthorizationUrlReceived(const QUrl & url)337 void TidalService::AuthorizationUrlReceived(const QUrl &url) {
338
339 qLog(Debug) << "Tidal: Authorization URL Received" << url;
340
341 QUrlQuery url_query(url);
342
343 if (url_query.hasQueryItem("token_type") && url_query.hasQueryItem("expires_in") && url_query.hasQueryItem("access_token")) {
344
345 access_token_ = url_query.queryItemValue("access_token").toUtf8();
346 if (url_query.hasQueryItem("refresh_token")) {
347 refresh_token_ = url_query.queryItemValue("refresh_token").toUtf8();
348 }
349 expires_in_ = url_query.queryItemValue("expires_in").toInt();
350 login_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
351 session_id_.clear();
352
353 QSettings s;
354 s.beginGroup(TidalSettingsPage::kSettingsGroup);
355 s.setValue("access_token", access_token_);
356 s.setValue("refresh_token", refresh_token_);
357 s.setValue("expires_in", expires_in_);
358 s.setValue("login_time", login_time_);
359 s.remove("session_id");
360 s.endGroup();
361
362 emit LoginComplete(true);
363 emit LoginSuccess();
364 }
365
366 else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
367
368 QString code = url_query.queryItemValue("code");
369
370 RequestAccessToken(code);
371
372 }
373
374 else {
375 LoginError(tr("Reply from Tidal is missing query items."));
376 return;
377 }
378
379 }
380
RequestAccessToken(const QString & code)381 void TidalService::RequestAccessToken(const QString &code) {
382
383 timer_refresh_login_->stop();
384
385 ParamList params = ParamList() << Param("client_id", client_id_);
386
387 if (!code.isEmpty()) {
388 params << Param("grant_type", "authorization_code");
389 params << Param("code", code);
390 params << Param("code_verifier", code_verifier_);
391 params << Param("redirect_uri", kOAuthRedirectUrl);
392 params << Param("scope", "r_usr w_usr");
393 }
394 else if (!refresh_token_.isEmpty() && enabled_ && oauth_) {
395 params << Param("grant_type", "refresh_token");
396 params << Param("refresh_token", refresh_token_);
397 }
398 else {
399 return;
400 }
401
402 QUrlQuery url_query;
403 for (const Param ¶m : params) {
404 url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
405 }
406
407 QUrl url(kOAuthAccessTokenUrl);
408 QNetworkRequest req(url);
409 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
410 req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
411 #else
412 req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
413 #endif
414 QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
415
416 login_errors_.clear();
417 QNetworkReply *reply = network_->post(req, query);
418 replies_ << reply;
419 QObject::connect(reply, &QNetworkReply::sslErrors, this, &TidalService::HandleLoginSSLErrors);
420 QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AccessTokenRequestFinished(reply); });
421
422 }
423
HandleLoginSSLErrors(const QList<QSslError> & ssl_errors)424 void TidalService::HandleLoginSSLErrors(const QList<QSslError> &ssl_errors) {
425
426 for (const QSslError &ssl_error : ssl_errors) {
427 login_errors_ += ssl_error.errorString();
428 }
429
430 }
431
AccessTokenRequestFinished(QNetworkReply * reply)432 void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) {
433
434 if (!replies_.contains(reply)) return;
435 replies_.removeAll(reply);
436 QObject::disconnect(reply, nullptr, this, nullptr);
437 reply->deleteLater();
438
439 if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
440 if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
441 // This is a network error, there is nothing more to do.
442 LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
443 return;
444 }
445 else {
446 // See if there is Json data containing "status" and "userMessage" then use that instead.
447 QByteArray data(reply->readAll());
448 QJsonParseError json_error;
449 QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
450 if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
451 QJsonObject json_obj = json_doc.object();
452 if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
453 int status = json_obj["status"].toInt();
454 int sub_status = json_obj["subStatus"].toInt();
455 QString user_message = json_obj["userMessage"].toString();
456 login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
457 }
458 }
459 if (login_errors_.isEmpty()) {
460 if (reply->error() != QNetworkReply::NoError) {
461 login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
462 }
463 else {
464 login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
465 }
466 }
467 LoginError();
468 return;
469 }
470 }
471
472 QByteArray data(reply->readAll());
473 QJsonParseError json_error;
474 QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
475
476 if (json_error.error != QJsonParseError::NoError) {
477 LoginError("Authentication reply from server missing Json data.");
478 return;
479 }
480
481 if (json_doc.isEmpty()) {
482 LoginError("Authentication reply from server has empty Json document.");
483 return;
484 }
485
486 if (!json_doc.isObject()) {
487 LoginError("Authentication reply from server has Json document that is not an object.", json_doc);
488 return;
489 }
490
491 QJsonObject json_obj = json_doc.object();
492 if (json_obj.isEmpty()) {
493 LoginError("Authentication reply from server has empty Json object.", json_doc);
494 return;
495 }
496
497 if (!json_obj.contains("access_token") || !json_obj.contains("expires_in")) {
498 LoginError("Authentication reply from server is missing access_token or expires_in", json_obj);
499 return;
500 }
501
502 access_token_ = json_obj["access_token"].toString();
503 expires_in_ = json_obj["expires_in"].toInt();
504 if (json_obj.contains("refresh_token")) {
505 refresh_token_ = json_obj["refresh_token"].toString();
506 }
507 login_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
508
509 if (json_obj.contains("user") && json_obj["user"].isObject()) {
510 QJsonObject obj_user = json_obj["user"].toObject();
511 if (obj_user.contains("countryCode") && obj_user.contains("userId")) {
512 country_code_ = obj_user["countryCode"].toString();
513 user_id_ = obj_user["userId"].toInt();
514 }
515 }
516
517 session_id_.clear();
518
519 QSettings s;
520 s.beginGroup(TidalSettingsPage::kSettingsGroup);
521 s.setValue("access_token", access_token_);
522 s.setValue("refresh_token", refresh_token_);
523 s.setValue("expires_in", expires_in_);
524 s.setValue("login_time", login_time_);
525 s.setValue("country_code", country_code_);
526 s.setValue("user_id", user_id_);
527 s.remove("session_id");
528 s.endGroup();
529
530 if (expires_in_ > 0) {
531 timer_refresh_login_->setInterval(static_cast<int>(expires_in_ * kMsecPerSec));
532 timer_refresh_login_->start();
533 }
534
535 qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_;
536
537 emit LoginComplete(true);
538 emit LoginSuccess();
539
540 }
541
SendLogin()542 void TidalService::SendLogin() {
543 SendLoginWithCredentials(api_token_, username_, password_);
544 }
545
SendLoginWithCredentials(const QString & api_token,const QString & username,const QString & password)546 void TidalService::SendLoginWithCredentials(const QString &api_token, const QString &username, const QString &password) {
547
548 login_sent_ = true;
549 ++login_attempts_;
550 timer_login_attempt_->start();
551 timer_refresh_login_->stop();
552
553 const ParamList params = ParamList() << Param("token", (api_token.isEmpty() ? api_token_ : api_token))
554 << Param("username", username)
555 << Param("password", password)
556 << Param("clientVersion", "2.2.1--7");
557
558 QUrlQuery url_query;
559 for (const Param ¶m : params) {
560 url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
561 }
562
563 QUrl url(kAuthUrl);
564 QNetworkRequest req(url);
565
566 req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
567 req.setRawHeader("X-Tidal-Token", (api_token.isEmpty() ? api_token_.toUtf8() : api_token.toUtf8()));
568
569 QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
570 QNetworkReply *reply = network_->post(req, query);
571 QObject::connect(reply, &QNetworkReply::sslErrors, this, &TidalService::HandleLoginSSLErrors);
572 QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { HandleAuthReply(reply); });
573 replies_ << reply;
574
575 //qLog(Debug) << "Tidal: Sending request" << url << query;
576
577 }
578
HandleAuthReply(QNetworkReply * reply)579 void TidalService::HandleAuthReply(QNetworkReply *reply) {
580
581 if (!replies_.contains(reply)) return;
582 replies_.removeAll(reply);
583 QObject::disconnect(reply, nullptr, this, nullptr);
584 reply->deleteLater();
585
586 login_sent_ = false;
587
588 if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
589 if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
590 // This is a network error, there is nothing more to do.
591 LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
592 login_errors_.clear();
593 return;
594 }
595 else {
596 // See if there is Json data containing "status" and "userMessage" - then use that instead.
597 QByteArray data(reply->readAll());
598 QJsonParseError json_error;
599 QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
600 if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
601 QJsonObject json_obj = json_doc.object();
602 if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
603 int status = json_obj["status"].toInt();
604 int sub_status = json_obj["subStatus"].toInt();
605 QString user_message = json_obj["userMessage"].toString();
606 login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
607 }
608 }
609 if (login_errors_.isEmpty()) {
610 if (reply->error() != QNetworkReply::NoError) {
611 login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
612 }
613 else {
614 login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
615 }
616 }
617 LoginError();
618 login_errors_.clear();
619 return;
620 }
621 }
622
623 login_errors_.clear();
624
625 QByteArray data(reply->readAll());
626 QJsonParseError json_error;
627 QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
628
629 if (json_error.error != QJsonParseError::NoError) {
630 LoginError("Authentication reply from server missing Json data.");
631 return;
632 }
633
634 if (json_doc.isEmpty()) {
635 LoginError("Authentication reply from server has empty Json document.");
636 return;
637 }
638
639 if (!json_doc.isObject()) {
640 LoginError("Authentication reply from server has Json document that is not an object.", json_doc);
641 return;
642 }
643
644 QJsonObject json_obj = json_doc.object();
645 if (json_obj.isEmpty()) {
646 LoginError("Authentication reply from server has empty Json object.", json_doc);
647 return;
648 }
649
650 if (!json_obj.contains("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode")) {
651 LoginError("Authentication reply from server is missing userId, sessionId or countryCode", json_obj);
652 return;
653 }
654
655 country_code_ = json_obj["countryCode"].toString();
656 session_id_ = json_obj["sessionId"].toString();
657 user_id_ = json_obj["userId"].toInt();
658 access_token_.clear();
659 refresh_token_.clear();
660
661 QSettings s;
662 s.beginGroup(TidalSettingsPage::kSettingsGroup);
663 s.remove("access_token");
664 s.remove("refresh_token");
665 s.remove("expires_in");
666 s.remove("login_time");
667 s.setValue("user_id", user_id_);
668 s.setValue("session_id", session_id_);
669 s.setValue("country_code", country_code_);
670 s.endGroup();
671
672 qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
673
674 login_attempts_ = 0;
675 timer_login_attempt_->stop();
676
677 emit LoginComplete(true);
678 emit LoginSuccess();
679
680 }
681
Logout()682 void TidalService::Logout() {
683
684 user_id_ = 0;
685 country_code_.clear();
686 access_token_.clear();
687 session_id_.clear();
688 expires_in_ = 0;
689 login_time_ = 0;
690
691 QSettings s;
692 s.beginGroup(TidalSettingsPage::kSettingsGroup);
693 s.remove("user_id");
694 s.remove("country_code");
695 s.remove("access_token");
696 s.remove("session_id");
697 s.remove("expires_in");
698 s.remove("login_time");
699 s.endGroup();
700
701 timer_refresh_login_->stop();
702
703 }
704
ResetLoginAttempts()705 void TidalService::ResetLoginAttempts() {
706 login_attempts_ = 0;
707 }
708
TryLogin()709 void TidalService::TryLogin() {
710
711 if (authenticated() || login_sent_) return;
712
713 if (api_token_.isEmpty()) {
714 emit LoginComplete(false, tr("Missing Tidal API token."));
715 return;
716 }
717 if (username_.isEmpty()) {
718 emit LoginComplete(false, tr("Missing Tidal username."));
719 return;
720 }
721 if (password_.isEmpty()) {
722 emit LoginComplete(false, tr("Missing Tidal password."));
723 return;
724 }
725 if (login_attempts_ >= kLoginAttempts) {
726 emit LoginComplete(false, tr("Not authenticated with Tidal and reached maximum number of login attempts."));
727 return;
728 }
729
730 emit RequestLogin();
731
732 }
733
ResetArtistsRequest()734 void TidalService::ResetArtistsRequest() {
735
736 if (artists_request_) {
737 QObject::disconnect(artists_request_.get(), nullptr, this, nullptr);
738 QObject::disconnect(this, nullptr, artists_request_.get(), nullptr);
739 artists_request_.reset();
740 }
741
742 }
743
GetArtists()744 void TidalService::GetArtists() {
745
746 if (!authenticated()) {
747 if (oauth_) {
748 emit ArtistsResults(SongMap(), tr("Not authenticated with Tidal."));
749 ShowConfig();
750 return;
751 }
752 else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
753 emit ArtistsResults(SongMap(), tr("Missing Tidal API token, username or password."));
754 ShowConfig();
755 return;
756 }
757 }
758
759 ResetArtistsRequest();
760
761 artists_request_ = std::make_shared<TidalRequest>(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Artists, this);
762
763 QObject::connect(artists_request_.get(), &TidalRequest::RequestLogin, this, &TidalService::SendLogin);
764 QObject::connect(artists_request_.get(), &TidalRequest::Results, this, &TidalService::ArtistsResultsReceived);
765 QObject::connect(artists_request_.get(), &TidalRequest::UpdateStatus, this, &TidalService::ArtistsUpdateStatusReceived);
766 QObject::connect(artists_request_.get(), &TidalRequest::ProgressSetMaximum, this, &TidalService::ArtistsProgressSetMaximumReceived);
767 QObject::connect(artists_request_.get(), &TidalRequest::UpdateProgress, this, &TidalService::ArtistsUpdateProgressReceived);
768 QObject::connect(this, &TidalService::LoginComplete, artists_request_.get(), &TidalRequest::LoginComplete);
769
770 artists_request_->Process();
771
772 }
773
ArtistsResultsReceived(const int id,const SongMap & songs,const QString & error)774 void TidalService::ArtistsResultsReceived(const int id, const SongMap &songs, const QString &error) {
775 Q_UNUSED(id);
776 emit ArtistsResults(songs, error);
777 }
778
ArtistsUpdateStatusReceived(const int id,const QString & text)779 void TidalService::ArtistsUpdateStatusReceived(const int id, const QString &text) {
780 Q_UNUSED(id);
781 emit ArtistsUpdateStatus(text);
782 }
783
ArtistsProgressSetMaximumReceived(const int id,const int max)784 void TidalService::ArtistsProgressSetMaximumReceived(const int id, const int max) {
785 Q_UNUSED(id);
786 emit ArtistsProgressSetMaximum(max);
787 }
788
ArtistsUpdateProgressReceived(const int id,const int progress)789 void TidalService::ArtistsUpdateProgressReceived(const int id, const int progress) {
790 Q_UNUSED(id);
791 emit ArtistsUpdateProgress(progress);
792 }
793
ResetAlbumsRequest()794 void TidalService::ResetAlbumsRequest() {
795
796 if (albums_request_) {
797 QObject::disconnect(albums_request_.get(), nullptr, this, nullptr);
798 QObject::disconnect(this, nullptr, albums_request_.get(), nullptr);
799 albums_request_.reset();
800 }
801
802 }
803
GetAlbums()804 void TidalService::GetAlbums() {
805
806 if (!authenticated()) {
807 if (oauth_) {
808 emit AlbumsResults(SongMap(), tr("Not authenticated with Tidal."));
809 ShowConfig();
810 return;
811 }
812 else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
813 emit AlbumsResults(SongMap(), tr("Missing Tidal API token, username or password."));
814 ShowConfig();
815 return;
816 }
817 }
818
819 ResetAlbumsRequest();
820 albums_request_ = std::make_shared<TidalRequest>(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Albums, this);
821 QObject::connect(albums_request_.get(), &TidalRequest::RequestLogin, this, &TidalService::SendLogin);
822 QObject::connect(albums_request_.get(), &TidalRequest::Results, this, &TidalService::AlbumsResultsReceived);
823 QObject::connect(albums_request_.get(), &TidalRequest::UpdateStatus, this, &TidalService::AlbumsUpdateStatusReceived);
824 QObject::connect(albums_request_.get(), &TidalRequest::ProgressSetMaximum, this, &TidalService::AlbumsProgressSetMaximumReceived);
825 QObject::connect(albums_request_.get(), &TidalRequest::UpdateProgress, this, &TidalService::AlbumsUpdateProgressReceived);
826 QObject::connect(this, &TidalService::LoginComplete, albums_request_.get(), &TidalRequest::LoginComplete);
827
828 albums_request_->Process();
829
830 }
831
AlbumsResultsReceived(const int id,const SongMap & songs,const QString & error)832 void TidalService::AlbumsResultsReceived(const int id, const SongMap &songs, const QString &error) {
833 Q_UNUSED(id);
834 emit AlbumsResults(songs, error);
835 }
836
AlbumsUpdateStatusReceived(const int id,const QString & text)837 void TidalService::AlbumsUpdateStatusReceived(const int id, const QString &text) {
838 Q_UNUSED(id);
839 emit AlbumsUpdateStatus(text);
840 }
841
AlbumsProgressSetMaximumReceived(const int id,const int max)842 void TidalService::AlbumsProgressSetMaximumReceived(const int id, const int max) {
843 Q_UNUSED(id);
844 emit AlbumsProgressSetMaximum(max);
845 }
846
AlbumsUpdateProgressReceived(const int id,const int progress)847 void TidalService::AlbumsUpdateProgressReceived(const int id, const int progress) {
848 Q_UNUSED(id);
849 emit AlbumsUpdateProgress(progress);
850 }
851
ResetSongsRequest()852 void TidalService::ResetSongsRequest() {
853
854 if (songs_request_) {
855 QObject::disconnect(songs_request_.get(), nullptr, this, nullptr);
856 QObject::disconnect(this, nullptr, songs_request_.get(), nullptr);
857 songs_request_.reset();
858 }
859
860 }
861
GetSongs()862 void TidalService::GetSongs() {
863
864 if (!authenticated()) {
865 if (oauth_) {
866 emit SongsResults(SongMap(), tr("Not authenticated with Tidal."));
867 ShowConfig();
868 return;
869 }
870 else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
871 emit SongsResults(SongMap(), tr("Missing Tidal API token, username or password."));
872 ShowConfig();
873 return;
874 }
875 }
876
877 ResetSongsRequest();
878 songs_request_ = std::make_shared<TidalRequest>(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Songs, this);
879 QObject::connect(songs_request_.get(), &TidalRequest::RequestLogin, this, &TidalService::SendLogin);
880 QObject::connect(songs_request_.get(), &TidalRequest::Results, this, &TidalService::SongsResultsReceived);
881 QObject::connect(songs_request_.get(), &TidalRequest::UpdateStatus, this, &TidalService::SongsUpdateStatusReceived);
882 QObject::connect(songs_request_.get(), &TidalRequest::ProgressSetMaximum, this, &TidalService::SongsProgressSetMaximumReceived);
883 QObject::connect(songs_request_.get(), &TidalRequest::UpdateProgress, this, &TidalService::SongsUpdateProgressReceived);
884 QObject::connect(this, &TidalService::LoginComplete, songs_request_.get(), &TidalRequest::LoginComplete);
885
886 songs_request_->Process();
887
888 }
889
SongsResultsReceived(const int id,const SongMap & songs,const QString & error)890 void TidalService::SongsResultsReceived(const int id, const SongMap &songs, const QString &error) {
891 Q_UNUSED(id);
892 emit SongsResults(songs, error);
893 }
894
SongsUpdateStatusReceived(const int id,const QString & text)895 void TidalService::SongsUpdateStatusReceived(const int id, const QString &text) {
896 Q_UNUSED(id);
897 emit SongsUpdateStatus(text);
898 }
899
SongsProgressSetMaximumReceived(const int id,const int max)900 void TidalService::SongsProgressSetMaximumReceived(const int id, const int max) {
901 Q_UNUSED(id);
902 emit SongsProgressSetMaximum(max);
903 }
904
SongsUpdateProgressReceived(const int id,const int progress)905 void TidalService::SongsUpdateProgressReceived(const int id, const int progress) {
906 Q_UNUSED(id);
907 emit SongsUpdateProgress(progress);
908 }
909
Search(const QString & text,InternetSearchView::SearchType type)910 int TidalService::Search(const QString &text, InternetSearchView::SearchType type) {
911
912 pending_search_id_ = next_pending_search_id_;
913 pending_search_text_ = text;
914 pending_search_type_ = type;
915
916 next_pending_search_id_++;
917
918 if (text.isEmpty()) {
919 timer_search_delay_->stop();
920 return pending_search_id_;
921 }
922 timer_search_delay_->start();
923
924 return pending_search_id_;
925
926 }
927
StartSearch()928 void TidalService::StartSearch() {
929
930 if (!authenticated()) {
931 if (oauth_) {
932 emit SearchResults(pending_search_id_, SongMap(), tr("Not authenticated with Tidal."));
933 ShowConfig();
934 return;
935 }
936 else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
937 emit SearchResults(pending_search_id_, SongMap(), tr("Missing Tidal API token, username or password."));
938 ShowConfig();
939 return;
940 }
941 }
942
943 search_id_ = pending_search_id_;
944 search_text_ = pending_search_text_;
945
946 SendSearch();
947
948 }
949
CancelSearch()950 void TidalService::CancelSearch() {
951 }
952
SendSearch()953 void TidalService::SendSearch() {
954
955 TidalBaseRequest::QueryType type = TidalBaseRequest::QueryType_None;
956
957 switch (pending_search_type_) {
958 case InternetSearchView::SearchType_Artists:
959 type = TidalBaseRequest::QueryType_SearchArtists;
960 break;
961 case InternetSearchView::SearchType_Albums:
962 type = TidalBaseRequest::QueryType_SearchAlbums;
963 break;
964 case InternetSearchView::SearchType_Songs:
965 type = TidalBaseRequest::QueryType_SearchSongs;
966 break;
967 default:
968 //Error("Invalid search type.");
969 return;
970 }
971
972 search_request_ = std::make_shared<TidalRequest>(this, url_handler_, app_, network_, type, this);
973
974 QObject::connect(search_request_.get(), &TidalRequest::RequestLogin, this, &TidalService::SendLogin);
975 QObject::connect(search_request_.get(), &TidalRequest::Results, this, &TidalService::SearchResultsReceived);
976 QObject::connect(search_request_.get(), &TidalRequest::UpdateStatus, this, &TidalService::SearchUpdateStatus);
977 QObject::connect(search_request_.get(), &TidalRequest::ProgressSetMaximum, this, &TidalService::SearchProgressSetMaximum);
978 QObject::connect(search_request_.get(), &TidalRequest::UpdateProgress, this, &TidalService::SearchUpdateProgress);
979 QObject::connect(this, &TidalService::LoginComplete, search_request_.get(), &TidalRequest::LoginComplete);
980
981 search_request_->Search(search_id_, search_text_);
982 search_request_->Process();
983
984 }
985
SearchResultsReceived(const int id,const SongMap & songs,const QString & error)986 void TidalService::SearchResultsReceived(const int id, const SongMap &songs, const QString &error) {
987 emit SearchResults(id, songs, error);
988 }
989
GetStreamURL(const QUrl & url)990 void TidalService::GetStreamURL(const QUrl &url) {
991
992 if (!authenticated()) {
993 if (oauth_) {
994 emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Not authenticated with Tidal."));
995 return;
996 }
997 else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
998 emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Tidal API token, username or password."));
999 return;
1000 }
1001 }
1002
1003 const int id = ++next_stream_url_request_id_;
1004 std::shared_ptr<TidalStreamURLRequest> stream_url_req = std::make_shared<TidalStreamURLRequest>(this, network_, url, id);
1005 stream_url_requests_.insert(id, stream_url_req);
1006
1007 QObject::connect(stream_url_req.get(), &TidalStreamURLRequest::TryLogin, this, &TidalService::TryLogin);
1008 QObject::connect(stream_url_req.get(), &TidalStreamURLRequest::StreamURLFinished, this, &TidalService::HandleStreamURLFinished);
1009 QObject::connect(this, &TidalService::LoginComplete, stream_url_req.get(), &TidalStreamURLRequest::LoginComplete);
1010
1011 stream_url_req->Process();
1012
1013 }
1014
HandleStreamURLFinished(const int id,const QUrl & original_url,const QUrl & stream_url,const Song::FileType filetype,const int samplerate,const int bit_depth,const qint64 duration,const QString & error)1015 void TidalService::HandleStreamURLFinished(const int id, const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, const QString &error) {
1016
1017 if (!stream_url_requests_.contains(id)) return;
1018 std::shared_ptr<TidalStreamURLRequest> stream_url_req = stream_url_requests_.take(id);
1019
1020 emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error);
1021
1022 }
1023
LoginError(const QString & error,const QVariant & debug)1024 void TidalService::LoginError(const QString &error, const QVariant &debug) {
1025
1026 if (!error.isEmpty()) login_errors_ << error;
1027
1028 QString error_html;
1029 for (const QString &e : login_errors_) {
1030 qLog(Error) << "Tidal:" << e;
1031 error_html += e + "<br />";
1032 }
1033 if (debug.isValid()) qLog(Debug) << debug;
1034
1035 emit LoginFailure(error_html);
1036 emit LoginComplete(false, error_html);
1037
1038 login_errors_.clear();
1039
1040 }
1041