重构下载源选择逻辑 (#4856)

This commit is contained in:
Glavo
2025-11-30 15:27:33 +08:00
committed by GitHub
parent 310a344f96
commit 8351ee4094
19 changed files with 214 additions and 345 deletions

View File

@@ -1,92 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download;
import org.jetbrains.annotations.Unmodifiable;
import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;
/**
* The download provider that changes the real download source in need.
*
* @author huangyuhui
*/
public class AdaptedDownloadProvider implements DownloadProvider {
private @Unmodifiable List<DownloadProvider> downloadProviderCandidates;
public void setDownloadProviderCandidates(List<DownloadProvider> downloadProviderCandidates) {
this.downloadProviderCandidates = List.copyOf(downloadProviderCandidates);
}
public DownloadProvider getPreferredDownloadProvider() {
List<DownloadProvider> d = downloadProviderCandidates;
if (d == null || d.isEmpty()) {
throw new IllegalStateException("No download provider candidate");
}
return d.get(0);
}
@Override
public String getVersionListURL() {
return getPreferredDownloadProvider().getVersionListURL();
}
@Override
public String getAssetBaseURL() {
return getPreferredDownloadProvider().getAssetBaseURL();
}
@Override
public String injectURL(String baseURL) {
return getPreferredDownloadProvider().injectURL(baseURL);
}
@Override
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.getAssetObjectCandidates(assetObjectLocation).stream())
.collect(Collectors.toList());
}
@Override
public List<URI> injectURLWithCandidates(String baseURL) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLWithCandidates(baseURL).stream())
.collect(Collectors.toList());
}
@Override
public List<URI> injectURLsWithCandidates(List<String> urls) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLsWithCandidates(urls).stream())
.collect(Collectors.toList());
}
@Override
public VersionList<?> getVersionListById(String id) {
return getPreferredDownloadProvider().getVersionListById(id);
}
@Override
public int getConcurrency() {
return getPreferredDownloadProvider().getConcurrency();
}
}

View File

