feat: add EnsureMetadataTask

This task is responsible for checking if the mod has metadata for a
specific provider, and create it if it doesn't.

In the context of the mod updater, this is not the best architecture,
since we do a single task for each mod. However, this way of structuring
it allows us to use it later on in more diverse scenarios.

This way we decouple this task from the mod updater, trading off some performance
(though that will be mitigated when we have a way of running arbitrary tasks
concurrently).

Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
flow 2022-06-03 19:08:01 -03:00
parent 4bcf8e6975
commit 844b245776
No known key found for this signature in database
GPG Key ID: 8D0F221F0A59F469
3 changed files with 288 additions and 0 deletions

View File

@ -482,6 +482,9 @@ set(API_SOURCES
modplatform/ModAPI.h modplatform/ModAPI.h
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.h
modplatform/flame/FlameAPI.cpp modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.h

View File

@ -0,0 +1,244 @@
#include "EnsureMetadataTask.h"
#include <MurmurHash2.h>
#include <QDebug>
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include "net/NetJob.h"
#include "tasks/MultipleOptionsTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir& dir, bool try_all, ModPlatform::Provider prov)
: m_mod(mod), m_index_dir(dir), m_provider(prov), m_try_all(try_all)
{}
bool EnsureMetadataTask::abort()
{
return m_task_handler->abort();
}
void EnsureMetadataTask::executeTask()
{
// They already have the right metadata :o
if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) {
emitReady();
return;
}
// Folders don't have metadata
if (m_mod.type() == Mod::MOD_FOLDER) {
emitReady();
return;
}
setStatus(tr("Generating %1's metadata...").arg(m_mod.name()));
qDebug() << QString("Generating %1's metadata...").arg(m_mod.name());
QByteArray jar_data;
try {
jar_data = FS::read(m_mod.fileinfo().absoluteFilePath());
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name());
qCritical() << QString("Reason: ") << e.cause();
emitFail();
return;
}
auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask");
switch (m_provider) {
case (ModPlatform::Provider::MODRINTH):
modrinthEnsureMetadata(*tsk, jar_data);
if (m_try_all)
flameEnsureMetadata(*tsk, jar_data);
break;
case (ModPlatform::Provider::FLAME):
flameEnsureMetadata(*tsk, jar_data);
if (m_try_all)
modrinthEnsureMetadata(*tsk, jar_data);
break;
}
connect(tsk, &MultipleOptionsTask::finished, this, [tsk] { tsk->deleteLater(); });
connect(tsk, &MultipleOptionsTask::failed, [this] {
qCritical() << QString("Download of %1's metadata failed").arg(m_mod.name());
emitFail();
});
connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady);
m_task_handler = tsk;
tsk->start();
}
void EnsureMetadataTask::emitReady()
{
emit metadataReady();
emitSucceeded();
}
void EnsureMetadataTask::emitFail()
{
qDebug() << QString("Failed to generate metadata for %1").arg(m_mod.name());
emit metadataFailed();
//emitFailed(tr("Failed to generate metadata for %1").arg(m_mod.name()));
emitSucceeded();
}
void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
{
// Modrinth currently garantees that some hash types will always be present.
// But let's be sure and cover all cases anyways :)
for (auto hash_type : ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)) {
auto* response = new QByteArray();
auto hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
auto ver_task = modrinth_api.currentVersion(hash, hash_type, response);
// Prevents unfortunate timings when aborting the task
if (!ver_task)
return;
connect(ver_task.get(), &NetJob::succeeded, this, [this, ver_task, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
ver_task->failed(parse_error.errorString());
return;
}
auto doc_obj = Json::requireObject(doc);
auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName());
// Minimal IndexedPack to create the metadata
ModPlatform::IndexedPack pack;
pack.name = m_mod.name();
pack.provider = ModPlatform::Provider::MODRINTH;
pack.addonId = ver.addonId;
// Prevent file name mismatch
ver.fileName = m_mod.fileinfo().fileName();
QDir tmp_index_dir(m_index_dir);
{
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
QEventLoop loop;
QTimer timeout;
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
update_metadata.start();
timeout.start(100);
loop.exec();
}
auto mod_name = m_mod.name();
auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
m_mod.setMetadata(meta);
});
tsk.addTask(ver_task);
}
}
void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data)
{
QByteArray jar_data_treated;
for (char c : jar_data) {
// CF-specific
if (!(c == 9 || c == 10 || c == 13 || c == 32))
jar_data_treated.push_back(c);
}
auto* response = new QByteArray();
std::list<uint> fingerprints;
auto murmur = MurmurHash2(jar_data_treated, jar_data_treated.length());
fingerprints.push_back(murmur);
auto ver_task = flame_api.matchFingerprints(fingerprints, response);
connect(ver_task.get(), &Task::succeeded, this, [this, ver_task, response] {
QDir tmp_index_dir(m_index_dir);
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
ver_task->failed(parse_error.errorString());
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::ensureObject(doc_obj, "data");
auto match_obj = Json::ensureObject(Json::ensureArray(data_obj, "exactMatches")[0], {});
if (match_obj.isEmpty()) {
qCritical() << "Fingerprint match is empty!";
ver_task->failed(parse_error.errorString());
return;
}
auto file_obj = Json::ensureObject(match_obj, "file");
ModPlatform::IndexedPack pack;
pack.name = m_mod.name();
pack.provider = ModPlatform::Provider::FLAME;
pack.addonId = Json::requireInteger(file_obj, "modId");
ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj);
// Prevent file name mismatch
ver.fileName = m_mod.fileinfo().fileName();
{
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
QEventLoop loop;
QTimer timeout;
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
update_metadata.start();
timeout.start(100);
loop.exec();
}
auto mod_name = m_mod.name();
auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name));
m_mod.setMetadata(meta);
} catch (Json::JsonException& e) {
emitFailed(e.cause() + " : " + e.what());
}
});
tsk.addTask(ver_task);
}

View File

@ -0,0 +1,41 @@
#pragma once
#include "ModIndex.h"
#include "tasks/SequentialTask.h"
class Mod;
class QDir;
class MultipleOptionsTask;
class EnsureMetadataTask : public Task {
Q_OBJECT
public:
EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
public slots:
bool abort() override;
protected slots:
void executeTask() override;
private:
// FIXME: Move to their own namespace
void modrinthEnsureMetadata(SequentialTask&, QByteArray&);
void flameEnsureMetadata(SequentialTask&, QByteArray&);
// Helpers
void emitReady();
void emitFail();
signals:
void metadataReady();
void metadataFailed();
private:
Mod& m_mod;
QDir& m_index_dir;
ModPlatform::Provider m_provider;
bool m_try_all;
MultipleOptionsTask* m_task_handler = nullptr;
};