fix(download): switch to api.curseforge.com.

This commit is contained in:
huanghongxun
2022-05-21 18:56:22 +08:00
parent 3766491c28
commit 99e578b1bc
23 changed files with 699 additions and 517 deletions

View File

@@ -41,9 +41,11 @@ val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
} }
} }
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5" val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
val versionType = System.getenv("VERSION_TYPE") ?: "nightly"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: "" val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: "" val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val versionType = System.getenv("VERSION_TYPE") ?: "nightly" val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
version = "$versionRoot.$buildNumber" version = "$versionRoot.$buildNumber"
@@ -147,6 +149,7 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
"Implementation-Version" to project.version, "Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId, "Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret, "Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType, "Build-Channel" to versionType,
"Class-Path" to "pack200.jar", "Class-Path" to "pack200.jar",
"Add-Opens" to listOf( "Add-Opens" to listOf(

View File

@@ -96,7 +96,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@FXML @FXML
private StackPane center; private StackPane center;
private VersionList<?> versionList; private final VersionList<?> versionList;
private CompletableFuture<?> executor; private CompletableFuture<?> executor;
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) { 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()); content.setTitle(remoteVersion.getSelfVersion());
if (remoteVersion.getReleaseDate() != null) { 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 { } else {
content.setSubtitle(""); content.setSubtitle("");
} }

View File

@@ -173,7 +173,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
} }
return gameVersion; return gameVersion;
}).thenApplyAsync(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) -> { }).whenComplete(Schedulers.javafx(), (result, exception) -> {
setLoading(false); setLoading(false);
if (exception == null) { if (exception == null) {

View File

@@ -103,9 +103,9 @@ public class DownloadPage extends Control implements DecoratorPage {
setFailed(false); setFailed(false);
Task.allOf( Task.allOf(
Task.supplyAsync(() -> addon.getData().loadDependencies()), Task.supplyAsync(() -> addon.getData().loadDependencies(repository)),
Task.supplyAsync(() -> { Task.supplyAsync(() -> {
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(); Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
// if (StringUtils.isNotBlank(version.getVersion())) { // if (StringUtils.isNotBlank(version.getVersion())) {
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar); // Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
// if (gameVersion.isPresent()) { // if (gameVersion.isPresent()) {
@@ -284,7 +284,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Node title = ComponentList.createComponentListTitle(i18n("mods.dependencies")); 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.managedProperty().bind(show);
title.visibleProperty().bind(show); title.visibleProperty().bind(show);
dependencyPane.managedProperty().bind(show); dependencyPane.managedProperty().bind(show);
@@ -385,7 +385,7 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.getChildren().setAll(graphicPane, content, saveAsButton); pane.getChildren().setAll(graphicPane, content, saveAsButton);
content.setTitle(dataItem.getName()); content.setTitle(dataItem.getName());
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished())); content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant()));
saveAsButton.setOnMouseClicked(e -> selfPage.saveAs(dataItem)); saveAsButton.setOnMouseClicked(e -> selfPage.saveAs(dataItem));
switch (dataItem.getVersionType()) { switch (dataItem.getVersionType()) {

View File

@@ -47,13 +47,21 @@ public class ModDownloadListPage extends DownloadListPage {
private class Repository implements RemoteModRepository { private class Repository implements RemoteModRepository {
private RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.INSTANCE;
} else {
return CurseForgeRemoteModRepository.MODS;
}
}
@Override @Override
public Type getType() { public Type getType() {
return Type.MOD; return Type.MOD;
} }
@Override @Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter; String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
List<ModTranslations.Mod> mods = ModTranslations.MOD.searchMod(searchFilter); List<ModTranslations.Mod> mods = ModTranslations.MOD.searchMod(searchFilter);
@@ -75,35 +83,32 @@ public class ModDownloadListPage extends DownloadListPage {
newSearchFilter = searchFilter; newSearchFilter = searchFilter;
} }
if ("mods.modrinth".equals(downloadSource.get())) { return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
return ModrinthRemoteModRepository.INSTANCE.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort);
} else {
return CurseForgeRemoteModRepository.MODS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort);
}
} }
@Override @Override
public Stream<Category> getCategories() throws IOException { public Stream<Category> getCategories() throws IOException {
if ("mods.modrinth".equals(downloadSource.get())) { return getBackedRemoteModRepository().getCategories();
return ModrinthRemoteModRepository.INSTANCE.getCategories();
} else {
return CurseForgeRemoteModRepository.MODS.getCategories();
}
} }
@Override @Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) { public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
throw new UnsupportedOperationException(); return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
} }
@Override @Override
public RemoteMod getModById(String id) { public RemoteMod getModById(String id) throws IOException {
throw new UnsupportedOperationException(); return getBackedRemoteModRepository().getModById(id);
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModFile(modId, fileId);
} }
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
throw new UnsupportedOperationException(); return getBackedRemoteModRepository().getRemoteVersionsById(id);
} }
} }

View File

