Add optional oauth authentication for tidal

This commit is contained in:
Jonas Kvinge 2019-06-09 19:29:25 +02:00
parent 85a0748ad9
commit c0c1457073
16 changed files with 766 additions and 399 deletions

View File

@ -135,6 +135,7 @@
#include "settings/backendsettingspage.h"
#include "settings/playlistsettingspage.h"
#ifdef HAVE_TIDAL
# include "tidal/tidalservice.h"
# include "settings/tidalsettingspage.h"
#endif
@ -548,6 +549,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(tidal_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(tidal_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(tidal_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
TidalService *tidalservice = qobject_cast<TidalService*> (app_->internet_services()->ServiceBySource(Song::Source_Tidal));
if (tidalservice)
connect(this, SIGNAL(AuthorisationUrlReceived(const QUrl&)), tidalservice, SLOT(AuthorisationUrlReceived(const QUrl&)));
#endif
// Playlist menu
@ -1797,6 +1802,7 @@ void MainWindow::CommandlineOptionsReceived(const quint32 instanceId, const QByt
}
void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
switch (options.player_action()) {
case CommandlineOptions::Player_Play:
if (options.urls().empty()) {
@ -1830,6 +1836,15 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
}
if (!options.urls().empty()) {
#ifdef HAVE_TIDAL
for (const QUrl url : options.urls()) {
if (url.scheme() == "tidal" && url.host() == "login") {
emit AuthorisationUrlReceived(url);
return;
}
}
#endif
MimeData *data = new MimeData;
data->setUrls(options.urls());
// Behaviour depends on command line options, so set it here

View File

@ -131,6 +131,8 @@ signals:
void IntroPointReached();
void AuthorisationUrlReceived(const QUrl &url);
private slots:
void FilePathChanged(const QString& path);

View File

@ -47,6 +47,8 @@ class InternetService : public QObject {
virtual void InitialLoadSettings() {}
virtual void ReloadSettings() {}
virtual QIcon Icon() { return Song::IconForSource(source_); }
virtual const bool oauth() = 0;
virtual const bool authenticated() = 0;
virtual int Search(const QString &query, InternetSearch::SearchType type) = 0;
virtual void CancelSearch() = 0;

View File

@ -185,6 +185,11 @@ void InternetTabsView::contextMenuEvent(QContextMenuEvent *e) {
void InternetTabsView::GetArtists() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->artists_collection->status()->clear();
ui_->artists_collection->progressbar()->show();
ui_->artists_collection->button_abort()->show();
@ -224,6 +229,11 @@ void InternetTabsView::ArtistsFinished(SongList songs) {
void InternetTabsView::GetAlbums() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->albums_collection->status()->clear();
ui_->albums_collection->progressbar()->show();
ui_->albums_collection->button_abort()->show();
@ -263,6 +273,11 @@ void InternetTabsView::AlbumsFinished(SongList songs) {
void InternetTabsView::GetSongs() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->songs_collection->status()->clear();
ui_->songs_collection->progressbar()->show();
ui_->songs_collection->button_abort()->show();

View File

@ -31,6 +31,7 @@
#include "core/iconloader.h"
#include "internet/internetservices.h"
#include "tidal/tidalservice.h"
#include "tidal/tidalstreamurlrequest.h"
const char *TidalSettingsPage::kSettingsGroup = "Tidal";
@ -44,7 +45,9 @@ TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent)
connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
connect(ui_->oauth, SIGNAL(toggled(bool)), SLOT(OAuthClicked(bool)));
connect(this, SIGNAL(Login()), service_, SLOT(StartAuthorisation()));
connect(this, SIGNAL(Login(QString, QString, QString)), service_, SLOT(SendLogin(QString, QString, QString)));
connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString)));
@ -63,6 +66,10 @@ TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent)
ui_->coversize->addItem("750x750", "750x750");
ui_->coversize->addItem("1280x1280", "1280x1280");
ui_->streamurl->addItem("streamurl", StreamUrlMethod_StreamUrl);
ui_->streamurl->addItem("urlpostpaywall", StreamUrlMethod_UrlPostPaywall);
ui_->streamurl->addItem("playbackinfopostpaywall", StreamUrlMethod_PlaybackInfoPostPaywall);
}
TidalSettingsPage::~TidalSettingsPage() { delete ui_; }
@ -72,12 +79,19 @@ void TidalSettingsPage::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
ui_->checkbox_enable->setChecked(s.value("enabled", false).toBool());
ui_->enable->setChecked(s.value("enabled", false).toBool());
ui_->oauth->setChecked(s.value("oauth", false).toBool());
ui_->client_id->setText(s.value("client_id").toString());
ui_->api_token->setText(s.value("api_token").toString());
ui_->user_id->setText(s.value("user_id").toString());
ui_->country_code->setText(s.value("country_code").toString());
ui_->username->setText(s.value("username").toString());
QByteArray password = s.value("password").toByteArray();
if (password.isEmpty()) ui_->password->clear();
else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password)));
ui_->token->setText(s.value("token").toString());
dialog()->ComboBoxLoadFromSettings(s, ui_->quality, "quality", "HIGH");
ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt());
ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 5).toInt());
@ -86,6 +100,12 @@ void TidalSettingsPage::Load() {
ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool());
ui_->checkbox_cache_album_covers->setChecked(s.value("cachealbumcovers", true).toBool());
dialog()->ComboBoxLoadFromSettings(s, ui_->coversize, "coversize", "320x320");
StreamUrlMethod stream_url = static_cast<StreamUrlMethod>(s.value("streamurl").toInt());
int i = ui_->streamurl->findData(stream_url);
if (i == -1) i = ui_->streamurl->findData(StreamUrlMethod_StreamUrl);
ui_->streamurl->setCurrentIndex(i);
s.endGroup();
if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
@ -96,10 +116,16 @@ void TidalSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("enabled", ui_->checkbox_enable->isChecked());
s.setValue("enabled", ui_->enable->isChecked());
s.setValue("oauth", ui_->oauth->isChecked());
s.setValue("client_id", ui_->client_id->text());
s.setValue("api_token", ui_->api_token->text());
s.setValue("user_id", ui_->user_id->text());
s.setValue("country_code", ui_->country_code->text());
s.setValue("username", ui_->username->text());
s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64()));
s.setValue("token", ui_->token->text());
s.setValue("quality", ui_->quality->itemData(ui_->quality->currentIndex()));
s.setValue("searchdelay", ui_->searchdelay->value());
s.setValue("artistssearchlimit", ui_->artistssearchlimit->value());
@ -108,6 +134,7 @@ void TidalSettingsPage::Save() {
s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked());
s.setValue("cachealbumcovers", ui_->checkbox_cache_album_covers->isChecked());
s.setValue("coversize", ui_->coversize->itemData(ui_->coversize->currentIndex()));
s.setValue("streamurl", ui_->streamurl->itemData(ui_->streamurl->currentIndex()));
s.endGroup();
service_->ReloadSettings();
@ -115,8 +142,19 @@ void TidalSettingsPage::Save() {
}
void TidalSettingsPage::LoginClicked() {
emit Login(ui_->username->text(), ui_->password->text(), ui_->token->text());
if (ui_->oauth->isChecked()) {
emit Login();
}
else {
if (ui_->username->text().isEmpty() || ui_->password->text().isEmpty()) {
QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing username or password."));
return;
}
emit Login(ui_->username->text(), ui_->password->text(), ui_->api_token->text());
}
ui_->button_login->setEnabled(false);
}
bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) {
@ -127,6 +165,16 @@ bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) {
}
return SettingsPage::eventFilter(object, event);
}
void TidalSettingsPage::OAuthClicked(bool enabled) {
ui_->client_id->setEnabled(enabled);
ui_->api_token->setEnabled(!enabled);
ui_->username->setEnabled(!enabled);
ui_->password->setEnabled(!enabled);
}
void TidalSettingsPage::LogoutClicked() {

View File

@ -38,15 +38,23 @@ class TidalSettingsPage : public SettingsPage {
static const char *kSettingsGroup;
enum StreamUrlMethod {
StreamUrlMethod_StreamUrl,
StreamUrlMethod_UrlPostPaywall,
StreamUrlMethod_PlaybackInfoPostPaywall,
};
void Load();
void Save();
bool eventFilter(QObject *object, QEvent *event);
signals:
void Login();
void Login(const QString &username, const QString &password, const QString &token);
private slots:
void OAuthClicked(bool enabled);
void LoginClicked();
void LogoutClicked();
void LoginSuccess();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>715</width>
<height>650</height>
<height>794</height>
</rect>
</property>
<property name="windowTitle">
@ -15,7 +15,7 @@
</property>
<layout class="QVBoxLayout" name="layout_tidalsettingspage">
<item>
<widget class="QCheckBox" name="checkbox_enable">
<widget class="QCheckBox" name="enable">
<property name="text">
<string>Enable</string>
</property>
@ -37,47 +37,113 @@
</sizepolicy>
</property>
<property name="title">
<string>Account details</string>
<string>Authentication</string>
</property>
<layout class="QFormLayout" name="layout_credential_group">
<item row="0" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_username">
<property name="text">
<string>Tidal username</string>
<string>Username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<item>
<widget class="QPushButton" name="button_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_password">
<property name="text">
<string>Tidal password</string>
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="6" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_client_id">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Client ID</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="client_id"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_api_token">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>API Token</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="api_token">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="oauth">
<property name="text">
<string>Use OAuth</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text">
<string>User ID</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="user_id"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_country_code">
<property name="text">
<string>Country Code</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="country_code"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
@ -93,304 +159,136 @@
<property name="title">
<string>Preferences</string>
</property>
<layout class="QVBoxLayout" name="layout_preferences">
<item>
<layout class="QHBoxLayout" name="layout_token">
<item>
<widget class="QLabel" name="label_token">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Token</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="token">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="spacer_token">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_quality">
<item>
<widget class="QLabel" name="label_quality">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Audio quality</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="quality"/>
</item>
<item>
<spacer name="spacer_quality">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_searchdelay">
<item>
<widget class="QLabel" name="label_searchdelay">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Search delay</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="searchdelay">
<property name="suffix">
<string>ms</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>50</number>
</property>
<property name="value">
<number>1500</number>
</property>
</widget>
</item>
<item>
<spacer name="spacer_searchdelay">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_artistssearchlimit">
<item>
<widget class="QLabel" name="label_artistssearchlimit">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Artists search limit</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="artistssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item>
<spacer name="spacer_artistssearchlimit">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_albumssearchlimit">
<item>
<widget class="QLabel" name="label_albumssearchlimit">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Albums search limit</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="albumssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item>
<spacer name="spacer_albumssearchlimit">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_songssearchlimit">
<item>
<widget class="QLabel" name="label_songssearchlimit">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Songs search limit</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="songssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item>
<spacer name="spacer_songssearchlimit">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkbox_fetchalbums">
<layout class="QFormLayout" name="layout_preferences">
<item row="0" column="0">
<widget class="QLabel" name="label_quality">
<property name="text">
<string>Fetch entire albums when searching songs</string>
<string>Audio quality</string>
</property>
</widget>
</item>
<item>
<item row="0" column="1">
<widget class="QComboBox" name="quality"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_searchdelay">
<property name="text">
<string>Search delay</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="searchdelay">
<property name="suffix">
<string>ms</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>50</number>
</property>
<property name="value">
<number>1500</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_artistssearchlimit">
<property name="text">
<string>Artists search limit</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="artistssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_albumssearchlimit">
<property name="text">
<string>Albums search limit</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="albumssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_songssearchlimit">
<property name="text">
<string>Songs search limit</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="checkbox_cache_album_covers">
<property name="text">
<string>Cache album covers</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_coversize">
<item>
<widget class="QLabel" name="label_coversize">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Album cover size</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="coversize"/>
</item>
<item>
<spacer name="spacer_coversize">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<item row="6" column="0">
<widget class="QCheckBox" name="checkbox_fetchalbums">
<property name="text">
<string>Fetch entire albums when searching songs</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="coversize"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_coversize">
<property name="text">
<string>Album cover size</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="songssearchlimit">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_streamurl">
<property name="text">
<string>Stream URL method</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QComboBox" name="streamurl"/>
</item>
</layout>
</widget>
@ -457,7 +355,6 @@
<tabstops>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>button_login</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>

View File

@ -40,7 +40,6 @@
#include "tidalbaserequest.h"
const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1";
const char *TidalBaseRequest::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng==";
TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) :
QObject(parent),
@ -53,7 +52,7 @@ TidalBaseRequest::~TidalBaseRequest() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, 0, nullptr, 0);
reply->abort();
if (reply->isRunning()) reply->abort();
reply->deleteLater();
}
@ -61,11 +60,7 @@ TidalBaseRequest::~TidalBaseRequest() {
QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const QList<Param> &params_provided) {
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
ParamList params = ParamList() << params_provided
<< Param("sessionId", session_id())
<< Param("countryCode", country_code());
QStringList query_items;
@ -80,7 +75,9 @@ QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, co
url.setQuery(url_query);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
QNetworkReply *reply = network_->get(req);
replies_ << reply;
@ -129,7 +126,7 @@ QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error,
}
if (status == 401 && sub_status == 6001) { // User does not have a valid session
emit service_->Logout();
if (send_login && login_attempts() < max_login_attempts() && !token().isEmpty() && !username().isEmpty() && !password().isEmpty()) {
if (!oauth() && send_login && login_attempts() < max_login_attempts() && !api_token().isEmpty() && !username().isEmpty() && !password().isEmpty()) {
qLog(Error) << "Tidal:" << failure_reason;
qLog(Info) << "Tidal:" << "Attempting to login.";
NeedLogin();

View File

@ -67,16 +67,23 @@ class TidalBaseRequest : public QObject {
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> &params_provided);
QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool send_login);
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
QJsonValue ExtractItems(QByteArray &data, QString &error);
QJsonValue ExtractItems(QJsonObject &json_obj, QString &error);
QString Error(QString error, QVariant debug = QVariant());
virtual QString Error(QString error, QVariant debug = QVariant());
QString api_url() { return QString(kApiUrl); }
QString token() { return service_->token(); }
const bool oauth() { return service_->oauth(); }
QString client_id() { return service_->client_id(); }
QString api_token() { return service_->api_token(); }
quint64 user_id() { return service_->user_id(); }
QString country_code() { return service_->country_code(); }
QString username() { return service_->username(); }
QString password() { return service_->password(); }
QString quality() { return service_->quality(); }
@ -86,9 +93,8 @@ class TidalBaseRequest : public QObject {
bool fetchalbums() { return service_->fetchalbums(); }
QString coversize() { return service_->coversize(); }
QString access_token() { return service_->access_token(); }
QString session_id() { return service_->session_id(); }
quint64 user_id() { return service_->user_id(); }
QString country_code() { return service_->country_code(); }
bool authenticated() { return service_->authenticated(); }
bool need_login() { return need_login(); }
@ -101,7 +107,6 @@ class TidalBaseRequest : public QObject {
private:
static const char *kApiUrl;
static const char *kApiTokenB64;
TidalService *service_;
NetworkAccessManager *network_;

View File

@ -143,7 +143,8 @@ void TidalFavoriteRequest::AddFavorites(const FavoriteType type, const SongList
QUrl url(api_url() + QString("/") + "users/" + QString::number(service_->user_id()) + "/favorites/" + FavoriteText(type));
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AddFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs);
@ -233,9 +234,6 @@ void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongLi
void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const int id, const SongList &songs) {
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
ParamList params = ParamList() << Param("countryCode", country_code());
QStringList query_items;
@ -250,7 +248,8 @@ void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const int id
url.setQuery(url_query);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
QNetworkReply *reply = network_->deleteResource(req);
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoveFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs);
replies_ << reply;

View File

@ -54,6 +54,7 @@ TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler,
network_(network),
type_(type),
search_id_(-1),
finished_(false),
artists_requests_active_(0),
artists_total_(0),
artists_received_(0),
@ -73,10 +74,10 @@ TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler,
TidalRequest::~TidalRequest() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
while (!album_cover_replies_.isEmpty()) {
QNetworkReply *reply = album_cover_replies_.takeFirst();
disconnect(reply, 0, nullptr, 0);
reply->abort();
if (reply->isRunning()) reply->abort();
reply->deleteLater();
}
@ -310,6 +311,8 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re
--artists_requests_active_;
if (finished_) return;
if (data.isEmpty()) {
ArtistsFinishCheck();
return;
@ -406,6 +409,8 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re
void TidalRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) {
if (finished_) return;
if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) {
int offset_next = offset + artists_received;
if (offset_next > 0 && offset_next < artists_total_) {
@ -441,6 +446,7 @@ void TidalRequest::ArtistsFinishCheck(const int limit, const int offset, const i
void TidalRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
--albums_requests_active_;
AlbumsReceived(reply, 0, limit_requested, offset_requested, (offset_requested == 0));
if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
}
void TidalRequest::AddArtistAlbumsRequest(const int artist_id, const int offset) {
@ -475,6 +481,7 @@ void TidalRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const int art
++artist_albums_received_;
emit UpdateProgress(artist_albums_received_);
AlbumsReceived(reply, artist_id, 0, offset_requested, false);
if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
}
@ -483,6 +490,8 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const int artist_id_requ
QString error;
QByteArray data = GetReplyData(reply, error, auto_login);
if (finished_) return;
if (data.isEmpty()) {
AlbumsFinishCheck(artist_id_requested);
return;
@ -619,6 +628,8 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const int artist_id_requ
void TidalRequest::AlbumsFinishCheck(const int artist_id, const int limit, const int offset, const int albums_total, const int albums_received) {
if (finished_) return;
if (limit == 0 || limit > albums_received) {
int offset_next = offset + albums_received;
if (offset_next > 0 && offset_next < albums_total) {
@ -639,9 +650,6 @@ void TidalRequest::AlbumsFinishCheck(const int artist_id, const int limit, const
}
}
if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
if (
albums_requests_queue_.isEmpty() &&
albums_requests_active_ <= 0 &&
@ -726,6 +734,8 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const int artist_id, cons
QString error;
QByteArray data = GetReplyData(reply, error, auto_login);
if (finished_) return;
if (data.isEmpty()) {
SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist);
return;
@ -814,6 +824,8 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const int artist_id, cons
void TidalRequest::SongsFinishCheck(const int artist_id, const int album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist) {
if (finished_) return;
if (limit == 0 || limit > songs_received) {
int offset_next = offset + songs_received;
if (offset_next > 0 && offset_next < songs_total) {
@ -1020,7 +1032,7 @@ void TidalRequest::FlushAlbumCoverRequests() {
QNetworkRequest req(request.url);
QNetworkReply *reply = network_->get(req);
replies_ << reply;
album_cover_replies_ << reply;
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, int, QUrl)), reply, request.album_id, request.url);
}
@ -1029,8 +1041,8 @@ void TidalRequest::FlushAlbumCoverRequests() {
void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const int album_id, const QUrl url) {
if (replies_.contains(reply)) {
replies_.removeAll(reply);
if (album_cover_replies_.contains(reply)) {
album_cover_replies_.removeAll(reply);
reply->deleteLater();
}
else {
@ -1040,6 +1052,9 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const int album_id,
--album_covers_requests_active_;
++album_covers_received_;
if (finished_) return;
emit UpdateProgress(album_covers_received_);
if (!album_covers_requests_sent_.contains(album_id)) {
@ -1099,6 +1114,7 @@ void TidalRequest::AlbumCoverFinishCheck() {
void TidalRequest::FinishCheck() {
if (
!finished_ &&
!need_login_ &&
albums_requests_queue_.isEmpty() &&
artists_requests_queue_.isEmpty() &&
@ -1120,6 +1136,7 @@ void TidalRequest::FinishCheck() {
album_covers_requests_active_ <= 0 &&
album_covers_received_ >= album_covers_requested_
) {
finished_ = true;
if (songs_.isEmpty()) {
if (IsSearch()) {
if (no_results_) emit ErrorSignal(search_id_, tr("No match"));

View File

@ -164,6 +164,8 @@ class TidalRequest : public TidalBaseRequest {
int search_id_;
QString search_text_;
bool finished_;
QQueue<Request> artists_requests_queue_;
QQueue<Request> albums_requests_queue_;
QQueue<Request> songs_requests_queue_;
@ -199,7 +201,7 @@ class TidalRequest : public TidalBaseRequest {
QString errors_;
bool need_login_;
bool no_results_;
QList<QNetworkReply*> replies_;
QList<QNetworkReply*> album_cover_replies_;
};

View File

@ -24,6 +24,7 @@
#include <QObject>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QByteArray>
#include <QPair>
#include <QList>
@ -55,12 +56,17 @@
#include "tidalfavoriterequest.h"
#include "tidalstreamurlrequest.h"
#include "settings/tidalsettingspage.h"
#include "internet/localredirectserver.h"
using std::shared_ptr;
const Song::Source TidalService::kSource = Song::Source_Tidal;
const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username";
const char *TidalService::kClientIdB64 = "dTVxUE5OWUliRDBTMG8zNk1yQWlGWjU2SzZxTUNyQ21ZUHpadVRuVg==";
const char *TidalService::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng==";
const char *TidalService::kOAuthUrl = "https://login.tidal.com/authorize";
const char *TidalService::kOAuthAccessTokenUrl = "https://login.tidal.com/oauth2/token";
const char *TidalService::kOAuthRedirectUrl = "tidal://login/auth";
const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username";
const int TidalService::kLoginAttempts = 2;
const int TidalService::kTimeResetLoginAttempts = 60000;
@ -89,13 +95,13 @@ TidalService::TidalService(Application *app, QObject *parent)
timer_search_delay_(new QTimer(this)),
timer_login_attempt_(new QTimer(this)),
favorite_request_(new TidalFavoriteRequest(this, network_, this)),
user_id_(0),
search_delay_(1500),
artistssearchlimit_(1),
albumssearchlimit_(1),
songssearchlimit_(1),
fetchalbums_(true),
cache_album_covers_(true),
user_id_(0),
pending_search_id_(0),
next_pending_search_id_(1),
search_id_(0),
@ -169,7 +175,6 @@ TidalService::TidalService(Application *app, QObject *parent)
connect(favorite_request_, SIGNAL(SongsRemoved(const SongList&)), songs_collection_backend_, SLOT(DeleteSongs(const SongList&)));
ReloadSettings();
LoadSessionID();
}
@ -178,7 +183,7 @@ TidalService::~TidalService() {
while (!stream_url_requests_.isEmpty()) {
TidalStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst();
disconnect(stream_url_req, 0, nullptr, 0);
delete stream_url_req;
stream_url_req->deleteLater();
}
}
@ -191,12 +196,20 @@ void TidalService::ReloadSettings() {
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
oauth_ = s.value("oauth", false).toBool();
client_id_ = s.value("client_id").toString();
if (client_id_.isEmpty()) client_id_ = QString::fromUtf8(QByteArray::fromBase64(kClientIdB64));
api_token_ = s.value("api_token").toString();
if (api_token_.isEmpty()) api_token_ = QString::fromUtf8(QByteArray::fromBase64(kApiTokenB64));
user_id_ = s.value("user_id", 0).toInt();
country_code_ = s.value("country_code", "US").toString();
username_ = s.value("username").toString();
QByteArray password = s.value("password").toByteArray();
if (password.isEmpty()) password_.clear();
else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
token_ = s.value("token").toString();
if (token_.isEmpty()) token_ = QString::fromUtf8(QByteArray::fromBase64(kApiTokenB64));
quality_ = s.value("quality", "LOSSLESS").toString();
search_delay_ = s.value("searchdelay", 1500).toInt();
artistssearchlimit_ = s.value("artistssearchlimit", 5).toInt();
@ -205,6 +218,13 @@ void TidalService::ReloadSettings() {
fetchalbums_ = s.value("fetchalbums", false).toBool();
coversize_ = s.value("coversize", "320x320").toString();
cache_album_covers_ = s.value("cachealbumcovers", true).toBool();
stream_url_method_ = static_cast<TidalSettingsPage::StreamUrlMethod>(s.value("streamurl").toInt());
access_token_ = s.value("access_token").toString();
refresh_token_ = s.value("refresh_token").toString();
session_id_ = s.value("session_id").toString();
expiry_time_ = s.value("expiry_time").toDateTime();
s.endGroup();
}
@ -213,20 +233,206 @@ QString TidalService::CoverCacheDir() {
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers";
}
void TidalService::LoadSessionID() {
void TidalService::StartAuthorisation() {
login_sent_ = true;
++login_attempts_;
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
timer_login_attempt_->start();
const ParamList params = ParamList()
//<< Param("response_type", "token")
<< Param("response_type", "code")
<< Param("code_challenge", "T36p0vieh1pnvNNsG-0kNNpZIk4ZuP8vna5ZAtooxqo")
<< Param("code_challenge_method", "S256")
<< Param("redirect_uri", kOAuthRedirectUrl)
<< Param("client_id", client_id_)
<< Param("scope", "r_usr w_usr");
QUrlQuery url_query;
for (const Param &param : params) {
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
url_query.addQueryItem(encoded_param.first, encoded_param.second);
}
QUrl url = QUrl(kOAuthUrl);
url.setQuery(url_query);
QDesktopServices::openUrl(url);
}
void TidalService::AuthorisationUrlReceived(const QUrl &url) {
qLog(Debug) << "Tidal: Authorisation URL Received" << url;
QUrlQuery url_query(url);
if (url_query.hasQueryItem("token_type") && url_query.hasQueryItem("expires_in") && url_query.hasQueryItem("access_token")) {
access_token_ = url_query.queryItemValue("access_token").toUtf8();
int expires_in = url_query.queryItemValue("expires_in").toInt();
expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120);
session_id_.clear();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("access_token", access_token_);
s.setValue("expiry_time", expiry_time_);
s.remove("refresh_token");
s.remove("session_id");
s.endGroup();
login_attempts_ = 0;
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
emit LoginComplete(true);
emit LoginSuccess();
}
else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
QString code = url_query.queryItemValue("code");
QString state = url_query.queryItemValue("state");
const ParamList params = ParamList() << Param("code", code)
<< Param("client_id", client_id_)
<< Param("grant_type", "authorization_code")
<< Param("redirect_uri", kOAuthRedirectUrl)
<< Param("scope", "r_usr w_usr")
<< Param("code_verifier", "128,113,65,59,36,187,64,14,99,32,149,202,178,5,165,106,14,184,157,42,5,198,243,245,75,115,227,169,183,199,216,67,42,202,105,33,1");
QUrlQuery url_query;
for (const Param &param : params) {
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
url_query.addQueryItem(encoded_param.first, encoded_param.second);
}
QUrl url(kOAuthAccessTokenUrl);
QNetworkRequest request = QNetworkRequest(url);
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(request, query);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply);
}
else {
LoginError(tr("Reply from Tidal is missing query items."));
return;
}
}
void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) {
reply->deleteLater();
login_sent_ = false;
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
return;
}
else {
// See if there is Json data containing "redirectUri" then use that instead.
QByteArray data(reply->readAll());
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
QString failure_reason;
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (!json_obj.isEmpty() && json_obj.contains("redirectUri")) {
QString redirect_uri = json_obj["redirectUri"].toString();
failure_reason = QString("Authentication failure: %1").arg(redirect_uri);
}
}
if (failure_reason.isEmpty()) {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
LoginError(failure_reason);
return;
}
}
QByteArray data(reply->readAll());
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
LoginError("Authentication reply from server missing Json data.");
return;
}
if (json_doc.isNull() || json_doc.isEmpty()) {
LoginError("Authentication reply from server has empty Json document.");
return;
}
if (!json_doc.isObject()) {
LoginError("Authentication reply from server has Json document that is not an object.", json_doc);
return;
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
LoginError("Authentication reply from server has empty Json object.", json_doc);
return;
}
if (!json_obj.contains("access_token") ||
!json_obj.contains("refresh_token") ||
!json_obj.contains("expires_in") ||
!json_obj.contains("user")
) {
LoginError("Authentication reply from server is missing access_token, refresh_token, expires_in or user", json_obj);
return;
}
access_token_ = json_obj["access_token"].toString();
refresh_token_ = json_obj["refresh_token"].toString();
int expires_in = json_obj["expires_in"].toInt();
expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120);
QJsonValue json_user = json_obj["user"];
if (!json_user.isObject()) {
LoginError("Authentication reply from server has Json user that is not an object.", json_doc);
return;
}
QJsonObject json_obj_user = json_user.toObject();
if (json_obj_user.isEmpty()) {
LoginError("Authentication reply from server has empty Json user object.", json_doc);
return;
}
country_code_ = json_obj_user["countryCode"].toString();
user_id_ = json_obj_user["userId"].toInt();
session_id_.clear();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
if (!s.contains("user_id") ||!s.contains("session_id") || !s.contains("country_code")) return;
session_id_ = s.value("session_id").toString();
user_id_ = s.value("user_id").toInt();
country_code_ = s.value("country_code").toString();
s.setValue("access_token", access_token_);
s.setValue("refresh_token", refresh_token_);
s.setValue("expiry_time", expiry_time_);
s.setValue("country_code", country_code_);
s.setValue("user_id", user_id_);
s.remove("session_id");
s.endGroup();
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "access token" << access_token_;
login_attempts_ = 0;
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
emit LoginComplete(true);
emit LoginSuccess();
}
void TidalService::SendLogin() {
SendLogin(username_, password_, token_);
SendLogin(username_, password_, api_token_);
}
void TidalService::SendLogin(const QString &username, const QString &password, const QString &token) {
@ -239,13 +445,10 @@ void TidalService::SendLogin(const QString &username, const QString &password, c
timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
timer_login_attempt_->start();
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
ParamList params = ParamList() << Param("token", token_)
<< Param("username", username)
<< Param("password", password)
<< Param("clientVersion", "2.2.1--7");
const ParamList params = ParamList() << Param("token", (token.isEmpty() ? api_token_ : token))
<< Param("username", username)
<< Param("password", password)
<< Param("clientVersion", "2.2.1--7");
QStringList query_items;
QUrlQuery url_query;
@ -259,12 +462,13 @@ void TidalService::SendLogin(const QString &username, const QString &password, c
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
req.setRawHeader("X-Tidal-Token", token_.toUtf8());
req.setRawHeader("X-Tidal-Token", (token.isEmpty() ? api_token_.toUtf8() : token.toUtf8()));
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply);
//qLog(Debug) << "Tidal: Sending request" << url;
//qLog(Debug) << "Tidal: Sending request" << url << query;
}
@ -336,12 +540,18 @@ void TidalService::HandleAuthReply(QNetworkReply *reply) {
country_code_ = json_obj["countryCode"].toString();
session_id_ = json_obj["sessionId"].toString();
user_id_ = json_obj["userId"].toInt();
access_token_.clear();
refresh_token_.clear();
expiry_time_ = QDateTime();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("user_id", user_id_);
s.setValue("session_id", session_id_);
s.setValue("country_code", country_code_);
s.remove("access_token");
s.remove("refresh_token");
s.remove("expiry_time");
s.endGroup();
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
@ -356,15 +566,15 @@ void TidalService::HandleAuthReply(QNetworkReply *reply) {
void TidalService::Logout() {
user_id_ = 0;
access_token_.clear();
session_id_.clear();
country_code_.clear();
expiry_time_ = QDateTime();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.remove("user_id");
s.remove("access_token");
s.remove("session_id");
s.remove("country_code");
s.remove("expiry_time");
s.endGroup();
}
@ -381,7 +591,7 @@ void TidalService::TryLogin() {
emit LoginComplete(false, "Maximum number of login attempts reached.");
return;
}
if (token_.isEmpty()) {
if (api_token_.isEmpty()) {
emit LoginComplete(false, "Missing Tidal API token.");
return;
}
@ -428,14 +638,12 @@ void TidalService::GetArtists() {
void TidalService::ArtistsResultsReceived(SongList songs) {
emit ArtistsResults(songs);
ResetArtistsRequest();
}
void TidalService::ArtistsErrorReceived(QString error) {
emit ArtistsError(error);
ResetArtistsRequest();
}
@ -467,14 +675,12 @@ void TidalService::GetAlbums() {
void TidalService::AlbumsResultsReceived(SongList songs) {
emit AlbumsResults(songs);
ResetAlbumsRequest();
}
void TidalService::AlbumsErrorReceived(QString error) {
emit AlbumsError(error);
ResetAlbumsRequest();
}
@ -506,14 +712,12 @@ void TidalService::GetSongs() {
void TidalService::SongsResultsReceived(SongList songs) {
emit SongsResults(songs);
ResetSongsRequest();
}
void TidalService::SongsErrorReceived(QString error) {
emit SongsError(error);
ResetSongsRequest();
}
@ -538,8 +742,8 @@ int TidalService::Search(const QString &text, InternetSearch::SearchType type) {
void TidalService::StartSearch() {
if (token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
emit SearchError(pending_search_id_, tr("Missing token, username and/or password."));
if ((oauth_ && !authenticated()) || api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
emit SearchError(pending_search_id_, tr("Not authenticated."));
next_pending_search_id_ = 1;
ShowConfig();
return;
@ -605,7 +809,7 @@ void TidalService::HandleStreamURLFinished(const QUrl original_url, const QUrl s
TidalStreamURLRequest *stream_url_req = qobject_cast<TidalStreamURLRequest*>(sender());
if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return;
delete stream_url_req;
stream_url_req->deleteLater();
stream_url_requests_.removeAll(stream_url_req);
emit StreamURLFinished(original_url, stream_url, filetype, error);

View File

@ -37,6 +37,7 @@
#include "core/song.h"
#include "internet/internetservice.h"
#include "internet/internetsearch.h"
#include "settings/tidalsettingspage.h"
class QSortFilterProxyModel;
class Application;
@ -68,7 +69,11 @@ class TidalService : public InternetService {
const int max_login_attempts() { return kLoginAttempts; }
QString token() { return token_; }
const bool oauth() { return oauth_; }
QString client_id() { return client_id_; }
QString api_token() { return api_token_; }
quint64 user_id() { return user_id_; }
QString country_code() { return country_code_; }
QString username() { return username_; }
QString password() { return password_; }
QString quality() { return quality_; }
@ -79,12 +84,12 @@ class TidalService : public InternetService {
bool fetchalbums() { return fetchalbums_; }
QString coversize() { return coversize_; }
bool cache_album_covers() { return cache_album_covers_; }
TidalSettingsPage::StreamUrlMethod stream_url_method() { return stream_url_method_; }
QString access_token() { return access_token_; }
QString session_id() { return session_id_; }
quint64 user_id() { return user_id_; }
QString country_code() { return country_code_; }
const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); }
const bool authenticated() { return (!access_token_.isEmpty() || !session_id_.isEmpty()); }
const bool login_sent() { return login_sent_; }
const bool login_attempts() { return login_attempts_; }
@ -125,6 +130,9 @@ class TidalService : public InternetService {
void ResetSongsRequest();
private slots:
void StartAuthorisation();
void AuthorisationUrlReceived(const QUrl &url);
void AccessTokenRequestFinished(QNetworkReply *reply);
void SendLogin();
void HandleAuthReply(QNetworkReply *reply);
void ResetLoginAttempts();
@ -141,12 +149,18 @@ class TidalService : public InternetService {
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
void LoadSessionID();
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
void SendSearch();
QString LoginError(QString error, QVariant debug = QVariant());
static const char *kAuthUrl;
static const char *kClientIdB64;
static const char *kApiTokenB64;
static const char *kOAuthUrl;
static const char *kOAuthAccessTokenUrl;
static const char *kOAuthRedirectUrl;
static const char *kAuthUrl;
static const int kLoginAttempts;
static const int kTimeResetLoginAttempts;
@ -183,7 +197,11 @@ class TidalService : public InternetService {
std::shared_ptr<TidalRequest> search_request_;
TidalFavoriteRequest *favorite_request_;
QString token_;
bool oauth_;
QString client_id_;
QString api_token_;
quint64 user_id_;
QString country_code_;
QString username_;
QString password_;
QString quality_;
@ -194,10 +212,12 @@ class TidalService : public InternetService {
bool fetchalbums_;
QString coversize_;
bool cache_album_covers_;
TidalSettingsPage::StreamUrlMethod stream_url_method_;
QString access_token_;
QString refresh_token_;
QString session_id_;
quint64 user_id_;
QString country_code_;
QDateTime expiry_time_;
int pending_search_id_;
int next_pending_search_id_;

View File

@ -20,20 +20,29 @@
#include "config.h"
#include <QObject>
#include <QStandardPaths>
#include <QMimeDatabase>
#include <QFile>
#include <QDir>
#include <QList>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QJsonValue>
#include <QJsonObject>
#include <QXmlStreamReader>
#include "core/logging.h"
#include "core/network.h"
#include "core/song.h"
#include "settings/tidalsettingspage.h"
#include "tidalservice.h"
#include "tidalbaserequest.h"
#include "tidalstreamurlrequest.h"
TidalStreamURLRequest::TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent)
: TidalBaseRequest(service, network, parent),
service_(service),
reply_(nullptr),
original_url_(original_url),
song_id_(original_url.path().toInt()),
@ -67,6 +76,10 @@ void TidalStreamURLRequest::LoginComplete(bool success, QString error) {
void TidalStreamURLRequest::Process() {
if (!authenticated()) {
if (oauth()) {
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, tr("Not authenticated."));
return;
}
need_login_ = true;
emit TryLogin();
return;
@ -81,7 +94,7 @@ void TidalStreamURLRequest::Cancel() {
reply_->abort();
}
else {
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, "Cancelled.");
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, tr("Cancelled."));
}
}
@ -90,16 +103,36 @@ void TidalStreamURLRequest::GetStreamURL() {
++tries_;
ParamList parameters;
parameters << Param("soundQuality", quality());
if (reply_) {
disconnect(reply_, 0, nullptr, 0);
if (reply_->isRunning()) reply_->abort();
reply_->deleteLater();
}
reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), parameters);
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
ParamList params;
switch (stream_url_method()) {
case TidalSettingsPage::StreamUrlMethod_StreamUrl:
params << Param("soundQuality", quality());
reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), params);
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
break;
case TidalSettingsPage::StreamUrlMethod_UrlPostPaywall:
params << Param("audioquality", quality());
params << Param("playbackmode", "STREAM");
params << Param("assetpresentation", "FULL");
params << Param("urlusagemode", "STREAM");
reply_ = CreateRequest(QString("tracks/%1/urlpostpaywall").arg(song_id_), params);
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
break;
case TidalSettingsPage::StreamUrlMethod_PlaybackInfoPostPaywall:
params << Param("audioquality", quality());
params << Param("playbackmode", "STREAM");
params << Param("assetpresentation", "FULL");
reply_ = CreateRequest(QString("tracks/%1/playbackinfopostpaywall").arg(song_id_), params);
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
break;
}
}
@ -123,26 +156,125 @@ void TidalStreamURLRequest::StreamURLReceived() {
}
reply_ = nullptr;
qLog(Debug) << "Tidal:" << data;
QJsonObject json_obj = ExtractJsonObj(data, error);
if (json_obj.isEmpty()) {
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
if (!json_obj.contains("url") || !json_obj.contains("codec")) {
error = Error("Invalid Json reply, stream missing url or codec.", json_obj);
if (!json_obj.contains("trackId")) {
error = Error("Invalid Json reply, stream missing trackId.", json_obj);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
int track_id(json_obj["trackId"].toInt());
if (track_id != song_id_) {
error = Error("Incorrect track ID returned.", json_obj);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
QUrl new_url(json_obj["url"].toString());
QString codec(json_obj["codec"].toString().toLower());
Song::FileType filetype(Song::FiletypeByExtension(codec));
if (filetype == Song::FileType_Unknown) {
qLog(Debug) << "Tidal: Unknown codec" << codec;
filetype = Song::FileType_Stream;
Song::FileType filetype(Song::FileType_Unknown);
if (json_obj.contains("codec") || json_obj.contains("codecs")) {
QString codec;
if (json_obj.contains("codec")) codec = json_obj["codec"].toString().toLower();
if (json_obj.contains("codecs")) codec = json_obj["codecs"].toString().toLower();
filetype = Song::FiletypeByExtension(codec);
if (filetype == Song::FileType_Unknown) {
qLog(Debug) << "Tidal: Unknown codec" << codec;
filetype = Song::FileType_Stream;
}
}
emit StreamURLFinished(original_url_, new_url, filetype, QString());
QList<QUrl> urls;
if (json_obj.contains("manifest")) {
QString manifest(json_obj["manifest"].toString());
QByteArray data_manifest = QByteArray::fromBase64(manifest.toUtf8());
qLog(Debug) << "Tidal:" << data_manifest;
QXmlStreamReader xml_reader(data_manifest);
if (!xml_reader.hasError()) {
QString filepath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/tidalstreams";
QString filename = "tidal-" + QString::number(song_id_) + ".xml";
if (!QDir().mkpath(filepath)) {
error = Error(QString("Failed to create directory %1.").arg(filepath), json_obj);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
QUrl url("file://" + filepath + "/" + filename);
QFile file(url.toLocalFile());
if (file.exists())
file.remove();
if (!file.open(QIODevice::WriteOnly)) {
error = Error(QString("Failed to open file %1 for writing.").arg(url.toLocalFile()), json_obj);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
file.write(data_manifest);
file.close();
urls << url;
}
else {
json_obj = ExtractJsonObj(data_manifest, error);
if (json_obj.isEmpty()) {
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
if (!json_obj.contains("mimeType")) {
error = Error("Invalid Json reply, stream url reply manifest is missing mimeType.", json_obj);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
QString mimetype = json_obj["mimeType"].toString();
QMimeDatabase mimedb;
for (QString suffix : mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes()) {
filetype = Song::FiletypeByExtension(suffix);
if (filetype != Song::FileType_Unknown) break;
}
if (filetype == Song::FileType_Unknown) {
qLog(Debug) << "Tidal: Unknown mimetype" << mimetype;
filetype = Song::FileType_Stream;
}
}
}
if (json_obj.contains("urls")) {
QJsonValue json_urls = json_obj["urls"];
if (!json_urls.isArray()) {
error = Error("Invalid Json reply, urls is not an array.", json_urls);
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
return;
}
QJsonArray json_array_urls = json_urls.toArray();
for (const QJsonValue &value : json_array_urls) {
urls << QUrl(value.toString());
}
}
else if (json_obj.contains("url")) {
QUrl new_url(json_obj["url"].toString());
urls << new_url;
}
if (urls.isEmpty()) {
error = Error("Missing stream urls.", json_obj);
emit StreamURLFinished(original_url_, original_url_, filetype);
return;
}
emit StreamURLFinished(original_url_, urls.first(), filetype);
}

View File

@ -27,6 +27,7 @@
#include "core/song.h"
#include "tidalbaserequest.h"
#include "settings/tidalsettingspage.h"
class QNetworkReply;
class NetworkAccessManager;
@ -44,6 +45,8 @@ class TidalStreamURLRequest : public TidalBaseRequest {
void NeedLogin() { need_login_ = true; }
void Cancel();
const bool oauth() { return service_->oauth(); }
TidalSettingsPage::StreamUrlMethod stream_url_method() { return service_->stream_url_method(); }
QUrl original_url() { return original_url_; }
int song_id() { return song_id_; }
bool need_login() { return need_login_; }
@ -57,6 +60,7 @@ class TidalStreamURLRequest : public TidalBaseRequest {
void StreamURLReceived();
private:
TidalService *service_;
QNetworkReply *reply_;
QUrl original_url_;
int song_id_;