Auto Updater!

- **Auto updating!** Fixed auto updater to work with this fork.
- Added Updater Notification Screen
- Fixed Chrome Session not being saved, forcing login on each start.
This commit is contained in:
Zarg 2024-12-20 12:17:11 +01:00
parent 6594b4273a
commit 56dd1f4bd6
11 changed files with 434 additions and 48 deletions

View file

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.30)
project(stremio VERSION "5.0.0")
project(stremio VERSION "5.0.1")
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/CMakeModules/")

214
build/build_checksums.js Normal file
View file

@ -0,0 +1,214 @@
/*
generate_sums.js
Usage:
node generate_sums.js "C:\\Program Files\\OpenSSL-Win64\\bin" "5.0.0-beta.1" "5.0.0" 4.20.11
This script:
1) Validates four arguments:
-- OPENSSL_BIN (path to folder containing openssl.exe)
-- GIT_TAG (e.g. "5.0.0-beta.1")
-- SHELL_VERSION (e.g. "5.0.0")
-- SERVER_VERSION (e.g. "4.20.11")
2) Locates and verifies "openssl.exe" in OPENSSL_BIN.
3) Computes sha256 checksums of "Stremio <SHELL_VERSION>.exe" and "server.js" using
"openssl dgst -sha256".
4) Updates version-details.json to:
shellVersion = SHELL_VERSION
windows.url = https://github.com/Zaarrg/stremio-desktop-v5/releases/download/<GIT_TAG>/Stremio.<SHELL_VERSION>.exe
windows.checksum = <exeSha256>
server.js.url = https://dl.strem.io/server/<SERVER_VERSION>/desktop/server.js
server.js.checksum = <serverSha256>
5) Signs version-details.json with private_key.pem, base64-encodes the signature,
inserts that signature into version.json.
6) Cleans up the signature files (version-details.json.sig / .sig.b64).
7) Exits 0 on success; 1 on any error.
No external packages needed; we only use Node's built-in fs, path, child_process [[1]].
*/
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
// Parse CLI arguments
// e.g. node generate_sums.js "C:\\OpenSSL\\bin" "5.0.0-beta.1" "5.0.0" 4.20.11
const [,, OPENSSL_BIN, GIT_TAG, SHELL_VERSION, SERVER_VERSION] = process.argv;
(async function main() {
// 1) Validate args
if (!OPENSSL_BIN || !GIT_TAG || !SHELL_VERSION || !SERVER_VERSION) {
console.error("Usage: node generate_sums.js <OpenSSLBinPath> <GitTag> <ShellVersion> <ServerVersion>");
console.error('Example: node generate_sums.js "C:\\Program Files\\OpenSSL-Win64\\bin" "5.0.0-beta.1" "5.0.0" 4.20.11');
process.exit(1);
}
// 2) Verify openssl.exe
const opensslExe = path.join(OPENSSL_BIN, "openssl.exe");
if (!fs.existsSync(opensslExe)) {
console.error("ERROR: Cannot find openssl.exe in:", opensslExe);
process.exit(1);
}
console.log("Using OpenSSL at:", OPENSSL_BIN);
console.log("Git Tag:", GIT_TAG);
console.log("Shell Version:", SHELL_VERSION);
console.log("server.js Version:", SERVER_VERSION);
console.log();
// 3) Build paths
// Assume this script is in /build; go up one directory for the project root
const scriptDir = path.dirname(__filename);
const projectRoot = path.resolve(scriptDir, "..");
// For Windows .exe, the local file name uses the Shell Version (5.0.0), not the Git tag
const EXE_PATH = path.join(projectRoot, "utils", `Stremio ${SHELL_VERSION}.exe`);
const SERVERJS_PATH = path.join(projectRoot, "utils", "windows", "server.js");
const VERSION_DETAILS_PATH = path.join(projectRoot, "version", "version-details.json");
const VERSION_JSON_PATH = path.join(projectRoot, "version", "version.json");
const PRIVATE_KEY = path.join(projectRoot, "private_key.pem");
// 4) Generate SHA-256 for the .exe and server.js
checkFileExists(EXE_PATH, "Stremio .exe");
const exeHash = computeSha256(opensslExe, EXE_PATH);
checkFileExists(SERVERJS_PATH, "server.js");
const serverHash = computeSha256(opensslExe, SERVERJS_PATH);
console.log("EXE sha256 =", exeHash);
console.log("server.js sha256 =", serverHash);
console.log();
// 5) Update version-details.json
checkFileExists(VERSION_DETAILS_PATH, "version-details.json");
let versionDetails;
try {
versionDetails = JSON.parse(fs.readFileSync(VERSION_DETAILS_PATH, "utf8"));
} catch (err) {
console.error("ERROR: Unable to parse version-details.json:", err.message);
process.exit(1);
}
// Update:
// versionDetails.shellVersion = SHELL_VERSION
// versionDetails.files.windows.url => uses GIT_TAG
// versionDetails.files.windows.checksum => exeHash
// versionDetails.files["server.js"].url => uses SERVER_VERSION
// versionDetails.files["server.js"].checksum => serverHash
versionDetails.shellVersion = SHELL_VERSION;
if (!versionDetails.files) {
console.error("ERROR: version-details.json missing property 'files'");
process.exit(1);
}
if (!versionDetails.files.windows) versionDetails.files.windows = {};
versionDetails.files.windows.url = `https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${GIT_TAG}/Stremio.${SHELL_VERSION}.exe`;
versionDetails.files.windows.checksum = exeHash;
if (!versionDetails.files["server.js"]) versionDetails.files["server.js"] = {};
versionDetails.files["server.js"].url = `https://dl.strem.io/server/${SERVER_VERSION}/desktop/server.js`;
versionDetails.files["server.js"].checksum = serverHash;
// Save updated version-details.json
try {
fs.writeFileSync(VERSION_DETAILS_PATH, JSON.stringify(versionDetails, null, 2), "utf8");
} catch (e) {
console.error("ERROR: Failed writing version-details.json:", e.message);
process.exit(1);
}
// 6) Sign version-details.json & base64-encode
checkFileExists(PRIVATE_KEY, "private_key.pem");
process.chdir(path.join(projectRoot, "version"));
const sigFile = path.join(process.cwd(), "version-details.json.sig");
const sigB64 = path.join(process.cwd(), "version-details.json.sig.b64");
if (fs.existsSync(sigFile)) fs.unlinkSync(sigFile);
if (fs.existsSync(sigB64)) fs.unlinkSync(sigB64);
console.log(`Signing version-details.json with ${PRIVATE_KEY}...`);
try {
execFileSync(opensslExe, ["dgst", "-sha256", "-sign", PRIVATE_KEY, "-out", "version-details.json.sig", "version-details.json"], { stdio: "inherit" });
} catch (err) {
console.error("ERROR: Signing failed:", err.message);
process.exit(1);
}
try {
execFileSync(opensslExe, ["base64", "-in", "version-details.json.sig", "-out", "version-details.json.sig.b64"], { stdio: "inherit" });
} catch (err) {
console.error("ERROR: Base64 encoding failed:", err.message);
process.exit(1);
}
if (!fs.existsSync(sigB64)) {
console.error("ERROR: Could not create signature file:", sigB64);
process.exit(1);
}
process.chdir(projectRoot);
// 7) Insert signature into version.json
checkFileExists(VERSION_JSON_PATH, "version.json");
console.log(`Updating signature in "${VERSION_JSON_PATH}"...`);
let signatureB64;
try {
signatureB64 = fs.readFileSync(sigB64, "utf8").replace(/\r?\n/g, "");
} catch (err) {
console.error("ERROR: Unable to read version-details.json.sig.b64:", err.message);
process.exit(1);
}
let versionJson;
try {
versionJson = JSON.parse(fs.readFileSync(VERSION_JSON_PATH, "utf8"));
} catch (err) {
console.error("ERROR: Unable to parse version.json:", err.message);
process.exit(1);
}
versionJson.signature = signatureB64;
try {
fs.writeFileSync(VERSION_JSON_PATH, JSON.stringify(versionJson, null, 2), "utf8");
} catch (err) {
console.error("ERROR: Unable to write version.json:", err.message);
process.exit(1);
}
// 8) Cleanup ephemeral files
try {
if (fs.existsSync(sigFile)) fs.unlinkSync(sigFile);
if (fs.existsSync(sigB64)) fs.unlinkSync(sigB64);
} catch (cleanupErr) {
console.error("WARNING: Could not remove signature files:", cleanupErr.message);
}
console.log("\nSuccess! Checksums and signature have been updated, ephemeral signature files removed.");
process.exit(0);
})().catch(err => {
console.error("Unexpected error:", err);
process.exit(1);
});
// Helper: checks that filePath exists, else exits
function checkFileExists(filePath, label) {
if (!fs.existsSync(filePath)) {
console.error(`ERROR: ${label} file not found at: ${filePath}`);
process.exit(1);
}
}
// Helper: runs "openssl dgst -sha256 <file>" and parses the output
function computeSha256(opensslExe, filePath) {
try {
const output = execFileSync(opensslExe, ["dgst", "-sha256", filePath], { encoding: "utf8" });
// Typically "SHA256(file)= <hexhash>"
const match = output.match(/=.\s*([0-9a-fA-F]+)/);
if (!match) {
console.error("ERROR: Unexpected openssl dgst output for", filePath, "-", output);
process.exit(1);
}
return match[1].toLowerCase();
} catch (err) {
console.error(`ERROR: openssl dgst failed for ${filePath}:`, err.message);
process.exit(1);
}
}

