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 "musesoundscheckupdateservice.h"
24 :
25 : #include <QBuffer>
26 : #include <QJsonParseError>
27 : #include <QJsonObject>
28 : #include <QJsonValue>
29 : #include <QJsonArray>
30 : #include <QJsonDocument>
31 :
32 : #include "musesoundserrors.h"
33 :
34 : #include "defer.h"
35 : #include "log.h"
36 :
37 : static const muse::UriQuery MUSEHUB_APP_URI("musehub://?from=musescore");
38 : static const muse::UriQuery MUSEHUB_APP_V1_URI("muse-hub://?from=musescore");
39 :
40 : using namespace mu::musesounds;
41 : using namespace muse;
42 : using namespace muse::update;
43 : using namespace muse::network;
44 : using namespace muse::async;
45 :
46 0 : Ret MuseSoundsCheckUpdateService::needCheckForUpdate() const
47 : {
48 0 : if (!configuration()->needCheckForMuseSoundsUpdate()) {
49 0 : return false;
50 : }
51 :
52 : #ifdef Q_OS_WIN
53 : return true;
54 : #elif defined(Q_OS_MAC)
55 : //! NOTE: If there is installed MuseHub, but we can't open it, then we shouldn't check update
56 : static const std::string MUSEHUB_APP_IDENTIFIER = "com.muse.hub";
57 : bool isMuseHubExists = platformInteractive()->isAppExists(MUSEHUB_APP_IDENTIFIER);
58 : if (isMuseHubExists) {
59 : bool canOpenMuseHubByUniversalUrl = platformInteractive()->canOpenApp(MUSEHUB_APP_URI);
60 : return canOpenMuseHubByUniversalUrl;
61 : }
62 :
63 : return true;
64 : #else
65 0 : return false;
66 : #endif
67 : }
68 :
69 0 : Promise<RetVal<ReleaseInfo> > MuseSoundsCheckUpdateService::checkForUpdate()
70 : {
71 0 : return Promise<muse::RetVal<ReleaseInfo> >([this](auto resolve, auto) {
72 0 : m_lastCheckResult.val = ReleaseInfo();
73 0 : m_lastCheckResult.ret = muse::make_ok();
74 :
75 0 : if (configuration()->museSoundsCheckForUpdateTestMode()) {
76 0 : return resolve(m_lastCheckResult);
77 : }
78 :
79 0 : auto buff = std::make_shared<QBuffer>();
80 0 : QUrl url = configuration()->checkForMuseSoundsUpdateUrl();
81 :
82 0 : if (!m_networkManager) {
83 0 : m_networkManager = networkManagerCreator()->makeNetworkManager();
84 : }
85 :
86 0 : RetVal<Progress> progress = m_networkManager->get(url, buff);
87 :
88 0 : if (!progress.ret) {
89 0 : m_lastCheckResult.ret = progress.ret;
90 0 : return resolve(m_lastCheckResult);
91 : }
92 :
93 0 : progress.val.finished().onReceive(this, [this, buff, resolve](const ProgressResult& res) {
94 0 : DEFER {
95 0 : (void)resolve(m_lastCheckResult);
96 : };
97 :
98 0 : if (!res.ret) {
99 0 : m_lastCheckResult.ret = res.ret;
100 0 : return;
101 : }
102 :
103 0 : m_lastCheckResult = parseRelease(buff->data());
104 0 : });
105 :
106 0 : return muse::async::Promise<muse::RetVal<ReleaseInfo> >::dummy_result();
107 0 : });
108 : }
109 :
110 0 : const RetVal<ReleaseInfo>& MuseSoundsCheckUpdateService::lastCheckResult() const
111 : {
112 0 : return m_lastCheckResult;
113 : }
114 :
115 0 : RetVal<ReleaseInfo> MuseSoundsCheckUpdateService::parseRelease(const QByteArray& json) const
116 : {
117 0 : RetVal<ReleaseInfo> result;
118 :
119 0 : QJsonParseError err;
120 0 : QJsonDocument jsonDoc = QJsonDocument::fromJson(json, &err);
121 0 : if (err.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
122 0 : LOGE() << "parse error: " << err.errorString();
123 0 : result.ret = make_ret(Err::NoUpdate);
124 0 : return result;
125 : }
126 :
127 : /*
128 : {
129 : "version": String,
130 : "image_url": String, // it can be base64 data, like "data:image/png;base64,iVBORw0KGgoA......"
131 : "content": {
132 : "locale_code": {
133 : "notes": String,
134 : "features": [String]
135 : "action_title": String // title of action button
136 : }
137 : },
138 :
139 : // open app or web page url, try in order,
140 : // like this ["musehub://?from=musescore", "muse-hub://?from=musescore", "https://www.musehub.com"]
141 : "actions": {
142 : "windows": [String],
143 : "macos": [String],
144 : "linux": [String]
145 : }
146 : }
147 : */
148 :
149 0 : QJsonObject releaseObj = jsonDoc.object();
150 :
151 0 : if (releaseObj.empty()) {
152 0 : result.ret = make_ret(Err::NoUpdate);
153 0 : return result;
154 : }
155 :
156 0 : QString languageCode = languagesConfiguration()->currentLanguageCode().val;
157 0 : QString locale(QLocale(languageCode).bcp47Name());
158 :
159 0 : QJsonObject contentObj = releaseObj.value("content").toObject();
160 :
161 0 : if (!contentObj.contains(locale)) {
162 0 : static const QString DEFAULT_LOCALE = "en";
163 0 : locale = DEFAULT_LOCALE;
164 :
165 0 : if (!contentObj.contains(locale)) {
166 0 : LOGE() << "failed parse, no release content";
167 0 : result.ret = make_ret(Ret::Code::InternalError);
168 0 : return result;
169 : }
170 : }
171 :
172 0 : QJsonObject contentLocaleObj = contentObj.value(locale).toObject();
173 :
174 0 : result.ret = make_ok();
175 :
176 0 : result.val.version = releaseObj.value("version").toString().toStdString();
177 0 : result.val.imageUrl = releaseObj.value("image_url").toString().toStdString();
178 :
179 0 : result.val.notes = contentLocaleObj.value("notes").toString().toStdString();
180 0 : ValList featuresList;
181 0 : QJsonArray features = contentLocaleObj.value("features").toArray();
182 0 : for (const QJsonValue& feature : features) {
183 0 : featuresList.push_back(Val(feature.toString().toStdString()));
184 0 : }
185 0 : result.val.additionInfo.insert({ "features", Val(featuresList) });
186 :
187 0 : result.val.actionTitle = contentLocaleObj.value("action_title").toString().toStdString();
188 0 : result.val.cancelTitle = contentLocaleObj.value("cancel_title").toString().toStdString();
189 :
190 0 : QJsonObject actionsObj = releaseObj.value("actions").toObject();
191 :
192 : #ifdef Q_OS_WIN
193 : QJsonArray actionsArr = actionsObj.value("windows").toArray();
194 : for (const QJsonValue& a : actionsArr) {
195 : result.val.actions.push_back(Val(a.toString().toStdString()));
196 : }
197 :
198 : // def
199 : if (result.val.actions.empty()) {
200 : result.val.actions.push_back(Val(MUSEHUB_APP_URI.toString()));
201 : result.val.actions.push_back(Val(MUSEHUB_APP_V1_URI.toString()));
202 : result.val.actions.push_back(Val(globalConfiguration()->museHubWebUrl()));
203 : }
204 :
205 : #elif defined(Q_OS_MAC)
206 : QJsonArray actionsArr = actionsObj.value("macos").toArray();
207 : for (const QJsonValue& a : actionsArr) {
208 : result.val.actions.push_back(Val(a.toString().toStdString()));
209 : }
210 :
211 : // def
212 : if (result.val.actions.empty()) {
213 : result.val.actions.push_back(Val(MUSEHUB_APP_URI.toString()));
214 : result.val.actions.push_back(Val(globalConfiguration()->museHubWebUrl()));
215 : }
216 : #else
217 0 : QJsonArray actionsArr = actionsObj.value("linux").toArray();
218 0 : for (const QJsonValue& a : actionsArr) {
219 0 : result.val.actions.push_back(Val(a.toString().toStdString()));
220 0 : }
221 :
222 : // def
223 0 : if (result.val.actions.empty()) {
224 0 : result.val.actions.push_back(Val(globalConfiguration()->museHubWebUrl()));
225 : }
226 : #endif
227 :
228 0 : return result;
229 0 : }
|