Qobuz: Fix authentication and add automatic credential fetching
Qobuz API now requires intent=stream parameter for stream URL requests, and the app_secret must be extracted using the Spoofbuz decoding method from bundle.js rather than plain-text values. Changes: - Add intent=stream parameter to stream URL requests - Add QobuzCredentialFetcher class to extract credentials from web player - Add "Fetch Credentials" button to Qobuz settings page - Decode obfuscated app secrets using seed/timezone/info/extras method This fixes "Invalid Request Signature" errors that prevented playback.
This commit is contained in:
committed by
Jonas Kvinge
parent
2cd9498469
commit
2bb0dbada2
@@ -1463,6 +1463,7 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.cpp
|
src/qobuz/qobuzrequest.cpp
|
||||||
src/qobuz/qobuzstreamurlrequest.cpp
|
src/qobuz/qobuzstreamurlrequest.cpp
|
||||||
src/qobuz/qobuzfavoriterequest.cpp
|
src/qobuz/qobuzfavoriterequest.cpp
|
||||||
|
src/qobuz/qobuzcredentialfetcher.cpp
|
||||||
src/settings/qobuzsettingspage.cpp
|
src/settings/qobuzsettingspage.cpp
|
||||||
src/covermanager/qobuzcoverprovider.cpp
|
src/covermanager/qobuzcoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1472,6 +1473,7 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.h
|
src/qobuz/qobuzrequest.h
|
||||||
src/qobuz/qobuzstreamurlrequest.h
|
src/qobuz/qobuzstreamurlrequest.h
|
||||||
src/qobuz/qobuzfavoriterequest.h
|
src/qobuz/qobuzfavoriterequest.h
|
||||||
|
src/qobuz/qobuzcredentialfetcher.h
|
||||||
src/settings/qobuzsettingspage.h
|
src/settings/qobuzsettingspage.h
|
||||||
src/covermanager/qobuzcoverprovider.h
|
src/covermanager/qobuzcoverprovider.h
|
||||||
UI
|
UI
|
||||||
|
|||||||
277
src/qobuz/qobuzcredentialfetcher.cpp
Normal file
277
src/qobuz/qobuzcredentialfetcher.cpp
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QRegularExpressionMatch>
|
||||||
|
#include <QRegularExpressionMatchIterator>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "qobuzcredentialfetcher.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char kLoginPageUrl[] = "https://play.qobuz.com/login";
|
||||||
|
constexpr char kPlayQobuzUrl[] = "https://play.qobuz.com";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QobuzCredentialFetcher::QobuzCredentialFetcher(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
network_(network),
|
||||||
|
login_page_reply_(nullptr),
|
||||||
|
bundle_reply_(nullptr) {}
|
||||||
|
|
||||||
|
void QobuzCredentialFetcher::FetchCredentials() {
|
||||||
|
|
||||||
|
qLog(Debug) << "Qobuz: Fetching credentials from web player";
|
||||||
|
|
||||||
|
QNetworkRequest request(QUrl(QString::fromLatin1(kLoginPageUrl)));
|
||||||
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"_s);
|
||||||
|
|
||||||
|
login_page_reply_ = network_->get(request);
|
||||||
|
QObject::connect(login_page_reply_, &QNetworkReply::finished, this, &QobuzCredentialFetcher::LoginPageReceived);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void QobuzCredentialFetcher::LoginPageReceived() {
|
||||||
|
|
||||||
|
if (!login_page_reply_) return;
|
||||||
|
|
||||||
|
QNetworkReply *reply = login_page_reply_;
|
||||||
|
login_page_reply_ = nullptr;
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
QString error = QStringLiteral("Failed to fetch login page: %1").arg(reply->errorString());
|
||||||
|
qLog(Error) << "Qobuz:" << error;
|
||||||
|
reply->deleteLater();
|
||||||
|
Q_EMIT CredentialsFetchError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString login_page = QString::fromUtf8(reply->readAll());
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
// Extract bundle.js URL from the login page
|
||||||
|
// Pattern: <script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>
|
||||||
|
static const QRegularExpression bundle_url_regex(u"<script src=\"(/resources/[\\d.]+-[a-z]\\d+/bundle\\.js)\"></script>"_s);
|
||||||
|
const QRegularExpressionMatch bundle_match = bundle_url_regex.match(login_page);
|
||||||
|
|
||||||
|
if (!bundle_match.hasMatch()) {
|
||||||
|
QString error = u"Failed to find bundle.js URL in login page"_s;
|
||||||
|
qLog(Error) << "Qobuz:" << error;
|
||||||
|
Q_EMIT CredentialsFetchError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_url_ = bundle_match.captured(1);
|
||||||
|
qLog(Debug) << "Qobuz: Found bundle URL:" << bundle_url_;
|
||||||
|
|
||||||
|
// Fetch the bundle.js
|
||||||
|
QNetworkRequest request(QUrl(QString::fromLatin1(kPlayQobuzUrl) + bundle_url_));
|
||||||
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"_s);
|
||||||
|
|
||||||
|
bundle_reply_ = network_->get(request);
|
||||||
|
QObject::connect(bundle_reply_, &QNetworkReply::finished, this, &QobuzCredentialFetcher::BundleReceived);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void QobuzCredentialFetcher::BundleReceived() {
|
||||||
|
|
||||||
|
if (!bundle_reply_) return;
|
||||||
|
|
||||||
|
QNetworkReply *reply = bundle_reply_;
|
||||||
|
bundle_reply_ = nullptr;
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
QString error = QStringLiteral("Failed to fetch bundle.js: %1").arg(reply->errorString());
|
||||||
|
qLog(Error) << "Qobuz:" << error;
|
||||||
|
reply->deleteLater();
|
||||||
|
Q_EMIT CredentialsFetchError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString bundle = QString::fromUtf8(reply->readAll());
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
qLog(Debug) << "Qobuz: Bundle size:" << bundle.length();
|
||||||
|
|
||||||
|
const QString app_id = ExtractAppId(bundle);
|
||||||
|
if (app_id.isEmpty()) {
|
||||||
|
QString error = u"Failed to extract app_id from bundle"_s;
|
||||||
|
qLog(Error) << "Qobuz:" << error;
|
||||||
|
Q_EMIT CredentialsFetchError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString app_secret = ExtractAppSecret(bundle);
|
||||||
|
if (app_secret.isEmpty()) {
|
||||||
|
QString error = u"Failed to extract app_secret from bundle"_s;
|
||||||
|
qLog(Error) << "Qobuz:" << error;
|
||||||
|
Q_EMIT CredentialsFetchError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Debug) << "Qobuz: Successfully extracted credentials - app_id:" << app_id;
|
||||||
|
Q_EMIT CredentialsFetched(app_id, app_secret);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QobuzCredentialFetcher::ExtractAppId(const QString &bundle) {
|
||||||
|
|
||||||
|
// Pattern: production:{api:{appId:"(\d+)"
|
||||||
|
static const QRegularExpression app_id_regex(u"production:\\{api:\\{appId:\"(\\d+)\""_s);
|
||||||
|
const QRegularExpressionMatch app_id_match = app_id_regex.match(bundle);
|
||||||
|
|
||||||
|
if (app_id_match.hasMatch()) {
|
||||||
|
return app_id_match.captured(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QobuzCredentialFetcher::ExtractAppSecret(const QString &bundle) {
|
||||||
|
|
||||||
|
// The plain-text appSecret in the bundle doesn't work for API requests.
|
||||||
|
// We need to use the Spoofbuz method to extract the real secrets:
|
||||||
|
// 1. Find seed/timezone pairs
|
||||||
|
// 2. Find info/extras for each timezone
|
||||||
|
// 3. Combine seed + info + extras, remove last 44 chars, base64 decode
|
||||||
|
|
||||||
|
// Pattern to find seed and timezone: [a-z].initialSeed("seed",window.utimezone.timezone)
|
||||||
|
static const QRegularExpression seed_regex(u"[a-z]\\.initialSeed\\(\"([\\w=]+)\",window\\.utimezone\\.([a-z]+)\\)"_s);
|
||||||
|
|
||||||
|
QMap<QString, QString> seeds; // timezone -> seed
|
||||||
|
QRegularExpressionMatchIterator seed_iter = seed_regex.globalMatch(bundle);
|
||||||
|
while (seed_iter.hasNext()) {
|
||||||
|
const QRegularExpressionMatch seed_match = seed_iter.next();
|
||||||
|
const QString seed = seed_match.captured(1);
|
||||||
|
const QString tz = seed_match.captured(2);
|
||||||
|
seeds[tz] = seed;
|
||||||
|
qLog(Debug) << "Qobuz: Found seed for timezone" << tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seeds.isEmpty()) {
|
||||||
|
qLog(Error) << "Qobuz: No seed/timezone pairs found in bundle";
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try each timezone - Berlin was confirmed working
|
||||||
|
const QStringList preferred_order = {u"berlin"_s, u"london"_s, u"abidjan"_s};
|
||||||
|
|
||||||
|
for (const QString &tz : preferred_order) {
|
||||||
|
if (!seeds.contains(tz)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern to find info and extras for this timezone
|
||||||
|
// name:"xxx/Berlin",info:"...",extras:"..."
|
||||||
|
const QString capitalized_tz = tz.at(0).toUpper() + tz.mid(1);
|
||||||
|
const QString info_pattern = QStringLiteral("name:\"\\w+/%1\",info:\"([\\w=]+)\",extras:\"([\\w=]+)\"").arg(capitalized_tz);
|
||||||
|
const QRegularExpression info_regex(info_pattern);
|
||||||
|
const QRegularExpressionMatch info_match = info_regex.match(bundle);
|
||||||
|
|
||||||
|
if (!info_match.hasMatch()) {
|
||||||
|
qLog(Debug) << "Qobuz: No info/extras found for timezone" << tz;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString seed = seeds[tz];
|
||||||
|
const QString info = info_match.captured(1);
|
||||||
|
const QString extras = info_match.captured(2);
|
||||||
|
|
||||||
|
qLog(Debug) << "Qobuz: Decoding secret for timezone" << tz;
|
||||||
|
|
||||||
|
// Combine seed + info + extras
|
||||||
|
const QString combined = seed + info + extras;
|
||||||
|
|
||||||
|
// Remove last 44 characters
|
||||||
|
if (combined.length() <= 44) {
|
||||||
|
qLog(Debug) << "Qobuz: Combined string too short for timezone" << tz;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString trimmed = combined.left(combined.length() - 44);
|
||||||
|
|
||||||
|
// Base64 decode
|
||||||
|
const QByteArray decoded = QByteArray::fromBase64(trimmed.toLatin1());
|
||||||
|
const QString secret = QString::fromLatin1(decoded);
|
||||||
|
|
||||||
|
// Validate: should be 32 hex characters
|
||||||
|
static const QRegularExpression hex_regex(u"^[a-f0-9]{32}$"_s);
|
||||||
|
if (hex_regex.match(secret).hasMatch()) {
|
||||||
|
qLog(Debug) << "Qobuz: Successfully decoded secret from timezone" << tz;
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Debug) << "Qobuz: Decoded secret invalid for timezone" << tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try any remaining timezones not in preferred order
|
||||||
|
for (auto it = seeds.constBegin(); it != seeds.constEnd(); ++it) {
|
||||||
|
const QString &tz = it.key();
|
||||||
|
if (preferred_order.contains(tz)) {
|
||||||
|
continue; // Already tried
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString capitalized_tz = tz.at(0).toUpper() + tz.mid(1);
|
||||||
|
const QString info_pattern = QStringLiteral("name:\"\\w+/%1\",info:\"([\\w=]+)\",extras:\"([\\w=]+)\"").arg(capitalized_tz);
|
||||||
|
const QRegularExpression info_regex(info_pattern);
|
||||||
|
const QRegularExpressionMatch info_match = info_regex.match(bundle);
|
||||||
|
|
||||||
|
if (!info_match.hasMatch()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString seed = it.value();
|
||||||
|
const QString info = info_match.captured(1);
|
||||||
|
const QString extras = info_match.captured(2);
|
||||||
|
|
||||||
|
const QString combined = seed + info + extras;
|
||||||
|
if (combined.length() <= 44) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString trimmed = combined.left(combined.length() - 44);
|
||||||
|
|
||||||
|
const QByteArray decoded = QByteArray::fromBase64(trimmed.toLatin1());
|
||||||
|
const QString secret = QString::fromLatin1(decoded);
|
||||||
|
|
||||||
|
static const QRegularExpression hex_regex(u"^[a-f0-9]{32}$"_s);
|
||||||
|
if (hex_regex.match(secret).hasMatch()) {
|
||||||
|
qLog(Debug) << "Qobuz: Successfully decoded secret from timezone" << tz;
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Error) << "Qobuz: Failed to decode any valid app_secret from bundle";
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
}
|
||||||
59
src/qobuz/qobuzcredentialfetcher.h
Normal file
59
src/qobuz/qobuzcredentialfetcher.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef QOBUZCREDENTIALFETCHER_H
|
||||||
|
#define QOBUZCREDENTIALFETCHER_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
|
||||||
|
class QobuzCredentialFetcher : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit QobuzCredentialFetcher(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void FetchCredentials();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void CredentialsFetched(const QString &app_id, const QString &app_secret);
|
||||||
|
void CredentialsFetchError(const QString &error);
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void LoginPageReceived();
|
||||||
|
void BundleReceived();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString ExtractAppId(const QString &bundle);
|
||||||
|
QString ExtractAppSecret(const QString &bundle);
|
||||||
|
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
QNetworkReply *login_page_reply_;
|
||||||
|
QNetworkReply *bundle_reply_;
|
||||||
|
QString bundle_url_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // QOBUZCREDENTIALFETCHER_H
|
||||||
@@ -106,6 +106,8 @@ class QobuzService : public StreamingService {
|
|||||||
bool login_sent() const { return login_sent_; }
|
bool login_sent() const { return login_sent_; }
|
||||||
bool login_attempts() const { return login_attempts_; }
|
bool login_attempts() const { return login_attempts_; }
|
||||||
|
|
||||||
|
SharedPtr<NetworkAccessManager> network() const { return network_; }
|
||||||
|
|
||||||
uint GetStreamURL(const QUrl &url, QString &error);
|
uint GetStreamURL(const QUrl &url, QString &error);
|
||||||
|
|
||||||
SharedPtr<CollectionBackend> artists_collection_backend() override { return artists_collection_backend_; }
|
SharedPtr<CollectionBackend> artists_collection_backend() override { return artists_collection_backend_; }
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ void QobuzStreamURLRequest::GetStreamURL() {
|
|||||||
const quint64 timestamp = static_cast<quint64>(QDateTime::currentSecsSinceEpoch());
|
const quint64 timestamp = static_cast<quint64>(QDateTime::currentSecsSinceEpoch());
|
||||||
|
|
||||||
ParamList params_to_sign = ParamList() << Param(u"format_id"_s, QString::number(service_->format()))
|
ParamList params_to_sign = ParamList() << Param(u"format_id"_s, QString::number(service_->format()))
|
||||||
|
<< Param(u"intent"_s, u"stream"_s)
|
||||||
<< Param(u"track_id"_s, QString::number(song_id_));
|
<< Param(u"track_id"_s, QString::number(song_id_));
|
||||||
|
|
||||||
std::sort(params_to_sign.begin(), params_to_sign.end());
|
std::sort(params_to_sign.begin(), params_to_sign.end());
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "widgets/loginstatewidget.h"
|
#include "widgets/loginstatewidget.h"
|
||||||
#include "qobuz/qobuzservice.h"
|
#include "qobuz/qobuzservice.h"
|
||||||
|
#include "qobuz/qobuzcredentialfetcher.h"
|
||||||
#include "constants/qobuzsettings.h"
|
#include "constants/qobuzsettings.h"
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
@@ -46,13 +47,15 @@ using namespace QobuzSettings;
|
|||||||
QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *dialog, const SharedPtr<QobuzService> service, QWidget *parent)
|
QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *dialog, const SharedPtr<QobuzService> service, QWidget *parent)
|
||||||
: SettingsPage(dialog, parent),
|
: SettingsPage(dialog, parent),
|
||||||
ui_(new Ui::QobuzSettingsPage),
|
ui_(new Ui::QobuzSettingsPage),
|
||||||
service_(service) {
|
service_(service),
|
||||||
|
credential_fetcher_(nullptr) {
|
||||||
|
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
setWindowIcon(IconLoader::Load(u"qobuz"_s, true, 0, 32));
|
setWindowIcon(IconLoader::Load(u"qobuz"_s, true, 0, 32));
|
||||||
|
|
||||||
QObject::connect(ui_->button_login, &QPushButton::clicked, this, &QobuzSettingsPage::LoginClicked);
|
QObject::connect(ui_->button_login, &QPushButton::clicked, this, &QobuzSettingsPage::LoginClicked);
|
||||||
QObject::connect(ui_->login_state, &LoginStateWidget::LogoutClicked, this, &QobuzSettingsPage::LogoutClicked);
|
QObject::connect(ui_->login_state, &LoginStateWidget::LogoutClicked, this, &QobuzSettingsPage::LogoutClicked);
|
||||||
|
QObject::connect(ui_->button_fetch_credentials, &QPushButton::clicked, this, &QobuzSettingsPage::FetchCredentialsClicked);
|
||||||
|
|
||||||
QObject::connect(this, &QobuzSettingsPage::Login, &*service_, &StreamingService::LoginWithCredentials);
|
QObject::connect(this, &QobuzSettingsPage::Login, &*service_, &StreamingService::LoginWithCredentials);
|
||||||
|
|
||||||
@@ -186,3 +189,40 @@ void QobuzSettingsPage::LoginFailure(const QString &failure_reason) {
|
|||||||
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
|
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QobuzSettingsPage::FetchCredentialsClicked() {
|
||||||
|
|
||||||
|
ui_->button_fetch_credentials->setEnabled(false);
|
||||||
|
ui_->button_fetch_credentials->setText(tr("Fetching..."));
|
||||||
|
|
||||||
|
if (!credential_fetcher_) {
|
||||||
|
credential_fetcher_ = new QobuzCredentialFetcher(service_->network(), this);
|
||||||
|
QObject::connect(credential_fetcher_, &QobuzCredentialFetcher::CredentialsFetched, this, &QobuzSettingsPage::CredentialsFetched);
|
||||||
|
QObject::connect(credential_fetcher_, &QobuzCredentialFetcher::CredentialsFetchError, this, &QobuzSettingsPage::CredentialsFetchError);
|
||||||
|
}
|
||||||
|
|
||||||
|
credential_fetcher_->FetchCredentials();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void QobuzSettingsPage::CredentialsFetched(const QString &app_id, const QString &app_secret) {
|
||||||
|
|
||||||
|
ui_->app_id->setText(app_id);
|
||||||
|
ui_->app_secret->setText(app_secret);
|
||||||
|
ui_->checkbox_base64_secret->setChecked(false);
|
||||||
|
|
||||||
|
ui_->button_fetch_credentials->setEnabled(true);
|
||||||
|
ui_->button_fetch_credentials->setText(tr("Fetch Credentials"));
|
||||||
|
|
||||||
|
QMessageBox::information(this, tr("Credentials fetched"), tr("App ID and secret have been successfully fetched from the Qobuz web player."));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void QobuzSettingsPage::CredentialsFetchError(const QString &error) {
|
||||||
|
|
||||||
|
ui_->button_fetch_credentials->setEnabled(true);
|
||||||
|
ui_->button_fetch_credentials->setText(tr("Fetch Credentials"));
|
||||||
|
|
||||||
|
QMessageBox::warning(this, tr("Credential fetch failed"), error);
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class QShowEvent;
|
|||||||
class QEvent;
|
class QEvent;
|
||||||
class SettingsDialog;
|
class SettingsDialog;
|
||||||
class QobuzService;
|
class QobuzService;
|
||||||
|
class QobuzCredentialFetcher;
|
||||||
class Ui_QobuzSettingsPage;
|
class Ui_QobuzSettingsPage;
|
||||||
|
|
||||||
class QobuzSettingsPage : public SettingsPage {
|
class QobuzSettingsPage : public SettingsPage {
|
||||||
@@ -55,10 +56,14 @@ class QobuzSettingsPage : public SettingsPage {
|
|||||||
void LogoutClicked();
|
void LogoutClicked();
|
||||||
void LoginSuccess();
|
void LoginSuccess();
|
||||||
void LoginFailure(const QString &failure_reason);
|
void LoginFailure(const QString &failure_reason);
|
||||||
|
void FetchCredentialsClicked();
|
||||||
|
void CredentialsFetched(const QString &app_id, const QString &app_secret);
|
||||||
|
void CredentialsFetchError(const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui_QobuzSettingsPage *ui_;
|
Ui_QobuzSettingsPage *ui_;
|
||||||
const SharedPtr<QobuzService> service_;
|
const SharedPtr<QobuzService> service_;
|
||||||
|
QobuzCredentialFetcher *credential_fetcher_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // QOBUZSETTINGSPAGE_H
|
#endif // QOBUZSETTINGSPAGE_H
|
||||||
|
|||||||
@@ -115,6 +115,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QPushButton" name="button_fetch_credentials">
|
||||||
|
<property name="text">
|
||||||
|
<string>Fetch Credentials</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Automatically fetch app ID and secret from Qobuz web player</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user