Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "audiocomservice.h"
24 :
25 : #include <QJsonDocument>
26 : #include <QJsonObject>
27 : #include <QBuffer>
28 : #include <QHttpMultiPart>
29 :
30 : #include "async/async.h"
31 : #include "containers.h"
32 :
33 : #include "clouderrors.h"
34 :
35 : #include "defer.h"
36 : #include "log.h"
37 :
38 : using namespace muse;
39 : using namespace muse::cloud;
40 : using namespace muse::network;
41 : using namespace muse::async;
42 :
43 : static const QString AUDIOCOM_CLOUD_TITLE("Audio.com");
44 : static const QString AUDIOCOM_CLOUD_URL("https://audio.com");
45 : static const QString AUDIOCOM_API_ROOT_URL("https://api.audio.com");
46 : static const QUrl AUDIOCOM_USER_INFO_API_URL(AUDIOCOM_API_ROOT_URL + "/me");
47 :
48 : static const QUrl AUDIOCOM_UPLOAD_AUDIO_API_URL(AUDIOCOM_API_ROOT_URL + "/audio");
49 :
50 : static const QString AUDIOCOM_LOGO_URL(AUDIOCOM_CLOUD_URL + "/img/mu-app-logo.svg");
51 :
52 0 : static QString audioMime(const QString& audioFormat)
53 : {
54 0 : if (audioFormat == "mp3") {
55 0 : return "audio/mpeg";
56 : }
57 :
58 0 : return "audio/x-wav";
59 : }
60 :
61 0 : static RetVal<AccountInfo> parseAudioComAccountInfo(const QByteArray& data)
62 : {
63 0 : QJsonParseError err;
64 0 : QJsonDocument doc = QJsonDocument::fromJson(data, &err);
65 0 : if (err.error != QJsonParseError::NoError || !doc.isObject()) {
66 0 : return muse::make_ret(Ret::Code::InternalError, err.errorString().toStdString());
67 : }
68 :
69 0 : QJsonObject user = doc.object();
70 0 : QString profileUrl = AUDIOCOM_CLOUD_URL + "/" + user.value("username").toString();
71 :
72 0 : AccountInfo info;
73 0 : info.id = user.value("id").toString();
74 0 : info.userName = user.value("profile").toObject().value("name").toString();
75 0 : info.profileUrl = QUrl(profileUrl);
76 0 : info.collectionUrl = info.profileUrl;
77 0 : info.avatarUrl = QUrl(user.value("avatar").toString());
78 :
79 0 : return RetVal<AccountInfo>::make_ok(info);
80 0 : }
81 :
82 0 : AudioComService::AudioComService(const modularity::ContextPtr& iocCtx, QObject* parent)
83 0 : : AbstractCloudService(iocCtx, parent)
84 : {
85 0 : }
86 :
87 0 : IAuthorizationServicePtr AudioComService::authorization()
88 : {
89 0 : return shared_from_this();
90 : }
91 :
92 0 : CloudInfo AudioComService::cloudInfo() const
93 : {
94 : return {
95 : AUDIO_COM_CLOUD_CODE,
96 : AUDIOCOM_CLOUD_TITLE,
97 : AUDIOCOM_CLOUD_URL,
98 : AUDIOCOM_LOGO_URL,
99 : logoColor()
100 0 : };
101 0 : }
102 :
103 0 : QUrl AudioComService::projectManagerUrl() const
104 : {
105 0 : return accountInfo().profileUrl.toString() + "/projects";
106 : }
107 :
108 0 : AbstractCloudService::ServerConfig AudioComService::serverConfig() const
109 : {
110 0 : ServerConfig serverConfig;
111 0 : serverConfig.serverCode = AUDIO_COM_CLOUD_CODE;
112 0 : serverConfig.serverUrl = AUDIOCOM_CLOUD_URL;
113 :
114 0 : serverConfig.serverAvailabilityUrl = AUDIOCOM_API_ROOT_URL + "/system/healthcheck";
115 :
116 0 : serverConfig.authorizationUrl = AUDIOCOM_CLOUD_URL + "/auth/sign-in";
117 0 : serverConfig.signUpUrl = AUDIOCOM_CLOUD_URL + "/auth/sign-up";
118 0 : serverConfig.signInSuccessUrl = AUDIOCOM_CLOUD_URL + "/my-audio?muAuthSuccess=true";
119 :
120 0 : serverConfig.accessTokenUrl = AUDIOCOM_API_ROOT_URL + "/auth/token";
121 0 : serverConfig.refreshApiUrl = AUDIOCOM_API_ROOT_URL + "/auth/token";
122 :
123 0 : serverConfig.headers = headers();
124 :
125 0 : static const QString CLIENT_ID = "1760627154191472";
126 0 : static const QString CLIENT_SECRET = "0qLugLJwBXsm8e-fOd3WgFx6TOTQkw74";
127 :
128 : serverConfig.authorizationParameters = {
129 : { "client_id", CLIENT_ID },
130 : { "client_secret", CLIENT_SECRET },
131 : { "grant_type", "authorization_code" },
132 : { "scope", "all" }
133 0 : };
134 :
135 : serverConfig.refreshParameters = {
136 : { "client_id", CLIENT_ID },
137 : { "client_secret", CLIENT_SECRET },
138 : { "grant_type", "refresh_token" }
139 0 : };
140 :
141 0 : return serverConfig;
142 0 : }
143 :
144 0 : RequestHeaders AudioComService::headers(const QString& token) const
145 : {
146 0 : RequestHeaders headers = defaultHeaders();
147 0 : headers.rawHeaders["Accept"] = "application/json";
148 0 : headers.rawHeaders["Content-Type"] = "application/json";
149 0 : headers.rawHeaders["Authorization"] = QString("Bearer " + (!token.isEmpty() ? token : accessToken())).toUtf8();
150 :
151 0 : return headers;
152 0 : }
153 :
154 0 : Promise<Ret> AudioComService::downloadAccountInfo()
155 : {
156 0 : TRACEFUNC;
157 :
158 0 : return make_promise<Ret>([this](auto resolve, auto) {
159 0 : auto receivedData = std::make_shared<QBuffer>();
160 0 : RetVal<Progress> progress = m_networkManager->get(AUDIOCOM_USER_INFO_API_URL, receivedData, headers());
161 0 : if (!progress.ret) {
162 0 : return resolve(progress.ret);
163 : }
164 :
165 0 : progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
166 0 : if (!res.ret) {
167 0 : printServerReply(*receivedData);
168 0 : (void)resolve(res.ret);
169 0 : return;
170 : }
171 :
172 0 : RetVal<AccountInfo> info = parseAudioComAccountInfo(receivedData->data());
173 0 : if (!info.ret) {
174 0 : (void)resolve(info.ret);
175 0 : return;
176 : }
177 :
178 0 : if (info.val.isValid()) {
179 0 : setAccountInfo(info.val);
180 : } else {
181 0 : setAccountInfo(AccountInfo());
182 : }
183 :
184 0 : (void)resolve(make_ok());
185 0 : });
186 :
187 0 : return Promise<Ret>::dummy_result();
188 0 : });
189 0 : }
190 :
191 0 : Promise<Ret> AudioComService::updateTokens()
192 : {
193 0 : TRACEFUNC;
194 :
195 0 : return make_promise<Ret>([this](auto resolve, auto) {
196 0 : ServerConfig serverConfig = this->serverConfig();
197 :
198 0 : QJsonObject json;
199 0 : json["refresh_token"] = refreshToken();
200 :
201 0 : for (const QString& key : serverConfig.authorizationParameters.keys()) {
202 0 : json.insert(key, serverConfig.refreshParameters.value(key).toString());
203 : }
204 :
205 0 : QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
206 0 : auto outgoingData = std::make_shared<QBuffer>();
207 0 : outgoingData->setData(jsonData);
208 0 : auto receivedData = std::make_shared<QBuffer>();
209 :
210 0 : RetVal<Progress> progress = m_networkManager->post(serverConfig.refreshApiUrl, outgoingData, receivedData, headers());
211 0 : if (!progress.ret) {
212 0 : return resolve(progress.ret);
213 : }
214 :
215 0 : progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
216 0 : if (!res.ret) {
217 0 : printServerReply(*receivedData);
218 0 : (void)resolve(res.ret);
219 0 : return;
220 : }
221 :
222 0 : QJsonParseError err;
223 0 : QJsonDocument doc = QJsonDocument::fromJson(receivedData->data(), &err);
224 0 : if (err.error != QJsonParseError::NoError || !doc.isObject()) {
225 0 : (void)resolve(Ret((int)Err::UnknownError, err.errorString().toStdString()));
226 0 : return;
227 : }
228 :
229 0 : QJsonObject tokens = doc.object();
230 0 : setAccessToken(tokens.value(ACCESS_TOKEN_KEY).toString());
231 0 : setRefreshToken(tokens.value(REFRESH_TOKEN_KEY).toString());
232 0 : (void)resolve(make_ok());
233 0 : });
234 :
235 0 : return Promise<Ret>::dummy_result();
236 0 : });
237 0 : }
238 :
239 0 : ProgressPtr AudioComService::uploadAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& existingUrl,
240 : Visibility visibility, bool replaceExisting)
241 : {
242 0 : ProgressPtr progress = std::make_shared<Progress>();
243 0 : progress->start();
244 :
245 0 : auto finishProgress = [this, progress](const Ret& ret) {
246 0 : ProgressResult result(ret);
247 :
248 0 : DEFER {
249 0 : m_currentUploadingAudioSlug.clear();
250 0 : m_currentUploadingAudioId.clear();
251 0 : m_currentUploadingAudioInfo = {};
252 0 : progress->finish(result);
253 0 : };
254 :
255 0 : if (!ret) {
256 0 : return;
257 : }
258 :
259 0 : ValMap audioMap;
260 0 : audioMap["editUrl"] = Val(QString("%2/audio/%3/edit").arg(
261 0 : accountInfo().collectionUrl.toString(), m_currentUploadingAudioSlug));
262 0 : audioMap["url"] = Val(AUDIOCOM_CLOUD_URL + "/audio/" + m_currentUploadingAudioId);
263 0 : result.val = Val(audioMap);
264 0 : };
265 :
266 0 : if (replaceExisting) {
267 0 : executeAsyncRequest([this, audioData, audioFormat, title, existingUrl, visibility, progress]() {
268 0 : return replaceExistingAudio(audioData, audioFormat, title, existingUrl, visibility, progress);
269 0 : }).onResolve(this, [finishProgress](const Ret& ret) {
270 0 : finishProgress(ret);
271 0 : });
272 : } else {
273 0 : executeAsyncRequest([this, audioData, audioFormat, title, existingUrl, visibility, progress]() {
274 0 : return uploadNewAudio(audioData, audioFormat, title, existingUrl, visibility, progress);
275 0 : }).onResolve(this, [finishProgress](const Ret& ret) {
276 0 : finishProgress(ret);
277 0 : });
278 : }
279 :
280 0 : return progress;
281 0 : }
282 :
283 0 : Promise<Ret> AudioComService::uploadNewAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& url,
284 : Visibility visibility, ProgressPtr progress)
285 : {
286 0 : std::weak_ptr<QIODevice> audioDataWeakPtr = audioData; // prevents memory leak
287 :
288 : // Create audio info -> upload audio
289 0 : return doCreateAudio(title, audioData->size(), audioFormat, url, visibility, false /*replaceExisting*/)
290 0 : .then<Ret>(this, [this, audioDataWeakPtr, audioFormat, progress](const Ret& ret, auto resolve) {
291 0 : DevicePtr audioData = audioDataWeakPtr.lock();
292 0 : if (!ret || !audioData) {
293 0 : return resolve(ret);
294 : }
295 :
296 0 : doUploadAudio(audioData, audioFormat, progress).onResolve(this, [resolve](const Ret& ret) {
297 0 : (void)resolve(ret);
298 : });
299 :
300 0 : return Promise<Ret>::dummy_result();
301 0 : });
302 0 : }
303 :
304 0 : Promise<Ret> AudioComService::replaceExistingAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& url,
305 : Visibility visibility, ProgressPtr progress)
306 : {
307 0 : std::weak_ptr<QIODevice> audioDataWeakPtr = audioData; // prevents memory leak
308 :
309 : // Update visibility -> update audio info -> upload audio
310 0 : return doUpdateVisibility(url, visibility).then<Ret>(this,
311 0 : [this, audioDataWeakPtr, audioFormat, title, url, visibility,
312 : progress](const Ret& ret, auto resolve) {
313 0 : DevicePtr audioData = audioDataWeakPtr.lock();
314 0 : if (!ret || !audioData) {
315 0 : return resolve(ret);
316 : }
317 :
318 0 : doCreateAudio(title, audioData->size(), audioFormat, url, visibility, true /*replaceExisting*/)
319 0 : .onResolve(this, [this, audioData, audioFormat, progress, resolve](const Ret& ret) {
320 0 : if (!ret) {
321 0 : (void)resolve(ret);
322 0 : return;
323 : }
324 :
325 0 : doUploadAudio(audioData, audioFormat, progress).onResolve(this, [resolve](const Ret& ret) {
326 0 : (void)resolve(ret);
327 : });
328 : });
329 :
330 0 : return Promise<Ret>::dummy_result();
331 0 : });
332 0 : }
333 :
334 0 : Promise<Ret> AudioComService::doUploadAudio(DevicePtr audioData, const QString& audioFormat, ProgressPtr progress)
335 : {
336 0 : TRACEFUNC;
337 :
338 0 : return make_promise<Ret>([this, audioData, audioFormat, progress](auto resolve, auto) {
339 0 : IF_FAILED(!m_currentUploadingAudioInfo.isEmpty()) {
340 0 : return resolve(make_ret(Err::UnknownError));
341 : }
342 :
343 0 : QUrl url = QUrl(m_currentUploadingAudioInfo.value("url").toString());
344 0 : QUrl success = QUrl(m_currentUploadingAudioInfo.value("success").toString());
345 0 : QUrl fail = QUrl(m_currentUploadingAudioInfo.value("fail").toString());
346 0 : QUrl progressUrl = QUrl(m_currentUploadingAudioInfo.value("progress").toString());
347 :
348 0 : if (!url.isValid() || !success.isValid() || !fail.isValid() || !progressUrl.isValid()) {
349 0 : return resolve(make_ret(Err::UnknownError));
350 : }
351 :
352 0 : audioData->seek(0);
353 :
354 0 : QString token;
355 :
356 0 : if (m_currentUploadingAudioInfo.contains("extra")) {
357 0 : QJsonObject extra = m_currentUploadingAudioInfo.value("extra").toObject();
358 0 : QJsonObject audio = extra.value("audio").toObject();
359 0 : m_currentUploadingAudioSlug = audio.value("slug").toString();
360 0 : m_currentUploadingAudioId = audio.value("id").toString();
361 0 : token = extra.value("token").toString();
362 0 : }
363 :
364 0 : RequestHeaders headers = defaultHeaders();
365 0 : headers.knownHeaders[QNetworkRequest::ContentTypeHeader] = audioMime(audioFormat);
366 :
367 0 : RetVal<Progress> putProgress = m_networkManager->put(url, audioData, nullptr, headers);
368 0 : if (!putProgress.ret) {
369 0 : return resolve(putProgress.ret);
370 : }
371 :
372 0 : putProgress.val.progressChanged().onReceive(this, [progress](int64_t current, int64_t total, const std::string& msg) {
373 0 : progress->progress(current, total, msg);
374 : });
375 :
376 0 : putProgress.val.finished().onReceive(this, [this, token, success, fail, resolve](const ProgressResult& res) {
377 0 : if (res.ret) {
378 0 : notifyServerAboutSuccessUpload(success, token);
379 0 : (void)resolve(make_ok());
380 : } else {
381 0 : notifyServerAboutFailUpload(fail, token);
382 0 : (void)resolve(uploadingDownloadingRetFromRawRet(res.ret));
383 : }
384 : });
385 :
386 0 : return Promise<Ret>::dummy_result();
387 0 : });
388 0 : }
389 :
390 0 : async::Promise<Ret> AudioComService::doUpdateVisibility(const QUrl& url, Visibility visibility)
391 : {
392 0 : return make_promise<Ret>([this, url, visibility](auto resolve, auto) {
393 0 : QUrl patchUrl(AUDIOCOM_API_ROOT_URL + "/audio/" + idFromCloudUrl(url).toQString());
394 :
395 0 : QJsonObject json;
396 0 : json["public"] = visibility == Visibility::Public;
397 0 : QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
398 :
399 0 : auto outgoingData = std::make_shared<QBuffer>();
400 0 : outgoingData->setData(jsonData);
401 :
402 0 : RetVal<Progress> progress = m_networkManager->patch(patchUrl, outgoingData, nullptr, headers());
403 0 : if (!progress.ret) {
404 0 : return resolve(progress.ret);
405 : }
406 :
407 0 : progress.val.finished().onReceive(this, [resolve](const ProgressResult& res) {
408 0 : (void)resolve(res.ret);
409 : });
410 :
411 0 : return Promise<Ret>::dummy_result();
412 0 : });
413 : }
414 :
415 0 : Promise<Ret> AudioComService::doCreateAudio(const QString& title, int size,
416 : const QString& audioFormat,
417 : const QUrl& existingUrl, Visibility visibility, bool replaceExisting)
418 : {
419 0 : TRACEFUNC;
420 :
421 0 : return make_promise<Ret>([this, title, size, audioFormat, existingUrl, visibility, replaceExisting](auto resolve, auto) {
422 0 : QJsonObject json;
423 0 : QString mime = audioMime(audioFormat);
424 0 : json["mime"] = mime;
425 0 : json["download_mime"] = mime;
426 0 : json["size"] = size;
427 0 : json["name"] = title;
428 0 : json["method"] = "PUT";
429 :
430 0 : QUrl postUrl;
431 0 : if (replaceExisting) {
432 0 : postUrl = QUrl(AUDIOCOM_API_ROOT_URL + "/audio/" + idFromCloudUrl(existingUrl).toQString() + "/source");
433 : } else {
434 0 : json["public"] = visibility == Visibility::Public;
435 0 : postUrl = AUDIOCOM_UPLOAD_AUDIO_API_URL;
436 : }
437 :
438 0 : QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
439 0 : auto outgoingData = std::make_shared<QBuffer>();
440 0 : outgoingData->setData(jsonData);
441 0 : auto receivedData = std::make_shared<QBuffer>();
442 :
443 0 : RetVal<Progress> progress = m_networkManager->post(postUrl, outgoingData, receivedData, headers());
444 0 : if (!progress.ret) {
445 0 : return resolve(progress.ret);
446 : }
447 :
448 0 : progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
449 0 : if (!res.ret) {
450 0 : (void)resolve(uploadingDownloadingRetFromRawRet(res.ret));
451 0 : return;
452 : }
453 :
454 0 : QJsonParseError err;
455 0 : QJsonDocument doc = QJsonDocument::fromJson(receivedData->data(), &err);
456 0 : if (err.error != QJsonParseError::NoError || !doc.isObject()) {
457 0 : (void)resolve(muse::make_ret(Ret::Code::InternalError, err.errorString().toStdString()));
458 0 : return;
459 : }
460 :
461 0 : m_currentUploadingAudioInfo = doc.object();
462 0 : (void)resolve(make_ok());
463 0 : });
464 :
465 0 : return Promise<Ret>::dummy_result();
466 0 : });
467 0 : }
468 :
469 0 : void AudioComService::notifyServerAboutFailUpload(const QUrl& failUrl, const QString& token)
470 : {
471 0 : RetVal<Progress> progress = m_networkManager->del(failUrl, nullptr, headers(token));
472 0 : if (!progress.ret) {
473 0 : LOGE() << progress.ret.toString();
474 0 : return;
475 : }
476 :
477 0 : progress.val.finished().onReceive(this, [](const ProgressResult& res) {
478 0 : if (!res.ret) {
479 0 : LOGE() << res.ret.toString();
480 : }
481 0 : });
482 0 : }
483 :
484 0 : void AudioComService::notifyServerAboutSuccessUpload(const QUrl& successUrl, const QString& token)
485 : {
486 0 : auto outData = std::make_shared<QBuffer>();
487 0 : RetVal<Progress> progress = m_networkManager->post(successUrl, outData, nullptr, headers(token));
488 0 : if (!progress.ret) {
489 0 : LOGE() << progress.ret.toString();
490 0 : return;
491 : }
492 :
493 0 : progress.val.finished().onReceive(this, [](const ProgressResult& res) {
494 0 : if (!res.ret) {
495 0 : LOGE() << res.ret.toString();
496 : }
497 0 : });
498 0 : }
|