From 93ad15fb974bd928754dc400bf146c4628994ec2 Mon Sep 17 00:00:00 2001 From: MelanX Date: Tue, 9 Aug 2022 22:02:14 +0200 Subject: [PATCH 1/6] Add modrinth support --- .gitignore | 1 + gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 10 +- .../modlistcreator/ModListCreator.java | 52 +++-- .../modlistcreator/curse/CurseModpack.java | 147 ------------- .../modlistcreator/platform/Modpack.java | 77 +++++++ .../platform/curse/CurseModpack.java | 120 ++++++++++ .../platform/modrinth/ModrinthModpack.java | 205 ++++++++++++++++++ .../modlistcreator/types/FileBase.java | 14 +- .../modlistcreator/types/files/HtmlFile.java | 21 +- .../types/files/MarkdownFile.java | 21 +- 13 files changed, 471 insertions(+), 201 deletions(-) delete mode 100644 src/main/java/org/moddingx/modlistcreator/curse/CurseModpack.java create mode 100644 src/main/java/org/moddingx/modlistcreator/platform/Modpack.java create mode 100644 src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java create mode 100644 src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java diff --git a/.gitignore b/.gitignore index 170d309..c6b96a6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ output/ modlist.html modlist.md manifest.json +*.mrpack diff --git a/gradle.properties b/gradle.properties index bad42ed..5b2a680 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.0.1 +version=3.1.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..41d9927a4d4fb3f96a785543079b8df6723c946b 100644 GIT binary patch delta 8958 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fc..8049c68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index c53aefa..1b6c787 100644 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # diff --git a/src/main/java/org/moddingx/modlistcreator/ModListCreator.java b/src/main/java/org/moddingx/modlistcreator/ModListCreator.java index 984350b..8a017af 100644 --- a/src/main/java/org/moddingx/modlistcreator/ModListCreator.java +++ b/src/main/java/org/moddingx/modlistcreator/ModListCreator.java @@ -1,15 +1,17 @@ package org.moddingx.modlistcreator; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; import org.moddingx.cursewrapper.api.CurseWrapper; -import org.moddingx.modlistcreator.curse.CurseModpack; +import org.moddingx.modlistcreator.platform.Modpack; import org.moddingx.modlistcreator.types.FileBase; import org.moddingx.modlistcreator.types.files.HtmlFile; import org.moddingx.modlistcreator.types.files.MarkdownFile; import org.moddingx.modlistcreator.util.NameFormat; -import joptsimple.ArgumentAcceptingOptionSpec; -import joptsimple.OptionParser; -import joptsimple.OptionSet; -import joptsimple.OptionSpec; import java.io.File; import java.io.IOException; @@ -17,13 +19,22 @@ import java.net.URI; import java.nio.file.*; import java.util.*; +import java.util.function.Predicate; public class ModListCreator { + + public static Gson GSON; private static OptionSet optionSet; - private static CurseWrapper wrapper; + private static final CurseWrapper wrapper = new CurseWrapper(URI.create("https://curse.melanx.de/")); + public static final Predicate filePredicate = file -> file.getName().equals("modrinth.index.json") || file.getName().equals("manifest.json"); + + static { + GsonBuilder builder = new GsonBuilder(); + builder.disableHtmlEscaping(); + GSON = builder.create(); + } public static void main(String[] args) throws InterruptedException, IOException { - wrapper = new CurseWrapper(URI.create("https://curse.melanx.de/")); OptionParser parser = new OptionParser(); List markdown = new ArrayList<>(); Collections.addAll(markdown, "md", "markdown"); @@ -71,7 +82,10 @@ public static void main(String[] args) throws InterruptedException, IOException file = zip; } - CurseModpack pack = CurseModpack.fromManifest(file); + if (!filePredicate.test(file) && file != zip) { + continue; + } + Modpack pack = Modpack.fromJson(file); if (pack == null) { continue; @@ -89,12 +103,16 @@ public static void main(String[] args) throws InterruptedException, IOException } } else { File file = optionSet.has(manifest) ? getValue(optionSet, manifest) : Paths.get("manifest.json").toFile(); + if (file == null) { + file = Paths.get("modrinth.index.json").toFile(); + } + File zip = getManifestFromZip(outDir, file); if (zip != null) { file = zip; } - CurseModpack pack = CurseModpack.fromManifest(file); + Modpack pack = Modpack.fromJson(file); if (pack != null) { generateForPack( @@ -115,7 +133,7 @@ public static CurseWrapper getWrapper() { return wrapper; } - private static void generateForPack(Set joins, CurseModpack pack, String name, File output) { + private static void generateForPack(Set joins, Modpack pack, String name, File output) { boolean alreadyGenerated = false; boolean detailed = optionSet.has("detailed"); boolean headless = optionSet.has("headless"); @@ -152,15 +170,15 @@ private static void generateForPack(Set joins, CurseModpack pack, String } } - private static String getFileName(NameFormat format, CurseModpack pack) { + private static String getFileName(NameFormat format, Modpack pack) { return getFileName(format, pack, ""); } - private static String getFileName(NameFormat format, CurseModpack pack, String prefix) { + private static String getFileName(NameFormat format, Modpack pack, String prefix) { return switch (format) { - case NAME -> pack.getName(); - case VERSION -> pack.getVersion(); - case NAME_VERSION -> pack.getName() + " - " + pack.getVersion(); + case NAME -> pack.title(); + case VERSION -> pack.version(); + case NAME_VERSION -> pack.title() + " - " + pack.version(); case DEFAULT -> !prefix.isEmpty() ? prefix + "-modlist" : "modlist"; }; } @@ -190,6 +208,10 @@ private static File getManifestFromZip(File output, File input) { // Getting manifest.json from zip try (FileSystem fs = FileSystems.newFileSystem(input.toPath(), (ClassLoader) null)) { Path zipManifest = fs.getPath("manifest.json"); + if (!Files.exists(zipManifest)) { + zipManifest = fs.getPath("modrinth.index.json"); + } + if (Files.exists(zipManifest)) { if (!output.exists()) { if (output.mkdirs()) { diff --git a/src/main/java/org/moddingx/modlistcreator/curse/CurseModpack.java b/src/main/java/org/moddingx/modlistcreator/curse/CurseModpack.java deleted file mode 100644 index 52cbf96..0000000 --- a/src/main/java/org/moddingx/modlistcreator/curse/CurseModpack.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.moddingx.modlistcreator.curse; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.stream.JsonReader; -import org.moddingx.cursewrapper.api.CurseWrapper; -import org.moddingx.cursewrapper.api.response.FileInfo; -import org.moddingx.cursewrapper.api.response.ProjectInfo; -import org.moddingx.modlistcreator.ModListCreator; -import org.moddingx.modlistcreator.types.FileBase; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.*; - -public class CurseModpack { - - private final Minecraft minecraft; - @SuppressWarnings("FieldCanBeLocal") - private final String manifestType = "minecraftModpack"; - @SuppressWarnings("FieldCanBeLocal") - private final String overrides = "overrides"; - @SuppressWarnings("FieldCanBeLocal") - private final int manifestVersion = 1; - private final String version; - private final String name; - private final List files = new ArrayList<>(); - - private CurseModpack(JsonElement element) throws IOException, NullPointerException { - CurseWrapper wrapper = ModListCreator.getWrapper(); - JsonObject json = element.getAsJsonObject(); - JsonObject minecraft = (JsonObject) json.get("minecraft"); - String version = minecraft.get("version").getAsString(); - Set loaders = new HashSet<>(); - for (JsonElement modLoader : minecraft.get("modLoaders").getAsJsonArray()) { - JsonObject loader = modLoader.getAsJsonObject(); - loaders.add(new ModLoader(loader.get("id").getAsString(), loader.get("primary").getAsBoolean())); - } - this.minecraft = new Minecraft(version, loaders); - if (json.has("version")) { - this.version = json.get("version").getAsString(); - } else { - this.version = "undefined version"; - } - this.name = json.get("name").getAsString(); - - Set projectIds = new HashSet<>(); - json.get("files").getAsJsonArray().forEach(file -> { - projectIds.add(file.getAsJsonObject().get("projectID").getAsInt()); - }); - Map projectInfoMap = wrapper.getProjects(projectIds); - - int i = 0; - final int total = projectIds.size(); - - for (JsonElement fileElement : json.get("files").getAsJsonArray()) { - JsonObject file = fileElement.getAsJsonObject(); - FileInfo fileInfo = wrapper.getFile(file.get("projectID").getAsInt(), file.get("fileID").getAsInt()); - ProjectEntry e = new ProjectEntry(fileInfo, projectInfoMap.get(file.get("projectID").getAsInt())); - this.files.add(e); - - // ' 001/354 ' - String progress = String.format("%1$" + (String.valueOf(total).length() * 2 + 1) + "s", ++i + "/" + total).replace(' ', '0') + " "; - - FileBase.log(this.name, progress + "\u001B[33m" + (e.getProject().name()) + "\u001B[0m found"); - } - this.files.sort(Comparator.comparing(o -> o.getProject().name().toLowerCase(Locale.ROOT))); - } - - public static CurseModpack fromManifest(File file) { - try { - JsonReader reader = new JsonReader(new FileReader(file)); - JsonElement jsonElement = JsonParser.parseReader(reader); - return new CurseModpack(jsonElement); - } catch (IOException | NullPointerException e) { - e.printStackTrace(); - } - - return null; - } - - public Minecraft getMinecraft() { - return this.minecraft; - } - - public String getManifestType() { - return this.manifestType; - } - - public String getOverrides() { - return this.overrides; - } - - public int getManifestVersion() { - return this.manifestVersion; - } - - public String getVersion() { - return this.version; - } - - public String getName() { - return this.name; - } - - public List getFiles() { - return this.files; - } - - public static class ProjectEntry { - - private final FileInfo file; - private final ProjectInfo project; - - @Deprecated - public ProjectEntry(FileInfo file) throws IOException { - this.file = file; - this.project = ModListCreator.getWrapper().getProject(file.projectId()); - } - - public ProjectEntry(FileInfo file, ProjectInfo project) { - this.file = file; - this.project = project; - } - - public FileInfo getFile() { - return this.file; - } - - public ProjectInfo getProject() { - return this.project; - } - - @Override - public String toString() { - return this.project.name() + "(File: " + this.file.name() + ")"; - } - } - - private record Minecraft(String version, Set loaders) { - } - - private record ModLoader(String id, boolean primary) { - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java b/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java new file mode 100644 index 0000000..95ab72a --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java @@ -0,0 +1,77 @@ +package org.moddingx.modlistcreator.platform; + +import com.google.gson.JsonObject; +import org.moddingx.modlistcreator.ModListCreator; +import org.moddingx.modlistcreator.platform.curse.CurseModpack; +import org.moddingx.modlistcreator.platform.modrinth.ModrinthModpack; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.Reader; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +public abstract class Modpack { + + protected final List files = new ArrayList<>(); + + public abstract int formatVersion(); + + public abstract String title(); + + public abstract Minecraft minecraft(); + + public abstract String version(); + + public abstract PackType packType(); + + public abstract Modpack load(JsonObject json); + + public abstract List files(); + + public abstract String authorLink(String author); + + public String game() { + return "minecraft"; + } + + public static Modpack fromJson(File file) { + try { + return Modpack.fromJson(new FileReader(file)); + } catch (FileNotFoundException e) { + throw new RuntimeException("Unable to load file", e); + } + } + + public static Modpack fromJson(Reader reader) { + return Modpack.fromJson(ModListCreator.GSON.fromJson(reader, JsonObject.class)); + } + + public static Modpack fromJson(JsonObject json) { + if (json.has("manifestVersion") && json.get("manifestVersion").getAsInt() == CurseModpack.FORMAT_VERSION) { + return new CurseModpack().load(json); + } + + if (json.has("formatVersion") && json.get("formatVersion").getAsInt() == ModrinthModpack.FORMAT_VERSION) { + return new ModrinthModpack().load(json); + } + + throw new IllegalStateException("Json does not match any known modpack platform."); + } + + public record ProjectEntry(String projectName, String fileName, String author, URI website, String fileId) { + } + + public record Minecraft(String version, ModLoader loaders) { + } + + public record ModLoader(String type, String version) { + } + + public enum PackType { + CURSEFORGE, + MODRINTH + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java new file mode 100644 index 0000000..95bce2e --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java @@ -0,0 +1,120 @@ +package org.moddingx.modlistcreator.platform.curse; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.moddingx.cursewrapper.api.CurseWrapper; +import org.moddingx.cursewrapper.api.response.FileInfo; +import org.moddingx.cursewrapper.api.response.ProjectInfo; +import org.moddingx.modlistcreator.ModListCreator; +import org.moddingx.modlistcreator.platform.Modpack; +import org.moddingx.modlistcreator.types.FileBase; + +import java.io.IOException; +import java.util.*; + +public class CurseModpack extends Modpack { + + public static final int FORMAT_VERSION = 1; + + private JsonObject json; + private List files; + + @Override + public int formatVersion() { + return FORMAT_VERSION; + } + + @Override + public String title() { + return this.json.get("name").getAsString(); + } + + @Override + public Modpack.Minecraft minecraft() { + JsonObject minecraft = this.json.get("minecraft").getAsJsonObject(); + String version = minecraft.get("version").getAsString(); + JsonObject loader = minecraft.get("modLoaders").getAsJsonArray().get(0).getAsJsonObject(); + Modpack.ModLoader mainLoader = new Modpack.ModLoader(loader.get("id").getAsString().split("-")[0], loader.get("id").getAsString().split("-")[1]); + + return new Modpack.Minecraft(version, mainLoader); + } + + @Override + public String version() { + if (this.json.has("version")) { + return this.json.get("version").getAsString(); + } else { + return "undefined version"; + } + } + + @Override + public PackType packType() { + return PackType.CURSEFORGE; + } + + @Override + public Modpack load(JsonObject json) { + this.json = json; + return this; + } + + @Override + public List files() { + if (this.files == null) { + + List projects = this.retrieveFiles(new HashMap<>(), 1); + projects.sort(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))); + + this.files = List.copyOf(projects); + } + + return this.files; + } + + @Override + public String authorLink(String author) { + return "https://www.curseforge.com/members/" + author + "/projects"; + } + + private List retrieveFiles(Map map, int step) { + List files = new ArrayList<>(map.values()); + CurseWrapper wrapper = ModListCreator.getWrapper(); + + try { + Set projectIds = new HashSet<>(); + this.json.get("files").getAsJsonArray().forEach(file -> { + projectIds.add(file.getAsJsonObject().get("projectID").getAsInt()); + }); + Map projectInfoMap = wrapper.getProjects(projectIds); + + int i = files.size(); + final int total = projectIds.size(); + + for (JsonElement fileElement : this.json.get("files").getAsJsonArray()) { + JsonObject file = fileElement.getAsJsonObject(); + int projectId = file.get("projectID").getAsInt(); + if (map.containsKey(projectId)) { + continue; + } + + FileInfo fileInfo = wrapper.getFile(projectId, file.get("fileID").getAsInt()); + ProjectInfo projectInfo = projectInfoMap.get(projectId); + Modpack.ProjectEntry e = new Modpack.ProjectEntry(projectInfo.name(), fileInfo.name(), projectInfo.owner(), projectInfo.website(), String.valueOf(fileInfo.fileId())); + files.add(e); + + // ' 001/354 ' + String progress = String.format("%1$" + (String.valueOf(total).length() * 2 + 1) + "s", ++i + "/" + total).replace(' ', '0') + " "; + FileBase.log(this.title(), progress + "\u001B[33m" + (e.projectName()) + "\u001B[0m found"); + } + + return files; + } catch (IOException e) { + if (step < 10) { + return this.retrieveFiles(map, ++step); + } + + throw new IllegalStateException("Failed to retrieve project information for '" + this.title() + "'", e); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java new file mode 100644 index 0000000..fcc931b --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java @@ -0,0 +1,205 @@ +package org.moddingx.modlistcreator.platform.modrinth; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.moddingx.modlistcreator.ModListCreator; +import org.moddingx.modlistcreator.platform.Modpack; +import org.moddingx.modlistcreator.types.FileBase; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ModrinthModpack extends Modpack { + + public static final int FORMAT_VERSION = 1; + + private JsonObject json; + private List files; + + @Override + public int formatVersion() { + return FORMAT_VERSION; + } + + @Override + public String title() { + return this.json.get("name").getAsString(); + } + + @Override + public Minecraft minecraft() { + JsonObject deps = this.json.get("dependencies").getAsJsonObject(); + return new Minecraft(deps.get("minecraft").getAsString(), this.modLoader(deps)); + } + + @Override + public String version() { + return this.json.get("versionId").getAsString(); + } + + @Override + public PackType packType() { + return PackType.MODRINTH; + } + + @Override + public Modpack load(JsonObject json) { + this.json = json; + return this; + } + + @Override + public List files() { + if (this.files == null) { + JsonArray array = this.json.get("files").getAsJsonArray(); + Set hashes = IntStream.range(0, array.size()) + .mapToObj(i -> array.get(i).getAsJsonObject()) + .map(json -> json.get("hashes").getAsJsonObject()) + .map(json -> json.get("sha512").getAsString()) + .collect(Collectors.toSet()); + List projects = this.retrieveFiles(hashes); + projects.sort(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))); + + this.files = List.copyOf(projects); + } + + return this.files; + } + + @Override + public String authorLink(String author) { + return "https://modrinth.com/user/" + author; + } + + private List retrieveFiles(Set hashes) { + JsonArray array = new JsonArray(); + hashes.forEach(array::add); + + JsonObject data = new JsonObject(); + data.add("hashes", array); + data.addProperty("algorithm", "sha512"); + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.modrinth.com/v2/version_files")) + .header("Content-Type", "application/json") + .header("User-Agent", "ModdingX/ModListCreator") + .POST(HttpRequest.BodyPublishers.ofString(data.toString())) + .build(); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonObject json = ModListCreator.GSON.fromJson(response.body(), JsonObject.class); + + int i = 0; + final int total = hashes.size(); + + Map fileInfoMap = new HashMap<>(); + for (String hash : hashes) { + JsonObject value = json.get(hash).getAsJsonObject(); + String projectId = value.get("project_id").getAsString(); + fileInfoMap.put(projectId, value); + } + + Map projectInfoMap = this.retrieveProjectMap(fileInfoMap.keySet()); + Map authorsMap = this.retrieveAuthors(projectInfoMap.values().stream().map(info -> info.get("team").getAsString()).collect(Collectors.toSet())); + + List files = new ArrayList<>(); + for (String hash : hashes) { + JsonObject project = json.get(hash).getAsJsonObject(); + String projectId = project.get("project_id").getAsString(); + JsonObject projectJson = projectInfoMap.get(projectId).getAsJsonObject(); + String teamId = projectJson.get("team").getAsString(); + ProjectEntry e = new ProjectEntry(projectJson.get("title").getAsString(), projectId, authorsMap.get(teamId), URI.create("https://modrinth.com/mod/" + projectJson.get("slug").getAsString()), project.get("id").getAsString()); + files.add(e); + + String progress = String.format("%1$" + (String.valueOf(total).length() * 2 + 1) + "s", ++i + "/" + total).replace(' ', '0') + " "; + FileBase.log(this.title(), progress + "\u001B[33m" + e.projectName() + "\u001B[0m found"); + } + + return files; + } catch (IOException | InterruptedException e) { + throw new IllegalStateException("Failed to retrieve project information for '" + this.title() + "'"); + } + } + + private Map retrieveProjectMap(Set projectIds) { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.modrinth.com/v2/projects?ids=" + projectIds.stream().collect(Collectors.joining("%22,%22", "[%22", "%22]")))) + .header("Content-Type", "application/json") + .header("User-Agent", "ModdingX/ModListCreator") + .GET() + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonArray array = ModListCreator.GSON.fromJson(response.body(), JsonArray.class); + + Map map = new HashMap<>(); + for (JsonElement element : array) { + map.put(element.getAsJsonObject().get("id").getAsString(), element.getAsJsonObject()); + } + + return Map.copyOf(map); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private Map retrieveAuthors(Set teamIds) { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.modrinth.com/v2/teams?ids=" + teamIds.stream().collect(Collectors.joining("%22,%22", "[%22", "%22]")))) + .header("Content-Type", "application/json") + .header("User-Agent", "ModdingX/ModListCreator") + .GET() + .build(); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonArray array = ModListCreator.GSON.fromJson(response.body(), JsonArray.class); + Map map = new HashMap<>(); + teamIds.forEach(id -> { + outer: + for (JsonElement element : array) { + for (JsonElement team : element.getAsJsonArray()) { + if (team.getAsJsonObject().get("team_id").getAsString().equals(id)) { + JsonObject user = team.getAsJsonObject().get("user").getAsJsonObject(); + if (team.getAsJsonObject().get("role").getAsString().equals("Owner")) { + map.put(id, user.get("username").getAsString()); + break outer; + } + } + } + + } + }); + + return Collections.unmodifiableMap(map); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException("Failed to retrieve authors information for '" + this.title() + "'"); + } + } + + private ModLoader modLoader(JsonObject deps) { + if (deps.has("forge")) { + return new ModLoader("forge", deps.get("forge").getAsString()); + } + + if (deps.has("fabric-loader")) { + return new ModLoader("fabric-loader", deps.get("fabric-loader").getAsString()); + } + + if (deps.has("quilt-loader")) { + return new ModLoader("quilt-loader", deps.get("quilt-loader").getAsString()); + } + + throw new IllegalStateException("Unknown launcher for pack '" + this.title() + "'"); + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/types/FileBase.java b/src/main/java/org/moddingx/modlistcreator/types/FileBase.java index 6d9dbd4..997fa8a 100644 --- a/src/main/java/org/moddingx/modlistcreator/types/FileBase.java +++ b/src/main/java/org/moddingx/modlistcreator/types/FileBase.java @@ -1,8 +1,6 @@ package org.moddingx.modlistcreator.types; -import org.moddingx.cursewrapper.api.response.FileInfo; -import org.moddingx.cursewrapper.api.response.ProjectInfo; -import org.moddingx.modlistcreator.curse.CurseModpack; +import org.moddingx.modlistcreator.platform.Modpack; import java.io.File; import java.io.FileWriter; @@ -11,11 +9,11 @@ public abstract class FileBase { protected final StringBuilder builder; - protected final CurseModpack pack; + protected final Modpack pack; protected final boolean detailed; protected final boolean headless; - protected FileBase(CurseModpack pack, boolean detailed, boolean headless) { + protected FileBase(Modpack pack, boolean detailed, boolean headless) { this.builder = new StringBuilder(); this.pack = pack; this.detailed = detailed; @@ -24,7 +22,7 @@ protected FileBase(CurseModpack pack, boolean detailed, boolean headless) { public abstract void generateFile(String name, File output); - protected abstract String getFormattedProject(ProjectInfo project, FileInfo file); + protected abstract String getFormattedProject(Modpack.ProjectEntry project); protected abstract String getFormattedAuthor(String member); @@ -55,11 +53,11 @@ public String getContent() { } protected String getHeader() { - return String.format("%s - %s", this.pack.getName(), this.pack.getVersion()); + return String.format("%s - %s", this.pack.title(), this.pack.version()); } protected void log(String text) { - System.out.println("[\u001B[32m" + this.pack.getName() + "\u001B[0m] " + text); + System.out.println("[\u001B[32m" + this.pack.title() + "\u001B[0m] " + text); } public static void log(String pack, String text) { diff --git a/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java b/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java index 879ad37..fb46bb6 100644 --- a/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java +++ b/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java @@ -1,21 +1,18 @@ package org.moddingx.modlistcreator.types.files; -import org.moddingx.cursewrapper.api.response.FileInfo; -import org.moddingx.cursewrapper.api.response.ProjectInfo; -import org.moddingx.modlistcreator.curse.CurseModpack; +import org.moddingx.modlistcreator.platform.Modpack; import org.moddingx.modlistcreator.types.FileBase; import java.io.File; public class HtmlFile extends FileBase { - public HtmlFile(CurseModpack pack, boolean detailed, boolean headless) { + public HtmlFile(Modpack pack, boolean detailed, boolean headless) { super(pack, detailed, headless); } @Override public void generateFile(String name, File output) { - this.log("\u001B[31mPutting \u001B[32meverything \u001B[34mtogether\u001B[35m.\u001B[36m.\u001B[33m.\u001B[0m"); if (!this.headless) { this.builder.append("

