@kuriyona/cecilia 0.2.0 → 1.0.0

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/dist/index.mjs CHANGED
@@ -1,3 +1,643 @@
1
+ import { constants, createCipheriv, createHash, createPublicKey, publicEncrypt, randomBytes, randomInt } from "node:crypto";
2
+ //#region src/cookies.ts
3
+ const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
4
+ const randomLowercase = (length) => {
5
+ let out = "";
6
+ for (let i = 0; i < length; i++) out += LOWERCASE[Math.floor(Math.random() * 26)];
7
+ return out;
8
+ };
9
+ const cookieToObject = (cookie) => {
10
+ if (!cookie) return {};
11
+ if (typeof cookie !== "string") return { ...cookie };
12
+ const result = {};
13
+ for (const part of cookie.split(";")) {
14
+ const index = part.indexOf("=");
15
+ if (index === -1) continue;
16
+ const key = part.slice(0, index).trim();
17
+ if (!key) continue;
18
+ result[key] = part.slice(index + 1).trim();
19
+ }
20
+ return result;
21
+ };
22
+ const cookieToString = (cookie) => Object.entries(cookie).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("; ");
23
+ const buildCookieObject = (cookie) => {
24
+ const input = cookieToObject(cookie);
25
+ const nuid = input._ntes_nuid || randomBytes(32).toString("hex");
26
+ const timestamp = Date.now();
27
+ return {
28
+ _ntes_nuid: nuid,
29
+ _ntes_nnid: `${nuid},${timestamp}`,
30
+ WNMCID: `${randomLowercase(6)}.${timestamp}.01.0`,
31
+ __remember_me: "true",
32
+ ntes_kaola_ad: "1",
33
+ WEVNSM: "1.0.0",
34
+ os: "pc",
35
+ appver: "3.1.17.204416",
36
+ osver: "Microsoft-Windows-10-Professional-build-19045-64bit",
37
+ channel: "netease",
38
+ deviceId: randomBytes(32).toString("hex").toUpperCase(),
39
+ ...input
40
+ };
41
+ };
42
+ //#endregion
43
+ //#region src/crypto/constants.ts
44
+ const PRESET_KEY = "0CoJUm6Qyw8W8jud";
45
+ const IV = "0102030405060708";
46
+ const BASE62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
47
+ const EAPI_KEY = "e82ckenh8dichen8";
48
+ const EAPI_SEP = "-36cd479b6b5-";
49
+ const WEAPI_DOMAIN = "https://music.163.com";
50
+ const EAPI_DOMAIN = "https://interfacepc.music.163.com";
51
+ const API_DOMAIN = "https://interface.music.163.com";
52
+ const WEAPI_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
53
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB
54
+ -----END PUBLIC KEY-----`;
55
+ //#endregion
56
+ //#region src/crypto/eapi.ts
57
+ const eapiEncrypt = (uri, data) => {
58
+ const text = JSON.stringify(data);
59
+ const digest = createHash("md5").update(`nobody${uri}use${text}md5forencrypt`, "utf8").digest("hex");
60
+ const cipher = createCipheriv("aes-128-ecb", EAPI_KEY, null);
61
+ const plain = `${uri}${EAPI_SEP}${text}${EAPI_SEP}${digest}`;
62
+ return Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]).toString("hex");
63
+ };
64
+ //#endregion
65
+ //#region src/crypto/weapi.ts
66
+ const aes128Cbc = (text, key, iv) => {
67
+ const cipher = createCipheriv("aes-128-cbc", Buffer.from(key, "utf8"), Buffer.from(iv, "utf8"));
68
+ return Buffer.concat([cipher.update(text, "utf8"), cipher.final()]).toString("base64");
69
+ };
70
+ const randomSecretKey = () => {
71
+ let out = "";
72
+ for (let i = 0; i < 16; i++) out += BASE62[randomInt(62)];
73
+ return out;
74
+ };
75
+ const rsaEncryptNoPadding = (text) => {
76
+ const key = createPublicKey(WEAPI_PUBLIC_KEY);
77
+ const modulusLength = key.asymmetricKeyDetails?.modulusLength;
78
+ const size = modulusLength ? modulusLength / 8 : 128;
79
+ const message = Buffer.from(text, "utf8");
80
+ const padded = Buffer.concat([Buffer.alloc(Math.max(0, size - message.length)), message]);
81
+ return publicEncrypt({
82
+ key,
83
+ padding: constants.RSA_NO_PADDING
84
+ }, padded).toString("hex");
85
+ };
86
+ const weapiEncrypt = (data, options) => {
87
+ const secretKey = options?.secretKey ?? randomSecretKey();
88
+ return {
89
+ params: aes128Cbc(aes128Cbc(JSON.stringify(data), PRESET_KEY, IV), secretKey, IV),
90
+ encSecKey: rsaEncryptNoPadding([...secretKey].reverse().join(""))
91
+ };
92
+ };
93
+ //#endregion
94
+ //#region src/errors.ts
95
+ const pickDetail = (body) => {
96
+ if (typeof body !== "object" || body === null) return void 0;
97
+ if ("msg" in body && typeof body.msg === "string" && body.msg.length > 0) return body.msg;
98
+ if ("message" in body && typeof body.message === "string" && body.message.length > 0) return body.message;
99
+ };
100
+ var NeteaseApiError = class extends Error {
101
+ code;
102
+ uri;
103
+ status;
104
+ body;
105
+ constructor(uri, code, status, body) {
106
+ const detail = pickDetail(body);
107
+ super(`${uri} 请求失败: code ${code}${detail ? ` ${detail}` : ""}`);
108
+ this.name = "NeteaseApiError";
109
+ this.code = code;
110
+ this.uri = uri;
111
+ this.status = status;
112
+ this.body = body;
113
+ }
114
+ };
115
+ //#endregion
116
+ //#region src/http.ts
117
+ const getSetCookies = (res) => {
118
+ try {
119
+ return res.headers.getSetCookie();
120
+ } catch {
121
+ return [];
122
+ }
123
+ };
124
+ async function postForm(url, body, headers, timeout) {
125
+ const res = await fetch(url, {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/x-www-form-urlencoded",
129
+ ...headers
130
+ },
131
+ body,
132
+ signal: AbortSignal.timeout(timeout)
133
+ });
134
+ const text = await res.text();
135
+ let json;
136
+ try {
137
+ json = JSON.parse(text);
138
+ } catch {
139
+ json = void 0;
140
+ }
141
+ return {
142
+ json,
143
+ setCookies: getSetCookies(res),
144
+ status: res.status,
145
+ text
146
+ };
147
+ }
148
+ async function getText(url, timeout, headers = {}) {
149
+ const res = await fetch(url, {
150
+ headers,
151
+ signal: AbortSignal.timeout(timeout)
152
+ });
153
+ const text = await res.text();
154
+ return {
155
+ json: void 0,
156
+ setCookies: getSetCookies(res),
157
+ status: res.status,
158
+ text
159
+ };
160
+ }
161
+ //#endregion
162
+ //#region src/utils/num.ts
163
+ const num = (v) => {
164
+ if (v === null || v === void 0 || v === "") return void 0;
165
+ const n = Number(v);
166
+ return Number.isNaN(n) ? void 0 : n;
167
+ };
168
+ //#endregion
169
+ //#region src/client.ts
170
+ const generateRequestId = () => `${Date.now()}_${String(randomInt(1e3)).padStart(4, "0")}`;
171
+ const readCode = (body) => {
172
+ if (typeof body !== "object" || body === null || !("code" in body)) return;
173
+ return num(body.code);
174
+ };
175
+ async function request(uri, data, crypto, options, acceptCodes = [200]) {
176
+ const timeout = options?.timeout ?? 1e4;
177
+ const cookie = buildCookieObject(options?.cookie);
178
+ const csrf = cookie.__csrf ?? "";
179
+ const payload = {
180
+ ...data,
181
+ e_r: false
182
+ };
183
+ const headers = {};
184
+ let url;
185
+ let body;
186
+ if (crypto === "eapi") {
187
+ const header = {
188
+ osver: cookie.osver ?? "",
189
+ deviceId: cookie.deviceId ?? "",
190
+ os: cookie.os ?? "",
191
+ appver: cookie.appver ?? "",
192
+ versioncode: "140",
193
+ mobilename: "",
194
+ buildver: String(Date.now()).slice(0, 10),
195
+ resolution: "1920x1080",
196
+ __csrf: csrf,
197
+ channel: cookie.channel ?? "",
198
+ requestId: generateRequestId()
199
+ };
200
+ if (cookie.MUSIC_U) header.MUSIC_U = cookie.MUSIC_U;
201
+ payload.header = header;
202
+ url = EAPI_DOMAIN + uri.replace(/^\/api/, "/eapi");
203
+ headers["User-Agent"] = options?.ua ?? "NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)";
204
+ headers.Cookie = cookieToString(header);
205
+ body = new URLSearchParams({ params: eapiEncrypt(uri, payload) }).toString();
206
+ } else if (crypto === "weapi") {
207
+ payload.csrf_token = csrf;
208
+ url = `${WEAPI_DOMAIN}/weapi/${uri.slice(5)}`;
209
+ headers.Referer = WEAPI_DOMAIN;
210
+ headers["User-Agent"] = options?.ua ?? "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0";
211
+ headers.Cookie = cookieToString(cookie);
212
+ body = new URLSearchParams({ ...weapiEncrypt(payload) }).toString();
213
+ } else {
214
+ const form = new URLSearchParams();
215
+ for (const [key, value] of Object.entries(payload)) form.append(key, String(value));
216
+ url = API_DOMAIN + uri;
217
+ headers.Referer = WEAPI_DOMAIN;
218
+ headers["User-Agent"] = options?.ua ?? "NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)";
219
+ headers.Cookie = cookieToString(cookie);
220
+ body = form.toString();
221
+ }
222
+ if (options?.realIP) {
223
+ headers["X-Real-IP"] = options.realIP;
224
+ headers["X-Forwarded-For"] = options.realIP;
225
+ }
226
+ let res;
227
+ try {
228
+ res = await postForm(url, body, headers, timeout);
229
+ } catch (error) {
230
+ throw new NeteaseApiError(uri, -1, 0, {
231
+ code: -1,
232
+ msg: error instanceof Error ? error.message : String(error)
233
+ });
234
+ }
235
+ if (res.json === void 0) throw new NeteaseApiError(uri, res.status, res.status, res.text);
236
+ const code = readCode(res.json);
237
+ if (code !== void 0) {
238
+ if (!acceptCodes.includes(code)) throw new NeteaseApiError(uri, code, res.status, res.json);
239
+ return {
240
+ status: res.status,
241
+ body: res.json,
242
+ cookies: res.setCookies
243
+ };
244
+ }
245
+ if (res.status < 200 || res.status >= 300) throw new NeteaseApiError(uri, res.status, res.status, res.json);
246
+ return {
247
+ status: res.status,
248
+ body: res.json,
249
+ cookies: res.setCookies
250
+ };
251
+ }
252
+ //#endregion
253
+ //#region src/apis/adapters.ts
254
+ const toArtistRef = (raw) => ({
255
+ id: num(raw.id) ?? 0,
256
+ name: raw.name ?? ""
257
+ });
258
+ const toUserRef = (raw) => ({
259
+ id: num(raw.userId ?? raw.id) ?? 0,
260
+ name: raw.nickname ?? raw.name ?? ""
261
+ });
262
+ const toAlbumRef = (raw) => ({
263
+ id: num(raw.id) ?? 0,
264
+ name: raw.name ?? "",
265
+ coverUrl: raw.picUrl ?? raw.coverImgUrl ?? ""
266
+ });
267
+ /** id/duration 缺失的条目返回 undefined,由调用方丢弃。 */
268
+ const toSong = (raw) => {
269
+ const id = num(raw.id);
270
+ const duration = num(raw.dt ?? raw.duration);
271
+ if (id === void 0 || duration === void 0) return void 0;
272
+ const album = raw.al ?? raw.album;
273
+ const alias = raw.alia ?? raw.alias;
274
+ const fee = num(raw.fee);
275
+ const state = num(raw.st);
276
+ return {
277
+ id,
278
+ name: raw.name ?? "",
279
+ artists: (raw.ar ?? raw.artists ?? []).map(toArtistRef),
280
+ album: album ? toAlbumRef(album) : {
281
+ id: 0,
282
+ name: "",
283
+ coverUrl: ""
284
+ },
285
+ duration,
286
+ ...alias?.length ? { alias } : {},
287
+ ...fee !== void 0 ? { fee } : {},
288
+ ...state !== void 0 ? { available: state >= 0 } : {}
289
+ };
290
+ };
291
+ const toSongs = (raws) => {
292
+ const songs = [];
293
+ for (const raw of raws ?? []) {
294
+ const song = toSong(raw);
295
+ if (song) songs.push(song);
296
+ }
297
+ return songs;
298
+ };
299
+ const toArtist = (raw) => {
300
+ const albumCount = num(raw.albumSize);
301
+ const songCount = num(raw.musicSize);
302
+ const mvCount = num(raw.mvSize);
303
+ const avatarUrl = raw.picUrl ?? raw.img1v1Url ?? raw.avatar;
304
+ return {
305
+ id: num(raw.id) ?? 0,
306
+ name: raw.name ?? "",
307
+ ...raw.alias?.length ? { alias: raw.alias } : {},
308
+ ...avatarUrl ? { avatarUrl } : {},
309
+ ...raw.briefDesc ? { briefDesc: raw.briefDesc } : {},
310
+ ...albumCount !== void 0 ? { albumCount } : {},
311
+ ...songCount !== void 0 ? { songCount } : {},
312
+ ...mvCount !== void 0 ? { mvCount } : {},
313
+ ...raw.followed !== void 0 ? { followed: raw.followed } : {}
314
+ };
315
+ };
316
+ const toArtists = (raws) => (raws ?? []).map(toArtist);
317
+ const toAlbum = (raw) => {
318
+ const publishTime = num(raw.publishTime);
319
+ const size = num(raw.size);
320
+ const description = raw.description ?? raw.desc;
321
+ const artists = raw.artists ?? raw.ar;
322
+ return {
323
+ id: num(raw.id) ?? 0,
324
+ name: raw.name ?? "",
325
+ coverUrl: raw.picUrl ?? raw.coverImgUrl ?? "",
326
+ ...artists?.length ? { artists: artists.map(toArtistRef) } : {},
327
+ ...publishTime !== void 0 ? { publishTime } : {},
328
+ ...raw.company ? { company: raw.company } : {},
329
+ ...description ? { description } : {},
330
+ ...size !== void 0 ? { size } : {}
331
+ };
332
+ };
333
+ const toAlbums = (raws) => (raws ?? []).map(toAlbum);
334
+ const toPlaylistSummary = (raw) => {
335
+ const trackCount = num(raw.trackCount);
336
+ const playCount = num(raw.playCount);
337
+ const updateTime = num(raw.updateTime);
338
+ return {
339
+ id: num(raw.id) ?? 0,
340
+ name: raw.name ?? "",
341
+ coverUrl: raw.coverImgUrl ?? "",
342
+ ...trackCount !== void 0 ? { trackCount } : {},
343
+ ...playCount !== void 0 ? { playCount } : {},
344
+ ...raw.creator ? { creator: toUserRef(raw.creator) } : {},
345
+ ...raw.description ? { description: raw.description } : {},
346
+ ...updateTime !== void 0 ? { updateTime } : {},
347
+ ...raw.tags?.length ? { tags: raw.tags } : {}
348
+ };
349
+ };
350
+ const toPlaylistSummaries = (raws) => (raws ?? []).map(toPlaylistSummary);
351
+ const toMvRef = (raw) => {
352
+ const playCount = num(raw.playCount);
353
+ const duration = num(raw.duration);
354
+ return {
355
+ id: num(raw.id) ?? 0,
356
+ name: raw.name ?? "",
357
+ coverUrl: raw.cover ?? raw.picUrl ?? raw.imgurl16v9 ?? raw.imgurl ?? "",
358
+ ...playCount !== void 0 ? { playCount } : {},
359
+ ...duration !== void 0 ? { duration } : {},
360
+ ...raw.artistName ? { artistName: raw.artistName } : {}
361
+ };
362
+ };
363
+ const toMvRefs = (raws) => (raws ?? []).map(toMvRef);
364
+ const toVideoRef = (raw) => {
365
+ const playCount = num(raw.playCount);
366
+ const duration = num(raw.duration);
367
+ return {
368
+ id: num(raw.vid ?? raw.id) ?? 0,
369
+ name: raw.title ?? raw.name ?? "",
370
+ coverUrl: raw.coverUrl ?? "",
371
+ ...playCount !== void 0 ? { playCount } : {},
372
+ ...duration !== void 0 ? { duration } : {},
373
+ ...raw.creator ? { creator: toUserRef(raw.creator) } : {}
374
+ };
375
+ };
376
+ const toVideoRefs = (raws) => (raws ?? []).map(toVideoRef);
377
+ //#endregion
378
+ //#region src/apis/require.ts
379
+ /** 上游响应缺少预期容器时直接报错,不返回伪造的空结果。 */
380
+ const requireField = (value, uri, status, field) => {
381
+ if (value === void 0) throw new NeteaseApiError(uri, -1, status, {
382
+ code: -1,
383
+ msg: `响应缺少 ${field}`
384
+ });
385
+ return value;
386
+ };
387
+ //#endregion
388
+ //#region src/apis/album.ts
389
+ const URI_ALBUM = "/api/v1/album";
390
+ const URI_ALBUM_PRODUCT = "/api/vipmall/albumproduct/detail";
391
+ const URI_ALBUM_DYNAMIC = "/api/album/detail/dynamic";
392
+ const URI_ALBUM_SALE_BOARD = "/api/feealbum/songsaleboard";
393
+ const URI_ALBUM_PRIVILEGE = "/api/album/privilege";
394
+ const URI_ALBUM_LIST = "/api/vipmall/albumproduct/list";
395
+ const URI_NEW_ALBUM = "/api/discovery/newAlbum";
396
+ /** 数字专辑/单曲产品与销售榜条目共用的「产品式」字段命名。 */
397
+ const toProductAlbum = (raw) => {
398
+ const publishTime = num(raw.publishTime ?? raw.pubTime);
399
+ return {
400
+ id: num(raw.albumId ?? raw.id) ?? 0,
401
+ name: raw.albumName ?? raw.name ?? "",
402
+ coverUrl: raw.coverUrl ?? raw.picUrl ?? "",
403
+ ...publishTime !== void 0 ? { publishTime } : {}
404
+ };
405
+ };
406
+ const toAlbumPrivilege = (raw) => {
407
+ const pl = num(raw.pl);
408
+ const dl = num(raw.dl);
409
+ const fl = num(raw.fl);
410
+ const st = num(raw.st);
411
+ const fee = num(raw.fee);
412
+ return {
413
+ id: num(raw.id) ?? 0,
414
+ ...raw.maxBrLevel ? { maxBrLevel: raw.maxBrLevel } : {},
415
+ ...raw.playMaxBrLevel ? { playMaxBrLevel: raw.playMaxBrLevel } : {},
416
+ ...raw.downloadMaxBrLevel ? { downloadMaxBrLevel: raw.downloadMaxBrLevel } : {},
417
+ ...pl !== void 0 ? { pl } : {},
418
+ ...dl !== void 0 ? { dl } : {},
419
+ ...fl !== void 0 ? { fl } : {},
420
+ ...st !== void 0 ? { st } : {},
421
+ ...fee !== void 0 ? { fee } : {}
422
+ };
423
+ };
424
+ const getAlbum = async (id, options) => {
425
+ const uri = `${URI_ALBUM}/${id}`;
426
+ const res = await request(uri, {}, "weapi", options);
427
+ const body = res.body;
428
+ return {
429
+ ...toAlbum(requireField(body.album, uri, res.status, "album")),
430
+ ...body.songs ? { songs: toSongs(body.songs) } : {}
431
+ };
432
+ };
433
+ const getAlbumProduct = async (id, options) => {
434
+ const res = await request(URI_ALBUM_PRODUCT, { id }, "weapi", options);
435
+ const body = res.body;
436
+ const album = requireField(body.album, URI_ALBUM_PRODUCT, res.status, "album");
437
+ const price = num(body.product?.price);
438
+ const publishTime = num(body.product?.pubTime);
439
+ return {
440
+ id: num(album.albumId) ?? 0,
441
+ name: album.albumName ?? "",
442
+ coverUrl: album.coverUrl ?? "",
443
+ ...album.artistName ? { artistName: album.artistName } : {},
444
+ ...price !== void 0 ? { price } : {},
445
+ ...publishTime !== void 0 ? { publishTime } : {}
446
+ };
447
+ };
448
+ const getAlbumDynamic = async (id, options) => {
449
+ const body = (await request(URI_ALBUM_DYNAMIC, { id }, "weapi", options)).body;
450
+ const commentCount = num(body.commentCount);
451
+ const shareCount = num(body.shareCount);
452
+ const subCount = num(body.subCount);
453
+ const likedCount = num(body.likedCount);
454
+ return {
455
+ ...commentCount !== void 0 ? { commentCount } : {},
456
+ ...shareCount !== void 0 ? { shareCount } : {},
457
+ ...subCount !== void 0 ? { subCount } : {},
458
+ ...likedCount !== void 0 ? { likedCount } : {},
459
+ ...body.isSub !== void 0 ? { isSub: body.isSub } : {}
460
+ };
461
+ };
462
+ const getAlbumSaleBoard = async (params, options) => {
463
+ const type = params.type ?? "daily";
464
+ const uri = `${URI_ALBUM_SALE_BOARD}/${type}/type`;
465
+ const data = { albumType: params.albumType ?? 0 };
466
+ if (type === "year") data.year = params.year;
467
+ return { albums: ((await request(uri, data, "weapi", options)).body.products ?? []).map(toProductAlbum) };
468
+ };
469
+ const getAlbumPrivileges = async (id, options) => {
470
+ return ((await request(URI_ALBUM_PRIVILEGE, { id }, "eapi", options)).body.data ?? []).map(toAlbumPrivilege);
471
+ };
472
+ const getAlbumList = async (params, options) => {
473
+ const body = (await request(URI_ALBUM_LIST, {
474
+ limit: params.limit ?? 30,
475
+ offset: params.offset ?? 0,
476
+ total: true,
477
+ area: params.area ?? "ALL",
478
+ type: params.type
479
+ }, "weapi", options)).body;
480
+ const total = num(body.total);
481
+ const products = body.products ?? body.albums;
482
+ return {
483
+ ...total !== void 0 ? { total } : {},
484
+ ...body.more !== void 0 ? { more: body.more } : {},
485
+ albums: products?.some(hasProductFields) ? products.map(toProductAlbum) : toAlbums(body.albums)
486
+ };
487
+ };
488
+ const PRODUCT_KEYS = [
489
+ "albumId",
490
+ "albumName",
491
+ "artistName"
492
+ ];
493
+ const hasProductFields = (raw) => PRODUCT_KEYS.some((key) => key in raw);
494
+ const getNewestAlbums = async (options) => {
495
+ const body = (await request(URI_NEW_ALBUM, {}, "weapi", options)).body;
496
+ return toAlbums(body.albums);
497
+ };
498
+ //#endregion
499
+ //#region src/apis/artist.ts
500
+ const URI_ARTIST = "/api/v1/artist";
501
+ const URI_ARTIST_HEAD_INFO = "/api/artist/head/info/get";
502
+ const URI_ARTIST_SONGS = "/api/v1/artist/songs";
503
+ const URI_ARTIST_TOP_SONG = "/api/artist/top/song";
504
+ const URI_ARTIST_ALBUMS = "/api/artist/albums";
505
+ const URI_ARTIST_LIST = "/api/v1/artist/list";
506
+ const URI_ARTIST_INTRO = "/api/artist/introduction";
507
+ const URI_ARTIST_MVS = "/api/artist/mvs";
508
+ const URI_ARTIST_VIDEO = "/api/mlog/artist/video";
509
+ const toArtistInitial = (value) => {
510
+ if (typeof value !== "string" || !Number.isNaN(Number(value))) return value;
511
+ return value.toUpperCase().charCodeAt(0) || void 0;
512
+ };
513
+ const getArtist = async (id, options) => {
514
+ const uri = `${URI_ARTIST}/${id}`;
515
+ const res = await request(uri, {}, "weapi", options);
516
+ const body = res.body;
517
+ return {
518
+ ...toArtist(requireField(body.artist, uri, res.status, "artist")),
519
+ ...body.hotSongs ? { topSongs: toSongs(body.hotSongs) } : {}
520
+ };
521
+ };
522
+ const getArtistDetail = async (id, options) => {
523
+ const res = await request(URI_ARTIST_HEAD_INFO, { id }, "eapi", options);
524
+ const body = res.body;
525
+ return toArtist(body.data?.artist ?? body.data ?? requireField(body.artist, URI_ARTIST_HEAD_INFO, res.status, "artist"));
526
+ };
527
+ const getArtistSongs = async (params, options) => {
528
+ const body = (await request(URI_ARTIST_SONGS, {
529
+ id: params.id,
530
+ private_cloud: "true",
531
+ work_type: 1,
532
+ order: params.order ?? "hot",
533
+ offset: params.offset ?? 0,
534
+ limit: params.limit ?? 100
535
+ }, "eapi", options)).body;
536
+ return toSongs(body.songs);
537
+ };
538
+ const getArtistTopSongs = async (id, options) => {
539
+ const body = (await request(URI_ARTIST_TOP_SONG, { id }, "weapi", options)).body;
540
+ return toSongs(body.songs);
541
+ };
542
+ const getArtistAlbums = async (params, options) => {
543
+ const body = (await request(`${URI_ARTIST_ALBUMS}/${params.id}`, {
544
+ limit: params.limit ?? 30,
545
+ offset: params.offset ?? 0,
546
+ total: true
547
+ }, "weapi", options)).body;
548
+ const total = num(body.total);
549
+ return {
550
+ ...total !== void 0 ? { total } : {},
551
+ ...body.more !== void 0 ? { more: body.more } : {},
552
+ albums: toAlbums(body.hotAlbums)
553
+ };
554
+ };
555
+ const getArtistList = async (params, options) => {
556
+ const body = (await request(URI_ARTIST_LIST, {
557
+ initial: toArtistInitial(params.initial),
558
+ offset: params.offset ?? 0,
559
+ limit: params.limit ?? 30,
560
+ total: true,
561
+ type: params.type ?? "1",
562
+ area: params.area
563
+ }, "weapi", options)).body;
564
+ const total = num(body.total ?? body.artistCount);
565
+ return {
566
+ ...total !== void 0 ? { total } : {},
567
+ ...body.more !== void 0 ? { more: body.more } : {},
568
+ artists: toArtists(body.artists)
569
+ };
570
+ };
571
+ const getArtistDesc = async (id, options) => {
572
+ const body = (await request(URI_ARTIST_INTRO, { id }, "weapi", options)).body;
573
+ return {
574
+ briefDesc: body.briefDesc ?? "",
575
+ sections: (body.introduction ?? []).map((item) => ({
576
+ title: item.ti ?? "",
577
+ text: item.tx ?? ""
578
+ }))
579
+ };
580
+ };
581
+ const getArtistMvs = async (params, options) => {
582
+ const body = (await request(URI_ARTIST_MVS, {
583
+ artistId: params.id,
584
+ limit: params.limit,
585
+ offset: params.offset,
586
+ total: true
587
+ }, "weapi", options)).body;
588
+ return toMvRefs(body.mvs);
589
+ };
590
+ const getArtistVideos = async (params, options) => {
591
+ const body = (await request(URI_ARTIST_VIDEO, {
592
+ artistId: params.id,
593
+ page: JSON.stringify({
594
+ size: params.size ?? 10,
595
+ cursor: params.cursor ?? 0
596
+ }),
597
+ tab: 0,
598
+ order: params.order ?? 0
599
+ }, "weapi", options)).body;
600
+ const data = body.data ?? {};
601
+ const videos = [];
602
+ for (const record of data.records ?? []) {
603
+ const base = record.resource?.mlogBaseData;
604
+ const id = num(base?.id);
605
+ if (id === void 0) continue;
606
+ const playCount = num(record.resource?.mlogExtVO?.playCount);
607
+ const duration = num(base?.duration);
608
+ videos.push({
609
+ id,
610
+ name: base?.text ?? "",
611
+ coverUrl: base?.coverUrl ?? "",
612
+ ...playCount !== void 0 ? { playCount } : {},
613
+ ...duration !== void 0 ? { duration } : {}
614
+ });
615
+ }
616
+ return {
617
+ hasMore: data.page?.more ?? body.hasMore ?? false,
618
+ videos
619
+ };
620
+ };
621
+ //#endregion
622
+ //#region src/utils/parseRelatedPlaylists.ts
623
+ const RELATED_PLAYLIST = /<div class="cver u-cover u-cover-3">[\s\S]*?<img src="([^"]+)">[\s\S]*?<a class="sname f-fs1 s-fc0" href="([^"]+)"[^>]*>([^<]+?)<\/a>[\s\S]*?<a class="nm nm f-thide s-fc3" href="([^"]+)"[^>]*>([^<]+?)<\/a>/g;
624
+ const parseRelatedPlaylists = (html) => {
625
+ const playlists = [];
626
+ for (const match of html.matchAll(RELATED_PLAYLIST)) {
627
+ const [, cover = "", playlistHref = "", name = "", userHref = "", user = ""] = match;
628
+ playlists.push({
629
+ id: Number(playlistHref.replace("/playlist?id=", "")),
630
+ name,
631
+ coverUrl: cover.replace(/\?param=.*$/, ""),
632
+ creator: {
633
+ id: Number(userHref.replace("/user/home?id=", "")),
634
+ name: user
635
+ }
636
+ });
637
+ }
638
+ return playlists;
639
+ };
640
+ //#endregion
1
641
  //#region src/utils/mergeLyricTimelines.ts
2
642
  function mergeLyricTimelines(original, translation) {
3
643
  const originalMap = /* @__PURE__ */ new Map();
@@ -23,61 +663,459 @@ function mergeLyricTimelines(original, translation) {
23
663
  return result;
24
664
  }
25
665
  //#endregion
26
- //#region src/index.ts
27
- const BASE_URL = "https://music.163.com/api/";
28
- const getPlaylistDetail = async (id) => {
29
- const data = await (await fetch(`${BASE_URL}/v6/playlist/detail?id=${id}`)).json();
30
- return {
31
- id: data.playlist.id,
32
- name: data.playlist.name,
33
- coverImgId: data.playlist.coverImgId,
34
- coverImgUrl: data.playlist.coverImgUrl,
35
- userId: data.playlist.userId,
36
- createTime: data.playlist.createTime,
37
- songs: data.playlist.trackIds.map((t) => ({
38
- id: t.id,
39
- addTime: t.at
40
- }))
41
- };
42
- };
666
+ //#region src/utils/parseLrc.ts
43
667
  const parseLrc = (lrc) => lrc.split("\n").filter((l) => l.startsWith("[")).map((l) => {
44
668
  const match = l.match(/^\[(\d{2}):(\d{2}(?:\.\d{2,3})?)\](.*)/);
45
669
  if (!match) return null;
46
- const minutes = parseInt(match[1], 10);
47
- const seconds = parseFloat(match[2]);
670
+ const [, minutes = "0", seconds = "0", text = ""] = match;
48
671
  return {
49
- time: minutes * 60 + seconds,
50
- text: match[3].trim()
672
+ time: parseInt(minutes, 10) * 60 + parseFloat(seconds),
673
+ text: text.trim()
51
674
  };
52
675
  }).filter((e) => e !== null);
53
- const getLyric = async (id) => {
54
- const data = await (await fetch(`${BASE_URL}/song/lyric?id=${id}&lv=-1&tv=-1`)).json();
55
- return {
56
- lines: mergeLyricTimelines(parseLrc(data.lrc.lyric), data.tlyric?.lyric ? parseLrc(data.tlyric.lyric) : []),
57
- ...data.transUser ? { translator: data.transUser } : {}
58
- };
59
- };
60
- const getSongsDetail = async (ids) => {
61
- const query = JSON.stringify(ids.map((id) => ({ id })));
62
- return (await (await fetch(`${BASE_URL}/v3/song/detail?c=${query}`)).json()).songs.map((s) => ({
63
- id: s.id,
64
- name: s.name,
65
- artists: s.ar.map((a) => ({
66
- id: a.id,
67
- name: a.name
68
- })),
69
- album: {
70
- id: s.al.id,
71
- name: s.al.name,
72
- picUrl: s.al.picUrl
73
- },
74
- duration: s.dt
676
+ //#endregion
677
+ //#region src/utils/parseYrc.ts
678
+ const YRC_LINE = /^\[(\d+),(\d+)\](.*)$/;
679
+ const YRC_WORD = /(\d+),(\d+),\d+,(\d+)\)([^()]*)/g;
680
+ const parseYrc = (yrc) => {
681
+ const lines = [];
682
+ for (const raw of yrc.split("\n")) {
683
+ const match = raw.match(YRC_LINE);
684
+ if (!match) continue;
685
+ const [, start = "0", span = "0", body = ""] = match;
686
+ const words = [];
687
+ for (const word of body.matchAll(YRC_WORD)) {
688
+ const [, wordStart = "0", , wordSpan = "0", text = ""] = word;
689
+ words.push({
690
+ time: Number(wordStart),
691
+ duration: Number(wordSpan),
692
+ text
693
+ });
694
+ }
695
+ lines.push({
696
+ time: Number(start),
697
+ duration: Number(span),
698
+ words
699
+ });
700
+ }
701
+ return lines;
702
+ };
703
+ //#endregion
704
+ //#region src/apis/song.ts
705
+ const URI_SONG_DETAIL = "/api/v3/song/detail";
706
+ const URI_SONG_URL = "/api/song/enhance/player/url";
707
+ const URI_LYRIC = "/api/song/lyric";
708
+ const URI_LYRIC_NEW = "/api/song/lyric/v1";
709
+ const URI_SIMI_SONG = "/api/link/position/show/resource";
710
+ const buildSongDetailQuery = (ids) => `[${ids.map((id) => `{"id":${id}}`).join(",")}]`;
711
+ const getSongsDetail = async (ids, options) => {
712
+ const body = (await request(URI_SONG_DETAIL, { c: buildSongDetailQuery(ids) }, "weapi", options)).body;
713
+ return toSongs(body.songs);
714
+ };
715
+ const readFreeTrialInfo = (raw) => {
716
+ if (!raw) return void 0;
717
+ const start = num(raw.start);
718
+ const end = num(raw.end);
719
+ if (start === void 0 || end === void 0) return void 0;
720
+ return {
721
+ start,
722
+ end
723
+ };
724
+ };
725
+ const getSongUrl = async (params, options) => {
726
+ const ids = String(params.id).split(",");
727
+ const items = [...(await request(URI_SONG_URL, {
728
+ ids: JSON.stringify(ids),
729
+ br: params.br ?? 999e3
730
+ }, "eapi", options)).body.data ?? []].sort((a, b) => ids.indexOf(String(a.id)) - ids.indexOf(String(b.id)));
731
+ const urls = [];
732
+ for (const item of items) {
733
+ const id = num(item.id);
734
+ if (id === void 0) continue;
735
+ const fee = num(item.fee);
736
+ const freeTrialInfo = readFreeTrialInfo(item.freeTrialInfo);
737
+ urls.push({
738
+ id,
739
+ url: item.url ?? null,
740
+ br: num(item.br) ?? 0,
741
+ size: num(item.size) ?? 0,
742
+ ...item.level ? { level: item.level } : {},
743
+ ...fee !== void 0 ? { fee } : {},
744
+ ...freeTrialInfo !== void 0 ? { freeTrialInfo } : {}
745
+ });
746
+ }
747
+ return urls;
748
+ };
749
+ const getLyric = async (id, options) => {
750
+ const body = (await request(URI_LYRIC, {
751
+ id,
752
+ tv: -1,
753
+ lv: -1,
754
+ rv: -1,
755
+ kv: -1,
756
+ _nmclfl: 1
757
+ }, "eapi", options)).body;
758
+ const translationText = body.tlyric?.lyric;
759
+ const translatorId = num(body.transUser?.id);
760
+ return {
761
+ lines: mergeLyricTimelines(parseLrc(body.lrc?.lyric ?? ""), translationText ? parseLrc(translationText) : []),
762
+ ...translatorId !== void 0 ? { translator: {
763
+ id: translatorId,
764
+ nickname: body.transUser?.nickname ?? ""
765
+ } } : {}
766
+ };
767
+ };
768
+ const getLyricNew = async (id, options) => {
769
+ const body = (await request(URI_LYRIC_NEW, {
770
+ id,
771
+ cp: false,
772
+ tv: 0,
773
+ lv: 0,
774
+ rv: 0,
775
+ kv: 0,
776
+ yv: 0,
777
+ ytv: 0,
778
+ yrv: 0
779
+ }, "eapi", options)).body;
780
+ const translationText = body.tlyric?.lyric;
781
+ const romaText = body.romalrc?.lyric;
782
+ const yrcText = body.yrc?.lyric;
783
+ const wordLines = yrcText ? parseYrc(yrcText) : void 0;
784
+ return {
785
+ lines: mergeLyricTimelines(parseLrc(body.lrc?.lyric ?? ""), translationText ? parseLrc(translationText) : []),
786
+ ...wordLines ? { wordLines } : {},
787
+ ...romaText ? { romaLines: parseLrc(romaText) } : {}
788
+ };
789
+ };
790
+ const checkMusic = async (params, options) => {
791
+ const body = (await request(URI_SONG_URL, {
792
+ ids: `[${params.id}]`,
793
+ br: params.br ?? 999e3
794
+ }, "weapi", options)).body;
795
+ const first = body.data?.[0];
796
+ const available = num(body.code) === 200 && num(first?.code) === 200;
797
+ return {
798
+ available,
799
+ message: available ? "ok" : first?.message ?? "亲爱的,暂无版权"
800
+ };
801
+ };
802
+ const getSimilarSongs = async (id, options) => {
803
+ const res = await request(URI_SIMI_SONG, {
804
+ positionCode: "toolBarRcmdSong",
805
+ resourceId: id,
806
+ resourceType: "song"
807
+ }, "eapi", options);
808
+ const body = res.body;
809
+ const ids = [];
810
+ for (const item of body.data?.commonResourceList ?? []) {
811
+ const resourceId = num(item.extraMap?.songId ?? item.resourceId);
812
+ if (resourceId !== void 0) ids.push(resourceId);
813
+ }
814
+ if (ids.length > 0) return getSongsDetail(ids, options);
815
+ const songs = toSongs(body.songs);
816
+ if (songs.length > 0) return songs;
817
+ throw new NeteaseApiError(URI_SIMI_SONG, -1, res.status, {
818
+ code: -1,
819
+ msg: "相似歌曲响应中未找到歌曲 id"
820
+ });
821
+ };
822
+ //#endregion
823
+ //#region src/apis/playlist.ts
824
+ const URI_PLAYLIST_DETAIL = "/api/v6/playlist/detail";
825
+ const URI_PLAYLIST_DYNAMIC = "/api/playlist/detail/dynamic";
826
+ const URI_HIGH_QUALITY_TAGS = "/api/playlist/highquality/tags";
827
+ const URI_PLAYLIST_LIST = "/api/playlist/list";
828
+ const URI_HIGH_QUALITY_LIST = "/api/playlist/highquality/list";
829
+ const URI_PLAYLIST_CATALOGUE = "/api/playlist/catalogue";
830
+ const getPlaylistDetail = async (id, options) => {
831
+ const { playlist } = (await request(URI_PLAYLIST_DETAIL, {
832
+ id,
833
+ n: 1e5,
834
+ s: 8
835
+ }, "eapi", options)).body;
836
+ const songs = [];
837
+ for (const track of playlist.trackIds ?? []) {
838
+ const trackId = num(track.id);
839
+ const addTime = num(track.at);
840
+ if (trackId === void 0 || addTime === void 0) continue;
841
+ songs.push({
842
+ id: trackId,
843
+ addTime
844
+ });
845
+ }
846
+ return {
847
+ id: num(playlist.id) ?? 0,
848
+ name: playlist.name ?? "",
849
+ creatorId: num(playlist.userId) ?? 0,
850
+ coverUrl: playlist.coverImgUrl ?? "",
851
+ createTime: num(playlist.createTime) ?? 0,
852
+ songs
853
+ };
854
+ };
855
+ const getPlaylistTracks = async (params, options) => {
856
+ const { playlist } = (await request(URI_PLAYLIST_DETAIL, {
857
+ id: params.id,
858
+ n: 1e5,
859
+ s: 8
860
+ }, "eapi", options)).body;
861
+ const offset = params.offset ?? 0;
862
+ const limit = params.limit ?? 1e3;
863
+ const ids = [];
864
+ for (const track of (playlist.trackIds ?? []).slice(offset, offset + limit)) {
865
+ const trackId = num(track.id);
866
+ if (trackId !== void 0) ids.push(trackId);
867
+ }
868
+ return getSongsDetail(ids, options);
869
+ };
870
+ const getPlaylistDetailDynamic = async (id, options) => {
871
+ const body = (await request(URI_PLAYLIST_DYNAMIC, {
872
+ id,
873
+ n: 1e5,
874
+ s: 8
875
+ }, "eapi", options)).body;
876
+ const commentCount = num(body.commentCount);
877
+ const shareCount = num(body.shareCount);
878
+ const playCount = num(body.playCount);
879
+ const subscribedCount = num(body.subscribedCount);
880
+ const playedCount = num(body.playedCount);
881
+ return {
882
+ ...commentCount !== void 0 ? { commentCount } : {},
883
+ ...shareCount !== void 0 ? { shareCount } : {},
884
+ ...playCount !== void 0 ? { playCount } : {},
885
+ ...subscribedCount !== void 0 ? { subscribedCount } : {},
886
+ ...playedCount !== void 0 ? { playedCount } : {}
887
+ };
888
+ };
889
+ const getHighQualityTags = async (options) => {
890
+ return ((await request(URI_HIGH_QUALITY_TAGS, {}, "weapi", options)).body.tags ?? []).map((tag) => ({
891
+ name: tag.name ?? "",
892
+ hot: tag.hot ?? false
75
893
  }));
76
894
  };
895
+ const toPlaylistPage = (body) => {
896
+ const total = num(body.total);
897
+ return {
898
+ ...total !== void 0 ? { total } : {},
899
+ ...body.more !== void 0 ? { more: body.more } : {},
900
+ playlists: toPlaylistSummaries(body.playlists)
901
+ };
902
+ };
903
+ const getTopPlaylists = async (params, options) => {
904
+ return toPlaylistPage((await request(URI_PLAYLIST_LIST, {
905
+ cat: params.cat ?? "全部",
906
+ order: params.order ?? "hot",
907
+ limit: params.limit ?? 50,
908
+ offset: params.offset ?? 0,
909
+ total: true
910
+ }, "weapi", options)).body);
911
+ };
912
+ const getHighQualityPlaylists = async (params, options) => {
913
+ return toPlaylistPage((await request(URI_HIGH_QUALITY_LIST, {
914
+ cat: params.cat ?? "全部",
915
+ limit: params.limit ?? 50,
916
+ lasttime: params.before ?? 0,
917
+ total: true
918
+ }, "weapi", options)).body);
919
+ };
920
+ const getPlaylistCategories = async (options) => {
921
+ const body = (await request(URI_PLAYLIST_CATALOGUE, {}, "eapi", options)).body;
922
+ const categories = body.categories ?? {};
923
+ const sub = body.sub ?? [];
924
+ return Object.keys(categories).sort((a, b) => Number(a) - Number(b)).map((key) => {
925
+ const category = Number(key);
926
+ return {
927
+ name: categories[key] ?? "",
928
+ subcategories: sub.filter((item) => num(item.category) === category).map((item) => ({
929
+ name: item.name ?? "",
930
+ category
931
+ }))
932
+ };
933
+ });
934
+ };
935
+ const getRelatedPlaylists = async (id, options) => {
936
+ const url = `${WEAPI_DOMAIN}/playlist?id=${id}`;
937
+ const res = await getText(url, options?.timeout ?? 1e4);
938
+ if (res.status !== 200) throw new NeteaseApiError(url, -1, res.status, {
939
+ code: -1,
940
+ msg: res.text.slice(0, 200)
941
+ });
942
+ const playlists = parseRelatedPlaylists(res.text);
943
+ if (playlists.length === 0 || playlists.some((item) => !item.coverUrl)) throw new NeteaseApiError(url, -1, res.status, {
944
+ code: -1,
945
+ msg: "相关歌单页面结构可能已变化"
946
+ });
947
+ return playlists;
948
+ };
949
+ //#endregion
950
+ //#region src/apis/search.ts
951
+ const URI_SEARCH = "/api/search/get";
952
+ const URI_SEARCH_VOICE = "/api/search/voice/get";
953
+ const URI_CLOUD_SEARCH = "/api/cloudsearch/pc";
954
+ const URI_SEARCH_SUGGEST = "/api/search/suggest/web";
955
+ const URI_SEARCH_SUGGEST_KEYWORD = "/api/search/suggest/keyword";
956
+ const URI_SEARCH_HOT = "/api/search/hot";
957
+ const URI_HOT_SEARCH_DETAIL = "/api/hotsearchlist/get";
958
+ const URI_DEFAULT_KEYWORD = "/api/search/defaultkeyword/get";
959
+ const URI_SEARCH_MULTIMATCH = "/api/search/suggest/multimatch";
960
+ const toSearchResult = (raw, total) => {
961
+ const lyrics = raw.lyrics?.map((item) => ({
962
+ id: num(item.id) ?? 0,
963
+ name: item.name ?? "",
964
+ artists: (item.artists ?? []).map(toArtistRef)
965
+ }));
966
+ return {
967
+ ...raw.songs ? { songs: toSongs(raw.songs) } : {},
968
+ ...raw.artists ? { artists: toArtists(raw.artists) } : {},
969
+ ...raw.albums ? { albums: toAlbums(raw.albums) } : {},
970
+ ...raw.playlists ? { playlists: toPlaylistSummaries(raw.playlists) } : {},
971
+ ...raw.userprofiles ? { users: raw.userprofiles.map(toUserRef) } : {},
972
+ ...raw.mvs ? { mvs: toMvRefs(raw.mvs) } : {},
973
+ ...raw.videos ? { videos: toVideoRefs(raw.videos) } : {},
974
+ ...lyrics ? { lyrics } : {},
975
+ ...total !== void 0 ? { total } : {}
976
+ };
977
+ };
978
+ const toVoiceSearchResult = (raw, status) => {
979
+ const container = raw.result ?? {};
980
+ const songs = container.songs ?? raw.songs;
981
+ if (songs) return toSearchResult({
982
+ ...container,
983
+ songs
984
+ }, num(container.songCount ?? raw.total));
985
+ const resources = container.resources ?? raw.resources;
986
+ if (resources) {
987
+ const radios = [];
988
+ for (const item of resources) {
989
+ const id = num(item.id ?? item.resourceId);
990
+ if (id === void 0) continue;
991
+ radios.push({
992
+ id,
993
+ name: item.name ?? item.title ?? "",
994
+ coverUrl: item.coverUrl ?? item.picUrl ?? "",
995
+ ...item.djName ? { djName: item.djName } : {}
996
+ });
997
+ }
998
+ return { radios };
999
+ }
1000
+ throw new NeteaseApiError(URI_SEARCH_VOICE, -1, status, {
1001
+ code: -1,
1002
+ msg: "响应中既没有 songs 也没有 resources"
1003
+ });
1004
+ };
1005
+ const search = async (params, options) => {
1006
+ if (params.type === 2e3) {
1007
+ const voice = await request(URI_SEARCH_VOICE, {
1008
+ keyword: params.keywords,
1009
+ scene: "normal",
1010
+ limit: params.limit ?? 30,
1011
+ offset: params.offset ?? 0
1012
+ }, "eapi", options);
1013
+ return toVoiceSearchResult(voice.body, voice.status);
1014
+ }
1015
+ const body = (await request(URI_SEARCH, {
1016
+ s: params.keywords,
1017
+ type: params.type ?? 1,
1018
+ limit: params.limit ?? 30,
1019
+ offset: params.offset ?? 0
1020
+ }, "eapi", options)).body;
1021
+ const container = body.result ?? {};
1022
+ return toSearchResult(container, num(container.songCount ?? container.albumCount ?? body.total));
1023
+ };
1024
+ const cloudSearch = async (params, options) => {
1025
+ const body = (await request(URI_CLOUD_SEARCH, {
1026
+ s: params.keywords,
1027
+ type: params.type ?? 1,
1028
+ limit: params.limit ?? 30,
1029
+ offset: params.offset ?? 0,
1030
+ total: true
1031
+ }, "eapi", options)).body;
1032
+ const container = body.result ?? {};
1033
+ return toSearchResult(container, num(container.songCount ?? container.albumCount ?? body.total));
1034
+ };
1035
+ const getSearchSuggest = async (params, options) => {
1036
+ const container = (await request(params.mobile ? URI_SEARCH_SUGGEST_KEYWORD : URI_SEARCH_SUGGEST, { s: params.keywords ?? "" }, "weapi", options)).body.result ?? {};
1037
+ return {
1038
+ keywords: container.order ?? [],
1039
+ songs: toSongs(container.songs),
1040
+ artists: toArtists(container.artists),
1041
+ albums: toAlbums(container.albums)
1042
+ };
1043
+ };
1044
+ const getHotSearches = async (options) => {
1045
+ return ((await request(URI_SEARCH_HOT, { type: 1111 }, "eapi", options)).body.result?.hots ?? []).map((item) => ({
1046
+ first: item.first ?? "",
1047
+ ...item.second !== void 0 && item.second !== null ? { second: item.second } : {},
1048
+ ...typeof item.third === "string" ? { third: item.third } : {}
1049
+ }));
1050
+ };
1051
+ const getHotSearchDetail = async (options) => {
1052
+ return ((await request(URI_HOT_SEARCH_DETAIL, {}, "weapi", options)).body.data ?? []).map((item, index) => ({
1053
+ keyword: item.searchWord ?? "",
1054
+ position: num(item.position) ?? index + 1,
1055
+ ...item.content ? { content: item.content } : {},
1056
+ ...item.iconUrl ? { iconUrl: item.iconUrl } : {}
1057
+ }));
1058
+ };
1059
+ const getDefaultSearchKeyword = async (options) => {
1060
+ const body = (await request(URI_DEFAULT_KEYWORD, {}, "eapi", options)).body;
1061
+ return body.data?.realkeyword ?? body.data?.showKeyword ?? "";
1062
+ };
1063
+ const searchMultimatch = async (params, options) => {
1064
+ const container = (await request(URI_SEARCH_MULTIMATCH, {
1065
+ type: params.type ?? 1,
1066
+ s: params.keywords ?? ""
1067
+ }, "weapi", options)).body.result ?? {};
1068
+ const songs = container.songs ?? container.song;
1069
+ const artists = container.artists ?? container.artist;
1070
+ const albums = container.albums ?? container.album;
1071
+ const playlists = container.playlists ?? container.playlist;
1072
+ return {
1073
+ ...songs ? { songs: toSongs(songs) } : {},
1074
+ ...artists ? { artists: toArtists(artists) } : {},
1075
+ ...albums ? { albums: toAlbums(albums) } : {},
1076
+ ...playlists ? { playlists: toPlaylistSummaries(playlists) } : {}
1077
+ };
1078
+ };
1079
+ //#endregion
1080
+ //#region src/index.ts
77
1081
  var src_default = {
78
- getPlaylistDetail,
1082
+ search,
1083
+ cloudSearch,
1084
+ getSearchSuggest,
1085
+ getHotSearches,
1086
+ getHotSearchDetail,
1087
+ getDefaultSearchKeyword,
1088
+ searchMultimatch,
1089
+ getSongsDetail,
1090
+ getSongUrl,
79
1091
  getLyric,
80
- getSongsDetail
1092
+ getLyricNew,
1093
+ checkMusic,
1094
+ getSimilarSongs,
1095
+ getPlaylistDetail,
1096
+ getPlaylistTracks,
1097
+ getPlaylistDetailDynamic,
1098
+ getHighQualityTags,
1099
+ getTopPlaylists,
1100
+ getHighQualityPlaylists,
1101
+ getPlaylistCategories,
1102
+ getRelatedPlaylists,
1103
+ getArtist,
1104
+ getArtistDetail,
1105
+ getArtistSongs,
1106
+ getArtistTopSongs,
1107
+ getArtistAlbums,
1108
+ getArtistList,
1109
+ getArtistDesc,
1110
+ getArtistMvs,
1111
+ getArtistVideos,
1112
+ getAlbum,
1113
+ getAlbumProduct,
1114
+ getAlbumDynamic,
1115
+ getAlbumSaleBoard,
1116
+ getAlbumPrivileges,
1117
+ getAlbumList,
1118
+ getNewestAlbums
81
1119
  };
82
1120
  //#endregion
83
- export { src_default as default, getLyric, getPlaylistDetail, getSongsDetail };
1121
+ export { NeteaseApiError, checkMusic, cloudSearch, src_default as default, getAlbum, getAlbumDynamic, getAlbumList, getAlbumPrivileges, getAlbumProduct, getAlbumSaleBoard, getArtist, getArtistAlbums, getArtistDesc, getArtistDetail, getArtistList, getArtistMvs, getArtistSongs, getArtistTopSongs, getArtistVideos, getDefaultSearchKeyword, getHighQualityPlaylists, getHighQualityTags, getHotSearchDetail, getHotSearches, getLyric, getLyricNew, getNewestAlbums, getPlaylistCategories, getPlaylistDetail, getPlaylistDetailDynamic, getPlaylistTracks, getRelatedPlaylists, getSearchSuggest, getSimilarSongs, getSongUrl, getSongsDetail, getTopPlaylists, request, search, searchMultimatch };