@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@@ -38,7 +39,9 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModTranslations { public final class ModTranslations {
public static ModTranslations MOD = new ModTranslations("/assets/mod_data.txt"); public static ModTranslations MOD = new ModTranslations("/assets/mod_data.txt");
public static ModTranslations MODPACK = new ModTranslations("/assets/modpack_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) { public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
switch (type) { switch (type) {
case MOD: case MOD:
@@ -46,7 +49,7 @@ public final class ModTranslations {
case MODPACK: case MODPACK:
return MODPACK; return MODPACK;
default: default:
throw new IllegalArgumentException(); return EMPTY;
} }
} }
@@ -61,12 +64,14 @@ public final class ModTranslations {
this.resourceName = resourceName; this.resourceName = resourceName;
} }
@Nullable
public Mod getModByCurseForgeId(String id) { public Mod getModByCurseForgeId(String id) {
if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null; if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null;
return curseForgeMap.get(id); return curseForgeMap.get(id);
} }
@Nullable
public Mod getModById(String id) { public Mod getModById(String id) {
if (StringUtils.isBlank(id) || !loadModIdMap()) return null; if (StringUtils.isBlank(id) || !loadModIdMap()) return null;
@@ -96,7 +101,7 @@ public final class ModTranslations {
} }
private boolean loadFromResource() { private boolean loadFromResource() {
if (mods != null) return true; if (mods != null || StringUtils.isBlank(resourceName)) return true;
try { try {
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8); 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()); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());

View File

@@ -50,7 +50,7 @@ public class ModpackDownloadListPage extends DownloadListPage {
} }
@Override @Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter; String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
List<ModTranslations.Mod> mods = ModTranslations.MODPACK.searchMod(searchFilter); List<ModTranslations.Mod> mods = ModTranslations.MODPACK.searchMod(searchFilter);
@@ -72,7 +72,7 @@ public class ModpackDownloadListPage extends DownloadListPage {
newSearchFilter = searchFilter; newSearchFilter = searchFilter;
} }
return CurseForgeRemoteModRepository.MODPACKS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); return CurseForgeRemoteModRepository.MODPACKS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
} }
@Override @Override
@@ -81,18 +81,23 @@ public class ModpackDownloadListPage extends DownloadListPage {
} }
@Override @Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) { public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
throw new UnsupportedOperationException(); return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionByLocalFile(localModFile, file);
} }
@Override @Override
public RemoteMod getModById(String id) { public RemoteMod getModById(String id) throws IOException {
throw new UnsupportedOperationException(); return CurseForgeRemoteModRepository.MODPACKS.getModById(id);
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
return CurseForgeRemoteModRepository.MODPACKS.getModFile(modId, fileId);
} }
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
throw new UnsupportedOperationException(); return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionsById(id);
} }
} }

View File

@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.time.Instant; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -36,7 +36,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
private final String libraryId; private final String libraryId;
private final String gameVersion; private final String gameVersion;
private final String selfVersion; private final String selfVersion;
private final Instant releaseDate; private final Date releaseDate;
private final List<String> urls; private final List<String> urls;
private final Type type; private final Type type;
@@ -47,7 +47,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
* @param selfVersion the version string of the remote version. * @param selfVersion the version string of the remote version.
* @param urls the installer or universal jar original URL. * @param urls the installer or universal jar original URL.
*/ */
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Instant releaseDate, List<String> urls) { public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Date releaseDate, List<String> urls) {
this(libraryId, gameVersion, selfVersion, releaseDate, Type.UNCATEGORIZED, urls); this(libraryId, gameVersion, selfVersion, releaseDate, Type.UNCATEGORIZED, urls);
} }
@@ -58,7 +58,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
* @param selfVersion the version string of the remote version. * @param selfVersion the version string of the remote version.
* @param urls the installer or universal jar URL. * @param urls the installer or universal jar URL.
*/ */
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Instant releaseDate, Type type, List<String> urls) { public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Date releaseDate, Type type, List<String> urls) {
this.libraryId = Objects.requireNonNull(libraryId); this.libraryId = Objects.requireNonNull(libraryId);
this.gameVersion = Objects.requireNonNull(gameVersion); this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion); this.selfVersion = Objects.requireNonNull(selfVersion);
@@ -83,7 +83,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
return getSelfVersion(); return getSelfVersion();
} }
public Instant getReleaseDate() { public Date getReleaseDate() {
return releaseDate; return releaseDate;
} }

View File

@@ -17,12 +17,14 @@
*/ */
package org.jackhuang.hmcl.download.fabric; 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.game.Version;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.time.Instant; import java.util.Date;
import java.util.List; import java.util.List;
public class FabricAPIRemoteVersion extends RemoteVersion { public class FabricAPIRemoteVersion extends RemoteVersion {
@@ -36,7 +38,7 @@ public class FabricAPIRemoteVersion extends RemoteVersion {
* @param selfVersion the version string of the remote version. * @param selfVersion the version string of the remote version.
* @param urls the installer or universal jar original URL. * @param urls the installer or universal jar original URL.
*/ */
FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, Instant datePublished, RemoteMod.Version version, List<String> urls) { FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, Date datePublished, RemoteMod.Version version, List<String> urls) {
super(LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId(), gameVersion, selfVersion, datePublished, urls); super(LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId(), gameVersion, selfVersion, datePublished, urls);
this.fullVersion = fullVersion; this.fullVersion = fullVersion;

View File

@@ -30,10 +30,7 @@ import org.jetbrains.annotations.Nullable;
import java.time.Instant; import java.time.Instant;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level; import java.util.logging.Level;
@@ -112,7 +109,7 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
} }
versions.put(gameVersion, new ForgeRemoteVersion( versions.put(gameVersion, new ForgeRemoteVersion(
version.getGameVersion(), version.getVersion(), releaseDate, urls)); version.getGameVersion(), version.getVersion(), releaseDate == null ? null : Date.from(releaseDate), urls));
} }
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();

View File

@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.time.Instant; import java.util.Date;
import java.util.List; import java.util.List;
public class ForgeRemoteVersion extends RemoteVersion { public class ForgeRemoteVersion extends RemoteVersion {
@@ -34,8 +34,8 @@ public class ForgeRemoteVersion extends RemoteVersion {
* @param selfVersion the version string of the remote version. * @param selfVersion the version string of the remote version.
* @param url the installer or universal jar original URL. * @param url the installer or universal jar original URL.
*/ */
public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant instant, List<String> url) { public ForgeRemoteVersion(String gameVersion, String selfVersion, Date releaseDate, List<String> url) {
super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, instant, url); super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, releaseDate, url);
} }
@Override @Override

