Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 54 additions & 7 deletions src/main/java/i18nupdatemod/core/I18nConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
* <a href="https://github.com/CFPAOrg/Minecraft-Mod-Language-Package">CFPAOrg/Minecraft-Mod-Language-Package</a>
Expand Down Expand Up @@ -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<GameAssetDetail.AssetDownloadDetail> 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<GameAssetDetail.AssetDownloadDetail> createDownloadDetailsFromGit(GameMetaData convert, String loader) {
try {
Map<String, String> 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);
}
}
}
105 changes: 104 additions & 1 deletion src/main/java/i18nupdatemod/util/AssetUtil.java
Original file line number Diff line number Diff line change
@@ -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<String> 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(),
Expand All @@ -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<String> urls = new ArrayList<>(MIRRORS);
urls.add(CFPA_ASSET_ROOT);

ExecutorService executor = Executors.newFixedThreadPool(Math.max(urls.size(), 10));
try {
List<CompletableFuture<String>> futures = new ArrayList<>();
for (String url : urls) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
return testUrlConnection(url);
} catch (IOException e) {
return null; // 表示失败
}
}, executor);
futures.add(future);
}

// 阻塞等待最快完成且成功的任务
String fastest = null;
while (!futures.isEmpty()) {
CompletableFuture<Object> first = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]));
fastest = (String) first.join();

// 移除已完成的 future
futures.removeIf(CompletableFuture::isDone);

if (fastest != null) {
// 成功,取消其他任务
for (CompletableFuture<String> 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<String, String> 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<Map<String, String>>() {
}.getType();
return new Gson().fromJson(reader, mapType);
} finally {
httpConn.disconnect();
}
} catch (Exception ignore) {
return new HashMap<>();
}
}
}