1 /*
2 * Strawberry Music Player
3 * Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
4 *
5 * Strawberry is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * Strawberry is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 #include "config.h"
21
22 #include <algorithm>
23
24 #include <QObject>
25 #include <QMimeDatabase>
26 #include <QMimeType>
27 #include <QPair>
28 #include <QList>
29 #include <QByteArray>
30 #include <QString>
31 #include <QChar>
32 #include <QUrl>
33 #include <QDateTime>
34 #include <QNetworkReply>
35 #include <QCryptographicHash>
36 #include <QJsonValue>
37 #include <QJsonObject>
38 #include <QtDebug>
39
40 #include "core/logging.h"
41 #include "core/networkaccessmanager.h"
42 #include "core/song.h"
43 #include "core/timeconstants.h"
44 #include "qobuzservice.h"
45 #include "qobuzbaserequest.h"
46 #include "qobuzstreamurlrequest.h"
47
QobuzStreamURLRequest(QobuzService * service,NetworkAccessManager * network,const QUrl & original_url,const int id,QObject * parent)48 QobuzStreamURLRequest::QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, const int id, QObject *parent)
49 : QobuzBaseRequest(service, network, parent),
50 service_(service),
51 reply_(nullptr),
52 original_url_(original_url),
53 id_(id),
54 song_id_(original_url.path().toInt()),
55 tries_(0),
56 need_login_(false) {}
57
~QobuzStreamURLRequest()58 QobuzStreamURLRequest::~QobuzStreamURLRequest() {
59
60 if (reply_) {
61 QObject::disconnect(reply_, nullptr, this, nullptr);
62 if (reply_->isRunning()) reply_->abort();
63 reply_->deleteLater();
64 }
65
66 }
67
LoginComplete(const bool success,const QString & error)68 void QobuzStreamURLRequest::LoginComplete(const bool success, const QString &error) {
69
70 if (!need_login_) return;
71 need_login_ = false;
72
73 if (!success) {
74 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error);
75 return;
76 }
77
78 Process();
79
80 }
81
Process()82 void QobuzStreamURLRequest::Process() {
83
84 if (app_id().isEmpty() || app_secret().isEmpty()) {
85 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret."));
86 return;
87 }
88
89 if (!authenticated()) {
90 need_login_ = true;
91 emit TryLogin();
92 return;
93 }
94 GetStreamURL();
95
96 }
97
Cancel()98 void QobuzStreamURLRequest::Cancel() {
99
100 if (reply_ && reply_->isRunning()) {
101 reply_->abort();
102 }
103 else {
104 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled."));
105 }
106
107 }
108
GetStreamURL()109 void QobuzStreamURLRequest::GetStreamURL() {
110
111 ++tries_;
112
113 if (reply_) {
114 QObject::disconnect(reply_, nullptr, this, nullptr);
115 if (reply_->isRunning()) reply_->abort();
116 reply_->deleteLater();
117 }
118
119 #if 0
120 QByteArray appid = app_id().toUtf8();
121 QByteArray secret_decoded = QByteArray::fromBase64(app_secret().toUtf8());
122 QString secret;
123 for (int x = 0, y = 0; x < secret_decoded.length(); ++x , ++y) {
124 if (y == appid.length()) y = 0;
125 secret.append(QChar(secret_decoded[x] ^ appid[y]));
126 }
127 #endif
128
129 QString secret = app_secret();
130 quint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
131
132 ParamList params_to_sign = ParamList() << Param("format_id", QString::number(format()))
133 << Param("track_id", QString::number(song_id_));
134
135 std::sort(params_to_sign.begin(), params_to_sign.end());
136
137 QString data_to_sign;
138 data_to_sign += "trackgetFileUrl";
139 for (const Param ¶m : params_to_sign) {
140 data_to_sign += param.first + param.second;
141 }
142 data_to_sign += QString::number(timestamp);
143 data_to_sign += secret.toUtf8();
144
145 QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
146 QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
147
148 ParamList params = params_to_sign;
149 params << Param("request_ts", QString::number(timestamp));
150 params << Param("request_sig", signature);
151 params << Param("user_auth_token", user_auth_token());
152
153 std::sort(params.begin(), params.end());
154
155 reply_ = CreateRequest(QString("track/getFileUrl"), params);
156 QObject::connect(reply_, &QNetworkReply::finished, this, &QobuzStreamURLRequest::StreamURLReceived);
157
158 }
159
StreamURLReceived()160 void QobuzStreamURLRequest::StreamURLReceived() {
161
162 if (!reply_) return;
163
164 QByteArray data = GetReplyData(reply_);
165
166 QObject::disconnect(reply_, nullptr, this, nullptr);
167 reply_->deleteLater();
168 reply_ = nullptr;
169
170 if (data.isEmpty()) {
171 if (!authenticated() && login_sent() && tries_ <= 1) {
172 need_login_ = true;
173 return;
174 }
175 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
176 return;
177 }
178
179 QJsonObject json_obj = ExtractJsonObj(data);
180 if (json_obj.isEmpty()) {
181 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
182 return;
183 }
184
185 if (!json_obj.contains("track_id")) {
186 Error("Invalid Json reply, stream url is missing track_id.", json_obj);
187 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
188 return;
189 }
190
191 int track_id = json_obj["track_id"].toInt();
192 if (track_id != song_id_) {
193 Error("Incorrect track ID returned.", json_obj);
194 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
195 return;
196 }
197
198 if (!json_obj.contains("mime_type") || !json_obj.contains("url")) {
199 Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj);
200 emit StreamURLFinished(id_, original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
201 return;
202 }
203
204 QUrl url(json_obj["url"].toString());
205 QString mimetype = json_obj["mime_type"].toString();
206
207 Song::FileType filetype(Song::FileType_Unknown);
208 QMimeDatabase mimedb;
209 QStringList suffixes = mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes();
210 for (const QString &suffix : suffixes) {
211 filetype = Song::FiletypeByExtension(suffix);
212 if (filetype != Song::FileType_Unknown) break;
213 }
214 if (filetype == Song::FileType_Unknown) {
215 qLog(Debug) << "Qobuz: Unknown mimetype" << mimetype;
216 filetype = Song::FileType_Stream;
217 }
218
219 if (!url.isValid()) {
220 Error("Returned stream url is invalid.", json_obj);
221 emit StreamURLFinished(id_, original_url_, original_url_, filetype, -1, -1, -1, errors_.first());
222 return;
223 }
224
225 qint64 duration = -1;
226 if (json_obj.contains("duration")) {
227 duration = json_obj["duration"].toInt() * kNsecPerSec;
228 }
229 int samplerate = -1;
230 if (json_obj.contains("sampling_rate")) {
231 samplerate = static_cast<int>(json_obj["sampling_rate"].toDouble()) * 1000;
232 }
233 int bit_depth = -1;
234 if (json_obj.contains("bit_depth")) {
235 bit_depth = static_cast<int>(json_obj["bit_depth"].toDouble());
236 }
237
238 emit StreamURLFinished(id_, original_url_, url, filetype, samplerate, bit_depth, duration);
239
240 }
241
Error(const QString & error,const QVariant & debug)242 void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) {
243
244 if (!error.isEmpty()) {
245 qLog(Error) << "Qobuz:" << error;
246 errors_ << error;
247 }
248 if (debug.isValid()) qLog(Debug) << debug;
249
250 }
251