Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,8 @@ public Mono<Version> resolveProjectVersion(Project project, ProjectRef projectRe
@Nullable Loader loader, String gameVersion,
VersionType defaultVersionType) {

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

if (projectRef.hasVersionName()) {
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
return getVersionsForProject(project.getId(), loader, gameVersion)
.flatMap(versions ->
Mono.justOrEmpty(versions.stream()
.filter(version ->
Expand All @@ -144,7 +142,7 @@ public Mono<Version> resolveProjectVersion(Project project, ProjectRef projectRe
));
}
if (projectRef.hasVersionType()) {
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
return getVersionsForProject(project.getId(), loader, gameVersion)
.mapNotNull(versions -> pickVersion(project, versions, projectRef.getVersionType()));
} else if (projectRef.hasVersionId()) {
return getVersionFromId(projectRef.getVersionId())
Expand All @@ -153,7 +151,7 @@ public Mono<Version> resolveProjectVersion(Project project, ProjectRef projectRe
projectRef.getVersionId(), project.getSlug()))
);
} else {
return getVersionsForProject(project.getId(), loaderToQuery, gameVersion)
return getVersionsForProject(project.getId(), loader, gameVersion)
.mapNotNull(versions -> pickVersion(project, versions, defaultVersionType));
}
}
Expand Down
57 changes: 27 additions & 30 deletions src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
public class ModrinthCommand implements Callable<Integer> {

public static final String DATAPACKS_SUBDIR = "datapacks";
@Option(names = "--projects", description = "Project ID or Slug",
@Option(names = "--projects", description = "Project ID or Slug. Prefix with loader: e.g. fabric:project-id",
split = SPLIT_COMMA_NL, splitSynopsisLabel = SPLIT_SYNOPSIS_COMMA_NL,
paramLabel = "id|slug"
paramLabel = "[loader:]id|slug"
)
List<String> projects;

Expand Down Expand Up @@ -157,7 +157,7 @@ private ModrinthManifest loadManifest() throws IOException {
return Manifests.load(outputDirectory, ModrinthManifest.ID, ModrinthManifest.class);
}

private Stream<Version> expandDependencies(ModrinthApiClient modrinthApiClient, Project project, Version version) {
private Stream<Version> expandDependencies(ModrinthApiClient modrinthApiClient, Loader loader, String gameVersion, Project project, Version version) {
log.debug("Expanding dependencies of version={}", version);
return version.getDependencies().stream()
.filter(this::filterDependency)
Expand All @@ -170,7 +170,7 @@ private Stream<Version> expandDependencies(ModrinthApiClient modrinthApiClient,
if (dep.getVersionId() == null) {
log.debug("Fetching versions of dep={} and picking", dep);
depVersion = pickVersion(
getVersionsForProject(modrinthApiClient, dep.getProjectId())
getVersionsForProject(modrinthApiClient, dep.getProjectId(), loader, gameVersion)
);
}
else {
Expand All @@ -193,7 +193,7 @@ private Stream<Version> expandDependencies(ModrinthApiClient modrinthApiClient,
log.debug("Resolved version={} for dep={}", depVersion.getVersionNumber(), dep);
return Stream.concat(
Stream.of(depVersion),
expandDependencies(modrinthApiClient, project, depVersion)
expandDependencies(modrinthApiClient, loader, gameVersion, project, depVersion)
)
.peek(expandedVer -> log.debug("Expanded dependency={} into version={}", dep, expandedVer));
}
Expand Down Expand Up @@ -229,16 +229,14 @@ private Version pickVersion(List<Version> versions, VersionType versionType) {
return null;
}

private Path download(boolean isDatapack, VersionFile versionFile) {
private Path download(Loader loader, VersionFile versionFile) {
final Path outPath;
try {
if (!isDatapack) {
outPath = Files.createDirectories(outputDirectory
.resolve(loader.getType())
)
.resolve(versionFile.getFilename());
}
else {
final Loader effectiveLoader = loader != null ? loader : loader;
final String outputType = effectiveLoader.getType();

if (outputType == null) {
// Datapack case
if (worldDirectory.isAbsolute()) {
outPath = Files.createDirectories(worldDirectory
.resolve(DATAPACKS_SUBDIR)
Expand All @@ -253,9 +251,15 @@ private Path download(boolean isDatapack, VersionFile versionFile) {
.resolve(versionFile.getFilename());
}
}
else {
outPath = Files.createDirectories(outputDirectory
.resolve(outputType)
)
.resolve(versionFile.getFilename());
}

} catch (IOException e) {
throw new RuntimeException("Creating mods directory", e);
throw new RuntimeException("Creating output directory", e);
}

try {
Expand All @@ -267,11 +271,11 @@ private Path download(boolean isDatapack, VersionFile versionFile) {
.handleStatus(Fetch.loggingDownloadStatusHandler(log))
.execute();
} catch (IOException e) {
throw new RuntimeException("Downloading mod file", e);
throw new RuntimeException("Downloading file", e);
}
}

private List<Version> getVersionsForProject(ModrinthApiClient modrinthApiClient, String project) {
private List<Version> getVersionsForProject(ModrinthApiClient modrinthApiClient, String project, Loader loader, String gameVersion) {
final List<Version> versions = modrinthApiClient.getVersionsForProject(
project, loader, gameVersion
)
Expand All @@ -294,10 +298,12 @@ private Stream<Path> processProject(ModrinthApiClient modrinthApiClient, Project
log.debug("Starting with project='{}' slug={}", project.getTitle(), project.getSlug());

if (projectsProcessed.add(project.getId())) {
final Loader effectiveLoader = projectRef.getLoader() != null ? projectRef.getLoader() : loader;

final Version version;
try {
version = modrinthApiClient.resolveProjectVersion(
project, projectRef, loader, gameVersion, defaultVersionType
project, projectRef, effectiveLoader, gameVersion, defaultVersionType
)
.block();
} catch (NoApplicableVersionsException | NoFilesAvailableException e) {
Expand All @@ -309,33 +315,24 @@ 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, project, version)
expandDependencies(modrinthApiClient, effectiveLoader, gameVersion, project, version)
)
.map(ModrinthApiClient::pickVersionFile)
.map(versionFile -> download(isDatapack, versionFile))
.flatMap(downloadedFile -> !isDatapack ? expandIfZip(downloadedFile) : Stream.empty());
.map(versionFile -> download(effectiveLoader, versionFile))
.flatMap(downloadedFile -> expandIfZip(downloadedFile));
}
else {
throw new InvalidParameterException(
String.format("Project %s does not have any matching versions for loader %s, game version %s",
projectRef, loader, gameVersion
projectRef, effectiveLoader, gameVersion
));
}
}
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
116 changes: 74 additions & 42 deletions src/main/java/me/itzg/helpers/modrinth/ProjectRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.ToString;
import me.itzg.helpers.errors.InvalidParameterException;
Expand All @@ -22,10 +25,13 @@ public class ProjectRef {
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 static final Set<String> VALID_LOADERS = Arrays.stream(Loader.values())
.map(Enum::name)
.map(String::toLowerCase)
.collect(Collectors.toSet());

private final String idOrSlug;
private final boolean datapack;
private final Loader loader;

/**
* Either a remote URI or a file URI for a locally provided file
Expand All @@ -36,32 +42,55 @@ public class ProjectRef {
private final String versionNumber;

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

// Handle cases with no colon
if (parts.length == 1) {
return new ProjectRef(parts[0], null, null);
}

return new ProjectRef(
m.group("idSlug"),
m.group("version"),
m.group("datapack") != null
);

// Check if first part is a valid loader
Loader loader = null;
int idIndex = 0;
if (VALID_LOADERS.contains(parts[0].toLowerCase())) {
loader = Loader.valueOf(parts[0].toLowerCase());
idIndex = 1;
}

// Handle remaining parts
if (parts.length == 2) {
// Either loader:id or id:version
if (loader != null) {
return new ProjectRef(parts[1], null, loader);
} else {
return new ProjectRef(parts[0], parts[1], null);
}
}
else if (parts.length == 3) {
// Must be loader:id:version
if (loader == null) {
throw new InvalidParameterException("Invalid loader in project reference: " + parts[0]);
}
return new ProjectRef(parts[1], parts[2], loader);
}

throw new InvalidParameterException("Invalid project reference: " + projectRef);
}

/**
*
* @param version can be a {@link VersionType}, ID, or name/number
*/
public ProjectRef(String projectSlug, String version) {
this(projectSlug, version, false);
this(projectSlug, version, null);
}

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

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

final String filename = extractFilename(projectUri);
Expand All @@ -103,37 +132,40 @@ public static ProjectRef fromPossibleUrl(
String possibleUrl, String defaultVersion)
{
// First, see if it is a modrinth page URL

final Matcher m = MODPACK_PAGE_URL.matcher(possibleUrl);
if(m.matches()) {
String projectSlug = m.group("slug");
String projectVersion = m.group("versionName") != null ?
m.group("versionName") : defaultVersion;
return new ProjectRef(projectSlug, projectVersion);
} else {
try {
// Might be custom URL, local file, or slug
// ...try as a (remote or file) URL first
return new ProjectRef(
new URL(possibleUrl).toURI(), defaultVersion
);
} catch(MalformedURLException | URISyntaxException e) {
// Not a valid URL, so
// narrow down if it is a file path by looking at suffix
if (possibleUrl.endsWith(".mrpack")) {
final Path path = Paths.get(possibleUrl);
if (!Files.exists(path)) {
throw new InvalidParameterException("Given modrinth project looks like a file, but doesn't exist");
}

try {
final Matcher m = MODPACK_PAGE_URL.matcher(possibleUrl);
if(m.matches()) {
String projectSlug = m.group("slug");
String projectVersion = m.group("versionName") != null ?
m.group("versionName") : defaultVersion;
return new ProjectRef(projectSlug, projectVersion);
} else {
try {
// Might be custom URL, local file, or slug
// ...try as a (remote or file) URL first
return new ProjectRef(
path.toUri(),
defaultVersion
new URL(possibleUrl).toURI(), defaultVersion
);
}
} catch(MalformedURLException | URISyntaxException e) {
// Not a valid URL, so
// narrow down if it is a file path by looking at suffix
if (possibleUrl.endsWith(".mrpack")) {
final Path path = Paths.get(possibleUrl);
if (!Files.exists(path)) {
throw new InvalidParameterException("Given modrinth project looks like a file, but doesn't exist");
}

return new ProjectRef(
path.toUri(),
defaultVersion
);
}

return new ProjectRef(possibleUrl, defaultVersion);
return new ProjectRef(possibleUrl, defaultVersion);
}
}
} catch (Exception e) {
throw new InvalidParameterException("Invalid project reference: " + possibleUrl, e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ void handlesDatapacksSpecificVersion(boolean absoluteWorldDir, @TempDir Path tem
tempDir.resolve(worldDir).toString()
: worldDir,
"--game-version", "1.21.1",
"--loader", "datapack",
"--loader", "paper",
"--projects", String.format("datapack:%s:%s", projectId, versionId)
);

Expand Down Expand Up @@ -496,4 +496,4 @@ private static void setupStubs() {
)
);
}
}
}
14 changes: 7 additions & 7 deletions src/test/java/me/itzg/helpers/modrinth/ProjectRefTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,22 @@ void constructorPullsProjectSlugFromFileURI(String input) {

@ParameterizedTest
@MethodSource("parseProjectRef_parameters")
void parseProjectRef(String input, String slugId, VersionType versionType, String versionId, String versionName, boolean datapack) {
void parseProjectRef(String input, String slugId, VersionType versionType, String versionId, String versionName, Loader loader) {
final ProjectRef result = ProjectRef.parse(input);
assertThat(result.getIdOrSlug()).isEqualTo(slugId);
assertThat(result.getVersionType()).isEqualTo(versionType);
assertThat(result.getVersionId()).isEqualTo(versionId);
assertThat(result.getVersionNumber()).isEqualTo(versionName);
assertThat(result.isDatapack()).isEqualTo(datapack);
assertThat(result.getLoader()).isEqualTo(loader);
}

public static Stream<Arguments> parseProjectRef_parameters() {
return Stream.of(
argumentSet("just slugId","terralith", "terralith", null, null, null, false),
argumentSet("datapack","datapack:terralith", "terralith", null, null, null, true),
argumentSet("with version ID","terralith:rEF3UnUI", "terralith", null, "rEF3UnUI", null, false),
argumentSet("with version type","terralith:release", "terralith", VersionType.release, null, null, false),
argumentSet("with version name","terralith:2.5.5", "terralith", null, null, "2.5.5", false)
argumentSet("just slugId","terralith", "terralith", null, null, null, null),
argumentSet("with loader prefix","fabric:terralith", "terralith", null, null, null, Loader.fabric),
argumentSet("with loader and version ID","paper:terralith:rEF3UnUI", "terralith", null, "rEF3UnUI", null, Loader.paper),
argumentSet("with loader and version type","datapack:terralith:release", "terralith", VersionType.release, null, null, Loader.datapack),
argumentSet("with loader and version name","forge:terralith:2.5.5", "terralith", null, null, "2.5.5", Loader.forge)
);
}
}