From 0a64a2a394a370ab7940919c2de0f274ff02fdc6 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Thu, 6 Sep 2018 17:39:26 +0200 Subject: [PATCH] Finish Tidal - Add configurable settings - Add progressbar - Simplify code --- src/settings/tidalsettingspage.cpp | 23 ++ src/settings/tidalsettingspage.ui | 220 +++++++++++++++++- src/tidal/tidalsearch.cpp | 16 ++ src/tidal/tidalsearch.h | 7 + src/tidal/tidalsearchmodel.cpp | 2 +- src/tidal/tidalsearchview.cpp | 32 ++- src/tidal/tidalsearchview.h | 3 + src/tidal/tidalsearchview.ui | 104 ++++++--- src/tidal/tidalservice.cpp | 351 +++++++++++++++-------------- src/tidal/tidalservice.h | 76 +++---- 10 files changed, 578 insertions(+), 256 deletions(-) diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp index 73399bd2e..c2ce346a9 100644 --- a/src/settings/tidalsettingspage.cpp +++ b/src/settings/tidalsettingspage.cpp @@ -54,6 +54,12 @@ TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent) ui_->combobox_quality->addItem("High", "HIGH"); ui_->combobox_quality->addItem("Lossless", "LOSSLESS"); + ui_->combobox_coversize->addItem("160x160", "160x160"); + ui_->combobox_coversize->addItem("320x320", "320x320"); + ui_->combobox_coversize->addItem("640x640", "640x640"); + ui_->combobox_coversize->addItem("750x750", "750x750"); + ui_->combobox_coversize->addItem("1280x1280", "1280x1280"); + } TidalSettingsPage::~TidalSettingsPage() { delete ui_; } @@ -61,11 +67,23 @@ TidalSettingsPage::~TidalSettingsPage() { delete ui_; } void TidalSettingsPage::Load() { QSettings s; + s.beginGroup(kSettingsGroup); + ui_->username->setText(s.value("username").toString()); ui_->password->setText(s.value("password").toString()); + QString quality = s.value("quality", "HIGH").toString(); ui_->combobox_quality->setCurrentIndex(ui_->combobox_quality->findData(quality)); + + ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt()); + ui_->spinbox_albumssearchlimit->setValue(s.value("albumssearchlimit", 40).toInt()); + ui_->spinbox_songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt()); + ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); + + QString coversize = s.value("coversize", "320x320").toString(); + ui_->combobox_coversize->setCurrentIndex(ui_->combobox_coversize->findData(coversize)); + s.endGroup(); if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); @@ -79,6 +97,11 @@ void TidalSettingsPage::Save() { s.setValue("username", ui_->username->text()); s.setValue("password", ui_->password->text()); s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex())); + s.setValue("searchdelay", ui_->spinbox_searchdelay->value()); + s.setValue("albumssearchlimit", ui_->spinbox_albumssearchlimit->value()); + s.setValue("songssearchlimit", ui_->spinbox_songssearchlimit->value()); + s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); + s.setValue("coversize", ui_->combobox_coversize->itemData(ui_->combobox_coversize->currentIndex())); s.endGroup(); service_->ReloadSettings(); diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui index dc2389f5c..081be2111 100644 --- a/src/settings/tidalsettingspage.ui +++ b/src/settings/tidalsettingspage.ui @@ -7,16 +7,13 @@ 0 0 715 - 425 + 483 Tidal - - - @@ -68,20 +65,221 @@ - + + + + Preferences - - - + + + + + + + + 150 + 0 + + + + Audio quality + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Search delay + + + + + + + ms + + + 0 + + + 10000 + + + 50 + + + 1500 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Albums search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 150 + 0 + + + + Songs search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + - Audio quality + Fetch entire albums when searching songs - - + + + + + + + 150 + 0 + + + + Album cover size + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + diff --git a/src/tidal/tidalsearch.cpp b/src/tidal/tidalsearch.cpp index a16a890b8..b2d5bf7f8 100644 --- a/src/tidal/tidalsearch.cpp +++ b/src/tidal/tidalsearch.cpp @@ -71,6 +71,9 @@ TidalSearch::TidalSearch(Application *app, QObject *parent) connect(this, SIGNAL(SearchAsyncSig(int, QString, TidalSettingsPage::SearchBy)), this, SLOT(DoSearchAsync(int, QString, TidalSettingsPage::SearchBy))); connect(this, SIGNAL(ResultsAvailable(int, TidalSearch::ResultList)), SLOT(ResultsAvailableSlot(int, TidalSearch::ResultList))); connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage))); + connect(service_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatusSlot(QString))); + connect(service_, SIGNAL(ProgressSetMaximum(int)), SLOT(ProgressSetMaximumSlot(int))); + connect(service_, SIGNAL(UpdateProgress(int)), SLOT(UpdateProgressSlot(int))); connect(service_, SIGNAL(SearchResults(int, SongList)), SLOT(SearchDone(int, SongList))); connect(service_, SIGNAL(SearchError(int, QString)), SLOT(HandleError(int, QString))); @@ -178,6 +181,7 @@ void TidalSearch::CancelSearch(int id) { return; } } + service_->CancelSearch(); } void TidalSearch::timerEvent(QTimerEvent *e) { @@ -311,3 +315,15 @@ MimeData *TidalSearch::LoadTracks(const ResultList &results) { return mime_data; } + +void TidalSearch::UpdateStatusSlot(QString text) { + emit UpdateStatus(text); +} + +void TidalSearch::ProgressSetMaximumSlot(int max) { + emit ProgressSetMaximum(max); +} + +void TidalSearch::UpdateProgressSlot(int progress) { + emit UpdateProgress(progress); +} diff --git a/src/tidal/tidalsearch.h b/src/tidal/tidalsearch.h index 6137eda64..702358b5e 100644 --- a/src/tidal/tidalsearch.h +++ b/src/tidal/tidalsearch.h @@ -75,6 +75,9 @@ class TidalSearch : public QObject { void AddResults(int id, const TidalSearch::ResultList &results); void SearchError(const int id, const QString error); void SearchFinished(int id); + void UpdateStatus(QString text); + void ProgressSetMaximum(int progress); + void UpdateProgress(int max); void ArtLoaded(int id, const QPixmap &pixmap); void ArtLoaded(int id, const QImage &image); @@ -113,6 +116,10 @@ class TidalSearch : public QObject { void ArtLoadedSlot(int id, const QImage &image); void AlbumArtLoaded(quint64 id, const QImage &image); + void UpdateStatusSlot(QString text); + void ProgressSetMaximumSlot(int progress); + void UpdateProgressSlot(int max); + private: void SearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby); void HandleLoadedArt(int id, const QImage &image); diff --git a/src/tidal/tidalsearchmodel.cpp b/src/tidal/tidalsearchmodel.cpp index ded76c6bf..c531bca75 100644 --- a/src/tidal/tidalsearchmodel.cpp +++ b/src/tidal/tidalsearchmodel.cpp @@ -45,7 +45,7 @@ TidalSearchModel::TidalSearchModel(TidalSearch *engine, QObject *parent) group_by_[0] = CollectionModel::GroupBy_Artist; group_by_[1] = CollectionModel::GroupBy_Album; group_by_[2] = CollectionModel::GroupBy_None; - + QIcon nocover = IconLoader::Load("cdcase"); no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); diff --git a/src/tidal/tidalsearchview.cpp b/src/tidal/tidalsearchview.cpp index 67cd35241..81917ad58 100644 --- a/src/tidal/tidalsearchview.cpp +++ b/src/tidal/tidalsearchview.cpp @@ -83,6 +83,8 @@ TidalSearchView::TidalSearchView(Application *app, QWidget *parent) error_(false) { ui_->setupUi(this); + ui_->progressbar->hide(); + ui_->progressbar->reset(); front_model_->set_proxy(front_proxy_); back_model_->set_proxy(back_proxy_); @@ -147,6 +149,11 @@ TidalSearchView::TidalSearchView(Application *app, QWidget *parent) connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*))); // These have to be queued connections because they may get emitted before our call to Search() (or whatever) returns and we add the ID to the map. + + connect(engine_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatus(QString))); + connect(engine_, SIGNAL(ProgressSetMaximum(int)), SLOT(ProgressSetMaximum(int)), Qt::QueuedConnection); + connect(engine_, SIGNAL(UpdateProgress(int)), SLOT(UpdateProgress(int)), Qt::QueuedConnection); + connect(engine_, SIGNAL(AddResults(int, TidalSearch::ResultList)), SLOT(AddResults(int, TidalSearch::ResultList)), Qt::QueuedConnection); connect(engine_, SIGNAL(SearchError(int, QString)), SLOT(SearchError(int, QString)), Qt::QueuedConnection); connect(engine_, SIGNAL(ArtLoaded(int, QPixmap)), SLOT(ArtLoaded(int, QPixmap)), Qt::QueuedConnection); @@ -219,8 +226,12 @@ void TidalSearchView::TextEdited(const QString &text) { if (trimmed.isEmpty()) { last_search_id_ = -1; ui_->label_helptext->setText("Enter search terms above to find music"); + ui_->label_status->clear(); + ui_->progressbar->hide(); + ui_->progressbar->reset(); } else { + ui_->progressbar->reset(); last_search_id_ = engine_->SearchAsync(trimmed, searchby_); } @@ -229,12 +240,18 @@ void TidalSearchView::TextEdited(const QString &text) { void TidalSearchView::AddResults(int id, const TidalSearch::ResultList &results) { if (id != last_search_id_) return; if (results.isEmpty()) return; + ui_->label_status->clear(); + ui_->progressbar->reset(); + ui_->progressbar->hide(); current_model_->AddResults(results); } void TidalSearchView::SearchError(const int id, const QString error) { error_ = true; ui_->label_helptext->setText(error); + ui_->label_status->clear(); + ui_->progressbar->reset(); + ui_->progressbar->hide(); ui_->results_stack->setCurrentWidget(ui_->help_page); } @@ -378,7 +395,7 @@ bool TidalSearchView::SearchKeyEvent(QKeyEvent *event) { break; case Qt::Key_Return: - AddSelectedToPlaylist(); + TextEdited(ui_->search->text()); break; default: @@ -542,3 +559,16 @@ void TidalSearchView::SetSearchBy(TidalSettingsPage::SearchBy searchby) { s.endGroup(); TextEdited(ui_->search->text()); } + +void TidalSearchView::UpdateStatus(QString text) { + ui_->progressbar->show(); + ui_->label_status->setText(text); +} + +void TidalSearchView::ProgressSetMaximum(int max) { + ui_->progressbar->setMaximum(max); +} + +void TidalSearchView::UpdateProgress(int progress) { + ui_->progressbar->setValue(progress); +} diff --git a/src/tidal/tidalsearchview.h b/src/tidal/tidalsearchview.h index d4dfe7a8d..ce88b5f96 100644 --- a/src/tidal/tidalsearchview.h +++ b/src/tidal/tidalsearchview.h @@ -77,6 +77,9 @@ signals: private slots: void SwapModels(); void TextEdited(const QString &text); + void UpdateStatus(QString text); + void ProgressSetMaximum(int progress); + void UpdateProgress(int max); void AddResults(int id, const TidalSearch::ResultList &results); void SearchError(const int id, const QString error); void ArtLoaded(int id, const QPixmap &pixmap); diff --git a/src/tidal/tidalsearchview.ui b/src/tidal/tidalsearchview.ui index 996b86019..b94672f53 100644 --- a/src/tidal/tidalsearchview.ui +++ b/src/tidal/tidalsearchview.ui @@ -7,7 +7,7 @@ 0 0 400 - 633 + 660 @@ -33,7 +33,7 @@ 0 - + @@ -108,6 +108,27 @@ + + + + + + + + + true + + + + + + + 0 + + + + + @@ -187,46 +208,55 @@ 0 0 398 - 533 + 502 - - - - 9 - 109 - 336 - 100 - - - - - 32 - - - 16 - - - 32 - - - 64 - - - - - Enter search terms above to find music + + + + + + 32 - - Qt::AlignCenter + + 16 - - true + + 32 - - - - + + 64 + + + + + Enter search terms above to find music + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp index 99c5328d6..46b33f942 100644 --- a/src/tidal/tidalservice.cpp +++ b/src/tidal/tidalservice.cpp @@ -59,24 +59,23 @@ const char *TidalService::kAuthUrl = "https://listen.tidal.com/v1/login/username const char *TidalService::kResourcesUrl = "http://resources.tidal.com"; const char *TidalService::kApiToken = "P5Xbeo5LFvESeDy6"; -const int TidalService::kSearchDelayMsec = 1500; -const int TidalService::kSearchAlbumsLimit = 40; -const int TidalService::kSearchTracksLimit = 10; - typedef QPair Param; TidalService::TidalService(Application *app, InternetModel *parent) : InternetService(kServiceName, app, parent, parent), network_(new NetworkAccessManager(this)), - search_delay_(new QTimer(this)), + timer_searchdelay_(new QTimer(this)), + searchdelay_(1500), + albumssearchlimit_(1), + songssearchlimit_(1), + fetchalbums_(false), pending_search_id_(0), next_pending_search_id_(1), - search_requests_(0), - login_sent_(false) { + login_sent_(false) + { - search_delay_->setInterval(kSearchDelayMsec); - search_delay_->setSingleShot(true); - connect(search_delay_, SIGNAL(timeout()), SLOT(StartSearch())); + timer_searchdelay_->setSingleShot(true); + connect(timer_searchdelay_, SIGNAL(timeout()), SLOT(StartSearch())); ReloadSettings(); LoadSessionID(); @@ -96,6 +95,11 @@ void TidalService::ReloadSettings() { username_ = s.value("username").toString(); password_ = s.value("password").toString(); quality_ = s.value("quality").toString(); + searchdelay_ = s.value("searchdelay", 1500).toInt(); + albumssearchlimit_ = s.value("albumssearchlimit", 40).toInt(); + songssearchlimit_ = s.value("songssearchlimit", 10).toInt(); + fetchalbums_ = s.value("fetchalbums", false).toBool(); + coversize_ = s.value("coversize", "320x320").toString(); s.endGroup(); } @@ -112,20 +116,12 @@ void TidalService::LoadSessionID() { } -void TidalService::Login(const QString &username, const QString &password) { - Login(nullptr, username, password); -} +void TidalService::Login(const QString &username, const QString &password, int search_id) { -void TidalService::Login(TidalSearchContext *search_ctx, const QString &username, const QString &password) { + if (search_id != 0) emit UpdateStatus("Authenticating..."); login_sent_ = true; - - int id = 0; - if (search_ctx) { - search_ctx->login_sent = true; - search_ctx->login_attempts++; - id = search_ctx->id; - } + login_attempts_++; typedef QPair Arg; typedef QList ArgList; @@ -148,28 +144,22 @@ void TidalService::Login(TidalSearchContext *search_ctx, const QString &username req.setRawHeader("Origin", "http://listen.tidal.com"); QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8()); - NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, id); + NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, search_id); } -void TidalService::HandleAuthReply(QNetworkReply *reply, int id) { +void TidalService::HandleAuthReply(QNetworkReply *reply, int search_id) { reply->deleteLater(); login_sent_ = false; - TidalSearchContext *search_ctx(nullptr); - if (id != 0 && requests_search_.contains(id)) { - search_ctx = requests_search_.value(id); - search_ctx->login_sent = false; - } - //int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError) { if (reply->error() < 200) { // This is a network error, there is nothing more to do. QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } @@ -191,7 +181,7 @@ void TidalService::HandleAuthReply(QNetworkReply *reply, int id) { else { failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } @@ -203,21 +193,21 @@ void TidalService::HandleAuthReply(QNetworkReply *reply, int id) { if (error.error != QJsonParseError::NoError) { QString failure_reason("Authentication reply from server missing Json data."); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } if (json_doc.isNull() || json_doc.isEmpty()) { QString failure_reason("Authentication reply from server has empty Json document."); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } if (!json_doc.isObject()) { QString failure_reason("Authentication reply from server has Json document that is not an object."); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } @@ -225,14 +215,14 @@ void TidalService::HandleAuthReply(QNetworkReply *reply, int id) { QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { QString failure_reason("Authentication reply from server has empty Json object."); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } if ( !json_obj.contains("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode") ) { QString failure_reason = tr("Authentication reply from server is missing userId, sessionId or countryCode"); - if (search_ctx) Error(search_ctx, failure_reason); + if (search_id != 0) Error(failure_reason); emit LoginFailure(failure_reason); return; } @@ -250,9 +240,9 @@ void TidalService::HandleAuthReply(QNetworkReply *reply, int id) { qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_; - if (search_ctx) { + if (search_id != 0) { qLog(Debug) << "Tidal: Resuming search"; - SendSearch(search_ctx); + SendSearch(); } emit LoginSuccess(); @@ -305,7 +295,7 @@ QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const } -QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply) { +QJsonObject TidalService::ExtractJsonObj(QNetworkReply *reply) { QByteArray data; @@ -317,7 +307,7 @@ QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetwor if (reply->error() < 200) { // This is a network error, there is nothing more to do. QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(search_ctx, failure_reason); + Error(failure_reason); } else { // See if there is Json data containing "userMessage" - then use that instead. @@ -340,21 +330,21 @@ QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetwor if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) { // Session is probably expired, attempt to login once Logout(); - if (search_ctx->login_attempts < 1 && !username_.isEmpty() && !password_.isEmpty()) { + if (login_attempts_ < 1 && !username_.isEmpty() && !password_.isEmpty()) { qLog(Error) << "Tidal:" << failure_reason; qLog(Error) << "Tidal:" << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); qLog(Error) << "Tidal:" << "Attempting to login."; - Login(search_ctx, username_, password_); + Login(username_, password_); } else { - Error(search_ctx, failure_reason); + Error(failure_reason); } } else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error qLog(Error) << "Tidal:" << failure_reason; } else { // Fail - Error(search_ctx, failure_reason); + Error(failure_reason); } } return QJsonObject(); @@ -364,23 +354,23 @@ QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetwor QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { - Error(search_ctx, "Reply from server missing Json data."); + Error("Reply from server missing Json data."); return QJsonObject(); } if (json_doc.isNull() || json_doc.isEmpty()) { - Error(search_ctx, "Received empty Json document."); + Error("Received empty Json document."); return QJsonObject(); } if (!json_doc.isObject()) { - Error(search_ctx, "Json document is not an object."); + Error("Json document is not an object."); return QJsonObject(); } QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { - Error(search_ctx, "Received empty Json object."); + Error("Received empty Json object."); return QJsonObject(); } @@ -390,19 +380,19 @@ QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetwor } -QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply) { +QJsonArray TidalService::ExtractItems(QNetworkReply *reply) { - QJsonObject json_obj = ExtractJsonObj(search_ctx, reply); + QJsonObject json_obj = ExtractJsonObj(reply); if (json_obj.isEmpty()) return QJsonArray(); if (!json_obj.contains("items")) { - Error(search_ctx, "Json reply is missing items."); + Error("Json reply is missing items."); return QJsonArray(); } QJsonArray json_items = json_obj["items"].toArray(); if (json_items.isEmpty()) { - Error(search_ctx, "No match."); + Error("No match."); return QJsonArray(); } @@ -413,16 +403,17 @@ QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkRe int TidalService::Search(const QString &text, TidalSettingsPage::SearchBy searchby) { pending_search_id_ = next_pending_search_id_; - pending_search_ = text; + pending_search_text_ = text; pending_searchby_ = searchby; next_pending_search_id_++; if (text.isEmpty()) { - search_delay_->stop(); + timer_searchdelay_->stop(); return pending_search_id_; } - search_delay_->start(); + timer_searchdelay_->setInterval(searchdelay_); + timer_searchdelay_->start(); return pending_search_id_; @@ -436,48 +427,54 @@ void TidalService::StartSearch() { ShowConfig(); return; } + ClearSearch(); + search_id_ = pending_search_id_; + search_text_ = pending_search_text_; - TidalSearchContext *search_ctx = CreateSearch(pending_search_id_, pending_search_); - if (authenticated()) SendSearch(search_ctx); - else Login(search_ctx, username_, password_); + if (authenticated()) SendSearch(); + else Login(username_, password_); } -TidalSearchContext *TidalService::CreateSearch(const int search_id, const QString text) { - - TidalSearchContext *search_ctx = new TidalSearchContext; - search_ctx->id = search_id; - search_ctx->text = text; - search_ctx->album_requests = 0; - search_ctx->song_requests = 0; - search_ctx->requests_album_.clear(); - search_ctx->requests_song_.clear(); - search_ctx->login_attempts = 0; - requests_search_.insert(search_id, search_ctx); - return search_ctx; - +void TidalService::CancelSearch() { + ClearSearch(); +} +void TidalService::ClearSearch() { + search_id_ = 0; + search_text_ = QString(); + search_error_ = QString(); + albums_requested_ = 0; + songs_requested_ = 0; + albums_received_ = 0; + songs_received_ = 0; + requests_album_.clear(); + requests_song_.clear(); + login_attempts_ = 0; + songs_.clear(); } -void TidalService::SendSearch(TidalSearchContext *search_ctx) { +void TidalService::SendSearch() { + + emit UpdateStatus("Searching..."); QList parameters; - parameters << Param("query", search_ctx->text); + parameters << Param("query", search_text_); QString searchparam; switch (pending_searchby_) { case TidalSettingsPage::SearchBy_Songs: searchparam = "search/tracks"; - parameters << Param("limit", QString::number(kSearchTracksLimit)); + parameters << Param("limit", QString::number(songssearchlimit_)); break; case TidalSettingsPage::SearchBy_Albums: default: searchparam = "search/albums"; - parameters << Param("limit", QString::number(kSearchAlbumsLimit)); + parameters << Param("limit", QString::number(albumssearchlimit_)); break; } QNetworkReply *reply = CreateRequest(searchparam, parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_ctx->id); + NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_id_); } @@ -485,12 +482,11 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) { reply->deleteLater(); - if (!requests_search_.contains(id)) return; - TidalSearchContext *search_ctx = requests_search_.value(id); + if (id != search_id_) return; - QJsonArray json_items = ExtractItems(search_ctx, reply); + QJsonArray json_items = ExtractItems(reply); if (json_items.isEmpty()) { - CheckFinish(search_ctx); + CheckFinish(); return; } @@ -498,6 +494,7 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) { QVector albums; for (const QJsonValue &value : json_items) { + //qLog(Debug) << value; if (!value.isObject()) { qLog(Error) << "Tidal: Invalid Json reply, item not a object."; qLog(Debug) << value; @@ -519,6 +516,10 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) { } else if (json_obj.contains("album")) { // This was a tracks search + if (!fetchalbums_) { + ParseSong(0, value); + 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."; @@ -540,7 +541,7 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) { continue; } - if (search_ctx->requests_album_.contains(album_id)) continue; + if (requests_album_.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."; @@ -572,17 +573,28 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) { } albums.insert(0, artist_album); - search_ctx->requests_album_.insert(album_id, album_id); - GetAlbum(search_ctx, album_id); - search_ctx->album_requests++; - if (search_ctx->album_requests >= kSearchAlbumsLimit) break; + requests_album_.insert(album_id, album_id); + GetAlbum(album_id); + albums_requested_++; + if (albums_requested_ >= albumssearchlimit_) break; } - CheckFinish(search_ctx); + if (albums_requested_ > 0) { + emit UpdateStatus(QString("Retriving %1 album%2...").arg(albums_requested_).arg(albums_requested_ == 1 ? "" : "s")); + emit ProgressSetMaximum(albums_requested_); + emit UpdateProgress(0); + } + else if (songs_requested_ > 0) { + emit UpdateStatus(QString("Retriving %1 song%2...").arg(songs_requested_).arg(songs_requested_ == 1 ? "" : "s")); + emit ProgressSetMaximum(songs_requested_); + emit UpdateProgress(songs_received_); + } + + CheckFinish(); } -void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) { +void TidalService::GetAlbum(const int album_id) { QList parameters; parameters << Param("token", session_id_) @@ -590,7 +602,7 @@ void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_ctx->id, album_id); + NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_id_, album_id); } @@ -598,52 +610,57 @@ void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int alb reply->deleteLater(); - if (!requests_search_.contains(search_id)) return; - TidalSearchContext *search_ctx = requests_search_.value(search_id); + if (search_id != search_id_) return; + if (!requests_album_.contains(album_id)) return; + albums_received_++; + emit UpdateProgress(albums_received_); - if (!search_ctx->requests_album_.contains(album_id)) return; - search_ctx->album_requests--; - - QJsonArray json_items = ExtractItems(search_ctx, reply); + QJsonArray json_items = ExtractItems(reply); if (json_items.isEmpty()) { - CheckFinish(search_ctx); + CheckFinish(); return; } bool compilation = false; bool multidisc = false; - Song *first_song(nullptr); - QList songs; + Song first_song; + QList songs; for (const QJsonValue &value : json_items) { - Song *song = ParseSong(search_ctx, album_id, value); - if (!song) continue; + Song song = ParseSong(album_id, value); + if (!song.is_valid()) continue; + if (song.disc() >= 2) multidisc = true; + if (song.is_compilation() || (first_song.is_valid() && song.artist() != first_song.artist())) compilation = true; + if (!first_song.is_valid()) first_song = song; songs << song; - if (song->disc() >= 2) multidisc = true; - if (song->is_compilation() || (first_song && song->artist() != first_song->artist())) compilation = true; - if (!first_song) first_song = song; } - if (compilation || multidisc) { - 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); - } + 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); } + requests_song_.insert(song.id(), song); + songs_requested_++; } - CheckFinish(search_ctx); + if (albums_requested_ <= albums_received_) { + emit UpdateStatus(QString("Retriving %1 song%2...").arg(songs_requested_).arg(songs_requested_ == 1 ? "" : "s")); + emit ProgressSetMaximum(songs_requested_); + emit UpdateProgress(songs_received_); + } + + CheckFinish(); } -Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value) { +Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &value) { Song song; if (!value.isObject()) { qLog(Error) << "Tidal: Invalid Json reply, track is not a object."; qLog(Debug) << value; - return nullptr; + return song; } QJsonObject json_obj = value.toObject(); @@ -665,7 +682,7 @@ Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id ) { qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values."; qLog(Debug) << json_obj; - return nullptr; + return song; } QJsonValue json_value_artist = json_obj["artist"]; @@ -673,7 +690,9 @@ Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id QJsonValue json_duration = json_obj["duration"]; QJsonArray json_artists = json_obj["artists"].toArray(); - int id = json_obj["id"].toInt(); + int song_id = json_obj["id"].toInt(); + if (requests_song_.contains(song_id)) return requests_song_.value(song_id); + QString title = json_obj["title"].toString(); QString url = json_obj["url"].toString(); int track = json_obj["trackNumber"].toInt(); @@ -684,38 +703,45 @@ Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id if (!json_value_artist.isObject()) { qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object."; qLog(Debug) << json_value_artist; - return nullptr; + return song; } 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 nullptr; + return song; } 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 nullptr; + return song; } QJsonObject json_album = json_value_album.toObject(); - if (!json_album.contains("title") || !json_album.contains("cover")) { - qLog(Error) << "Tidal: Invalid Json reply, track album is missing title or cover."; + 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 nullptr; + return song; + } + 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 is wrong."; + qLog(Debug) << json_album; + return song; } QString album = json_album["title"].toString(); QString cover = json_album["cover"].toString(); if (!allow_streaming || !stream_ready) { qLog(Error) << "Tidal: Skipping song" << artist << album << title << "because allowStreaming is false OR streamReady is false."; - qLog(Debug) << json_obj; - return nullptr; + //qLog(Debug) << json_obj; + return song; } //qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url; + song.set_id(song_id); song.set_album_id(album_id); song.set_artist(artist); song.set_album(album); @@ -732,30 +758,19 @@ Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id song.set_length_nanosec(duration); } - // Check and see if there is more than 1 artist on the song. - //int i = 0; - //for (const QJsonValue &a : json_artists) { - //i++; - //qLog(Debug) << a << i; - //} - //if (i > 1) song.set_compilation_detected(true); - cover = cover.replace("-", "/"); - //QUrl cover_url (QString("%1/images/%2/750x750.jpg").arg(kResourcesUrl).arg(cover)); - QUrl cover_url (QString("%1/images/%2/320x320.jpg").arg(kResourcesUrl).arg(cover)); + QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(coversize_)); song.set_art_automatic(cover_url.toEncoded()); - if (search_ctx->requests_song_.contains(id)) return search_ctx->requests_song_.value(id); - Song *song_new = new Song(song); - search_ctx->requests_song_.insert(id, song_new); - search_ctx->song_requests++; - GetStreamURL(search_ctx, album_id, id); + song.set_valid(true); - return song_new; + GetStreamURL(album_id, song_id); + + return song; } -void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id) { +void TidalService::GetStreamURL(const int album_id, const int song_id) { QList parameters; parameters << Param("token", session_id_) @@ -763,7 +778,7 @@ void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_ QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_ctx->id, song_id); + NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_id_, song_id); } @@ -771,63 +786,63 @@ void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int search_i reply->deleteLater(); - if (!requests_search_.contains(search_id)) return; - TidalSearchContext *search_ctx = requests_search_.value(search_id); + if (search_id != search_id_) return; - if (!search_ctx->requests_song_.contains(song_id)) { - CheckFinish(search_ctx); + if (!requests_song_.contains(song_id)) { + CheckFinish(); return; } - Song *song = search_ctx->requests_song_.value(song_id); + Song song = requests_song_.value(song_id); + songs_received_++; - search_ctx->song_requests--; + if (albums_requested_ <= albums_received_) { + emit UpdateProgress(songs_received_); + } - QJsonObject json_obj = ExtractJsonObj(search_ctx, reply); + QJsonObject json_obj = ExtractJsonObj(reply); if (json_obj.isEmpty()) { - delete search_ctx->requests_song_.take(song_id); - CheckFinish(search_ctx); + requests_song_.remove(song_id); + CheckFinish(); return; } if (!json_obj.contains("url") || !json_obj.contains("codec")) { qLog(Error) << "Tidal: Invalid Json reply, stream missing url or codec."; qLog(Debug) << json_obj; - delete search_ctx->requests_song_.take(song_id); - CheckFinish(search_ctx); + requests_song_.remove(song_id); + CheckFinish(); return; } - song->set_url(QUrl(json_obj["url"].toString())); - song->set_valid(true); + song.set_url(QUrl(json_obj["url"].toString())); + song.set_valid(true); QString codec = json_obj["codec"].toString(); - if (codec == "AAC") song->set_filetype(Song::Type_MP4); + if (codec == "AAC") song.set_filetype(Song::Type_MP4); else qLog(Debug) << "Tidal codec" << codec; - //qLog(Debug) << song->artist() << song->album() << song->title() << song->url() << song->filetype(); + //qLog(Debug) << song.artist() << song.album() << song.title() << song.url() << song.filetype(); - search_ctx->songs << *song; + songs_ << song; - delete search_ctx->requests_song_.take(song_id); + requests_song_.remove(song_id); - CheckFinish(search_ctx); + CheckFinish(); } -void TidalService::CheckFinish(TidalSearchContext *search_ctx) { +void TidalService::CheckFinish() { - if (!search_ctx->login_sent && search_ctx->album_requests <= 0 && search_ctx->song_requests <= 0) { - if (search_ctx->songs.isEmpty()) emit SearchError(search_ctx->id, search_ctx->error); - else emit SearchResults(search_ctx->id, search_ctx->songs); - delete requests_search_.take(search_ctx->id); + if (!login_sent_ && albums_requested_ <= albums_received_ && songs_requested_ <= songs_received_) { + if (songs_.isEmpty()) emit SearchError(search_id_, search_error_); + else emit SearchResults(search_id_, songs_); + ClearSearch(); } } -void TidalService::Error(TidalSearchContext *search_ctx, QString error, QString debug) { +void TidalService::Error(QString error, QString debug) { qLog(Error) << "Tidal:" << error; if (!debug.isEmpty()) qLog(Debug) << debug; - if (search_ctx) { - search_ctx->error = error; - CheckFinish(search_ctx); - } + search_error_ = error; + CheckFinish(); } diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index 96ba93fba..75432c97d 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -40,20 +40,6 @@ class NetworkAccessManager; -struct TidalSearchContext { - int id; - QString text; - QHash requests_album_; - QHash requests_song_; - int album_requests; - int song_requests; - SongList songs; - QString error; - bool login_sent; - int login_attempts; -}; -Q_DECLARE_METATYPE(TidalSearchContext); - class TidalService : public InternetService { Q_OBJECT @@ -65,9 +51,10 @@ class TidalService : public InternetService { void ReloadSettings(); - void Login(const QString &username, const QString &password); + void Login(const QString &username, const QString &password, int search_id = 0); void Logout(); int Search(const QString &query, TidalSettingsPage::SearchBy searchby); + void CancelSearch(); const bool login_sent() { return login_sent_; } const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); } @@ -77,31 +64,32 @@ class TidalService : public InternetService { 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); public slots: void ShowConfig(); private slots: - void HandleAuthReply(QNetworkReply *reply, int id); + void HandleAuthReply(QNetworkReply *reply, int search_id); void StartSearch(); - void SearchFinished(QNetworkReply *reply, int id); + void SearchFinished(QNetworkReply *reply, int search_id); void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id); void GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id); private: - void Login(TidalSearchContext *search_ctx, const QString &username, const QString &password); + void ClearSearch(); void LoadSessionID(); QNetworkReply *CreateRequest(const QString &ressource_name, const QList> ¶ms); - QJsonObject ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply); - QJsonArray ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply); - TidalSearchContext *CreateSearch(const int search_id, const QString text); - void SendSearch(TidalSearchContext *search_ctx); - void GetAlbum(TidalSearchContext *search_ctx, const int album_id); - Song *ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value); - Song ExtractSong(TidalSearchContext *search_ctx, const QJsonValue &value); - void GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id); - void CheckFinish(TidalSearchContext *search_ctx); - void Error(TidalSearchContext *search_ctx, QString error, QString debug = ""); + QJsonObject ExtractJsonObj(QNetworkReply *reply); + QJsonArray ExtractItems(QNetworkReply *reply); + void SendSearch(); + void GetAlbum(const int album_id); + Song ParseSong(const int album_id_requested, const QJsonValue &value); + void GetStreamURL(const int album_id, const int song_id); + void CheckFinish(); + void Error(QString error, QString debug = ""); static const char *kApiUrl; static const char *kAuthUrl; @@ -109,25 +97,37 @@ class TidalService : public InternetService { static const char *kApiToken; NetworkAccessManager *network_; - QTimer *search_delay_; - int pending_search_id_; - int next_pending_search_id_; - int search_requests_; - bool login_sent_; - static const int kSearchAlbumsLimit; - static const int kSearchTracksLimit; - static const int kSearchDelayMsec; + QTimer *timer_searchdelay_; QString username_; QString password_; QString quality_; + int searchdelay_; + int albumssearchlimit_; + int songssearchlimit_; + bool fetchalbums_; + QString coversize_; QString session_id_; quint64 user_id_; QString country_code_; - QString pending_search_; + int pending_search_id_; + int next_pending_search_id_; + QString pending_search_text_; TidalSettingsPage::SearchBy pending_searchby_; - QHash requests_search_; + + int search_id_; + QString search_text_; + QHash requests_album_; + QHash requests_song_; + int albums_requested_; + int albums_received_; + int songs_requested_; + int songs_received_; + SongList songs_; + QString search_error_; + bool login_sent_; + int login_attempts_; };