PrismLauncher/launcher/modplatform/EnsureMetadataTask.cpp
Trial97 effa8bedb1
chore(clang-tidy): modernize the code
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-10 21:11:46 +03:00

532 lines
17 KiB
C++

#include "EnsureMetadataTask.h"
#include <MurmurHash2.h>
#include <QDebug>
#include "Application.h"
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/mod/tasks/LocalResourceUpdateTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include "settings/SettingsObject.h"
#include "tasks/ConcurrentTask.h"
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, const QDir& dir, ModPlatform::ResourceProvider prov)
: m_indexDir(dir), m_provider(prov), m_hashingTask(nullptr), m_currentTask(nullptr)
{
auto hashTask = createNewHash(resource);
if (!hashTask) {
return;
}
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](const QString& hash) { m_resources.insert(hash, resource); });
connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
m_hashingTask = hashTask;
}
EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, const QDir& dir, ModPlatform::ResourceProvider prov)
: m_indexDir(dir), m_provider(prov), m_currentTask(nullptr)
{
auto cHashTask = makeShared<ConcurrentTask>("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
m_hashingTask = cHashTask;
for (auto* resource : resources) {
auto hashTask = createNewHash(resource);
if (!hashTask) {
continue;
}
connect(hashTask.get(), &Hashing::Hasher::resultsReady,
[this, resource](const QString& hash) { m_resources.insert(hash, resource); });
connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
cHashTask->addTask(hashTask);
}
}
EnsureMetadataTask::EnsureMetadataTask(QHash<QString, Resource*>& resources, const QDir& dir, ModPlatform::ResourceProvider prov)
: m_resources(resources), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr)
{}
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource)
{
if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) {
return nullptr;
}
return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider);
}
QString EnsureMetadataTask::getExistingHash(Resource* resource)
{
// Check for already computed hashes
// (linear on the number of mods vs. linear on the size of the mod's JAR)
auto it = m_resources.keyValueBegin();
while (it != m_resources.keyValueEnd()) {
if ((*it).second == resource) {
break;
}
it++;
}
// We already have the hash computed
if (it != m_resources.keyValueEnd()) {
return (*it).first;
}
// No existing hash
return {};
}
bool EnsureMetadataTask::abort()
{
// Prevent sending signals to a dead object
QObject::disconnect(this, nullptr, nullptr, nullptr);
if (m_currentTask) {
return m_currentTask->abort();
}
return true;
}
void EnsureMetadataTask::executeTask()
{
setStatus(tr("Checking if resources have metadata..."));
for (auto* resource : m_resources) {
if (!resource->valid()) {
qDebug() << "Resource" << resource->name() << "is invalid!";
emitFail(resource);
continue;
}
// They already have the right metadata :o
if (resource->status() != ResourceStatus::NoMetadata && resource->metadata() && resource->metadata()->provider == m_provider) {
qDebug() << "Resource" << resource->name() << "already has metadata!";
emitReady(resource);
continue;
}
// Folders don't have metadata
if (resource->type() == ResourceType::FOLDER) {
emitReady(resource);
}
}
Task::Ptr versionTask;
switch (m_provider) {
case (ModPlatform::ResourceProvider::MODRINTH):
versionTask = modrinthVersionsTask();
break;
case (ModPlatform::ResourceProvider::FLAME):
versionTask = flameVersionsTask();
break;
}
auto invalidadeLeftover = [this] {
for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) {
emitFail(resource.value(), resource.key(), RemoveFromList::No);
}
m_resources.clear();
emitSucceeded();
};
connect(versionTask.get(), &Task::finished, this, [this, invalidadeLeftover] {
Task::Ptr projectTask;
switch (m_provider) {
case (ModPlatform::ResourceProvider::MODRINTH):
projectTask = modrinthProjectsTask();
break;
case (ModPlatform::ResourceProvider::FLAME):
projectTask = flameProjectsTask();
break;
}
if (!projectTask) {
invalidadeLeftover();
return;
}
connect(projectTask.get(), &Task::finished, this, [this, invalidadeLeftover, projectTask] {
invalidadeLeftover();
projectTask->deleteLater();
if (m_currentTask) {
m_currentTask.reset();
}
});
connect(projectTask.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed);
m_currentTask = projectTask;
projectTask->start();
});
if (m_resources.size() > 1) {
setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider)));
} else if (!m_resources.empty()) {
setStatus(tr("Requesting metadata information from %1 for '%2'...")
.arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name()));
}
m_currentTask = versionTask;
versionTask->start();
}
void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove)
{
if (!resource) {
qCritical() << "Tried to mark a null resource as ready.";
if (!key.isEmpty()) {
m_resources.remove(key);
}
return;
}
qDebug() << QString("Generated metadata for %1").arg(resource->name());
emit metadataReady(resource);
if (remove == RemoveFromList::Yes) {
if (key.isEmpty()) {
key = getExistingHash(resource);
}
m_resources.remove(key);
}
}
void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove)
{
if (!resource) {
qCritical() << "Tried to mark a null resource as failed.";
if (!key.isEmpty()) {
m_resources.remove(key);
}
return;
}
qDebug() << QString("Failed to generate metadata for %1").arg(resource->name());
emit metadataFailed(resource);
if (remove == RemoveFromList::Yes) {
if (key.isEmpty()) {
key = getExistingHash(resource);
}
m_resources.remove(key);
}
}
// Modrinth
Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
auto hashType = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto [verTask, response] = ModrinthAPI::currentVersions(m_resources.keys(), hashType);
// Prevents unfortunate timings when aborting the task
if (!verTask) {
return Task::Ptr{ nullptr };
}
connect(verTask.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
const QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parseError.offset
<< "reason:" << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto entries = Json::requireObject(doc);
for (auto& hash : m_resources.keys()) {
auto* resource = m_resources.find(hash).value();
try {
auto entry = Json::requireObject(entries, hash);
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name()));
qDebug() << "Getting version for" << resource->name() << "from Modrinth";
m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
emitFail(resource);
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
});
return verTask;
}
Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
{
QHash<QString, QString> addonIds;
for (const auto& data : m_tempVersions) {
addonIds.insert(data.addonId.toString(), data.hash);
}
Task::Ptr projTask;
QByteArray* response = nullptr;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
} else if (addonIds.size() == 1) {
std::tie(projTask, response) = ModrinthAPI().getProject(*addonIds.keyBegin());
} else {
std::tie(projTask, response) = ModrinthAPI().getProjects(addonIds.keys());
}
// Prevents unfortunate timings when aborting the task
if (!projTask) {
return Task::Ptr{ nullptr };
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parseError.offset
<< "reason:" << parseError.errorString();
qWarning() << *response;
return;
}
QJsonArray entries;
try {
if (addonIds.size() == 1) {
entries = { doc.object() };
} else {
entries = Json::requireArray(doc);
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
for (auto entry : entries) {
ModPlatform::IndexedPack pack;
try {
auto entryObj = Json::requireObject(entry);
Modrinth::loadIndexedPack(pack, entryObj);
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
// Skip this entry, since it has problems
continue;
}
auto hash = addonIds.find(pack.addonId.toString()).value();
auto resourceIter = m_resources.find(hash);
if (resourceIter == m_resources.end()) {
qWarning() << "Invalid project id from the API response.";
continue;
}
auto* resource = resourceIter.value();
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name()));
updateMetadata(pack, m_tempVersions.find(hash).value(), resource);
}
});
return projTask;
}
// Flame
Task::Ptr EnsureMetadataTask::flameVersionsTask()
{
QList<uint> fingerprints;
for (auto& murmur : m_resources.keys()) {
fingerprints.push_back(murmur.toUInt());
}
auto [verTask, response] = FlameAPI::matchFingerprints(fingerprints);
connect(verTask.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
const QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame::CurrentVersions at" << parseError.offset
<< "reason:" << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto docObj = Json::requireObject(doc);
auto dataObj = Json::requireObject(docObj, "data");
auto dataArr = Json::requireArray(dataObj, "exactMatches");
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
return;
}
for (auto match : dataArr) {
auto matchObj = match.toObject();
auto fileObj = matchObj["file"].toObject();
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
return;
}
auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger());
auto resource = m_resources.find(fingerprint);
if (resource == m_resources.end()) {
qWarning() << "Invalid fingerprint from the API response.";
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name()));
m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(fileObj));
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
});
return verTask;
}
Task::Ptr EnsureMetadataTask::flameProjectsTask()
{
QHash<QString, QString> addonIds;
for (const auto& hash : m_resources.keys()) {
if (m_tempVersions.contains(hash)) {
auto data = m_tempVersions.find(hash).value();
auto idStr = data.addonId.toString();
if (!idStr.isEmpty()) {
addonIds.insert(data.addonId.toString(), hash);
}
}
}
Task::Ptr projTask;
QByteArray* response = nullptr;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
} else if (addonIds.size() == 1) {
std::tie(projTask, response) = FlameAPI().getProject(*addonIds.keyBegin());
} else {
std::tie(projTask, response) = FlameAPI().getProjects(addonIds.keys());
}
// Prevents unfortunate timings when aborting the task
if (!projTask) {
return Task::Ptr{ nullptr };
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame projects task at" << parseError.offset
<< "reason:" << parseError.errorString();
qWarning() << *response;
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1) {
entries = { Json::requireObject(Json::requireObject(doc), "data") };
} else {
entries = Json::requireArray(Json::requireObject(doc), "data");
}
for (auto entry : entries) {
auto entryObj = Json::requireObject(entry);
auto id = QString::number(Json::requireInteger(entryObj, "id"));
auto hash = addonIds.find(id).value();
auto* resource = m_resources.find(hash).value();
ModPlatform::IndexedPack pack;
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
FlameMod::loadIndexedPack(pack, entryObj);
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
emitFail(resource);
}
updateMetadata(pack, m_tempVersions.find(hash).value(), resource);
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
});
return projTask;
}
void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource)
{
try {
// Prevent file name mismatch
ver.fileName = resource->fileinfo().fileName();
if (ver.fileName.endsWith(".disabled")) {
ver.fileName.chop(9);
}
auto task = makeShared<LocalResourceUpdateTask>(m_indexDir, pack, ver);
connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); });
m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task;
task->start();
} catch (Json::JsonException& e) {
qDebug() << e.cause();
emitFail(resource);
}
}
void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource)
{
QDir tmpIndexDir(m_indexDir);
auto metadata = Metadata::get(tmpIndexDir, pack.slug);
if (!metadata.isValid()) {
qCritical() << "Failed to generate metadata at last step!";
emitFail(resource);
return;
}
resource->setMetadata(metadata);
emitReady(resource);
}