6a18079953
Firstly, this abstract away behavior in the mod download models that can also be applied to other types of resources into a superclass, allowing other resource types to be implemented without so much code duplication. For that, this also generalizes the APIs used (currently, ModrinthAPI and FlameAPI) to be able to make requests to other types of resources. It also does a general cleanup of both of those. In particular, this makes use of std::optional instead of invalid values for errors and, well, optional values :p This is a squash of some commits that were becoming too interlaced together to be cleanly separated. Signed-off-by: flow <flowlnlnln@gmail.com>
549 lines
17 KiB
C++
549 lines
17 KiB
C++
#include "EnsureMetadataTask.h"
|
|
|
|
#include <MurmurHash2.h>
|
|
#include <QDebug>
|
|
|
|
#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"
|
|
|
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
|
|
|
static ModrinthAPI modrinth_api;
|
|
static FlameAPI flame_api;
|
|
|
|
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov)
|
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
|
|
{
|
|
auto hash_task = createNewHash(mod);
|
|
if (!hash_task)
|
|
return;
|
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
|
hash_task->start();
|
|
}
|
|
|
|
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::ResourceProvider prov)
|
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
|
|
{
|
|
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
|
|
for (auto* mod : mods) {
|
|
auto hash_task = createNewHash(mod);
|
|
if (!hash_task)
|
|
continue;
|
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
|
m_hashing_task->addTask(hash_task);
|
|
}
|
|
}
|
|
|
|
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
|
|
{
|
|
if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER)
|
|
return nullptr;
|
|
|
|
return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider);
|
|
}
|
|
|
|
QString EnsureMetadataTask::getExistingHash(Mod* mod)
|
|
{
|
|
// Check for already computed hashes
|
|
// (linear on the number of mods vs. linear on the size of the mod's JAR)
|
|
auto it = m_mods.keyValueBegin();
|
|
while (it != m_mods.keyValueEnd()) {
|
|
if ((*it).second == mod)
|
|
break;
|
|
it++;
|
|
}
|
|
|
|
// We already have the hash computed
|
|
if (it != m_mods.keyValueEnd()) {
|
|
return (*it).first;
|
|
}
|
|
|
|
// No existing hash
|
|
return {};
|
|
}
|
|
|
|
bool EnsureMetadataTask::abort()
|
|
{
|
|
// Prevent sending signals to a dead object
|
|
disconnect(this, 0, 0, 0);
|
|
|
|
if (m_current_task)
|
|
return m_current_task->abort();
|
|
return true;
|
|
}
|
|
|
|
void EnsureMetadataTask::executeTask()
|
|
{
|
|
setStatus(tr("Checking if mods have metadata..."));
|
|
|
|
for (auto* mod : m_mods) {
|
|
if (!mod->valid()) {
|
|
qDebug() << "Mod" << mod->name() << "is invalid!";
|
|
emitFail(mod);
|
|
continue;
|
|
}
|
|
|
|
// They already have the right metadata :o
|
|
if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) {
|
|
qDebug() << "Mod" << mod->name() << "already has metadata!";
|
|
emitReady(mod);
|
|
continue;
|
|
}
|
|
|
|
// Folders don't have metadata
|
|
if (mod->type() == ResourceType::FOLDER) {
|
|
emitReady(mod);
|
|
}
|
|
}
|
|
|
|
NetJob::Ptr version_task;
|
|
|
|
switch (m_provider) {
|
|
case (ModPlatform::ResourceProvider::MODRINTH):
|
|
version_task = modrinthVersionsTask();
|
|
break;
|
|
case (ModPlatform::ResourceProvider::FLAME):
|
|
version_task = flameVersionsTask();
|
|
break;
|
|
}
|
|
|
|
auto invalidade_leftover = [this] {
|
|
for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++)
|
|
emitFail(mod.value(), mod.key(), RemoveFromList::No);
|
|
m_mods.clear();
|
|
|
|
emitSucceeded();
|
|
};
|
|
|
|
connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
|
|
NetJob::Ptr project_task;
|
|
|
|
switch (m_provider) {
|
|
case (ModPlatform::ResourceProvider::MODRINTH):
|
|
project_task = modrinthProjectsTask();
|
|
break;
|
|
case (ModPlatform::ResourceProvider::FLAME):
|
|
project_task = flameProjectsTask();
|
|
break;
|
|
}
|
|
|
|
if (!project_task) {
|
|
invalidade_leftover();
|
|
return;
|
|
}
|
|
|
|
connect(project_task.get(), &Task::finished, this, [=] {
|
|
invalidade_leftover();
|
|
project_task->deleteLater();
|
|
m_current_task = nullptr;
|
|
});
|
|
|
|
m_current_task = project_task.get();
|
|
project_task->start();
|
|
});
|
|
|
|
connect(version_task.get(), &Task::finished, [=] {
|
|
version_task->deleteLater();
|
|
m_current_task = nullptr;
|
|
});
|
|
|
|
if (m_mods.size() > 1)
|
|
setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
|
|
else if (!m_mods.empty())
|
|
setStatus(tr("Requesting metadata information from %1 for '%2'...")
|
|
.arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
|
|
|
|
m_current_task = version_task.get();
|
|
version_task->start();
|
|
}
|
|
|
|
void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove)
|
|
{
|
|
if (!m) {
|
|
qCritical() << "Tried to mark a null mod as ready.";
|
|
if (!key.isEmpty())
|
|
m_mods.remove(key);
|
|
|
|
return;
|
|
}
|
|
|
|
qDebug() << QString("Generated metadata for %1").arg(m->name());
|
|
emit metadataReady(m);
|
|
|
|
if (remove == RemoveFromList::Yes) {
|
|
if (key.isEmpty())
|
|
key = getExistingHash(m);
|
|
m_mods.remove(key);
|
|
}
|
|
}
|
|
|
|
void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
|
|
{
|
|
if (!m) {
|
|
qCritical() << "Tried to mark a null mod as failed.";
|
|
if (!key.isEmpty())
|
|
m_mods.remove(key);
|
|
|
|
return;
|
|
}
|
|
|
|
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
|
|
emit metadataFailed(m);
|
|
|
|
if (remove == RemoveFromList::Yes) {
|
|
if (key.isEmpty())
|
|
key = getExistingHash(m);
|
|
m_mods.remove(key);
|
|
}
|
|
}
|
|
|
|
// Modrinth
|
|
|
|
NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
|
|
{
|
|
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
|
|
|
|
auto* response = new QByteArray();
|
|
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
|
|
|
|
// Prevents unfortunate timings when aborting the task
|
|
if (!ver_task)
|
|
return {};
|
|
|
|
connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
|
|
QJsonParseError parse_error{};
|
|
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
|
if (parse_error.error != QJsonParseError::NoError) {
|
|
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
|
|
<< " reason: " << parse_error.errorString();
|
|
qWarning() << *response;
|
|
|
|
failed(parse_error.errorString());
|
|
return;
|
|
}
|
|
|
|
try {
|
|
auto entries = Json::requireObject(doc);
|
|
for (auto& hash : m_mods.keys()) {
|
|
auto mod = m_mods.find(hash).value();
|
|
try {
|
|
auto entry = Json::requireObject(entries, hash);
|
|
|
|
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
|
|
qDebug() << "Getting version for" << mod->name() << "from Modrinth";
|
|
|
|
m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << entries;
|
|
|
|
emitFail(mod);
|
|
}
|
|
}
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return ver_task;
|
|
}
|
|
|
|
NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
|
|
{
|
|
QHash<QString, QString> addonIds;
|
|
for (auto const& data : m_temp_versions)
|
|
addonIds.insert(data.addonId.toString(), data.hash);
|
|
|
|
auto response = new QByteArray();
|
|
NetJob::Ptr proj_task;
|
|
|
|
if (addonIds.isEmpty()) {
|
|
qWarning() << "No addonId found!";
|
|
} else if (addonIds.size() == 1) {
|
|
proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
|
|
} else {
|
|
proj_task = modrinth_api.getProjects(addonIds.keys(), response);
|
|
}
|
|
|
|
// Prevents unfortunate timings when aborting the task
|
|
if (!proj_task)
|
|
return {};
|
|
|
|
connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
|
|
QJsonParseError parse_error{};
|
|
auto doc = QJsonDocument::fromJson(*response, &parse_error);
|
|
if (parse_error.error != QJsonParseError::NoError) {
|
|
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
|
|
<< " reason: " << parse_error.errorString();
|
|
qWarning() << *response;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
QJsonArray entries;
|
|
if (addonIds.size() == 1)
|
|
entries = { doc.object() };
|
|
else
|
|
entries = Json::requireArray(doc);
|
|
|
|
for (auto entry : entries) {
|
|
auto entry_obj = Json::requireObject(entry);
|
|
|
|
ModPlatform::IndexedPack pack;
|
|
Modrinth::loadIndexedPack(pack, entry_obj);
|
|
|
|
auto hash = addonIds.find(pack.addonId.toString()).value();
|
|
|
|
auto mod_iter = m_mods.find(hash);
|
|
if (mod_iter == m_mods.end()) {
|
|
qWarning() << "Invalid project id from the API response.";
|
|
continue;
|
|
}
|
|
|
|
auto* mod = mod_iter.value();
|
|
|
|
try {
|
|
setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
|
|
|
|
modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << entries;
|
|
|
|
emitFail(mod);
|
|
}
|
|
}
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return proj_task;
|
|
}
|
|
|
|
// Flame
|
|
NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
|
|
{
|
|
auto* response = new QByteArray();
|
|
|
|
QList<uint> fingerprints;
|
|
for (auto& murmur : m_mods.keys()) {
|
|
fingerprints.push_back(murmur.toUInt());
|
|
}
|
|
|
|
auto ver_task = flame_api.matchFingerprints(fingerprints, response);
|
|
|
|
connect(ver_task.get(), &Task::succeeded, this, [this, response] {
|
|
QJsonParseError parse_error{};
|
|
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
|
if (parse_error.error != QJsonParseError::NoError) {
|
|
qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
|
|
<< " reason: " << parse_error.errorString();
|
|
qWarning() << *response;
|
|
|
|
failed(parse_error.errorString());
|
|
return;
|
|
}
|
|
|
|
try {
|
|
auto doc_obj = Json::requireObject(doc);
|
|
auto data_obj = Json::requireObject(doc_obj, "data");
|
|
auto data_arr = Json::requireArray(data_obj, "exactMatches");
|
|
|
|
if (data_arr.isEmpty()) {
|
|
qWarning() << "No matches found for fingerprint search!";
|
|
|
|
return;
|
|
}
|
|
|
|
for (auto match : data_arr) {
|
|
auto match_obj = Json::ensureObject(match, {});
|
|
auto file_obj = Json::ensureObject(match_obj, "file", {});
|
|
|
|
if (match_obj.isEmpty() || file_obj.isEmpty()) {
|
|
qWarning() << "Fingerprint match is empty!";
|
|
|
|
return;
|
|
}
|
|
|
|
auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
|
|
auto mod = m_mods.find(fingerprint);
|
|
if (mod == m_mods.end()) {
|
|
qWarning() << "Invalid fingerprint from the API response.";
|
|
continue;
|
|
}
|
|
|
|
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name()));
|
|
|
|
m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
|
|
}
|
|
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return ver_task;
|
|
}
|
|
|
|
NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
|
|
{
|
|
QHash<QString, QString> addonIds;
|
|
for (auto const& hash : m_mods.keys()) {
|
|
if (m_temp_versions.contains(hash)) {
|
|
auto const& data = m_temp_versions.find(hash).value();
|
|
|
|
auto id_str = data.addonId.toString();
|
|
if (!id_str.isEmpty())
|
|
addonIds.insert(data.addonId.toString(), hash);
|
|
}
|
|
}
|
|
|
|
auto response = new QByteArray();
|
|
NetJob::Ptr proj_task;
|
|
|
|
if (addonIds.isEmpty()) {
|
|
qWarning() << "No addonId found!";
|
|
} else if (addonIds.size() == 1) {
|
|
proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
|
|
} else {
|
|
proj_task = flame_api.getProjects(addonIds.keys(), response);
|
|
}
|
|
|
|
// Prevents unfortunate timings when aborting the task
|
|
if (!proj_task)
|
|
return {};
|
|
|
|
connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
|
|
QJsonParseError parse_error{};
|
|
auto doc = QJsonDocument::fromJson(*response, &parse_error);
|
|
if (parse_error.error != QJsonParseError::NoError) {
|
|
qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
|
|
<< " reason: " << parse_error.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 entry_obj = Json::requireObject(entry);
|
|
|
|
auto id = QString::number(Json::requireInteger(entry_obj, "id"));
|
|
auto hash = addonIds.find(id).value();
|
|
auto mod = m_mods.find(hash).value();
|
|
|
|
try {
|
|
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name()));
|
|
|
|
ModPlatform::IndexedPack pack;
|
|
FlameMod::loadIndexedPack(pack, entry_obj);
|
|
|
|
flameCallback(pack, m_temp_versions.find(hash).value(), mod);
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << entries;
|
|
|
|
emitFail(mod);
|
|
}
|
|
}
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
qDebug() << doc;
|
|
}
|
|
});
|
|
|
|
return proj_task;
|
|
}
|
|
|
|
void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
|
|
{
|
|
// Prevent file name mismatch
|
|
ver.fileName = mod->fileinfo().fileName();
|
|
if (ver.fileName.endsWith(".disabled"))
|
|
ver.fileName.chop(9);
|
|
|
|
QDir tmp_index_dir(m_index_dir);
|
|
|
|
{
|
|
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
|
|
QEventLoop loop;
|
|
|
|
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
|
|
|
|
update_metadata.start();
|
|
|
|
if (!update_metadata.isFinished())
|
|
loop.exec();
|
|
}
|
|
|
|
auto metadata = Metadata::get(tmp_index_dir, pack.slug);
|
|
if (!metadata.isValid()) {
|
|
qCritical() << "Failed to generate metadata at last step!";
|
|
emitFail(mod);
|
|
return;
|
|
}
|
|
|
|
mod->setMetadata(metadata);
|
|
|
|
emitReady(mod);
|
|
}
|
|
|
|
void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
|
|
{
|
|
try {
|
|
// Prevent file name mismatch
|
|
ver.fileName = mod->fileinfo().fileName();
|
|
if (ver.fileName.endsWith(".disabled"))
|
|
ver.fileName.chop(9);
|
|
|
|
QDir tmp_index_dir(m_index_dir);
|
|
|
|
{
|
|
LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
|
|
QEventLoop loop;
|
|
|
|
QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
|
|
|
|
update_metadata.start();
|
|
|
|
if (!update_metadata.isFinished())
|
|
loop.exec();
|
|
}
|
|
|
|
auto metadata = Metadata::get(tmp_index_dir, pack.slug);
|
|
if (!metadata.isValid()) {
|
|
qCritical() << "Failed to generate metadata at last step!";
|
|
emitFail(mod);
|
|
return;
|
|
}
|
|
|
|
mod->setMetadata(metadata);
|
|
|
|
emitReady(mod);
|
|
} catch (Json::JsonException& e) {
|
|
qDebug() << e.cause();
|
|
|
|
emitFail(mod);
|
|
}
|
|
}
|