21
docs/RELEASE.md Normal file
View file

@ -0,0 +1,21 @@
# Releasing New Version
---
## 🚀 Quick Overview
1. Bump version in ``cmakelists``
2. Build new ``runtime`` and `installer`
3. Make sure `installer` is in `/utils` and `server.js` in `/utils/windows`
4. Run ``build/build_checksums.js`` this will generate `version.json` and `version-details.json` needed for the auto updater
```
node build_checksums.js <OpenSSL_Bin> <Git_Tag> <Shell_Version> <Server.js_Version>
node build_checksums.js "C:\Program Files\OpenSSL-Win64\bin" "5.0.0-beta.1" "5.0.0" v4.20.8
```
> **⏳Note:** Only Windows at the moment
5. Commit Changes
6. Make new release with the Git tag used when running ``build_checksums.js``
> **⏳Note:** Alternatively u can separate the version bump commit. Instead:
> Commit - Release - Build Checksums - Commit Built Checksums

View file

@ -97,9 +97,9 @@ Ensure the following are installed on your system:
4. Build distributable
```cmd
build_windows_vcpkg.bat {cmake-build-folder} {openssl-bin}
build_windows.bat {cmake-build-folder} {openssl-bin}
build_windows_vcpkg.bat cmake-build-release
build_windows.bat cmake-build-release "C:\Program Files\OpenSSL-Win64\bin"
```

