支持通过 MCIM 加速模组搜索 (#4225)
This commit is contained in:
@@ -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<String> 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<RemoteMod> 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?");
|
||||
|
||||
@@ -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<Node> 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<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
HMCL/src/main/resources/assets/img/mcim.png
Normal file
BIN
HMCL/src/main/resources/assets/img/mcim.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 503 B |
@@ -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.
|
||||
|
||||
@@ -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=提供預設背景圖
|
||||
|
||||
@@ -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=提供默认背景图
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||
|
||||
@@ -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 extends HttpRequest> 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,12 +109,12 @@ 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<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||
Response<List<CurseAddon>> 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)),
|
||||
@@ -114,15 +123,14 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
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)
|
||||
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<String, Integer> 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<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432")
|
||||
Response<FingerprintMatchesResult> 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<CurseAddon> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id)
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon> 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<CurseAddon.LatestFile> response = HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon.LatestFile> 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<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.LatestFile>> 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<CurseAddon.Category> getCategoriesImpl() throws IOException {
|
||||
Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
|
||||
return reorganizeCategories(categories.getData(), section);
|
||||
}
|
||||
|
||||
@@ -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<List<String>> 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<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||
Response<ProjectSearchResult> 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));
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class CacheFileTask extends FetchTask<Path> {
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user