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 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<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("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(

View File

@@ -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("");
}

View File

@@ -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) {

View File

@@ -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<RemoteMod.Version> versions = addon.getData().loadVersions();
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
// if (StringUtils.isNotBlank(version.getVersion())) {
// Optional<String> 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()) {

View File

@@ -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<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;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
List<ModTranslations.Mod> 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<Category> getCategories() throws IOException {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.INSTANCE.getCategories();
} else {
return CurseForgeRemoteModRepository.MODS.getCategories();
}
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) {
throw new UnsupportedOperationException();
public Optional<RemoteMod.Version> 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<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.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());

View File

@@ -50,7 +50,7 @@ public class ModpackDownloadListPage extends DownloadListPage {
}
@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;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
List<ModTranslations.Mod> 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<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) {
throw new UnsupportedOperationException();
public Optional<RemoteMod.Version> 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<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.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<RemoteVersion> {
private final String libraryId;
private final String gameVersion;
private final String selfVersion;
private final Instant releaseDate;
private final Date releaseDate;
private final List<String> urls;
private final Type type;
@@ -47,7 +47,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
* @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<String> urls) {
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Date releaseDate, List<String> 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 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.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
@@ -83,7 +83,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
return getSelfVersion();
}
public Instant getReleaseDate() {
public Date getReleaseDate() {
return releaseDate;
}

View File

@@ -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<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);
this.fullVersion = fullVersion;

View File

@@ -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<ForgeRemoteVersion>
}
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();

View File

@@ -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<String> url) {
super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, instant, url);
public ForgeRemoteVersion(String gameVersion, String selfVersion, Date releaseDate, List<String> url) {
super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, releaseDate, url);
}
@Override

View File

@@ -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<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);
this.type = type;
}

View File

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

View File

@@ -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<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 {
@@ -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<String> dependencies;
private final List<String> gameVersions;
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.modid = modid;
this.name = name;
@@ -146,7 +146,7 @@ public class RemoteMod {
return changelog;
}
public Instant getDatePublished() {
public Date getDatePublished() {
return datePublished;
}

View File

@@ -44,13 +44,20 @@ public interface RemoteModRepository {
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;
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) 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<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.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<Author> authors;
private final List<Attachment> attachments;
private final String websiteUrl;
private final int gameId;
private final String summary;
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 name;
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 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 String primaryLanguage; // e.g. enUS
private final List<String> modLoaders;
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.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<Author> getAuthors() {
return authors;
}
public List<Attachment> 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<LatestFile> getLatestFiles() {
return latestFiles;
}
public List<Category> 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<GameVersionLatestFile> 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<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() {
return gamePopularityRank;
}
public String getPrimaryLanguage() {
return primaryLanguage;
}
public List<String> getModLoaders() {
return modLoaders;
}
public boolean isAvailable() {
return isAvailable;
}
public boolean isExperimental() {
return isExperimental;
public int getThumbsUpCount() {
return thumbsUpCount;
}
@Override
public List<RemoteMod> loadDependencies() throws IOException {
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
Set<Integer> 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<RemoteMod> 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<RemoteMod.Version> loadVersions() throws IOException {
return CurseForgeRemoteModRepository.MODS.getRemoteVersionsById(Integer.toString(id));
public Stream<RemoteMod.Version> 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 <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
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<LatestFileHash> 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<String> gameVersions;
private final List<Dependency> dependencies;
private final boolean isAvailable;
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 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<Dependency> dependencies, boolean isAvailable, List<String> 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<LatestFileHash> hashes, Date fileDate, int fileLength, int downloadCount, String downloadUrl, List<String> gameVersions, List<Dependency> 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<LatestFileHash> 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<String> getGameVersions() {
return gameVersions;
}
public List<Dependency> getDependencies() {
return dependencies;
}
public boolean isAvailable() {
return isAvailable;
}
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 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 <a href="https://docs.curseforge.com/#tocS_FileIndex">Schema</a>
*/
@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<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.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<Void> {
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;

View File

@@ -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<CurseAddon> 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<List<CurseAddon>>() {
}.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<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;
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<List<CurseAddon>> 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<Response<List<CurseAddon>>>() {
}.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<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints")
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
.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.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<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
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id + "/files"));
List<CurseAddon.LatestFile> files = JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
}.getType());
return files.stream().map(CurseAddon.LatestFile::toVersion);
Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files")
.header("X-API-KEY", apiKey)
.getJson(new TypeToken<Response<List<CurseAddon.LatestFile>>>() {
}.getType());
return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
}
public List<Category> getCategoriesImpl() throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/category/section/" + section));
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() {
}.getType());
return reorganizeCategories(categories, section);
public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
.header("X-API-KEY", apiKey)
.getJson(new TypeToken<Response<List<CurseAddon.Category>>>() {
}.getType());
return reorganizeCategories(categories.getData(), section);
}
@Override
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) {
List<Category> result = new ArrayList<>();
private List<CurseAddon.Category> reorganizeCategories(List<CurseAddon.Category> categories, int rootId) {
List<CurseAddon.Category> result = new ArrayList<>();
Map<Integer, Category> categoryMap = new HashMap<>();
for (Category category : categories) {
categoryMap.put(category.id, category);
Map<Integer, CurseAddon.Category> 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<Category> 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<Category> 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<Category> 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<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 List<CurseAddon> exactMatches;
private final List<FingerprintMatch> exactMatches;
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.exactMatches = exactMatches;
this.exactFingerprints = exactFingerprints;
@@ -252,7 +285,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
return isCacheBuilt;
}
public List<CurseAddon> getExactMatches() {
public List<FingerprintMatch> getExactMatches() {
return exactMatches;
}
@@ -260,4 +293,31 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
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.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) {

View File

@@ -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<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(
pair("query", searchFilter),
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))
.getJson(new TypeToken<Response<ModResult>>() {
}.getType());
return response.getHits();
}
@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);
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<RemoteMod.Version> 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<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.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<RemoteMod> loadDependencies() throws IOException {
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
return Collections.emptyList();
}
@Override
public Stream<RemoteMod.Version> loadVersions() throws IOException {
return ModrinthRemoteModRepository.INSTANCE.getRemoteVersionsById(getModId());
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
return modRepository.getRemoteVersionsById(getModId());
}
public RemoteMod toMod() {

View File

@@ -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<T> extends Task<T> {
protected CacheRepository repository = CacheRepository.getInstance();
public FetchTask(List<URL> 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());
}

View File

@@ -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<Date>, 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);
}
}
}

View File

@@ -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;
}