View File

@@ -25,7 +25,7 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import java.time.Instant; import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@@ -37,7 +37,7 @@ public final class GameRemoteVersion extends RemoteVersion {
private final ReleaseType type; private final ReleaseType type;
public GameRemoteVersion(String gameVersion, String selfVersion, List<String> url, ReleaseType type, Instant releaseDate) { public GameRemoteVersion(String gameVersion, String selfVersion, List<String> url, ReleaseType type, Date releaseDate) {
super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, releaseDate, getReleaseType(type), url); super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, releaseDate, getReleaseType(type), url);
this.type = type; this.type = type;
} }

View File

@@ -59,7 +59,7 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
remoteVersion.getGameVersion(), remoteVersion.getGameVersion(),
remoteVersion.getGameVersion(), remoteVersion.getGameVersion(),
Collections.singletonList(remoteVersion.getUrl()), Collections.singletonList(remoteVersion.getUrl()),
remoteVersion.getType(), remoteVersion.getReleaseTime().toInstant())); remoteVersion.getType(), remoteVersion.getReleaseTime()));
} }
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();

View File

@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -90,9 +90,9 @@ public class RemoteMod {
} }
public interface IMod { public interface IMod {
List<RemoteMod> loadDependencies() throws IOException; List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException;
Stream<Version> loadVersions() throws IOException; Stream<Version> loadVersions(RemoteModRepository modRepository) throws IOException;
} }
public interface IVersion { public interface IVersion {
@@ -105,14 +105,14 @@ public class RemoteMod {
private final String name; private final String name;
private final String version; private final String version;
private final String changelog; private final String changelog;
private final Instant datePublished; private final Date datePublished;
private final VersionType versionType; private final VersionType versionType;
private final File file; private final File file;
private final List<String> dependencies; private final List<String> dependencies;
private final List<String> gameVersions; private final List<String> gameVersions;
private final List<ModLoaderType> loaders; private final List<ModLoaderType> loaders;
public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List<String> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) { public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List<String> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) {
this.self = self; this.self = self;
this.modid = modid; this.modid = modid;
this.name = name; this.name = name;
@@ -146,7 +146,7 @@ public class RemoteMod {
return changelog; return changelog;
} }
public Instant getDatePublished() { public Date getDatePublished() {
return datePublished; return datePublished;
} }

View File

@@ -44,13 +44,20 @@ public interface RemoteModRepository {
TOTAL_DOWNLOADS TOTAL_DOWNLOADS
} }
Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) enum SortOrder {
ASC,
DESC
}
Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
throws IOException; throws IOException;
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
RemoteMod getModById(String id) throws IOException; RemoteMod getModById(String id) throws IOException;
RemoteMod.File getModFile(String modId, String fileId) throws IOException;
Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException; Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException;
Stream<Category> getCategories() throws IOException; Stream<Category> getCategories() throws IOException;

View File

