@kuriyona/cecilia 0.2.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.
@@ -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 };
package/dist/index.cjs CHANGED
@@ -2,89 +2,44 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- //#region src/utils/mergeLyricTimelines.ts
6
- function mergeLyricTimelines(original, translation) {
7
- const originalMap = /* @__PURE__ */ new Map();
8
- for (const item of original) originalMap.set(item.time, item.text);
9
- const translationMap = /* @__PURE__ */ new Map();
10
- for (const item of translation) translationMap.set(item.time, item.text);
11
- const allTimes = /* @__PURE__ */ new Set();
12
- for (const item of original) allTimes.add(item.time);
13
- for (const item of translation) allTimes.add(item.time);
14
- const sortedTimes = [...allTimes].sort((a, b) => a - b);
15
- const result = [];
16
- let lastOriginal;
17
- let lastTranslation;
18
- for (const time of sortedTimes) {
19
- if (originalMap.has(time)) lastOriginal = originalMap.get(time);
20
- if (translationMap.has(time)) lastTranslation = translationMap.get(time);
21
- result.push({
22
- time,
23
- text: lastOriginal ?? "",
24
- ...lastTranslation !== void 0 ? { translation: lastTranslation } : {}
25
- });
26
- }
27
- return result;
28
- }
29
- //#endregion
30
- //#region src/index.ts
31
- const BASE_URL = "https://music.163.com/api/";
32
- const getPlaylistDetail = async (id) => {
33
- const data = await (await fetch(`${BASE_URL}/v6/playlist/detail?id=${id}`)).json();
34
- return {
35
- id: data.playlist.id,
36
- name: data.playlist.name,
37
- coverImgId: data.playlist.coverImgId,
38
- coverImgUrl: data.playlist.coverImgUrl,
39
- userId: data.playlist.userId,
40
- createTime: data.playlist.createTime,
41
- songs: data.playlist.trackIds.map((t) => ({
42
- id: t.id,
43
- addTime: t.at
44
- }))
45
- };
46
- };
47
- const parseLrc = (lrc) => lrc.split("\n").filter((l) => l.startsWith("[")).map((l) => {
48
- const match = l.match(/^\[(\d{2}):(\d{2}(?:\.\d{2,3})?)\](.*)/);
49
- if (!match) return null;
50
- const minutes = parseInt(match[1], 10);
51
- const seconds = parseFloat(match[2]);
52
- return {
53
- time: minutes * 60 + seconds,
54
- text: match[3].trim()
55
- };
56
- }).filter((e) => e !== null);
57
- const getLyric = async (id) => {
58
- const data = await (await fetch(`${BASE_URL}/song/lyric?id=${id}&lv=-1&tv=-1`)).json();
59
- return {
60
- lines: mergeLyricTimelines(parseLrc(data.lrc.lyric), data.tlyric?.lyric ? parseLrc(data.tlyric.lyric) : []),
61
- ...data.transUser ? { translator: data.transUser } : {}
62
- };
63
- };
64
- const getSongsDetail = async (ids) => {
65
- const query = JSON.stringify(ids.map((id) => ({ id })));
66
- return (await (await fetch(`${BASE_URL}/v3/song/detail?c=${query}`)).json()).songs.map((s) => ({
67
- id: s.id,
68
- name: s.name,
69
- artists: s.ar.map((a) => ({
70
- id: a.id,
71
- name: a.name
72
- })),
73
- album: {
74
- id: s.al.id,
75
- name: s.al.name,
76
- picUrl: s.al.picUrl
77
- },
78
- duration: s.dt
79
- }));
80
- };
81
- var src_default = {
82
- getPlaylistDetail,
83
- getLyric,
84
- getSongsDetail
85
- };
86
- //#endregion
87
- exports.default = src_default;
88
- exports.getLyric = getLyric;
89
- exports.getPlaylistDetail = getPlaylistDetail;
90
- exports.getSongsDetail = getSongsDetail;
5
+ const require_src = require("./src-BoyQdkLi.cjs");
6
+ exports.NeteaseApiError = require_src.NeteaseApiError;
7
+ exports.checkMusic = require_src.checkMusic;
8
+ exports.cloudSearch = require_src.cloudSearch;
9
+ exports.default = require_src.src_default;
10
+ exports.getAlbum = require_src.getAlbum;
11
+ exports.getAlbumDynamic = require_src.getAlbumDynamic;
12
+ exports.getAlbumList = require_src.getAlbumList;
13
+ exports.getAlbumPrivileges = require_src.getAlbumPrivileges;
14
+ exports.getAlbumProduct = require_src.getAlbumProduct;
15
+ exports.getAlbumSaleBoard = require_src.getAlbumSaleBoard;
16
+ exports.getArtist = require_src.getArtist;
17
+ exports.getArtistAlbums = require_src.getArtistAlbums;
18
+ exports.getArtistDesc = require_src.getArtistDesc;
19
+ exports.getArtistDetail = require_src.getArtistDetail;
20
+ exports.getArtistList = require_src.getArtistList;
21
+ exports.getArtistMvs = require_src.getArtistMvs;
22
+ exports.getArtistSongs = require_src.getArtistSongs;
23
+ exports.getArtistTopSongs = require_src.getArtistTopSongs;
24
+ exports.getArtistVideos = require_src.getArtistVideos;
25
+ exports.getDefaultSearchKeyword = require_src.getDefaultSearchKeyword;
26
+ exports.getHighQualityPlaylists = require_src.getHighQualityPlaylists;
27
+ exports.getHighQualityTags = require_src.getHighQualityTags;
28
+ exports.getHotSearchDetail = require_src.getHotSearchDetail;
29
+ exports.getHotSearches = require_src.getHotSearches;
30
+ exports.getLyric = require_src.getLyric;
31
+ exports.getLyricNew = require_src.getLyricNew;
32
+ exports.getNewestAlbums = require_src.getNewestAlbums;
33
+ exports.getPlaylistCategories = require_src.getPlaylistCategories;
34
+ exports.getPlaylistDetail = require_src.getPlaylistDetail;
35
+ exports.getPlaylistDetailDynamic = require_src.getPlaylistDetailDynamic;
36
+ exports.getPlaylistTracks = require_src.getPlaylistTracks;
37
+ exports.getRelatedPlaylists = require_src.getRelatedPlaylists;
38
+ exports.getSearchSuggest = require_src.getSearchSuggest;
39
+ exports.getSimilarSongs = require_src.getSimilarSongs;
40
+ exports.getSongUrl = require_src.getSongUrl;
41
+ exports.getSongsDetail = require_src.getSongsDetail;
42
+ exports.getTopPlaylists = require_src.getTopPlaylists;
43
+ exports.request = require_src.request;
44
+ exports.search = require_src.search;
45
+ exports.searchMultimatch = require_src.searchMultimatch;