Skip to content

Commit d14887b

Browse files
authored
fix: Detect Curseforge versions using indexed versions instead of querying paginated endpoint (#54)
1 parent 89b0d91 commit d14887b

2 files changed

Lines changed: 50 additions & 30 deletions

File tree

cli/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,15 @@ async function getMods(modQueryService: ModQueryService, options: CliOptions): P
8989
continue;
9090
}
9191

92+
// Rebuild mod ID to ensure user input doesn't mess things up
9293
let fullId = `${repo.getRepositoryName()}/${modId}`.toLowerCase();
94+
95+
// Add mod to map
9396
if (modsMap.has(fullId)) {
9497
logger.warn(`Duplicate mod ID from --mod-id: ${id}`);
9598
} else {
9699
modsMap.set(fullId, [{
97-
id,
100+
id: modId,
98101
repository: repo.getRepositoryName(),
99102
name: id,
100103
homepageURL: "",

mclib/src/repos/CurseForgeRepository.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import type { IRepository } from "./IRepository";
2-
import { RawModRepoRelease, ModRepositoryName, ModLoader, ModRepoMetadata, MCVersion, ModLoaderUtil } from "..";
2+
import { RawModRepoRelease, ModRepositoryName, ModLoader, ModRepoMetadata, MCVersion } from "..";
33
import { cf_fingerprint } from 'cf-fingerprint';
44
import { logger } from "../logger";
55

6+
// Translation map for Curseforge modloader IDs
7+
// Source: https://docs.curseforge.com/rest-api/#tocS_ModLoaderType
8+
const LOADERS: { [key: number]: ModLoader } = {
9+
1: ModLoader.FORGE,
10+
4: ModLoader.FABRIC,
11+
5: ModLoader.QUILT,
12+
6: ModLoader.NEOFORGE
13+
}
14+
615
/**
716
* Implementation of IRepository for the CurseForge repository.
817
*/
@@ -21,37 +30,22 @@ export class CurseForgeRepository implements IRepository {
2130
}
2231

2332
async getModReleases(modId: string): Promise<RawModRepoRelease[]> {
24-
type Data = {
25-
data: {
26-
displayName: string;
27-
gameVersions: string[];
28-
downloadUrl: string;
29-
}[];
30-
};
33+
const modInfo = await this.fetchModInfo(Number(modId));
34+
if (!modInfo || !modInfo.latestFilesIndexes) return [];
3135

32-
const filesResp = await this.fetchClient(`${CurseForgeRepository.BASE_URL}/mods/${modId}/files`);
33-
if (!filesResp.ok) throw new Error("Could not fetch files from CurseForge");
34-
const jsonResp: Data = await filesResp.json();
35-
36-
const releases: RawModRepoRelease[] = jsonResp.data.map(file => {
36+
const releases: RawModRepoRelease[] = modInfo.latestFilesIndexes.map(file => {
3737
const mcVersions: Set<MCVersion> = new Set();
3838
const loaders: Set<ModLoader> = new Set();
39-
for (let gameVersion of file.gameVersions || []) {
40-
gameVersion = gameVersion.toLowerCase();
41-
if (/^[a-z]+$/.test(gameVersion)) {
42-
loaders.add(ModLoaderUtil.from(gameVersion));
43-
} else {
44-
mcVersions.add(gameVersion);
45-
}
46-
}
47-
48-
return ({
39+
if (file.gameVersion) mcVersions.add(file.gameVersion);
40+
if (file.modLoader && LOADERS[file.modLoader]) loaders.add(LOADERS[file.modLoader]);
41+
42+
return {
4943
mcVersions: mcVersions,
50-
modVersion: file.displayName,
44+
modVersion: file.filename,
5145
repository: ModRepositoryName.CURSEFORGE,
5246
loaders: loaders,
53-
downloadUrl: file.downloadUrl || '',
54-
})
47+
downloadUrl: '', // No downloadUrl in latestFilesIndexes, could be fetched if needed
48+
};
5549
});
5650

5751
return releases;
@@ -95,6 +89,12 @@ export class CurseForgeRepository implements IRepository {
9589
}));
9690
}
9791

92+
private async fetchModInfo(modId: number): Promise<ModInfoData | null> {
93+
const modResp = await this.fetchClient(`${CurseForgeRepository.BASE_URL}/mods/${modId}`);
94+
if (!modResp.ok) return null;
95+
return (await modResp.json()).data as ModInfoData;
96+
}
97+
9898
async getByDataHash(modData: Uint8Array): Promise<ModRepoMetadata | null> {
9999
// Calculate CurseForge fingerprint
100100
const start = Date.now();
@@ -124,9 +124,8 @@ export class CurseForgeRepository implements IRepository {
124124
const modId: number = fileMatch.file.modId;
125125

126126
// Get mod info using the mod ID
127-
const modResp = await this.fetchClient(`${CurseForgeRepository.BASE_URL}/mods/${modId}`);
128-
if (!modResp.ok) return null;
129-
const modInfo = (await modResp.json()).data;
127+
const modInfo = await this.fetchModInfo(modId);
128+
if (!modInfo) return null;
130129

131130
return {
132131
id: modId.toString(),
@@ -142,3 +141,21 @@ export class CurseForgeRepository implements IRepository {
142141
return ModRepositoryName.CURSEFORGE;
143142
}
144143
}
144+
145+
type ModInfoData = {
146+
id: number;
147+
name: string;
148+
links: {
149+
websiteUrl: string;
150+
};
151+
logo?: {
152+
url?: string;
153+
};
154+
downloadCount?: number;
155+
latestFilesIndexes?: {
156+
gameVersion: string;
157+
fileId: number;
158+
filename: string;
159+
modLoader?: number;
160+
}[];
161+
};

0 commit comments

Comments
 (0)