Skip to content

Commit

Permalink
cf: use fallback URL for curseforge files like modpack files (#493)
Browse files Browse the repository at this point in the history
  • Loading branch information
itzg authored Nov 23, 2024
1 parent cb93498 commit f6ac245
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 207 deletions.
142 changes: 53 additions & 89 deletions src/main/java/me/itzg/helpers/curseforge/CurseForgeFilesCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import static me.itzg.helpers.curseforge.CurseForgeApiClient.*;
import static me.itzg.helpers.curseforge.ModFileRefResolver.idsFrom;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -20,13 +18,12 @@
import me.itzg.helpers.cache.ApiCachingImpl;
import me.itzg.helpers.cache.CacheArgs;
import me.itzg.helpers.curseforge.CurseForgeFilesManifest.FileEntry;
import me.itzg.helpers.curseforge.model.Category;
import me.itzg.helpers.curseforge.OutputSubdirResolver.Result;
import me.itzg.helpers.curseforge.model.CurseForgeFile;
import me.itzg.helpers.curseforge.model.CurseForgeMod;
import me.itzg.helpers.curseforge.model.FileDependency;
import me.itzg.helpers.curseforge.model.FileRelationType;
import me.itzg.helpers.curseforge.model.ModLoaderType;
import me.itzg.helpers.errors.GenericException;
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.files.Manifests;
import me.itzg.helpers.http.SharedFetchArgs;
Expand All @@ -38,7 +35,6 @@
import picocli.CommandLine.Parameters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

Expand All @@ -51,13 +47,6 @@ public class CurseForgeFilesCommand implements Callable<Integer> {
CATEGORY_BUKKIT_PLUGINS
);

private static final Map<String/*categorySlug*/, String /*subdir*/> categorySubdirs = new HashMap<>();

static {
categorySubdirs.put(CATEGORY_MC_MODS, "mods");
categorySubdirs.put(CATEGORY_BUKKIT_PLUGINS, "plugins");
}

@Option(names = {"--help", "-h"}, usageHelp = true)
boolean help;

Expand Down Expand Up @@ -135,7 +124,8 @@ public Integer call() throws Exception {
apiCaching
)
) {
newManifest = apiClient.loadCategoryInfo(Arrays.asList(CATEGORY_MC_MODS, CATEGORY_BUKKIT_PLUGINS))
newManifest =
apiClient.loadCategoryInfo(Arrays.asList(CATEGORY_MC_MODS, CATEGORY_BUKKIT_PLUGINS))
.flatMap(categoryInfo ->
processModFileRefs(categoryInfo, previousFiles, apiClient)
.map(entries -> CurseForgeFilesManifest.builder()
Expand Down Expand Up @@ -178,37 +168,63 @@ private Mono<List<FileEntry>> processModFileRefs(CategoryInfo categoryInfo,

final ModFileRefResolver modFileRefResolver = new ModFileRefResolver(apiClient, categoryInfo);

return modFileRefResolver.resolveModFiles(modFileRefs, defaultCategory, gameVersion, modLoaderType)
.flatMapMany(modFiles -> {
final OutputSubdirResolver outputSubdirResolver = new OutputSubdirResolver(outputDir, categoryInfo);

return
modFileRefResolver.resolveModFiles(modFileRefs, defaultCategory, gameVersion, modLoaderType)
.flatMapMany(modFiles ->
{
final Set<Integer> requestedModIds = modFiles.stream()
.map(CurseForgeFile::getModId)
.collect(Collectors.toSet());

return Flux.fromIterable(modFiles)
.flatMap(modFile -> {

final Mono<FileEntry> retrieval;
final ModFileIds modFileIds = idsFrom(modFile);
final FileEntry entry = previousFiles.get(modFileIds);
if (entry != null
&& Files.exists(outputDir.resolve(entry.getFilePath()))
) {
log.debug("Mod file {} already exists at {}", modFile.getFileName(), entry.getFilePath());
retrieval = Mono.just(entry);
}
else {
retrieval = retrieveModFile(apiClient, categoryInfo, modFile)
.map(path -> new FileEntry(modFileIds, outputDir.relativize(path).toString()));
}

return reportMissingDependencies(apiClient, modFile, requestedModIds)
.then(retrieval);
});
.flatMap(cfFile -> processFile(apiClient, outputSubdirResolver, previousFiles, requestedModIds, cfFile));
}
)
.collectList();
}

private Mono<FileEntry> processFile(CurseForgeApiClient apiClient, OutputSubdirResolver outputSubdirResolver, Map<ModFileIds, FileEntry> previousFiles,
Set<Integer> requestedModIds, CurseForgeFile cfFile
) {
final ModFileIds modFileIds = idsFrom(cfFile);
final FileEntry entry = previousFiles.get(modFileIds);

final Mono<FileEntry> retrievalMono;
if (entry != null) {
log.debug("Mod file {} already exists at {}", cfFile.getFileName(), entry.getFilePath());
retrievalMono = Mono.just(entry);
}
else {
retrievalMono =
resolveOutputSubdir(apiClient, outputSubdirResolver, cfFile)
.flatMap(subdir ->
apiClient.download(cfFile,
subdir.resolve(cfFile.getFileName()),
modFileDownloadStatusHandler(outputDir, log)
)
.map(path -> new FileEntry(modFileIds,
outputDir.relativize(path).toString()
))
);
}

return reportMissingDependencies(apiClient, cfFile, requestedModIds)
.then(retrievalMono);
}

private Mono<Path> resolveOutputSubdir(
CurseForgeApiClient apiClient, OutputSubdirResolver outputSubdirResolver,
CurseForgeFile cfFile
) {
return apiClient.getModInfo(cfFile.getModId())
.flatMap(modInfo ->
outputSubdirResolver.resolve(modInfo)
.map(Result::getDir)
);
}

/**
*
* @return flux of missing, required dependencies
Expand Down Expand Up @@ -251,9 +267,11 @@ else if (missingDep.getT2().getRelationType() == FileRelationType.OptionalDepend
}

@NotNull
private static Map<ModFileIds, FileEntry> buildPreviousFilesFromManifest(CurseForgeFilesManifest oldManifest) {
private Map<ModFileIds, FileEntry> buildPreviousFilesFromManifest(CurseForgeFilesManifest oldManifest) {
return oldManifest != null ?
oldManifest.getEntries().stream()
// make sure file still exists
.filter(fileEntry -> Files.exists(outputDir.resolve(fileEntry.getFilePath())))
.collect(Collectors.toMap(
FileEntry::getIds,
fileEntry -> fileEntry,
Expand All @@ -266,60 +284,6 @@ private static Map<ModFileIds, FileEntry> buildPreviousFilesFromManifest(CurseFo
: Collections.emptyMap();
}

@NotNull
private Mono<Path> retrieveModFile(CurseForgeApiClient apiClient, CategoryInfo categoryInfo,
CurseForgeFile curseForgeFile
) {
return apiClient.getModInfo(curseForgeFile.getModId())
.flatMap(curseForgeMod -> {
if (curseForgeFile.getDownloadUrl() == null) {
log.error("The authors of the mod '{}' have disallowed automated downloads. " +
"Manually download the file '{}' from {} and supply separately.",
curseForgeMod.getName(), curseForgeFile.getDisplayName(), curseForgeMod.getLinks().getWebsiteUrl()
);

return Mono.error(new InvalidParameterException(
String.format("The authors of %s do not allow automated downloads",
curseForgeMod.getName()
))
);
}

return setupSubdir(categoryInfo, curseForgeMod)
.flatMap(subdir ->
apiClient.download(curseForgeFile,
outputDir.resolve(subdir).resolve(curseForgeFile.getFileName()),
modFileDownloadStatusHandler(outputDir, log)
)
);
}
)
.checkpoint(String.format("Retrieving %d:%d", curseForgeFile.getModId(), curseForgeFile.getId()));
}

@NotNull
private Mono<String> setupSubdir(CategoryInfo categoryInfo, CurseForgeMod curseForgeMod) {
return Mono.defer(() -> {
final Category category = categoryInfo.getCategory(curseForgeMod.getClassId());

final String subdir = categorySubdirs.get(category.getSlug());
if (subdir == null) {
return Mono.error(new InvalidParameterException(
String.format("Category %s does not have a known subdir", category.getName())));
}

try {
//noinspection BlockingMethodInNonBlockingContext due to subscribeOn
Files.createDirectories(outputDir.resolve(subdir));
} catch (IOException e) {
return Mono.error(new GenericException("Failed to create mod file directory", e));
}

return Mono.just(subdir);
})
.subscribeOn(Schedulers.boundedElastic());
}

private static List<String> mapFilePathsFromEntries(CurseForgeFilesManifest oldManifest) {
return
oldManifest != null ?
Expand Down
Loading

0 comments on commit f6ac245

Please sign in to comment.