"); this.builder.append(this.getHeader()); @@ -23,11 +20,11 @@ public void generateFile(String name, File output) { this.builder.append("\n\n"); } - this.pack.getFiles().forEach(entry -> { + this.pack.files().forEach(entry -> { this.builder.append("
  • "); - this.builder.append(this.getFormattedProject(entry.getProject(), entry.getFile())); + this.builder.append(this.getFormattedProject(entry)); this.builder.append(" (by "); - this.builder.append(this.getFormattedAuthor(entry.getProject().owner())); + this.builder.append(this.getFormattedAuthor(entry.author())); this.builder.append(")
  • \n"); }); @@ -35,16 +32,16 @@ public void generateFile(String name, File output) { } @Override - protected String getFormattedProject(ProjectInfo project, FileInfo file) { + protected String getFormattedProject(Modpack.ProjectEntry project) { return String.format("%s", project.website(), - this.detailed ? "/files/" + file.fileId() : "", - this.detailed ? file.name() : project.name()); + this.detailed ? "/files/" + project.fileId() : "", + this.detailed ? project.fileName() : project.projectName()); } @Override protected String getFormattedAuthor(String member) { - return String.format("%s", member.toLowerCase(), member); + return String.format("%s", member.toLowerCase(), member); } @Override diff --git a/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java b/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java index 08e5fd5..f587bb3 100644 --- a/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java +++ b/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java @@ -1,32 +1,29 @@ package org.moddingx.modlistcreator.types.files; -import org.moddingx.cursewrapper.api.response.FileInfo; -import org.moddingx.cursewrapper.api.response.ProjectInfo; -import org.moddingx.modlistcreator.curse.CurseModpack; +import org.moddingx.modlistcreator.platform.Modpack; import org.moddingx.modlistcreator.types.FileBase; import java.io.File; public class MarkdownFile extends FileBase { - public MarkdownFile(CurseModpack pack, boolean detailed, boolean headless) { + public MarkdownFile(Modpack pack, boolean detailed, boolean headless) { super(pack, detailed, headless); } @Override public void generateFile(String name, File output) { - this.log("\u001B[31mPutting \u001B[32meverything \u001B[34mtogether\u001B[35m.\u001B[36m.\u001B[33m.\u001B[0m"); if (!this.headless) { this.builder.append("## "); this.builder.append(this.getHeader()); this.builder.append("\n"); } - this.pack.getFiles().forEach(entry -> { + this.pack.files().forEach(entry -> { this.builder.append("- "); - this.builder.append(this.getFormattedProject(entry.getProject(), entry.getFile())); + this.builder.append(this.getFormattedProject(entry)); this.builder.append(" (by "); - this.builder.append(this.getFormattedAuthor(entry.getProject().owner())); + this.builder.append(this.getFormattedAuthor(entry.author())); this.builder.append(")\n"); }); @@ -34,16 +31,16 @@ public void generateFile(String name, File output) { } @Override - protected String getFormattedProject(ProjectInfo project, FileInfo file) { + protected String getFormattedProject(Modpack.ProjectEntry project) { return String.format("[%s](%s%s)", - this.detailed ? file.name() : project.name(), + this.detailed ? project.fileName() : project.projectName(), project.website(), - this.detailed ? "/files/" + file.fileId() : ""); + this.detailed ? "/files/" + project.fileId() : ""); } @Override protected String getFormattedAuthor(String member) { - return String.format("[%s](https://www.curseforge.com/members/%s/projects)", member, member.toLowerCase()); + return String.format("[%s](" + this.pack.authorLink(member) + ")", member, member.toLowerCase()); } @Override From cbd924a1ca263b0f28bfb6b45727ca450832e923 Mon Sep 17 00:00:00 2001 From: noeppi_noeppi Date: Fri, 12 Aug 2022 16:39:57 +0200 Subject: [PATCH 2/6] Changes --- .gitignore | 2 +- build.gradle | 8 +- gradle.properties | 2 +- .../org/moddingx/modlistcreator/Main.java | 24 ++ .../modlistcreator/ModListCreator.java | 234 ------------------ .../modlist/ModListCreator.java | 128 ++++++++++ .../modlist/ModListFormatter.java | 25 ++ .../modlistcreator/output/HtmlTarget.java | 52 ++++ .../modlistcreator/output/MarkdownTarget.java | 53 ++++ .../modlistcreator/output/OutputTarget.java | 35 +++ .../output/PlainTextTarget.java | 65 +++++ .../modlistcreator/platform/CurseModpack.java | 121 +++++++++ .../modlistcreator/platform/Modpack.java | 134 +++++----- .../platform/ModrinthModpack.java | 174 +++++++++++++ .../platform/curse/CurseModpack.java | 120 --------- .../platform/modrinth/ModrinthModpack.java | 205 --------------- .../modlistcreator/types/FileBase.java | 66 ----- .../modlistcreator/types/files/HtmlFile.java | 51 ---- .../types/files/MarkdownFile.java | 50 ---- .../modlistcreator/util/EnumConverters.java | 28 +++ .../moddingx/modlistcreator/util/MapUtil.java | 23 -- .../modlistcreator/util/NameFormat.java | 12 - 22 files changed, 784 insertions(+), 828 deletions(-) create mode 100644 src/main/java/org/moddingx/modlistcreator/Main.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/ModListCreator.java create mode 100644 src/main/java/org/moddingx/modlistcreator/modlist/ModListCreator.java create mode 100644 src/main/java/org/moddingx/modlistcreator/modlist/ModListFormatter.java create mode 100644 src/main/java/org/moddingx/modlistcreator/output/HtmlTarget.java create mode 100644 src/main/java/org/moddingx/modlistcreator/output/MarkdownTarget.java create mode 100644 src/main/java/org/moddingx/modlistcreator/output/OutputTarget.java create mode 100644 src/main/java/org/moddingx/modlistcreator/output/PlainTextTarget.java create mode 100644 src/main/java/org/moddingx/modlistcreator/platform/CurseModpack.java create mode 100644 src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/types/FileBase.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java create mode 100644 src/main/java/org/moddingx/modlistcreator/util/EnumConverters.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/util/MapUtil.java delete mode 100644 src/main/java/org/moddingx/modlistcreator/util/NameFormat.java diff --git a/.gitignore b/.gitignore index c6b96a6..cfb128f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ hs_err_pid* config/ build/ input/ -output/ +/output/ modlist.html modlist.md manifest.json diff --git a/build.gradle b/build.gradle index e38ada1..592a43d 100644 --- a/build.gradle +++ b/build.gradle @@ -12,18 +12,16 @@ repositories { } dependencies { - implementation "org.moddingx:CurseWrapper:2.1" + implementation "org.moddingx:CurseWrapper:3.0" - implementation 'com.atlassian.commonmark:commonmark:0.17.0' + implementation 'org.jsoup:jsoup:1.15.2' implementation "net.sf.jopt-simple:jopt-simple:6.0-alpha-3" } -java.toolchain.languageVersion = JavaLanguageVersion.of(17) - jar { manifest { attributes([ - "Main-Class": "org.moddingx.modlistcreator.ModListCreator" + "Main-Class": "org.moddingx.modlistcreator.Main" ]) } } diff --git a/gradle.properties b/gradle.properties index 5b2a680..19a0188 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.1.0 +version=4.0.0 diff --git a/src/main/java/org/moddingx/modlistcreator/Main.java b/src/main/java/org/moddingx/modlistcreator/Main.java new file mode 100644 index 0000000..f119f9f --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/Main.java @@ -0,0 +1,24 @@ +package org.moddingx.modlistcreator; + +import org.moddingx.modlistcreator.modlist.ModListCreator; + +import java.io.IOException; +import java.util.Locale; + +public class Main { + + public static void main(String[] args) throws IOException { + String cmd = args.length == 0 ? "" : args[0]; + String[] newArgs = new String[Math.max(0, args.length - 1)]; + if (newArgs.length > 0) { + System.arraycopy(args, 1, newArgs, 0, newArgs.length); + } + switch (cmd.toLowerCase(Locale.ROOT)) { + case "modlist" -> ModListCreator.run(newArgs); + default -> { + System.err.println("ModListCreator - Choose sub-command\n"); + System.err.println(" modlist: Create a modlist file from a CurseForge or Modrinth modpack."); + } + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/ModListCreator.java b/src/main/java/org/moddingx/modlistcreator/ModListCreator.java deleted file mode 100644 index 8a017af..0000000 --- a/src/main/java/org/moddingx/modlistcreator/ModListCreator.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.moddingx.modlistcreator; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import joptsimple.ArgumentAcceptingOptionSpec; -import joptsimple.OptionParser; -import joptsimple.OptionSet; -import joptsimple.OptionSpec; -import org.moddingx.cursewrapper.api.CurseWrapper; -import org.moddingx.modlistcreator.platform.Modpack; -import org.moddingx.modlistcreator.types.FileBase; -import org.moddingx.modlistcreator.types.files.HtmlFile; -import org.moddingx.modlistcreator.types.files.MarkdownFile; -import org.moddingx.modlistcreator.util.NameFormat; - -import java.io.File; -import java.io.IOException; -import java.io.StringWriter; -import java.net.URI; -import java.nio.file.*; -import java.util.*; -import java.util.function.Predicate; - -public class ModListCreator { - - public static Gson GSON; - private static OptionSet optionSet; - private static final CurseWrapper wrapper = new CurseWrapper(URI.create("https://curse.melanx.de/")); - public static final Predicate filePredicate = file -> file.getName().equals("modrinth.index.json") || file.getName().equals("manifest.json"); - - static { - GsonBuilder builder = new GsonBuilder(); - builder.disableHtmlEscaping(); - GSON = builder.create(); - } - - public static void main(String[] args) throws InterruptedException, IOException { - OptionParser parser = new OptionParser(); - List markdown = new ArrayList<>(); - Collections.addAll(markdown, "md", "markdown"); - - parser.accepts("help", "Prints this overview"); - parser.accepts("detailed", "Shows exact version of each mod"); - parser.accepts("headless", "Generates the file without pack name/version"); - parser.accepts("html", "Exports HTML files"); - parser.acceptsAll(markdown, "Exports Markdown files"); - OptionSpec nameFormat = parser.accepts("nameFormat", "Allowed values: DEFAULT, VERSION, NAME and NAME_VERSION").withRequiredArg().ofType(String.class).defaultsTo("default"); - OptionSpec toolDir = parser.accepts("workingDir", "Defines the path where input and output should be").withRequiredArg().ofType(File.class).defaultsTo(new File(Paths.get("").toUri())); - OptionSpec packs = parser.accepts("input", "Defines the input directory for multiple manifests").withRequiredArg().ofType(File.class); - OptionSpec output = parser.accepts("output", "Defines the output directory for generated files").withRequiredArg().ofType(File.class); - OptionSpec manifest = parser.accepts("manifest", "Defines manifest file").withRequiredArg().ofType(File.class); - OptionSpec empty = parser.nonOptions(); - optionSet = parser.parse(args); - - if (!optionSet.hasOptions() || optionSet.has("help")) { - printHelp(parser); - System.exit(0); - } - - if (optionSet.has(packs) && optionSet.has(manifest)) { - printHelp(parser); - throw new IllegalArgumentException("Can't set a single manifest and a path with multiple manifests."); - } - - List list = optionSet.valuesOf(empty); - if (!list.isEmpty()) { - System.out.println("Completely ignored arguments: " + list); - } - - Set joins = new HashSet<>(); - - NameFormat format = NameFormat.get(getValue(optionSet, nameFormat)); - File workDir = getValue(optionSet, toolDir); - File inDir = optionSet.has(packs) ? getValue(optionSet, packs) : new File(workDir, "input/"); - File outDir = optionSet.has(output) ? getValue(optionSet, output) : new File(workDir, "output/"); - if (optionSet.has(packs)) { - if (inDir.isDirectory()) { - for (File file : Objects.requireNonNull(inDir.listFiles())) { - File zip = getManifestFromZip(outDir, file); - - if (zip != null) { - file = zip; - } - - if (!filePredicate.test(file) && file != zip) { - continue; - } - Modpack pack = Modpack.fromJson(file); - - if (pack == null) { - continue; - } - - generateForPack( - joins, - pack, - getFileName(format, pack, file.getName().replace(".json", "")), - outDir - ); - } - } else { - throw new IllegalArgumentException("Path to packs is no directory: " + inDir); - } - } else { - File file = optionSet.has(manifest) ? getValue(optionSet, manifest) : Paths.get("manifest.json").toFile(); - if (file == null) { - file = Paths.get("modrinth.index.json").toFile(); - } - - File zip = getManifestFromZip(outDir, file); - - if (zip != null) { - file = zip; - } - Modpack pack = Modpack.fromJson(file); - - if (pack != null) { - generateForPack( - joins, - pack, - getFileName(format, pack), - outDir - ); - } - } - for (Thread t : joins) { - t.join(); - } - System.exit(0); - } - - public static CurseWrapper getWrapper() { - return wrapper; - } - - private static void generateForPack(Set joins, Modpack pack, String name, File output) { - boolean alreadyGenerated = false; - boolean detailed = optionSet.has("detailed"); - boolean headless = optionSet.has("headless"); - - if (optionSet.has("html")) { - Thread t = new Thread(() -> { - FileBase html = new HtmlFile(pack, detailed, headless); - html.generateFile(name, output); - }); - joins.add(t); - t.start(); - alreadyGenerated = true; - } - - if (optionSet.has("md") || optionSet.has("markdown")) { - Thread t = new Thread(() -> { - FileBase markdown = new MarkdownFile(pack, detailed, headless); - markdown.generateFile(name, output); - }); - joins.add(t); - t.start(); - alreadyGenerated = true; - } - - if (!alreadyGenerated) { - Set set = new HashSet<>(); - set.add(new HtmlFile(pack, detailed, headless)); - set.add(new MarkdownFile(pack, detailed, headless)); - set.forEach(file -> { - Thread t = new Thread(() -> file.generateFile(name, output)); - joins.add(t); - t.start(); - }); - } - } - - private static String getFileName(NameFormat format, Modpack pack) { - return getFileName(format, pack, ""); - } - - private static String getFileName(NameFormat format, Modpack pack, String prefix) { - return switch (format) { - case NAME -> pack.title(); - case VERSION -> pack.version(); - case NAME_VERSION -> pack.title() + " - " + pack.version(); - case DEFAULT -> !prefix.isEmpty() ? prefix + "-modlist" : "modlist"; - }; - } - - private static T getValue(OptionSet set, OptionSpec option) { - try { - return set.valueOf(option); - } catch (Throwable throwable) { - if (option instanceof ArgumentAcceptingOptionSpec spec) { - List list = spec.defaultValues(); - if (!list.isEmpty()) { - return list.get(0); - } - } - - throw throwable; - } - } - - private static void printHelp(OptionParser parser) throws IOException { - StringWriter writer = new StringWriter(); - parser.printHelpOn(writer); - System.out.println(writer); - } - - private static File getManifestFromZip(File output, File input) { - // Getting manifest.json from zip - try (FileSystem fs = FileSystems.newFileSystem(input.toPath(), (ClassLoader) null)) { - Path zipManifest = fs.getPath("manifest.json"); - if (!Files.exists(zipManifest)) { - zipManifest = fs.getPath("modrinth.index.json"); - } - - if (Files.exists(zipManifest)) { - if (!output.exists()) { - if (output.mkdirs()) { - System.out.println("Created output directory: " + output); - } - } - File tempFile = output.toPath().resolve("manifest" + UUID.randomUUID() + ".json").toFile(); - Files.copy(zipManifest, tempFile.toPath()); - input = tempFile; - input.deleteOnExit(); - - return input; - } - } catch (Exception e) { - // - } - - return null; - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/modlist/ModListCreator.java b/src/main/java/org/moddingx/modlistcreator/modlist/ModListCreator.java new file mode 100644 index 0000000..d520c09 --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/modlist/ModListCreator.java @@ -0,0 +1,128 @@ +package org.moddingx.modlistcreator.modlist; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.util.PathConverter; +import joptsimple.util.PathProperties; +import org.moddingx.modlistcreator.output.OutputTarget; +import org.moddingx.modlistcreator.platform.Modpack; +import org.moddingx.modlistcreator.util.EnumConverters; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.function.BiFunction; + +public class ModListCreator { + + public static Gson GSON; + + static { + GsonBuilder builder = new GsonBuilder(); + builder.disableHtmlEscaping(); + GSON = builder.create(); + } + + public static void run(String[] args) throws IOException { + OptionParser options = new OptionParser(); + OptionSpec specNoHeader = options.accepts("no-header", "Generates the file without pack name and version"); + OptionSpec specDetailed = options.accepts("detailed", "Shows exact version of each mod"); + OptionSpec specFormat = options.accepts("format", "The output format to use").withRequiredArg().withValuesConvertedBy(EnumConverters.enumArg(OutputTarget.Type.class)).withValuesSeparatedBy(",").defaultsTo(OutputTarget.Type.MARKDOWN); + OptionSpec specOutput = options.accepts("output", "Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file.").withRequiredArg().withValuesConvertedBy(new PathConverter()); + OptionSpec specPattern = options.accepts("pattern", "Defines the output file name pattern. %n is replaced with pack name, %v with pack version.").withRequiredArg().ofType(String.class); + OptionSpec specInput = options.nonOptions("Input files. Can be either modpack zips or json files.").withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); + + OptionSet set; + try { + set = options.parse(args); + if (!set.has(specOutput)) missing(options, specOutput); + if (!set.has(specPattern) && set.valuesOf(specFormat).size() != 1) missing(options, specPattern, "Name pattern needed for multiple output formats"); + if (!set.has(specPattern) && set.valuesOf(specInput).size() != 1) missing(options, specPattern, "Name pattern needed for multiple input files"); + if (set.valuesOf(specInput).isEmpty()) missing(options, specInput, "No inputs"); + } catch (OptionException e) { + System.err.println(e.getMessage() + "\n"); + options.printHelpOn(System.err); + System.exit(1); + throw new Error(); + } + + BiFunction outputPaths = outputPathFunc(set.valueOf(specOutput), set.has(specPattern) ? set.valueOf(specPattern) : null); + List outputTypes = set.valuesOf(specFormat).stream().distinct().toList(); + boolean includeHeader = !set.has(specNoHeader); + boolean detailed = set.has(specDetailed); + List inputs = set.valuesOf(specInput); + + ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(Math.min(inputs.size(), Runtime.getRuntime().availableProcessors() - 1)); + List> joins = new ArrayList<>(); + for (Path path : inputs) { + joins.add(executor.submit(() -> { + try { + Modpack pack = fromPath(path); + for (OutputTarget.Type type : outputTypes) { + Path outputPath = outputPaths.apply(pack, type); + if (!Files.exists(outputPath.getParent())) { + Files.createDirectories(outputPath.getParent()); + } + Files.writeString(outputPath, ModListFormatter.format(pack, type, includeHeader, detailed), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } catch (IOException e) { + throw new RuntimeException("Failed for path: " + path, e); + } + })); + } + for (Future future : joins) { + try { + future.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + e.getCause().printStackTrace(); + } + } + System.exit(0); + } + + @SuppressWarnings("UnusedReturnValue") + private static T missing(OptionParser options, OptionSpec spec) throws IOException { + return missing(options, spec, "Missing required option"); + } + + private static T missing(OptionParser options, OptionSpec spec, String msg) throws IOException { + System.err.println(msg + ": " + spec + "\n"); + options.printHelpOn(System.err); + System.exit(1); + throw new Error(); + } + + private static BiFunction outputPathFunc(Path basePath, @Nullable String pattern) { + Path normPath = basePath.toAbsolutePath().normalize(); + if (pattern == null) return (pack, type) -> normPath; + return (pack, type) -> normPath.resolve(pattern + .replace("%n", pack.title().replace(' ', '_')) + .replace("%v", pack.version().replace(' ', '_')) + .replace("%%", "%") + "." + type.extension + ); + } + + private static Modpack fromPath(Path path) throws IOException { + try { + return Modpack.loadZip(path); + } catch (ProviderNotFoundException e) { + // Not a zip file + return Modpack.load(path); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/modlist/ModListFormatter.java b/src/main/java/org/moddingx/modlistcreator/modlist/ModListFormatter.java new file mode 100644 index 0000000..a1e10bc --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/modlist/ModListFormatter.java @@ -0,0 +1,25 @@ +package org.moddingx.modlistcreator.modlist; + +import org.moddingx.modlistcreator.output.OutputTarget; +import org.moddingx.modlistcreator.platform.Modpack; + +import java.util.Comparator; + +public class ModListFormatter { + + public static String format(Modpack pack, OutputTarget.Type outputType, boolean includeHeader, boolean detailed) { + OutputTarget target = outputType.create(); + if (includeHeader) { + target.addHeader(pack.title() + " - " + pack.version()); + } + + target.beginList(false); + for (Modpack.File file : pack.files().stream().sorted(Comparator.comparing(Modpack.File::projectSlug)).toList()) { + String projectPart = detailed ? target.formatLink(file.fileName(), file.fileWebsite()) : target.formatLink(file.projectName(), file.projectWebsite()); + target.addListElement(projectPart + " (by " + target.formatLink(file.author(), file.authorWebsite()) + ")"); + } + target.endList(); + + return target.result(); + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/output/HtmlTarget.java b/src/main/java/org/moddingx/modlistcreator/output/HtmlTarget.java new file mode 100644 index 0000000..d70b793 --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/output/HtmlTarget.java @@ -0,0 +1,52 @@ +package org.moddingx.modlistcreator.output; + +import org.jsoup.nodes.Element; + +import java.net.URI; +import java.util.Stack; + +public class HtmlTarget implements OutputTarget { + + private final Element tag = new Element("body"); + private final Stack lists = new Stack<>(); + + @Override + public void addHeader(String content) { + this.tag.appendChild(new Element("h2").append(content)); + } + + @Override + public void addParagraph(String content) { + this.tag.appendChild(new Element("p").append(content)); + } + + @Override + public void beginList(boolean numbered) { + this.lists.push(new Element(numbered ? "ol" : "ul")); + } + + @Override + public void addListElement(String content) { + this.lists.peek().appendChild(new Element("li").append(content)); + } + + @Override + public void endList() { + Element elem = this.lists.pop(); + if (this.lists.isEmpty()) { + this.tag.appendChild(elem); + } else { + this.lists.peek().appendChild(elem); + } + } + + @Override + public String formatLink(String text, URI url) { + return new Element("a").attr("href", url.toString()).append(text).outerHtml(); + } + + @Override + public String result() { + return this.tag.html(); + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/output/MarkdownTarget.java b/src/main/java/org/moddingx/modlistcreator/output/MarkdownTarget.java new file mode 100644 index 0000000..350cc89 --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/output/MarkdownTarget.java @@ -0,0 +1,53 @@ +package org.moddingx.modlistcreator.output; + +import java.net.URI; +import java.util.Stack; + +public class MarkdownTarget implements OutputTarget { + + private final StringBuilder sb = new StringBuilder(); + private final Stack lists = new Stack<>(); + + @Override + public void addHeader(String content) { + this.sb.append("## ").append(content).append("\n\n"); + } + + @Override + public void addParagraph(String content) { + this.sb.append(content).append("\n\n"); + } + + @Override + public void beginList(boolean numbered) { + this.lists.push(numbered ? 1: 0); + } + + @Override + public void addListElement(String content) { + this.sb.append(" ".repeat(this.lists.size())); + if (this.lists.peek() == 0) { + this.sb.append("* "); + } else { + int nextIdx = this.lists.pop(); + this.sb.append(nextIdx).append(". "); + this.lists.push(nextIdx + 1); + } + this.sb.append(content).append("\n"); + } + + @Override + public void endList() { + this.lists.pop(); + } + + @Override + public String formatLink(String text, URI url) { + return "[" + text + "](" + url + ")"; + } + + @Override + public String result() { + return this.sb.toString(); + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/output/OutputTarget.java b/src/main/java/org/moddingx/modlistcreator/output/OutputTarget.java new file mode 100644 index 0000000..168a1ac --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/output/OutputTarget.java @@ -0,0 +1,35 @@ +package org.moddingx.modlistcreator.output; + +import java.net.URI; +import java.util.function.Supplier; + +public interface OutputTarget { + + void addHeader(String content); + void addParagraph(String content); + void beginList(boolean numbered); + void addListElement(String content); + void endList(); + String formatLink(String text, URI url); + + String result(); + + enum Type { + + PLAIN_TEXT("txt", PlainTextTarget::new), + HTML("html", HtmlTarget::new), + MARKDOWN("md", MarkdownTarget::new); + + public final String extension; + private final Supplier factory; + + Type(String extension, Supplier factory) { + this.extension = extension; + this.factory = factory; + } + + public OutputTarget create() { + return this.factory.get(); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/output/PlainTextTarget.java b/src/main/java/org/moddingx/modlistcreator/output/PlainTextTarget.java new file mode 100644 index 0000000..d3d4051 --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/output/PlainTextTarget.java @@ -0,0 +1,65 @@ +package org.moddingx.modlistcreator.output; + +import java.net.URI; +import java.util.Stack; + +public class PlainTextTarget implements OutputTarget { + + private final StringBuilder sb = new StringBuilder(); + private final Stack lists = new Stack<>(); + + @Override + public void addHeader(String content) { + this.sb.append(" ").append(content).append(" \n"); + this.sb.append(" ").append("=".repeat(content.length() + 2)).append(" \n\n"); + } + + @Override + public void addParagraph(String content) { + int len = 0; + for (String word : content.split(" ")) { + if (len > 0 && len + word.length() > 80) { + len = 0; + this.sb.append("\n"); + } + if (len > 0) { + this.sb.append(" "); + } + len += word.length(); + this.sb.append(word); + } + } + + @Override + public void beginList(boolean numbered) { + this.lists.push(numbered ? 1 : 0); + } + + @Override + public void addListElement(String content) { + this.sb.append(" ".repeat(this.lists.size())); + if (this.lists.peek() == 0) { + this.sb.append("* "); + } else { + int nextIdx = this.lists.pop(); + this.sb.append(nextIdx).append(". "); + this.lists.push(nextIdx + 1); + } + this.sb.append(content).append("\n"); + } + + @Override + public void endList() { + this.lists.pop(); + } + + @Override + public String formatLink(String text, URI url) { + return text; + } + + @Override + public String result() { + return this.sb.toString(); + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/CurseModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/CurseModpack.java new file mode 100644 index 0000000..af1e00b --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/platform/CurseModpack.java @@ -0,0 +1,121 @@ +package org.moddingx.modlistcreator.platform; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import org.moddingx.cursewrapper.api.CurseWrapper; +import org.moddingx.cursewrapper.api.response.FileInfo; +import org.moddingx.cursewrapper.api.response.ProjectInfo; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public record CurseModpack( + String title, + Modpack.Minecraft minecraft, + String version, + List files +) implements Modpack { + + public static final int MANIFEST_VERSION = 1; + private static final CurseWrapper API = new CurseWrapper(URI.create("https://curse.melanx.de/")); + + + public static Optional load(JsonElement json) throws IOException { + if (!json.isJsonObject() || !json.getAsJsonObject().has("manifestVersion") || json.getAsJsonObject().get("manifestVersion").getAsInt() != MANIFEST_VERSION) { + return Optional.empty(); + } + JsonObject obj = json.getAsJsonObject(); + String title = Objects.requireNonNull(obj.get("name"), "Missing property: name").getAsString(); + String version = obj.has("version") ? obj.get("version").getAsString() : "unknown"; + + JsonObject minecraft = Objects.requireNonNull(obj.get("minecraft"), "Missing property: minecraft").getAsJsonObject(); + String mcVersion = Objects.requireNonNull(minecraft.get("version"), "Missing property: minecraft.version").getAsString(); + JsonArray loaderArray = Objects.requireNonNull(minecraft.get("modLoaders"), "Missing property: minecraft.modLoaders").getAsJsonArray(); + if (loaderArray.size() != 1) throw new JsonSyntaxException("Modpack must define exactly one mod loader"); + String loaderId = Objects.requireNonNull(loaderArray.get(0).getAsJsonObject().get("id"), "Missing property: minecraft.modLoaders[0].id").getAsString(); + if (!loaderId.contains("-")) throw new JsonSyntaxException("Modpack loader id is invalid: " + loaderId); + + JsonArray filesArray = Objects.requireNonNull(obj.get("files"), "Missing property: files").getAsJsonArray(); + Set projectIds = new HashSet<>(); + Map fileIds = new HashMap<>(); + for (int i = 0; i < filesArray.size(); i++) { + JsonObject fileObj = filesArray.get(i).getAsJsonObject(); + int projectId = Objects.requireNonNull(fileObj.get("projectID"), "Missing property: files[" + i + "].projectID").getAsInt(); + int fileId = Objects.requireNonNull(fileObj.get("fileID"), "Missing property: files[" + i + "].fileID").getAsInt(); + projectIds.add(projectId); + fileIds.put(projectId, fileId); + } + Map resolvedProjects = API.getProjects(projectIds); + if (projectIds.stream().anyMatch(id -> !resolvedProjects.containsKey(id))) { + throw new IllegalStateException("Not all projects could be resolved."); + } + List files = fileIds.entrySet().stream() + .map(entry -> new CurseFile(resolvedProjects.get(entry.getKey()), entry.getValue())) + .toList(); + + return Optional.of(new CurseModpack(title, new Modpack.Minecraft( + mcVersion, loaderId.substring(0, loaderId.indexOf('-')), loaderId.substring(loaderId.indexOf('-') + 1) + ), version, List.copyOf(files))); + } + + // Don't resolve file name and URL if not needed + private static class CurseFile implements Modpack.File { + + private final ProjectInfo project; + private final int fileId; + private FileInfo file; + + private CurseFile(ProjectInfo project, int fileId) { + this.project = project; + this.fileId = fileId; + this.file = null; + } + + @Override + public String projectSlug() { + return this.project.slug(); + } + + @Override + public String projectName() { + return this.project.name(); + } + + @Override + public String fileName() { + if (this.file == null) { + try { + this.file = API.getFile(this.project.projectId(), this.fileId); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return this.file.name(); + } + + @Override + public String author() { + return this.project.owner(); + } + + @Override + public URI projectWebsite() { + return this.project.website(); + } + + @Override + public URI fileWebsite() { + return URI.create(this.projectWebsite() + "/").resolve("files/" + this.fileId); + } + + @Override + public URI authorWebsite() { + return URI.create("https://www.curseforge.com/members/" + URLEncoder.encode(this.author(), StandardCharsets.UTF_8) + "/projects"); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java b/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java index 95ab72a..e9c6eb7 100644 --- a/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java +++ b/src/main/java/org/moddingx/modlistcreator/platform/Modpack.java @@ -1,77 +1,91 @@ package org.moddingx.modlistcreator.platform; -import com.google.gson.JsonObject; -import org.moddingx.modlistcreator.ModListCreator; -import org.moddingx.modlistcreator.platform.curse.CurseModpack; -import org.moddingx.modlistcreator.platform.modrinth.ModrinthModpack; +import com.google.gson.JsonElement; +import org.moddingx.modlistcreator.modlist.ModListCreator; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.Reader; +import java.io.*; import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -public abstract class Modpack { - - protected final List files = new ArrayList<>(); - - public abstract int formatVersion(); - - public abstract String title(); - - public abstract Minecraft minecraft(); - - public abstract String version(); - - public abstract PackType packType(); - - public abstract Modpack load(JsonObject json); - - public abstract List files(); - - public abstract String authorLink(String author); - - public String game() { - return "minecraft"; - } - - public static Modpack fromJson(File file) { - try { - return Modpack.fromJson(new FileReader(file)); - } catch (FileNotFoundException e) { - throw new RuntimeException("Unable to load file", e); +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public interface Modpack { + + String title(); + Minecraft minecraft(); + String version(); + List files(); + + static Modpack loadZip(Path path) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + path.toAbsolutePath().normalize().toUri()), Map.of())) { + for (Type type : Type.values()) { + Path manifest = fs.getPath("/").resolve(type.manifestPath).toAbsolutePath().normalize(); + if (Files.isRegularFile(manifest)) { + return load(manifest, type); + } + } + throw new IOException("Failed to load modpack: Format unknown, no manifest file found in archive"); } } - - public static Modpack fromJson(Reader reader) { - return Modpack.fromJson(ModListCreator.GSON.fromJson(reader, JsonObject.class)); + + static Modpack load(Path path) throws IOException { + JsonElement json; + try (Reader reader = Files.newBufferedReader(path)) { + json = ModListCreator.GSON.fromJson(reader, JsonElement.class); + } + for (Type type : Type.values()) { + Optional pack = type.factory.apply(json); + if (pack.isPresent()) { + return pack.get(); + } + } + throw new IOException("Failed to load modpack: Format unknown, manifest file has no known format"); } - - public static Modpack fromJson(JsonObject json) { - if (json.has("manifestVersion") && json.get("manifestVersion").getAsInt() == CurseModpack.FORMAT_VERSION) { - return new CurseModpack().load(json); + + static Modpack load(Path path, Type type) throws IOException { + JsonElement json; + try (Reader reader = Files.newBufferedReader(path)) { + json = ModListCreator.GSON.fromJson(reader, JsonElement.class); } - - if (json.has("formatVersion") && json.get("formatVersion").getAsInt() == ModrinthModpack.FORMAT_VERSION) { - return new ModrinthModpack().load(json); + Optional pack = type.factory.apply(json); + if (pack.isEmpty()) { + throw new IOException("Invalid " + type.name().toLowerCase(Locale.ROOT) + " modpack: Invalid manifest"); + } else { + return pack.get(); } - - throw new IllegalStateException("Json does not match any known modpack platform."); } - public record ProjectEntry(String projectName, String fileName, String author, URI website, String fileId) { + interface File { + String projectSlug(); + String projectName(); + String fileName(); + String author(); + URI projectWebsite(); + URI fileWebsite(); + URI authorWebsite(); } + + record DefaultFile(String projectSlug, String projectName, String fileName, String author, URI projectWebsite, URI fileWebsite, URI authorWebsite) implements File {} - public record Minecraft(String version, ModLoader loaders) { - } + record Minecraft(String version, String loader, String loaderVersion) {} - public record ModLoader(String type, String version) { - } + enum Type { + CURSEFORGE("manifest.json", CurseModpack::load), + MODRINTH("modrinth.index.json", ModrinthModpack::load); + + private final String manifestPath; + private final IOFunction> factory; - public enum PackType { - CURSEFORGE, - MODRINTH + Type(String manifestPath, IOFunction> factory) { + this.manifestPath = manifestPath; + this.factory = factory; + } + } + + @FunctionalInterface + interface IOFunction { + R apply(T arg) throws IOException; } } diff --git a/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java new file mode 100644 index 0000000..8f40e42 --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java @@ -0,0 +1,174 @@ +package org.moddingx.modlistcreator.platform; + +import com.google.gson.*; +import org.moddingx.modlistcreator.modlist.ModListCreator; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public record ModrinthModpack( + String title, + Modpack.Minecraft minecraft, + String version, + List files +) implements Modpack { + + public static final int FORMAT_VERSION = 1; + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + + public static Optional load(JsonElement json) throws IOException { + if (!json.isJsonObject() || !json.getAsJsonObject().has("formatVersion") || json.getAsJsonObject().get("formatVersion").getAsInt() != FORMAT_VERSION) { + return Optional.empty(); + } + JsonObject obj = json.getAsJsonObject(); + String title = Objects.requireNonNull(obj.get("name"), "Missing property: name").getAsString(); + String version = Objects.requireNonNull(obj.get("versionId"), "Missing property: version").getAsString(); + + JsonObject deps = Objects.requireNonNull(obj.get("dependencies"), "Missing property: dependencies").getAsJsonObject(); + String mcVersion = Objects.requireNonNull(deps.get("minecraft"), "Missing property: dependencies.minecraft").getAsString(); + + if (deps.size() != 2) throw new JsonSyntaxException("Modpack must specify exactly one loader dependency alog side its minecraft dependency"); + String loader = null; + String loaderVersion = null; + for (String key : deps.keySet()) { + if ("minecraft".equals(key)) continue; + loader = key.endsWith("-loader") ? key.substring(0, key.length() - 7) : key; + loaderVersion = deps.get(key).getAsString(); + } + + JsonArray filesArray = Objects.requireNonNull(obj.get("files"), "Missing property: files").getAsJsonArray(); + Set hashes = new HashSet<>(); + for (int i = 0; i < filesArray.size(); i++) { + JsonObject fileObj = filesArray.get(i).getAsJsonObject(); + JsonObject hashesObj = Objects.requireNonNull(fileObj.get("hashes"), "Missing property: files[" + i + "].hashes").getAsJsonObject(); + String sha512 = Objects.requireNonNull(hashesObj.get("sha512"), "Missing property: files[" + i + "].hashes.sha512").getAsString(); + hashes.add(sha512); + } + + JsonObject requestData = new JsonObject(); + requestData.addProperty("algorithm", "sha512"); + JsonArray hashesArray = new JsonArray(); + hashes.forEach(hashesArray::add); + requestData.add("hashes", hashesArray); + + List files = new ArrayList<>(); + try { + JsonObject filesResponse = makeRequest(HttpRequest.newBuilder() + .uri(URI.create("https://api.modrinth.com/v2/version_files")) + .POST(HttpRequest.BodyPublishers.ofString(ModListCreator.GSON.toJson(requestData), StandardCharsets.UTF_8)) + .header("Content-Type", "application/json") + ).getAsJsonObject(); + + record FileData(String projectId, String versionId, String fileName) {} + List fileData = new ArrayList<>(); + + for (String hash : hashes) { + if (!filesResponse.has(hash)) { + throw new IllegalArgumentException("File not hosted on modrinth: sha512=" + hash); + } + JsonObject versionData = filesResponse.get(hash).getAsJsonObject(); + String fileName = null; + for (JsonElement versionFile : versionData.get("files").getAsJsonArray()) { + if (versionFile.getAsJsonObject().get("primary").getAsBoolean()) { + fileName = versionFile.getAsJsonObject().get("filename").getAsString(); + break; + } + } + if (fileName == null) { + throw new IOException("Version has no primary file"); + } + fileData.add(new FileData(versionData.get("project_id").getAsString(), versionData.get("id").getAsString(), fileName)); + } + + JsonArray allProjectIds = new JsonArray(); + fileData.stream().map(FileData::projectId).distinct().forEach(allProjectIds::add); + JsonArray projectsResponse = makeRequest(HttpRequest.newBuilder() + .GET() + .uri(URI.create("https://api.modrinth.com/v2/projects?ids=" + URLEncoder.encode(ModListCreator.GSON.toJson(allProjectIds), StandardCharsets.UTF_8))) + ).getAsJsonArray(); + + record ProjectData(String slug, String name, URI website, String teamId) {} + Map projectData = new HashMap<>(); + for (JsonElement entry : projectsResponse) { + JsonObject projectEntry = entry.getAsJsonObject(); + projectData.put(projectEntry.get("id").getAsString(), new ProjectData( + projectEntry.get("slug").getAsString(), + projectEntry.get("title").getAsString(), + URI.create("https://modrinth.com/" + URLEncoder.encode(projectEntry.get("project_type").getAsString(), StandardCharsets.UTF_8) + "/" + URLEncoder.encode(projectEntry.get("slug").getAsString(), StandardCharsets.UTF_8)), + projectEntry.get("team").getAsString() + )); + } + + JsonArray allTeamIds = new JsonArray(); + projectData.values().stream().map(ProjectData::teamId).distinct().forEach(allTeamIds::add); + JsonArray teamsResponse = makeRequest(HttpRequest.newBuilder() + .GET() + .uri(URI.create("https://api.modrinth.com/v2/teams?ids=" + URLEncoder.encode(ModListCreator.GSON.toJson(allTeamIds), StandardCharsets.UTF_8))) + ).getAsJsonArray(); + + record TeamData(String owner, URI teamURL) {} + Map teamData = new HashMap<>(); + for (JsonElement entryArr : teamsResponse) { + for (JsonElement entry : entryArr.getAsJsonArray()) { + JsonObject teamEntry = entry.getAsJsonObject(); + if ("Owner".equals(teamEntry.get("role").getAsString())) { + JsonObject user = teamEntry.get("user").getAsJsonObject(); + String name = user.get("username").getAsString(); + if (user.has("name") && !user.get("name").isJsonNull()) { + name = user.get("name").getAsString(); + } + teamData.put(teamEntry.get("team_id").getAsString(), new TeamData( + name, URI.create("https://modrinth.com/user/" + URLEncoder.encode(user.get("username").getAsString(), StandardCharsets.UTF_8)) + )); + } + } + } + + for (FileData fd : fileData) { + ProjectData pd = projectData.get(fd.projectId()); + if (pd == null) throw new IOException("Project not resolved: " + fd.projectId()); + TeamData td = teamData.get(pd.teamId()); + if (td == null) throw new IOException("Team not resolved: " + pd.teamId() + " (of project " + pd.slug() + ")"); + files.add(new DefaultFile(pd.slug(), pd.name(), fd.fileName(), td.owner(), pd.website(), URI.create(pd.website() + "/").resolve("version/" + fd.versionId()), td.teamURL())); + } + } catch (JsonParseException e) { + throw new IOException("Failed to query modrinth api", e); + } + + return Optional.of(new ModrinthModpack(title, new Modpack.Minecraft(mcVersion, loader, loaderVersion), version, List.copyOf(files))); + } + + private static JsonElement makeRequest(HttpRequest.Builder builder) throws IOException { + HttpRequest request = builder + .header("Accept", "application/json") + .header("User-Agent", "ModdingX/ModListCreator") + .build(); + try { + String response = CLIENT.send(request, resp -> { + if ((resp.statusCode() / 100) == 2 && resp.statusCode() != 204) { + return HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8); + } else { + return HttpResponse.BodySubscribers.replacing("\0" + resp.statusCode()); + } + }).body(); + if (response.startsWith("\0")) { + throw new IOException("HTTP " + response.substring(1)); + } else { + try { + return ModListCreator.GSON.fromJson(response, JsonElement.class); + } catch (JsonParseException e) { + throw new IOException("Invalid jso nresponse from modrinth api: " + response, e); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted"); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java deleted file mode 100644 index 95bce2e..0000000 --- a/src/main/java/org/moddingx/modlistcreator/platform/curse/CurseModpack.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.moddingx.modlistcreator.platform.curse; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.moddingx.cursewrapper.api.CurseWrapper; -import org.moddingx.cursewrapper.api.response.FileInfo; -import org.moddingx.cursewrapper.api.response.ProjectInfo; -import org.moddingx.modlistcreator.ModListCreator; -import org.moddingx.modlistcreator.platform.Modpack; -import org.moddingx.modlistcreator.types.FileBase; - -import java.io.IOException; -import java.util.*; - -public class CurseModpack extends Modpack { - - public static final int FORMAT_VERSION = 1; - - private JsonObject json; - private List files; - - @Override - public int formatVersion() { - return FORMAT_VERSION; - } - - @Override - public String title() { - return this.json.get("name").getAsString(); - } - - @Override - public Modpack.Minecraft minecraft() { - JsonObject minecraft = this.json.get("minecraft").getAsJsonObject(); - String version = minecraft.get("version").getAsString(); - JsonObject loader = minecraft.get("modLoaders").getAsJsonArray().get(0).getAsJsonObject(); - Modpack.ModLoader mainLoader = new Modpack.ModLoader(loader.get("id").getAsString().split("-")[0], loader.get("id").getAsString().split("-")[1]); - - return new Modpack.Minecraft(version, mainLoader); - } - - @Override - public String version() { - if (this.json.has("version")) { - return this.json.get("version").getAsString(); - } else { - return "undefined version"; - } - } - - @Override - public PackType packType() { - return PackType.CURSEFORGE; - } - - @Override - public Modpack load(JsonObject json) { - this.json = json; - return this; - } - - @Override - public List files() { - if (this.files == null) { - - List projects = this.retrieveFiles(new HashMap<>(), 1); - projects.sort(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))); - - this.files = List.copyOf(projects); - } - - return this.files; - } - - @Override - public String authorLink(String author) { - return "https://www.curseforge.com/members/" + author + "/projects"; - } - - private List retrieveFiles(Map map, int step) { - List files = new ArrayList<>(map.values()); - CurseWrapper wrapper = ModListCreator.getWrapper(); - - try { - Set projectIds = new HashSet<>(); - this.json.get("files").getAsJsonArray().forEach(file -> { - projectIds.add(file.getAsJsonObject().get("projectID").getAsInt()); - }); - Map projectInfoMap = wrapper.getProjects(projectIds); - - int i = files.size(); - final int total = projectIds.size(); - - for (JsonElement fileElement : this.json.get("files").getAsJsonArray()) { - JsonObject file = fileElement.getAsJsonObject(); - int projectId = file.get("projectID").getAsInt(); - if (map.containsKey(projectId)) { - continue; - } - - FileInfo fileInfo = wrapper.getFile(projectId, file.get("fileID").getAsInt()); - ProjectInfo projectInfo = projectInfoMap.get(projectId); - Modpack.ProjectEntry e = new Modpack.ProjectEntry(projectInfo.name(), fileInfo.name(), projectInfo.owner(), projectInfo.website(), String.valueOf(fileInfo.fileId())); - files.add(e); - - // ' 001/354 ' - String progress = String.format("%1$" + (String.valueOf(total).length() * 2 + 1) + "s", ++i + "/" + total).replace(' ', '0') + " "; - FileBase.log(this.title(), progress + "\u001B[33m" + (e.projectName()) + "\u001B[0m found"); - } - - return files; - } catch (IOException e) { - if (step < 10) { - return this.retrieveFiles(map, ++step); - } - - throw new IllegalStateException("Failed to retrieve project information for '" + this.title() + "'", e); - } - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java deleted file mode 100644 index fcc931b..0000000 --- a/src/main/java/org/moddingx/modlistcreator/platform/modrinth/ModrinthModpack.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.moddingx.modlistcreator.platform.modrinth; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.moddingx.modlistcreator.ModListCreator; -import org.moddingx.modlistcreator.platform.Modpack; -import org.moddingx.modlistcreator.types.FileBase; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class ModrinthModpack extends Modpack { - - public static final int FORMAT_VERSION = 1; - - private JsonObject json; - private List files; - - @Override - public int formatVersion() { - return FORMAT_VERSION; - } - - @Override - public String title() { - return this.json.get("name").getAsString(); - } - - @Override - public Minecraft minecraft() { - JsonObject deps = this.json.get("dependencies").getAsJsonObject(); - return new Minecraft(deps.get("minecraft").getAsString(), this.modLoader(deps)); - } - - @Override - public String version() { - return this.json.get("versionId").getAsString(); - } - - @Override - public PackType packType() { - return PackType.MODRINTH; - } - - @Override - public Modpack load(JsonObject json) { - this.json = json; - return this; - } - - @Override - public List files() { - if (this.files == null) { - JsonArray array = this.json.get("files").getAsJsonArray(); - Set hashes = IntStream.range(0, array.size()) - .mapToObj(i -> array.get(i).getAsJsonObject()) - .map(json -> json.get("hashes").getAsJsonObject()) - .map(json -> json.get("sha512").getAsString()) - .collect(Collectors.toSet()); - List projects = this.retrieveFiles(hashes); - projects.sort(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))); - - this.files = List.copyOf(projects); - } - - return this.files; - } - - @Override - public String authorLink(String author) { - return "https://modrinth.com/user/" + author; - } - - private List retrieveFiles(Set hashes) { - JsonArray array = new JsonArray(); - hashes.forEach(array::add); - - JsonObject data = new JsonObject(); - data.add("hashes", array); - data.addProperty("algorithm", "sha512"); - - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.modrinth.com/v2/version_files")) - .header("Content-Type", "application/json") - .header("User-Agent", "ModdingX/ModListCreator") - .POST(HttpRequest.BodyPublishers.ofString(data.toString())) - .build(); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - JsonObject json = ModListCreator.GSON.fromJson(response.body(), JsonObject.class); - - int i = 0; - final int total = hashes.size(); - - Map fileInfoMap = new HashMap<>(); - for (String hash : hashes) { - JsonObject value = json.get(hash).getAsJsonObject(); - String projectId = value.get("project_id").getAsString(); - fileInfoMap.put(projectId, value); - } - - Map projectInfoMap = this.retrieveProjectMap(fileInfoMap.keySet()); - Map authorsMap = this.retrieveAuthors(projectInfoMap.values().stream().map(info -> info.get("team").getAsString()).collect(Collectors.toSet())); - - List files = new ArrayList<>(); - for (String hash : hashes) { - JsonObject project = json.get(hash).getAsJsonObject(); - String projectId = project.get("project_id").getAsString(); - JsonObject projectJson = projectInfoMap.get(projectId).getAsJsonObject(); - String teamId = projectJson.get("team").getAsString(); - ProjectEntry e = new ProjectEntry(projectJson.get("title").getAsString(), projectId, authorsMap.get(teamId), URI.create("https://modrinth.com/mod/" + projectJson.get("slug").getAsString()), project.get("id").getAsString()); - files.add(e); - - String progress = String.format("%1$" + (String.valueOf(total).length() * 2 + 1) + "s", ++i + "/" + total).replace(' ', '0') + " "; - FileBase.log(this.title(), progress + "\u001B[33m" + e.projectName() + "\u001B[0m found"); - } - - return files; - } catch (IOException | InterruptedException e) { - throw new IllegalStateException("Failed to retrieve project information for '" + this.title() + "'"); - } - } - - private Map retrieveProjectMap(Set projectIds) { - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.modrinth.com/v2/projects?ids=" + projectIds.stream().collect(Collectors.joining("%22,%22", "[%22", "%22]")))) - .header("Content-Type", "application/json") - .header("User-Agent", "ModdingX/ModListCreator") - .GET() - .build(); - - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - JsonArray array = ModListCreator.GSON.fromJson(response.body(), JsonArray.class); - - Map map = new HashMap<>(); - for (JsonElement element : array) { - map.put(element.getAsJsonObject().get("id").getAsString(), element.getAsJsonObject()); - } - - return Map.copyOf(map); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private Map retrieveAuthors(Set teamIds) { - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.modrinth.com/v2/teams?ids=" + teamIds.stream().collect(Collectors.joining("%22,%22", "[%22", "%22]")))) - .header("Content-Type", "application/json") - .header("User-Agent", "ModdingX/ModListCreator") - .GET() - .build(); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - JsonArray array = ModListCreator.GSON.fromJson(response.body(), JsonArray.class); - Map map = new HashMap<>(); - teamIds.forEach(id -> { - outer: - for (JsonElement element : array) { - for (JsonElement team : element.getAsJsonArray()) { - if (team.getAsJsonObject().get("team_id").getAsString().equals(id)) { - JsonObject user = team.getAsJsonObject().get("user").getAsJsonObject(); - if (team.getAsJsonObject().get("role").getAsString().equals("Owner")) { - map.put(id, user.get("username").getAsString()); - break outer; - } - } - } - - } - }); - - return Collections.unmodifiableMap(map); - } catch (IOException | InterruptedException e) { - throw new IllegalStateException("Failed to retrieve authors information for '" + this.title() + "'"); - } - } - - private ModLoader modLoader(JsonObject deps) { - if (deps.has("forge")) { - return new ModLoader("forge", deps.get("forge").getAsString()); - } - - if (deps.has("fabric-loader")) { - return new ModLoader("fabric-loader", deps.get("fabric-loader").getAsString()); - } - - if (deps.has("quilt-loader")) { - return new ModLoader("quilt-loader", deps.get("quilt-loader").getAsString()); - } - - throw new IllegalStateException("Unknown launcher for pack '" + this.title() + "'"); - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/types/FileBase.java b/src/main/java/org/moddingx/modlistcreator/types/FileBase.java deleted file mode 100644 index 997fa8a..0000000 --- a/src/main/java/org/moddingx/modlistcreator/types/FileBase.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.moddingx.modlistcreator.types; - -import org.moddingx.modlistcreator.platform.Modpack; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Paths; - -public abstract class FileBase { - protected final StringBuilder builder; - protected final Modpack pack; - protected final boolean detailed; - protected final boolean headless; - - protected FileBase(Modpack pack, boolean detailed, boolean headless) { - this.builder = new StringBuilder(); - this.pack = pack; - this.detailed = detailed; - this.headless = headless; - } - - public abstract void generateFile(String name, File output); - - protected abstract String getFormattedProject(Modpack.ProjectEntry project); - - protected abstract String getFormattedAuthor(String member); - - public abstract String getExtension(); - - protected void generateFinalFile(String name, File output) { - if (this.builder.toString().isEmpty()) { - throw new IllegalStateException("Nothing to write to the file!"); - } - try { - if (!output.exists()) { - if (output.mkdirs()) { - System.out.println("Created output directory: " + output); - } - } - File file = new File(Paths.get(output.toString()) + File.separator + name + "." + this.getExtension()); - FileWriter writer = new FileWriter(file); - writer.write(this.getContent()); - writer.close(); - System.out.println("Successfully generated " + file.getName()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public String getContent() { - return this.builder.toString(); - } - - protected String getHeader() { - return String.format("%s - %s", this.pack.title(), this.pack.version()); - } - - protected void log(String text) { - System.out.println("[\u001B[32m" + this.pack.title() + "\u001B[0m] " + text); - } - - public static void log(String pack, String text) { - System.out.println("[\u001B[32m" + pack + "\u001B[0m] " + text); - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java b/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java deleted file mode 100644 index fb46bb6..0000000 --- a/src/main/java/org/moddingx/modlistcreator/types/files/HtmlFile.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.moddingx.modlistcreator.types.files; - -import org.moddingx.modlistcreator.platform.Modpack; -import org.moddingx.modlistcreator.types.FileBase; - -import java.io.File; - -public class HtmlFile extends FileBase { - - public HtmlFile(Modpack pack, boolean detailed, boolean headless) { - super(pack, detailed, headless); - } - - @Override - public void generateFile(String name, File output) { - if (!this.headless) { - this.builder.append("

    "); - this.builder.append(this.getHeader()); - this.builder.append("

    "); - this.builder.append("\n\n"); - } - - this.pack.files().forEach(entry -> { - this.builder.append("
  • "); - this.builder.append(this.getFormattedProject(entry)); - this.builder.append(" (by "); - this.builder.append(this.getFormattedAuthor(entry.author())); - this.builder.append(")
  • \n"); - }); - - this.generateFinalFile(name, output); - } - - @Override - protected String getFormattedProject(Modpack.ProjectEntry project) { - return String.format("%s", - project.website(), - this.detailed ? "/files/" + project.fileId() : "", - this.detailed ? project.fileName() : project.projectName()); - } - - @Override - protected String getFormattedAuthor(String member) { - return String.format("%s", member.toLowerCase(), member); - } - - @Override - public String getExtension() { - return "html"; - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java b/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java deleted file mode 100644 index f587bb3..0000000 --- a/src/main/java/org/moddingx/modlistcreator/types/files/MarkdownFile.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.moddingx.modlistcreator.types.files; - -import org.moddingx.modlistcreator.platform.Modpack; -import org.moddingx.modlistcreator.types.FileBase; - -import java.io.File; - -public class MarkdownFile extends FileBase { - - public MarkdownFile(Modpack pack, boolean detailed, boolean headless) { - super(pack, detailed, headless); - } - - @Override - public void generateFile(String name, File output) { - if (!this.headless) { - this.builder.append("## "); - this.builder.append(this.getHeader()); - this.builder.append("\n"); - } - - this.pack.files().forEach(entry -> { - this.builder.append("- "); - this.builder.append(this.getFormattedProject(entry)); - this.builder.append(" (by "); - this.builder.append(this.getFormattedAuthor(entry.author())); - this.builder.append(")\n"); - }); - - this.generateFinalFile(name, output); - } - - @Override - protected String getFormattedProject(Modpack.ProjectEntry project) { - return String.format("[%s](%s%s)", - this.detailed ? project.fileName() : project.projectName(), - project.website(), - this.detailed ? "/files/" + project.fileId() : ""); - } - - @Override - protected String getFormattedAuthor(String member) { - return String.format("[%s](" + this.pack.authorLink(member) + ")", member, member.toLowerCase()); - } - - @Override - public String getExtension() { - return "md"; - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/util/EnumConverters.java b/src/main/java/org/moddingx/modlistcreator/util/EnumConverters.java new file mode 100644 index 0000000..b19cd8d --- /dev/null +++ b/src/main/java/org/moddingx/modlistcreator/util/EnumConverters.java @@ -0,0 +1,28 @@ +package org.moddingx.modlistcreator.util; + +import joptsimple.util.EnumConverter; + +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; + +public class EnumConverters { + + public static > EnumConverter enumArg(Class cls) { + return new ConcreteEnumConverter<>(cls); + } + + private static class ConcreteEnumConverter> extends EnumConverter { + + private ConcreteEnumConverter(Class clazz) { + super(clazz); + } + + @Override + public String valuePattern() { + return Arrays.stream(this.valueType().getEnumConstants()) + .map(v -> v.name().toLowerCase(Locale.ROOT)) + .collect(Collectors.joining("|")); + } + } +} diff --git a/src/main/java/org/moddingx/modlistcreator/util/MapUtil.java b/src/main/java/org/moddingx/modlistcreator/util/MapUtil.java deleted file mode 100644 index 4a55630..0000000 --- a/src/main/java/org/moddingx/modlistcreator/util/MapUtil.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.moddingx.modlistcreator.util; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/* - * Taken from here: https://stackoverflow.com/questions/109383/sort-a-mapkey-value-by-values - */ -public class MapUtil { - public static > Map sortByValue(Map map) { - List> list = new ArrayList<>(map.entrySet()); - list.sort(Map.Entry.comparingByValue()); - - Map result = new LinkedHashMap<>(); - for (Map.Entry entry : list) { - result.put(entry.getKey(), entry.getValue()); - } - - return result; - } -} diff --git a/src/main/java/org/moddingx/modlistcreator/util/NameFormat.java b/src/main/java/org/moddingx/modlistcreator/util/NameFormat.java deleted file mode 100644 index 259fcb2..0000000 --- a/src/main/java/org/moddingx/modlistcreator/util/NameFormat.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.moddingx.modlistcreator.util; - -public enum NameFormat { - DEFAULT, - VERSION, - NAME, - NAME_VERSION; - - public static NameFormat get(String value) { - return NameFormat.valueOf(value.toUpperCase()); - } -} From f7b2bdcc9307c240e97df0a9527e3561a11965cf Mon Sep 17 00:00:00 2001 From: MelanX Date: Fri, 12 Aug 2022 19:26:43 +0200 Subject: [PATCH 3/6] Edit readme for changes --- README.md | 132 +++++++++++++++++------------------------------------- 1 file changed, 42 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 2ab6864..045ea9d 100644 --- a/README.md +++ b/README.md @@ -2,134 +2,86 @@ [![Total downloads](https://img.shields.io/github/downloads/ModdingX/ModListCreator/total.svg)](https://www.github.com/ModdingX/ModListCreator/releases/) -## How to use - -1. Put the ModListCreator file into a folder -2. Extract the `manifest.json` file from an exported modpack file into the same folder -3. Run ModListCreator jar file -4. Wait for all the `output/modlist.*` files - -## Arguments you could use - -Argument | Output ------------------ | -------------------------------------------------- -`help` | Shows descriptions to all arguments -`md` / `markdown` | Exports **Markdown** files -`html` | Exports **HTML** files -`detailed` | Shows exact version of each mod -`headless` | Generates the file without pack name/version -`nameFormat` | Defines the name format -`manifest` | Defines manifest file -`input` | Defines the input directory for multiple manifests/exported zips -`output` | Defines the output directory for generated files -`workingDir` | Defines the path where input and output should be - -You can combine multiple arguments in one command. You can see the usage below. - -### MD / Markdown / HTML - -To use this argument, use the following command: +## How to get -`$ java -jar ModListCreator-.jar --md` +1. [Download here](https://github.com/ModdingX/ModListCreator/releases) OR -`$ java -jar ModListCreator-.jar --markdown` +1. Clone this repository +2. Run `gradlew build` +3. Get file from path/build/libs/ -OR +## How to use - Modlist + +1. Put the ModListCreator file into a folder +2. Open terminal/cmd +3. Add `modlist` after `java -jar ModListCreator--fatjar.jar` +4. Set arguments listed below +5. After all arguments, set the input files (`folder/*` for whole folder) +6. Run it and wait for output file(s) -`$ java -jar ModListCreator-.jar --html` +## Arguments you could use -This will generate only the given file type. Otherwise it will generate all types. +| Argument | Output | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| no-header | Generates the file without pack name and version | +| detailed | Shows exact version of each mod | +| format | The output format to use (`txt`, `html`, or `md` (default)) | +| ** +output** | Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file. Use `folder/*` for all files in folder. | +| pattern | Defines the output file name pattern. %n is replaced with pack name, %v with pack version. | +## Examples ### Detailed To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --detailed` +`$ java -jar ModListCreator--fatjar.jar --detailed` -Without argument | With argument -------------------------------- | ---------------------------------------- -AIOT Botania (by MelanX) | aiotbotania-1.16.2-1.3.2.jar (by MelanX) -Automatic Tool Swap (by MelanX) | ToolSwap-1.16.2-1.2.0.jar (by MelanX) -Botania (by Vazkii) | Botania-1.16.3-409.jar (by Vazkii) +| Without argument | With argument | +|---------------------------------|------------------------------------------| +| AIOT Botania (by MelanX) | aiotbotania-1.16.2-1.3.2.jar (by MelanX) | +| Automatic Tool Swap (by MelanX) | ToolSwap-1.16.2-1.2.0.jar (by MelanX) | +| Botania (by Vazkii) | Botania-1.16.3-409.jar (by Vazkii) | -### Headless +### No Header To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --headless` +`$ java -jar ModListCreator--fatjar.jar --no-header` -Without argument | With argument -------------------------------------------- | ------------- -Garden of Glass (Questbook Edition) - 4.2.0 | _nothing_ +| Without argument | With argument | +|---------------------------------------------|---------------| +| Garden of Glass (Questbook Edition) - 4.2.0 | _nothing_ | -### NameFormat +### Pattern To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --nameFormat DEFAULT` - -Allowed values: +`$ java -jar ModListCreator--fatjar.jar --pattern "This is %n in version %v` -Format type | Output ------------- | ---------------------------------------------- -DEFAULT | modlist.md -VERSION | 4.2.0.md -NAME | Garden of Glass (Questbook Edition).md -NAME_VERSION | Garden of Glass (Questbook Edition) - 4.2.0.md - -If you generate .html files, the extension will be `.html`. +> This is CaveStone in version 0.4.0 ### Input To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --input examplePath` - -If you don't set the input, it will just use the [manifest.json](#manifest). +`$ java -jar ModListCreator--fatjar.jar --pattern "Name" --output output modpacks/*` -Otherwise, this will try to get each file in `examplePath` and creates a modlist in the [output](#output). +This will use the folder `modpacks` as input and tries to generate a modlist for each file in this folder. ### Output To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --output examplePath` +`$ java -jar ModListCreator--fatjar.jar --output output.md` -If you don't set the output, it will just use the `output` folder in the [base directory](#workingDir). - -Otherwise, this will try to generate all the modlist files into `examplePath`. If the folder doesn't exist, it will be generated. - -### Manifest - -To use this argument, use the following command: - -`$ java -jar ModListCreator-.jar --manifest example.json` - -You can also define an exported zip containing a `manifest.json`. -If you don't set the manifest, it will just use the `manifest.json` in the [base directory](#workingDir). - -### WorkingDir - -To use this argument, use the following command: - -`$ java -jar ModListCreator-.jar --workingDir D:\path\to\directory` - -If you don't set this, it will use the location of you Jar as base directory. +This will generate a file called `output.md`. If you set `--pattern` argument, it will generate a folder +called `output.md`. ## Why use this instead of exported modlist? - This tool sorts the project names alphabetically - This tool links to the project and the author - The official `modlist.html` contains broken links to the projects - -## How to get - -1. [Download here](https://github.com/ModdingX/ModListCreator/releases) - -OR - -1. Clone this repository -2. Run `gradlew build` -3. Get file from path/build/libs/ From ceb13cd213b8484e15f96c174c730aedba6a7fae Mon Sep 17 00:00:00 2001 From: MelanX Date: Fri, 12 Aug 2022 19:27:33 +0200 Subject: [PATCH 4/6] Edit readme for changes --- README.md | 131 +++++++++++++++++------------------------------------- 1 file changed, 41 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 2ab6864..e939856 100644 --- a/README.md +++ b/README.md @@ -2,134 +2,85 @@ [![Total downloads](https://img.shields.io/github/downloads/ModdingX/ModListCreator/total.svg)](https://www.github.com/ModdingX/ModListCreator/releases/) -## How to use - -1. Put the ModListCreator file into a folder -2. Extract the `manifest.json` file from an exported modpack file into the same folder -3. Run ModListCreator jar file -4. Wait for all the `output/modlist.*` files - -## Arguments you could use - -Argument | Output ------------------ | -------------------------------------------------- -`help` | Shows descriptions to all arguments -`md` / `markdown` | Exports **Markdown** files -`html` | Exports **HTML** files -`detailed` | Shows exact version of each mod -`headless` | Generates the file without pack name/version -`nameFormat` | Defines the name format -`manifest` | Defines manifest file -`input` | Defines the input directory for multiple manifests/exported zips -`output` | Defines the output directory for generated files -`workingDir` | Defines the path where input and output should be - -You can combine multiple arguments in one command. You can see the usage below. - -### MD / Markdown / HTML - -To use this argument, use the following command: +## How to get -`$ java -jar ModListCreator-.jar --md` +1. [Download here](https://github.com/ModdingX/ModListCreator/releases) OR -`$ java -jar ModListCreator-.jar --markdown` +1. Clone this repository +2. Run `gradlew build` +3. Get file from path/build/libs/ -OR +## How to use - Modlist + +1. Put the ModListCreator file into a folder +2. Open terminal/cmd +3. Add `modlist` after `java -jar ModListCreator--fatjar.jar` +4. Set arguments listed below +5. After all arguments, set the input files (`folder/*` for whole folder) +6. Run it and wait for output file(s) -`$ java -jar ModListCreator-.jar --html` +## Arguments you could use -This will generate only the given file type. Otherwise it will generate all types. +| Argument | Output | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| no-header | Generates the file without pack name and version | +| detailed | Shows exact version of each mod | +| format | The output format to use (`txt`, `html`, or `md` (default)) | +| **output** | Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file. Use `folder/*` for all files in folder. | +| pattern | Defines the output file name pattern. %n is replaced with pack name, %v with pack version. | +## Examples ### Detailed To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --detailed` +`$ java -jar ModListCreator--fatjar.jar --detailed` -Without argument | With argument -------------------------------- | ---------------------------------------- -AIOT Botania (by MelanX) | aiotbotania-1.16.2-1.3.2.jar (by MelanX) -Automatic Tool Swap (by MelanX) | ToolSwap-1.16.2-1.2.0.jar (by MelanX) -Botania (by Vazkii) | Botania-1.16.3-409.jar (by Vazkii) +| Without argument | With argument | +|---------------------------------|------------------------------------------| +| AIOT Botania (by MelanX) | aiotbotania-1.16.2-1.3.2.jar (by MelanX) | +| Automatic Tool Swap (by MelanX) | ToolSwap-1.16.2-1.2.0.jar (by MelanX) | +| Botania (by Vazkii) | Botania-1.16.3-409.jar (by Vazkii) | -### Headless +### No Header To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --headless` +`$ java -jar ModListCreator--fatjar.jar --no-header` -Without argument | With argument -------------------------------------------- | ------------- -Garden of Glass (Questbook Edition) - 4.2.0 | _nothing_ +| Without argument | With argument | +|---------------------------------------------|---------------| +| Garden of Glass (Questbook Edition) - 4.2.0 | _nothing_ | -### NameFormat +### Pattern To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --nameFormat DEFAULT` - -Allowed values: +`$ java -jar ModListCreator--fatjar.jar --pattern "This is %n in version %v` -Format type | Output ------------- | ---------------------------------------------- -DEFAULT | modlist.md -VERSION | 4.2.0.md -NAME | Garden of Glass (Questbook Edition).md -NAME_VERSION | Garden of Glass (Questbook Edition) - 4.2.0.md - -If you generate .html files, the extension will be `.html`. +> This is CaveStone in version 0.4.0 ### Input To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --input examplePath` - -If you don't set the input, it will just use the [manifest.json](#manifest). +`$ java -jar ModListCreator--fatjar.jar --pattern "Name" --output output modpacks/*` -Otherwise, this will try to get each file in `examplePath` and creates a modlist in the [output](#output). +This will use the folder `modpacks` as input and tries to generate a modlist for each file in this folder. ### Output To use this argument, use the following command: -`$ java -jar ModListCreator-.jar --output examplePath` +`$ java -jar ModListCreator--fatjar.jar --output output.md` -If you don't set the output, it will just use the `output` folder in the [base directory](#workingDir). - -Otherwise, this will try to generate all the modlist files into `examplePath`. If the folder doesn't exist, it will be generated. - -### Manifest - -To use this argument, use the following command: - -`$ java -jar ModListCreator-.jar --manifest example.json` - -You can also define an exported zip containing a `manifest.json`. -If you don't set the manifest, it will just use the `manifest.json` in the [base directory](#workingDir). - -### WorkingDir - -To use this argument, use the following command: - -`$ java -jar ModListCreator-.jar --workingDir D:\path\to\directory` - -If you don't set this, it will use the location of you Jar as base directory. +This will generate a file called `output.md`. If you set `--pattern` argument, it will generate a folder +called `output.md`. ## Why use this instead of exported modlist? - This tool sorts the project names alphabetically - This tool links to the project and the author - The official `modlist.html` contains broken links to the projects - -## How to get - -1. [Download here](https://github.com/ModdingX/ModListCreator/releases) - -OR - -1. Clone this repository -2. Run `gradlew build` -3. Get file from path/build/libs/ From 00812925df4bbcb18841029ca0ad9c87dcb03e95 Mon Sep 17 00:00:00 2001 From: MelanX Date: Fri, 12 Aug 2022 20:02:18 +0200 Subject: [PATCH 5/6] Fix readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e939856..8b65a17 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ OR ## Arguments you could use -| Argument | Output | -|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| no-header | Generates the file without pack name and version | -| detailed | Shows exact version of each mod | -| format | The output format to use (`txt`, `html`, or `md` (default)) | -| **output** | Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file. Use `folder/*` for all files in folder. | -| pattern | Defines the output file name pattern. %n is replaced with pack name, %v with pack version. | +| Argument | Output | +|------------|---------------------------------------------------------------------------------------------------------------------------------| +| no-header | Generates the file without pack name and version | +| detailed | Shows exact version of each mod | +| format | The output format to use (`txt`, `html`, or `md` (default)) | +| **output** | Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file. | +| pattern | Defines the output file name pattern. %n is replaced with pack name, %v with pack version. | ## Examples ### Detailed From 11223cf1078da2902aba2d0a63d0d2f46c5e0d9b Mon Sep 17 00:00:00 2001 From: MelanX Date: Sun, 14 Aug 2022 14:54:08 +0200 Subject: [PATCH 6/6] Fixes --- .../platform/ModrinthModpack.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java b/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java index 8f40e42..763d184 100644 --- a/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java +++ b/src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java @@ -67,19 +67,26 @@ public static Optional load(JsonElement json) throws IOExceptio record FileData(String projectId, String versionId, String fileName) {} List fileData = new ArrayList<>(); - + for (String hash : hashes) { if (!filesResponse.has(hash)) { throw new IllegalArgumentException("File not hosted on modrinth: sha512=" + hash); } + JsonObject versionData = filesResponse.get(hash).getAsJsonObject(); String fileName = null; - for (JsonElement versionFile : versionData.get("files").getAsJsonArray()) { - if (versionFile.getAsJsonObject().get("primary").getAsBoolean()) { - fileName = versionFile.getAsJsonObject().get("filename").getAsString(); - break; + JsonArray versionFiles = versionData.get("files").getAsJsonArray(); + if (versionFiles.size() == 1) { + fileName = versionFiles.get(0).getAsJsonObject().get("filename").getAsString(); + } else { + for (JsonElement versionFile : versionFiles) { + if (versionFile.getAsJsonObject().get("primary").getAsBoolean()) { + fileName = versionFile.getAsJsonObject().get("filename").getAsString(); + break; + } } } + if (fileName == null) { throw new IOException("Version has no primary file"); } @@ -120,7 +127,7 @@ record TeamData(String owner, URI teamURL) {} if ("Owner".equals(teamEntry.get("role").getAsString())) { JsonObject user = teamEntry.get("user").getAsJsonObject(); String name = user.get("username").getAsString(); - if (user.has("name") && !user.get("name").isJsonNull()) { + if (user.has("name") && !user.get("name").isJsonNull() && !user.get("name").getAsString().isEmpty()) { name = user.get("name").getAsString(); } teamData.put(teamEntry.get("team_id").getAsString(), new TeamData(