Feature: 下载并发控制 (#5026)

This commit is contained in:
Burning_TNT
2026-01-11 21:45:31 +08:00
committed by GitHub
parent 74d0a89820
commit 3860e68caa
5 changed files with 181 additions and 96 deletions

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting; package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -114,6 +115,8 @@ public final class ProxyManager {
config().hasProxyAuthProperty().addListener(updateAuthenticator); config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator); config().proxyUserProperty().addListener(updateAuthenticator);
config().proxyPassProperty().addListener(updateAuthenticator); config().proxyPassProperty().addListener(updateAuthenticator);
FetchTask.notifyInitialized();
} }
private static abstract class AbstractProxySelector extends ProxySelector { private static abstract class AbstractProxySelector extends ProxySelector {

View File

@@ -1217,12 +1217,14 @@ public final class FXUtils {
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(url) return new CacheFileTask(url)
.setSignificance(Task.TaskSignificance.MINOR)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth)) .thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
.setSignificance(Task.TaskSignificance.MINOR); .setSignificance(Task.TaskSignificance.MINOR);
} }
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(uri) return new CacheFileTask(uri)
.setSignificance(Task.TaskSignificance.MINOR)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth)) .thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
.setSignificance(Task.TaskSignificance.MINOR); .setSignificance(Task.TaskSignificance.MINOR);
} }
@@ -1237,6 +1239,7 @@ public final class FXUtils {
LOG.warning("An exception encountered while loading remote image: " + url, exception); LOG.warning("An exception encountered while loading remote image: " + url, exception);
} }
}) })
.setSignificance(Task.TaskSignificance.MINOR)
.start(); .start();
return image; return image;
} }

View File