View file

@ -237,7 +237,7 @@ ApplicationWindow {
}
}
if (ev === "autoupdater-notif-clicked" && autoUpdater.onNotifClicked) {
autoUpdater.onNotifClicked();
autoUpdater.onNotifClicked(); //Event not used. We use qt updateScreen notification instead
}
if (ev === "screensaver-toggle") shouldDisableScreensaver(args.disabled)
if (ev === "file-close") fileDialog.close()
@ -362,20 +362,149 @@ ApplicationWindow {
id: splashScreen;
color: "#0c0b11";
anchors.fill: parent;
Image {
id: splashLogo
source: "qrc:///images/stremio.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation {
id: pulseOpacity
running: true
NumberAnimation { target: splashLogo; property: "opacity"; to: 1.0; duration: 600;
easing.type: Easing.Linear; }
NumberAnimation { target: splashLogo; property: "opacity"; to: 0.3; duration: 600;
easing.type: Easing.Linear; }
loops: Animation.Infinite
Column {
anchors.centerIn: parent
spacing: 20
Image {
id: splashLogo
source: "qrc:///images/stremio.png"
anchors.horizontalCenter: parent.horizontalCenter
SequentialAnimation {
id: pulseOpacity
running: true
NumberAnimation { target: splashLogo; property: "opacity"; to: 1.0; duration: 600;
easing.type: Easing.Linear; }
NumberAnimation { target: splashLogo; property: "opacity"; to: 0.3; duration: 600;
easing.type: Easing.Linear; }
loops: Animation.Infinite
}
}
Column {
height: 90 //Height of updateScreen elements 45 + 30 + 15
width: 100
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
//Connection to show update Screen. Needed because autoupdater runs in different thread
QtObject {
id: autoUpdateTransport
signal showUpdateScreen()
}
Connections {
target: autoUpdateTransport
onShowUpdateScreen: {
updateScreen.visible = true;
updateScreen.focus = true;
}
}
//
// Update screen
// Must be over the UI
//
Rectangle {
id: updateScreen
color: "#0c0b11"
anchors.fill: parent
visible: false
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
Column {
anchors.centerIn: parent
spacing: 20
Image {
id: updateLogo
source: "qrc:///images/stremio.png"
anchors.horizontalCenter: parent.horizontalCenter // Align like splashLogo
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 15
Text {
text: "Stremio update available!"
color: "white"
font.bold: true
font.pointSize: 14
height: 30
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 25
Button {
width: 80
height: 45
background: Rectangle {
id: updateButtonBg
color: parent.hovered ? "#6B64F2" : "#5351D9"
radius: 5
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onEntered: parent.hovered = true
onExited: parent.hovered = false
}
}
contentItem: Text {
text: "Update"
color: "white"
font.weight: Font.DemiBold
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
}
onClicked: {
updateScreen.visible = false
autoUpdater.onNotifClicked();
}
}
Button {
width: 80
height: 45
background: Rectangle {
id: laterButtonBg
color: parent.hovered ? "#4A4A4A" : "#353637"
radius: 5
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onEntered: parent.hovered = true
onExited: parent.hovered = false
}
}
contentItem: Text {
text: "Later"
color: "white"
font.weight: Font.DemiBold
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
}
onClicked: {
// Handle postpone action
updateScreen.visible = false
webView.visible = true
webView.focus = true
}
}
}
}
}
}

View file

@ -1,14 +1,9 @@
static const char key[] = "-----BEGIN PUBLIC KEY-----\n"
"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA265O2NgcDI5hk1n90xzH\n"
"P7nZU+sSQoJcpOVd71bnqhjTyrmEHk9gJzyB0O8OJPlNonE16lQfM+a0ncH+XLt7\n"
"HKF73ZwsPCHg1MX/lGpSQtfC3jUGZdFssGhKBo06Thtp8coptRiRl2ZAtvgPNKME\n"
"NZLHzck+tI15UwKkKfUL3zfxgapJEPfd7XKT7NCj+n9fa3MXFnSIiDjMvb6f2oeC\n"
"YPpE7pqAaSgde/ZeveJGWgexHzOgA27nz+LuBo3ITYp3HFu9Q0xymQs0fxUFazw+\n"
"YIRJZD5yNNitPOsrRrYgaprNrVS6AvB7lhauMjz+R/r2OdbaszO1Gi9wqGWGoIcy\n"
"VfBCWf75SNwanf8/V5RiQP32bKFiHPlQkqMjrrS3FwPqbXwj/W0rFGgy6GolrFIJ\n"
"rdWeaLSgIY5M3qhowArB7aTMPZItQhOIPqZgTt79GCsa4zKrBvFr7TM7dTRVbLzG\n"
"2yA/cS88sQpnsO/Li/oNunG7Z1DRmWc3otUtsVVvmxFT5p62aoGEpFvbDQuel2yO\n"
"o8VDGK1klqEzRvH7SGLA8VLyvk3gwhXRJxep6l5LLiPXSVw7DqNJ+QhtsCPkteBU\n"
"sVdpnLa3TQFB8Ztn/ZC0L/foupvn95OF4bC6/Owc16kQjSs9V+GgtzGTGxjuGR96\n"
"kKc4nu1CZCwxqHpRpquQIu8CAwEAAQ==\n"
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoXoJRQ81xOT3Gx6+hsWM\n"
"ZiD4PwtLdxxNhEdL/iK0yp6AdO/L0kcSHk9YCPPx0XPK9sssjSV5vCbNE/2IJxnh\n"
"/mV+3GAMmXgMvTL+DZgrHafnxe1K50M+8Z2z+uM5YC9XDLppgnC6OrUjwRqNHrKI\n"
"T1vcgKf16e/TdKj8xlgadoHBECjv6dr87nbHW115bw8PVn2tSk/zC+QdUud+p6KV\n"
"zA6+FT9ZpHJvdS3R0V0l7snr2cwapXF6J36aLGjJ7UviRFVWEEsQaKtAAtTTBzdD\n"
"4B9FJ2IJb/ifdnVzeuNTDYApCSE1F89XFWN9FoDyw7Jkk+7u4rsKjpcnCDTd9ziG\n"
"kwIDAQAB\n"
"-----END PUBLIC KEY-----";

View file

@ -107,17 +107,9 @@ int AutoUpdater::executeCmd(QString cmd, QStringList args, bool noWait = false)
// CHECK FOR UPDATES
void AutoUpdater::checkForUpdatesPerform(QString endpoint, QString userAgent)
{
QByteArray serverHash = getFileChecksum(QCoreApplication::applicationDirPath() + QDir::separator() + SERVER_FNAME);
QByteArray asarHash = getFileChecksum(QCoreApplication::applicationDirPath() + QDir::separator() + ASAR_FNAME);
QUrl url = QUrl(endpoint);
QUrlQuery query = QUrlQuery(url);
qDebug() << "AUTOUPDATER: Checking for updates...";
query.addQueryItem("serverSum", serverHash.toHex());
query.addQueryItem("asarSum", asarHash.toHex());
query.addQueryItem("shellVersion", QCoreApplication::applicationVersion());
url.setQuery(query);
auto request = QNetworkRequest(QUrl(url));
request.setRawHeader("User-Agent", userAgent.toUtf8());
currentCheck = manager->get(request);
@ -218,14 +210,19 @@ void AutoUpdater::prepareUpdate(QJsonDocument versionDescDoc) {
QJsonObject versionDesc = versionDescDoc.object();
QJsonObject files = versionDesc.value("files").toObject();
QByteArray serverHash = getFileChecksum(QCoreApplication::applicationDirPath() + QDir::separator() + SERVER_FNAME);
QString serverHashHex = QString::fromLatin1(serverHash.toHex());
QVector<QString> toDownload;
if (forceFullUpdate
|| versionDesc.value("shellVersion").toString() != QCoreApplication::applicationVersion()
) {
toDownload = FULL_UPDATE_FILES;
} else {
} else if (files.value(PARTIAL_UPDATE_FILES).toObject().value("checksum").toString() != serverHashHex) {
toDownload = PARTIAL_UPDATE_FILES;
} else {
qDebug() << "Everything is up to date";
return;
}
if (! toDownload.length()) {

View file

@ -28,9 +28,9 @@ extern "C" {
// TODO Move to somewhere? Document that we can override?
#define SERVER_FNAME "server.js"
#define ASAR_FNAME "stremio.asar"
//#define ASAR_FNAME "stremio.asar"
#define PARTIAL_UPDATE_FILES { SERVER_FNAME, ASAR_FNAME }
#define PARTIAL_UPDATE_FILES { SERVER_FNAME }
#if defined(Q_OS_WIN)
#define FULL_UPDATE_FILES { "windows" }

View file

@ -8,8 +8,7 @@
var errorCounter = MAX_ERROR_COUNT;
var endpoints = ["https://www.strem.io/updater/check", "https://www.stremio.com/updater/check",
"https://www.stremio.net/updater/check"];
var endpoints = ["https://raw.githubusercontent.com/Zaarrg/stremio-desktop-v5/refs/heads/master/version/version.json"];
var fallbackSite = "https://www.stremio.com/?fromFailedAutoupdate=true";
var doAutoupdate = autoUpdater.isInstalled()
@ -93,7 +92,7 @@
// a notification event once it loads
// Then, we must set .onNotifClicked to what we'll do when the notification is clicked
if (preparedFiles.length == 2) {
if (firstFile && firstFile.match(".js")) {
//
// Prepare partial auto-update
//
@ -104,7 +103,11 @@
autoUpdaterErr("preparing partial update failed", null)
return
}
transport.queueEvent("autoupdater-show-notif", { mode: "reload" })
//transport.queueEvent("autoupdater-show-notif", { mode: "reload" })
Qt.callLater(function () {
autoUpdateTransport.showUpdateScreen();
});
autoUpdater.onNotifClicked = function() {
splashScreen.visible = true
pulseOpacity.running = true
@ -141,7 +144,10 @@
return;
}
transport.queueEvent("autoupdater-show-notif", { mode: "restart" })
//transport.queueEvent("autoupdater-show-notif", { mode: "restart" })
Qt.callLater(function () {
autoUpdateTransport.showUpdateScreen();
});
autoUpdater.onNotifClicked = function() {
autoUpdater.executeCmd("/bin/sh", ["-c", "sleep 5; open -n /Applications/Stremio.app"], true)
quitApp();
@ -150,7 +156,10 @@
//
// Prepare launch-based auto-update (launch new installer/appimage on Windows)
//
transport.queueEvent("autoupdater-show-notif", { mode: "launchNew" })
//transport.queueEvent("autoupdater-show-notif", { mode: "launchNew" })
Qt.callLater(function () {
autoUpdateTransport.showUpdateScreen();
});
autoUpdater.onNotifClicked = function() {
Qt.openUrlExternally("file:///"+firstFile.replace(/\\/g,'/'))
quitApp();
@ -168,7 +177,10 @@
autoUpdaterErr("preparing Linux .appimage failed", null);
return;
}
transport.queueEvent("autoupdater-show-notif", { mode: "launchNew" })
//transport.queueEvent("autoupdater-show-notif", { mode: "launchNew" })
Qt.callLater(function () {
autoUpdateTransport.showUpdateScreen();
});
autoUpdater.onNotifClicked = function() {
autoUpdater.executeCmd("/bin/sh", ["-c", "$HOME/'"+baseName+"'"], true)
// crappy, but otherwise we have to write code to get env var

View file

@ -0,0 +1,13 @@
{
"shellVersion": "5.0.1",
"files": {
"windows": {
"url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.2/Stremio.5.0.1.exe",
"checksum": "38f11ab6dcb4fc93cbe54c88a4f8042fef8c09fc9ba83938b916dbe5ba3fe45f"
},
"server.js": {
"url": "https://dl.strem.io/server/v4.20.8/desktop/server.js",
"checksum": "7113200f5775c958fd141bc502a808ab00ebfbb53799a13c3ab0aca84c5fb476"
}
}
}

5
version/version.json Normal file
View file

@ -0,0 +1,5 @@
{
"upToDate": false,
"versionDesc": "https://raw.githubusercontent.com/Zaarrg/stremio-desktop-v5/refs/heads/master/version/version-details.json",
"signature": "bRMydlBNCo4eqIIQwTQ1O1vgOs20fpfRh6ymmu9G9rqQtJlEC5uDe8uavu7wmDJC/ARHgsSQxzb8tw/HCbR4knI6gPQs1oasDhSn6o4b5YyWdLJo0vfyicrJj+xI5cngdS5NLnyrCxVf7sO5NRi1NtLWJ0XcF8M8f93VfqTClVAR/SDbX5vFod+CppZDNbpdKHvyCdk/AT232Pv9q6+2TQyLQk7rLvlBXXH/8OCU+tmmTe7arTSKNr6NA8k5N+hevjay+cuhXluSomesXIjJVIPFolu7yaC3MX6doxE3hBhTA3wnUlm5nDZOSPplYRQfHubA8ms+8BOzZE32sZw2qA=="
}