@@ -19,157 +19,167 @@ package org.jackhuang.hmcl.mod.curse;
import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@Immutable @Immutable
public class CurseAddon implements RemoteMod.IMod { public class CurseAddon implements RemoteMod.IMod {
private final int id; private final int id;
private final String name;
private final List<Author> authors;
private final List<Attachment> attachments;
private final String websiteUrl;
private final int gameId; private final int gameId;
private final String summary; private final String name;
private final int defaultFileId;
private final LatestFile file;
private final List<LatestFile> latestFiles;
private final List<Category> categories;
private final int status;
private final int primaryCategoryId;
private final String slug; private final String slug;
private final List<GameVersionLatestFile> gameVersionLatestFiles; private final Links links;
private final String summary;
private final int status;
private final int downloadCount;
private final boolean isFeatured; private final boolean isFeatured;
private final double popularityScore; private final int primaryCategoryId;
private final List<Category> categories;
private final int classId;
private final List<Author> authors;
private final Logo logo;
private final int mainFileId;
private final List<LatestFile> latestFiles;
private final List<LatestFileIndex> latestFileIndices;
private final Date dateCreated;
private final Date dateModified;
private final Date dateReleased;
private final boolean allowModDistribution;
private final int gamePopularityRank; private final int gamePopularityRank;
private final String primaryLanguage; // e.g. enUS
private final List<String> modLoaders;
private final boolean isAvailable; private final boolean isAvailable;
private final boolean isExperimental; private final int thumbsUpCount;
public CurseAddon(int id, String name, List<Author> authors, List<Attachment> attachments, String websiteUrl, int gameId, String summary, int defaultFileId, LatestFile file, List<LatestFile> latestFiles, List<Category> categories, int status, int primaryCategoryId, String slug, List<GameVersionLatestFile> gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List<String> 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<Category> categories, int classId, List<Author> authors, Logo logo, int mainFileId, List<LatestFile> latestFiles, List<LatestFileIndex> latestFileIndices, Date dateCreated, Date dateModified, Date dateReleased, boolean allowModDistribution, int gamePopularityRank, boolean isAvailable, int thumbsUpCount) {
this.id = id; this.id = id;
this.name = name;
this.authors = authors;
this.attachments = attachments;
this.websiteUrl = websiteUrl;
this.gameId = gameId; this.gameId = gameId;
this.summary = summary; this.name = name;
this.defaultFileId = defaultFileId;
this.file = file;
this.latestFiles = latestFiles;
this.categories = categories;
this.status = status;
this.primaryCategoryId = primaryCategoryId;
this.slug = slug; this.slug = slug;
this.gameVersionLatestFiles = gameVersionLatestFiles; this.links = links;
this.summary = summary;
this.status = status;
this.downloadCount = downloadCount;
this.isFeatured = isFeatured; 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.gamePopularityRank = gamePopularityRank;
this.primaryLanguage = primaryLanguage;
this.modLoaders = modLoaders;
this.isAvailable = isAvailable; this.isAvailable = isAvailable;
this.isExperimental = isExperimental; this.thumbsUpCount = thumbsUpCount;
} }
public int getId() { public int getId() {
return id; return id;
} }
public String getName() {
return name;
}
public List<Author> getAuthors() {
return authors;
}
public List<Attachment> getAttachments() {
return attachments;
}
public String getWebsiteUrl() {
return websiteUrl;
}
public int getGameId() { public int getGameId() {
return gameId; return gameId;
} }
public String getSummary() { public String getName() {
return summary; return name;
}
public int getDefaultFileId() {
return defaultFileId;
}
public LatestFile getFile() {
return file;
}
public List<LatestFile> getLatestFiles() {
return latestFiles;
}
public List<Category> getCategories() {
return categories;
}
public int getStatus() {
return status;
}
public int getPrimaryCategoryId() {
return primaryCategoryId;
} }
public String getSlug() { public String getSlug() {
return slug; return slug;
} }
public List<GameVersionLatestFile> getGameVersionLatestFiles() { public Links getLinks() {
return gameVersionLatestFiles; return links;
}
public String getSummary() {
return summary;
}
public int getStatus() {
return status;
}
public int getDownloadCount() {
return downloadCount;
} }
public boolean isFeatured() { public boolean isFeatured() {
return isFeatured; return isFeatured;
} }
public double getPopularityScore() { public int getPrimaryCategoryId() {
return popularityScore; return primaryCategoryId;
}
public List<Category> getCategories() {
return categories;
}
public int getClassId() {
return classId;
}
public List<Author> getAuthors() {
return authors;
}
public Logo getLogo() {
return logo;
}
public int getMainFileId() {
return mainFileId;
}
public List<LatestFile> getLatestFiles() {
return latestFiles;
}
public List<LatestFileIndex> 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() { public int getGamePopularityRank() {
return gamePopularityRank; return gamePopularityRank;
} }
public String getPrimaryLanguage() {
return primaryLanguage;
}
public List<String> getModLoaders() {
return modLoaders;
}
public boolean isAvailable() { public boolean isAvailable() {
return isAvailable; return isAvailable;
} }
public boolean isExperimental() { public int getThumbsUpCount() {
return isExperimental; return thumbsUpCount;
} }
@Override @Override
public List<RemoteMod> loadDependencies() throws IOException { public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
Set<Integer> dependencies = latestFiles.stream() Set<Integer> dependencies = latestFiles.stream()
.flatMap(latestFile -> latestFile.getDependencies().stream()) .flatMap(latestFile -> latestFile.getDependencies().stream())
.filter(dep -> dep.getType() == 3) .filter(dep -> dep.getType() == 3)
@@ -177,52 +187,78 @@ public class CurseAddon implements RemoteMod.IMod {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
List<RemoteMod> mods = new ArrayList<>(); List<RemoteMod> mods = new ArrayList<>();
for (int dependencyId : dependencies) { for (int dependencyId : dependencies) {
mods.add(CurseForgeRemoteModRepository.MODS.getModById(Integer.toString(dependencyId))); mods.add(modRepository.getModById(Integer.toString(dependencyId)));
} }
return mods; return mods;
} }
@Override @Override
public Stream<RemoteMod.Version> loadVersions() throws IOException { public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
return CurseForgeRemoteModRepository.MODS.getRemoteVersionsById(Integer.toString(id)); return modRepository.getRemoteVersionsById(Integer.toString(id));
} }
public RemoteMod toMod() { public RemoteMod toMod() {
String iconUrl = null; String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse("");
for (CurseAddon.Attachment attachment : attachments) {
if (attachment.isDefault()) {
iconUrl = attachment.getThumbnailUrl();
}
}
return new RemoteMod( return new RemoteMod(
slug, slug,
"", "",
name, name,
summary, summary,
categories.stream().map(category -> Integer.toString(category.getCategoryId())).collect(Collectors.toList()), categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()),
websiteUrl, links.websiteUrl,
iconUrl, iconUrl,
this 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 @Immutable
public static class Author { public static class Author {
private final int id;
private final String name; private final String name;
private final String url; 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.name = name;
this.url = url; this.url = url;
this.projectId = projectId; }
this.id = id;
this.userId = userId; public int getId() {
this.twitchId = twitchId; return id;
} }
public String getName() { public String getName() {
@@ -232,21 +268,48 @@ public class CurseAddon implements RemoteMod.IMod {
public String getUrl() { public String getUrl() {
return url; return url;
} }
}
public int getProjectId() { @Immutable
return projectId; 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() { public int getId() {
return id; return id;
} }
public int getUserId() { public int getModId() {
return userId; return modId;
} }
public int getTwitchId() { public String getTitle() {
return twitchId; 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 <a href="https://docs.curseforge.com/#schemafilehash">Schema</a>
*/
@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 <a href="https://docs.curseforge.com/#tocS_File">Schema</a>
*/
@Immutable @Immutable
public static class LatestFile implements RemoteMod.IVersion { public static class LatestFile implements RemoteMod.IVersion {
private final int id; private final int id;
private final int gameId;
private final int modId;
private final boolean isAvailable;
private final String displayName; private final String displayName;
private final String fileName; private final String fileName;
private final String fileDate;
private final int fileLength;
private final int releaseType; private final int releaseType;
private final int fileStatus; private final int fileStatus;
private final List<LatestFileHash> hashes;
private final Date fileDate;
private final int fileLength;
private final int downloadCount;
private final String downloadUrl; private final String downloadUrl;
private final boolean isAlternate; private final List<String> gameVersions;
private final int alternateFileId;
private final List<Dependency> dependencies; private final List<Dependency> dependencies;
private final boolean isAvailable; private final int alternateFileId;
private final List<String> 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 boolean isServerPack; private final boolean isServerPack;
private final int serverPackFileId; private final long fileFingerprint;
private transient Instant fileDataInstant; public LatestFile(int id, int gameId, int modId, boolean isAvailable, String displayName, String fileName, int releaseType, int fileStatus, List<LatestFileHash> hashes, Date fileDate, int fileLength, int downloadCount, String downloadUrl, List<String> gameVersions, List<Dependency> dependencies, int alternateFileId, boolean isServerPack, long fileFingerprint) {
public LatestFile(int id, String displayName, String fileName, String fileDate, int fileLength, int releaseType, int fileStatus, String downloadUrl, boolean isAlternate, int alternateFileId, List<Dependency> dependencies, boolean isAvailable, List<String> gameVersion, boolean hasInstallScript, boolean isCompatibleWIthClient, int categorySectionPackageType, int restrictProjectFileAccess, int projectStatus, int projectId, boolean isServerPack, int serverPackFileId) {
this.id = id; this.id = id;
this.gameId = gameId;
this.modId = modId;
this.isAvailable = isAvailable;
this.displayName = displayName; this.displayName = displayName;
this.fileName = fileName; this.fileName = fileName;
this.fileDate = fileDate;
this.fileLength = fileLength;
this.releaseType = releaseType; this.releaseType = releaseType;
this.fileStatus = fileStatus; this.fileStatus = fileStatus;
this.hashes = hashes;
this.fileDate = fileDate;
this.fileLength = fileLength;
this.downloadCount = downloadCount;
this.downloadUrl = downloadUrl; this.downloadUrl = downloadUrl;
this.isAlternate = isAlternate; this.gameVersions = gameVersions;
this.alternateFileId = alternateFileId;
this.dependencies = dependencies; this.dependencies = dependencies;
this.isAvailable = isAvailable; this.alternateFileId = alternateFileId;
this.gameVersion = gameVersion;
this.hasInstallScript = hasInstallScript;
this.isCompatibleWIthClient = isCompatibleWIthClient;
this.categorySectionPackageType = categorySectionPackageType;
this.restrictProjectFileAccess = restrictProjectFileAccess;
this.projectStatus = projectStatus;
this.projectId = projectId;
this.isServerPack = isServerPack; this.isServerPack = isServerPack;
this.serverPackFileId = serverPackFileId; this.fileFingerprint = fileFingerprint;
} }
public int getId() { public int getId() {
return id; return id;
} }
public int getGameId() {
return gameId;
}
public int getModId() {
return modId;
}
public boolean isAvailable() {
return isAvailable;
}
public String getDisplayName() { public String getDisplayName() {
return displayName; return displayName;
} }
@@ -402,14 +494,6 @@ public class CurseAddon implements RemoteMod.IMod {
return fileName; return fileName;
} }
public String getFileDate() {
return fileDate;
}
public int getFileLength() {
return fileLength;
}
public int getReleaseType() { public int getReleaseType() {
return releaseType; return releaseType;
} }
@@ -418,67 +502,49 @@ public class CurseAddon implements RemoteMod.IMod {
return fileStatus; return fileStatus;
} }
public List<LatestFileHash> getHashes() {
return hashes;
}
public Date getFileDate() {
return fileDate;
}
public int getFileLength() {
return fileLength;
}
public int getDownloadCount() {
return downloadCount;
}
public String getDownloadUrl() { 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; return downloadUrl;
} }
public boolean isAlternate() { public List<String> getGameVersions() {
return isAlternate; return gameVersions;
}
public int getAlternateFileId() {
return alternateFileId;
} }
public List<Dependency> getDependencies() { public List<Dependency> getDependencies() {
return dependencies; return dependencies;
} }
public boolean isAvailable() { public int getAlternateFileId() {
return isAvailable; return alternateFileId;
}
public List<String> 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 boolean isServerPack() { public boolean isServerPack() {
return isServerPack; return isServerPack;
} }
public int getServerPackFileId() { public long getFileFingerprint() {
return serverPackFileId; return fileFingerprint;
}
public Instant getParsedFileDate() {
if (fileDataInstant == null) {
fileDataInstant = Instant.parse(fileDate);
}
return fileDataInstant;
} }
@Override @Override
@@ -504,9 +570,9 @@ public class CurseAddon implements RemoteMod.IMod {
} }
ModLoaderType modLoaderType; ModLoaderType modLoaderType;
if (gameVersion.contains("Forge")) { if (gameVersions.contains("Forge")) {
modLoaderType = ModLoaderType.FORGE; modLoaderType = ModLoaderType.FORGE;
} else if (gameVersion.contains("Fabric")) { } else if (gameVersions.contains("Fabric")) {
modLoaderType = ModLoaderType.FABRIC; modLoaderType = ModLoaderType.FABRIC;
} else { } else {
modLoaderType = ModLoaderType.UNKNOWN; modLoaderType = ModLoaderType.UNKNOWN;
@@ -514,94 +580,38 @@ public class CurseAddon implements RemoteMod.IMod {
return new RemoteMod.Version( return new RemoteMod.Version(
this, this,
Integer.toString(projectId), Integer.toString(modId),
getDisplayName(), getDisplayName(),
getFileName(), getFileName(),
null, null,
getParsedFileDate(), getFileDate(),
versionType, versionType,
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
Collections.emptyList(), 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) Collections.singletonList(modLoaderType)
); );
} }
} }
/**
* @see <a href="https://docs.curseforge.com/#tocS_FileIndex">Schema</a>
*/
@Immutable @Immutable
public static class Category { public static class LatestFileIndex {
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 {
private final String gameVersion; private final String gameVersion;
private final String projectFileId; private final int fileId;
private final String projectFileName; private final String filename;
private final int fileType; private final int releaseType;
private final Integer modLoader; // optional 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.gameVersion = gameVersion;
this.projectFileId = projectFileId; this.fileId = fileId;
this.projectFileName = projectFileName; this.filename = filename;
this.fileType = fileType; this.releaseType = releaseType;
this.gameVersionTypeId = gameVersionTypeId;
this.modLoader = modLoader; this.modLoader = modLoader;
} }
@@ -609,20 +619,111 @@ public class CurseAddon implements RemoteMod.IMod {
return gameVersion; return gameVersion;
} }
public String getProjectFileId() { public int getFileId() {
return projectFileId; return fileId;
} }
public String getProjectFileName() { public String getFilename() {
return projectFileName; return filename;
} }
public int getFileType() { public int getReleaseType() {
return fileType; return releaseType;
} }
public Integer getModLoader() { @Nullable
public int getGameVersionTypeId() {
return gameVersionTypeId;
}
public int getModLoader() {
return modLoader; 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<Category> 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<Category> getSubcategories() {
return subcategories;
}
public RemoteModRepository.Category toCategory() {
return new RemoteModRepository.Category(
this,
Integer.toString(id),
getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList()));
}
}
} }

View File

@@ -21,13 +21,13 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -117,35 +117,18 @@ public final class CurseCompletionTask extends Task<Void> {
manifest.getFiles().parallelStream() manifest.getFiles().parallelStream()
.map(file -> { .map(file -> {
updateProgress(finished.incrementAndGet(), manifest.getFiles().size()); updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
if (StringUtils.isBlank(file.getFileName())) { if (StringUtils.isBlank(file.getFileName()) || file.getUrl() == null) {
try { try {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl())); RemoteMod.File remoteFile = CurseForgeRemoteModRepository.MODS.getModFile(Integer.toString(file.getProjectID()), Integer.toString(file.getFileID()));
} catch (IOException e) { return file.withFileName(remoteFile.getFilename()).withURL(remoteFile.getUrl());
try { } catch (FileNotFoundException fof) {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID()))); Logging.LOG.log(Level.WARNING, "Could not query api.curseforge.com for deleted mods: " + file.getProjectID() + ", " +file.getFileID(), fof);
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); notFound.set(true);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL()); return file;
} catch (FileNotFoundException fof) { } catch (IOException | JsonParseException e) {
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof); Logging.LOG.log(Level.WARNING, "Unable to fetch the file name projectID=" + file.getProjectID() + ", fileID=" +file.getFileID(), e);
notFound.set(true); allNameKnown.set(false);
return file; 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;
}
}
} }
} else { } else {
return file; return file;

View File

@@ -22,16 +22,15 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.MurmurHash2; 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.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.*; import java.io.ByteArrayOutputStream;
import java.net.URL; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf; 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 { 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 Type type;
private final int section; private final int section;
private CurseForgeRemoteModRepository(Type type, int section) { public CurseForgeRemoteModRepository(Type type, int section) {
this.type = type; this.type = type;
this.section = section; this.section = section;
} }
@@ -54,27 +60,55 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return type; return type;
} }
public List<CurseAddon> searchPaginated(String gameVersion, int category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { private int toModsSearchSortField(SortType sort) {
String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery(PREFIX + "/api/v2/addon/search", mapOf( // https://docs.curseforge.com/#tocS_ModsSearchSortField
pair("categoryId", Integer.toString(category)), switch (sort) {
pair("gameId", "432"), case DATE_CREATED:
pair("gameVersion", gameVersion), return 1;
pair("index", Integer.toString(pageOffset)), case POPULARITY:
pair("pageSize", Integer.toString(pageSize)), return 2;
pair("searchFilter", searchFilter), case LAST_UPDATED:
pair("sectionId", Integer.toString(section)), return 3;
pair("sort", Integer.toString(sort)) case NAME:
)))); return 4;
return JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon>>() { case AUTHOR:
}.getType()); 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 @Override
public Stream<RemoteMod> search(String gameVersion, RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { public Stream<RemoteMod> search(String gameVersion, RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
int categoryId = 0; int categoryId = 0;
if (category != null) categoryId = ((Category) category.getSelf()).getId(); if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
return searchPaginated(gameVersion, categoryId, pageOffset, pageSize, searchFilter, sort.ordinal()).stream() Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
.map(CurseAddon::toMod); 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<Response<List<CurseAddon>>>() {
}.getType());
return response.getData().stream().map(CurseAddon::toMod);
} }
@Override @Override
@@ -95,60 +129,74 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
FingerprintResponse response = HttpRequest.POST(PREFIX + "/api/v2/fingerprint") Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints")
.json(Collections.singletonList(hash)) .json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
.getJson(FingerprintResponse.class); .header("X-API-KEY", apiKey)
.getJson(new TypeToken<Response<FingerprintMatchesResult>>() {
}.getType());
if (response.getExactMatches() == null || response.getExactMatches().isEmpty()) { if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(response.getExactMatches().get(0).getFile().toVersion()); return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion());
} }
@Override @Override
public RemoteMod getModById(String id) throws IOException { public RemoteMod getModById(String id) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id)); return HttpRequest.GET(PREFIX + "/v1/mods/" + id)
return JsonUtils.fromNonNullJson(response, CurseAddon.class).toMod(); .header("X-API-KEY", apiKey)
.getJson(CurseAddon.class).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)
.getJson(new TypeToken<Response<CurseAddon.LatestFile>>() {
}.getType());
return response.getData().toVersion().getFile();
} }
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id + "/files")); Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files")
List<CurseAddon.LatestFile> files = JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() { .header("X-API-KEY", apiKey)
}.getType()); .getJson(new TypeToken<Response<List<CurseAddon.LatestFile>>>() {
return files.stream().map(CurseAddon.LatestFile::toVersion); }.getType());
return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
} }
public List<Category> getCategoriesImpl() throws IOException { public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/category/section/" + section)); Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() { .header("X-API-KEY", apiKey)
}.getType()); .getJson(new TypeToken<Response<List<CurseAddon.Category>>>() {
return reorganizeCategories(categories, section); }.getType());
return reorganizeCategories(categories.getData(), section);
} }
@Override @Override
public Stream<RemoteModRepository.Category> getCategories() throws IOException { public Stream<RemoteModRepository.Category> getCategories() throws IOException {
return getCategoriesImpl().stream().map(Category::toCategory); return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory);
} }
private List<Category> reorganizeCategories(List<Category> categories, int rootId) { private List<CurseAddon.Category> reorganizeCategories(List<CurseAddon.Category> categories, int rootId) {
List<Category> result = new ArrayList<>(); List<CurseAddon.Category> result = new ArrayList<>();
Map<Integer, Category> categoryMap = new HashMap<>(); Map<Integer, CurseAddon.Category> categoryMap = new HashMap<>();
for (Category category : categories) { for (CurseAddon.Category category : categories) {
categoryMap.put(category.id, category); categoryMap.put(category.getId(), category);
} }
for (Category category : categories) { for (CurseAddon.Category category : categories) {
if (category.parentGameCategoryId == rootId) { if (category.getParentCategoryId() == rootId) {
result.add(category); result.add(category);
} else { } else {
Category parentCategory = categoryMap.get(category.parentGameCategoryId); CurseAddon.Category parentCategory = categoryMap.get(category.getParentCategoryId());
if (parentCategory == null) { if (parentCategory == null) {
// Category list is not correct, so we ignore this item. // Category list is not correct, so we ignore this item.
continue; continue;
} }
parentCategory.subcategories.add(category); parentCategory.getSubcategories().add(category);
} }
} }
return result; return result;
@@ -165,84 +213,69 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
public static final int SECTION_UNKNOWN2 = 4979; public static final int SECTION_UNKNOWN2 = 4979;
public static final int SECTION_UNKNOWN3 = 4984; public static final int SECTION_UNKNOWN3 = 4984;
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(Type.MOD, SECTION_MOD); public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD);
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(Type.MODPACK, SECTION_MODPACK); public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK);
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(Type.RESOURCE_PACK, SECTION_RESOURCE_PACK); public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(Type.WORLD, SECTION_WORLD); public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD);
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(Type.CUSTOMIZATION, SECTION_CUSTOMIZATION); public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
public static class Category { public static class Pagination {
private final int id; private final int index;
private final String name; private final int pageSize;
private final String slug; private final int resultCount;
private final String avatarUrl; private final int totalCount;
private final int parentGameCategoryId;
private final int rootGameCategoryId;
private final int gameId;
private final List<Category> subcategories; public Pagination(int index, int pageSize, int resultCount, int totalCount) {
this.index = index;
public Category() { this.pageSize = pageSize;
this(0, "", "", "", 0, 0, 0, new ArrayList<>()); this.resultCount = resultCount;
this.totalCount = totalCount;
} }
public Category(int id, String name, String slug, String avatarUrl, int parentGameCategoryId, int rootGameCategoryId, int gameId, List<Category> subcategories) { public int getIndex() {
this.id = id; return index;
this.name = name;
this.slug = slug;
this.avatarUrl = avatarUrl;
this.parentGameCategoryId = parentGameCategoryId;
this.rootGameCategoryId = rootGameCategoryId;
this.gameId = gameId;
this.subcategories = subcategories;
} }
public int getId() { public int getPageSize() {
return id; return pageSize;
} }
public String getName() { public int getResultCount() {
return name; return resultCount;
} }
public String getSlug() { public int getTotalCount() {
return slug; return totalCount;
}
public String getAvatarUrl() {
return avatarUrl;
}
public int getParentGameCategoryId() {
return parentGameCategoryId;
}
public int getRootGameCategoryId() {
return rootGameCategoryId;
}
public int getGameId() {
return gameId;
}
public List<Category> getSubcategories() {
return subcategories;
}
public RemoteModRepository.Category toCategory() {
return new RemoteModRepository.Category(
this,
Integer.toString(id),
getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList()));
} }
} }
private static class FingerprintResponse { public static class Response<T> {
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 <a href="https://docs.curseforge.com/#tocS_FingerprintsMatchesResult">Schema</a>
*/
private static class FingerprintMatchesResult {
private final boolean isCacheBuilt; private final boolean isCacheBuilt;
private final List<CurseAddon> exactMatches; private final List<FingerprintMatch> exactMatches;
private final List<Long> exactFingerprints; private final List<Long> exactFingerprints;
public FingerprintResponse(boolean isCacheBuilt, List<CurseAddon> exactMatches, List<Long> exactFingerprints) { public FingerprintMatchesResult(boolean isCacheBuilt, List<FingerprintMatch> exactMatches, List<Long> exactFingerprints) {
this.isCacheBuilt = isCacheBuilt; this.isCacheBuilt = isCacheBuilt;
this.exactMatches = exactMatches; this.exactMatches = exactMatches;
this.exactFingerprints = exactFingerprints; this.exactFingerprints = exactFingerprints;
@@ -252,7 +285,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return isCacheBuilt; return isCacheBuilt;
} }
public List<CurseAddon> getExactMatches() { public List<FingerprintMatch> getExactMatches() {
return exactMatches; return exactMatches;
} }
@@ -260,4 +293,31 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return exactFingerprints; return exactFingerprints;
} }
} }
/**
* @see <a href="https://docs.curseforge.com/#tocS_FingerprintMatch">Schema</a>
*/
private static class FingerprintMatch {
private final int id;
private final CurseAddon.LatestFile file;
private final List<CurseAddon.LatestFile> latestFiles;
public FingerprintMatch(int id, CurseAddon.LatestFile file, List<CurseAddon.LatestFile> latestFiles) {
this.id = id;
this.file = file;
this.latestFiles = latestFiles;
}
public int getId() {
return id;
}
public CurseAddon.LatestFile getFile() {
return file;
}
public List<CurseAddon.LatestFile> getLatestFiles() {
return latestFiles;
}
}
} }

