@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/README.md +113 -28
- package/dist/index.cjs +1119 -45
- package/dist/index.d.cts +429 -33
- package/dist/index.d.mts +429 -33
- package/dist/index.mjs +1084 -46
- package/package.json +15 -4
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/
|
|
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 =
|
|
47
|
-
const seconds = parseFloat(match[2]);
|
|
670
|
+
const [, minutes = "0", seconds = "0", text = ""] = match;
|
|
48
671
|
return {
|
|
49
|
-
time: minutes * 60 + seconds,
|
|
50
|
-
text:
|
|
672
|
+
time: parseInt(minutes, 10) * 60 + parseFloat(seconds),
|
|
673
|
+
text: text.trim()
|
|
51
674
|
};
|
|
52
675
|
}).filter((e) => e !== null);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
1082
|
+
search,
|
|
1083
|
+
cloudSearch,
|
|
1084
|
+
getSearchSuggest,
|
|
1085
|
+
getHotSearches,
|
|
1086
|
+
getHotSearchDetail,
|
|
1087
|
+
getDefaultSearchKeyword,
|
|
1088
|
+
searchMultimatch,
|
|
1089
|
+
getSongsDetail,
|
|
1090
|
+
getSongUrl,
|
|
79
1091
|
getLyric,
|
|
80
|
-
|
|
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 };
|