Add optional oauth authentication for tidal
This commit is contained in:
parent
85a0748ad9
commit
c0c1457073
@ -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
|
||||
|
@ -131,6 +131,8 @@ signals:
|
||||
|
||||
void IntroPointReached();
|
||||
|
||||
void AuthorisationUrlReceived(const QUrl &url);
|
||||
|
||||
private slots:
|
||||
void FilePathChanged(const QString& path);
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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"/>
|
||||
|
@ -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> ¶ms_provided) {
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
typedef QList<EncodedParam> EncodedParamList;
|
||||
|
||||
ParamList params = ParamList() << params_provided
|
||||
<< Param("sessionId", session_id())
|
||||
<< Param("countryCode", country_code());
|
||||
|
||||
QStringList query_items;
|
||||
@ -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();
|
||||
|
@ -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> ¶ms_provided);
|
||||
QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool send_login);
|
||||
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QByteArray &data, QString &error);
|
||||
QJsonValue ExtractItems(QJsonObject &json_obj, QString &error);
|
||||
|
||||
QString Error(QString error, QVariant debug = QVariant());
|
||||
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_;
|
||||
|
@ -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;
|
||||
|
@ -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"));
|
||||
|
@ -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_;
|
||||
|
||||
};
|
||||
|
||||
|
@ -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 ¶m : 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 ¶m : 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);
|
||||
|
@ -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_;
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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_;
|
||||
|
Loading…
x
Reference in New Issue
Block a user