Skip to content

Commit f942c23

Browse files
committed
Release view
1 parent 8bea291 commit f942c23

19 files changed

+10944
-4901
lines changed

caches/curseforge_releases.json

+5,796-2,689
Large diffs are not rendered by default.

caches/github_releases.json

+2,082-694
Large diffs are not rendered by default.

caches/modrinth_releases.json

+2,464-1,078
Large diffs are not rendered by default.

index-curseforge-releases.ts

+71-30
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { writeFileSync } from "node:fs";
2-
import {
3-
CurseforgeRelease,
4-
CurseforgeReleaseType,
5-
} from "./lib/releases/CurseforgeRelease";
62
import { coerce, lte } from "semver";
3+
import { CurseforgeRelease, ModLoader, ReleaseAssetType, ReleaseType } from "./lib/releases/types.js";
74

85
function sleep(ms: number): Promise<void> {
96
return new Promise((resolve) => setTimeout(resolve, ms));
107
}
118

9+
function filterTruthy<T>(values: (T|undefined|null)[]): T[] {
10+
return values.filter(Boolean) as T[];
11+
}
12+
13+
export enum CurseforgeReleaseType {
14+
RELEASE = 1,
15+
BETA = 2,
16+
ALPHA = 3,
17+
}
18+
1219
// Describes shape of data observed that CF website API returns for each release
1320
type ReleaseApiRecord = {
1421
totalDownloads: number;
@@ -29,34 +36,52 @@ function getGameVersions(record: ReleaseApiRecord) {
2936
return record.gameVersions.filter((v) => v.match(/\d+\.\d+(\.\d+|)/));
3037
}
3138

32-
function getModLoaders(record: ReleaseApiRecord) {
39+
function getModLoaders(record: ReleaseApiRecord): ModLoader[] {
3340
let loaders = record.gameVersions.map((v) => {
3441
switch (v) {
3542
case "Forge":
36-
return "forge";
43+
return ModLoader.FORGE;
3744
case "NeoForge":
38-
return "neoforge";
45+
return ModLoader.NEOFORGE;
3946
case "Fabric":
40-
return "fabric";
47+
return ModLoader.FABRIC;
4148
default:
4249
// Anything below 1.16.2 had to be Forge
4350
if (v.match(/\d+\.\d+(\.\d+|)/)) {
4451
if (lte(coerce(v) ?? "0.0.0", "1.16.1")) {
45-
return "forge";
52+
return ModLoader.FORGE;
4653
}
4754
}
4855
}
4956
});
5057

51-
loaders = loaders.filter(Boolean); // Filter out undefined values
58+
const actualLoaders = filterTruthy(loaders); // Filter out undefined values
5259

53-
if (loaders.length === 0) {
60+
if (actualLoaders.length === 0) {
5461
console.warn(
5562
"Failed to determine the mod loader for Curseforge release %s",
5663
record.displayName
5764
);
5865
}
59-
return loaders;
66+
return actualLoaders;
67+
}
68+
69+
function convertReleaseType(releaseType: CurseforgeReleaseType): ReleaseType {
70+
switch (releaseType) {
71+
case CurseforgeReleaseType.BETA:
72+
return ReleaseType.BETA;
73+
case CurseforgeReleaseType.ALPHA:
74+
return ReleaseType.ALPHA;
75+
default:
76+
return ReleaseType.STABLE;
77+
}
78+
}
79+
80+
function guessModVersionFromFilename(filename: string): string | undefined {
81+
const m = filename.match(
82+
/^appliedenergistics2-(?:forge-|fabric-|)(.+)\.jar$/
83+
);
84+
return m ? m[1] : undefined;
6085
}
6186

6287
async function fetchReleases(): Promise<CurseforgeRelease[]> {
@@ -74,17 +99,17 @@ async function fetchReleases(): Promise<CurseforgeRelease[]> {
7499
const response = await fetch(url, {
75100
headers: {
76101
"User-Agent":
77-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0",
78-
},
102+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"
103+
}
79104
});
80105

81106
if (!response.ok) {
82107
throw new Error(
83108
"Failed to fetch. " +
84-
response.status +
85-
" (" +
86-
(await response.text()) +
87-
")"
109+
response.status +
110+
" (" +
111+
(await response.text()) +
112+
")"
88113
);
89114
}
90115

@@ -112,24 +137,40 @@ async function fetchReleases(): Promise<CurseforgeRelease[]> {
112137
}
113138
}
114139

115-
return data.map((record: ReleaseApiRecord) => ({
116-
id: record.id,
117-
filename: record.fileName,
118-
fileSize: record.fileLength,
119-
displayName: record.displayName,
120-
type: record.releaseType,
121-
gameVersions: getGameVersions(record),
122-
modLoaders: getModLoaders(record),
123-
published: record.dateCreated,
124-
totalDownloads: record.totalDownloads,
125-
}));
140+
return data.flatMap((record: ReleaseApiRecord) => {
141+
const modVersion = guessModVersionFromFilename(record.fileName);
142+
if (!modVersion) {
143+
console.warn("Cannot deduce Mod version from Curseforge filename: %s", record.fileName);
144+
return [];
145+
}
146+
147+
return [{
148+
source: "curseforge",
149+
id: String(record.id),
150+
url: "https://www.curseforge.com/minecraft/mc-mods/applied-energistics-2/files/" + record.id,
151+
modVersion,
152+
releaseType: convertReleaseType(record.releaseType),
153+
gameVersions: getGameVersions(record),
154+
modLoaders: getModLoaders(record),
155+
published: new Date(record.dateCreated).getTime(),
156+
totalDownloads: record.totalDownloads,
157+
assets: {
158+
[ReleaseAssetType.MOD]: {
159+
filename: record.fileName,
160+
size: record.fileLength,
161+
browser_download_url: "https://www.curseforge.com/minecraft/mc-mods/applied-energistics-2/download/" + record.id
162+
}
163+
}
164+
} satisfies CurseforgeRelease
165+
];
166+
});
126167
}
127168

128169
const releases = await fetchReleases();
129170
writeFileSync(
130171
"caches/curseforge_releases.json",
131172
JSON.stringify(releases, null, 2),
132173
{
133-
encoding: "utf-8",
174+
encoding: "utf-8"
134175
}
135176
);

index-github-releases.ts

+62-57
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ import { paginateRest } from "@octokit/plugin-paginate-rest";
55
import extractModMetadata from "./lib/releases/extractModMetadata.js";
66
import getMinecraftVersions from "./lib/releases/getMinecraftVersions.js";
77
import {
8+
GithubRelease,
89
GithubReleaseCache,
910
ModLoader,
10-
ReleaseAssetType,
11+
ModReleaseAsset,
12+
ReleaseAssetType, ReleaseType
1113
} from "./lib/releases/types.js";
12-
import {
13-
CachedGithubAsset,
14-
CachedGithubRelease,
15-
} from "./lib/releases/GithubRelease.js";
1614

17-
type GithubRelease = components["schemas"]["release"];
18-
type GithubReleaseAsset = components["schemas"]["release-asset"];
15+
type ApiGithubRelease = components["schemas"]["release"];
16+
type ApiGithubReleaseAsset = components["schemas"]["release-asset"];
1917

2018
const githubToken = process.env.GITHUB_TOKEN;
2119
if (typeof githubToken !== "string") {
@@ -25,7 +23,7 @@ if (typeof githubToken !== "string") {
2523
const PaginatingOctokit = Octokit.plugin(paginateRest);
2624
const octokit = new PaginatingOctokit({
2725
auth: githubToken,
28-
userAgent: "AE2-Release-Indexer",
26+
userAgent: "AE2-Release-Indexer"
2927
});
3028

3129
const owner = "AppliedEnergistics";
@@ -35,7 +33,7 @@ const tagPatterns: [RegExp, ModLoader[]][] = [
3533
[/^fabric\/v([0-9].*)$/, [ModLoader.FABRIC]],
3634
[/^forge\/v([0-9].*)$/, [ModLoader.FORGE]],
3735
[/^v([0-9].*)$/, [ModLoader.FORGE]],
38-
[/^(rv.*)/, [ModLoader.FORGE]],
36+
[/^(rv.*)/, [ModLoader.FORGE]]
3937
];
4038

4139
async function listReleases(): Promise<
@@ -44,7 +42,7 @@ async function listReleases(): Promise<
4442
const options = octokit.rest.repos.listReleases.endpoint.merge({
4543
owner,
4644
repo,
47-
per_page: 100,
45+
per_page: 100
4846
});
4947
return await octokit.paginate(options);
5048
}
@@ -77,16 +75,16 @@ const jarSuffixToAssetType: [string, ReleaseAssetType][] = [
7775
["-javadoc.jar", ReleaseAssetType.API],
7876
["-api.jar", ReleaseAssetType.API],
7977
["-dev.jar", ReleaseAssetType.UNOBF],
80-
[".jar", ReleaseAssetType.MOD],
78+
[".jar", ReleaseAssetType.MOD]
8179
];
8280

8381
/**
8482
* Try to find the mod jar among the release assets and download it.
8583
*/
8684
function classifyReleaseAssets(
87-
assets: GithubReleaseAsset[]
88-
): Partial<Record<ReleaseAssetType, GithubReleaseAsset>> {
89-
const result: Partial<Record<ReleaseAssetType, GithubReleaseAsset>> = {};
85+
assets: ApiGithubReleaseAsset[]
86+
): Partial<Record<ReleaseAssetType, ApiGithubReleaseAsset>> {
87+
const result: Partial<Record<ReleaseAssetType, ApiGithubReleaseAsset>> = {};
9088

9189
const assetsByName = Object.fromEntries(
9290
assets.map((asset) => [asset.name, asset])
@@ -131,7 +129,7 @@ function classifyReleaseAssets(
131129

132130
async function processRelease(
133131
cache: GithubReleaseCache,
134-
release: GithubRelease,
132+
release: ApiGithubRelease,
135133
allMinecraftVersions: string[]
136134
) {
137135
const { tag_name: tagName } = release;
@@ -140,55 +138,63 @@ async function processRelease(
140138
return;
141139
}
142140

143-
let cachedData = cache.get(tagName);
144-
if (!cachedData) {
145-
cachedData = {
146-
tagName,
147-
url: release.html_url,
148-
assets: {},
149-
};
150-
}
151-
141+
let oldCachedData = cache.get(tagName);
152142
const assetsByType = classifyReleaseAssets(release.assets);
153143

154144
// Update basic release properties we can gather from the top-level listing
155-
cachedData.tagName = tagName;
156-
cachedData.url = release.html_url;
157-
cachedData.assets = Object.fromEntries(
145+
const assets: GithubRelease["assets"] = Object.fromEntries(
158146
Object.entries(assetsByType).map(([type, asset]) => [
159147
type,
160148
{
161149
filename: asset.name,
162150
size: asset.size,
163151
browser_download_url: asset.browser_download_url,
164-
url: asset.url,
165-
} as CachedGithubAsset,
152+
url: asset.url
153+
} satisfies ModReleaseAsset
166154
])
167155
);
168-
cachedData.published = release.published_at ?? undefined;
169-
cachedData.changelog = release.body?.replaceAll("\r\n", "\n") ?? undefined;
170-
cache.set(tagName, cachedData);
171-
172-
// Update Mod metadata if it's missing
173-
if (
174-
!cachedData.version ||
175-
!cachedData.minecraftVersions ||
176-
!cachedData.modLoaders
177-
) {
178-
// Try deducing a version from the tag first, which will then be overwritten by the mod-data if successful
179-
for (const [pattern, loaders] of tagPatterns) {
180-
const m = tagName.match(pattern);
181-
if (m) {
182-
cachedData.version = m[1];
183-
// rv.beta.1 is actually versioned rv-beta-1 in the mod metadata
184-
if (tagName.match(/^(rv.*)/)) {
185-
cachedData.version = cachedData.version.replaceAll(".", "-");
186-
}
187-
cachedData.modLoaders = loaders.slice();
188-
break;
156+
157+
// Try deducing a version from the tag first, which will then be overwritten by the mod-data if successful
158+
let modVersion: string | undefined;
159+
let modLoaders: ModLoader[] | undefined;
160+
for (const [pattern, loaders] of tagPatterns) {
161+
const m = tagName.match(pattern);
162+
if (m) {
163+
modVersion = m[1];
164+
// rv.beta.1 is actually versioned rv-beta-1 in the mod metadata
165+
if (tagName.match(/^(rv.*)/)) {
166+
modVersion = modVersion.replaceAll(".", "-");
189167
}
168+
modLoaders = loaders.slice();
169+
break;
190170
}
171+
}
191172

173+
if (!modVersion || !modLoaders) {
174+
console.warn("Failed to determine mod version from tag name: %s", tagName);
175+
return;
176+
}
177+
178+
let releaseType = ReleaseType.STABLE;
179+
180+
181+
const releaseInfo: GithubRelease = {
182+
...oldCachedData,
183+
source: "github",
184+
modVersion,
185+
modLoaders,
186+
releaseType,
187+
gameVersions: [],
188+
url: release.html_url,
189+
tagName,
190+
published: new Date(release.published_at ?? release.created_at).getTime(),
191+
changelog: release.body?.replaceAll("\r\n", "\n") ?? undefined,
192+
assets
193+
};
194+
cache.set(tagName, releaseInfo);
195+
196+
// Try updating mod metadata if it's missing
197+
if (!releaseInfo.gameVersions || !releaseInfo.modLoaders) {
192198
const modJarAsset = assetsByType[ReleaseAssetType.MOD];
193199
if (!modJarAsset) {
194200
console.warn(
@@ -200,15 +206,14 @@ async function processRelease(
200206

201207
const { data } = await octokit.request(modJarAsset.url, {
202208
headers: {
203-
accept: "application/octet-stream",
204-
},
209+
accept: "application/octet-stream"
210+
}
205211
});
206212
const modMetadata = extractModMetadata(data, allMinecraftVersions);
207-
cachedData.version = modMetadata.modVersion;
208213
// Remove anything that is not in the version list. Sometimes this includes "Java" or "Forge"
209-
cachedData.minecraftVersions = modMetadata.minecraftVersions;
210-
cachedData.modLoaders = modMetadata.modLoaders;
211-
cache.set(tagName, cachedData);
214+
releaseInfo.gameVersions = modMetadata.minecraftVersions;
215+
releaseInfo.modLoaders = modMetadata.modLoaders;
216+
cache.set(tagName, releaseInfo);
212217
}
213218
}
214219

@@ -221,7 +226,7 @@ console.info(
221226
const releases = await listReleases();
222227
console.info("Read %d releases", releases.length);
223228

224-
const cache = new PersistentCache<CachedGithubRelease>(
229+
const cache = new PersistentCache<GithubRelease>(
225230
"caches/github_releases.json"
226231
);
227232
try {

0 commit comments

Comments
 (0)