@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/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
网易云音乐 API 的非官方 TypeScript 封装。自行实现 weapi / eapi 加密与传输层(`node:crypto` + 全局 `fetch`),**不支持浏览器、Edge 等运行时,需要 Node ≥ 18**。
|
|
4
4
|
|
|
5
|
+
可选地,通过 `@kuriyona/cecilia/elysia` 子路径可以把这 37 个函数起成一个 HTTP 服务(各一条 `GET` 路由);该子路径需要额外安装 `elysia` + `@elysiajs/cors`,并**只能在 Bun 下监听**。
|
|
6
|
+
|
|
5
7
|
## 安装
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -124,6 +126,101 @@ const songs = await getSongsDetail([1, 2])
|
|
|
124
126
|
// ]
|
|
125
127
|
```
|
|
126
128
|
|
|
129
|
+
## 起一个 HTTP 服务器(可选,Bun)
|
|
130
|
+
|
|
131
|
+
`@kuriyona/cecilia/elysia` 把 37 个函数各暴露成一条 `GET` 路由:既可以直接拿它返回的 Elysia 实例挂进自己的应用,也可以一键起服务。
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm add elysia @elysiajs/cors
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createApp, startServer } from '@kuriyona/cecilia/elysia'
|
|
139
|
+
|
|
140
|
+
// 1) 只要现成的 Elysia 实例,自己 .use() / .listen() / 挂到别的实例上
|
|
141
|
+
const app = createApp({ cors: { origin: ['http://localhost:5173'] } })
|
|
142
|
+
|
|
143
|
+
// 2) 或者直接起服务:host/port 默认取 HOST / PORT 环境变量(缺省 127.0.0.1:3000)
|
|
144
|
+
const { app, url } = startServer({
|
|
145
|
+
port: 3000,
|
|
146
|
+
cors: { origin: true },
|
|
147
|
+
auth: { token: process.env.TOKEN! }, // 要求 Authorization: Bearer <token>
|
|
148
|
+
})
|
|
149
|
+
console.log(`listening on ${url}`)
|
|
150
|
+
// 关闭:app.stop()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
仓库内自带可运行示例:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
PORT=3000 TOKEN=demo bun run examples/elysia-server.ts
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> `startServer` 依赖 Bun 的 `serve`,在 Node 下会**立即抛错**;`createApp()` 与 `app.handle()` 与运行时无关,也可以在 Node 里自备适配器使用。
|
|
160
|
+
|
|
161
|
+
### 服务器选项
|
|
162
|
+
|
|
163
|
+
| 字段 | 说明 |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `host` | 监听地址,默认 `process.env.HOST ?? '127.0.0.1'` |
|
|
166
|
+
| `port` | 监听端口,默认 `process.env.PORT ?? 3000`;`PORT` 非法直接抛错;`port: 0` 取随机端口(返回的 `url` 是实际地址) |
|
|
167
|
+
| `cors` | 原样透传给 `@elysiajs/cors` 的 `CORSConfig`;不传则完全不启用 CORS |
|
|
168
|
+
| `auth` | `{ token }`:所有路由都要求 `Authorization: Bearer <token>`(scheme 大小写不敏感、常量时间比较),失败返回 `401 { code: 401, message: 'Unauthorized' }` |
|
|
169
|
+
|
|
170
|
+
- 入站 `Cookie` 头会**原样转发**给上游,客户端因此可以自带登录态(含 `MUSIC_U`);服务端不保存任何 token。
|
|
171
|
+
- 参数错误:id 列表不合法 → `422`;`type` / `order` / `albumType` 等枚举越界 → `400`;未知路径 → `404`。
|
|
172
|
+
- 上游报错(`NeteaseApiError`)不拦截,走 Elysia 默认错误处理(`500`)。
|
|
173
|
+
|
|
174
|
+
### 路由表
|
|
175
|
+
|
|
176
|
+
| `GET` 路径 | query 参数 | 对应函数 |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `/search` | `keywords`(必填)、`type`、`limit`、`offset` | `search` |
|
|
179
|
+
| `/cloudsearch` | 同上 | `cloudSearch` |
|
|
180
|
+
| `/search/suggest` | `keywords`、`mobile` | `getSearchSuggest` |
|
|
181
|
+
| `/search/hot` | — | `getHotSearches` |
|
|
182
|
+
| `/search/hot/detail` | — | `getHotSearchDetail` |
|
|
183
|
+
| `/search/default-keyword` | — | `getDefaultSearchKeyword` |
|
|
184
|
+
| `/search/multimatch` | `keywords`、`type` | `searchMultimatch` |
|
|
185
|
+
| `/song/detail` | `ids`(`1,2,3`) | `getSongsDetail` |
|
|
186
|
+
| `/song/url` | `id`(`1,2`)、`br` | `getSongUrl` |
|
|
187
|
+
| `/lyric` | `id` | `getLyric` |
|
|
188
|
+
| `/lyric/new` | `id` | `getLyricNew` |
|
|
189
|
+
| `/check/music` | `id`、`br` | `checkMusic` |
|
|
190
|
+
| `/simi/song` | `id` | `getSimilarSongs` |
|
|
191
|
+
| `/playlist/detail` | `id` | `getPlaylistDetail` |
|
|
192
|
+
| `/playlist/track/all` | `id`、`limit`、`offset` | `getPlaylistTracks` |
|
|
193
|
+
| `/playlist/detail/dynamic` | `id` | `getPlaylistDetailDynamic` |
|
|
194
|
+
| `/playlist/highquality/tags` | — | `getHighQualityTags` |
|
|
195
|
+
| `/playlist/top` | `cat`、`order`(`hot`/`new`)、`limit`、`offset` | `getTopPlaylists` |
|
|
196
|
+
| `/playlist/highquality/list` | `cat`、`limit`、`before` | `getHighQualityPlaylists` |
|
|
197
|
+
| `/playlist/catalogue` | — | `getPlaylistCategories` |
|
|
198
|
+
| `/playlist/related` | `id` | `getRelatedPlaylists` |
|
|
199
|
+
| `/artist` | `id` | `getArtist` |
|
|
200
|
+
| `/artist/detail` | `id` | `getArtistDetail` |
|
|
201
|
+
| `/artist/songs` | `id`、`order`(`hot`/`time`)、`limit`、`offset` | `getArtistSongs` |
|
|
202
|
+
| `/artist/top/song` | `id` | `getArtistTopSongs` |
|
|
203
|
+
| `/artist/album` | `id`、`limit`、`offset` | `getArtistAlbums` |
|
|
204
|
+
| `/artist/list` | `area`、`type`、`initial`、`limit`、`offset` | `getArtistList` |
|
|
205
|
+
| `/artist/desc` | `id` | `getArtistDesc` |
|
|
206
|
+
| `/artist/mv` | `id`、`limit`、`offset` | `getArtistMvs` |
|
|
207
|
+
| `/artist/video` | `id`、`size`、`cursor`、`order` | `getArtistVideos` |
|
|
208
|
+
| `/album` | `id` | `getAlbum` |
|
|
209
|
+
| `/album/product` | `id` | `getAlbumProduct` |
|
|
210
|
+
| `/album/dynamic` | `id` | `getAlbumDynamic` |
|
|
211
|
+
| `/album/sale/board` | `albumType`(`0`/`1`)、`type`(`daily`/`week`/`year`/`total`)、`year` | `getAlbumSaleBoard` |
|
|
212
|
+
| `/album/privilege` | `id` | `getAlbumPrivileges` |
|
|
213
|
+
| `/album/list` | `area`、`type`、`limit`、`offset` | `getAlbumList` |
|
|
214
|
+
| `/album/new` | — | `getNewestAlbums` |
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// 例:带上自己的登录 cookie 走一遍服务端
|
|
218
|
+
const res = await fetch('http://127.0.0.1:3000/search?keywords=HoYo-MiX&type=100', {
|
|
219
|
+
headers: { cookie: 'MUSIC_U=...' },
|
|
220
|
+
})
|
|
221
|
+
const { artists } = (await res.json()) as SearchResult
|
|
222
|
+
```
|
|
223
|
+
|
|
127
224
|
## 从 0.2 升级
|
|
128
225
|
|
|
129
226
|
`getPlaylistDetail` / `getLyric` / `getSongsDetail` 已改为走加密接口并对齐新词表(破坏性变更):
|
|
@@ -140,6 +237,7 @@ const songs = await getSongsDetail([1, 2])
|
|
|
140
237
|
- `search({ type: 2000 })` 的语音搜索分支依赖上游当前响应结构(`songs` 或 `resources`),结构变化时会抛 `NeteaseApiError` 而不是返回空结果。
|
|
141
238
|
- `getRelatedPlaylists` 抓取 `music.163.com/playlist?id=` 的 HTML;页面结构变化时会抛 `NeteaseApiError`。
|
|
142
239
|
- 上游非 200 的成功码(如 `201`)默认视为失败;`request()` 的 `acceptCodes` 内部参数可按接口放行。
|
|
240
|
+
- `@kuriyona/cecilia/elysia` 的 `startServer` 只在 **Bun** 下可用(Node 下抛错);它不缓存响应、不存 token,也不会注入匿名 token,会员态仍需客户端自带 cookie。
|
|
143
241
|
|
|
144
242
|
## 开发
|
|
145
243
|
|
|
@@ -147,8 +245,9 @@ const songs = await getSongsDetail([1, 2])
|
|
|
147
245
|
pnpm test # 离线测试(mock fetch + 加密自洽 + 响应整形)
|
|
148
246
|
pnpm test:live # 真实网络测试(含 test/probe.live.test.ts 探针,dump 到 docs/probe/)
|
|
149
247
|
pnpm test:watch # 监听模式
|
|
150
|
-
pnpm build #
|
|
248
|
+
pnpm build # 构建(dist/{index,elysia}.{mjs,cjs,d.mts,d.cts})
|
|
151
249
|
pnpm clone:api-enhanced # 拉取 api-enhanced 参考实现(仅供比对,运行时不依赖)
|
|
250
|
+
bun run examples/elysia-server.ts # 起 Elysia 示例服务器(需 Bun;PORT / TOKEN 可选)
|
|
152
251
|
```
|
|
153
252
|
|
|
154
253
|
离线测试直接断言各接口的请求 URL、加密方式与整形结果,不需要网络;`pnpm test:live -t probe` 会把上游原始响应写入 `docs/probe/<api>.json`(已 gitignore),用于核对字段路径。
|
package/dist/elysia.cjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_src = require("./src-BoyQdkLi.cjs");
|
|
3
|
+
let node_crypto = require("node:crypto");
|
|
4
|
+
let _elysiajs_cors = require("@elysiajs/cors");
|
|
5
|
+
let elysia = require("elysia");
|
|
6
|
+
//#region src/elysia/routes.ts
|
|
7
|
+
/** 入站 Cookie 原样转发给上游,会员态由调用方自带。 */
|
|
8
|
+
const optionsOf = (request) => {
|
|
9
|
+
const cookie = request.headers.get("cookie");
|
|
10
|
+
return cookie === null ? void 0 : { cookie };
|
|
11
|
+
};
|
|
12
|
+
/** 逗号分隔的数字 id 列表,`1,2,3`。 */
|
|
13
|
+
const idListSchema = elysia.t.String({ pattern: "^[0-9]+(,[0-9]+)*$" });
|
|
14
|
+
const idsOf = (value) => value.split(",").map(Number);
|
|
15
|
+
const pagingSchema = {
|
|
16
|
+
limit: elysia.t.Optional(elysia.t.Numeric()),
|
|
17
|
+
offset: elysia.t.Optional(elysia.t.Numeric())
|
|
18
|
+
};
|
|
19
|
+
/** 与 src/types/search.ts 的 SearchType 保持同步。 */
|
|
20
|
+
const SEARCH_TYPES = [
|
|
21
|
+
1,
|
|
22
|
+
10,
|
|
23
|
+
100,
|
|
24
|
+
1e3,
|
|
25
|
+
1002,
|
|
26
|
+
1004,
|
|
27
|
+
1006,
|
|
28
|
+
1009,
|
|
29
|
+
1014,
|
|
30
|
+
2e3
|
|
31
|
+
];
|
|
32
|
+
const searchTypeOf = (value) => value === void 0 ? void 0 : SEARCH_TYPES.find((candidate) => candidate === value);
|
|
33
|
+
/** 白名单外的取值返回 undefined,由调用方回 400。 */
|
|
34
|
+
const pickEnum = (value, allowed) => value === void 0 ? void 0 : allowed.find((candidate) => candidate === value);
|
|
35
|
+
const invalidEnum = (field, allowed) => ({
|
|
36
|
+
code: 400,
|
|
37
|
+
message: `${field} 只支持 ${allowed.join(" / ")}`
|
|
38
|
+
});
|
|
39
|
+
const TOP_PLAYLIST_ORDERS = ["hot", "new"];
|
|
40
|
+
const ARTIST_SONG_ORDERS = ["hot", "time"];
|
|
41
|
+
const ALBUM_SALE_TYPES = [
|
|
42
|
+
"daily",
|
|
43
|
+
"week",
|
|
44
|
+
"year",
|
|
45
|
+
"total"
|
|
46
|
+
];
|
|
47
|
+
const ALBUM_TYPES = ["0", "1"];
|
|
48
|
+
/** Query 里 `albumType` 是字符串,窄化回 0 | 1。 */
|
|
49
|
+
const albumTypeOf = (value) => value === void 0 ? void 0 : value === "0" ? 0 : 1;
|
|
50
|
+
const searchRoutes = (app) => app.get("/search", ({ query, request, set }) => {
|
|
51
|
+
const type = searchTypeOf(query.type);
|
|
52
|
+
if (query.type !== void 0 && type === void 0) {
|
|
53
|
+
set.status = 400;
|
|
54
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
55
|
+
}
|
|
56
|
+
return require_src.search({
|
|
57
|
+
keywords: query.keywords,
|
|
58
|
+
type,
|
|
59
|
+
limit: query.limit,
|
|
60
|
+
offset: query.offset
|
|
61
|
+
}, optionsOf(request));
|
|
62
|
+
}, { query: elysia.t.Object({
|
|
63
|
+
keywords: elysia.t.String({ minLength: 1 }),
|
|
64
|
+
type: elysia.t.Optional(elysia.t.Numeric()),
|
|
65
|
+
...pagingSchema
|
|
66
|
+
}) }).get("/cloudsearch", ({ query, request, set }) => {
|
|
67
|
+
const type = searchTypeOf(query.type);
|
|
68
|
+
if (query.type !== void 0 && type === void 0) {
|
|
69
|
+
set.status = 400;
|
|
70
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
71
|
+
}
|
|
72
|
+
return require_src.cloudSearch({
|
|
73
|
+
keywords: query.keywords,
|
|
74
|
+
type,
|
|
75
|
+
limit: query.limit,
|
|
76
|
+
offset: query.offset
|
|
77
|
+
}, optionsOf(request));
|
|
78
|
+
}, { query: elysia.t.Object({
|
|
79
|
+
keywords: elysia.t.String({ minLength: 1 }),
|
|
80
|
+
type: elysia.t.Optional(elysia.t.Numeric()),
|
|
81
|
+
...pagingSchema
|
|
82
|
+
}) }).get("/search/suggest", ({ query, request }) => require_src.getSearchSuggest({
|
|
83
|
+
keywords: query.keywords,
|
|
84
|
+
mobile: query.mobile
|
|
85
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
86
|
+
keywords: elysia.t.Optional(elysia.t.String()),
|
|
87
|
+
mobile: elysia.t.Optional(elysia.t.BooleanString())
|
|
88
|
+
}) }).get("/search/hot", ({ request }) => require_src.getHotSearches(optionsOf(request))).get("/search/hot/detail", ({ request }) => require_src.getHotSearchDetail(optionsOf(request))).get("/search/default-keyword", ({ request }) => require_src.getDefaultSearchKeyword(optionsOf(request))).get("/search/multimatch", ({ query, request, set }) => {
|
|
89
|
+
const type = searchTypeOf(query.type);
|
|
90
|
+
if (query.type !== void 0 && type === void 0) {
|
|
91
|
+
set.status = 400;
|
|
92
|
+
return invalidEnum("type", SEARCH_TYPES);
|
|
93
|
+
}
|
|
94
|
+
return require_src.searchMultimatch({
|
|
95
|
+
keywords: query.keywords,
|
|
96
|
+
type
|
|
97
|
+
}, optionsOf(request));
|
|
98
|
+
}, { query: elysia.t.Object({
|
|
99
|
+
keywords: elysia.t.Optional(elysia.t.String()),
|
|
100
|
+
type: elysia.t.Optional(elysia.t.Numeric())
|
|
101
|
+
}) });
|
|
102
|
+
const songRoutes = (app) => app.get("/song/detail", ({ query, request }) => require_src.getSongsDetail(idsOf(query.ids), optionsOf(request)), { query: elysia.t.Object({ ids: idListSchema }) }).get("/song/url", ({ query, request }) => require_src.getSongUrl({
|
|
103
|
+
id: idsOf(query.id),
|
|
104
|
+
br: query.br
|
|
105
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
106
|
+
id: idListSchema,
|
|
107
|
+
br: elysia.t.Optional(elysia.t.Numeric())
|
|
108
|
+
}) }).get("/lyric", ({ query, request }) => require_src.getLyric(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/lyric/new", ({ query, request }) => require_src.getLyricNew(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/check/music", ({ query, request }) => require_src.checkMusic({
|
|
109
|
+
id: query.id,
|
|
110
|
+
br: query.br
|
|
111
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
112
|
+
id: elysia.t.Numeric(),
|
|
113
|
+
br: elysia.t.Optional(elysia.t.Numeric())
|
|
114
|
+
}) }).get("/simi/song", ({ query, request }) => require_src.getSimilarSongs(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) });
|
|
115
|
+
const playlistRoutes = (app) => app.get("/playlist/detail", ({ query, request }) => require_src.getPlaylistDetail(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/playlist/track/all", ({ query, request }) => require_src.getPlaylistTracks({
|
|
116
|
+
id: query.id,
|
|
117
|
+
limit: query.limit,
|
|
118
|
+
offset: query.offset
|
|
119
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
120
|
+
id: elysia.t.Numeric(),
|
|
121
|
+
...pagingSchema
|
|
122
|
+
}) }).get("/playlist/detail/dynamic", ({ query, request }) => require_src.getPlaylistDetailDynamic(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/playlist/highquality/tags", ({ request }) => require_src.getHighQualityTags(optionsOf(request))).get("/playlist/top", ({ query, request, set }) => {
|
|
123
|
+
const order = pickEnum(query.order, TOP_PLAYLIST_ORDERS);
|
|
124
|
+
if (query.order !== void 0 && order === void 0) {
|
|
125
|
+
set.status = 400;
|
|
126
|
+
return invalidEnum("order", TOP_PLAYLIST_ORDERS);
|
|
127
|
+
}
|
|
128
|
+
return require_src.getTopPlaylists({
|
|
129
|
+
cat: query.cat,
|
|
130
|
+
order,
|
|
131
|
+
limit: query.limit,
|
|
132
|
+
offset: query.offset
|
|
133
|
+
}, optionsOf(request));
|
|
134
|
+
}, { query: elysia.t.Object({
|
|
135
|
+
cat: elysia.t.Optional(elysia.t.String()),
|
|
136
|
+
order: elysia.t.Optional(elysia.t.String()),
|
|
137
|
+
...pagingSchema
|
|
138
|
+
}) }).get("/playlist/highquality/list", ({ query, request }) => require_src.getHighQualityPlaylists({
|
|
139
|
+
cat: query.cat,
|
|
140
|
+
limit: query.limit,
|
|
141
|
+
before: query.before
|
|
142
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
143
|
+
cat: elysia.t.Optional(elysia.t.String()),
|
|
144
|
+
limit: elysia.t.Optional(elysia.t.Numeric()),
|
|
145
|
+
before: elysia.t.Optional(elysia.t.Numeric())
|
|
146
|
+
}) }).get("/playlist/catalogue", ({ request }) => require_src.getPlaylistCategories(optionsOf(request))).get("/playlist/related", ({ query, request }) => require_src.getRelatedPlaylists(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) });
|
|
147
|
+
const artistRoutes = (app) => app.get("/artist", ({ query, request }) => require_src.getArtist(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/artist/detail", ({ query, request }) => require_src.getArtistDetail(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/artist/songs", ({ query, request, set }) => {
|
|
148
|
+
const order = pickEnum(query.order, ARTIST_SONG_ORDERS);
|
|
149
|
+
if (query.order !== void 0 && order === void 0) {
|
|
150
|
+
set.status = 400;
|
|
151
|
+
return invalidEnum("order", ARTIST_SONG_ORDERS);
|
|
152
|
+
}
|
|
153
|
+
return require_src.getArtistSongs({
|
|
154
|
+
id: query.id,
|
|
155
|
+
order,
|
|
156
|
+
limit: query.limit,
|
|
157
|
+
offset: query.offset
|
|
158
|
+
}, optionsOf(request));
|
|
159
|
+
}, { query: elysia.t.Object({
|
|
160
|
+
id: elysia.t.Numeric(),
|
|
161
|
+
order: elysia.t.Optional(elysia.t.String()),
|
|
162
|
+
...pagingSchema
|
|
163
|
+
}) }).get("/artist/top/song", ({ query, request }) => require_src.getArtistTopSongs(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/artist/album", ({ query, request }) => require_src.getArtistAlbums({
|
|
164
|
+
id: query.id,
|
|
165
|
+
limit: query.limit,
|
|
166
|
+
offset: query.offset
|
|
167
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
168
|
+
id: elysia.t.Numeric(),
|
|
169
|
+
...pagingSchema
|
|
170
|
+
}) }).get("/artist/list", ({ query, request }) => require_src.getArtistList({
|
|
171
|
+
area: query.area,
|
|
172
|
+
type: query.type,
|
|
173
|
+
initial: query.initial,
|
|
174
|
+
limit: query.limit,
|
|
175
|
+
offset: query.offset
|
|
176
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
177
|
+
area: elysia.t.Optional(elysia.t.Numeric()),
|
|
178
|
+
type: elysia.t.Optional(elysia.t.Numeric()),
|
|
179
|
+
initial: elysia.t.Optional(elysia.t.String()),
|
|
180
|
+
...pagingSchema
|
|
181
|
+
}) }).get("/artist/desc", ({ query, request }) => require_src.getArtistDesc(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/artist/mv", ({ query, request }) => require_src.getArtistMvs({
|
|
182
|
+
id: query.id,
|
|
183
|
+
limit: query.limit,
|
|
184
|
+
offset: query.offset
|
|
185
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
186
|
+
id: elysia.t.Numeric(),
|
|
187
|
+
...pagingSchema
|
|
188
|
+
}) }).get("/artist/video", ({ query, request }) => require_src.getArtistVideos({
|
|
189
|
+
id: query.id,
|
|
190
|
+
size: query.size,
|
|
191
|
+
cursor: query.cursor,
|
|
192
|
+
order: query.order
|
|
193
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
194
|
+
id: elysia.t.Numeric(),
|
|
195
|
+
size: elysia.t.Optional(elysia.t.Numeric()),
|
|
196
|
+
cursor: elysia.t.Optional(elysia.t.Numeric()),
|
|
197
|
+
order: elysia.t.Optional(elysia.t.Numeric())
|
|
198
|
+
}) });
|
|
199
|
+
const albumRoutes = (app) => app.get("/album", ({ query, request }) => require_src.getAlbum(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/album/product", ({ query, request }) => require_src.getAlbumProduct(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/album/dynamic", ({ query, request }) => require_src.getAlbumDynamic(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/album/sale/board", ({ query, request, set }) => {
|
|
200
|
+
const albumType = pickEnum(query.albumType, ALBUM_TYPES);
|
|
201
|
+
if (query.albumType !== void 0 && albumType === void 0) {
|
|
202
|
+
set.status = 400;
|
|
203
|
+
return invalidEnum("albumType", ALBUM_TYPES);
|
|
204
|
+
}
|
|
205
|
+
const type = pickEnum(query.type, ALBUM_SALE_TYPES);
|
|
206
|
+
if (query.type !== void 0 && type === void 0) {
|
|
207
|
+
set.status = 400;
|
|
208
|
+
return invalidEnum("type", ALBUM_SALE_TYPES);
|
|
209
|
+
}
|
|
210
|
+
return require_src.getAlbumSaleBoard({
|
|
211
|
+
albumType: albumTypeOf(albumType),
|
|
212
|
+
type,
|
|
213
|
+
year: query.year
|
|
214
|
+
}, optionsOf(request));
|
|
215
|
+
}, { query: elysia.t.Object({
|
|
216
|
+
albumType: elysia.t.Optional(elysia.t.String()),
|
|
217
|
+
type: elysia.t.Optional(elysia.t.String()),
|
|
218
|
+
year: elysia.t.Optional(elysia.t.Numeric())
|
|
219
|
+
}) }).get("/album/privilege", ({ query, request }) => require_src.getAlbumPrivileges(query.id, optionsOf(request)), { query: elysia.t.Object({ id: elysia.t.Numeric() }) }).get("/album/list", ({ query, request }) => require_src.getAlbumList({
|
|
220
|
+
area: query.area,
|
|
221
|
+
type: query.type,
|
|
222
|
+
limit: query.limit,
|
|
223
|
+
offset: query.offset
|
|
224
|
+
}, optionsOf(request)), { query: elysia.t.Object({
|
|
225
|
+
area: elysia.t.Optional(elysia.t.String()),
|
|
226
|
+
type: elysia.t.Optional(elysia.t.String()),
|
|
227
|
+
...pagingSchema
|
|
228
|
+
}) }).get("/album/new", ({ request }) => require_src.getNewestAlbums(optionsOf(request)));
|
|
229
|
+
/** 37 个函数各一条 GET 路由,按 搜索 / 歌曲 / 歌单 / 歌手 / 专辑 依次注册。 */
|
|
230
|
+
const registerRoutes = (app) => albumRoutes(artistRoutes(playlistRoutes(songRoutes(searchRoutes(app)))));
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/elysia/app.ts
|
|
233
|
+
/** scheme 大小写不敏感,比较用 timingSafeEqual(长度不等直接 false)。 */
|
|
234
|
+
const isAuthorized = (request, token) => {
|
|
235
|
+
const header = request.headers.get("authorization");
|
|
236
|
+
if (header === null) return false;
|
|
237
|
+
const [scheme, value, ...rest] = header.split(" ");
|
|
238
|
+
if (rest.length > 0 || scheme?.toLowerCase() !== "bearer" || value === void 0) return false;
|
|
239
|
+
const expected = Buffer.from(token, "utf8");
|
|
240
|
+
const actual = Buffer.from(value, "utf8");
|
|
241
|
+
return expected.length === actual.length && (0, node_crypto.timingSafeEqual)(expected, actual);
|
|
242
|
+
};
|
|
243
|
+
/** 中间件风格插件:挂在实例上后,后续注册的路由都要先过鉴权。 */
|
|
244
|
+
const withAuth = (options) => (app) => app.onBeforeHandle(({ request, set }) => {
|
|
245
|
+
if (isAuthorized(request, options.token)) return;
|
|
246
|
+
set.status = 401;
|
|
247
|
+
return {
|
|
248
|
+
code: 401,
|
|
249
|
+
message: "Unauthorized"
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
/**
|
|
253
|
+
* 把 37 个 Cecilia 函数各注册成一条 GET 路由,返回可直接 `.use()` 或 `.listen()` 的 Elysia 实例。
|
|
254
|
+
* CORS 先注册(预检早于鉴权短路),再鉴权,最后才是路由。
|
|
255
|
+
*/
|
|
256
|
+
const createApp = (options) => {
|
|
257
|
+
let app = new elysia.Elysia();
|
|
258
|
+
if (options?.cors !== void 0) app = app.use((0, _elysiajs_cors.cors)(options.cors));
|
|
259
|
+
if (options?.auth !== void 0) app = withAuth(options.auth)(app);
|
|
260
|
+
return registerRoutes(app);
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/elysia/server.ts
|
|
264
|
+
const readPortEnv = () => {
|
|
265
|
+
const raw = process.env.PORT;
|
|
266
|
+
if (raw === void 0 || raw === "") return 3e3;
|
|
267
|
+
const port = Number(raw);
|
|
268
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`PORT 环境变量非法: ${raw}`);
|
|
269
|
+
return port;
|
|
270
|
+
};
|
|
271
|
+
/** 同步返回:Bun 的 listen 是同步的,端口占用会同步抛出。 */
|
|
272
|
+
const startServer = (options) => {
|
|
273
|
+
if (!("Bun" in globalThis)) throw new Error("@kuriyona/cecilia/elysia 的 startServer 需要 Bun 运行时(例如 bun run server.ts);Node 下请用 createApp() 自备适配器。");
|
|
274
|
+
const port = options?.port ?? readPortEnv();
|
|
275
|
+
const host = options?.host ?? process.env.HOST ?? "127.0.0.1";
|
|
276
|
+
const app = createApp(options);
|
|
277
|
+
app.listen({
|
|
278
|
+
hostname: host,
|
|
279
|
+
port
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
app,
|
|
283
|
+
url: app.server?.url.origin ?? `http://${host}:${port}`
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
//#endregion
|
|
287
|
+
exports.createApp = createApp;
|
|
288
|
+
exports.startServer = startServer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CORSConfig, CORSConfig as CORSConfig$1 } from "@elysiajs/cors";
|
|
2
|
+
import { AnyElysia } from "elysia";
|
|
3
|
+
|
|
4
|
+
//#region src/elysia/app.d.ts
|
|
5
|
+
interface AuthOptions {
|
|
6
|
+
/** 静态 token;请求需带 `Authorization: Bearer <token>`。 */
|
|
7
|
+
token: string;
|
|
8
|
+
}
|
|
9
|
+
interface CeciliaAppOptions {
|
|
10
|
+
/** 透传给 @elysiajs/cors;不传则完全不启用 CORS。 */
|
|
11
|
+
cors?: CORSConfig$1;
|
|
12
|
+
/** 不传则全部路由公开。 */
|
|
13
|
+
auth?: AuthOptions;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 把 37 个 Cecilia 函数各注册成一条 GET 路由,返回可直接 `.use()` 或 `.listen()` 的 Elysia 实例。
|
|
17
|
+
* CORS 先注册(预检早于鉴权短路),再鉴权,最后才是路由。
|
|
18
|
+
*/
|
|
19
|
+
declare const createApp: (options?: CeciliaAppOptions) => AnyElysia;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/elysia/server.d.ts
|
|
22
|
+
interface CeciliaServerOptions extends CeciliaAppOptions {
|
|
23
|
+
/** 默认 `process.env.HOST ?? "127.0.0.1"`。 */
|
|
24
|
+
host?: string;
|
|
25
|
+
/** 默认 `process.env.PORT ?? 3000`;非法 PORT 直接抛错。 */
|
|
26
|
+
port?: number;
|
|
27
|
+
}
|
|
28
|
+
interface CeciliaServer {
|
|
29
|
+
/** 关闭用 `app.stop()`。 */
|
|
30
|
+
app: AnyElysia;
|
|
31
|
+
/** 形如 `http://127.0.0.1:3000`(取自实际监听地址,`port: 0` 也可用)。 */
|
|
32
|
+
url: string;
|
|
33
|
+
}
|
|
34
|
+
/** 同步返回:Bun 的 listen 是同步的,端口占用会同步抛出。 */
|
|
35
|
+
declare const startServer: (options?: CeciliaServerOptions) => CeciliaServer;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type AuthOptions, type CORSConfig, type CeciliaAppOptions, type CeciliaServer, type CeciliaServerOptions, createApp, startServer };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CORSConfig, CORSConfig as CORSConfig$1 } from "@elysiajs/cors";
|
|
2
|
+
import { AnyElysia } from "elysia";
|
|
3
|
+
|
|
4
|
+
//#region src/elysia/app.d.ts
|
|
5
|
+
interface AuthOptions {
|
|
6
|
+
/** 静态 token;请求需带 `Authorization: Bearer <token>`。 */
|
|
7
|
+
token: string;
|
|
8
|
+
}
|
|
9
|
+
interface CeciliaAppOptions {
|
|
10
|
+
/** 透传给 @elysiajs/cors;不传则完全不启用 CORS。 */
|
|
11
|
+
cors?: CORSConfig$1;
|
|
12
|
+
/** 不传则全部路由公开。 */
|
|
13
|
+
auth?: AuthOptions;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 把 37 个 Cecilia 函数各注册成一条 GET 路由,返回可直接 `.use()` 或 `.listen()` 的 Elysia 实例。
|
|
17
|
+
* CORS 先注册(预检早于鉴权短路),再鉴权,最后才是路由。
|
|
18
|
+
*/
|
|
19
|
+
declare const createApp: (options?: CeciliaAppOptions) => AnyElysia;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/elysia/server.d.ts
|
|
22
|
+
interface CeciliaServerOptions extends CeciliaAppOptions {
|
|
23
|
+
/** 默认 `process.env.HOST ?? "127.0.0.1"`。 */
|
|
24
|
+
host?: string;
|
|
25
|
+
/** 默认 `process.env.PORT ?? 3000`;非法 PORT 直接抛错。 */
|
|
26
|
+
port?: number;
|
|
27
|
+
}
|
|
28
|
+
interface CeciliaServer {
|
|
29
|
+
/** 关闭用 `app.stop()`。 */
|
|
30
|
+
app: AnyElysia;
|
|
31
|
+
/** 形如 `http://127.0.0.1:3000`(取自实际监听地址,`port: 0` 也可用)。 */
|
|
32
|
+
url: string;
|
|
33
|
+
}
|
|
34
|
+
/** 同步返回:Bun 的 listen 是同步的,端口占用会同步抛出。 */
|
|
35
|
+
declare const startServer: (options?: CeciliaServerOptions) => CeciliaServer;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type AuthOptions, type CORSConfig, type CeciliaAppOptions, type CeciliaServer, type CeciliaServerOptions, createApp, startServer };
|