@@ -18,60 +18,99 @@
package org.jackhuang.hmcl.download;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
/**
* Official Download Provider fetches version list from Mojang and
* download files from mcbbs.
*
* @author huangyuhui
*/
public class AutoDownloadProvider implements DownloadProvider {
private final DownloadProvider versionListProvider;
private final DownloadProvider fileProvider;
/// @author huangyuhui
public final class AutoDownloadProvider implements DownloadProvider {
private final List<DownloadProvider> versionListProviders;
private final List<DownloadProvider> fileProviders;
private final ConcurrentMap<String, VersionList<?>> versionLists = new ConcurrentHashMap<>();
public AutoDownloadProvider(DownloadProvider versionListProvider, DownloadProvider fileProvider) {
this.versionListProvider = versionListProvider;
this.fileProvider = fileProvider;
public AutoDownloadProvider(
List<DownloadProvider> versionListProviders,
List<DownloadProvider> fileProviders) {
if (versionListProviders == null || versionListProviders.isEmpty()) {
throw new IllegalArgumentException("versionListProviders must not be null or empty");
}
if (fileProviders == null || fileProviders.isEmpty()) {
throw new IllegalArgumentException("fileProviders must not be null or empty");
}
this.versionListProviders = versionListProviders;
this.fileProviders = fileProviders;
}
public AutoDownloadProvider(DownloadProvider... downloadProviderCandidate) {
if (downloadProviderCandidate.length == 0) {
throw new IllegalArgumentException("Download provider must have at least one download provider");
}
this.versionListProviders = List.of(downloadProviderCandidate);
this.fileProviders = versionListProviders;
}
private DownloadProvider getPreferredDownloadProvider() {
return fileProviders.get(0);
}
private static List<URI> getAll(
List<DownloadProvider> providers,
Function<DownloadProvider, List<URI>> function) {
LinkedHashSet<URI> result = new LinkedHashSet<>();
for (DownloadProvider provider : providers) {
result.addAll(function.apply(provider));
}
return List.copyOf(result);
}
@Override
public String getVersionListURL() {
return versionListProvider.getVersionListURL();
}
@Override
public String getAssetBaseURL() {
return fileProvider.getAssetBaseURL();
public List<URI> getVersionListURLs() {
return getAll(versionListProviders, DownloadProvider::getVersionListURLs);
}
@Override
public String injectURL(String baseURL) {
return fileProvider.injectURL(baseURL);
return getPreferredDownloadProvider().injectURL(baseURL);
}
@Override
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return fileProvider.getAssetObjectCandidates(assetObjectLocation);
return getAll(fileProviders, provider -> provider.getAssetObjectCandidates(assetObjectLocation));
}
@Override
public List<URI> injectURLWithCandidates(String baseURL) {
return fileProvider.injectURLWithCandidates(baseURL);
return getAll(fileProviders, provider -> provider.injectURLWithCandidates(baseURL));
}
@Override
public List<URI> injectURLsWithCandidates(List<String> urls) {
return fileProvider.injectURLsWithCandidates(urls);
return getAll(fileProviders, provider -> provider.injectURLsWithCandidates(urls));
}
@Override
public VersionList<?> getVersionListById(String id) {
return versionListProvider.getVersionListById(id);
return versionLists.computeIfAbsent(id, value -> {
VersionList<?>[] lists = new VersionList<?>[versionListProviders.size()];
for (int i = 0; i < versionListProviders.size(); i++) {
lists[i] = versionListProviders.get(i).getVersionListById(value);
}
return new MultipleSourceVersionList(lists);
});
}
@Override
public int getConcurrency() {
return fileProvider.getConcurrency();
return getPreferredDownloadProvider().getConcurrency();
}
@Override
public String toString() {
return "AutoDownloadProvider[versionListProviders=%s, fileProviders=%s]".formatted(versionListProviders, fileProviders);
}
}

View File

@@ -28,7 +28,9 @@ import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
import org.jackhuang.hmcl.download.quilt.QuiltAPIVersionList;
import org.jackhuang.hmcl.download.quilt.QuiltVersionList;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
@@ -99,13 +101,13 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
}
@Override
public String getVersionListURL() {
return apiRoot + "/mc/game/version_manifest.json";
public List<URI> getVersionListURLs() {
return List.of(URI.create(apiRoot + "/mc/game/version_manifest.json"));
}
@Override
public String getAssetBaseURL() {
return apiRoot + "/assets/";
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return List.of(NetworkUtils.toURI(apiRoot + "/assets/" + assetObjectLocation));
}
@Override

View File

@@ -1,67 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download;
import java.util.HashMap;
import java.util.Map;
/**
* Official Download Provider fetches version list from Mojang and
* download files from mcbbs.
*
* @author huangyuhui
*/
public final class BalancedDownloadProvider implements DownloadProvider {
private final DownloadProvider[] candidates;
private final Map<String, VersionList<?>> versionLists = new HashMap<>();
public BalancedDownloadProvider(DownloadProvider... candidates) {
this.candidates = candidates;
}
@Override
public String getVersionListURL() {
throw new UnsupportedOperationException();
}
@Override
public String getAssetBaseURL() {
throw new UnsupportedOperationException();
}
@Override
public String injectURL(String baseURL) {
throw new UnsupportedOperationException();
}
@Override
public VersionList<?> getVersionListById(String id) {
return versionLists.computeIfAbsent(id, value -> {
VersionList<?>[] lists = new VersionList<?>[candidates.length];
for (int i = 0; i < candidates.length; i++) {
lists[i] = candidates[i].getVersionListById(value);
}
return new MultipleSourceVersionList(lists);
});
}
@Override
public int getConcurrency() {
throw new UnsupportedOperationException();
}
}

View File

@@ -20,64 +20,55 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
/**
* The service provider that provides Minecraft online file downloads.
*
* @author huangyuhui
*/
/// The service provider that provides Minecraft online file downloads.
///
/// @author huangyuhui
public interface DownloadProvider {
String getVersionListURL();
List<URI> getVersionListURLs();
String getAssetBaseURL();
List<URI> getAssetObjectCandidates(String assetObjectLocation);
default List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return List.of(NetworkUtils.toURI(getAssetBaseURL() + assetObjectLocation));
}
/**
* Inject into original URL provided by Mojang and Forge.
*
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
* this method provides a way to change them.
*
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
/// Inject into original URL provided by Mojang and Forge.
///
/// Since there are many provided URLs that are written in JSONs and are unmodifiable,
/// this method provides a way to change them.
///
/// @param baseURL original URL provided by Mojang and Forge.
/// @return the URL that is equivalent to `baseURL``, but belongs to your own service provider.
String injectURL(String baseURL);
/**
* Inject into original URL provided by Mojang and Forge.
*
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
* this method provides a way to change them.
*
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
/// Inject into original URL provided by Mojang and Forge.
///
/// Since there are many provided URLs that are written in JSONs and are unmodifiable,
/// this method provides a way to change them.
///
/// @param baseURL original URL provided by Mojang and Forge.
/// @return the URL that is equivalent to `baseURL`, but belongs to your own service provider.
default List<URI> injectURLWithCandidates(String baseURL) {
return List.of(NetworkUtils.toURI(injectURL(baseURL)));
}
default List<URI> injectURLsWithCandidates(List<String> urls) {
return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList());
LinkedHashSet<URI> result = new LinkedHashSet<>();
for (String url : urls) {
result.addAll(injectURLWithCandidates(url));
}
return List.copyOf(result);
}
/**
* the specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
*
* @param id the id of specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
* @return the version list
* @throws IllegalArgumentException if the version list does not exist
*/
/// the specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
///
/// @param id the id of specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
/// @return the version list
/// @throws IllegalArgumentException if the version list does not exist
VersionList<?> getVersionListById(String id);
/**
* The maximum download concurrency that this download provider supports.
* @return the maximum download concurrency.
*/
/// The maximum download concurrency that this download provider supports.
///
/// @return the maximum download concurrency.
int getConcurrency();
}

View File

@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.task.Task;
import java.net.URI;
import java.util.List;
import java.util.Objects;
@@ -26,7 +28,7 @@ import java.util.Objects;
*/
public final class DownloadProviderWrapper implements DownloadProvider {
private DownloadProvider provider;
private volatile DownloadProvider provider;
public DownloadProviderWrapper(DownloadProvider provider) {
this.provider = provider;
@@ -46,13 +48,8 @@ public final class DownloadProviderWrapper implements DownloadProvider {
}
@Override
public String getVersionListURL() {
return getProvider().getVersionListURL();
}
@Override
public String getAssetBaseURL() {
return getProvider().getAssetBaseURL();
public List<URI> getVersionListURLs() {
return getProvider().getVersionListURLs();
}
@Override
@@ -72,11 +69,41 @@ public final class DownloadProviderWrapper implements DownloadProvider {
@Override
public VersionList<?> getVersionListById(String id) {
return getProvider().getVersionListById(id);
return new VersionList<>() {
@Override
public boolean hasType() {
return getProvider().getVersionListById(id).hasType();
}
@Override
public Task<?> refreshAsync() {
throw new UnsupportedOperationException();
}
@Override
public Task<?> refreshAsync(String gameVersion) {
return getProvider().getVersionListById(id).refreshAsync(gameVersion)
.thenComposeAsync(() -> {
lock.writeLock().lock();
try {
versions.putAll(gameVersion, getProvider().getVersionListById(id).getVersions(gameVersion));
} finally {
lock.writeLock().unlock();
}
return null;
});
}
};
}
@Override
public int getConcurrency() {
return getProvider().getConcurrency();
}
@Override
public String toString() {
return "DownloadProviderWrapper[provider=%s]".formatted(provider);
}
}

View File

@@ -27,6 +27,10 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeOfficialVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
import org.jackhuang.hmcl.download.quilt.QuiltAPIVersionList;
import org.jackhuang.hmcl.download.quilt.QuiltVersionList;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI;
import java.util.List;
/**
* @author huangyuhui
@@ -61,13 +65,13 @@ public class MojangDownloadProvider implements DownloadProvider {
}
@Override
public String getVersionListURL() {
return "https://piston-meta.mojang.com/mc/game/version_manifest.json";
public List<URI> getVersionListURLs() {
return List.of(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest.json"));
}
@Override
public String getAssetBaseURL() {
return "https://resources.download.minecraft.net/";
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return List.of(NetworkUtils.toURI("https://resources.download.minecraft.net/" + assetObjectLocation));
}
@Override

View File

@@ -40,11 +40,6 @@ public class MultipleSourceVersionList extends VersionList<RemoteVersion> {
return hasType;
}
@Override
public Task<?> loadAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
}
@Override
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");

View File

@@ -71,17 +71,6 @@ public abstract class VersionList<T extends RemoteVersion> {
return refreshAsync();
}
public Task<?> loadAsync() {
return Task.composeAsync(() -> {
lock.readLock().lock();
try {
return isLoaded() ? null : refreshAsync();
} finally {
lock.readLock().unlock();
}
});
}
public Task<?> loadAsync(String gameVersion) {
return Task.composeAsync(() -> {
lock.readLock().lock();

View File

@@ -56,11 +56,6 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
return false;
}
@Override
public Task<?> loadAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
}
@Override
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");

View File

@@ -53,7 +53,7 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
@Override
public Task<?> refreshAsync() {
return new GetTask(downloadProvider.getVersionListURL()).thenGetJsonAsync(GameRemoteVersions.class)
return new GetTask(downloadProvider.getVersionListURLs()).thenGetJsonAsync(GameRemoteVersions.class)
.thenAcceptAsync(root -> {
GameRemoteVersions unlistedVersions = null;
@@ -91,4 +91,9 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
}
});
}
@Override
public String toString() {
return "GameVersionList[downloadProvider=%s]".formatted(downloadProvider);
}
}

View File

@@ -60,17 +60,19 @@ public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemot
@Override
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(NetworkUtils.withQuery(downloadProvider.injectURLWithCandidates("https://bmclapi2.bangbang93.com/liteloader/list"), Map.of("mcversion", gameVersion)))
return new GetTask(
NetworkUtils.withQuery(downloadProvider.getApiRoot() + "/liteloader/list", Map.of(
"mcversion", gameVersion
)))
.thenGetJsonAsync(LiteLoaderBMCLVersion.class)
.thenAcceptAsync(v -> {
lock.writeLock().lock();
try {
versions.clear();
versions.put(gameVersion, new LiteLoaderRemoteVersion(
gameVersion, v.version, RemoteVersion.Type.UNCATEGORIZED,
Collections.singletonList(NetworkUtils.withQuery(
downloadProvider.injectURL("https://bmclapi2.bangbang93.com/liteloader/download"),
downloadProvider.getApiRoot() + "/liteloader/download",
Collections.singletonMap("version", v.version)
)),
v.build.getTweakClass(), v.build.getLibraries()

View File

@@ -52,7 +52,7 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
@Override
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(downloadProvider.injectURL(LITELOADER_LIST))
return new GetTask(downloadProvider.injectURLWithCandidates(LITELOADER_LIST))
.thenGetJsonAsync(LiteLoaderVersionsRoot.class)
.thenAcceptAsync(root -> {
LiteLoaderGameVersions versions = root.getVersions().get(gameVersion);

View File

@@ -45,11 +45,6 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
return true;
}
@Override
public Task<?> loadAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
}
@Override
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");

View File

@@ -24,7 +24,6 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
}
private static final String OLD_URL = "https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge";
private static final String META_URL = "https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge";
@Override
@@ -38,8 +37,8 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
@Override
public Task<?> refreshAsync() {
return Task.allOf(
new GetTask(downloadProvider.injectURL(OLD_URL)).thenGetJsonAsync(OfficialAPIResult.class),
new GetTask(downloadProvider.injectURL(META_URL)).thenGetJsonAsync(OfficialAPIResult.class)
new GetTask(downloadProvider.injectURLWithCandidates(OLD_URL)).thenGetJsonAsync(OfficialAPIResult.class),
new GetTask(downloadProvider.injectURLWithCandidates(META_URL)).thenGetJsonAsync(OfficialAPIResult.class)
).thenAcceptAsync(results -> {
lock.writeLock().lock();

View File

@@ -80,7 +80,7 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
Set<String> duplicates = new HashSet<>();
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
String mirror = "https://bmclapi2.bangbang93.com/optifine/" + toLookupVersion(element.getGameVersion()) + "/" + element.getType() + "/" + element.getPatch();
String mirror = apiRoot + "/optifine/" + toLookupVersion(element.getGameVersion()) + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;