Add Modrinth API client with version and tag endpoints

This commit is contained in:
CPTProgrammer 2026-07-06 21:47:38 +08:00
parent cb05223145
commit 376bba5b75
No known key found for this signature in database
4 changed files with 209 additions and 0 deletions

View File

@ -0,0 +1,50 @@
/** Modrinth API v2 的基础请求客户端,封装鉴权与 User-Agent。 */
export class ModrinthClient {
private base = "https://api.modrinth.com/v2";
private token: string;
private userAgent: string;
constructor(token: string, userAgent?: string) {
this.token = token;
this.userAgent = userAgent ?? "";
}
/** 构造通用请求头Authorization + User-Agent。 */
private headers(): Record<string, string> {
return {
Authorization: this.token,
"User-Agent": this.userAgent,
};
}
/** 发送 GET 请求并解析 JSON 响应体。 */
async get<T>(path: string): Promise<T> {
const res = await fetch(`${this.base}${path}`, {
headers: this.headers(),
});
if (!res.ok) {
throw new Error(
`Modrinth API GET ${path} failed: ${res.status} ${res.statusText}`,
);
}
return res.json() as Promise<T>;
}
/**
* POST body FormDatamultipart/form-data
* Content-Type fetch boundary
*/
async post<T>(path: string, body: FormData): Promise<T> {
const res = await fetch(`${this.base}${path}`, {
method: "POST",
headers: this.headers(),
body,
});
if (!res.ok) {
throw new Error(
`Modrinth API POST ${path} failed: ${res.status} ${res.statusText}`,
);
}
return res.json() as Promise<T>;
}
}

View File

@ -0,0 +1,9 @@
export { ModrinthClient } from "./client";
export {
listVersions,
getVersion,
getVersionByNumber,
getVersions,
createVersion,
} from "./version";
export { getLoaders, getGameVersions } from "./tag";

29
src/lib/modrinth/tag.ts Normal file
View File

@ -0,0 +1,29 @@
import { ModrinthClient } from "./client";
import {
LoaderTagSchema,
GameVersionTagSchema,
type LoaderTag,
type GameVersionTag,
} from "@/types";
/**
* Modrinth
* GET /tag/loader
*/
export async function getLoaders(
client: ModrinthClient,
): Promise<LoaderTag[]> {
const data = await client.get<unknown[]>("/tag/loader");
return LoaderTagSchema.array().parse(data);
}
/**
* Modrinth Minecraft
* GET /tag/game_version
*/
export async function getGameVersions(
client: ModrinthClient,
): Promise<GameVersionTag[]> {
const data = await client.get<unknown[]>("/tag/game_version");
return GameVersionTagSchema.array().parse(data);
}

121
src/lib/modrinth/version.ts Normal file
View File

@ -0,0 +1,121 @@
import { ModrinthClient } from "./client";
import {
VersionSchema,
type Version,
type CreatableVersion,
} from "@/types";
/**
*
* GET /project/{id|slug}/version
*/
export async function listVersions(
client: ModrinthClient,
projectId: string,
options?: {
loaders?: string[];
game_versions?: string[];
featured?: boolean;
include_changelog?: boolean;
},
): Promise<Version[]> {
const params = new URLSearchParams();
if (options?.loaders) {
params.set("loaders", JSON.stringify(options.loaders));
}
if (options?.game_versions) {
params.set("game_versions", JSON.stringify(options.game_versions));
}
if (options?.featured !== undefined) {
params.set("featured", String(options.featured));
}
// 强烈建议不带 changelog 以减小响应体
params.set(
"include_changelog",
String(options?.include_changelog ?? false),
);
const qs = params.toString();
const path = `/project/${encodeURIComponent(projectId)}/version${qs ? `?${qs}` : ""}`;
const data = await client.get<unknown[]>(path);
return VersionSchema.array().parse(data);
}
/**
*
* GET /version/{id}
*/
export async function getVersion(
client: ModrinthClient,
versionId: string,
): Promise<Version> {
const path = `/version/${encodeURIComponent(versionId)}`;
const data = await client.get<unknown>(path);
return VersionSchema.parse(data);
}
/**
* ID +
* GET /project/{id|slug}/version/{id|number}
*/
export async function getVersionByNumber(
client: ModrinthClient,
projectId: string,
versionNumber: string,
): Promise<Version> {
const path = `/project/${encodeURIComponent(projectId)}/version/${encodeURIComponent(versionNumber)}`;
const data = await client.get<unknown>(path);
return VersionSchema.parse(data);
}
/**
*
* GET /versions?ids=["...","..."]
*/
export async function getVersions(
client: ModrinthClient,
versionIds: string[],
): Promise<Version[]> {
const params = new URLSearchParams();
params.set("ids", JSON.stringify(versionIds));
const path = `/versions?${params.toString()}`;
const data = await client.get<unknown[]>(path);
return VersionSchema.array().parse(data);
}
/**
*
* POST /version
*
* @param client Modrinth
* @param data Zod CreatableVersion
* @param files key multipart data.file_parts value Blob
*/
export async function createVersion(
client: ModrinthClient,
data: CreatableVersion,
files: Record<string, Blob>,
): Promise<Version> {
const form = new FormData();
// 元数据 JSON 字符串
form.append("data", JSON.stringify(data));
// 逐一添加文件
for (const fieldName of data.file_parts) {
const blob = files[fieldName];
if (!blob) {
throw new Error(
`createVersion: file field "${fieldName}" declared in file_parts but not provided in files`,
);
}
// 从 Blob 推断文件名;若无则使用字段名作为回退
const filename =
blob instanceof File ? blob.name : `${fieldName}.jar`;
form.append(fieldName, blob, filename);
}
const result = await client.post<unknown>("/version", form);
return VersionSchema.parse(result);
}