inki-music-api 2.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.
@@ -0,0 +1,1146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LxProjectApi = void 0;
4
+ const zlib_1 = require("zlib");
5
+ const TX_SEARCH_HEADERS = { 'User-Agent': 'QQMusic 14090508(android 12)' };
6
+ const KG_HEADERS = {
7
+ 'KG-RC': 1,
8
+ 'KG-THash': 'expand_search_manager.cpp:852736169:451',
9
+ 'User-Agent': 'KuGou2012-9020-ExpandSearchManager',
10
+ };
11
+ const KW_RANK_MAP = { hot: '16', new: '17', rise: '93' };
12
+ const KG_RANK_MAP = { top500: '8888', hot: '23784', rise: '6666' };
13
+ const TX_RANK_MAP = { hot: '26', new: '27', rise: '62' };
14
+ const WY_RANK_MAP = { hot: '3778678', new: '3779629', rise: '19723756' };
15
+ const MG_RANK_MAP = { hot: '27186466', new: '27553319', rise: '75959118' };
16
+ const decodeHtmlMap = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" };
17
+ const encKey = Buffer.from([
18
+ 0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69,
19
+ ]);
20
+ const asRecord = (value) => value && typeof value === 'object' ? value : {};
21
+ const asArray = (value) => (Array.isArray(value) ? value.map(asRecord) : []);
22
+ const asString = (value) => (typeof value === 'string' ? value : value == null ? '' : String(value));
23
+ const asNumber = (value) => {
24
+ const n = Number(value);
25
+ return Number.isFinite(n) ? n : 0;
26
+ };
27
+ const decodeName = (str = '') => String(str).replace(/&(amp|lt|gt|quot|#39);/g, (m) => decodeHtmlMap[m] || m);
28
+ const formatPlayTime = (time) => `${String(Math.trunc(Math.max(0, time) / 60)).padStart(2, '0')}:${String(Math.trunc(Math.max(0, time) % 60)).padStart(2, '0')}`;
29
+ const sizeFormate = (size) => {
30
+ if (!size)
31
+ return null;
32
+ const mb = size / (1024 * 1024);
33
+ return mb < 1 ? `${(size / 1024).toFixed(2)}KB` : `${mb.toFixed(2)}MB`;
34
+ };
35
+ const formatSingerName = (list, key = 'name') => asArray(list)
36
+ .map((item) => asString(item[key]))
37
+ .filter(Boolean)
38
+ .join('、');
39
+ const emptyLyric = () => ({ lyric: '', tlyric: '', rlyric: '', lxlyric: '' });
40
+ const baseMusicInfo = (source) => ({
41
+ songmid: '',
42
+ source,
43
+ name: '',
44
+ singer: '',
45
+ albumName: '',
46
+ interval: '00:00',
47
+ img: null,
48
+ types: [],
49
+ _types: {},
50
+ typeUrl: {},
51
+ });
52
+ const formatDate = (time) => {
53
+ const d = new Date(time);
54
+ const y = d.getFullYear();
55
+ const m = String(d.getMonth() + 1).padStart(2, '0');
56
+ const day = String(d.getDate()).padStart(2, '0');
57
+ const hh = String(d.getHours()).padStart(2, '0');
58
+ const mm = String(d.getMinutes()).padStart(2, '0');
59
+ const ss = String(d.getSeconds()).padStart(2, '0');
60
+ return `${y}-${m}-${day} ${hh}:${mm}:${ss}`;
61
+ };
62
+ const createComment = (item) => ({
63
+ id: item.id || '',
64
+ text: item.text || '',
65
+ time: item.time || 0,
66
+ timeStr: item.timeStr || '',
67
+ userName: item.userName || '',
68
+ avatar: item.avatar || '',
69
+ userId: item.userId || '',
70
+ likedCount: item.likedCount || 0,
71
+ });
72
+ const txHotTagReg = {
73
+ row: /class="c_bg_link js_tag_item" data-id="\w+">.+?<\/a>/g,
74
+ tag: /data-id="(\w+)">(.+?)<\/a>/,
75
+ };
76
+ const formatPlayCountText = (num) => {
77
+ if (num > 100000000)
78
+ return `${Math.trunc(num / 1000000) / 100}亿`;
79
+ if (num > 10000)
80
+ return `${Math.trunc(num / 100) / 100}万`;
81
+ return String(num);
82
+ };
83
+ const createSongListItem = (item) => ({
84
+ id: item.id || '',
85
+ name: item.name || '',
86
+ author: item.author || '',
87
+ img: item.img || '',
88
+ desc: item.desc || '',
89
+ play_count: item.play_count || '0',
90
+ total: item.total,
91
+ time: item.time,
92
+ source: item.source || '',
93
+ });
94
+ const createSongListDetailInfo = (info) => ({
95
+ name: info.name || '',
96
+ img: info.img || '',
97
+ desc: info.desc || '',
98
+ author: info.author || '',
99
+ play_count: info.play_count || '0',
100
+ });
101
+ const parseSongListId = (rawId, reg) => {
102
+ if (!/[?&:/]/.test(rawId))
103
+ return rawId;
104
+ const matched = reg.exec(rawId);
105
+ if (!matched)
106
+ return rawId;
107
+ return matched[1];
108
+ };
109
+ const mapKwTypes = (nMinfo) => {
110
+ const types = [];
111
+ const _types = {};
112
+ const reg = /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/g;
113
+ let matched;
114
+ while ((matched = reg.exec(nMinfo))) {
115
+ const size = String(matched[4]).toUpperCase();
116
+ if (matched[2] === '4000') {
117
+ types.push({ type: 'flac24bit', size });
118
+ _types.flac24bit = { size };
119
+ }
120
+ else if (matched[2] === '2000') {
121
+ types.push({ type: 'flac', size });
122
+ _types.flac = { size };
123
+ }
124
+ else if (matched[2] === '320') {
125
+ types.push({ type: '320k', size });
126
+ _types['320k'] = { size };
127
+ }
128
+ else if (matched[2] === '128') {
129
+ types.push({ type: '128k', size });
130
+ _types['128k'] = { size };
131
+ }
132
+ }
133
+ return { types: types.reverse(), _types };
134
+ };
135
+ const mapKgTypes = (item) => {
136
+ const types = [];
137
+ const _types = {};
138
+ const add = (key, type, sizeKey, hashKey) => {
139
+ const size = sizeFormate(asNumber(item[sizeKey]));
140
+ const hash = asString(item[hashKey]);
141
+ if (!size || !hash)
142
+ return;
143
+ types.push({ type, size, hash });
144
+ _types[key] = { size, hash };
145
+ };
146
+ add('128k', '128k', 'FileSize', 'FileHash');
147
+ add('320k', '320k', 'HQFileSize', 'HQFileHash');
148
+ add('flac', 'flac', 'SQFileSize', 'SQFileHash');
149
+ add('flac24bit', 'flac24bit', 'ResFileSize', 'ResFileHash');
150
+ if (!types.length) {
151
+ add('128k', '128k', 'filesize', 'hash');
152
+ add('320k', '320k', '320filesize', '320hash');
153
+ add('flac', 'flac', 'sqfilesize', 'sqhash');
154
+ add('flac24bit', 'flac24bit', 'filesize_high', 'hash_high');
155
+ }
156
+ return { types, _types };
157
+ };
158
+ const mapTxTypes = (file) => {
159
+ const types = [];
160
+ const _types = {};
161
+ const add = (key, type, sizeKey) => {
162
+ const size = sizeFormate(asNumber(file[sizeKey]));
163
+ if (!size)
164
+ return;
165
+ types.push({ type, size });
166
+ _types[key] = { size };
167
+ };
168
+ add('128k', '128k', 'size_128mp3');
169
+ add('320k', '320k', 'size_320mp3');
170
+ add('flac', 'flac', 'size_flac');
171
+ add('flac24bit', 'flac24bit', 'size_hires');
172
+ return { types, _types };
173
+ };
174
+ const parseKrc = (base64) => new Promise((resolve, reject) => {
175
+ const buf = Buffer.from(base64, 'base64').subarray(4);
176
+ for (let i = 0; i < buf.length; i++)
177
+ buf[i] = buf[i] ^ encKey[i % 16];
178
+ (0, zlib_1.inflate)(buf, (err, result) => {
179
+ if (err)
180
+ return reject(err);
181
+ resolve({
182
+ lyric: result
183
+ .toString()
184
+ .replace(/\r/g, '')
185
+ .replace(/<\d+,\d+>/g, ''),
186
+ tlyric: '',
187
+ rlyric: '',
188
+ lxlyric: '',
189
+ });
190
+ });
191
+ });
192
+ class LxProjectApi {
193
+ constructor(httpClient) {
194
+ this.httpClient = httpClient;
195
+ }
196
+ async http(url, options = {}) {
197
+ return this.httpClient.requestPromise(url, options);
198
+ }
199
+ async search(source, keywords, page = 1, limit = 20) {
200
+ if (source === 'kw') {
201
+ const response = asRecord((await this.http(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(keywords)}&pn=${page - 1}&rn=${limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`)).body);
202
+ const list = asArray(response.abslist).map((item) => {
203
+ const mapped = mapKwTypes(asString(item.N_MINFO));
204
+ return {
205
+ ...baseMusicInfo('kw'),
206
+ singer: decodeName(asString(item.ARTIST)),
207
+ name: decodeName(asString(item.SONGNAME)),
208
+ albumName: decodeName(asString(item.ALBUM)),
209
+ albumId: decodeName(asString(item.ALBUMID)),
210
+ interval: formatPlayTime(asNumber(item.DURATION)),
211
+ songmid: asString(item.MUSICRID).replace('MUSIC_', ''),
212
+ lrc: null,
213
+ ...mapped,
214
+ };
215
+ });
216
+ return {
217
+ list,
218
+ allPage: Math.ceil(asNumber(response.TOTAL) / limit),
219
+ total: asNumber(response.TOTAL),
220
+ limit,
221
+ source,
222
+ };
223
+ }
224
+ if (source === 'kg') {
225
+ const response = asRecord((await this.http(`https://songsearch.kugou.com/song_search_v2?keyword=${encodeURIComponent(keywords)}&page=${page}&pagesize=${limit}&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1`)).body);
226
+ const data = asRecord(response.data);
227
+ const list = asArray(data.lists).map((item) => {
228
+ const mapped = mapKgTypes(item);
229
+ return {
230
+ ...baseMusicInfo('kg'),
231
+ singer: decodeName(formatSingerName(item.Singers)),
232
+ name: decodeName(asString(item.SongName)),
233
+ albumName: decodeName(asString(item.AlbumName)),
234
+ albumId: asString(item.AlbumID),
235
+ interval: formatPlayTime(asNumber(item.Duration)),
236
+ songmid: asString(item.Audioid),
237
+ _interval: asNumber(item.Duration),
238
+ hash: asString(item.FileHash),
239
+ lrc: null,
240
+ ...mapped,
241
+ };
242
+ });
243
+ return { list, allPage: Math.ceil(asNumber(data.total) / limit), total: asNumber(data.total), limit, source };
244
+ }
245
+ if (source === 'tx') {
246
+ const response = asRecord((await this.http('https://u.y.qq.com/cgi-bin/musicu.fcg', {
247
+ method: 'post',
248
+ headers: TX_SEARCH_HEADERS,
249
+ body: {
250
+ comm: { ct: '11', cv: '14090508', tmeAppID: 'qqmusic' },
251
+ req: {
252
+ module: 'music.search.SearchCgiService',
253
+ method: 'DoSearchForQQMusicMobile',
254
+ param: {
255
+ search_type: 0,
256
+ query: keywords,
257
+ page_num: page,
258
+ num_per_page: limit,
259
+ highlight: 0,
260
+ nqc_flag: 0,
261
+ multi_zhida: 0,
262
+ cat: 2,
263
+ grp: 1,
264
+ sin: 0,
265
+ sem: 0,
266
+ },
267
+ },
268
+ },
269
+ })).body);
270
+ const req = asRecord(response.req);
271
+ const data = asRecord(req.data);
272
+ const body = asRecord(data.body);
273
+ const meta = asRecord(data.meta);
274
+ const list = asArray(body.item_song).map((item) => {
275
+ const file = asRecord(item.file);
276
+ const album = asRecord(item.album);
277
+ const mapped = mapTxTypes(file);
278
+ const albumId = asString(album.mid);
279
+ return {
280
+ ...baseMusicInfo('tx'),
281
+ singer: formatSingerName(item.singer),
282
+ name: `${asString(item.name)}${asString(item.title_extra)}`,
283
+ albumName: asString(album.name),
284
+ albumId,
285
+ interval: formatPlayTime(asNumber(item.interval)),
286
+ songId: asNumber(item.id),
287
+ albumMid: albumId,
288
+ strMediaMid: asString(file.media_mid),
289
+ songmid: asString(item.mid),
290
+ img: albumId ? `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg` : '',
291
+ ...mapped,
292
+ };
293
+ });
294
+ return {
295
+ list,
296
+ allPage: Math.ceil(asNumber(meta.estimate_sum) / limit),
297
+ total: asNumber(meta.estimate_sum),
298
+ limit,
299
+ source,
300
+ };
301
+ }
302
+ if (source === 'wy') {
303
+ const offset = (page - 1) * limit;
304
+ const response = asRecord((await this.http(`https://music.163.com/api/search/get/web?csrf_token=&s=${encodeURIComponent(keywords)}&type=1&offset=${offset}&limit=${limit}`)).body);
305
+ const result = asRecord(response.result);
306
+ const list = asArray(result.songs).map((item) => {
307
+ const album = asRecord(item.album);
308
+ return {
309
+ ...baseMusicInfo('wy'),
310
+ singer: formatSingerName(item.artists),
311
+ name: asString(item.name),
312
+ albumName: asString(album.name),
313
+ albumId: asString(album.id),
314
+ interval: formatPlayTime(asNumber(item.duration) / 1000),
315
+ songmid: asString(item.id),
316
+ img: asString(album.picUrl),
317
+ lrc: null,
318
+ types: [{ type: '128k', size: null }],
319
+ _types: { '128k': { size: null } },
320
+ };
321
+ });
322
+ return {
323
+ list,
324
+ allPage: Math.ceil(asNumber(result.songCount) / limit),
325
+ total: asNumber(result.songCount),
326
+ limit,
327
+ source,
328
+ };
329
+ }
330
+ if (source === 'mg') {
331
+ const response = asRecord((await this.http(`http://jadeite.migu.cn:7090/music_search/v3/search/searchAll?isCorrect=0&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(keywords)}&pageNo=${page}&sort=0&sid=USS`)).body);
332
+ const result = asRecord(response.songResultData);
333
+ const list = [];
334
+ for (const part of asArray(result.resultList)) {
335
+ for (const item of asArray(part)) {
336
+ const songmid = asString(item.songId);
337
+ const copyrightId = asString(item.copyrightId);
338
+ if (!songmid || !copyrightId)
339
+ continue;
340
+ list.push({
341
+ ...baseMusicInfo('mg'),
342
+ singer: formatSingerName(item.singerList),
343
+ name: asString(item.name),
344
+ albumName: asString(item.album),
345
+ albumId: asString(item.albumId),
346
+ songmid,
347
+ copyrightId,
348
+ interval: formatPlayTime(asNumber(item.duration)),
349
+ img: asString(item.img3) || asString(item.img2) || asString(item.img1) || null,
350
+ lrcUrl: asString(item.lrcUrl),
351
+ mrcUrl: asString(item.mrcurl),
352
+ trcUrl: asString(item.trcUrl),
353
+ types: [{ type: '128k', size: null }],
354
+ _types: { '128k': { size: null } },
355
+ });
356
+ }
357
+ }
358
+ return {
359
+ list,
360
+ allPage: Math.ceil(asNumber(result.totalCount) / limit),
361
+ total: asNumber(result.totalCount),
362
+ limit,
363
+ source,
364
+ };
365
+ }
366
+ throw new Error(`unsupported source: ${source}`);
367
+ }
368
+ async hot(source, page = 1, limit = 20) {
369
+ return this.rank(source, 'hot', page, limit);
370
+ }
371
+ async rank(source, rankType, page = 1, limit = 20) {
372
+ if (source === 'kw') {
373
+ const id = KW_RANK_MAP[rankType] || String(rankType || KW_RANK_MAP.hot);
374
+ const body = asRecord((await this.http(`http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${page - 1}&rn=${limit}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`)).body);
375
+ const list = asArray(body.musiclist).map((item, idx) => ({
376
+ ...baseMusicInfo('kw'),
377
+ rank: (page - 1) * limit + idx + 1,
378
+ singer: decodeName(asString(item.artist) || asString(item.ARTIST)),
379
+ name: decodeName(asString(item.name) || asString(item.SONGNAME)),
380
+ albumName: decodeName(asString(item.album) || asString(item.ALBUM)),
381
+ albumId: asString(item.albumid) || asString(item.ALBUMID),
382
+ songmid: asString(item.id) || asString(item.MUSICRID).replace('MUSIC_', ''),
383
+ interval: formatPlayTime(asNumber(item.duration) || asNumber(item.DURATION)),
384
+ img: asString(item.pic) || null,
385
+ ...mapKwTypes(asString(item.n_minfo) || asString(item.N_MINFO)),
386
+ }));
387
+ return { rankType: id, total: asNumber(body.num) || list.length, list, page, limit, source };
388
+ }
389
+ if (source === 'kg') {
390
+ const id = KG_RANK_MAP[rankType] || String(rankType || KG_RANK_MAP.hot);
391
+ const body = asRecord((await this.http(`http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=${limit}&area_code=1&page=${page}&rankid=${id}&with_res_tag=0&show_portrait_mv=1`)).body);
392
+ const data = asRecord(body.data);
393
+ const list = asArray(data.info).map((item, idx) => ({
394
+ ...baseMusicInfo('kg'),
395
+ rank: (page - 1) * limit + idx + 1,
396
+ singer: formatSingerName(item.authors, 'author_name'),
397
+ name: decodeName(asString(item.songname)),
398
+ albumName: decodeName(asString(item.remark)),
399
+ albumId: asString(item.album_id),
400
+ songmid: asString(item.audio_id),
401
+ interval: formatPlayTime(asNumber(item.duration)),
402
+ hash: asString(item.hash),
403
+ ...mapKgTypes(item),
404
+ }));
405
+ return { rankType: id, total: asNumber(data.total) || list.length, list, page, limit, source };
406
+ }
407
+ if (source === 'tx') {
408
+ const id = Number(TX_RANK_MAP[rankType] || rankType || TX_RANK_MAP.hot);
409
+ const body = asRecord((await this.http('https://u.y.qq.com/cgi-bin/musicu.fcg', {
410
+ method: 'post',
411
+ body: {
412
+ toplist: {
413
+ module: 'musicToplist.ToplistInfoServer',
414
+ method: 'GetDetail',
415
+ param: { topid: id, num: limit },
416
+ },
417
+ comm: { uin: 0, format: 'json', ct: 20, cv: 1859 },
418
+ },
419
+ })).body);
420
+ const rows = asArray(asRecord(asRecord(body.toplist).data).songInfoList);
421
+ const list = rows.map((item, idx) => {
422
+ const album = asRecord(item.album);
423
+ const file = asRecord(item.file);
424
+ const albumId = asString(album.mid);
425
+ return {
426
+ ...baseMusicInfo('tx'),
427
+ rank: (page - 1) * limit + idx + 1,
428
+ singer: formatSingerName(item.singer),
429
+ name: asString(item.title),
430
+ albumName: asString(album.name),
431
+ albumId,
432
+ interval: formatPlayTime(asNumber(item.interval)),
433
+ songId: asNumber(item.id),
434
+ albumMid: albumId,
435
+ strMediaMid: asString(file.media_mid),
436
+ songmid: asString(item.mid),
437
+ img: albumId ? `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg` : '',
438
+ ...mapTxTypes(file),
439
+ };
440
+ });
441
+ return { rankType: String(id), total: list.length, list, page: 1, limit, source };
442
+ }
443
+ if (source === 'wy') {
444
+ const id = WY_RANK_MAP[rankType] || String(rankType || WY_RANK_MAP.hot);
445
+ const body = asRecord((await this.http(`https://music.163.com/api/playlist/detail?id=${id}`)).body);
446
+ const tracks = asArray(asRecord(body.result).tracks);
447
+ const list = tracks.map((item, idx) => {
448
+ const album = asRecord(item.album);
449
+ return {
450
+ ...baseMusicInfo('wy'),
451
+ rank: (page - 1) * limit + idx + 1,
452
+ singer: formatSingerName(item.artists),
453
+ name: asString(item.name),
454
+ albumName: asString(album.name),
455
+ albumId: asString(album.id),
456
+ interval: formatPlayTime(asNumber(item.duration) / 1000),
457
+ songmid: asString(item.id),
458
+ img: asString(album.picUrl),
459
+ types: [{ type: '128k', size: null }],
460
+ _types: { '128k': { size: null } },
461
+ };
462
+ });
463
+ return { rankType: id, total: list.length, list, page: 1, limit, source };
464
+ }
465
+ if (source === 'mg') {
466
+ const id = MG_RANK_MAP[rankType] || String(rankType || MG_RANK_MAP.hot);
467
+ const body = asRecord((await this.http(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0`))
468
+ .body);
469
+ const list = asArray(asRecord(body.columnInfo).contents)
470
+ .map((item) => asRecord(item.objectInfo))
471
+ .filter((item) => Object.keys(item).length)
472
+ .map((item, idx) => ({
473
+ ...baseMusicInfo('mg'),
474
+ rank: (page - 1) * limit + idx + 1,
475
+ singer: formatSingerName(item.singerList),
476
+ name: asString(item.songName) || asString(item.name),
477
+ albumName: asString(item.album),
478
+ albumId: asString(item.albumId),
479
+ songmid: asString(item.songId),
480
+ copyrightId: asString(item.copyrightId),
481
+ interval: formatPlayTime(asNumber(item.duration)),
482
+ img: asString(item.img3) || asString(item.img2) || asString(item.img1) || null,
483
+ lrcUrl: asString(item.lrcUrl),
484
+ mrcUrl: asString(item.mrcurl),
485
+ trcUrl: asString(item.trcUrl),
486
+ types: [{ type: '128k', size: null }],
487
+ _types: { '128k': { size: null } },
488
+ }));
489
+ return { rankType: id, total: list.length, list, page, limit, source };
490
+ }
491
+ throw new Error(`unsupported source: ${source}`);
492
+ }
493
+ async lyric(source, musicInfo) {
494
+ if (source === 'kw') {
495
+ const body = asRecord((await this.http(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${musicInfo.songmid}`)).body);
496
+ const lyric = asArray(asRecord(body.data).lrclist)
497
+ .map((line) => `[${formatPlayTime(asNumber(line.time))}.${String(Math.trunc((asNumber(line.time) % 1) * 1000)).padStart(3, '0')}]${decodeName(asString(line.lineLyric))}`)
498
+ .join('\n');
499
+ return { ...emptyLyric(), lyric };
500
+ }
501
+ if (source === 'kg') {
502
+ const duration = musicInfo._interval ??
503
+ asNumber(musicInfo.interval.split(':')[0]) * 60 + asNumber(musicInfo.interval.split(':')[1]);
504
+ const search = asRecord((await this.http(`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(musicInfo.name)}&hash=${musicInfo.hash || ''}&timelength=${duration}&lrctxt=1`, { headers: KG_HEADERS })).body);
505
+ const cand = asRecord(asArray(search.candidates)[0]);
506
+ if (!Object.keys(cand).length)
507
+ return emptyLyric();
508
+ const down = asRecord((await this.http(`http://lyrics.kugou.com/download?ver=1&client=pc&id=${asString(cand.id)}&accesskey=${asString(cand.accesskey)}&fmt=${asNumber(cand.krctype) === 1 ? 'krc' : 'lrc'}&charset=utf8`, { headers: KG_HEADERS })).body);
509
+ if (asString(down.fmt) === 'krc')
510
+ return parseKrc(asString(down.content));
511
+ return { ...emptyLyric(), lyric: Buffer.from(asString(down.content), 'base64').toString('utf-8') };
512
+ }
513
+ if (source === 'tx') {
514
+ const body = asRecord((await this.http('https://u.y.qq.com/cgi-bin/musicu.fcg', {
515
+ method: 'post',
516
+ headers: { referer: 'https://y.qq.com' },
517
+ body: {
518
+ comm: { ct: '19', cv: '1859', uin: '0' },
519
+ req: {
520
+ method: 'GetPlayLyricInfo',
521
+ module: 'music.musichallSong.PlayLyricInfo',
522
+ param: { format: 'json', crypt: 0, qrc: 0, roma: 0, trans: 1, songID: musicInfo.songId || 0 },
523
+ },
524
+ },
525
+ })).body);
526
+ const data = asRecord(asRecord(body.req).data);
527
+ return { ...emptyLyric(), lyric: asString(data.lyric), tlyric: asString(data.trans) };
528
+ }
529
+ if (source === 'wy') {
530
+ const body = asRecord((await this.http(`https://music.163.com/api/song/lyric?id=${musicInfo.songmid}&lv=-1&tv=-1&rv=-1`)).body);
531
+ return {
532
+ ...emptyLyric(),
533
+ lyric: asString(asRecord(body.lrc).lyric),
534
+ tlyric: asString(asRecord(body.tlyric).lyric),
535
+ rlyric: asString(asRecord(body.romalrc).lyric),
536
+ };
537
+ }
538
+ if (source === 'mg') {
539
+ if (!musicInfo.lrcUrl)
540
+ return emptyLyric();
541
+ const lyricBody = (await this.http(musicInfo.lrcUrl)).body;
542
+ const tlyricBody = musicInfo.trcUrl ? (await this.http(musicInfo.trcUrl)).body : '';
543
+ return {
544
+ ...emptyLyric(),
545
+ lyric: typeof lyricBody === 'string' ? lyricBody : '',
546
+ tlyric: typeof tlyricBody === 'string' ? tlyricBody : '',
547
+ };
548
+ }
549
+ throw new Error(`unsupported source: ${source}`);
550
+ }
551
+ async pic(source, musicInfo) {
552
+ if (source === 'kw') {
553
+ const body = (await this.http(`http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=${musicInfo.songmid}`)).body;
554
+ return typeof body === 'string' && /^http/.test(body) ? body : null;
555
+ }
556
+ if (source === 'kg') {
557
+ const body = asRecord((await this.http('http://media.store.kugou.com/v1/get_res_privilege', {
558
+ method: 'post',
559
+ headers: KG_HEADERS,
560
+ body: {
561
+ appid: 1001,
562
+ area_code: '1',
563
+ behavior: 'play',
564
+ clientver: '9020',
565
+ need_hash_offset: 1,
566
+ relate: 1,
567
+ resource: [
568
+ {
569
+ album_audio_id: musicInfo.songmid,
570
+ album_id: musicInfo.albumId,
571
+ hash: musicInfo.hash,
572
+ id: 0,
573
+ name: `${musicInfo.singer} - ${musicInfo.name}.mp3`,
574
+ type: 'audio',
575
+ },
576
+ ],
577
+ token: '',
578
+ userid: 2626431536,
579
+ vip: 1,
580
+ },
581
+ })).body);
582
+ const info = asRecord(asRecord(asArray(body.data)[0]).info);
583
+ const image = asString(info.image);
584
+ const size = asString(asArray(info.imgsize)[0]);
585
+ return image ? (size ? image.replace('{size}', size) : image) : null;
586
+ }
587
+ if (source === 'tx')
588
+ return musicInfo.albumId ? `https://y.gtimg.cn/music/photo_new/T002R500x500M000${musicInfo.albumId}.jpg` : null;
589
+ if (source === 'wy') {
590
+ if (musicInfo.img)
591
+ return musicInfo.img;
592
+ const body = asRecord((await this.http(`https://music.163.com/api/song/detail/?id=${musicInfo.songmid}&ids=[${musicInfo.songmid}]`))
593
+ .body);
594
+ return asString(asRecord(asRecord(asArray(body.songs)[0]).album).picUrl) || null;
595
+ }
596
+ if (source === 'mg') {
597
+ if (musicInfo.img)
598
+ return musicInfo.img;
599
+ const body = asRecord((await this.http(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${musicInfo.songmid}`, {
600
+ headers: { Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu' },
601
+ })).body);
602
+ const url = asString(body.largePic) || asString(body.mediumPic) || asString(body.smallPic);
603
+ if (!url)
604
+ return null;
605
+ return /^https?:/.test(url) ? url : `http:${url}`;
606
+ }
607
+ throw new Error(`unsupported source: ${source}`);
608
+ }
609
+ async comment(source, musicInfo, page = 1, limit = 20, hot = false) {
610
+ if (source === 'kw') {
611
+ const type = hot ? 'get_rec_comment' : 'get_comment';
612
+ const countKey = hot ? 'hot_comments_counts' : 'comments_counts';
613
+ const listKey = hot ? 'hot_comments' : 'comments';
614
+ const body = asRecord((await this.http(`http://ncomment.kuwo.cn/com.s?f=web&type=${type}&aapiver=1&prod=kwplayer_ar_10.5.2.0&digest=15&sid=${musicInfo.songmid}&start=${limit * (page - 1)}&msgflag=1&count=${limit}&newver=3&uid=0`, {
615
+ headers: { 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)' },
616
+ })).body);
617
+ const total = asNumber(body[countKey]);
618
+ const comments = asArray(body[listKey]).map((item) => {
619
+ const time = asNumber(item.time) * 1000;
620
+ return createComment({
621
+ id: asString(item.id),
622
+ text: asString(item.msg),
623
+ time,
624
+ timeStr: formatDate(time),
625
+ userName: asString(item.u_name),
626
+ avatar: asString(item.u_pic),
627
+ userId: asString(item.u_id),
628
+ likedCount: asNumber(item.like_num),
629
+ });
630
+ });
631
+ return { source, comments, total, page, limit, maxPage: Math.ceil(total / limit) || 1 };
632
+ }
633
+ if (source === 'kg') {
634
+ const hash = musicInfo.hash || '';
635
+ const timestamp = Date.now();
636
+ const path = hot ? 'topliked' : 'newest';
637
+ const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0`;
638
+ const body = asRecord((await this.http(`http://m.comment.service.kugou.com/r/v1/rank/${path}?${params}`, {
639
+ headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)' },
640
+ })).body);
641
+ const total = asNumber(body.count);
642
+ const comments = asArray(body.list).map((item) => {
643
+ const user = asRecord(item.user);
644
+ const time = asNumber(item.addtime) * 1000;
645
+ return createComment({
646
+ id: asString(item.id),
647
+ text: asString(item.content),
648
+ time,
649
+ timeStr: formatDate(time),
650
+ userName: asString(user.name),
651
+ avatar: asString(user.avatar),
652
+ userId: asString(user.userid),
653
+ likedCount: asNumber(item.like),
654
+ });
655
+ });
656
+ return { source, comments, total, page, limit, maxPage: Math.ceil(total / limit) || 1 };
657
+ }
658
+ if (source === 'tx') {
659
+ const cmd = hot ? '9' : '8';
660
+ const body = asRecord((await this.http('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', {
661
+ method: 'POST',
662
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)' },
663
+ form: {
664
+ uin: '0',
665
+ format: 'json',
666
+ cid: '205360772',
667
+ reqtype: '2',
668
+ biztype: '1',
669
+ topid: String(musicInfo.songId || musicInfo.songmid),
670
+ cmd,
671
+ needmusiccrit: '1',
672
+ pagenum: page - 1,
673
+ pagesize: limit,
674
+ },
675
+ })).body);
676
+ const comment = asRecord(body.comment);
677
+ const total = asNumber(comment.commenttotal);
678
+ const comments = asArray(comment.commentlist).map((item) => {
679
+ const user = asRecord(item.rootcommentuin ? item : asRecord(item.middlecommentcontent));
680
+ const time = asNumber(item.time) * 1000;
681
+ return createComment({
682
+ id: asString(item.rootcommentid || item.commentid),
683
+ text: asString(item.rootcommentcontent || item.rootcommentcontentdetail || item.middlecommentcontent),
684
+ time,
685
+ timeStr: formatDate(time),
686
+ userName: asString(item.rootcommentnick || user.nick),
687
+ avatar: asString(item.avatarurl || ''),
688
+ userId: asString(item.rootcommentuin || ''),
689
+ likedCount: asNumber(item.praisenum || 0),
690
+ });
691
+ });
692
+ return { source, comments, total, page, limit, maxPage: Math.ceil(total / limit) || 1 };
693
+ }
694
+ if (source === 'wy') {
695
+ const offset = (page - 1) * limit;
696
+ const body = asRecord((await this.http(`https://music.163.com/api/v1/resource/comments/R_SO_4_${musicInfo.songmid}?offset=${offset}&limit=${limit}`)).body);
697
+ const raw = hot ? asArray(body.hotComments) : asArray(body.comments);
698
+ const total = asNumber(body.total);
699
+ const comments = raw.map((item) => {
700
+ const user = asRecord(item.user);
701
+ const time = asNumber(item.time);
702
+ return createComment({
703
+ id: asString(item.commentId),
704
+ text: asString(item.content),
705
+ time,
706
+ timeStr: formatDate(time),
707
+ userName: asString(user.nickname),
708
+ avatar: asString(user.avatarUrl),
709
+ userId: asString(user.userId),
710
+ likedCount: asNumber(item.likedCount),
711
+ });
712
+ });
713
+ return { source, comments, total, page, limit, maxPage: Math.ceil(total / limit) || 1 };
714
+ }
715
+ if (source === 'mg') {
716
+ const queryType = hot ? 2 : 1;
717
+ const songId = musicInfo.songId || musicInfo.songmid;
718
+ const body = asRecord((await this.http(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=${queryType}&resourceId=${songId}&resourceType=2&hotCommentStart=${(page - 1) * limit}`, {
719
+ headers: { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)' },
720
+ })).body);
721
+ const data = asRecord(body.data);
722
+ const total = asNumber(hot ? data.cfgHotCount : data.commentNums);
723
+ const comments = asArray(hot ? data.hotComments : data.comments).map((item) => {
724
+ const user = asRecord(item.user);
725
+ const opNumItem = asRecord(item.opNumItem);
726
+ const time = asNumber(item.commentTime);
727
+ return createComment({
728
+ id: asString(item.commentId),
729
+ text: asString(item.commentInfo),
730
+ time,
731
+ timeStr: formatDate(time),
732
+ userName: asString(user.nickName),
733
+ avatar: asString(user.middleIcon || user.bigIcon || user.smallIcon),
734
+ userId: asString(user.userId),
735
+ likedCount: asNumber(opNumItem.thumbNum),
736
+ });
737
+ });
738
+ return { source, comments, total, page, limit, maxPage: Math.ceil(total / limit) || 1 };
739
+ }
740
+ throw new Error(`unsupported source: ${source}`);
741
+ }
742
+ getMusicDetailPageUrl(source, musicInfo) {
743
+ if (source === 'kw')
744
+ return `http://www.kuwo.cn/play_detail/${musicInfo.songmid}`;
745
+ if (source === 'kg')
746
+ return `https://www.kugou.com/song/#hash=${musicInfo.hash || ''}&album_id=${musicInfo.albumId || ''}`;
747
+ if (source === 'tx')
748
+ return `https://y.qq.com/n/yqq/song/${musicInfo.songmid}.html`;
749
+ if (source === 'wy')
750
+ return `https://music.163.com/#/song?id=${musicInfo.songmid}`;
751
+ if (source === 'mg')
752
+ return `http://music.migu.cn/v3/music/song/${musicInfo.copyrightId || ''}`;
753
+ return null;
754
+ }
755
+ async songListTags(source) {
756
+ if (source === 'kw') {
757
+ const tagsBody = asRecord((await this.http('http://wapi.kuwo.cn/api/pc/classify/playlist/getTagList?cmd=rcm_keyword_playlist&user=0&prod=kwplayer_pc_9.0.5.0&vipver=9.0.5.0&source=kwplayer_pc_9.0.5.0&loginUid=0&loginSid=0&appUid=76039576')).body);
758
+ const hotBody = asRecord((await this.http('http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmTagList?loginUid=0&loginSid=0&appUid=76039576')).body);
759
+ const tags = asArray(tagsBody.data).map((group) => ({
760
+ name: asString(group.name),
761
+ list: asArray(group.data).map((tag) => ({
762
+ id: `${asString(tag.id)}-${asString(tag.digest)}`,
763
+ name: asString(tag.name),
764
+ parent_id: asString(group.id),
765
+ parent_name: asString(group.name),
766
+ source,
767
+ })),
768
+ }));
769
+ const hotTag = asArray(asRecord(asArray(hotBody.data)[0]).data).map((tag) => ({
770
+ id: `${asString(tag.id)}-${asString(tag.digest)}`,
771
+ name: asString(tag.name),
772
+ source,
773
+ }));
774
+ return { source, tags, hotTag };
775
+ }
776
+ if (source === 'tx') {
777
+ const tagsBody = asRecord((await this.http('https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=%7B%22tags%22%3A%7B%22method%22%3A%22get_all_categories%22%2C%22param%22%3A%7B%22qq%22%3A%22%22%7D%2C%22module%22%3A%22playlist.PlaylistAllCategoriesServer%22%7D%7D')).body);
778
+ const hotHtml = (await this.http('https://c.y.qq.com/node/pc/wk_v15/category_playlist.html')).body;
779
+ const tags = asArray(asRecord(asRecord(tagsBody.tags).data).v_group).map((group) => ({
780
+ name: asString(group.group_name),
781
+ list: asArray(group.v_item).map((tag) => ({
782
+ id: asString(tag.id),
783
+ name: asString(tag.name),
784
+ parent_id: asString(group.group_id),
785
+ parent_name: asString(group.group_name),
786
+ source,
787
+ })),
788
+ }));
789
+ const hotTag = typeof hotHtml === 'string'
790
+ ? (hotHtml.match(txHotTagReg.row) || [])
791
+ .map((row) => {
792
+ const matched = row.match(txHotTagReg.tag);
793
+ if (!matched)
794
+ return null;
795
+ return { id: matched[1], name: matched[2], source };
796
+ })
797
+ .filter((item) => item != null)
798
+ : [];
799
+ return { source, tags, hotTag };
800
+ }
801
+ if (source === 'wy') {
802
+ const body = asRecord((await this.http('https://music.163.com/api/playlist/catalogue')).body);
803
+ const all = asRecord(body.all);
804
+ const sub = asArray(body.sub);
805
+ const groups = Object.entries(all).map(([id, name]) => ({
806
+ name: asString(name),
807
+ list: sub
808
+ .filter((tag) => asString(tag.category) === id)
809
+ .map((tag) => ({
810
+ id: asString(tag.name),
811
+ name: asString(tag.name),
812
+ parent_id: id,
813
+ parent_name: asString(name),
814
+ source,
815
+ })),
816
+ }));
817
+ const hotTag = asArray(body.hot).map((tag) => ({
818
+ id: asString(tag.name),
819
+ name: asString(tag.name),
820
+ source,
821
+ }));
822
+ return { source, tags: groups, hotTag };
823
+ }
824
+ if (source === 'kg') {
825
+ const body = asRecord((await this.http('http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&')).body);
826
+ const data = asRecord(body.data);
827
+ const tags = Object.keys(data).map((name) => {
828
+ const group = asRecord(data[name]);
829
+ return {
830
+ name,
831
+ list: asArray(group.data).map((tag) => ({
832
+ id: asString(tag.id),
833
+ name: asString(tag.name),
834
+ parent_id: asString(tag.parent_id),
835
+ parent_name: asString(tag.pname),
836
+ source,
837
+ })),
838
+ };
839
+ });
840
+ const hotTag = [];
841
+ return { source, tags, hotTag };
842
+ }
843
+ if (source === 'mg') {
844
+ const body = asRecord((await this.http('https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-taglist/release', {
845
+ headers: {
846
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)',
847
+ Referer: 'https://m.music.migu.cn/',
848
+ },
849
+ })).body);
850
+ const contents = asArray(asRecord(body.data).contents);
851
+ const groups = contents.map((group) => ({
852
+ name: asString(group.title || group.columnTitle),
853
+ list: asArray(group.contents).map((tag) => {
854
+ const info = asRecord(tag.objectInfo);
855
+ return {
856
+ id: asString(info.tagId || info.columnId),
857
+ name: asString(info.tagName || info.columnTitle),
858
+ source,
859
+ };
860
+ }),
861
+ }));
862
+ const hotTag = groups[0]?.list || [];
863
+ return { source, tags: groups, hotTag };
864
+ }
865
+ throw new Error(`unsupported source: ${source}`);
866
+ }
867
+ async songList(source, sortId = 'hot', tagId = '', page = 1, limit = 30) {
868
+ if (source === 'kw') {
869
+ const requestUrl = tagId
870
+ ? `http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&pn=${page}&id=${asString(tagId).split('-')[0]}&rn=${limit}`
871
+ : `http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmPlayList?loginUid=0&loginSid=0&appUid=76039576&&pn=${page}&rn=${limit}&order=${sortId}`;
872
+ const body = asRecord((await this.http(requestUrl)).body);
873
+ const data = asRecord(body.data);
874
+ const list = asArray(data.data).map((item) => createSongListItem({
875
+ id: `digest-${asString(item.digest)}__${asString(item.id)}`,
876
+ name: asString(item.name),
877
+ author: asString(item.uname),
878
+ img: asString(item.img),
879
+ desc: asString(item.desc),
880
+ total: asNumber(item.total),
881
+ play_count: formatPlayCountText(asNumber(item.listencnt)),
882
+ source,
883
+ }));
884
+ return {
885
+ source,
886
+ list,
887
+ total: asNumber(data.total),
888
+ page: asNumber(data.pn) || page,
889
+ limit: asNumber(data.rn) || limit,
890
+ };
891
+ }
892
+ if (source === 'tx') {
893
+ const url = tagId
894
+ ? `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ comm: { cv: 1602, ct: 20 }, playlist: { method: 'get_category_content', param: { titleid: Number(tagId), caller: '0', category_id: Number(tagId), size: limit, page: page - 1, use_page: 1 }, module: 'playlist.PlayListCategoryServer' } }))}`
895
+ : `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ comm: { cv: 1602, ct: 20 }, playlist: { method: 'get_playlist_by_tag', param: { id: 10000000, sin: limit * (page - 1), size: limit, order: Number(sortId) || 5, cur_page: page }, module: 'playlist.PlayListPlazaServer' } }))}`;
896
+ const body = asRecord((await this.http(url)).body);
897
+ const playlist = asRecord(body.playlist);
898
+ const data = asRecord(playlist.data);
899
+ if (tagId) {
900
+ const content = asRecord(data.content);
901
+ const list = asArray(content.v_item).map((item) => {
902
+ const basic = asRecord(item.basic);
903
+ const creator = asRecord(basic.creator);
904
+ const cover = asRecord(basic.cover);
905
+ return createSongListItem({
906
+ id: asString(basic.tid),
907
+ name: asString(basic.title),
908
+ author: asString(creator.nick),
909
+ img: asString(cover.medium_url || cover.default_url),
910
+ desc: decodeName(asString(basic.desc)).replace(/<br>/g, '\n'),
911
+ play_count: formatPlayCountText(asNumber(basic.play_cnt)),
912
+ source,
913
+ });
914
+ });
915
+ return { source, list, total: asNumber(content.total_cnt), page, limit };
916
+ }
917
+ const list = asArray(data.v_playlist).map((item) => {
918
+ const creator = asRecord(item.creator_info);
919
+ return createSongListItem({
920
+ id: asString(item.tid),
921
+ name: asString(item.title),
922
+ author: asString(creator.nick),
923
+ img: asString(item.cover_url_medium),
924
+ desc: decodeName(asString(item.desc)).replace(/<br>/g, '\n'),
925
+ total: asNumber(asArray(item.song_ids).length),
926
+ play_count: formatPlayCountText(asNumber(item.access_num)),
927
+ source,
928
+ });
929
+ });
930
+ return { source, list, total: asNumber(data.total), page, limit };
931
+ }
932
+ if (source === 'wy') {
933
+ const offset = (page - 1) * limit;
934
+ const url = `https://music.163.com/api/playlist/list?cat=${encodeURIComponent(tagId || '全部')}&order=${sortId || 'hot'}&offset=${offset}&total=true&limit=${limit}`;
935
+ const body = asRecord((await this.http(url)).body);
936
+ const list = asArray(body.playlists).map((item) => {
937
+ const creator = asRecord(item.creator);
938
+ return createSongListItem({
939
+ id: asString(item.id),
940
+ name: asString(item.name),
941
+ author: asString(creator.nickname),
942
+ img: asString(item.coverImgUrl),
943
+ desc: asString(item.description),
944
+ total: asNumber(item.trackCount),
945
+ play_count: formatPlayCountText(asNumber(item.playCount)),
946
+ source,
947
+ });
948
+ });
949
+ return { source, list, total: asNumber(body.total), page, limit };
950
+ }
951
+ if (source === 'kg') {
952
+ const body = asRecord((await this.http(`http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId || '5'}&c=${tagId || ''}&p=${page}`)).body);
953
+ const list = asArray(body.special_db).map((item) => createSongListItem({
954
+ id: `id_${asString(item.specialid)}`,
955
+ name: asString(item.specialname),
956
+ author: asString(item.nickname),
957
+ img: asString(item.img || item.imgurl),
958
+ desc: asString(item.intro),
959
+ total: asNumber(item.songcount),
960
+ play_count: formatPlayCountText(asNumber(item.total_play_count || item.play_count)),
961
+ source,
962
+ }));
963
+ return { source, list, total: list.length, page, limit };
964
+ }
965
+ if (source === 'mg') {
966
+ const url = tagId
967
+ ? `https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-listbytag/release?pageNumber=${page}&templateVersion=2&tagId=${tagId}`
968
+ : `https://app.c.nf.migu.cn/pc/bmw/page-data/playlist-square-recommend/v1.0?templateVersion=2&pageNo=${page}`;
969
+ const body = asRecord((await this.http(url, {
970
+ headers: {
971
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)',
972
+ Referer: 'https://m.music.migu.cn/',
973
+ },
974
+ })).body);
975
+ const data = asRecord(body.data);
976
+ const list = asArray(data.contents).map((item) => createSongListItem({
977
+ id: asString(item.resId || item.contentId),
978
+ name: asString(item.txt || item.title),
979
+ author: '',
980
+ img: asString(item.img || item.imageUrl),
981
+ desc: asString(item.txt2 || ''),
982
+ play_count: formatPlayCountText(asNumber(asRecord(item.opNumItem).playNum)),
983
+ source,
984
+ }));
985
+ return { source, list, total: list.length, page, limit };
986
+ }
987
+ throw new Error(`unsupported source: ${source}`);
988
+ }
989
+ async songListDetail(source, listId, page = 1, limit = 100) {
990
+ if (source === 'kw') {
991
+ const id = parseSongListId(listId, /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/);
992
+ const body = asRecord((await this.http(`http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}&rn=${limit}&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1`)).body);
993
+ const list = asArray(body.musiclist).map((item) => {
994
+ const mapped = mapKwTypes(asString(item.N_MINFO || item.n_minfo));
995
+ return {
996
+ ...baseMusicInfo(source),
997
+ singer: decodeName(asString(item.ARTIST || item.artist)),
998
+ name: decodeName(asString(item.SONGNAME || item.name)),
999
+ albumName: decodeName(asString(item.ALBUM || item.album)),
1000
+ albumId: asString(item.ALBUMID || item.albumid),
1001
+ interval: formatPlayTime(asNumber(item.DURATION || item.duration)),
1002
+ songmid: asString(item.MUSICRID || item.id).replace('MUSIC_', ''),
1003
+ img: asString(item.web_albumpic_short),
1004
+ ...mapped,
1005
+ };
1006
+ });
1007
+ const info = createSongListDetailInfo({
1008
+ name: asString(body.title),
1009
+ img: asString(body.pic),
1010
+ desc: asString(body.info),
1011
+ author: asString(body.uname),
1012
+ play_count: formatPlayCountText(asNumber(body.playnum)),
1013
+ });
1014
+ return { source, list, page, limit: asNumber(body.rn) || limit, total: asNumber(body.total), info };
1015
+ }
1016
+ if (source === 'tx') {
1017
+ const id = parseSongListId(listId, /(?:\/playlist\/|id=)(\d+)/);
1018
+ const body = asRecord((await this.http(`https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg?type=1&json=1&utf8=1&onlysong=0&new_format=1&disstid=${id}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0`, {
1019
+ headers: { Origin: 'https://y.qq.com', Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html` },
1020
+ })).body);
1021
+ const cdlist = asRecord(asArray(body.cdlist)[0]);
1022
+ const songlist = asArray(cdlist.songlist);
1023
+ const list = songlist.map((item) => {
1024
+ const album = asRecord(item.album);
1025
+ const file = asRecord(item.file);
1026
+ const albumId = asString(album.mid);
1027
+ return {
1028
+ ...baseMusicInfo(source),
1029
+ singer: formatSingerName(item.singer),
1030
+ name: asString(item.title),
1031
+ albumName: asString(album.name),
1032
+ albumId,
1033
+ interval: formatPlayTime(asNumber(item.interval)),
1034
+ songmid: asString(item.mid),
1035
+ songId: asNumber(item.id),
1036
+ img: albumId ? `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg` : '',
1037
+ ...mapTxTypes(file),
1038
+ };
1039
+ });
1040
+ const info = createSongListDetailInfo({
1041
+ name: asString(cdlist.dissname),
1042
+ img: asString(cdlist.logo),
1043
+ desc: decodeName(asString(cdlist.desc)).replace(/<br>/g, '\n'),
1044
+ author: asString(cdlist.nickname),
1045
+ play_count: formatPlayCountText(asNumber(cdlist.visitnum)),
1046
+ });
1047
+ return { source, list, page: 1, limit: songlist.length, total: songlist.length, info };
1048
+ }
1049
+ if (source === 'wy') {
1050
+ const id = parseSongListId(listId, /(?:id=|\/playlist\/)(\d+)/);
1051
+ const body = asRecord((await this.http(`https://music.163.com/api/v3/playlist/detail?id=${id}&n=100000&s=8`)).body);
1052
+ const playlist = asRecord(body.playlist);
1053
+ const tracks = asArray(playlist.tracks);
1054
+ const list = tracks.map((item) => {
1055
+ const album = asRecord(item.al);
1056
+ return {
1057
+ ...baseMusicInfo(source),
1058
+ singer: formatSingerName(item.ar),
1059
+ name: asString(item.name),
1060
+ albumName: asString(album.name),
1061
+ albumId: asString(album.id),
1062
+ interval: formatPlayTime(asNumber(item.dt) / 1000),
1063
+ songmid: asString(item.id),
1064
+ img: asString(album.picUrl),
1065
+ types: [{ type: '128k', size: null }],
1066
+ _types: { '128k': { size: null } },
1067
+ };
1068
+ });
1069
+ const info = createSongListDetailInfo({
1070
+ name: asString(playlist.name),
1071
+ img: asString(playlist.coverImgUrl),
1072
+ desc: asString(playlist.description),
1073
+ author: asString(asRecord(playlist.creator).nickname),
1074
+ play_count: formatPlayCountText(asNumber(playlist.playCount)),
1075
+ });
1076
+ return { source, list, page, limit, total: asNumber(playlist.trackCount), info };
1077
+ }
1078
+ if (source === 'kg') {
1079
+ const id = parseSongListId(listId.replace(/^id_/, ''), /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/);
1080
+ const htmlBody = (await this.http(`http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html`)).body;
1081
+ if (typeof htmlBody !== 'string')
1082
+ throw new Error('invalid kg song list detail response');
1083
+ const match = htmlBody.match(/global\.data = (\[.+\]);/);
1084
+ const listData = match ? JSON.parse(match[1]) : [];
1085
+ const list = asArray(listData).map((item) => {
1086
+ const mapped = mapKgTypes(item);
1087
+ return {
1088
+ ...baseMusicInfo(source),
1089
+ singer: asString(item.singername),
1090
+ name: decodeName(asString(item.songname)),
1091
+ albumName: asString(item.album_name),
1092
+ albumId: asString(item.album_id),
1093
+ interval: formatPlayTime(asNumber(item.duration)),
1094
+ songmid: asString(item.audio_id),
1095
+ hash: asString(item.hash),
1096
+ ...mapped,
1097
+ };
1098
+ });
1099
+ const info = createSongListDetailInfo({ name: '', img: '', desc: '', author: '', play_count: '0' });
1100
+ return { source, list, page: 1, limit: list.length, total: list.length, info };
1101
+ }
1102
+ if (source === 'mg') {
1103
+ const id = parseSongListId(listId, /(?:playlistId|id|\/playlist\/)(\d+)/);
1104
+ const body = asRecord((await this.http(`https://app.c.nf.migu.cn/MIGUM3.0/resource/playlist/song/v2.0?pageNo=${page}&pageSize=${limit}&playlistId=${id}`, {
1105
+ headers: {
1106
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)',
1107
+ Referer: 'https://m.music.migu.cn/',
1108
+ },
1109
+ })).body);
1110
+ const data = asRecord(body.data);
1111
+ const list = asArray(data.songList).map((item) => ({
1112
+ ...baseMusicInfo(source),
1113
+ singer: formatSingerName(item.singerList),
1114
+ name: asString(item.songName || item.name),
1115
+ albumName: asString(item.album),
1116
+ albumId: asString(item.albumId),
1117
+ interval: formatPlayTime(asNumber(item.duration)),
1118
+ songmid: asString(item.songId),
1119
+ copyrightId: asString(item.copyrightId),
1120
+ img: asString(item.img3 || item.img2 || item.img1),
1121
+ lrcUrl: asString(item.lrcUrl),
1122
+ mrcUrl: asString(item.mrcurl),
1123
+ trcUrl: asString(item.trcUrl),
1124
+ types: [{ type: '128k', size: null }],
1125
+ _types: { '128k': { size: null } },
1126
+ }));
1127
+ const detailBody = asRecord((await this.http(`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`, {
1128
+ headers: {
1129
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)',
1130
+ Referer: 'https://m.music.migu.cn/',
1131
+ },
1132
+ })).body);
1133
+ const detail = asRecord(detailBody.data);
1134
+ const info = createSongListDetailInfo({
1135
+ name: asString(detail.title),
1136
+ img: asString(asRecord(detail.imgItem).img),
1137
+ desc: asString(detail.summary),
1138
+ author: asString(detail.ownerName),
1139
+ play_count: formatPlayCountText(asNumber(asRecord(detail.opNumItem).playNum)),
1140
+ });
1141
+ return { source, list, page, limit, total: asNumber(data.totalCount), info };
1142
+ }
1143
+ throw new Error(`unsupported source: ${source}`);
1144
+ }
1145
+ }
1146
+ exports.LxProjectApi = LxProjectApi;