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 &param : 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 &param : 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 &param : 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