diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index dc936842b..ccf916908 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -41,9 +41,11 @@ val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number -> } } val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5" +val versionType = System.getenv("VERSION_TYPE") ?: "nightly" + val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: "" val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: "" -val versionType = System.getenv("VERSION_TYPE") ?: "nightly" +val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: "" version = "$versionRoot.$buildNumber" @@ -147,6 +149,7 @@ tasks.getByName("sha "Implementation-Version" to project.version, "Microsoft-Auth-Id" to microsoftAuthId, "Microsoft-Auth-Secret" to microsoftAuthSecret, + "CurseForge-Api-Key" to curseForgeApiKey, "Build-Channel" to versionType, "Class-Path" to "pack200.jar", "Add-Opens" to listOf( diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index a169a5285..de0d763f0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -96,7 +96,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres @FXML private StackPane center; - private VersionList versionList; + private final VersionList versionList; private CompletableFuture executor; public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) { @@ -144,7 +144,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres content.setTitle(remoteVersion.getSelfVersion()); if (remoteVersion.getReleaseDate() != null) { - content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate())); + content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant())); } else { content.setSubtitle(""); } 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 df0a9ab36..12fcf0249 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 @@ -173,7 +173,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } return gameVersion; }).thenApplyAsync(gameVersion -> { - return repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort); + return repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC); }).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); if (exception == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 9789cc5d0..cc15e8898 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -103,9 +103,9 @@ public class DownloadPage extends Control implements DecoratorPage { setFailed(false); Task.allOf( - Task.supplyAsync(() -> addon.getData().loadDependencies()), + Task.supplyAsync(() -> addon.getData().loadDependencies(repository)), Task.supplyAsync(() -> { - Stream versions = addon.getData().loadVersions(); + Stream versions = addon.getData().loadVersions(repository); // if (StringUtils.isNotBlank(version.getVersion())) { // Optional gameVersion = GameVersion.minecraftVersion(versionJar); // if (gameVersion.isPresent()) { @@ -284,7 +284,7 @@ public class DownloadPage extends Control implements DecoratorPage { Node title = ComponentList.createComponentListTitle(i18n("mods.dependencies")); - BooleanBinding show = Bindings.createBooleanBinding(() -> !control.dependencies.isEmpty(), control.loaded); + BooleanBinding show = Bindings.createBooleanBinding(() -> control.loaded.get() && !control.dependencies.isEmpty(), control.loaded); title.managedProperty().bind(show); title.visibleProperty().bind(show); dependencyPane.managedProperty().bind(show); @@ -385,7 +385,7 @@ public class DownloadPage extends Control implements DecoratorPage { pane.getChildren().setAll(graphicPane, content, saveAsButton); content.setTitle(dataItem.getName()); - content.setSubtitle(FORMATTER.format(dataItem.getDatePublished())); + content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant())); saveAsButton.setOnMouseClicked(e -> selfPage.saveAs(dataItem)); switch (dataItem.getVersionType()) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java index 132803e0e..3c4f03d98 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -47,13 +47,21 @@ public class ModDownloadListPage extends DownloadListPage { private class Repository implements RemoteModRepository { + private RemoteModRepository getBackedRemoteModRepository() { + if ("mods.modrinth".equals(downloadSource.get())) { + return ModrinthRemoteModRepository.INSTANCE; + } else { + return CurseForgeRemoteModRepository.MODS; + } + } + @Override public Type getType() { return Type.MOD; } @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { String newSearchFilter; if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { List mods = ModTranslations.MOD.searchMod(searchFilter); @@ -75,35 +83,32 @@ public class ModDownloadListPage extends DownloadListPage { newSearchFilter = searchFilter; } - if ("mods.modrinth".equals(downloadSource.get())) { - return ModrinthRemoteModRepository.INSTANCE.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); - } else { - return CurseForgeRemoteModRepository.MODS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); - } + return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); } @Override public Stream getCategories() throws IOException { - if ("mods.modrinth".equals(downloadSource.get())) { - return ModrinthRemoteModRepository.INSTANCE.getCategories(); - } else { - return CurseForgeRemoteModRepository.MODS.getCategories(); - } + return getBackedRemoteModRepository().getCategories(); } @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) { - throw new UnsupportedOperationException(); + public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file); } @Override - public RemoteMod getModById(String id) { - throw new UnsupportedOperationException(); + public RemoteMod getModById(String id) throws IOException { + return getBackedRemoteModRepository().getModById(id); + } + + @Override + public RemoteMod.File getModFile(String modId, String fileId) throws IOException { + return getBackedRemoteModRepository().getModFile(modId, fileId); } @Override public Stream getRemoteVersionsById(String id) throws IOException { - throw new UnsupportedOperationException(); + return getBackedRemoteModRepository().getRemoteVersionsById(id); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java index 3e2ba72cd..bab76367b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.IOUtils; +import org.jetbrains.annotations.Nullable; import java.nio.charset.StandardCharsets; import java.util.*; @@ -38,7 +39,9 @@ import static org.jackhuang.hmcl.util.Pair.pair; public final class ModTranslations { public static ModTranslations MOD = new ModTranslations("/assets/mod_data.txt"); public static ModTranslations MODPACK = new ModTranslations("/assets/modpack_data.txt"); + public static ModTranslations EMPTY = new ModTranslations(""); + @Nullable public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) { switch (type) { case MOD: @@ -46,7 +49,7 @@ public final class ModTranslations { case MODPACK: return MODPACK; default: - throw new IllegalArgumentException(); + return EMPTY; } } @@ -61,12 +64,14 @@ public final class ModTranslations { this.resourceName = resourceName; } + @Nullable public Mod getModByCurseForgeId(String id) { if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null; return curseForgeMap.get(id); } + @Nullable public Mod getModById(String id) { if (StringUtils.isBlank(id) || !loadModIdMap()) return null; @@ -96,7 +101,7 @@ public final class ModTranslations { } private boolean loadFromResource() { - if (mods != null) return true; + if (mods != null || StringUtils.isBlank(resourceName)) return true; try { String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java index 2d6d149d3..3dd7aab7d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java @@ -50,7 +50,7 @@ public class ModpackDownloadListPage extends DownloadListPage { } @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { String newSearchFilter; if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { List mods = ModTranslations.MODPACK.searchMod(searchFilter); @@ -72,7 +72,7 @@ public class ModpackDownloadListPage extends DownloadListPage { newSearchFilter = searchFilter; } - return CurseForgeRemoteModRepository.MODPACKS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); + return CurseForgeRemoteModRepository.MODPACKS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); } @Override @@ -81,18 +81,23 @@ public class ModpackDownloadListPage extends DownloadListPage { } @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) { - throw new UnsupportedOperationException(); + public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { + return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionByLocalFile(localModFile, file); } @Override - public RemoteMod getModById(String id) { - throw new UnsupportedOperationException(); + public RemoteMod getModById(String id) throws IOException { + return CurseForgeRemoteModRepository.MODPACKS.getModById(id); + } + + @Override + public RemoteMod.File getModFile(String modId, String fileId) throws IOException { + return CurseForgeRemoteModRepository.MODPACKS.getModFile(modId, fileId); } @Override public Stream getRemoteVersionsById(String id) throws IOException { - throw new UnsupportedOperationException(); + return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionsById(id); } } 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 4bc1a90c0..9322a26af 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java @@ -22,7 +22,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.versioning.VersionNumber; -import java.time.Instant; +import java.util.Date; import java.util.List; import java.util.Objects; @@ -36,7 +36,7 @@ public class RemoteVersion implements Comparable { private final String libraryId; private final String gameVersion; private final String selfVersion; - private final Instant releaseDate; + private final Date releaseDate; private final List urls; private final Type type; @@ -47,7 +47,7 @@ public class RemoteVersion implements Comparable { * @param selfVersion the version string of the remote version. * @param urls the installer or universal jar original URL. */ - public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Instant releaseDate, List urls) { + public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Date releaseDate, List urls) { this(libraryId, gameVersion, selfVersion, releaseDate, Type.UNCATEGORIZED, urls); } @@ -58,7 +58,7 @@ public class RemoteVersion implements Comparable { * @param selfVersion the version string of the remote version. * @param urls the installer or universal jar URL. */ - public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Instant releaseDate, Type type, List urls) { + public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Date releaseDate, Type type, List urls) { this.libraryId = Objects.requireNonNull(libraryId); this.gameVersion = Objects.requireNonNull(gameVersion); this.selfVersion = Objects.requireNonNull(selfVersion); @@ -83,7 +83,7 @@ public class RemoteVersion implements Comparable { return getSelfVersion(); } - public Instant getReleaseDate() { + public Date getReleaseDate() { return releaseDate; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java index ce3dd4b7b..4bc1303d1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java @@ -17,12 +17,14 @@ */ package org.jackhuang.hmcl.download.fabric; -import org.jackhuang.hmcl.download.*; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.task.Task; -import java.time.Instant; +import java.util.Date; import java.util.List; public class FabricAPIRemoteVersion extends RemoteVersion { @@ -36,7 +38,7 @@ public class FabricAPIRemoteVersion extends RemoteVersion { * @param selfVersion the version string of the remote version. * @param urls the installer or universal jar original URL. */ - FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, Instant datePublished, RemoteMod.Version version, List urls) { + FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, Date datePublished, RemoteMod.Version version, List urls) { super(LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId(), gameVersion, selfVersion, datePublished, urls); this.fullVersion = fullVersion; 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 74ac3886d..f71076f25 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 @@ -30,10 +30,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; import java.time.format.DateTimeParseException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -112,7 +109,7 @@ public final class ForgeBMCLVersionList extends VersionList } versions.put(gameVersion, new ForgeRemoteVersion( - version.getGameVersion(), version.getVersion(), releaseDate, urls)); + version.getGameVersion(), version.getVersion(), releaseDate == null ? null : Date.from(releaseDate), urls)); } } finally { lock.writeLock().unlock(); 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 36f29b2be..3f3e9bd49 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 @@ -23,7 +23,7 @@ import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; -import java.time.Instant; +import java.util.Date; import java.util.List; public class ForgeRemoteVersion extends RemoteVersion { @@ -34,8 +34,8 @@ public class ForgeRemoteVersion extends RemoteVersion { * @param selfVersion the version string of the remote version. * @param url the installer or universal jar original URL. */ - public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant instant, List url) { - super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, instant, url); + public ForgeRemoteVersion(String gameVersion, String selfVersion, Date releaseDate, List url) { + super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, releaseDate, url); } @Override 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 64b9a0e10..08cbd40c6 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 @@ -25,7 +25,7 @@ import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; -import java.time.Instant; +import java.util.Date; import java.util.List; /** @@ -37,7 +37,7 @@ public final class GameRemoteVersion extends RemoteVersion { private final ReleaseType type; - public GameRemoteVersion(String gameVersion, String selfVersion, List url, ReleaseType type, Instant releaseDate) { + public GameRemoteVersion(String gameVersion, String selfVersion, List url, ReleaseType type, Date releaseDate) { super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, releaseDate, getReleaseType(type), url); this.type = type; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java index f62398926..4d04b76a5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java @@ -59,7 +59,7 @@ public final class GameVersionList extends VersionList { remoteVersion.getGameVersion(), remoteVersion.getGameVersion(), Collections.singletonList(remoteVersion.getUrl()), - remoteVersion.getType(), remoteVersion.getReleaseTime().toInstant())); + remoteVersion.getType(), remoteVersion.getReleaseTime())); } } finally { lock.writeLock().unlock(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index ded775afc..f3930a691 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -20,7 +20,7 @@ package org.jackhuang.hmcl.mod; import org.jackhuang.hmcl.task.FileDownloadTask; import java.io.IOException; -import java.time.Instant; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -90,9 +90,9 @@ public class RemoteMod { } public interface IMod { - List loadDependencies() throws IOException; + List loadDependencies(RemoteModRepository modRepository) throws IOException; - Stream loadVersions() throws IOException; + Stream loadVersions(RemoteModRepository modRepository) throws IOException; } public interface IVersion { @@ -105,14 +105,14 @@ public class RemoteMod { private final String name; private final String version; private final String changelog; - private final Instant datePublished; + private final Date datePublished; private final VersionType versionType; private final File file; private final List dependencies; private final List gameVersions; private final List loaders; - public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { this.self = self; this.modid = modid; this.name = name; @@ -146,7 +146,7 @@ public class RemoteMod { return changelog; } - public Instant getDatePublished() { + public Date getDatePublished() { return datePublished; } 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 52576b90b..249533d28 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -44,13 +44,20 @@ public interface RemoteModRepository { TOTAL_DOWNLOADS } - Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) + enum SortOrder { + ASC, + DESC + } + + Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException; Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; RemoteMod getModById(String id) throws IOException; + RemoteMod.File getModFile(String modId, String fileId) throws IOException; + Stream getRemoteVersionsById(String id) throws IOException; Stream getCategories() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 4071b98a1..82a2485e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -19,157 +19,167 @@ package org.jackhuang.hmcl.mod.curse; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.Immutable; +import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @Immutable public class CurseAddon implements RemoteMod.IMod { private final int id; - private final String name; - private final List authors; - private final List attachments; - private final String websiteUrl; private final int gameId; - private final String summary; - private final int defaultFileId; - private final LatestFile file; - private final List latestFiles; - private final List categories; - private final int status; - private final int primaryCategoryId; + private final String name; private final String slug; - private final List gameVersionLatestFiles; + private final Links links; + private final String summary; + private final int status; + private final int downloadCount; private final boolean isFeatured; - private final double popularityScore; + private final int primaryCategoryId; + private final List categories; + private final int classId; + private final List authors; + private final Logo logo; + private final int mainFileId; + private final List latestFiles; + private final List latestFileIndices; + private final Date dateCreated; + private final Date dateModified; + private final Date dateReleased; + private final boolean allowModDistribution; private final int gamePopularityRank; - private final String primaryLanguage; // e.g. enUS - private final List modLoaders; private final boolean isAvailable; - private final boolean isExperimental; + private final int thumbsUpCount; - public CurseAddon(int id, String name, List authors, List attachments, String websiteUrl, int gameId, String summary, int defaultFileId, LatestFile file, List latestFiles, List categories, int status, int primaryCategoryId, String slug, List gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List modLoaders, boolean isAvailable, boolean isExperimental) { + public CurseAddon(int id, int gameId, String name, String slug, Links links, String summary, int status, int downloadCount, boolean isFeatured, int primaryCategoryId, List categories, int classId, List authors, Logo logo, int mainFileId, List latestFiles, List latestFileIndices, Date dateCreated, Date dateModified, Date dateReleased, boolean allowModDistribution, int gamePopularityRank, boolean isAvailable, int thumbsUpCount) { this.id = id; - this.name = name; - this.authors = authors; - this.attachments = attachments; - this.websiteUrl = websiteUrl; this.gameId = gameId; - this.summary = summary; - this.defaultFileId = defaultFileId; - this.file = file; - this.latestFiles = latestFiles; - this.categories = categories; - this.status = status; - this.primaryCategoryId = primaryCategoryId; + this.name = name; this.slug = slug; - this.gameVersionLatestFiles = gameVersionLatestFiles; + this.links = links; + this.summary = summary; + this.status = status; + this.downloadCount = downloadCount; this.isFeatured = isFeatured; - this.popularityScore = popularityScore; + this.primaryCategoryId = primaryCategoryId; + this.categories = categories; + this.classId = classId; + this.authors = authors; + this.logo = logo; + this.mainFileId = mainFileId; + this.latestFiles = latestFiles; + this.latestFileIndices = latestFileIndices; + this.dateCreated = dateCreated; + this.dateModified = dateModified; + this.dateReleased = dateReleased; + this.allowModDistribution = allowModDistribution; this.gamePopularityRank = gamePopularityRank; - this.primaryLanguage = primaryLanguage; - this.modLoaders = modLoaders; this.isAvailable = isAvailable; - this.isExperimental = isExperimental; + this.thumbsUpCount = thumbsUpCount; } public int getId() { return id; } - public String getName() { - return name; - } - - public List getAuthors() { - return authors; - } - - public List getAttachments() { - return attachments; - } - - public String getWebsiteUrl() { - return websiteUrl; - } - public int getGameId() { return gameId; } - public String getSummary() { - return summary; - } - - public int getDefaultFileId() { - return defaultFileId; - } - - public LatestFile getFile() { - return file; - } - - public List getLatestFiles() { - return latestFiles; - } - - public List getCategories() { - return categories; - } - - public int getStatus() { - return status; - } - - public int getPrimaryCategoryId() { - return primaryCategoryId; + public String getName() { + return name; } public String getSlug() { return slug; } - public List getGameVersionLatestFiles() { - return gameVersionLatestFiles; + public Links getLinks() { + return links; + } + + public String getSummary() { + return summary; + } + + public int getStatus() { + return status; + } + + public int getDownloadCount() { + return downloadCount; } public boolean isFeatured() { return isFeatured; } - public double getPopularityScore() { - return popularityScore; + public int getPrimaryCategoryId() { + return primaryCategoryId; + } + + public List getCategories() { + return categories; + } + + public int getClassId() { + return classId; + } + + public List getAuthors() { + return authors; + } + + public Logo getLogo() { + return logo; + } + + public int getMainFileId() { + return mainFileId; + } + + public List getLatestFiles() { + return latestFiles; + } + + public List getLatestFileIndices() { + return latestFileIndices; + } + + public Date getDateCreated() { + return dateCreated; + } + + public Date getDateModified() { + return dateModified; + } + + public Date getDateReleased() { + return dateReleased; + } + + public boolean isAllowModDistribution() { + return allowModDistribution; } public int getGamePopularityRank() { return gamePopularityRank; } - public String getPrimaryLanguage() { - return primaryLanguage; - } - - public List getModLoaders() { - return modLoaders; - } - public boolean isAvailable() { return isAvailable; } - public boolean isExperimental() { - return isExperimental; + public int getThumbsUpCount() { + return thumbsUpCount; } @Override - public List loadDependencies() throws IOException { + public List loadDependencies(RemoteModRepository modRepository) throws IOException { Set dependencies = latestFiles.stream() .flatMap(latestFile -> latestFile.getDependencies().stream()) .filter(dep -> dep.getType() == 3) @@ -177,52 +187,78 @@ public class CurseAddon implements RemoteMod.IMod { .collect(Collectors.toSet()); List mods = new ArrayList<>(); for (int dependencyId : dependencies) { - mods.add(CurseForgeRemoteModRepository.MODS.getModById(Integer.toString(dependencyId))); + mods.add(modRepository.getModById(Integer.toString(dependencyId))); } return mods; } @Override - public Stream loadVersions() throws IOException { - return CurseForgeRemoteModRepository.MODS.getRemoteVersionsById(Integer.toString(id)); + public Stream loadVersions(RemoteModRepository modRepository) throws IOException { + return modRepository.getRemoteVersionsById(Integer.toString(id)); } public RemoteMod toMod() { - String iconUrl = null; - for (CurseAddon.Attachment attachment : attachments) { - if (attachment.isDefault()) { - iconUrl = attachment.getThumbnailUrl(); - } - } + String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse(""); return new RemoteMod( slug, "", name, summary, - categories.stream().map(category -> Integer.toString(category.getCategoryId())).collect(Collectors.toList()), - websiteUrl, + categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()), + links.websiteUrl, iconUrl, this ); } + @Immutable + public static class Links { + private final String websiteUrl; + private final String wikiUrl; + private final String issuesUrl; + private final String sourceUrl; + + public Links(String websiteUrl, String wikiUrl, String issuesUrl, String sourceUrl) { + this.websiteUrl = websiteUrl; + this.wikiUrl = wikiUrl; + this.issuesUrl = issuesUrl; + this.sourceUrl = sourceUrl; + } + + public String getWebsiteUrl() { + return websiteUrl; + } + + public String getWikiUrl() { + return wikiUrl; + } + + @Nullable + public String getIssuesUrl() { + return issuesUrl; + } + + @Nullable + public String getSourceUrl() { + return sourceUrl; + } + } + @Immutable public static class Author { + private final int id; private final String name; private final String url; - private final int projectId; - private final int id; - private final int userId; - private final int twitchId; - public Author(String name, String url, int projectId, int id, int userId, int twitchId) { + public Author(int id, String name, String url) { + this.id = id; this.name = name; this.url = url; - this.projectId = projectId; - this.id = id; - this.userId = userId; - this.twitchId = twitchId; + } + + public int getId() { + return id; } public String getName() { @@ -232,21 +268,48 @@ public class CurseAddon implements RemoteMod.IMod { public String getUrl() { return url; } + } - public int getProjectId() { - return projectId; + @Immutable + public static class Logo { + private final int id; + private final int modId; + private final String title; + private final String description; + private final String thumbnailUrl; + private final String url; + + public Logo(int id, int modId, String title, String description, String thumbnailUrl, String url) { + this.id = id; + this.modId = modId; + this.title = title; + this.description = description; + this.thumbnailUrl = thumbnailUrl; + this.url = url; } public int getId() { return id; } - public int getUserId() { - return userId; + public int getModId() { + return modId; } - public int getTwitchId() { - return twitchId; + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public String getUrl() { + return url; } } @@ -340,60 +403,89 @@ public class CurseAddon implements RemoteMod.IMod { } } + /** + * @see Schema + */ + @Immutable + public static class LatestFileHash { + private final String value; + private final int algo; + + public LatestFileHash(String value, int algo) { + this.value = value; + this.algo = algo; + } + + public String getValue() { + return value; + } + + public int getAlgo() { + return algo; + } + } + + /** + * @see Schema + */ @Immutable public static class LatestFile implements RemoteMod.IVersion { private final int id; + private final int gameId; + private final int modId; + private final boolean isAvailable; private final String displayName; private final String fileName; - private final String fileDate; - private final int fileLength; private final int releaseType; private final int fileStatus; + private final List hashes; + private final Date fileDate; + private final int fileLength; + private final int downloadCount; private final String downloadUrl; - private final boolean isAlternate; - private final int alternateFileId; + private final List gameVersions; private final List dependencies; - private final boolean isAvailable; - private final List gameVersion; - private final boolean hasInstallScript; - private final boolean isCompatibleWIthClient; - private final int categorySectionPackageType; - private final int restrictProjectFileAccess; - private final int projectStatus; - private final int projectId; + private final int alternateFileId; private final boolean isServerPack; - private final int serverPackFileId; + private final long fileFingerprint; - private transient Instant fileDataInstant; - - public LatestFile(int id, String displayName, String fileName, String fileDate, int fileLength, int releaseType, int fileStatus, String downloadUrl, boolean isAlternate, int alternateFileId, List dependencies, boolean isAvailable, List gameVersion, boolean hasInstallScript, boolean isCompatibleWIthClient, int categorySectionPackageType, int restrictProjectFileAccess, int projectStatus, int projectId, boolean isServerPack, int serverPackFileId) { + public LatestFile(int id, int gameId, int modId, boolean isAvailable, String displayName, String fileName, int releaseType, int fileStatus, List hashes, Date fileDate, int fileLength, int downloadCount, String downloadUrl, List gameVersions, List dependencies, int alternateFileId, boolean isServerPack, long fileFingerprint) { this.id = id; + this.gameId = gameId; + this.modId = modId; + this.isAvailable = isAvailable; this.displayName = displayName; this.fileName = fileName; - this.fileDate = fileDate; - this.fileLength = fileLength; this.releaseType = releaseType; this.fileStatus = fileStatus; + this.hashes = hashes; + this.fileDate = fileDate; + this.fileLength = fileLength; + this.downloadCount = downloadCount; this.downloadUrl = downloadUrl; - this.isAlternate = isAlternate; - this.alternateFileId = alternateFileId; + this.gameVersions = gameVersions; this.dependencies = dependencies; - this.isAvailable = isAvailable; - this.gameVersion = gameVersion; - this.hasInstallScript = hasInstallScript; - this.isCompatibleWIthClient = isCompatibleWIthClient; - this.categorySectionPackageType = categorySectionPackageType; - this.restrictProjectFileAccess = restrictProjectFileAccess; - this.projectStatus = projectStatus; - this.projectId = projectId; + this.alternateFileId = alternateFileId; this.isServerPack = isServerPack; - this.serverPackFileId = serverPackFileId; + this.fileFingerprint = fileFingerprint; } public int getId() { return id; } + public int getGameId() { + return gameId; + } + + public int getModId() { + return modId; + } + + public boolean isAvailable() { + return isAvailable; + } + public String getDisplayName() { return displayName; } @@ -402,14 +494,6 @@ public class CurseAddon implements RemoteMod.IMod { return fileName; } - public String getFileDate() { - return fileDate; - } - - public int getFileLength() { - return fileLength; - } - public int getReleaseType() { return releaseType; } @@ -418,67 +502,49 @@ public class CurseAddon implements RemoteMod.IMod { return fileStatus; } + public List getHashes() { + return hashes; + } + + public Date getFileDate() { + return fileDate; + } + + public int getFileLength() { + return fileLength; + } + + public int getDownloadCount() { + return downloadCount; + } + public String getDownloadUrl() { + if (downloadUrl == null) { + // This addon is not allowed for distribution, and downloadUrl will be null. + // We try to find its download url. + return String.format("https://edge.forgecdn.net/files/%d/%d/%s", id / 1000, id % 1000, fileName); + } return downloadUrl; } - public boolean isAlternate() { - return isAlternate; - } - - public int getAlternateFileId() { - return alternateFileId; + public List getGameVersions() { + return gameVersions; } public List getDependencies() { return dependencies; } - public boolean isAvailable() { - return isAvailable; - } - - public List getGameVersion() { - return gameVersion; - } - - public boolean isHasInstallScript() { - return hasInstallScript; - } - - public boolean isCompatibleWIthClient() { - return isCompatibleWIthClient; - } - - public int getCategorySectionPackageType() { - return categorySectionPackageType; - } - - public int getRestrictProjectFileAccess() { - return restrictProjectFileAccess; - } - - public int getProjectStatus() { - return projectStatus; - } - - public int getProjectId() { - return projectId; + public int getAlternateFileId() { + return alternateFileId; } public boolean isServerPack() { return isServerPack; } - public int getServerPackFileId() { - return serverPackFileId; - } - - public Instant getParsedFileDate() { - if (fileDataInstant == null) { - fileDataInstant = Instant.parse(fileDate); - } - return fileDataInstant; + public long getFileFingerprint() { + return fileFingerprint; } @Override @@ -504,9 +570,9 @@ public class CurseAddon implements RemoteMod.IMod { } ModLoaderType modLoaderType; - if (gameVersion.contains("Forge")) { + if (gameVersions.contains("Forge")) { modLoaderType = ModLoaderType.FORGE; - } else if (gameVersion.contains("Fabric")) { + } else if (gameVersions.contains("Fabric")) { modLoaderType = ModLoaderType.FABRIC; } else { modLoaderType = ModLoaderType.UNKNOWN; @@ -514,94 +580,38 @@ public class CurseAddon implements RemoteMod.IMod { return new RemoteMod.Version( this, - Integer.toString(projectId), + Integer.toString(modId), getDisplayName(), getFileName(), null, - getParsedFileDate(), + getFileDate(), versionType, new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), Collections.emptyList(), - gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), + gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), Collections.singletonList(modLoaderType) ); } } + /** + * @see Schema + */ @Immutable - public static class Category { - private final int categoryId; - private final String name; - private final String url; - private final String avatarUrl; - private final int parentId; - private final int rootId; - private final int projectId; - private final int avatarId; - private final int gameId; - - public Category(int categoryId, String name, String url, String avatarUrl, int parentId, int rootId, int projectId, int avatarId, int gameId) { - this.categoryId = categoryId; - this.name = name; - this.url = url; - this.avatarUrl = avatarUrl; - this.parentId = parentId; - this.rootId = rootId; - this.projectId = projectId; - this.avatarId = avatarId; - this.gameId = gameId; - } - - public int getCategoryId() { - return categoryId; - } - - public String getName() { - return name; - } - - public String getUrl() { - return url; - } - - public String getAvatarUrl() { - return avatarUrl; - } - - public int getParentId() { - return parentId; - } - - public int getRootId() { - return rootId; - } - - public int getProjectId() { - return projectId; - } - - public int getAvatarId() { - return avatarId; - } - - public int getGameId() { - return gameId; - } - } - - @Immutable - public static class GameVersionLatestFile { + public static class LatestFileIndex { private final String gameVersion; - private final String projectFileId; - private final String projectFileName; - private final int fileType; - private final Integer modLoader; // optional + private final int fileId; + private final String filename; + private final int releaseType; + private final int gameVersionTypeId; + private final int modLoader; - public GameVersionLatestFile(String gameVersion, String projectFileId, String projectFileName, int fileType, Integer modLoader) { + public LatestFileIndex(String gameVersion, int fileId, String filename, int releaseType, int gameVersionTypeId, int modLoader) { this.gameVersion = gameVersion; - this.projectFileId = projectFileId; - this.projectFileName = projectFileName; - this.fileType = fileType; + this.fileId = fileId; + this.filename = filename; + this.releaseType = releaseType; + this.gameVersionTypeId = gameVersionTypeId; this.modLoader = modLoader; } @@ -609,20 +619,111 @@ public class CurseAddon implements RemoteMod.IMod { return gameVersion; } - public String getProjectFileId() { - return projectFileId; + public int getFileId() { + return fileId; } - public String getProjectFileName() { - return projectFileName; + public String getFilename() { + return filename; } - public int getFileType() { - return fileType; + public int getReleaseType() { + return releaseType; } - public Integer getModLoader() { + @Nullable + public int getGameVersionTypeId() { + return gameVersionTypeId; + } + + public int getModLoader() { return modLoader; } } + + @Immutable + public static class Category { + private final int id; + private final int gameId; + private final String name; + private final String slug; + private final String url; + private final String iconUrl; + private final Date dateModified; + private final boolean isClass; + private final int classId; + private final int parentCategoryId; + + private transient final List subcategories; + + public Category() { + this(0, 0, "", "", "", "", new Date(), false, 0, 0); + } + + public Category(int id, int gameId, String name, String slug, String url, String iconUrl, Date dateModified, boolean isClass, int classId, int parentCategoryId) { + this.id = id; + this.gameId = gameId; + this.name = name; + this.slug = slug; + this.url = url; + this.iconUrl = iconUrl; + this.dateModified = dateModified; + this.isClass = isClass; + this.classId = classId; + this.parentCategoryId = parentCategoryId; + + this.subcategories = new ArrayList<>(); + } + + public int getId() { + return id; + } + + public int getGameId() { + return gameId; + } + + public String getName() { + return name; + } + + public String getSlug() { + return slug; + } + + public String getUrl() { + return url; + } + + public String getIconUrl() { + return iconUrl; + } + + public Date getDateModified() { + return dateModified; + } + + public boolean isClass() { + return isClass; + } + + public int getClassId() { + return classId; + } + + public int getParentCategoryId() { + return parentCategoryId; + } + + public List getSubcategories() { + return subcategories; + } + + public RemoteModRepository.Category toCategory() { + return new RemoteModRepository.Category( + this, + Integer.toString(id), + getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList())); + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java index 4e3f08d98..6a2c156be 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java @@ -21,13 +21,13 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.File; import java.io.FileNotFoundException; @@ -117,35 +117,18 @@ public final class CurseCompletionTask extends Task { manifest.getFiles().parallelStream() .map(file -> { updateProgress(finished.incrementAndGet(), manifest.getFiles().size()); - if (StringUtils.isBlank(file.getFileName())) { + if (StringUtils.isBlank(file.getFileName()) || file.getUrl() == null) { try { - return file.withFileName(NetworkUtils.detectFileName(file.getUrl())); - } catch (IOException e) { - try { - String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID()))); - CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); - return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL()); - } catch (FileNotFoundException fof) { - Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof); - notFound.set(true); - return file; - } catch (IOException | JsonParseException e2) { - try { - String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID()))); - CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); - return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL()); - } catch (FileNotFoundException fof) { - Logging.LOG.log(Level.WARNING, "Could not query forgesvc for deleted mods: " + file.getUrl(), fof); - notFound.set(true); - return file; - } catch (IOException | JsonParseException e3) { - Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e); - Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e2); - Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e3); - allNameKnown.set(false); - return file; - } - } + RemoteMod.File remoteFile = CurseForgeRemoteModRepository.MODS.getModFile(Integer.toString(file.getProjectID()), Integer.toString(file.getFileID())); + return file.withFileName(remoteFile.getFilename()).withURL(remoteFile.getUrl()); + } catch (FileNotFoundException fof) { + Logging.LOG.log(Level.WARNING, "Could not query api.curseforge.com for deleted mods: " + file.getProjectID() + ", " +file.getFileID(), fof); + notFound.set(true); + return file; + } catch (IOException | JsonParseException e) { + Logging.LOG.log(Level.WARNING, "Unable to fetch the file name projectID=" + file.getProjectID() + ", fileID=" +file.getFileID(), e); + allNameKnown.set(false); + return file; } } else { return file; 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 3289e2302..56b788c10 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 @@ -22,16 +22,15 @@ import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.MurmurHash2; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.HttpRequest; -import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.io.JarUtils; -import java.io.*; -import java.net.URL; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.Lang.mapOf; @@ -39,12 +38,19 @@ import static org.jackhuang.hmcl.util.Pair.pair; public final class CurseForgeRemoteModRepository implements RemoteModRepository { - private static final String PREFIX = "https://addons-ecs.forgesvc.net"; + private static final String PREFIX = "https://api.curseforge.com"; + + private static String apiKey; + + static { + apiKey = System.getProperty("hmcl.curseforge.apikey", + JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("CurseForge-Api-Key")).orElse("")); + } private final Type type; private final int section; - private CurseForgeRemoteModRepository(Type type, int section) { + public CurseForgeRemoteModRepository(Type type, int section) { this.type = type; this.section = section; } @@ -54,27 +60,55 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository return type; } - public List searchPaginated(String gameVersion, int category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { - String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery(PREFIX + "/api/v2/addon/search", mapOf( - pair("categoryId", Integer.toString(category)), - pair("gameId", "432"), - pair("gameVersion", gameVersion), - pair("index", Integer.toString(pageOffset)), - pair("pageSize", Integer.toString(pageSize)), - pair("searchFilter", searchFilter), - pair("sectionId", Integer.toString(section)), - pair("sort", Integer.toString(sort)) - )))); - return JsonUtils.fromNonNullJson(response, new TypeToken>() { - }.getType()); + private int toModsSearchSortField(SortType sort) { + // https://docs.curseforge.com/#tocS_ModsSearchSortField + switch (sort) { + case DATE_CREATED: + return 1; + case POPULARITY: + return 2; + case LAST_UPDATED: + return 3; + case NAME: + return 4; + case AUTHOR: + return 5; + case TOTAL_DOWNLOADS: + return 6; + default: + return 8; + } + } + + private String toSortOrder(SortOrder sortOrder) { + // https://docs.curseforge.com/#tocS_SortOrder + switch (sortOrder) { + case ASC: + return "asc"; + case DESC: + return "desc"; + } + return "asc"; } @Override - public Stream search(String gameVersion, RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { + public Stream search(String gameVersion, RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { int categoryId = 0; - if (category != null) categoryId = ((Category) category.getSelf()).getId(); - return searchPaginated(gameVersion, categoryId, pageOffset, pageSize, searchFilter, sort.ordinal()).stream() - .map(CurseAddon::toMod); + if (category != null) 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)), + pair("pageSize", Integer.toString(pageSize))) + .header("X-API-KEY", apiKey) + .getJson(new TypeToken>>() { + }.getType()); + return response.getData().stream().map(CurseAddon::toMod); } @Override @@ -95,60 +129,74 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); - FingerprintResponse response = HttpRequest.POST(PREFIX + "/api/v2/fingerprint") - .json(Collections.singletonList(hash)) - .getJson(FingerprintResponse.class); + Response response = HttpRequest.POST(PREFIX + "/v1/fingerprints") + .json(mapOf(pair("fingerprints", Collections.singletonList(hash)))) + .header("X-API-KEY", apiKey) + .getJson(new TypeToken>() { + }.getType()); - if (response.getExactMatches() == null || response.getExactMatches().isEmpty()) { + if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) { return Optional.empty(); } - return Optional.of(response.getExactMatches().get(0).getFile().toVersion()); + return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion()); } @Override public RemoteMod getModById(String id) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id)); - return JsonUtils.fromNonNullJson(response, CurseAddon.class).toMod(); + return HttpRequest.GET(PREFIX + "/v1/mods/" + id) + .header("X-API-KEY", apiKey) + .getJson(CurseAddon.class).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) + .getJson(new TypeToken>() { + }.getType()); + return response.getData().toVersion().getFile(); } @Override public Stream getRemoteVersionsById(String id) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id + "/files")); - List files = JsonUtils.fromNonNullJson(response, new TypeToken>() { - }.getType()); - return files.stream().map(CurseAddon.LatestFile::toVersion); + Response> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files") + .header("X-API-KEY", apiKey) + .getJson(new TypeToken>>() { + }.getType()); + return response.getData().stream().map(CurseAddon.LatestFile::toVersion); } - public List getCategoriesImpl() throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/category/section/" + section)); - List categories = JsonUtils.fromNonNullJson(response, new TypeToken>() { - }.getType()); - return reorganizeCategories(categories, section); + public List getCategoriesImpl() throws IOException { + Response> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")) + .header("X-API-KEY", apiKey) + .getJson(new TypeToken>>() { + }.getType()); + return reorganizeCategories(categories.getData(), section); } @Override public Stream getCategories() throws IOException { - return getCategoriesImpl().stream().map(Category::toCategory); + return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory); } - private List reorganizeCategories(List categories, int rootId) { - List result = new ArrayList<>(); + private List reorganizeCategories(List categories, int rootId) { + List result = new ArrayList<>(); - Map categoryMap = new HashMap<>(); - for (Category category : categories) { - categoryMap.put(category.id, category); + Map categoryMap = new HashMap<>(); + for (CurseAddon.Category category : categories) { + categoryMap.put(category.getId(), category); } - for (Category category : categories) { - if (category.parentGameCategoryId == rootId) { + for (CurseAddon.Category category : categories) { + if (category.getParentCategoryId() == rootId) { result.add(category); } else { - Category parentCategory = categoryMap.get(category.parentGameCategoryId); + CurseAddon.Category parentCategory = categoryMap.get(category.getParentCategoryId()); if (parentCategory == null) { // Category list is not correct, so we ignore this item. continue; } - parentCategory.subcategories.add(category); + parentCategory.getSubcategories().add(category); } } return result; @@ -165,84 +213,69 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository public static final int SECTION_UNKNOWN2 = 4979; public static final int SECTION_UNKNOWN3 = 4984; - public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(Type.MOD, SECTION_MOD); - public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(Type.MODPACK, SECTION_MODPACK); - public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(Type.RESOURCE_PACK, SECTION_RESOURCE_PACK); - public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(Type.WORLD, SECTION_WORLD); - public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(Type.CUSTOMIZATION, SECTION_CUSTOMIZATION); + public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD); + public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK); + public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK); + public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD); + public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION); - public static class Category { - private final int id; - private final String name; - private final String slug; - private final String avatarUrl; - private final int parentGameCategoryId; - private final int rootGameCategoryId; - private final int gameId; + public static class Pagination { + private final int index; + private final int pageSize; + private final int resultCount; + private final int totalCount; - private final List subcategories; - - public Category() { - this(0, "", "", "", 0, 0, 0, new ArrayList<>()); + public Pagination(int index, int pageSize, int resultCount, int totalCount) { + this.index = index; + this.pageSize = pageSize; + this.resultCount = resultCount; + this.totalCount = totalCount; } - public Category(int id, String name, String slug, String avatarUrl, int parentGameCategoryId, int rootGameCategoryId, int gameId, List subcategories) { - this.id = id; - this.name = name; - this.slug = slug; - this.avatarUrl = avatarUrl; - this.parentGameCategoryId = parentGameCategoryId; - this.rootGameCategoryId = rootGameCategoryId; - this.gameId = gameId; - this.subcategories = subcategories; + public int getIndex() { + return index; } - public int getId() { - return id; + public int getPageSize() { + return pageSize; } - public String getName() { - return name; + public int getResultCount() { + return resultCount; } - public String getSlug() { - return slug; - } - - public String getAvatarUrl() { - return avatarUrl; - } - - public int getParentGameCategoryId() { - return parentGameCategoryId; - } - - public int getRootGameCategoryId() { - return rootGameCategoryId; - } - - public int getGameId() { - return gameId; - } - - public List getSubcategories() { - return subcategories; - } - - public RemoteModRepository.Category toCategory() { - return new RemoteModRepository.Category( - this, - Integer.toString(id), - getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList())); + public int getTotalCount() { + return totalCount; } } - private static class FingerprintResponse { + public static class Response { + private final T data; + private final Pagination pagination; + + public Response(T data, Pagination pagination) { + this.data = data; + this.pagination = pagination; + } + + public T getData() { + return data; + } + + public Pagination getPagination() { + return pagination; + } + } + + /** + * @see Schema + */ + private static class FingerprintMatchesResult { private final boolean isCacheBuilt; - private final List exactMatches; + private final List exactMatches; private final List exactFingerprints; - public FingerprintResponse(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { + public FingerprintMatchesResult(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { this.isCacheBuilt = isCacheBuilt; this.exactMatches = exactMatches; this.exactFingerprints = exactFingerprints; @@ -252,7 +285,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository return isCacheBuilt; } - public List getExactMatches() { + public List getExactMatches() { return exactMatches; } @@ -260,4 +293,31 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository return exactFingerprints; } } + + /** + * @see Schema + */ + private static class FingerprintMatch { + private final int id; + private final CurseAddon.LatestFile file; + private final List latestFiles; + + public FingerprintMatch(int id, CurseAddon.LatestFile file, List latestFiles) { + this.id = id; + this.file = file; + this.latestFiles = latestFiles; + } + + public int getId() { + return id; + } + + public CurseAddon.LatestFile getFile() { + return file; + } + + public List getLatestFiles() { + return latestFiles; + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java index e14a592d0..150bf608d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java @@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jetbrains.annotations.Nullable; import java.net.URL; import java.util.Objects; @@ -82,9 +83,17 @@ public final class CurseManifestFile implements Validation { throw new JsonParseException("Missing Project ID or File ID."); } + @Nullable public URL getUrl() { - return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file") - : NetworkUtils.toURL(NetworkUtils.encodeLocation(url)); + if (url == null) { + if (fileName != null) { + return NetworkUtils.toURL(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName))); + } else { + return null; + } + } else { + return NetworkUtils.toURL(NetworkUtils.encodeLocation(url)); + } } public CurseManifestFile withFileName(String fileName) { 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 6714904f2..9b9f54032 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 @@ -34,10 +34,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,7 +71,8 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { } } - public List searchPaginated(String gameVersion, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { + @Override + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { Map query = mapOf( pair("query", searchFilter), pair("offset", Integer.toString(pageOffset)), @@ -87,13 +85,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { Response response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query)) .getJson(new TypeToken>() { }.getType()); - return response.getHits(); - } - - @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { - return searchPaginated(gameVersion, pageOffset, pageSize, searchFilter, sort).stream() - .map(ModResult::toMod); + return response.getHits().stream().map(ModResult::toMod); } @Override @@ -119,6 +111,11 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { throw new UnsupportedOperationException(); } + @Override + public RemoteMod.File getModFile(String modId, String fileId) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public Stream getRemoteVersionsById(String id) throws IOException { id = StringUtils.removePrefix(id, "local-"); @@ -238,7 +235,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { private final String changelog; @SerializedName("date_published") - private final Instant datePublished; + private final Date datePublished; private final int downloads; @@ -254,7 +251,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { private final List loaders; - public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Instant datePublished, int downloads, String versionType, List files, List dependencies, List gameVersions, List loaders) { + public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Date datePublished, int downloads, String versionType, List files, List dependencies, List gameVersions, List loaders) { this.id = id; this.modId = modId; this.authorId = authorId; @@ -294,7 +291,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { return changelog; } - public Instant getDatePublished() { + public Date getDatePublished() { return datePublished; } @@ -501,13 +498,13 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { } @Override - public List loadDependencies() throws IOException { + public List loadDependencies(RemoteModRepository modRepository) throws IOException { return Collections.emptyList(); } @Override - public Stream loadVersions() throws IOException { - return ModrinthRemoteModRepository.INSTANCE.getRemoteVersionsById(getModId()); + public Stream loadVersions(RemoteModRepository modRepository) throws IOException { + return modRepository.getRemoteVersionsById(getModId()); } public RemoteMod toMod() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java index 772956872..c7a07c98a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java @@ -38,6 +38,7 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; +import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.Lang.threadPool; @@ -48,12 +49,14 @@ public abstract class FetchTask extends Task { protected CacheRepository repository = CacheRepository.getInstance(); public FetchTask(List urls, int retry) { - if (urls == null || urls.isEmpty()) - throw new IllegalArgumentException("At least one URL is required"); + Objects.requireNonNull(urls); - this.urls = new ArrayList<>(urls); + this.urls = urls.stream().filter(Objects::nonNull).collect(Collectors.toList()); this.retry = retry; + if (this.urls.isEmpty()) + throw new IllegalArgumentException("At least one URL is required"); + setExecutor(download()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/DateTypeAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/DateTypeAdapter.java index 5c9f2de29..b53ae0e4b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/DateTypeAdapter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/DateTypeAdapter.java @@ -23,6 +23,11 @@ import java.lang.reflect.Type; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.Locale; @@ -66,14 +71,14 @@ public final class DateTypeAdapter implements JsonSerializer, JsonDeserial return EN_US_FORMAT.parse(string); } catch (ParseException ex1) { try { - return ISO_8601_FORMAT.parse(string); - } catch (ParseException ex2) { + ZonedDateTime zonedDateTime = ZonedDateTime.parse(string, DateTimeFormatter.ISO_DATE_TIME); + return Date.from(zonedDateTime.toInstant()); + } catch (DateTimeParseException e) { try { - String cleaned = string.replace("Z", "+00:00"); - cleaned = cleaned.substring(0, 22) + cleaned.substring(23); - return ISO_8601_FORMAT.parse(cleaned); - } catch (Exception e) { - throw new JsonParseException("Invalid date: " + string, e); + LocalDateTime localDateTime = LocalDateTime.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } catch (DateTimeParseException e2) { + throw new JsonParseException("Invalid date: " + string, e2); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java index 63e05bab8..821e2fa79 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java @@ -195,7 +195,7 @@ public final class JavaVersion { JavaVersion javaVersion = new JavaVersion(executable, version, platform); if (javaVersion.getParsedVersion() == UNKNOWN) - throw new IOException("Unrecognized Java version " + version); + throw new IOException("Unrecognized Java version " + version + " at " + executable); fromExecutableCache.put(executable, javaVersion); return javaVersion; }