mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2026-10-16 21:21:33 +08:00
Feat/curse retry and cache (#6595)
Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
+28
-3
@@ -56,6 +56,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
private static final String PREFIX = "https://api.curseforge.com";
|
||||
private static final String BASE = "https://www.curseforge.com";
|
||||
private static final Semaphore SEMAPHORE = new Semaphore(16);
|
||||
private static final int DEFAULT_RETRY_COUNT = 3;
|
||||
|
||||
public static final String API_KEY = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
|
||||
|
||||
@@ -126,8 +127,8 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
/// Searches the configured section, preserving the order returned by the server.
|
||||
///
|
||||
/// @throws UnsupportedOperationException if this repository has no configured section
|
||||
/// @throws NoCandidatesException if no response is obtained and no I/O failure was recorded
|
||||
/// @throws IOException if all candidate requests fail with I/O errors
|
||||
/// @throws NoCandidatesException if no response is obtained and no I/O failure was recorded
|
||||
/// @throws IOException if all candidate requests fail with I/O errors
|
||||
@Override
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteAddonRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
if (type == null) throw new UnsupportedOperationException();
|
||||
@@ -158,6 +159,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
LOG.info("Fetching " + candidate);
|
||||
try {
|
||||
response = withApiKey(HttpRequest.GET(candidate.toString()))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
@@ -249,6 +251,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
try {
|
||||
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
|
||||
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(FingerprintMatchesResult.class));
|
||||
|
||||
if (response.data().exactMatches() == null || response.data().exactMatches().isEmpty()) {
|
||||
@@ -266,6 +269,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
return response.data.toAddon();
|
||||
} finally {
|
||||
@@ -291,6 +295,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, projectId, fileId)))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.LatestFile.class));
|
||||
return response.data().toVersion().file();
|
||||
} finally {
|
||||
@@ -304,6 +309,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
try {
|
||||
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000")))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
|
||||
return response.data().stream().map(CurseAddon.LatestFile::toVersion);
|
||||
} finally {
|
||||
@@ -316,6 +322,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<String> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, addonId, versionId)))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(String.class));
|
||||
return response.data();
|
||||
} finally {
|
||||
@@ -328,6 +335,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + version.projectId()))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
var addon = response.data();
|
||||
var classId = addon.classId();
|
||||
@@ -356,6 +364,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<List<Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(Category.class)));
|
||||
return reorganizeCategories(categories.data(), section).stream().map(Category::toCategory);
|
||||
} finally {
|
||||
@@ -574,6 +583,22 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
default -> RemoteAddon.VersionType.Release;
|
||||
};
|
||||
|
||||
Map<String, String> knownHashes;
|
||||
if (hashes != null) {
|
||||
knownHashes = new HashMap<>();
|
||||
for (LatestFileHash hash : hashes) {
|
||||
if (hash.value != null) {
|
||||
switch (hash.algo) {
|
||||
case 1 -> knownHashes.put("sha1", hash.value);
|
||||
case 2 -> knownHashes.put("md5", hash.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
knownHashes = Map.copyOf(knownHashes);
|
||||
} else {
|
||||
knownHashes = Map.of();
|
||||
}
|
||||
|
||||
return new RemoteAddon.Version(
|
||||
this,
|
||||
Integer.toString(id()),
|
||||
@@ -582,7 +607,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
fileName(),
|
||||
fileDate(),
|
||||
versionType,
|
||||
new RemoteAddon.File(Collections.emptyMap(), downloadUrl(), fileName()),
|
||||
new RemoteAddon.File(knownHashes, downloadUrl(), fileName()),
|
||||
dependencies.stream().map(dependency -> {
|
||||
if (!RELATION_TYPE.containsKey(dependency.relationType())) {
|
||||
throw new IllegalStateException("Broken datas.");
|
||||
|
||||
@@ -134,7 +134,10 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
if (StringUtils.isBlank(file.fileName()) || file.url() == null) {
|
||||
try {
|
||||
RemoteAddon.File remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID()));
|
||||
return file.withFileName(remoteFile.filename()).withURL(remoteFile.url());
|
||||
return file
|
||||
.withFileName(remoteFile.filename())
|
||||
.withURL(remoteFile.url())
|
||||
.withHashes(remoteFile.hashes());
|
||||
} catch (FileNotFoundException fof) {
|
||||
LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof);
|
||||
notFound.set(true);
|
||||
@@ -165,7 +168,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var task = new FileDownloadTask(f.url(), path);
|
||||
var task = new FileDownloadTask(f.url(), path, f.getIntegrityCheck());
|
||||
task.setCacheRepository(dependency.getCacheRepository());
|
||||
task.setCaching(true);
|
||||
return Stream.of(task.withCounter("hmcl.modpack.download"));
|
||||
|
||||
@@ -19,19 +19,33 @@ package org.jackhuang.hmcl.modpack.curse;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/// @author huangyuhui
|
||||
@JsonSerializable
|
||||
public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
@SerializedName("fileID") int fileID,
|
||||
@SerializedName("fileName") String fileName,
|
||||
@SerializedName("url") String url,
|
||||
@SerializedName("required") boolean required) implements Validation {
|
||||
@SerializedName("required") boolean required,
|
||||
@SerializedName("hashes") Map<String, String> hashes) implements Validation {
|
||||
|
||||
private static final List<Pair<String, String>> HASH_ALGORITHMS = List.of(
|
||||
pair("sha1", "SHA-1"),
|
||||
pair("sha256", "SHA-256"),
|
||||
pair("sha512", "SHA-512"),
|
||||
pair("md5", "MD5")
|
||||
);
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
@@ -51,12 +65,29 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
|
||||
if (hashes == null || hashes.isEmpty()) return null;
|
||||
|
||||
for (Pair<String, String> algorithm : HASH_ALGORITHMS) {
|
||||
String hash = hashes.get(algorithm.key());
|
||||
if (hash != null) {
|
||||
return new FileDownloadTask.IntegrityCheck(algorithm.value(), hash);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public CurseManifestFile withFileName(String fileName) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
}
|
||||
|
||||
public CurseManifestFile withURL(String url) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
}
|
||||
|
||||
public CurseManifestFile withHashes(Map<String, String> hashes) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user