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 &param : 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