#include "configbundle.h" #include "../../config/configstore.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr char kKSchemaVersion[] = "schemaVersion"; constexpr char kKAppVersion[] = "appVersion"; constexpr char kKDeviceName[] = "deviceName"; constexpr char kKOsName[] = "osName"; constexpr char kKOsPretty[] = "osPretty"; constexpr char kKExportedAt[] = "exportedAt"; constexpr char kKMetadata[] = "metadata"; constexpr char kKServers[] = "servers"; constexpr char kKConfigEntries[] = "configEntries"; QString serversJsonPath() { const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); return QDir(dir).filePath(QStringLiteral("servers.json")); } QString compactProductType(QString type) { type = type.trimmed().toLower(); if (type.isEmpty()) { return QStringLiteral("OS"); } if (type == QStringLiteral("ubuntu")) return QStringLiteral("Ubuntu"); if (type == QStringLiteral("debian")) return QStringLiteral("Debian"); if (type == QStringLiteral("arch")) return QStringLiteral("Arch"); if (type == QStringLiteral("fedora")) return QStringLiteral("Fedora"); if (type == QStringLiteral("centos")) return QStringLiteral("CentOS"); if (type == QStringLiteral("opensuse")) return QStringLiteral("openSUSE"); if (type == QStringLiteral("manjaro")) return QStringLiteral("Manjaro"); if (type == QStringLiteral("rhel")) return QStringLiteral("RHEL"); if (type == QStringLiteral("linux")) return QStringLiteral("Linux"); type[0] = type.at(0).toUpper(); return type; } QString readableOsName() { const QString type = QSysInfo::productType().toLower(); const QString version = QSysInfo::productVersion().trimmed(); if (type == QStringLiteral("macos") || type == QStringLiteral("osx")) return version.isEmpty() ? QStringLiteral("macOS") : QStringLiteral("macOS%1").arg(version); const QString name = compactProductType(type); return version.isEmpty() ? name : name + version; } QJsonValue variantToJson(const QVariant &v) { if (!v.isValid() || v.isNull()) { return QJsonValue::Null; } switch (static_cast(v.userType())) { case QMetaType::Bool: return v.toBool(); case QMetaType::Int: case QMetaType::UInt: case QMetaType::LongLong: case QMetaType::ULongLong: return v.toLongLong(); case QMetaType::Double: case QMetaType::Float: return v.toDouble(); case QMetaType::QStringList: { QJsonArray arr; for (const QString &s : v.toStringList()) { arr.append(s); } return arr; } case QMetaType::QByteArray: { const QByteArray ba = v.toByteArray(); const QJsonDocument tryDoc = QJsonDocument::fromJson(ba); if (!tryDoc.isNull()) { if (tryDoc.isObject()) return tryDoc.object(); if (tryDoc.isArray()) return tryDoc.array(); } return QString::fromUtf8(ba); } case QMetaType::QString: default: return v.toString(); } } QVariant jsonToVariant(const QJsonValue &v) { switch (v.type()) { case QJsonValue::Bool: return v.toBool(); case QJsonValue::Double: { const double d = v.toDouble(); const qlonglong i = static_cast(d); if (static_cast(i) == d) { return QVariant::fromValue(i); } return d; } case QJsonValue::String: return v.toString(); case QJsonValue::Array: return QString::fromUtf8( QJsonDocument(v.toArray()).toJson(QJsonDocument::Compact)); case QJsonValue::Object: return QString::fromUtf8( QJsonDocument(v.toObject()).toJson(QJsonDocument::Compact)); case QJsonValue::Null: case QJsonValue::Undefined: default: return QVariant(); } } } QJsonObject ConfigBundleMetadata::toJson() const { QJsonObject obj; obj.insert(QString::fromLatin1(kKSchemaVersion), schemaVersion); obj.insert(QString::fromLatin1(kKAppVersion), appVersion); obj.insert(QString::fromLatin1(kKDeviceName), deviceName); obj.insert(QString::fromLatin1(kKOsName), osName); obj.insert(QString::fromLatin1(kKOsPretty), osPretty); obj.insert(QString::fromLatin1(kKExportedAt), exportedAt); return obj; } ConfigBundleMetadata ConfigBundleMetadata::fromJson(const QJsonObject &obj) { ConfigBundleMetadata m; m.schemaVersion = obj.value(QString::fromLatin1(kKSchemaVersion)).toInt(1); m.appVersion = obj.value(QString::fromLatin1(kKAppVersion)).toString(); m.deviceName = obj.value(QString::fromLatin1(kKDeviceName)).toString(); m.osName = obj.value(QString::fromLatin1(kKOsName)).toString(); m.osPretty = obj.value(QString::fromLatin1(kKOsPretty)).toString(); m.exportedAt = static_cast(obj.value(QString::fromLatin1(kKExportedAt)).toDouble(0.0)); return m; } ConfigBundleMetadata ConfigBundleMetadata::current() { ConfigBundleMetadata m; m.schemaVersion = 1; m.appVersion = QCoreApplication::applicationVersion(); if (m.appVersion.isEmpty()) { m.appVersion = QStringLiteral("dev"); } m.deviceName = QSysInfo::machineHostName(); m.osName = readableOsName(); m.osPretty = QSysInfo::prettyProductName(); m.exportedAt = QDateTime::currentMSecsSinceEpoch(); return m; } const QStringList &ConfigBundle::includedNamespaces() { static const QStringList list = { QStringLiteral("general/"), QStringLiteral("network/"), QStringLiteral("appearance/"), QStringLiteral("library/"), QStringLiteral("player/"), QStringLiteral("danmaku/"), QStringLiteral("ext_player/"), QStringLiteral("search/"), QStringLiteral("sort/"), QStringLiteral("view/"), QStringLiteral("server/"), QStringLiteral("app/"), }; return list; } bool ConfigBundle::shouldExcludeKey(const QString &key) { static const QStringList exactBlacklist = { QStringLiteral("general/last_selected_server_id"), QStringLiteral("app/last_selected_server_id"), QStringLiteral("appearance/startup_window_state"), QStringLiteral("ext_player/detected_list"), QStringLiteral("player/last_subtitle_dir"), }; if (exactBlacklist.contains(key)) { return true; } static const QStringList prefixBlacklist = { QStringLiteral("cache/"), QStringLiteral("player/external_subtitle/"), QStringLiteral("download/"), }; for (const QString &prefix : prefixBlacklist) { if (key.startsWith(prefix)) { return true; } } return false; } QJsonObject ConfigBundle::toJson() const { QJsonObject root; root.insert(QString::fromLatin1(kKMetadata), metadata.toJson()); root.insert(QString::fromLatin1(kKServers), servers); root.insert(QString::fromLatin1(kKConfigEntries), configEntries); return root; } QByteArray ConfigBundle::serialize(bool indented) const { return QJsonDocument(toJson()).toJson(indented ? QJsonDocument::Indented : QJsonDocument::Compact); } ConfigBundle ConfigBundle::fromJson(const QJsonObject &obj) { ConfigBundle b; b.metadata = ConfigBundleMetadata::fromJson( obj.value(QString::fromLatin1(kKMetadata)).toObject()); b.servers = obj.value(QString::fromLatin1(kKServers)).toArray(); b.configEntries = obj.value(QString::fromLatin1(kKConfigEntries)).toObject(); return b; } std::optional ConfigBundle::deserialize(const QByteArray &bytes) { QJsonParseError err{}; const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) { qWarning() << "[ConfigBundle] deserialize parse failed:" << err.errorString(); return std::nullopt; } return fromJson(doc.object()); } ConfigBundle ConfigBundle::collectFromLocal() { ConfigBundle b; b.metadata = ConfigBundleMetadata::current(); { QFile f(serversJsonPath()); if (f.open(QIODevice::ReadOnly)) { const QByteArray bytes = f.readAll(); f.close(); const QJsonDocument doc = QJsonDocument::fromJson(bytes); if (doc.isArray()) { b.servers = doc.array(); } else { qWarning() << "[ConfigBundle] servers.json is not array, skipped"; } } else { qDebug() << "[ConfigBundle] servers.json not present at" << serversJsonPath(); } } ConfigStore *store = ConfigStore::instance(); if (store) { const QStringList allKeys = store->allKeys(); const QStringList &nsList = includedNamespaces(); for (const QString &key : allKeys) { bool inWhitelist = false; for (const QString &ns : nsList) { if (key.startsWith(ns)) { inWhitelist = true; break; } } if (!inWhitelist) { continue; } if (shouldExcludeKey(key)) { continue; } const QVariant val = store->get(key); b.configEntries.insert(key, variantToJson(val)); } } qDebug() << "[ConfigBundle] collectFromLocal" << "| servers:" << b.servers.size() << "| entries:" << b.configEntries.size() << "| host:" << b.metadata.deviceName << "| os:" << b.metadata.osPretty << "| app:" << b.metadata.appVersion; return b; }