@@ -35,7 +35,15 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -46,6 +54,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
private static final String PREFIX = "https://api.curseforge.com"; private static final String PREFIX = "https://api.curseforge.com";
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", "")); private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
private static final Semaphore SEMAPHORE = new Semaphore(16);
private static final int WORD_PERFECT_MATCH_WEIGHT = 5; private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
@@ -110,6 +119,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
@Override @Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
int categoryId = 0; int categoryId = 0;
if (category != null && category.getSelf() instanceof CurseAddon.Category) { if (category != null && category.getSelf() instanceof CurseAddon.Category) {
categoryId = ((CurseAddon.Category) category.getSelf()).getId(); categoryId = ((CurseAddon.Category) category.getSelf()).getId();
@@ -150,6 +161,9 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return pair(remoteMod, diff); return pair(remoteMod, diff);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
@@ -173,6 +187,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return Optional.empty(); return Optional.empty();
} }
SEMAPHORE.acquireUninterruptibly();
try {
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432")) Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
.json(mapOf(pair("fingerprints", Collections.singletonList(hash)))) .json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
.getJson(Response.typeOf(FingerprintMatchesResult.class)); .getJson(Response.typeOf(FingerprintMatchesResult.class));
@@ -182,39 +198,58 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
} }
return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion()); return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion());
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
public RemoteMod getModById(String id) throws IOException { public RemoteMod getModById(String id) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
.getJson(Response.typeOf(CurseAddon.class)); .getJson(Response.typeOf(CurseAddon.class));
return response.data.toMod(); return response.data.toMod();
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException { public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))) Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId)))
.getJson(Response.typeOf(CurseAddon.LatestFile.class)); .getJson(Response.typeOf(CurseAddon.LatestFile.class));
return response.getData().toVersion().getFile(); return response.getData().toVersion().getFile();
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
pair("pageSize", "10000"))) pair("pageSize", "10000")))
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class))); .getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
return response.getData().stream().map(CurseAddon.LatestFile::toVersion); return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
} finally {
SEMAPHORE.release();
} }
public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
return reorganizeCategories(categories.getData(), section);
} }
@Override @Override
public Stream<RemoteModRepository.Category> getCategories() throws IOException { public Stream<RemoteModRepository.Category> getCategories() throws IOException {
return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory); SEMAPHORE.acquireUninterruptibly();
try {
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
return reorganizeCategories(categories.getData(), section).stream().map(CurseAddon.Category::toCategory);
} finally {
SEMAPHORE.release();
}
} }
private List<CurseAddon.Category> reorganizeCategories(List<CurseAddon.Category> categories, int rootId) { private List<CurseAddon.Category> reorganizeCategories(List<CurseAddon.Category> categories, int rootId) {

View File

@@ -24,7 +24,11 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -35,7 +39,14 @@ import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -49,6 +60,8 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack"); public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack");
public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader"); public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader");
private static final Semaphore SEMAPHORE = new Semaphore(16);
private static final String PREFIX = "https://api.modrinth.com"; private static final String PREFIX = "https://api.modrinth.com";
private final String projectType; private final String projectType;
@@ -81,6 +94,8 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override @Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
List<List<String>> facets = new ArrayList<>(); List<List<String>> facets = new ArrayList<>();
facets.add(Collections.singletonList("project_type:" + projectType)); facets.add(Collections.singletonList("project_type:" + projectType));
if (StringUtils.isNotBlank(gameVersion)) { if (StringUtils.isNotBlank(gameVersion)) {
@@ -99,12 +114,16 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
Response<ProjectSearchResult> response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query))) Response<ProjectSearchResult> response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query)))
.getJson(Response.typeOf(ProjectSearchResult.class)); .getJson(Response.typeOf(ProjectSearchResult.class));
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize)); return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
String sha1 = DigestUtils.digestToString("SHA-1", file); String sha1 = DigestUtils.digestToString("SHA-1", file);
SEMAPHORE.acquireUninterruptibly();
try { try {
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1, ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
pair("algorithm", "sha1")) pair("algorithm", "sha1"))
@@ -118,14 +137,21 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
} }
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
return Optional.empty(); return Optional.empty();
} finally {
SEMAPHORE.release();
} }
} }
@Override @Override
public RemoteMod getModById(String id) throws IOException { public RemoteMod getModById(String id) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
id = StringUtils.removePrefix(id, "local-"); id = StringUtils.removePrefix(id, "local-");
Project project = HttpRequest.GET(PREFIX + "/v2/project/" + id).getJson(Project.class); Project project = HttpRequest.GET(PREFIX + "/v2/project/" + id).getJson(Project.class);
return project.toMod(); return project.toMod();
} finally {
SEMAPHORE.release();
}
} }
@Override @Override
@@ -135,20 +161,28 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
id = StringUtils.removePrefix(id, "local-"); id = StringUtils.removePrefix(id, "local-");
List<ProjectVersion> versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version") List<ProjectVersion> versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version")
.getJson(listTypeOf(ProjectVersion.class)); .getJson(listTypeOf(ProjectVersion.class));
return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream);
} finally {
SEMAPHORE.release();
} }
public List<Category> getCategoriesImpl() throws IOException {
List<Category> categories = HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(listTypeOf(Category.class));
return categories.stream().filter(category -> category.getProjectType().equals(projectType)).collect(Collectors.toList());
} }
@Override @Override
public Stream<RemoteModRepository.Category> getCategories() throws IOException { public Stream<RemoteModRepository.Category> getCategories() throws IOException {
return getCategoriesImpl().stream().map(Category::toCategory); SEMAPHORE.acquireUninterruptibly();
try {
List<Category> categories = HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(listTypeOf(Category.class));
return categories.stream()
.filter(category -> category.getProjectType().equals(projectType))
.map(Category::toCategory);
} finally {
SEMAPHORE.release();
}
} }
public static class Category { public static class Category {
@@ -160,7 +194,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
private final String projectType; private final String projectType;
public Category() { public Category() {
this("","",""); this("", "", "");
} }
public Category(String icon, String name, String projectType) { public Category(String icon, String name, String projectType) {

View File

@@ -509,12 +509,22 @@ public abstract class FetchTask<T> extends Task<T> {
return downloadExecutorConcurrency; return downloadExecutorConcurrency;
} }
private static volatile boolean initialized = false;
public static void notifyInitialized() {
initialized = true;
}
/// Ensure that [#HTTP_CLIENT] is initialized after ProxyManager has been initialized. /// Ensure that [#HTTP_CLIENT] is initialized after ProxyManager has been initialized.
private static final class Holder { private static final class Holder {
private static final HttpClient HTTP_CLIENT; private static final HttpClient HTTP_CLIENT;
private static final String USER_AGENT = System.getProperty("http.agent", "HMCL"); private static final String USER_AGENT = System.getProperty("http.agent", "HMCL");
static { static {
if (!initialized) {
throw new AssertionError("FetchTask.Holder accessed before ProxyManager initialization.");
}
boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2")); boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2"));
HTTP_CLIENT = HttpClient.newBuilder() HTTP_CLIENT = HttpClient.newBuilder()