diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java index 8c3f2a564..53a865f7c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java @@ -17,6 +17,9 @@ */ package org.jackhuang.hmcl.download; +import java.util.Arrays; +import java.util.stream.Stream; + /** * The service provider that provides Minecraft online file downloads. * @@ -39,6 +42,12 @@ public interface DownloadProvider { */ String injectURL(String baseURL); + default Stream injectURLs(String[] baseURLs) { + Stream urls = Arrays.stream(baseURLs); + Stream jsonURLs = Arrays.stream(baseURLs).map(this::injectURL); + return Stream.concat(jsonURLs, urls); + } + /** * the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine" * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java index a18dc875f..edf178507 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.versioning.VersionNumber; +import java.util.List; import java.util.Objects; /** @@ -34,7 +35,7 @@ public class RemoteVersion implements Comparable { private final String libraryId; private final String gameVersion; private final String selfVersion; - private final String url; + private final String[] url; private final Type type; /** @@ -44,8 +45,8 @@ public class RemoteVersion implements Comparable { * @param selfVersion the version string of the remote version. * @param url the installer or universal jar URL. */ - public RemoteVersion(String libraryId, String gameVersion, String selfVersion, String url) { - this(libraryId, gameVersion, selfVersion, url, Type.UNCATEGORIZED); + public RemoteVersion(String libraryId, String gameVersion, String selfVersion, String... url) { + this(libraryId, gameVersion, selfVersion, Type.UNCATEGORIZED, url); } /** @@ -55,7 +56,7 @@ public class RemoteVersion implements Comparable { * @param selfVersion the version string of the remote version. * @param url the installer or universal jar URL. */ - public RemoteVersion(String libraryId, String gameVersion, String selfVersion, String url, Type type) { + public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Type type, String... url) { this.libraryId = Objects.requireNonNull(libraryId); this.gameVersion = Objects.requireNonNull(gameVersion); this.selfVersion = Objects.requireNonNull(selfVersion); @@ -75,7 +76,7 @@ public class RemoteVersion implements Comparable { return selfVersion; } - public String getUrl() { + public String[] getUrl() { return url; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java index 1ad1f56f8..1150cfa87 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -81,22 +82,28 @@ public final class ForgeBMCLVersionList extends VersionList for (ForgeVersion version : forgeVersions) { if (version == null) continue; - String jar = null; + List urls = new ArrayList<>(); for (ForgeVersion.File file : version.getFiles()) if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) { - jar = NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf( + String classifier = gameVersion + "-" + version.getVersion() + + (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : ""); + String fileName1 = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat(); + String fileName2 = "forge-" + classifier + "-" + gameVersion + "-" + file.getCategory() + "." + file.getFormat(); + urls.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf( pair("mcversion", version.getGameVersion()), pair("version", version.getVersion()), pair("branch", version.getBranch()), pair("category", file.getCategory()), pair("format", file.getFormat()) - )); + ))); + urls.add("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1); + urls.add("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "-" + gameVersion + "/" + fileName2); } - if (jar == null) + if (urls.isEmpty()) continue; versions.put(gameVersion, new ForgeRemoteVersion( - version.getGameVersion(), version.getVersion(), jar + version.getGameVersion(), version.getVersion(), urls.toArray(new String[0]) )); } } finally { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 186109be6..21053a408 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -33,10 +33,8 @@ import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.StringUtils.removePrefix; import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; @@ -69,7 +67,11 @@ public final class ForgeInstallTask extends Task { public void preExecute() throws Exception { installer = Files.createTempFile("forge-installer", ".jar"); - dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile()) + dependent = new FileDownloadTask( + Arrays.stream(remote.getUrl()) + .map(NetworkUtils::toURL) + .collect(Collectors.toList()), + installer.toFile(), null) .setCacheRepository(dependencyManager.getCacheRepository()) .setCaching(true); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index 197753d24..7fc2d4e32 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -31,7 +31,7 @@ public class ForgeRemoteVersion extends RemoteVersion { * @param selfVersion the version string of the remote version. * @param url the installer or universal jar URL. */ - public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) { + public ForgeRemoteVersion(String gameVersion, String selfVersion, String... url) { super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, url); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java index 8d2015b69..29acd43a8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java @@ -38,7 +38,7 @@ public final class GameRemoteVersion extends RemoteVersion { private final Date time; public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) { - super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, url, getReleaseType(type)); + super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, getReleaseType(type), url); this.type = type; this.time = time; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java index 4db5a45ac..7b4176dfa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java @@ -25,9 +25,14 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.nio.charset.StandardCharsets.UTF_8; /** * @@ -65,7 +70,9 @@ public final class VersionJsonDownloadTask extends Task { public void execute() throws IOException { RemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion) .orElseThrow(() -> new IOException("Cannot find specific version " + gameVersion + " in remote repository")); - String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl()); - dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL)).storeTo(this::setResult)); + dependencies.add(new GetTask( + dependencyManager.getDownloadProvider().injectURLs(remoteVersion.getUrl()) + .map(NetworkUtils::toURL).collect(Collectors.toList()), + UTF_8).storeTo(this::setResult)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java index 6547ed215..da8d09076 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java @@ -67,7 +67,7 @@ public final class LiteLoaderInstallTask extends Task { Library library = new Library( new Artifact("com.mumfrey", "liteloader", remote.getSelfVersion()), "http://dl.liteloader.com/versions/", - new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl())) + new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()[0])) ); setResult(new Version(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId(), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java index 0ff0f3815..2843a5ded 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java @@ -72,7 +72,7 @@ public final class OptiFineBMCLVersionList extends VersionList mirror, isPre)); + versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, mirror, isPre)); } } }; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index 87be4a3f5..0c8b5df21 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -45,11 +45,8 @@ import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.Lang.getOrDefault; @@ -91,7 +88,7 @@ public final class OptiFineInstallTask extends Task { new Artifact("optifine", "OptiFine", mavenVersion, "installer"), null, new LibrariesDownloadInfo(new LibraryDownloadInfo( "optifine/OptiFine/" + mavenVersion + "/OptiFine-" + mavenVersion + "-installer.jar", - remote.getUrl())) + remote.getUrl()[0])) ); } @@ -105,7 +102,9 @@ public final class OptiFineInstallTask extends Task { dest = Files.createTempFile("optifine-installer", ".jar"); if (installer == null) { - dependents.add(new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), dest.toFile()) + dependents.add(new FileDownloadTask( + Arrays.stream(remote.getUrl()).map(NetworkUtils::toURL).collect(Collectors.toList()), + dest.toFile(), null) .setCacheRepository(dependencyManager.getCacheRepository()) .setCaching(true)); } else { @@ -223,7 +222,7 @@ public final class OptiFineInstallTask extends Task { throw new VersionMismatchException(mcVersion, gameVersion.get()); return new OptiFineInstallTask(dependencyManager, version, - new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, () -> null, false), installer); + new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, "", false), installer); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java index e0eb95964..3589f9f33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java @@ -26,17 +26,9 @@ import org.jackhuang.hmcl.task.Task; import java.util.function.Supplier; public class OptiFineRemoteVersion extends RemoteVersion { - private final Supplier url; - public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier url, boolean snapshot) { - super(LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId(), gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE); - - this.url = url; - } - - @Override - public String getUrl() { - return url.get(); + public OptiFineRemoteVersion(String gameVersion, String selfVersion, String url, boolean snapshot) { + super(LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId(), gameVersion, selfVersion, snapshot ? Type.SNAPSHOT : Type.RELEASE, url); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java index ff0890a8d..0c85c491f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java @@ -33,6 +33,9 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.logging.Level; @@ -80,11 +83,10 @@ public class FileDownloadTask extends Task { } } - private final URL url; + private final List urls; private final File file; private final IntegrityCheck integrityCheck; private final int retry; - private final EventManager> onFailed = new EventManager<>(); private Path candidate; private boolean caching; private CacheRepository repository = CacheRepository.getInstance(); @@ -115,7 +117,7 @@ public class FileDownloadTask extends Task { * @param retry the times for retrying if downloading fails. */ public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck, int retry) { - this.url = url; + this.urls = Collections.singletonList(url); this.file = file; this.integrityCheck = integrityCheck; this.retry = retry; @@ -124,6 +126,25 @@ public class FileDownloadTask extends Task { setExecutor(Schedulers.io()); } + /** + * Constructor. + * @param urls urls of remote file, will be attempted in order. + * @param file the location that download to. + * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed + */ + public FileDownloadTask(List urls, File file, IntegrityCheck integrityCheck) { + if (urls == null || urls.isEmpty()) + throw new IllegalArgumentException("At least one URL is required"); + + this.urls = new ArrayList<>(urls); + this.file = file; + this.integrityCheck = integrityCheck; + this.retry = urls.size(); + + setName(file.getName()); + setExecutor(Schedulers.io()); + } + private void closeFiles() { if (rFile != null) try { @@ -143,14 +164,6 @@ public class FileDownloadTask extends Task { stream = null; } - public EventManager> getOnFailed() { - return onFailed; - } - - public URL getUrl() { - return url; - } - public File getFile() { return file; } @@ -172,8 +185,6 @@ public class FileDownloadTask extends Task { @Override public void execute() throws Exception { - URL currentURL = url; - boolean checkETag; // Check cache if (integrityCheck != null && caching) { @@ -182,7 +193,7 @@ public class FileDownloadTask extends Task { if (cache.isPresent()) { try { FileUtils.copyFile(cache.get().toFile(), file); - Logging.LOG.log(Level.FINER, "Successfully verified file " + file + " from " + currentURL); + Logging.LOG.log(Level.FINER, "Successfully verified file " + file + " from " + urls.get(0)); return; } catch (IOException e) { Logging.LOG.log(Level.WARNING, "Failed to copy cache files", e); @@ -192,15 +203,11 @@ public class FileDownloadTask extends Task { checkETag = true; } - Logging.LOG.log(Level.FINER, "Downloading " + currentURL + " to " + file); + Logging.LOG.log(Level.FINER, "Downloading " + urls.get(0) + " to " + file); Exception exception = null; for (int repeat = 0; repeat < retry; repeat++) { - if (repeat > 0) { - FailedEvent event = new FailedEvent<>(this, repeat, currentURL); - onFailed.fireEvent(event); - currentURL = event.getNewResult(); - } + URL url = urls.get(repeat % urls.size()); if (Thread.interrupted()) { Thread.currentThread().interrupt(); break; @@ -226,7 +233,7 @@ public class FileDownloadTask extends Task { repository.removeRemoteEntry(con); } } else if (con.getResponseCode() / 100 != 2) { - throw new ResponseCodeException(currentURL, con.getResponseCode()); + throw new ResponseCodeException(url, con.getResponseCode()); } int contentLength = con.getContentLength(); @@ -316,16 +323,14 @@ public class FileDownloadTask extends Task { if (temp != null) temp.toFile().delete(); exception = e; - - if (e instanceof ResponseCodeException && ((ResponseCodeException) e).getResponseCode() == 404) - break; + Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + repeat + 1, e); } finally { closeFiles(); } } if (exception != null) - throw new DownloadException(currentURL, exception); + throw new DownloadException(urls.get(0), exception); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java index cafa5e8ca..53df35762 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java @@ -30,6 +30,9 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.logging.Level; import static java.nio.charset.StandardCharsets.UTF_8; @@ -40,7 +43,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; */ public final class GetTask extends Task { - private final URL url; + private final List urls; private final Charset charset; private final int retry; private CacheRepository repository = CacheRepository.getInstance(); @@ -54,7 +57,7 @@ public final class GetTask extends Task { } public GetTask(URL url, Charset charset, int retry) { - this.url = url; + this.urls = Collections.singletonList(url); this.charset = charset; this.retry = retry; @@ -62,6 +65,15 @@ public final class GetTask extends Task { setExecutor(Schedulers.io()); } + public GetTask(List urls, Charset charset) { + this.urls = new ArrayList<>(urls); + this.charset = charset; + this.retry = urls.size(); + + setName(urls.get(0).toString()); + setExecutor(Schedulers.io()); + } + public GetTask setCacheRepository(CacheRepository repository) { this.repository = repository; return this; @@ -72,8 +84,7 @@ public final class GetTask extends Task { Exception exception = null; boolean checkETag = true; for (int time = 0; time < retry; ++time) { - if (time > 0) - Logging.LOG.log(Level.WARNING, "Failed to download, repeat times: " + time); + URL url = urls.get(time % urls.size()); try { updateProgress(0); HttpURLConnection conn = NetworkUtils.createConnection(url); @@ -122,10 +133,11 @@ public final class GetTask extends Task { return; } catch (IOException ex) { exception = ex; + Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + time + 1, ex); } } if (exception != null) - throw new DownloadException(url, exception); + throw new DownloadException(urls.get(0), exception); } }