diff --git a/src/main/java/i18nupdatemod/core/I18nConfig.java b/src/main/java/i18nupdatemod/core/I18nConfig.java index fab2c2e..b0169c7 100644 --- a/src/main/java/i18nupdatemod/core/I18nConfig.java +++ b/src/main/java/i18nupdatemod/core/I18nConfig.java @@ -12,8 +12,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import static i18nupdatemod.util.AssetUtil.getFastestUrl; +import static i18nupdatemod.util.AssetUtil.getGitIndex; + public class I18nConfig { /** * CFPAOrg/Minecraft-Mod-Language-Package @@ -58,17 +62,60 @@ public static GameAssetDetail getAssetDetail(String minecraftVersion, String loa GameMetaData convert = getGameMetaData(minecraftVersion); GameAssetDetail ret = new GameAssetDetail(); - ret.downloads = convert.convertFrom.stream().map(it -> getAssetMetaData(it, loader)).map(it -> { + String assetRoot = getFastestUrl(); + Log.debug("Using asset root: " + assetRoot); + + if (assetRoot.equals("https://raw.githubusercontent.com/")) { + ret.downloads = createDownloadDetailsFromGit(convert, loader); + } else { + ret.downloads = createDownloadDetails(convert, loader, assetRoot); + } + + ret.covertPackFormat = convert.packFormat; + ret.covertFileName = + String.format("Minecraft-Mod-Language-Modpack-Converted-%s.zip", minecraftVersion); + return ret; + } + + private static List createDownloadDetails(GameMetaData convert, String loader, String assetRoot) { + return convert.convertFrom.stream().map(it -> getAssetMetaData(it, loader)).map(it -> { GameAssetDetail.AssetDownloadDetail adi = new GameAssetDetail.AssetDownloadDetail(); adi.fileName = it.filename; - adi.fileUrl = CFPA_ASSET_ROOT + it.filename; - adi.md5Url = CFPA_ASSET_ROOT + it.md5Filename; + adi.fileUrl = assetRoot + it.filename; + adi.md5Url = assetRoot + it.md5Filename; adi.targetVersion = it.targetVersion; return adi; }).collect(Collectors.toList()); - ret.covertPackFormat = convert.packFormat; - ret.covertFileName = - String.format("Minecraft-Mod-Language-Modpack-Converted-%s.zip", minecraftVersion); - return ret; + } + + private static List createDownloadDetailsFromGit(GameMetaData convert, String loader) { + try { + Map index = getGitIndex(); + String releaseTag; + String version = convert.convertFrom.get(0); + + if (loader.toLowerCase().contains("fabric")) { + releaseTag = index.get(version + "-fabric"); + } else { + releaseTag = index.get(version); + } + if (releaseTag == null) { + Log.debug("Error getting index: " + version + "-" + loader); + Log.debug(index.toString()); + throw new Exception(); + } + String assetRoot = "https://github.com/CFPAOrg/Minecraft-Mod-Language-Package/releases/download/" + releaseTag + "/"; + + return convert.convertFrom.stream().map(it -> getAssetMetaData(it, loader)).map(it -> { + GameAssetDetail.AssetDownloadDetail adi = new GameAssetDetail.AssetDownloadDetail(); + adi.fileName = it.filename; + adi.fileUrl = assetRoot + it.filename; + adi.md5Url = assetRoot + it.md5Filename; + adi.targetVersion = it.targetVersion; + return adi; + }).collect(Collectors.toList()); + } catch (Exception ignore) { + return createDownloadDetails(convert, loader, CFPA_ASSET_ROOT); + } } } diff --git a/src/main/java/i18nupdatemod/util/AssetUtil.java b/src/main/java/i18nupdatemod/util/AssetUtil.java index 713ea10..d75c840 100644 --- a/src/main/java/i18nupdatemod/util/AssetUtil.java +++ b/src/main/java/i18nupdatemod/util/AssetUtil.java @@ -1,16 +1,38 @@ package i18nupdatemod.util; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; public class AssetUtil { + private static final String CFPA_ASSET_ROOT = "http://downloader1.meitangdehulu.com:22943/"; + private static final List MIRRORS; + + static { + // 镜像地址可以改成服务器下发 + MIRRORS = new ArrayList<>(); + MIRRORS.add("https://raw.githubusercontent.com/"); + // 此镜像源维护者:502y + MIRRORS.add("http://8.137.167.65:64684/"); + } + public static void download(String url, Path localFile) throws IOException, URISyntaxException { Log.info("Downloading: %s -> %s", url, localFile); FileUtils.copyURLToFile(new URI(url).toURL(), localFile.toFile(), @@ -21,4 +43,85 @@ public static void download(String url, Path localFile) throws IOException, URIS public static String getString(String url) throws IOException, URISyntaxException { return IOUtils.toString(new URI(url).toURL(), StandardCharsets.UTF_8); } + + public static String getFastestUrl() { + List urls = new ArrayList<>(MIRRORS); + urls.add(CFPA_ASSET_ROOT); + + ExecutorService executor = Executors.newFixedThreadPool(Math.max(urls.size(), 10)); + try { + List> futures = new ArrayList<>(); + for (String url : urls) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + return testUrlConnection(url); + } catch (IOException e) { + return null; // 表示失败 + } + }, executor); + futures.add(future); + } + + // 阻塞等待最快完成且成功的任务 + String fastest = null; + while (!futures.isEmpty()) { + CompletableFuture first = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0])); + fastest = (String) first.join(); + + // 移除已完成的 future + futures.removeIf(CompletableFuture::isDone); + + if (fastest != null) { + // 成功,取消其他任务 + for (CompletableFuture f : futures) { + f.cancel(true); + } + Log.info("Using fastest url: %s", fastest); + return fastest; + } + } + + // 全部失败,返回默认 URL + Log.info("All urls are unreachable, using CFPA_ASSET_ROOT"); + return CFPA_ASSET_ROOT; + + } finally { + executor.shutdownNow(); + } + } + + private static String testUrlConnection(String url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("HEAD"); + conn.setConnectTimeout(3000); + conn.setReadTimeout(5000); + conn.connect(); + int code = conn.getResponseCode(); + if (code >= 200 && code < 300) { + return url; + } + Log.debug("URL unreachable: %s, code: %d", url, code); + throw new IOException("URL unreachable: " + url); + } + + @NotNull + public static Map getGitIndex() { + try { + URL index_url = new URL("https://raw.githubusercontent.com/CFPAOrg/Minecraft-Mod-Language-Package/refs/heads/index/version-index.json"); + HttpURLConnection httpConn = (HttpURLConnection) index_url.openConnection(); + httpConn.setRequestMethod("GET"); + httpConn.setConnectTimeout(5000); + httpConn.setReadTimeout(5000); + + try (InputStreamReader reader = new InputStreamReader(httpConn.getInputStream())) { + Type mapType = new TypeToken>() { + }.getType(); + return new Gson().fromJson(reader, mapType); + } finally { + httpConn.disconnect(); + } + } catch (Exception ignore) { + return new HashMap<>(); + } + } }