diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index fc934b1fb..8a1589817 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.game; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; @@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor protected abstract SortType getBackedRemoteModRepositorySortOrder(); @Override - public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { + public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { if (!StringUtils.containsChinese(searchFilter)) { - return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder); + return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder); } Set englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY); @@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor RemoteMod[] searchResultArray = new RemoteMod[pageSize]; int totalPages, chineseIndex = 0, englishIndex = pageSize - 1; { - SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder); + SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder); for (Iterator iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) { if (chineseIndex > englishIndex) { LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 525209e1e..2a78ed1b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -39,10 +39,12 @@ import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.*; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; +import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -90,6 +92,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); private int searchID = 0; protected RemoteModRepository repository; + private final DownloadProvider downloadProvider; private Runnable retrySearch; @@ -101,6 +104,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP this.repository = repository; this.callback = callback; this.versionSelection = versionSelection; + this.downloadProvider = DownloadProviders.getDownloadProvider(); } public ObservableList getActions() { @@ -171,7 +175,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP : ""; } }).thenApplyAsync( - gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC) + gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC) ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (searchID != currentSearchID) { return; @@ -231,6 +235,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private static class ModDownloadListPageSkin extends SkinBase { private final JFXListView listView = new JFXListView<>(); + protected ModDownloadListPageSkin(DownloadListPage control) { super(control); diff --git a/HMCL/src/main/resources/assets/about/thanks.json b/HMCL/src/main/resources/assets/about/thanks.json index 203b157e2..2782c38bf 100644 --- a/HMCL/src/main/resources/assets/about/thanks.json +++ b/HMCL/src/main/resources/assets/about/thanks.json @@ -64,6 +64,12 @@ "titleLocalized" : "about.thanks_to.mcbbs", "subtitleLocalized" : "about.thanks_to.mcbbs.statement" }, + { + "image" : "/assets/img/mcim.png", + "titleLocalized" : "about.thanks_to.mcim", + "subtitleLocalized" : "about.thanks_to.mcim.statement", + "externalLink" : "https://github.com/mcmod-info-mirror" + }, { "image" : "/assets/img/github.png", "titleLocalized" : "about.thanks_to.contributors", diff --git a/HMCL/src/main/resources/assets/img/mcim.png b/HMCL/src/main/resources/assets/img/mcim.png new file mode 100644 index 000000000..6f9a5c0ba Binary files /dev/null and b/HMCL/src/main/resources/assets/img/mcim.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 22b0f3166..45f864ef7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Contribute a lot of technical support to about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation of HMCL. about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum) about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available) +about.thanks_to.mcim=mcmod-info-mirror +about.thanks_to.mcim.statement=Provides mod information cache acceleration service about.thanks_to.mcmod=MCMod (mcmod.cn) about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods. about.thanks_to.red_lnn.statement=For providing the default background image. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index d19bb2386..99342e9fc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=為 HMCL 貢獻許多技術支援 about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件 about.thanks_to.mcbbs=MCBBS (我的世界中文論壇) about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務) +about.thanks_to.mcim=mcmod-info-mirror +about.thanks_to.mcim.statement=提供 Mod 信息緩存加速服務 about.thanks_to.mcmod=MC 百科 (mcmod.cn) about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科 about.thanks_to.red_lnn.statement=提供預設背景圖 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 75893fb05..888375b98 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=为 HMCL 贡献许多技术支持 about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档 about.thanks_to.mcbbs=MCBBS (我的世界中文论坛) about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务) +about.thanks_to.mcim=mcmod-info-mirror +about.thanks_to.mcim.statement=提供 Mod 信息缓存加速服务 about.thanks_to.mcmod=MC 百科 (mcmod.cn) about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科 about.thanks_to.red_lnn.statement=提供默认背景图 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index 432606593..496e59d5c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -78,7 +78,14 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider { pair("https://maven.fabricmc.net", apiRoot + "/maven"), pair("https://authlib-injector.yushi.moe", apiRoot + "/mirrors/authlib-injector"), pair("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"), - pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto") + pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + + // https://github.com/mcmod-info-mirror/mcim-rust-api + pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"), + pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"), + pair("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge"), + pair("https://edge.forgecdn.net", "https://mod.mcimirror.top"), + pair("https://mediafilez.forgecdn.net", "https://mod.mcimirror.top") ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 3dc48fde3..5f74ba7d4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.mod; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -83,7 +84,7 @@ public interface RemoteModRepository { } } - SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) + SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException; Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 087f8a0c1..e9cf35773 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.mod.curse; import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; @@ -26,6 +27,7 @@ import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.JarUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; @@ -47,6 +49,13 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository private static final int WORD_PERFECT_MATCH_WEIGHT = 5; + private static R withApiKey(R request) { + if (request.getUrl().startsWith(PREFIX) && !apiKey.isEmpty()) { + request.header("X-API-KEY", apiKey); + } + return request; + } + public static boolean isAvailable() { return !apiKey.isEmpty(); } @@ -100,29 +109,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository } @Override - public SearchResult search(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 { int categoryId = 0; if (category != null && category.getSelf() instanceof CurseAddon.Category) { categoryId = ((CurseAddon.Category) category.getSelf()).getId(); } - Response> response = HttpRequest.GET(PREFIX + "/v1/mods/search", - pair("gameId", "432"), - pair("classId", Integer.toString(section)), - pair("categoryId", Integer.toString(categoryId)), - pair("gameVersion", gameVersion), - pair("searchFilter", searchFilter), - pair("sortField", Integer.toString(toModsSearchSortField(sortType))), - pair("sortOrder", toSortOrder(sortOrder)), - pair("index", Integer.toString(pageOffset * pageSize)), - pair("pageSize", Integer.toString(pageSize))) - .header("X-API-KEY", apiKey) + Response> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", mapOf( + pair("gameId", "432"), + pair("classId", Integer.toString(section)), + pair("categoryId", Integer.toString(categoryId)), + pair("gameVersion", gameVersion), + pair("searchFilter", searchFilter), + pair("sortField", Integer.toString(toModsSearchSortField(sortType))), + pair("sortOrder", toSortOrder(sortOrder)), + pair("index", Integer.toString(pageOffset * pageSize)), + pair("pageSize", Integer.toString(pageSize))))))) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); } // https://github.com/HMCL-dev/HMCL/issues/1549 - String lowerCaseSearchFilter = searchFilter.toLowerCase(); + String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT); Map searchFilterWords = new HashMap<>(); for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) { searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1); @@ -162,9 +170,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); - Response response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432") + Response response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432")) .json(mapOf(pair("fingerprints", Collections.singletonList(hash)))) - .header("X-API-KEY", apiKey) .getJson(Response.typeOf(FingerprintMatchesResult.class)); if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) { @@ -176,32 +183,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository @Override public RemoteMod getModById(String id) throws IOException { - Response response = HttpRequest.GET(PREFIX + "/v1/mods/" + id) - .header("X-API-KEY", apiKey) + Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); return response.data.toMod(); } @Override public RemoteMod.File getModFile(String modId, String fileId) throws IOException { - Response response = HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId)) - .header("X-API-KEY", apiKey) + Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))) .getJson(Response.typeOf(CurseAddon.LatestFile.class)); return response.getData().toVersion().getFile(); } @Override public Stream getRemoteVersionsById(String id) throws IOException { - Response> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", - pair("pageSize", "10000")) - .header("X-API-KEY", apiKey) + Response> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", + pair("pageSize", "10000"))) .getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class))); return response.getData().stream().map(CurseAddon.LatestFile::toVersion); } public List getCategoriesImpl() throws IOException { - Response> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")) - .header("X-API-KEY", apiKey) + Response> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))) .getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class))); return reorganizeCategories(categories.getData(), section); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 24557e73f..4b60bf3cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod.modrinth; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; @@ -79,7 +80,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { } @Override - public SearchResult search(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 { List> facets = new ArrayList<>(); facets.add(Collections.singletonList("project_type:" + projectType)); if (StringUtils.isNotBlank(gameVersion)) { @@ -95,7 +96,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { pair("limit", Integer.toString(pageSize)), pair("index", convertSortType(sort)) ); - Response response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query)) + Response response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query))) .getJson(Response.typeOf(ProjectSearchResult.class)); return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java index 63201df78..a0658eea1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java @@ -38,11 +38,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class CacheFileTask extends FetchTask { public CacheFileTask(@NotNull String uri) { - super(List.of(NetworkUtils.toURI(uri))); + this(NetworkUtils.toURI(uri)); } public CacheFileTask(@NotNull URI uri) { super(List.of(uri)); + setName(uri.toString()); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java index c701ffd2d..13d6854b1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java @@ -57,6 +57,10 @@ public abstract class HttpRequest { this.method = method; } + public String getUrl() { + return url; + } + public HttpRequest accept(String contentType) { return header("Accept", contentType); }