View File

@@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.net.URL; import java.net.URL;
import java.util.Objects; import java.util.Objects;
@@ -82,9 +83,17 @@ public final class CurseManifestFile implements Validation {
throw new JsonParseException("Missing Project ID or File ID."); throw new JsonParseException("Missing Project ID or File ID.");
} }
@Nullable
public URL getUrl() { public URL getUrl() {
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file") if (url == null) {
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url)); 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) { public CurseManifestFile withFileName(String fileName) {

View File

@@ -34,10 +34,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -74,7 +71,8 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
} }
} }
public List<ModResult> searchPaginated(String gameVersion, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException { @Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
Map<String, String> query = mapOf( Map<String, String> query = mapOf(
pair("query", searchFilter), pair("query", searchFilter),
pair("offset", Integer.toString(pageOffset)), pair("offset", Integer.toString(pageOffset)),
@@ -87,13 +85,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
Response<ModResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query)) Response<ModResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query))
.getJson(new TypeToken<Response<ModResult>>() { .getJson(new TypeToken<Response<ModResult>>() {
}.getType()); }.getType());
return response.getHits(); return response.getHits().stream().map(ModResult::toMod);
}
@Override
public Stream<RemoteMod> 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);
} }
@Override @Override
@@ -119,6 +111,11 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
throw new UnsupportedOperationException();
}
@Override @Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException { public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
id = StringUtils.removePrefix(id, "local-"); id = StringUtils.removePrefix(id, "local-");
@@ -238,7 +235,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
private final String changelog; private final String changelog;
@SerializedName("date_published") @SerializedName("date_published")
private final Instant datePublished; private final Date datePublished;
private final int downloads; private final int downloads;
@@ -254,7 +251,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
private final List<String> loaders; private final List<String> loaders;
public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Instant datePublished, int downloads, String versionType, List<ModVersionFile> files, List<String> dependencies, List<String> gameVersions, List<String> loaders) { public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Date datePublished, int downloads, String versionType, List<ModVersionFile> files, List<String> dependencies, List<String> gameVersions, List<String> loaders) {
this.id = id; this.id = id;
this.modId = modId; this.modId = modId;
this.authorId = authorId; this.authorId = authorId;
@@ -294,7 +291,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return changelog; return changelog;
} }
public Instant getDatePublished() { public Date getDatePublished() {
return datePublished; return datePublished;
} }
@@ -501,13 +498,13 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
} }
@Override @Override
public List<RemoteMod> loadDependencies() throws IOException { public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
return Collections.emptyList(); return Collections.emptyList();
} }
@Override @Override
public Stream<RemoteMod.Version> loadVersions() throws IOException { public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
return ModrinthRemoteModRepository.INSTANCE.getRemoteVersionsById(getModId()); return modRepository.getRemoteVersionsById(getModId());
} }
public RemoteMod toMod() { public RemoteMod toMod() {

View File

@@ -38,6 +38,7 @@ import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.Lang.threadPool;
@@ -48,12 +49,14 @@ public abstract class FetchTask<T> extends Task<T> {
protected CacheRepository repository = CacheRepository.getInstance(); protected CacheRepository repository = CacheRepository.getInstance();
public FetchTask(List<URL> urls, int retry) { public FetchTask(List<URL> urls, int retry) {
if (urls == null || urls.isEmpty()) Objects.requireNonNull(urls);
throw new IllegalArgumentException("At least one URL is required");
this.urls = new ArrayList<>(urls); this.urls = urls.stream().filter(Objects::nonNull).collect(Collectors.toList());
this.retry = retry; this.retry = retry;
if (this.urls.isEmpty())
throw new IllegalArgumentException("At least one URL is required");
setExecutor(download()); setExecutor(download());
} }

View File

@@ -23,6 +23,11 @@ import java.lang.reflect.Type;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; 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.Date;
import java.util.Locale; import java.util.Locale;
@@ -66,14 +71,14 @@ public final class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserial
return EN_US_FORMAT.parse(string); return EN_US_FORMAT.parse(string);
} catch (ParseException ex1) { } catch (ParseException ex1) {
try { try {
return ISO_8601_FORMAT.parse(string); ZonedDateTime zonedDateTime = ZonedDateTime.parse(string, DateTimeFormatter.ISO_DATE_TIME);
} catch (ParseException ex2) { return Date.from(zonedDateTime.toInstant());
} catch (DateTimeParseException e) {
try { try {
String cleaned = string.replace("Z", "+00:00"); LocalDateTime localDateTime = LocalDateTime.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
cleaned = cleaned.substring(0, 22) + cleaned.substring(23); return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
return ISO_8601_FORMAT.parse(cleaned); } catch (DateTimeParseException e2) {
} catch (Exception e) { throw new JsonParseException("Invalid date: " + string, e2);
throw new JsonParseException("Invalid date: " + string, e);
} }
} }
} }

View File

@@ -195,7 +195,7 @@ public final class JavaVersion {
JavaVersion javaVersion = new JavaVersion(executable, version, platform); JavaVersion javaVersion = new JavaVersion(executable, version, platform);
if (javaVersion.getParsedVersion() == UNKNOWN) if (javaVersion.getParsedVersion() == UNKNOWN)
throw new IOException("Unrecognized Java version " + version); throw new IOException("Unrecognized Java version " + version + " at " + executable);
fromExecutableCache.put(executable, javaVersion); fromExecutableCache.put(executable, javaVersion);
return javaVersion; return javaVersion;
} }