@kuriyona/cecilia 1.0.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1
- package/dist/elysia.cjs +288 -0
- package/dist/elysia.d.cts +37 -0
- package/dist/elysia.d.mts +37 -0
- package/dist/elysia.mjs +286 -0
- package/dist/index.cjs +41 -1160
- package/dist/index.mjs +1 -1120
- package/dist/src-BoyQdkLi.cjs +1360 -0
- package/dist/src-_5V0pZj_.mjs +1121 -0
- package/package.json +26 -1
package/dist/elysia.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { A as getArtistTopSongs, C as getArtist, D as getArtistList, E as getArtistDetail, F as getAlbumPrivileges, I as getAlbumProduct, L as getAlbumSaleBoard, M as getAlbum, N as getAlbumDynamic, O as getArtistMvs, P as getAlbumList, R as getNewestAlbums, S as getSongsDetail, T as getArtistDesc, _ as checkMusic, a as getHotSearches, b as getSimilarSongs, c as searchMultimatch, d as getPlaylistCategories, f as getPlaylistDetail, g as getTopPlaylists, h as getRelatedPlaylists, i as getHotSearchDetail, j as getArtistVideos, k as getArtistSongs, l as getHighQualityPlaylists, m as getPlaylistTracks, n as cloudSearch, o as getSearchSuggest, p as getPlaylistDetailDynamic, r as getDefaultSearchKeyword, s as search, u as getHighQualityTags, v as getLyric, w as getArtistAlbums, x as getSongUrl, y as getLyricNew } from "./src-_5V0pZj_.mjs";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { cors } from "@elysiajs/cors";
|
|
4
|
+
import { Elysia, t } from "elysia";
|
|
5
|
+
//#region src/elysia/routes.ts
|
|
6
|
+
/** 入站 Cookie 原样转发给上游,会员态由调用方自带。 */
|
|
7
|
+
const optionsOf = (request) => {
|
|
8
|
+
const cookie = request.headers.get("cookie");
|
|
9
|
+
return cookie === null ? void 0 : { cookie };
|
|
10
|
+
};
|
|
11
|
+
/** 逗号分隔的数字 id 列表,`1,2,3`。 */
|
|
12
|
+
const idListSchema = t.String({ pattern: "^[0-9]+(,[0-9]+)*$" });
|
|
13
|
+
const idsOf = (value) => value.split(",").map(Number);
|
|
14
|
+
const pagingSchema = {
|
|
15
|
+
limit: t.Optional(t.Numeric()),
|
|
16
|
+
offset: t.Optional(t.Numeric())
|
|
17
|
+
};
|
|
18
|
+
/** 与 src/types/search.ts 的 SearchType 保持同步。 */
|
|
19
|
+
const SEARCH_TYPES = [
|
|
20
|
+
1,
|
|
21
|
+
10,
|
|
22
|
+
100,
|
|
23
|
+
1e3,
|
|
24
|
+
1002,
|
|
25
|
+
1004,
|
|
26
|
+
1006,
|
|
27
|
+
1009,
|
|
28
|
+
1014,
|
|
29
|
+
2e3
|
|
30
|
+
];
|
|
31
|
+
const searchTypeOf = (value) => value === void 0 ? void 0 : SEARCH_TYPES.find((candidate) => candidate === value);
|
|
32
|
+
/** 白名单外的取值返回 undefined,由调用方回 400。 */
|
|
33
|
+
const pickEnum = (value, allowed) => value === void 0 ? void 0 : allowed.find((candidate) => candidate === value);
|
|
34
|
+
const invalidEnum = (field, allowed) => ({
|
|
35
|
+
code: 400,
|
|
36
|
+
message: `${field} 只支持 ${allowed.join(" / ")}`
|
|
37
|
+
});
|
|
38
|
+
const TOP_PLAYLIST_ORDERS = ["hot", "new"];
|
|
39
|
+
const ARTIST_SONG_ORDERS = ["hot", "time"];
|
|
40
|
+
const ALBUM_SALE_TYPES = [
|
|
41
|
+
"daily",
|
|
42
|
+
"week",
|
|
43
|
+
"year",
|
|
44
|
+
"total"
|
|
45
|
+
];
|
|
46
|
+
const ALBUM_TYPES = ["0", "1"];
|
|
47
|
+
/** Query 里 `albumType` 是字符串,窄化回 0 | 1。 */
|
|
48
|
+
const albumTypeOf = (value) => value === void 0 ? void 0 : value === "0" ? 0 : 1;
|
|
49
|
+
const searchRoutes = (app) => app.get("/search", ({ query, request, set }) => {
|
|
50
|
+
const type = searchTypeOf(query.type);
|
|
51
|
+
if (query.type !== void 0 && type === void 0) {
|
|
52
|
+
set.status = 400;
|
|
53
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
54
|
+
}
|
|
55
|
+
return search({
|
|
56
|
+
keywords: query.keywords,
|
|
57
|
+
type,
|
|
58
|
+
limit: query.limit,
|
|
59
|
+
offset: query.offset
|
|
60
|
+
}, optionsOf(request));
|
|
61
|
+
}, { query: t.Object({
|
|
62
|
+
keywords: t.String({ minLength: 1 }),
|
|
63
|
+
type: t.Optional(t.Numeric()),
|
|
64
|
+
...pagingSchema
|
|
65
|
+
}) }).get("/cloudsearch", ({ query, request, set }) => {
|
|
66
|
+
const type = searchTypeOf(query.type);
|
|
67
|
+
if (query.type !== void 0 && type === void 0) {
|
|
68
|
+
set.status = 400;
|
|
69
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
70
|
+
}
|
|
71
|
+
return cloudSearch({
|
|
72
|
+
keywords: query.keywords,
|
|
73
|
+
type,
|
|
74
|
+
limit: query.limit,
|
|
75
|
+
offset: query.offset
|
|
76
|
+
}, optionsOf(request));
|
|
77
|
+
}, { query: t.Object({
|
|
78
|
+
keywords: t.String({ minLength: 1 }),
|
|
79
|
+
type: t.Optional(t.Numeric()),
|
|
80
|
+
...pagingSchema
|
|
81
|
+
}) }).get("/search/suggest", ({ query, request }) => getSearchSuggest({
|
|
82
|
+
keywords: query.keywords,
|
|
83
|
+
mobile: query.mobile
|
|
84
|
+
}, optionsOf(request)), { query: t.Object({
|
|
85
|
+
keywords: t.Optional(t.String()),
|
|
86
|
+
mobile: t.Optional(t.BooleanString())
|
|
87
|
+
}) }).get("/search/hot", ({ request }) => getHotSearches(optionsOf(request))).get("/search/hot/detail", ({ request }) => getHotSearchDetail(optionsOf(request))).get("/search/default-keyword", ({ request }) => getDefaultSearchKeyword(optionsOf(request))).get("/search/multimatch", ({ query, request, set }) => {
|
|
88
|
+
const type = searchTypeOf(query.type);
|
|
89
|
+
if (query.type !== void 0 && type === void 0) {
|
|
90
|
+
set.status = 400;
|
|
91
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
92
|
+
}
|
|
93
|
+
return searchMultimatch({
|
|
94
|
+
keywords: query.keywords,
|
|
95
|
+
type
|
|
96
|
+
}, optionsOf(request));
|
|
97
|
+
}, { query: t.Object({
|
|
98
|
+
keywords: t.Optional(t.String()),
|
|
99
|
+
type: t.Optional(t.Numeric())
|
|
100
|
+
}) });
|
|
101
|
+
const songRoutes = (app) => app.get("/song/detail", ({ query, request }) => getSongsDetail(idsOf(query.ids), optionsOf(request)), { query: t.Object({ ids: idListSchema }) }).get("/song/url", ({ query, request }) => getSongUrl({
|
|
102
|
+
id: idsOf(query.id),
|
|
103
|
+
br: query.br
|
|
104
|
+
}, optionsOf(request)), { query: t.Object({
|
|
105
|
+
id: idListSchema,
|
|
106
|
+
br: t.Optional(t.Numeric())
|
|
107
|
+
}) }).get("/lyric", ({ query, request }) => getLyric(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/lyric/new", ({ query, request }) => getLyricNew(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/check/music", ({ query, request }) => checkMusic({
|
|
108
|
+
id: query.id,
|
|
109
|
+
br: query.br
|
|
110
|
+
}, optionsOf(request)), { query: t.Object({
|
|
111
|
+
id: t.Numeric(),
|
|
112
|
+
br: t.Optional(t.Numeric())
|
|
113
|
+
}) }).get("/simi/song", ({ query, request }) => getSimilarSongs(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) });
|
|
114
|
+
const playlistRoutes = (app) => app.get("/playlist/detail", ({ query, request }) => getPlaylistDetail(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/playlist/track/all", ({ query, request }) => getPlaylistTracks({
|
|
115
|
+
id: query.id,
|
|
116
|
+
limit: query.limit,
|
|
117
|
+
offset: query.offset
|
|
118
|
+
}, optionsOf(request)), { query: t.Object({
|
|
119
|
+
id: t.Numeric(),
|
|
120
|
+
...pagingSchema
|
|
121
|
+
}) }).get("/playlist/detail/dynamic", ({ query, request }) => getPlaylistDetailDynamic(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/playlist/highquality/tags", ({ request }) => getHighQualityTags(optionsOf(request))).get("/playlist/top", ({ query, request, set }) => {
|
|
122
|
+
const order = pickEnum(query.order, TOP_PLAYLIST_ORDERS);
|
|
123
|
+
if (query.order !== void 0 && order === void 0) {
|
|
124
|
+
set.status = 400;
|
|
125
|
+
return invalidEnum("order", TOP_PLAYLIST_ORDERS);
|
|
126
|
+
}
|
|
127
|
+
return getTopPlaylists({
|
|
128
|
+
cat: query.cat,
|
|
129
|
+
order,
|
|
130
|
+
limit: query.limit,
|
|
131
|
+
offset: query.offset
|
|
132
|
+
}, optionsOf(request));
|
|
133
|
+
}, { query: t.Object({
|
|
134
|
+
cat: t.Optional(t.String()),
|
|
135
|
+
order: t.Optional(t.String()),
|
|
136
|
+
...pagingSchema
|
|
137
|
+
}) }).get("/playlist/highquality/list", ({ query, request }) => getHighQualityPlaylists({
|
|
138
|
+
cat: query.cat,
|
|
139
|
+
limit: query.limit,
|
|
140
|
+
before: query.before
|
|
141
|
+
}, optionsOf(request)), { query: t.Object({
|
|
142
|
+
cat: t.Optional(t.String()),
|
|
143
|
+
limit: t.Optional(t.Numeric()),
|
|
144
|
+
before: t.Optional(t.Numeric())
|
|
145
|
+
}) }).get("/playlist/catalogue", ({ request }) => getPlaylistCategories(optionsOf(request))).get("/playlist/related", ({ query, request }) => getRelatedPlaylists(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) });
|
|
146
|
+
const artistRoutes = (app) => app.get("/artist", ({ query, request }) => getArtist(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/artist/detail", ({ query, request }) => getArtistDetail(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/artist/songs", ({ query, request, set }) => {
|
|
147
|
+
const order = pickEnum(query.order, ARTIST_SONG_ORDERS);
|
|
148
|
+
if (query.order !== void 0 && order === void 0) {
|
|
149
|
+
set.status = 400;
|
|
150
|
+
return invalidEnum("order", ARTIST_SONG_ORDERS);
|
|
151
|
+
}
|
|
152
|
+
return getArtistSongs({
|
|
153
|
+
id: query.id,
|
|
154
|
+
order,
|
|
155
|
+
limit: query.limit,
|
|
156
|
+
offset: query.offset
|
|
157
|
+
}, optionsOf(request));
|
|
158
|
+
}, { query: t.Object({
|
|
159
|
+
id: t.Numeric(),
|
|
160
|
+
order: t.Optional(t.String()),
|
|
161
|
+
...pagingSchema
|
|
162
|
+
}) }).get("/artist/top/song", ({ query, request }) => getArtistTopSongs(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/artist/album", ({ query, request }) => getArtistAlbums({
|
|
163
|
+
id: query.id,
|
|
164
|
+
limit: query.limit,
|
|
165
|
+
offset: query.offset
|
|
166
|
+
}, optionsOf(request)), { query: t.Object({
|
|
167
|
+
id: t.Numeric(),
|
|
168
|
+
...pagingSchema
|
|
169
|
+
}) }).get("/artist/list", ({ query, request }) => getArtistList({
|
|
170
|
+
area: query.area,
|
|
171
|
+
type: query.type,
|
|
172
|
+
initial: query.initial,
|
|
173
|
+
limit: query.limit,
|
|
174
|
+
offset: query.offset
|
|
175
|
+
}, optionsOf(request)), { query: t.Object({
|
|
176
|
+
area: t.Optional(t.Numeric()),
|
|
177
|
+
type: t.Optional(t.Numeric()),
|
|
178
|
+
initial: t.Optional(t.String()),
|
|
179
|
+
...pagingSchema
|
|
180
|
+
}) }).get("/artist/desc", ({ query, request }) => getArtistDesc(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/artist/mv", ({ query, request }) => getArtistMvs({
|
|
181
|
+
id: query.id,
|
|
182
|
+
limit: query.limit,
|
|
183
|
+
offset: query.offset
|
|
184
|
+
}, optionsOf(request)), { query: t.Object({
|
|
185
|
+
id: t.Numeric(),
|
|
186
|
+
...pagingSchema
|
|
187
|
+
}) }).get("/artist/video", ({ query, request }) => getArtistVideos({
|
|
188
|
+
id: query.id,
|
|
189
|
+
size: query.size,
|
|
190
|
+
cursor: query.cursor,
|
|
191
|
+
order: query.order
|
|
192
|
+
}, optionsOf(request)), { query: t.Object({
|
|
193
|
+
id: t.Numeric(),
|
|
194
|
+
size: t.Optional(t.Numeric()),
|
|
195
|
+
cursor: t.Optional(t.Numeric()),
|
|
196
|
+
order: t.Optional(t.Numeric())
|
|
197
|
+
}) });
|
|
198
|
+
const albumRoutes = (app) => app.get("/album", ({ query, request }) => getAlbum(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/album/product", ({ query, request }) => getAlbumProduct(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/album/dynamic", ({ query, request }) => getAlbumDynamic(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/album/sale/board", ({ query, request, set }) => {
|
|
199
|
+
const albumType = pickEnum(query.albumType, ALBUM_TYPES);
|
|
200
|
+
if (query.albumType !== void 0 && albumType === void 0) {
|
|
201
|
+
set.status = 400;
|
|
202
|
+
return invalidEnum("albumType", ALBUM_TYPES);
|
|
203
|
+
}
|
|
204
|
+
const type = pickEnum(query.type, ALBUM_SALE_TYPES);
|
|
205
|
+
if (query.type !== void 0 && type === void 0) {
|
|
206
|
+
set.status = 400;
|
|
207
|
+
return invalidEnum("type", ALBUM_SALE_TYPES);
|
|
208
|
+
}
|
|
209
|
+
return getAlbumSaleBoard({
|
|
210
|
+
albumType: albumTypeOf(albumType),
|
|
211
|
+
type,
|
|
212
|
+
year: query.year
|
|
213
|
+
}, optionsOf(request));
|
|
214
|
+
}, { query: t.Object({
|
|
215
|
+
albumType: t.Optional(t.String()),
|
|
216
|
+
type: t.Optional(t.String()),
|
|
217
|
+
year: t.Optional(t.Numeric())
|
|
218
|
+
}) }).get("/album/privilege", ({ query, request }) => getAlbumPrivileges(query.id, optionsOf(request)), { query: t.Object({ id: t.Numeric() }) }).get("/album/list", ({ query, request }) => getAlbumList({
|
|
219
|
+
area: query.area,
|
|
220
|
+
type: query.type,
|
|
221
|
+
limit: query.limit,
|
|
222
|
+
offset: query.offset
|
|
223
|
+
}, optionsOf(request)), { query: t.Object({
|
|
224
|
+
area: t.Optional(t.String()),
|
|
225
|
+
type: t.Optional(t.String()),
|
|
226
|
+
...pagingSchema
|
|
227
|
+
}) }).get("/album/new", ({ request }) => getNewestAlbums(optionsOf(request)));
|
|
228
|
+
/** 37 个函数各一条 GET 路由,按 搜索 / 歌曲 / 歌单 / 歌手 / 专辑 依次注册。 */
|
|
229
|
+
const registerRoutes = (app) => albumRoutes(artistRoutes(playlistRoutes(songRoutes(searchRoutes(app)))));
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/elysia/app.ts
|
|
232
|
+
/** scheme 大小写不敏感,比较用 timingSafeEqual(长度不等直接 false)。 */
|
|
233
|
+
const isAuthorized = (request, token) => {
|
|
234
|
+
const header = request.headers.get("authorization");
|
|
235
|
+
if (header === null) return false;
|
|
236
|
+
const [scheme, value, ...rest] = header.split(" ");
|
|
237
|
+
if (rest.length > 0 || scheme?.toLowerCase() !== "bearer" || value === void 0) return false;
|
|
238
|
+
const expected = Buffer.from(token, "utf8");
|
|
239
|
+
const actual = Buffer.from(value, "utf8");
|
|
240
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
241
|
+
};
|
|
242
|
+
/** 中间件风格插件:挂在实例上后,后续注册的路由都要先过鉴权。 */
|
|
243
|
+
const withAuth = (options) => (app) => app.onBeforeHandle(({ request, set }) => {
|
|
244
|
+
if (isAuthorized(request, options.token)) return;
|
|
245
|
+
set.status = 401;
|
|
246
|
+
return {
|
|
247
|
+
code: 401,
|
|
248
|
+
message: "Unauthorized"
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
/**
|
|
252
|
+
* 把 37 个 Cecilia 函数各注册成一条 GET 路由,返回可直接 `.use()` 或 `.listen()` 的 Elysia 实例。
|
|
253
|
+
* CORS 先注册(预检早于鉴权短路),再鉴权,最后才是路由。
|
|
254
|
+
*/
|
|
255
|
+
const createApp = (options) => {
|
|
256
|
+
let app = new Elysia();
|
|
257
|
+
if (options?.cors !== void 0) app = app.use(cors(options.cors));
|
|
258
|
+
if (options?.auth !== void 0) app = withAuth(options.auth)(app);
|
|
259
|
+
return registerRoutes(app);
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/elysia/server.ts
|
|
263
|
+
const readPortEnv = () => {
|
|
264
|
+
const raw = process.env.PORT;
|
|
265
|
+
if (raw === void 0 || raw === "") return 3e3;
|
|
266
|
+
const port = Number(raw);
|
|
267
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`PORT 环境变量非法: ${raw}`);
|
|
268
|
+
return port;
|
|
269
|
+
};
|
|
270
|
+
/** 同步返回:Bun 的 listen 是同步的,端口占用会同步抛出。 */
|
|
271
|
+
const startServer = (options) => {
|
|
272
|
+
if (!("Bun" in globalThis)) throw new Error("@kuriyona/cecilia/elysia 的 startServer 需要 Bun 运行时(例如 bun run server.ts);Node 下请用 createApp() 自备适配器。");
|
|
273
|
+
const port = options?.port ?? readPortEnv();
|
|
274
|
+
const host = options?.host ?? process.env.HOST ?? "127.0.0.1";
|
|
275
|
+
const app = createApp(options);
|
|
276
|
+
app.listen({
|
|
277
|
+
hostname: host,
|
|
278
|
+
port
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
app,
|
|
282
|
+
url: app.server?.url.origin ?? `http://${host}:${port}`
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
export { createApp, startServer };
|