diff options
Diffstat (limited to 'launcher/modplatform/EnsureMetadataTask.cpp')
-rw-r--r-- | launcher/modplatform/EnsureMetadataTask.cpp | 550 |
1 files changed, 413 insertions, 137 deletions
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index dc92d8ab..cf4e55b9 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -19,226 +19,502 @@ 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() +EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) { - return m_task_handler->abort(); + auto hash = getHash(mod); + if (hash.isEmpty()) + emitFail(mod); + else + m_mods.insert(hash, mod); } -void EnsureMetadataTask::executeTask() +EnsureMetadataTask::EnsureMetadataTask(std::list<Mod>& mods, QDir dir, ModPlatform::Provider prov) + : Task(nullptr), m_index_dir(dir), m_provider(prov) { - // They already have the right metadata :o - if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) { - emitReady(); - return; - } + for (auto& mod : mods) { + if (!mod.valid()) { + emitFail(mod); + continue; + } - // Folders don't have metadata - if (m_mod.type() == Mod::MOD_FOLDER) { - emitReady(); - return; - } + auto hash = getHash(mod); + if (hash.isEmpty()) { + emitFail(mod); + continue; + } - setStatus(tr("Generating %1's metadata...").arg(m_mod.name())); - qDebug() << QString("Generating %1's metadata...").arg(m_mod.name()); + m_mods.insert(hash, mod); + } +} +QString EnsureMetadataTask::getHash(Mod& mod) +{ + /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */ QByteArray jar_data; - try { - jar_data = FS::read(m_mod.fileinfo().absoluteFilePath()); + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name()); + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); qCritical() << QString("Reason: ") << e.cause(); - emitFail(); - return; + return {}; } - auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask"); + switch (m_provider) { + case ModPlatform::Provider::MODRINTH: { + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + + return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); + } + case ModPlatform::Provider::FLAME: { + 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); + } + + return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length())); + } + } + + 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()) + 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); + return; + } + + // Folders don't have metadata + if (mod.type() == Mod::MOD_FOLDER) { + emitReady(mod); + return; + } + } + + NetJob::Ptr version_task; switch (m_provider) { case (ModPlatform::Provider::MODRINTH): - modrinthEnsureMetadata(*tsk, jar_data); - if (m_try_all) - flameEnsureMetadata(*tsk, jar_data); - + version_task = modrinthVersionsTask(); break; case (ModPlatform::Provider::FLAME): - flameEnsureMetadata(*tsk, jar_data); - if (m_try_all) - modrinthEnsureMetadata(*tsk, jar_data); - + version_task = flameVersionsTask(); 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()); + auto invalidade_leftover = [this] { + QMutableHashIterator<QString, Mod> mods_iter(m_mods); + while (mods_iter.hasNext()) { + auto mod = mods_iter.next(); + emitFail(mod.value()); + } + + emitSucceeded(); + }; + + connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { + NetJob::Ptr project_task; + + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + project_task = modrinthProjectsTask(); + break; + case (ModPlatform::Provider::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; + }); - emitFail(); + m_current_task = project_task.get(); + project_task->start(); }); - connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady); - m_task_handler = tsk; + 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())); - tsk->start(); + m_current_task = version_task.get(); + version_task->start(); } -void EnsureMetadataTask::emitReady() +void EnsureMetadataTask::emitReady(Mod& m) { - emit metadataReady(); - emitSucceeded(); + qDebug() << QString("Generated metadata for %1").arg(m.name()); + emit metadataReady(m); + + m_mods.remove(getHash(m)); } -void EnsureMetadataTask::emitFail() +void EnsureMetadataTask::emitFail(Mod& m) { - 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(); + qDebug() << QString("Failed to generate metadata for %1").arg(m.name()); + emit metadataFailed(m); + + m_mods.remove(getHash(m)); } -void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +// Modrinth + +NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() { - // 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; + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); - 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; + auto* response = new QByteArray(); + auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); - ver_task->failed(parse_error.errorString()); - return; + // 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; + } + }); - auto doc_obj = Json::requireObject(doc); - auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName()); + 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; + } - // Minimal IndexedPack to create the metadata - ModPlatform::IndexedPack pack; - pack.name = m_mod.name(); - pack.provider = ModPlatform::Provider::MODRINTH; - pack.addonId = ver.addonId; + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { doc.object() }; + else + entries = Json::requireArray(doc); - // Prevent file name mismatch - ver.fileName = m_mod.fileinfo().fileName(); + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + auto entry_id = Json::requireString(entry_obj, "id"); - QDir tmp_index_dir(m_index_dir); + auto hash = addonIds.find(entry_id).value(); - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; - QTimer timeout; + auto mod = m_mods.find(hash).value(); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + try { + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name())); - update_metadata.start(); - timeout.start(100); + ModPlatform::IndexedPack pack; + Modrinth::loadIndexedPack(pack, entry_obj); - loop.exec(); - } + modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; - auto mod_name = m_mod.name(); - auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); - m_mod.setMetadata(meta); - }); + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); - tsk.addTask(ver_task); - } + return proj_task; } -void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +// Flame +NetJob::Ptr EnsureMetadataTask::flameVersionsTask() { - 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); + 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, ver_task, response] { - QDir tmp_index_dir(m_index_dir); - + 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 " << m_mod.name() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; - ver_task->failed(parse_error.errorString()); + 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!"; + 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!"; - ver_task->failed(parse_error.errorString()); return; } - auto file_obj = Json::ensureObject(match_obj, "file"); + for (auto match : data_arr) { + auto match_obj = Json::ensureObject(match, {}); + 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"); + if (match_obj.isEmpty() || file_obj.isEmpty()) { + qWarning() << "Fingerprint match is empty!"; - ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj); + return; + } - // Prevent file name mismatch - ver.fileName = m_mod.fileinfo().fileName(); + 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; + } - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; - QTimer timeout; + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); + } - update_metadata.start(); - timeout.start(100); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); - loop.exec(); - } + 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(); + 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())); - auto mod_name = m_mod.name(); - auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); - m_mod.setMetadata(meta); + 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) { - emitFailed(e.cause() + " : " + e.what()); + qDebug() << e.cause(); + qDebug() << doc; } }); - tsk.addTask(ver_task); + return proj_task; +} + +void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod) +{ + // Prevent file name mismatch + ver.fileName = mod.fileinfo().fileName(); + + 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(); + + 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); + } } |