Skip to content

Commit

Permalink
modrinth: support datapack retrieval (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
itzg authored Oct 7, 2024
1 parent db89f8b commit 87a056c
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 35 deletions.
3 changes: 2 additions & 1 deletion src/main/java/me/itzg/helpers/modrinth/Loader.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum Loader {
pufferfish("plugins", paper),
purpur("plugins", paper),
bungeecord("plugins", null),
velocity("plugins", null);
velocity("plugins", null),
datapack(null, null);

private final String type;
private final Loader compatibleWith;
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ public Mono<List<ResolvedProject>> bulkGetProjects(Stream<ProjectRef> projectRef
public Mono<Version> resolveProjectVersion(Project project, ProjectRef projectRef,
@Nullable Loader loader, String gameVersion,
VersionType defaultVersionType) {

final Loader loaderToQuery = projectRef.isDatapack() ? Loader.datapack : loader;

if (projectRef.hasVersionName()) {
return getVersionsForProject(project.getId(), loader, gameVersion)
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
.flatMap(versions ->
Mono.justOrEmpty(versions.stream()
.filter(version ->
Expand All @@ -137,12 +140,12 @@ public Mono<Version> resolveProjectVersion(Project project, ProjectRef projectRe
));
}
if (projectRef.hasVersionType()) {
return getVersionsForProject(project.getId(), loader, gameVersion)
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
.mapNotNull(versions -> pickVersion(project, versions, projectRef.getVersionType()));
} else if (projectRef.hasVersionId()) {
return getVersionFromId(projectRef.getVersionId());
} else {
return getVersionsForProject(project.getId(), loader, gameVersion)
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
.mapNotNull(versions -> pickVersion(project, versions, defaultVersionType));
}
}
Expand Down Expand Up @@ -190,7 +193,7 @@ public Mono<List<Version>> getVersionsForProject(String projectIdOrSlug,
);
}

private List<String> expandCompatibleLoaders(Loader loader) {
private List<String> expandCompatibleLoaders(@Nullable Loader loader) {
if (loader == null) {
return null;
}
Expand Down
62 changes: 52 additions & 10 deletions src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.SharedFetchArgs;
import me.itzg.helpers.json.ObjectMappers;
import me.itzg.helpers.modrinth.model.Constants;
import me.itzg.helpers.modrinth.model.DependencyType;
import me.itzg.helpers.modrinth.model.Project;
import me.itzg.helpers.modrinth.model.ProjectType;
Expand All @@ -42,6 +43,7 @@
@Slf4j
public class ModrinthCommand implements Callable<Integer> {

public static final String DATAPACKS_SUBDIR = "datapacks";
@Option(names = "--projects", description = "Project ID or Slug",
split = SPLIT_COMMA_NL, splitSynopsisLabel = SPLIT_SYNOPSIS_COMMA_NL,
paramLabel = "id|slug"
Expand Down Expand Up @@ -78,6 +80,11 @@ public enum DownloadDependencies {
)
String baseUrl;

@Option(names = "--world-directory", defaultValue = "${env:LEVEL:-world}",
description = "Used for datapacks, a path relative to the output directory or an absolute path\nDefault: ${DEFAULT-VALUE}"
)
Path worldDirectory;

@ArgGroup(exclusive = false)
SharedFetchArgs sharedFetchArgs = new SharedFetchArgs();

Expand Down Expand Up @@ -115,7 +122,9 @@ private List<Path> processProjects(List<String> projects) {
.defaultIfEmpty(Collections.emptyList())
.block()
.stream()
.flatMap(resolvedProject -> processProject(modrinthApiClient, resolvedProject.getProjectRef(), resolvedProject.getProject()))
.flatMap(resolvedProject -> processProject(
modrinthApiClient, resolvedProject.getProjectRef(), resolvedProject.getProject()
))
.collect(Collectors.toList());
}
}
Expand Down Expand Up @@ -201,14 +210,31 @@ private Version pickVersion(List<Version> versions, VersionType versionType) {
return null;
}

private Path download(ProjectType projectType, VersionFile versionFile) {
if (projectType != ProjectType.mod) {
throw new InvalidParameterException("Only mod project types can be downloaded for now");
}
private Path download(boolean isDatapack, VersionFile versionFile) {
final Path outPath;
try {
outPath = Files.createDirectories(outputDirectory.resolve(loader.getType()))
.resolve(versionFile.getFilename());
if (!isDatapack) {
outPath = Files.createDirectories(outputDirectory
.resolve(loader.getType())
)
.resolve(versionFile.getFilename());
}
else {
if (worldDirectory.isAbsolute()) {
outPath = Files.createDirectories(worldDirectory
.resolve(DATAPACKS_SUBDIR)
)
.resolve(versionFile.getFilename());
}
else {
outPath = Files.createDirectories(outputDirectory
.resolve(worldDirectory)
.resolve(DATAPACKS_SUBDIR)
)
.resolve(versionFile.getFilename());
}
}

} catch (IOException e) {
throw new RuntimeException("Creating mods directory", e);
}
Expand Down Expand Up @@ -238,7 +264,14 @@ private List<Version> getVersionsForProject(ModrinthApiClient modrinthApiClient,


private Stream<Path> processProject(ModrinthApiClient modrinthApiClient, ProjectRef projectRef, Project project) {
log.debug("Starting with projectRef={}", projectRef);
if (project.getProjectType() != ProjectType.mod) {
throw new InvalidParameterException(
String.format("Requested project '%s' is not a mod, but has type %s",
project.getTitle(), project.getProjectType()
));
}

log.debug("Starting with project='{}' slug={}", project.getTitle(), project.getSlug());

if (projectsProcessed.add(project.getId())) {
final Version version;
Expand All @@ -256,13 +289,15 @@ private Stream<Path> processProject(ModrinthApiClient modrinthApiClient, Project
throw new GenericException(String.format("Project %s has no files declared", project.getSlug()));
}

final boolean isDatapack = isDatapack(version);

return Stream.concat(
Stream.of(version),
expandDependencies(modrinthApiClient, version)
)
.map(ModrinthApiClient::pickVersionFile)
.map(versionFile -> download(project.getProjectType(), versionFile))
.flatMap(this::expandIfZip);
.map(versionFile -> download(isDatapack, versionFile))
.flatMap(downloadedFile -> !isDatapack ? expandIfZip(downloadedFile) : Stream.empty());
}
else {
throw new InvalidParameterException(
Expand All @@ -274,6 +309,13 @@ private Stream<Path> processProject(ModrinthApiClient modrinthApiClient, Project
return Stream.empty();
}

private boolean isDatapack(Version version) {
return
version.getLoaders() != null
&& version.getLoaders().size() == 1
&& version.getLoaders().get(0).equals(Constants.LOADER_DATAPACK);
}

/**
* If downloadedFile ends in .zip, then expand it, return its files and given file.
*
Expand Down
36 changes: 27 additions & 9 deletions src/main/java/me/itzg/helpers/modrinth/ProjectRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,38 @@
import lombok.ToString;
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.modrinth.model.VersionType;
import org.jetbrains.annotations.Nullable;

@Getter
@ToString
public class ProjectRef {
private static final Pattern VERSIONS = Pattern.compile("[a-zA-Z0-9]{8}");
private final static Pattern MODPACK_PAGE_URL = Pattern.compile(
private static final Pattern MODPACK_PAGE_URL = Pattern.compile(
"https://modrinth.com/modpack/(?<slug>.+?)(/version/(?<versionName>.+))?"
);
private static final Pattern PROJECT_REF = Pattern.compile("(?<datapack>datapack:)?(?<idSlug>[^:]+?)(:(?<version>[^:]+))?");

private final String idOrSlug;
private final boolean datapack;

final String idOrSlug;
/**
* Either a remote URI or a file URI for a locally provided file
*/
final URI projectUri;
final VersionType versionType;
final String versionId;
final String versionName;
private final URI projectUri;
private final VersionType versionType;
private final String versionId;
private final String versionName;

public static ProjectRef parse(String projectRef) {
final String[] projectRefParts = projectRef.split(":", 2);
final Matcher m = PROJECT_REF.matcher(projectRef);
if (!m.matches()) {
throw new InvalidParameterException("Invalid project reference: " + projectRef);
}

return new ProjectRef(projectRefParts[0],
projectRefParts.length > 1 ? projectRefParts[1] : null
return new ProjectRef(
m.group("idSlug"),
m.group("version"),
m.group("datapack") != null
);
}

Expand All @@ -44,7 +53,15 @@ public static ProjectRef parse(String projectRef) {
* @param version can be a {@link VersionType}, ID, or name/number
*/
public ProjectRef(String projectSlug, String version) {
this(projectSlug, version, false);
}

/**
* @param version can be a {@link VersionType}, ID, or name/number
*/
public ProjectRef(String projectSlug, @Nullable String version, boolean datapack) {
this.idOrSlug = projectSlug;
this.datapack = datapack;
this.projectUri = null;
this.versionType = parseVersionType(version);
if (this.versionType == null) {
Expand All @@ -64,6 +81,7 @@ public ProjectRef(String projectSlug, String version) {
}

public ProjectRef(URI projectUri, String versionId) {
this.datapack = false;
this.projectUri = projectUri;

final String filename = extractFilename(projectUri);
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/me/itzg/helpers/modrinth/model/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.itzg.helpers.modrinth.model;

public class Constants {

public static final String LOADER_DATAPACK = "datapack";

}
24 changes: 13 additions & 11 deletions src/main/java/me/itzg/helpers/modrinth/model/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,31 @@

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;

import java.time.Instant;
import java.util.List;
import lombok.Data;

@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Version {
String id;

String projectId;
private String id;

private String projectId;

private String name;

String name;
private Instant datePublished;

Instant datePublished;
private String versionNumber;

String versionNumber;
private VersionType versionType;

VersionType versionType;
private List<VersionFile> files;

List<VersionFile> files;
private List<VersionDependency> dependencies;

List<VersionDependency> dependencies;
private List<String> gameVersions;

List<String> gameVersions;
private List<String> loaders;
}
Loading

0 comments on commit 87a056c

Please sign in to comment.