summaryrefslogtreecommitdiffstats
path: root/xbmc/addons/AddonDatabase.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/addons/AddonDatabase.cpp')
-rw-r--r--xbmc/addons/AddonDatabase.cpp1254
1 files changed, 1254 insertions, 0 deletions
diff --git a/xbmc/addons/AddonDatabase.cpp b/xbmc/addons/AddonDatabase.cpp
new file mode 100644
index 0000000..29837be
--- /dev/null
+++ b/xbmc/addons/AddonDatabase.cpp
@@ -0,0 +1,1254 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AddonDatabase.h"
+
+#include "XBDateTime.h"
+#include "addons/AddonBuilder.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dbwrappers/dataset.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <utility>
+
+using namespace ADDON;
+
+std::string CAddonDatabaseSerializer::SerializeMetadata(const CAddonInfo& addon)
+{
+ CVariant variant;
+ variant["author"] = addon.Author();
+ variant["disclaimer"] = addon.Disclaimer();
+ variant["lifecycletype"] = static_cast<unsigned int>(addon.LifecycleState());
+ variant["lifecycledesc"] = addon.LifecycleStateDescription();
+ variant["size"] = addon.PackageSize();
+
+ variant["path"] = addon.Path();
+ variant["icon"] = addon.Icon();
+
+ variant["art"] = CVariant(CVariant::VariantTypeObject);
+ for (const auto& item : addon.Art())
+ variant["art"][item.first] = item.second;
+
+ variant["screenshots"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& item : addon.Screenshots())
+ variant["screenshots"].push_back(item);
+
+ variant["extensions"] = CVariant(CVariant::VariantTypeArray);
+ variant["extensions"].push_back(SerializeExtensions(*addon.Type(addon.MainType())));
+
+ variant["dependencies"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& dep : addon.GetDependencies())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["addonId"] = dep.id;
+ info["version"] = dep.version.asString();
+ info["minversion"] = dep.versionMin.asString();
+ info["optional"] = dep.optional;
+ variant["dependencies"].push_back(std::move(info));
+ }
+
+ variant["extrainfo"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& kv : addon.ExtraInfo())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["key"] = kv.first;
+ info["value"] = kv.second;
+ variant["extrainfo"].push_back(std::move(info));
+ }
+
+ std::string json;
+ CJSONVariantWriter::Write(variant, json, true);
+ return json;
+}
+
+CVariant CAddonDatabaseSerializer::SerializeExtensions(const CAddonExtensions& addonType)
+{
+ CVariant variant;
+ variant["type"] = addonType.m_point;
+
+ variant["values"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& value : addonType.m_values)
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = value.first;
+ info["content"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& content : value.second)
+ {
+ CVariant contentEntry(CVariant::VariantTypeObject);
+ contentEntry["key"] = content.first;
+ contentEntry["value"] = content.second.str;
+ info["content"].push_back(std::move(contentEntry));
+ }
+
+ variant["values"].push_back(std::move(info));
+ }
+
+ variant["children"] = CVariant(CVariant::VariantTypeArray);
+ for (auto& child : addonType.m_children)
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = child.first;
+ info["child"] = SerializeExtensions(child.second);
+ variant["children"].push_back(std::move(info));
+ }
+
+ return variant;
+}
+
+void CAddonDatabaseSerializer::DeserializeMetadata(const std::string& document,
+ CAddonInfoBuilderFromDB& builder)
+{
+ CVariant variant;
+ if (!CJSONVariantParser::Parse(document, variant))
+ return;
+
+ builder.SetAuthor(variant["author"].asString());
+ builder.SetDisclaimer(variant["disclaimer"].asString());
+ builder.SetLifecycleState(
+ static_cast<AddonLifecycleState>(variant["lifecycletype"].asUnsignedInteger()),
+ variant["lifecycledesc"].asString());
+ builder.SetPackageSize(variant["size"].asUnsignedInteger());
+
+ builder.SetPath(variant["path"].asString());
+ builder.SetIcon(variant["icon"].asString());
+
+ std::map<std::string, std::string> art;
+ for (auto it = variant["art"].begin_map(); it != variant["art"].end_map(); ++it)
+ art.emplace(it->first, it->second.asString());
+ builder.SetArt(std::move(art));
+
+ std::vector<std::string> screenshots;
+ for (auto it = variant["screenshots"].begin_array(); it != variant["screenshots"].end_array(); ++it)
+ screenshots.push_back(it->asString());
+ builder.SetScreenshots(std::move(screenshots));
+
+ CAddonType addonType;
+ DeserializeExtensions(variant["extensions"][0], addonType);
+ addonType.m_type = CAddonInfo::TranslateType(addonType.m_point);
+ builder.SetExtensions(std::move(addonType));
+
+ {
+ std::vector<DependencyInfo> deps;
+ for (auto it = variant["dependencies"].begin_array(); it != variant["dependencies"].end_array(); ++it)
+ {
+ deps.emplace_back((*it)["addonId"].asString(), CAddonVersion((*it)["minversion"].asString()),
+ CAddonVersion((*it)["version"].asString()), (*it)["optional"].asBoolean());
+ }
+ builder.SetDependencies(std::move(deps));
+ }
+
+ InfoMap extraInfo;
+ for (auto it = variant["extrainfo"].begin_array(); it != variant["extrainfo"].end_array(); ++it)
+ extraInfo.emplace((*it)["key"].asString(), (*it)["value"].asString());
+ builder.SetExtrainfo(std::move(extraInfo));
+}
+
+void CAddonDatabaseSerializer::DeserializeExtensions(const CVariant& variant,
+ CAddonExtensions& addonType)
+{
+ addonType.m_point = variant["type"].asString();
+
+ for (auto value = variant["values"].begin_array(); value != variant["values"].end_array();
+ ++value)
+ {
+ std::string id = (*value)["id"].asString();
+ std::vector<std::pair<std::string, SExtValue>> extValues;
+ for (auto content = (*value)["content"].begin_array();
+ content != (*value)["content"].end_array(); ++content)
+ {
+ extValues.emplace_back((*content)["key"].asString(), SExtValue{(*content)["value"].asString()});
+ }
+
+ addonType.m_values.emplace_back(id, extValues);
+ }
+
+ for (auto child = variant["children"].begin_array(); child != variant["children"].end_array();
+ ++child)
+ {
+ CAddonExtensions childExt;
+ DeserializeExtensions((*child)["child"], childExt);
+ std::string id = (*child)["id"].asString();
+ addonType.m_children.emplace_back(id, childExt);
+ }
+
+ return;
+}
+
+CAddonDatabase::CAddonDatabase() = default;
+
+CAddonDatabase::~CAddonDatabase() = default;
+
+bool CAddonDatabase::Open()
+{
+ return CDatabase::Open();
+}
+
+int CAddonDatabase::GetMinSchemaVersion() const
+{
+ return 21;
+}
+
+int CAddonDatabase::GetSchemaVersion() const
+{
+ return 33;
+}
+
+void CAddonDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create addons table");
+ m_pDS->exec("CREATE TABLE addons ("
+ "id INTEGER PRIMARY KEY,"
+ "metadata BLOB,"
+ "addonID TEXT NOT NULL,"
+ "version TEXT NOT NULL,"
+ "name TEXT NOT NULL,"
+ "summary TEXT NOT NULL,"
+ "news TEXT NOT NULL,"
+ "description TEXT NOT NULL)");
+
+ CLog::Log(LOGINFO, "create repo table");
+ m_pDS->exec("CREATE TABLE repo (id integer primary key, addonID text,"
+ "checksum text, lastcheck text, version text, nextcheck TEXT)\n");
+
+ CLog::Log(LOGINFO, "create addonlinkrepo table");
+ m_pDS->exec("CREATE TABLE addonlinkrepo (idRepo integer, idAddon integer)\n");
+
+ CLog::Log(LOGINFO, "create update_rules table");
+ m_pDS->exec(
+ "CREATE TABLE update_rules (id integer primary key, addonID TEXT, updateRule INTEGER)\n");
+
+ CLog::Log(LOGINFO, "create package table");
+ m_pDS->exec("CREATE TABLE package (id integer primary key, addonID text, filename text, hash text)\n");
+
+ CLog::Log(LOGINFO, "create installed table");
+ m_pDS->exec("CREATE TABLE installed (id INTEGER PRIMARY KEY, addonID TEXT UNIQUE, "
+ "enabled BOOLEAN, installDate TEXT, lastUpdated TEXT, lastUsed TEXT, "
+ "origin TEXT NOT NULL DEFAULT '', disabledReason INTEGER NOT NULL DEFAULT 0) \n");
+}
+
+void CAddonDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxAddons ON addons(addonID)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_addonlinkrepo_1 ON addonlinkrepo ( idAddon, idRepo )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_addonlinkrepo_2 ON addonlinkrepo ( idRepo, idAddon )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX idxUpdate_rules ON update_rules(addonID, updateRule)");
+ m_pDS->exec("CREATE UNIQUE INDEX idxPackage ON package(filename)");
+}
+
+void CAddonDatabase::UpdateTables(int version)
+{
+ if (version < 22)
+ {
+ m_pDS->exec("DROP TABLE system");
+ }
+ if (version < 24)
+ {
+ m_pDS->exec("DELETE FROM addon");
+ m_pDS->exec("DELETE FROM addonextra");
+ m_pDS->exec("DELETE FROM dependencies");
+ m_pDS->exec("DELETE FROM addonlinkrepo");
+ m_pDS->exec("DELETE FROM repo");
+ }
+ if (version < 25)
+ {
+ m_pDS->exec("ALTER TABLE installed ADD origin TEXT NOT NULL DEFAULT ''");
+ }
+ if (version < 26)
+ {
+ m_pDS->exec("DROP TABLE addon");
+ m_pDS->exec("DROP TABLE addonextra");
+ m_pDS->exec("DROP TABLE dependencies");
+ m_pDS->exec("DELETE FROM addonlinkrepo");
+ m_pDS->exec("DELETE FROM repo");
+ m_pDS->exec("CREATE TABLE addons ("
+ "id INTEGER PRIMARY KEY,"
+ "metadata BLOB,"
+ "addonID TEXT NOT NULL,"
+ "version TEXT NOT NULL,"
+ "name TEXT NOT NULL,"
+ "summary TEXT NOT NULL,"
+ "description TEXT NOT NULL)");
+ }
+ if (version < 27)
+ {
+ m_pDS->exec("ALTER TABLE addons ADD news TEXT NOT NULL DEFAULT ''");
+ }
+ if (version < 28)
+ {
+ m_pDS->exec("ALTER TABLE installed ADD disabledReason INTEGER NOT NULL DEFAULT 0");
+ // On adding this field we will use user disabled as the default reason for any disabled addons
+ m_pDS->exec("UPDATE installed SET disabledReason=1 WHERE enabled=0");
+ }
+ if (version < 29)
+ {
+ m_pDS->exec("DROP TABLE broken");
+ }
+ if (version < 30)
+ {
+ m_pDS->exec("ALTER TABLE repo ADD nextcheck TEXT");
+ }
+ if (version < 31)
+ {
+ m_pDS->exec("UPDATE installed SET origin = addonID WHERE (origin='') AND "
+ "EXISTS (SELECT * FROM repo WHERE repo.addonID = installed.addonID)");
+ }
+ if (version < 32)
+ {
+ m_pDS->exec(
+ "CREATE TABLE update_rules (id integer primary key, addonID text, updateRule INTEGER)");
+ m_pDS->exec("INSERT INTO update_rules (addonID, updateRule) SELECT addonID, 1 updateRule FROM "
+ "blacklist");
+ m_pDS->exec("DROP INDEX IF EXISTS idxBlack");
+ m_pDS->exec("DROP TABLE blacklist");
+ }
+ if (version < 33)
+ {
+ m_pDS->query(PrepareSQL("SELECT * FROM addons"));
+ while (!m_pDS->eof())
+ {
+ const int id = m_pDS->fv("id").get_asInt();
+ const std::string metadata = m_pDS->fv("metadata").get_asString();
+
+ CVariant variant;
+ if (!CJSONVariantParser::Parse(metadata, variant))
+ continue;
+
+ // Replace obsolete "broken" with new in json text
+ if (variant.isMember("broken") && variant["broken"].asString().empty())
+ {
+ variant["lifecycletype"] = static_cast<unsigned int>(AddonLifecycleState::BROKEN);
+ variant["lifecycledesc"] = variant["broken"].asString();
+ variant.erase("broken");
+ }
+
+ // Fix wrong added change about "broken" to "lifecycle..." (request 18286)
+ // as there was every addon marked as broken. This checks his text and if
+ // them empty, becomes it declared as normal.
+ if (variant.isMember("lifecycledesc") && variant.isMember("lifecycletype") &&
+ variant["lifecycledesc"].asString().empty() &&
+ variant["lifecycletype"].asUnsignedInteger() !=
+ static_cast<unsigned int>(AddonLifecycleState::NORMAL))
+ {
+ variant["lifecycletype"] = static_cast<unsigned int>(AddonLifecycleState::NORMAL);
+ }
+
+ CVariant variantUpdate;
+ variantUpdate["type"] = variant["extensions"][0].asString();
+ variantUpdate["values"] = CVariant(CVariant::VariantTypeArray);
+ variantUpdate["children"] = CVariant(CVariant::VariantTypeArray);
+
+ for (auto it = variant["extrainfo"].begin_array(); it != variant["extrainfo"].end_array();
+ ++it)
+ {
+ if ((*it)["key"].asString() == "provides")
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = (*it)["key"].asString();
+ info["content"] = CVariant(CVariant::VariantTypeArray);
+
+ CVariant contentEntry(CVariant::VariantTypeObject);
+ contentEntry["key"] = (*it)["key"].asString();
+ contentEntry["value"] = (*it)["value"].asString();
+ info["content"].push_back(std::move(contentEntry));
+
+ variantUpdate["values"].push_back(std::move(info));
+ break;
+ }
+ }
+ variant["extensions"][0] = variantUpdate;
+
+ std::string json;
+ CJSONVariantWriter::Write(variant, json, true);
+ m_pDS->exec(PrepareSQL("UPDATE addons SET metadata='%s' WHERE id=%i", json.c_str(), id));
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+}
+
+void CAddonDatabase::SyncInstalled(const std::set<std::string>& ids,
+ const std::set<std::string>& system,
+ const std::set<std::string>& optional)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ std::set<std::string> db;
+ m_pDS->query(PrepareSQL("SELECT addonID FROM installed"));
+ while (!m_pDS->eof())
+ {
+ db.insert(m_pDS->fv("addonID").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ std::set<std::string> added;
+ std::set<std::string> removed;
+ std::set_difference(ids.begin(), ids.end(), db.begin(), db.end(), std::inserter(added, added.end()));
+ std::set_difference(db.begin(), db.end(), ids.begin(), ids.end(), std::inserter(removed, removed.end()));
+
+ for (const auto& id : added)
+ CLog::Log(LOGDEBUG, "CAddonDatabase: {} has been installed.", id);
+
+ for (const auto& id : removed)
+ CLog::Log(LOGDEBUG, "CAddonDatabase: {} has been uninstalled.", id);
+
+ std::string now = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+ BeginTransaction();
+ for (const auto& id : added)
+ {
+ int enable = 0;
+
+ if (system.find(id) != system.end() || optional.find(id) != optional.end())
+ enable = 1;
+
+ m_pDS->exec(PrepareSQL("INSERT INTO installed(addonID, enabled, installDate) "
+ "VALUES('%s', %d, '%s')", id.c_str(), enable, now.c_str()));
+ }
+
+ for (const auto& id : removed)
+ {
+ m_pDS->exec(PrepareSQL("DELETE FROM installed WHERE addonID='%s'", id.c_str()));
+ RemoveAllUpdateRulesForAddon(id);
+ DeleteRepository(id);
+ }
+
+ for (const auto& id : system)
+ {
+ m_pDS->exec(PrepareSQL("UPDATE installed SET enabled=1 WHERE addonID='%s'", id.c_str()));
+ // Make sure system addons always have ORIGIN_SYSTEM origin
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", ORIGIN_SYSTEM,
+ id.c_str()));
+ }
+
+ for (const auto& id : optional)
+ {
+ // Make sure optional system addons always have ORIGIN_SYSTEM origin
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", ORIGIN_SYSTEM,
+ id.c_str()));
+ }
+
+ CommitTransaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+}
+
+bool CAddonDatabase::SetLastUpdated(const std::string& addonId, const CDateTime& dateTime)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->exec(PrepareSQL("UPDATE installed SET lastUpdated='%s' WHERE addonID='%s'",
+ dateTime.GetAsDBDateTime().c_str(), addonId.c_str()));
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::SetOrigin(const std::string& addonId, const std::string& origin)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", origin.c_str(), addonId.c_str()));
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::SetLastUsed(const std::string& addonId, const CDateTime& dateTime)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+ m_pDS->exec(PrepareSQL("UPDATE installed SET lastUsed='%s' WHERE addonID='%s'",
+ dateTime.GetAsDBDateTime().c_str(), addonId.c_str()));
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase::SetLastUsed[{}] took {} ms", addonId, duration.count());
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::FindByAddonId(const std::string& addonId, ADDON::VECADDONS& result) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL(
+ "SELECT addons.version, addons.name, addons.summary, addons.description, addons.metadata, addons.news,"
+ "repo.addonID AS repoID FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE "
+ "repo.checksum IS NOT NULL AND repo.checksum != '' "
+ "AND EXISTS (SELECT * FROM installed WHERE installed.addonID=repoID AND installed.enabled=1) "
+ "AND addons.addonID='%s'", addonId.c_str());
+
+ m_pDS->query(sql);
+
+ VECADDONS addons;
+ addons.reserve(m_pDS->num_rows());
+
+ while (!m_pDS->eof())
+ {
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(addonId);
+ builder.SetVersion(CAddonVersion(m_pDS->fv("version").get_asString()));
+ builder.SetName(m_pDS->fv("name").get_asString());
+ builder.SetSummary(m_pDS->fv("summary").get_asString());
+ builder.SetDescription(m_pDS->fv("description").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS->fv("metadata").get_asString(), builder);
+ builder.SetChangelog(m_pDS->fv("news").get_asString());
+ builder.SetOrigin(m_pDS->fv("repoID").get_asString());
+
+ auto addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ if (addon)
+ addons.push_back(std::move(addon));
+ else
+ CLog::Log(LOGERROR, "CAddonDatabase: failed to build {}", addonId);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ result = std::move(addons);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetAddon(const std::string& addonID,
+ const CAddonVersion& version,
+ const std::string& repoId,
+ AddonPtr& addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("SELECT addons.id FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE addons.addonID='%s' AND addons.version='%s' AND repo.addonID='%s'",
+ addonID.c_str(), version.asString().c_str(), repoId.c_str());
+
+ m_pDS->query(sql);
+ if (m_pDS->eof())
+ return false;
+
+ return GetAddon(m_pDS->fv("id").get_asInt(), addon);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonID);
+ }
+ return false;
+
+}
+
+bool CAddonDatabase::GetAddon(int id, AddonPtr &addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS2)
+ return false;
+
+ m_pDS2->query(PrepareSQL("SELECT addons.*, repo.addonID as origin FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE addons.id=%i",
+ id));
+ if (m_pDS2->eof())
+ return false;
+
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(m_pDS2->fv("addonID").get_asString());
+ builder.SetOrigin(m_pDS2->fv("origin").get_asString());
+ builder.SetVersion(CAddonVersion(m_pDS2->fv("version").get_asString()));
+ builder.SetName(m_pDS2->fv("name").get_asString());
+ builder.SetSummary(m_pDS2->fv("summary").get_asString());
+ builder.SetDescription(m_pDS2->fv("description").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS2->fv("metadata").get_asString(), builder);
+
+ addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ return addon != nullptr;
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, id);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetRepositoryContent(VECADDONS& addons) const
+{
+ return GetRepositoryContent("", addons);
+}
+
+bool CAddonDatabase::GetRepositoryContent(const std::string& id, VECADDONS& addons) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ // Ensure that the repositories we fetch from are enabled and valid.
+ std::vector<std::string> repoIds;
+ {
+ std::string sql = PrepareSQL(
+ " SELECT repo.id FROM repo"
+ " WHERE repo.checksum IS NOT NULL AND repo.checksum != ''"
+ " AND EXISTS (SELECT * FROM installed WHERE installed.addonID=repo.addonID AND"
+ " installed.enabled=1)");
+
+ if (!id.empty())
+ sql += PrepareSQL(" AND repo.addonId='%s'", id.c_str());
+
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ repoIds.emplace_back(m_pDS->fv("id").get_asString());
+ m_pDS->next();
+ }
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase: SELECT repo.id FROM repo .. took {} ms", duration.count());
+
+ if (repoIds.empty())
+ {
+ if (id.empty())
+ {
+ CLog::Log(LOGDEBUG, "CAddonDatabase: no valid repository, continuing");
+ addons = {};
+ return true;
+ }
+
+ CLog::Log(LOGDEBUG, "CAddonDatabase: no valid repository matching '{}'", id);
+ return false;
+ }
+
+ {
+ std::string sql = PrepareSQL(" SELECT addons.*, repo.addonID AS repoID FROM addons"
+ " JOIN addonlinkrepo ON addons.id=addonlinkrepo.idAddon"
+ " JOIN repo ON repo.id=addonlinkrepo.idRepo"
+ " WHERE addonlinkrepo.idRepo IN (%s)"
+ " ORDER BY repo.addonID, addons.addonID",
+ StringUtils::Join(repoIds, ",").c_str());
+
+ start = std::chrono::steady_clock::now();
+ m_pDS->query(sql);
+
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase: query {} returned {} rows in {} ms", sql,
+ m_pDS->num_rows(), duration.count());
+ }
+
+ VECADDONS result;
+ result.reserve(m_pDS->num_rows());
+
+ while (!m_pDS->eof())
+ {
+ std::string addonId = m_pDS->fv("addonID").get_asString();
+ CAddonVersion version(m_pDS->fv("version").get_asString());
+
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(addonId);
+ builder.SetVersion(version);
+ builder.SetName(m_pDS->fv("name").get_asString());
+ builder.SetSummary(m_pDS->fv("summary").get_asString());
+ builder.SetDescription(m_pDS->fv("description").get_asString());
+ builder.SetOrigin(m_pDS->fv("repoID").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS->fv("metadata").get_asString(), builder);
+
+ auto addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ if (addon)
+ {
+ result.emplace_back(std::move(addon));
+ }
+ else
+ CLog::Log(LOGWARNING, "CAddonDatabase: failed to build {}", addonId);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ addons = std::move(result);
+
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase::GetAddons took {} ms", duration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CAddonDatabase::DeleteRepository(const std::string& id)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ int idRepo = GetRepositoryId(id);
+ if (idRepo < 0)
+ return;
+
+ m_pDS->exec(PrepareSQL("DELETE FROM repo WHERE id=%i", idRepo));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+}
+
+void CAddonDatabase::DeleteRepositoryContents(const std::string& id)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ int idRepo = GetRepositoryId(id);
+ if (idRepo < 0)
+ return;
+
+ m_pDS->exec(PrepareSQL("DELETE FROM addons WHERE id IN (SELECT idAddon FROM addonlinkrepo WHERE idRepo=%i)", idRepo));
+ m_pDS->exec(PrepareSQL("DELETE FROM addonlinkrepo WHERE idRepo=%i", idRepo));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+}
+
+int CAddonDatabase::GetRepositoryId(const std::string& addonId)
+{
+ if (!m_pDB)
+ return -1;
+ if (!m_pDS)
+ return -1;
+
+ m_pDS->query(PrepareSQL("SELECT id FROM repo WHERE addonID='%s'", addonId.c_str()));
+ if (m_pDS->eof())
+ return -1;
+
+ return m_pDS->fv("id").get_asInt();
+}
+
+bool CAddonDatabase::UpdateRepositoryContent(const std::string& repository,
+ const CAddonVersion& version,
+ const std::string& checksum,
+ const std::vector<AddonInfoPtr>& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ DeleteRepositoryContents(repository);
+ int idRepo = GetRepositoryId(repository);
+ if (idRepo < 0)
+ return false;
+
+ assert(idRepo > 0);
+
+ m_pDB->start_transaction();
+ m_pDS->exec(
+ PrepareSQL("UPDATE repo SET checksum='%s' WHERE id='%i'", checksum.c_str(), idRepo));
+ for (const auto& addon : addons)
+ {
+ m_pDS->exec(PrepareSQL(
+ "INSERT INTO addons (id, metadata, addonID, version, name, summary, description, news) "
+ "VALUES (NULL, '%s', '%s', '%s', '%s','%s', '%s','%s')",
+ CAddonDatabaseSerializer::SerializeMetadata(*addon).c_str(), addon->ID().c_str(),
+ addon->Version().asString().c_str(), addon->Name().c_str(), addon->Summary().c_str(),
+ addon->Description().c_str(), addon->ChangeLog().c_str()));
+
+ int idAddon = static_cast<int>(m_pDS->lastinsertid());
+ if (idAddon <= 0)
+ {
+ CLog::Log(LOGERROR, "{} insert failed on addon '{}'", __FUNCTION__, addon->ID());
+ RollbackTransaction();
+ return false;
+ }
+
+ m_pDS->exec(PrepareSQL("INSERT INTO addonlinkrepo (idRepo, idAddon) VALUES (%i, %i)", idRepo, idAddon));
+ }
+
+ m_pDB->commit_transaction();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, repository);
+ RollbackTransaction();
+ }
+ return false;
+}
+
+int CAddonDatabase::GetRepoChecksum(const std::string& id, std::string& checksum)
+{
+ try
+ {
+ if (!m_pDB)
+ return -1;
+ if (!m_pDS)
+ return -1;
+
+ std::string strSQL = PrepareSQL("select * from repo where addonID='%s'",id.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ {
+ checksum = m_pDS->fv("checksum").get_asString();
+ return m_pDS->fv("id").get_asInt();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ checksum.clear();
+ return -1;
+}
+
+CAddonDatabase::RepoUpdateData CAddonDatabase::GetRepoUpdateData(const std::string& id)
+{
+ RepoUpdateData result{};
+ try
+ {
+ if (m_pDB && m_pDS)
+ {
+ std::string strSQL = PrepareSQL("select * from repo where addonID='%s'",id.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ {
+ result.lastCheckedAt.SetFromDBDateTime(m_pDS->fv("lastcheck").get_asString());
+ result.lastCheckedVersion = CAddonVersion(m_pDS->fv("version").get_asString());
+ result.nextCheckAt.SetFromDBDateTime(m_pDS->fv("nextcheck").get_asString());
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ return result;
+}
+
+int CAddonDatabase::SetRepoUpdateData(const std::string& id, const RepoUpdateData& updateData)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ int retId = -1;
+ std::string sql = PrepareSQL("SELECT * FROM repo WHERE addonID='%s'", id.c_str());
+ m_pDS->query(sql);
+
+ if (m_pDS->eof())
+ {
+ sql = PrepareSQL("INSERT INTO repo (id, addonID, lastcheck, version, nextcheck) "
+ "VALUES (NULL, '%s', '%s', '%s', '%s')",
+ id.c_str(), updateData.lastCheckedAt.GetAsDBDateTime().c_str(),
+ updateData.lastCheckedVersion.asString().c_str(),
+ updateData.nextCheckAt.GetAsDBDateTime().c_str());
+ m_pDS->exec(sql);
+ retId = static_cast<int>(m_pDS->lastinsertid());
+ }
+ else
+ {
+ retId = m_pDS->fv("id").get_asInt();
+ sql = PrepareSQL(
+ "UPDATE repo SET lastcheck='%s', version='%s', nextcheck='%s' WHERE addonID='%s'",
+ updateData.lastCheckedAt.GetAsDBDateTime().c_str(),
+ updateData.lastCheckedVersion.asString().c_str(),
+ updateData.nextCheckAt.GetAsDBDateTime().c_str(), id.c_str());
+ m_pDS->exec(sql);
+ }
+
+ return retId;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ return -1;
+}
+
+bool CAddonDatabase::Search(const std::string& search, VECADDONS& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT id FROM addons WHERE name LIKE '%%%s%%' OR summary LIKE '%%%s%%' "
+ "OR description LIKE '%%%s%%'", search.c_str(), search.c_str(), search.c_str());
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL)) return false;
+ if (m_pDS->num_rows() == 0) return false;
+
+ while (!m_pDS->eof())
+ {
+ AddonPtr addon;
+ GetAddon(m_pDS->fv("id").get_asInt(), addon);
+ if (static_cast<int>(addon->Type()) >= static_cast<int>(AddonType::UNKNOWN) + 1 &&
+ static_cast<int>(addon->Type()) < static_cast<int>(AddonType::SCRAPER_LIBRARY))
+ addons.push_back(addon);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::DisableAddon(const std::string& addonID, AddonDisabledReason disabledReason)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("UPDATE installed SET enabled=0, disabledReason=%d WHERE addonID='%s'",
+ static_cast<int>(disabledReason), addonID.c_str());
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::EnableAddon(const std::string& addonID)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql = PrepareSQL(
+ "UPDATE installed SET enabled=1, disabledReason=0 WHERE addonID='%s'", addonID.c_str());
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetDisabled(std::map<std::string, AddonDisabledReason>& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("SELECT addonID, disabledReason FROM installed WHERE enabled=0");
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ addons.insert({m_pDS->fv("addonID").get_asString(),
+ static_cast<AddonDisabledReason>(m_pDS->fv("disabledReason").get_asInt())});
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetAddonUpdateRules(
+ std::map<std::string, std::vector<AddonUpdateRule>>& rulesMap) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT * FROM update_rules");
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ rulesMap[m_pDS->fv("addonID").get_asString()].emplace_back(
+ static_cast<AddonUpdateRule>(m_pDS->fv("updateRule").get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::AddUpdateRuleForAddon(const std::string& addonID, AddonUpdateRule updateRule)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("INSERT INTO update_rules(id, addonID, updateRule) VALUES(NULL, '%s', %d)",
+ addonID.c_str(), static_cast<int>(updateRule));
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::RemoveAllUpdateRulesForAddon(const std::string& addonID)
+{
+ return RemoveUpdateRuleForAddon(addonID, AddonUpdateRule::ANY);
+}
+
+bool CAddonDatabase::RemoveUpdateRuleForAddon(const std::string& addonID,
+ AddonUpdateRule updateRule)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("DELETE FROM update_rules WHERE addonID='%s'", addonID.c_str());
+
+ if (updateRule != AddonUpdateRule::ANY)
+ {
+ sql += PrepareSQL(" AND updateRule = %d", static_cast<int>(updateRule));
+ }
+
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::AddPackage(const std::string& addonID,
+ const std::string& packageFileName,
+ const std::string& hash)
+{
+ std::string sql = PrepareSQL("insert or ignore into package(id, addonID, filename, hash)"
+ "values(NULL, '%s', '%s', '%s')",
+ addonID.c_str(), packageFileName.c_str(), hash.c_str());
+ return ExecuteQuery(sql);
+}
+
+bool CAddonDatabase::GetPackageHash(const std::string& addonID,
+ const std::string& packageFileName,
+ std::string& hash)
+{
+ std::string where = PrepareSQL("addonID='%s' and filename='%s'",
+ addonID.c_str(), packageFileName.c_str());
+ hash = GetSingleValue("package", "hash", where);
+ return !hash.empty();
+}
+
+bool CAddonDatabase::RemovePackage(const std::string& packageFileName)
+{
+ std::string sql = PrepareSQL("delete from package where filename='%s'", packageFileName.c_str());
+ return ExecuteQuery(sql);
+}
+
+void CAddonDatabase::OnPostUnInstall(const std::string& addonId)
+{
+ RemoveAllUpdateRulesForAddon(addonId);
+ DeleteRepository(addonId);
+
+ //! @todo should be done before uninstall to avoid any race conditions
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+ m_pDS->exec(PrepareSQL("DELETE FROM installed WHERE addonID='%s'", addonId.c_str()));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonId);
+ }
+}
+
+void CAddonDatabase::GetInstallData(const AddonInfoPtr& addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ m_pDS->query(PrepareSQL("SELECT addonID, installDate, lastUpdated, lastUsed, "
+ "origin FROM installed WHERE addonID='%s'",
+ addon->ID().c_str()));
+ if (!m_pDS->eof())
+ {
+ CAddonInfoBuilder::SetInstallData(
+ addon, CDateTime::FromDBDateTime(m_pDS->fv("installDate").get_asString()),
+ CDateTime::FromDBDateTime(m_pDS->fv("lastUpdated").get_asString()),
+ CDateTime::FromDBDateTime(m_pDS->fv("lastUsed").get_asString()),
+ m_pDS->fv("origin").get_asString());
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CAddonDatabase::{}: failed", __FUNCTION__);
+ }
+}
+
+bool CAddonDatabase::AddInstalledAddon(const std::shared_ptr<CAddonInfo>& addon,
+ const std::string& origin)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->query(PrepareSQL("SELECT * FROM installed WHERE addonID='%s'", addon->ID().c_str()));
+
+ if (m_pDS->eof())
+ {
+ std::string now = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+
+ m_pDS->exec(PrepareSQL("INSERT INTO installed(addonID, enabled, installDate, origin) "
+ "VALUES('%s', 1, '%s', '%s')",
+ addon->ID().c_str(), now.c_str(), origin.c_str()));
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}