PrismLauncher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
flow 6a18079953
refactor: generalize mod models and APIs to resources
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>
2023-01-13 16:23:00 -03:00

197 lines
7.1 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ModrinthPackIndex.h"
#include "ModrinthAPI.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "net/NetJob.h"
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::ensureString(obj, "project_id");
if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id");
pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", "");
if (!pack.slug.isEmpty())
pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else
pack.websiteUrl = "";
pack.description = Json::ensureString(obj, "description", "");
pack.logoUrl = Json::requireString(obj, "icon_url");
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
// Modrinth can have more data than what's provided by the basic search :)
pack.extraDataLoaded = false;
}
void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url");
if (pack.extraData.issuesUrl.endsWith('/'))
pack.extraData.issuesUrl.chop(1);
pack.extraData.sourceUrl = Json::ensureString(obj, "source_url");
if (pack.extraData.sourceUrl.endsWith('/'))
pack.extraData.sourceUrl.chop(1);
pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url");
if (pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
pack.extraData.discordUrl = Json::ensureString(obj, "discord_url");
if (pack.extraData.discordUrl.endsWith('/'))
pack.extraData.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls");
for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d);
ModPlatform::DonationData donate;
donate.id = Json::ensureString(d_obj, "id");
donate.platform = Json::ensureString(d_obj, "platform");
donate.url = Json::ensureString(d_obj, "url");
pack.extraData.donate.append(donate);
}
pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
pack.extraDataLoaded = true;
}
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
QString mcVersion = (static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
-> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
file.addonId = Json::requireString(obj, "project_id");
file.fileId = Json::requireString(obj, "id");
file.date = Json::requireString(obj, "date_published");
auto versionArray = Json::requireArray(obj, "game_versions");
if (versionArray.empty()) {
return {};
}
for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString());
}
auto loaders = Json::requireArray(obj, "loaders");
for (auto loader : loaders) {
file.loaders.append(loader.toString());
}
file.version = Json::requireString(obj, "name");
file.version_number = Json::requireString(obj, "version_number");
file.changelog = Json::requireString(obj, "changelog");
auto files = Json::requireArray(obj, "files");
int i = 0;
if (files.empty()) {
// This should not happen normally, but check just in case
qWarning() << "Modrinth returned an unexpected empty list of files:" << obj;
return {};
}
// Find correct file (needed in cases where one version may have multiple files)
// Will default to the last one if there's no primary (though I think Modrinth requires that
// at least one file is primary, idk)
// NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed
while (i < files.count() - 1) {
auto parent = files[i].toObject();
auto fileName = Json::requireString(parent, "filename");
if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) {
file.is_preferred = true;
break;
}
// Grab the primary file, if available
if (Json::requireBoolean(parent, "primary"))
break;
i++;
}
auto parent = files[i].toObject();
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
if (hash_list.contains(preferred_hash_type)) {
file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type;
} else {
auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
for (auto& hash_type : hash_types) {
if (hash_list.contains(hash_type)) {
file.hash = Json::requireString(hash_list, hash_type);
file.hash_type = hash_type;
break;
}
}
}
return file;
}
return {};
}