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 "musesoundsrepository.h"
24 :
25 : #include "serialization/json.h"
26 :
27 : #include <QBuffer>
28 : #include <QUrlQuery>
29 :
30 : using namespace mu::musesounds;
31 : using namespace muse;
32 : using namespace muse::network;
33 :
34 0 : static std::string platformMuseSoundsAppName()
35 : {
36 : #if defined(Q_OS_WIN) | defined(Q_OS_MAC)
37 : return "museHub";
38 : #else
39 0 : return "museSoundManager";
40 : #endif
41 : }
42 :
43 0 : static muse::UriQuery correctThumbnailSize(const UriQuery& uri)
44 : {
45 0 : String uriStr = String::fromStdString(uri.toString());
46 :
47 : // Thumbnails can live under different CDN folders (library-images, bundle-images,
48 : // platform-subscription-images, etc.), so resize by host rather than a fixed folder name
49 0 : static const String host(u"muse-cdn.com/");
50 0 : const size_t hostIdx = uriStr.indexOf(host);
51 0 : if (hostIdx != muse::nidx) {
52 0 : uriStr.insert(hostIdx + host.size(), u"cdn-cgi/image/w=240,q=80,f=webp/");
53 : }
54 :
55 0 : return UriQuery(uriStr.toStdString());
56 0 : }
57 :
58 0 : static QUrl soundsRequestUrl(const QUrl& baseUrl)
59 : {
60 0 : QLocale locale = QLocale();
61 0 : String localeStr = locale.bcp47Name() + "-" + QLocale::territoryToCode(locale.territory());
62 :
63 0 : JsonObject variables;
64 0 : variables["locale"] = localeStr;
65 :
66 0 : QUrlQuery query;
67 0 : query.addQueryItem("pqId", "musescore-studio-sounds-page-v1");
68 0 : query.addQueryItem("variables",
69 0 : QString::fromUtf8(JsonDocument(variables).toJson(JsonDocument::Format::Compact).toQByteArray()));
70 :
71 0 : QUrl url = baseUrl;
72 0 : url.setQuery(query);
73 :
74 0 : return url;
75 0 : }
76 :
77 0 : void MuseSoundsRepository::init()
78 : {
79 0 : TRACEFUNC;
80 :
81 0 : QUrl url = soundsRequestUrl(QUrl(QString::fromStdString(configuration()->soundsUri().toString())));
82 0 : RequestHeaders headers = configuration()->headers();
83 :
84 0 : auto receivedData = std::make_shared<QBuffer>();
85 :
86 0 : if (!m_networkManager) {
87 0 : m_networkManager = networkManagerCreator()->makeNetworkManager();
88 : }
89 :
90 0 : RetVal<Progress> progress = m_networkManager->get(url, receivedData, headers);
91 0 : if (!progress.ret) {
92 0 : LOGE() << progress.ret.toString();
93 0 : return;
94 : }
95 :
96 0 : progress.val.finished().onReceive(this, [this, receivedData](const muse::ProgressResult& res) {
97 0 : if (!res.ret) {
98 0 : LOGE() << "Unable to download MuseSounds list: " << res.ret.toString();
99 0 : return;
100 : }
101 :
102 0 : RetVal<SoundCatalogInfoList> result;
103 :
104 0 : std::string err;
105 0 : JsonDocument soundsInfoDoc = JsonDocument::fromJson(ByteArray::fromQByteArray(receivedData->data()), &err);
106 0 : if (!err.empty()) {
107 0 : result.ret = make_ret(Ret::Code::InternalError, err);
108 : } else {
109 0 : result.ret = make_ok();
110 0 : result.val = parseSounds(soundsInfoDoc);
111 : }
112 :
113 0 : m_soundsCatalogs = result.val;
114 0 : m_soundsCatalogsChanged.notify();
115 0 : });
116 0 : }
117 :
118 0 : const SoundCatalogInfoList& MuseSoundsRepository::soundsCatalogs() const
119 : {
120 0 : return m_soundsCatalogs;
121 : }
122 :
123 0 : async::Notification MuseSoundsRepository::soundsCatalogsChanged() const
124 : {
125 0 : return m_soundsCatalogsChanged;
126 : }
127 :
128 0 : SoundCatalogInfoList MuseSoundsRepository::parseSounds(const JsonDocument& soundsDoc) const
129 : {
130 0 : SoundCatalogInfoList result;
131 :
132 0 : JsonObject obj = soundsDoc.rootObject();
133 0 : JsonObject data = !obj.empty() ? obj.value("data").toObject() : JsonObject();
134 0 : JsonObject productsSearch = !data.empty() ? data.value("product_pages_configuration").toObject() : JsonObject();
135 0 : JsonArray catalogs = !productsSearch.empty() ? productsSearch.value("museScoreStudioPageSections").toArray() : JsonArray();
136 :
137 0 : std::string museSoundsAppName = platformMuseSoundsAppName();
138 :
139 0 : for (size_t catalogIdx = 0; catalogIdx < catalogs.size(); ++catalogIdx) {
140 0 : JsonObject catalogObj = catalogs.at(catalogIdx).toObject();
141 0 : if (catalogObj.empty()) {
142 0 : continue;
143 : }
144 :
145 0 : SoundCatalogInfo catalog;
146 0 : catalog.title = catalogObj.value("title").toString();
147 :
148 0 : JsonArray soundsItems = catalogObj.value("productCards").toArray();
149 0 : if (soundsItems.empty()) {
150 0 : continue;
151 : }
152 :
153 0 : for (size_t soundIdx = 0; soundIdx < soundsItems.size(); ++soundIdx) {
154 0 : JsonObject soundItemObj = soundsItems.at(soundIdx).toObject();
155 0 : if (soundItemObj.empty()) {
156 0 : continue;
157 : }
158 :
159 0 : JsonObject productObj = soundItemObj.value("product").toObject();
160 0 : if (productObj.empty()) {
161 0 : continue;
162 : }
163 :
164 0 : JsonObject compatibleWithObj = productObj.value("compatibleWith").toObject();
165 0 : if (compatibleWithObj.empty()) {
166 : // Products without compatibleWith info are only manageable through MuseHub
167 0 : if (museSoundsAppName != "museHub") {
168 0 : continue;
169 : }
170 0 : } else if (compatibleWithObj.contains(museSoundsAppName) && !compatibleWithObj.value(museSoundsAppName).toBool()) {
171 0 : continue;
172 : }
173 :
174 0 : SoundLibraryInfo soundLibrary;
175 0 : soundLibrary.title = productObj.value("title").toString();
176 0 : soundLibrary.subtitle = productObj.value("subtitle").toString();
177 0 : soundLibrary.thumbnail = correctThumbnailSize(UriQuery(soundItemObj.value("iconImageUrl").toStdString()));
178 0 : soundLibrary.code = productObj.value("code").toString();
179 0 : soundLibrary.uri = configuration()->soundPageUri(soundLibrary.code);
180 :
181 0 : catalog.soundLibraries.emplace_back(soundLibrary);
182 0 : }
183 :
184 0 : if (catalog.soundLibraries.empty()) {
185 0 : continue;
186 : }
187 :
188 0 : result.emplace_back(catalog);
189 0 : }
190 :
191 0 : return result;
192 0 : }
|