@eldment/meting-mcp 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +64 -0
- package/src/index.js +53 -0
- package/src/mcp-server.js +242 -0
- package/src/meting.js +192 -0
- package/src/providers/baidu.js +280 -0
- package/src/providers/base.js +247 -0
- package/src/providers/index.js +49 -0
- package/src/providers/kugou.js +417 -0
- package/src/providers/kuwo.js +226 -0
- package/src/providers/netease.js +363 -0
- package/src/providers/tencent.js +328 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import BaseProvider from './base.js';
|
|
3
|
+
|
|
4
|
+
// eapi 相关常量
|
|
5
|
+
const EAPI_KEY = 'e82ckenh8dichen8';
|
|
6
|
+
const EAPI_IV = Buffer.from('0102030405060708');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 网易云音乐平台提供者
|
|
10
|
+
*/
|
|
11
|
+
export default class NeteaseProvider extends BaseProvider {
|
|
12
|
+
constructor(meting) {
|
|
13
|
+
super(meting);
|
|
14
|
+
this.name = 'netease';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 获取网易云音乐的请求头配置(EAPI)
|
|
19
|
+
*/
|
|
20
|
+
getHeaders() {
|
|
21
|
+
const timestamp = Date.now().toString();
|
|
22
|
+
const deviceId = this._generateDeviceId();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
'Referer': 'music.163.com',
|
|
26
|
+
'Cookie': `osver=android; appver=8.7.01; os=android; deviceId=${deviceId}; channel=netease; requestId=${timestamp}_${Math.floor(Math.random() * 1000).toString().padStart(4, '0')}; __remember_me=true`,
|
|
27
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; M2007J3SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 MQQBrowser/6.2 TBS/045714 Mobile Safari/537.36 NeteaseMusic/8.7.01',
|
|
28
|
+
'Accept': '*/*',
|
|
29
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
30
|
+
'Connection': 'keep-alive',
|
|
31
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 搜索歌曲
|
|
37
|
+
*/
|
|
38
|
+
search(keyword, option = {}) {
|
|
39
|
+
return {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
url: 'http://music.163.com/api/cloudsearch/pc',
|
|
42
|
+
body: {
|
|
43
|
+
s: keyword,
|
|
44
|
+
type: option.type || 1,
|
|
45
|
+
limit: option.limit || 30,
|
|
46
|
+
total: 'true',
|
|
47
|
+
offset: (option.page && option.limit) ? (option.page - 1) * option.limit : 0
|
|
48
|
+
},
|
|
49
|
+
encode: 'netease_eapi',
|
|
50
|
+
format: 'result.songs'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取歌曲详情
|
|
56
|
+
*/
|
|
57
|
+
song(id) {
|
|
58
|
+
return {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
url: 'http://music.163.com/api/v3/song/detail/',
|
|
61
|
+
body: {
|
|
62
|
+
c: `[{"id":${id},"v":0}]`
|
|
63
|
+
},
|
|
64
|
+
encode: 'netease_eapi',
|
|
65
|
+
format: 'songs'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取专辑信息
|
|
71
|
+
*/
|
|
72
|
+
album(id) {
|
|
73
|
+
return {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
url: `http://music.163.com/api/v1/album/${id}`,
|
|
76
|
+
body: {
|
|
77
|
+
total: 'true',
|
|
78
|
+
offset: '0',
|
|
79
|
+
id: id,
|
|
80
|
+
limit: '1000',
|
|
81
|
+
ext: 'true',
|
|
82
|
+
private_cloud: 'true'
|
|
83
|
+
},
|
|
84
|
+
encode: 'netease_eapi',
|
|
85
|
+
format: 'songs'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取艺术家作品
|
|
91
|
+
*/
|
|
92
|
+
artist(id, limit = 50) {
|
|
93
|
+
return {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
url: `http://music.163.com/api/v1/artist/${id}`,
|
|
96
|
+
body: {
|
|
97
|
+
ext: 'true',
|
|
98
|
+
private_cloud: 'true',
|
|
99
|
+
top: limit,
|
|
100
|
+
id: id
|
|
101
|
+
},
|
|
102
|
+
encode: 'netease_eapi',
|
|
103
|
+
format: 'hotSongs'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取播放列表
|
|
109
|
+
*/
|
|
110
|
+
playlist(id) {
|
|
111
|
+
return {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
url: 'http://music.163.com/api/v6/playlist/detail',
|
|
114
|
+
body: {
|
|
115
|
+
s: '0',
|
|
116
|
+
id: id,
|
|
117
|
+
n: '1000',
|
|
118
|
+
t: '0'
|
|
119
|
+
},
|
|
120
|
+
encode: 'netease_eapi',
|
|
121
|
+
format: 'playlist.tracks'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 获取音频播放链接
|
|
127
|
+
*/
|
|
128
|
+
url(id, br = 320) {
|
|
129
|
+
return {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
url: 'http://music.163.com/api/song/enhance/player/url',
|
|
132
|
+
body: {
|
|
133
|
+
ids: [id],
|
|
134
|
+
br: br * 1000
|
|
135
|
+
},
|
|
136
|
+
encode: 'netease_eapi',
|
|
137
|
+
decode: 'netease_url'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 获取歌词
|
|
143
|
+
*/
|
|
144
|
+
lyric(id) {
|
|
145
|
+
return {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
url: 'http://music.163.com/api/song/lyric',
|
|
148
|
+
body: {
|
|
149
|
+
id: id,
|
|
150
|
+
os: 'linux',
|
|
151
|
+
lv: -1,
|
|
152
|
+
kv: -1,
|
|
153
|
+
tv: -1
|
|
154
|
+
},
|
|
155
|
+
encode: 'netease_eapi',
|
|
156
|
+
decode: 'netease_lyric'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 获取封面图片
|
|
162
|
+
*/
|
|
163
|
+
async pic(id, size = 300) {
|
|
164
|
+
const url = `https://p3.music.126.net/${this._encryptId(id)}/${id}.jpg?param=${size}y${size}`;
|
|
165
|
+
return JSON.stringify({ url: url });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 格式化网易云音乐数据
|
|
170
|
+
*/
|
|
171
|
+
format(data) {
|
|
172
|
+
const result = {
|
|
173
|
+
id: data.id,
|
|
174
|
+
name: data.name,
|
|
175
|
+
artist: [],
|
|
176
|
+
album: data.al.name,
|
|
177
|
+
pic_id: data.al.pic_str || data.al.pic,
|
|
178
|
+
url_id: data.id,
|
|
179
|
+
lyric_id: data.id,
|
|
180
|
+
source: 'netease'
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (data.al.picUrl) {
|
|
184
|
+
const match = data.al.picUrl.match(/\/(\d+)\./);
|
|
185
|
+
if (match) {
|
|
186
|
+
result.pic_id = match[1];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
data.ar.forEach(artist => {
|
|
191
|
+
result.artist.push(artist.name);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 处理网易云音乐的编码逻辑
|
|
199
|
+
*/
|
|
200
|
+
async handleEncode(api) {
|
|
201
|
+
if (api.encode === 'netease_eapi') {
|
|
202
|
+
return this.eapiEncrypt(api);
|
|
203
|
+
}
|
|
204
|
+
return api;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 网易云音乐 EAPI 加密
|
|
209
|
+
*/
|
|
210
|
+
async eapiEncrypt(api) {
|
|
211
|
+
const text = JSON.stringify(api.body);
|
|
212
|
+
const url = api.url.replace(/https?:\/\/[^\/]+/, '');
|
|
213
|
+
|
|
214
|
+
// 构建 eapi 加密消息
|
|
215
|
+
const message = `nobody${url}use${text}md5forencrypt`;
|
|
216
|
+
const digest = crypto.createHash('md5').update(message).digest('hex');
|
|
217
|
+
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
|
|
218
|
+
|
|
219
|
+
// AES-128-ECB 加密
|
|
220
|
+
const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(EAPI_KEY, 'utf8'), null);
|
|
221
|
+
cipher.setAutoPadding(true);
|
|
222
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
223
|
+
encrypted += cipher.final('hex');
|
|
224
|
+
|
|
225
|
+
// 转换 URL 路径
|
|
226
|
+
api.url = api.url.replace('/api/', '/eapi/');
|
|
227
|
+
|
|
228
|
+
// 构建 eapi 请求体
|
|
229
|
+
api.body = {
|
|
230
|
+
params: encrypted.toUpperCase()
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return api;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 网易云音乐 URL 解码
|
|
238
|
+
*/
|
|
239
|
+
urlDecode(result) {
|
|
240
|
+
const data = JSON.parse(result);
|
|
241
|
+
let url;
|
|
242
|
+
|
|
243
|
+
if (data.data[0].uf && data.data[0].uf.url) {
|
|
244
|
+
data.data[0].url = data.data[0].uf.url;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (data.data[0].url) {
|
|
248
|
+
url = {
|
|
249
|
+
url: data.data[0].url,
|
|
250
|
+
size: data.data[0].size,
|
|
251
|
+
br: data.data[0].br / 1000
|
|
252
|
+
};
|
|
253
|
+
} else {
|
|
254
|
+
url = {
|
|
255
|
+
url: '',
|
|
256
|
+
size: 0,
|
|
257
|
+
br: -1
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return JSON.stringify(url);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 网易云音乐歌词解码
|
|
266
|
+
*/
|
|
267
|
+
lyricDecode(result) {
|
|
268
|
+
const data = JSON.parse(result);
|
|
269
|
+
const lyricData = {
|
|
270
|
+
lyric: (data.lrc && data.lrc.lyric) ? data.lrc.lyric : '',
|
|
271
|
+
tlyric: (data.tlyric && data.tlyric.lyric) ? data.tlyric.lyric : ''
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return JSON.stringify(lyricData);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ========== 私有工具方法 ==========
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 生成随机 IP 地址
|
|
281
|
+
*/
|
|
282
|
+
_generateRandomIP() {
|
|
283
|
+
const min = 1884815360; // 112.74.200.0
|
|
284
|
+
const max = 1884890111; // 112.74.243.255
|
|
285
|
+
const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
(randomInt >>> 24) & 0xFF,
|
|
289
|
+
(randomInt >>> 16) & 0xFF,
|
|
290
|
+
(randomInt >>> 8) & 0xFF,
|
|
291
|
+
randomInt & 0xFF
|
|
292
|
+
].join('.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 生成随机十六进制字符串
|
|
297
|
+
*/
|
|
298
|
+
_getRandomHex(length) {
|
|
299
|
+
return crypto.randomBytes(Math.ceil(length / 2))
|
|
300
|
+
.toString('hex')
|
|
301
|
+
.slice(0, length);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 生成设备 ID
|
|
306
|
+
*/
|
|
307
|
+
_generateDeviceId() {
|
|
308
|
+
// 生成类似移动端的设备 ID
|
|
309
|
+
const randomBytes = crypto.randomBytes(16);
|
|
310
|
+
const deviceId = randomBytes.toString('hex').toUpperCase();
|
|
311
|
+
return deviceId;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 网易云音乐 ID 加密
|
|
316
|
+
*/
|
|
317
|
+
_encryptId(id) {
|
|
318
|
+
const magic = '3go8&$8*3*3h0k(2)2'.split('');
|
|
319
|
+
const song_id = String(id).split('');
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < song_id.length; i++) {
|
|
322
|
+
song_id[i] = String.fromCharCode(
|
|
323
|
+
song_id[i].charCodeAt(0) ^ magic[i % magic.length].charCodeAt(0)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const result = crypto.createHash('md5')
|
|
328
|
+
.update(song_id.join(''), 'binary')
|
|
329
|
+
.digest('base64')
|
|
330
|
+
.replace(/\//g, '_')
|
|
331
|
+
.replace(/\+/g, '-');
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 大数运算相关工具方法
|
|
338
|
+
*/
|
|
339
|
+
_bchexdec(hex) {
|
|
340
|
+
return BigInt('0x' + hex);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_str2hex(str) {
|
|
344
|
+
return Buffer.from(str, 'utf8').toString('hex');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 大数幂模运算
|
|
349
|
+
*/
|
|
350
|
+
_powMod(base, exponent, modulus) {
|
|
351
|
+
if (modulus === 1n) return 0n;
|
|
352
|
+
let result = 1n;
|
|
353
|
+
base = base % modulus;
|
|
354
|
+
while (exponent > 0n) {
|
|
355
|
+
if (exponent % 2n === 1n) {
|
|
356
|
+
result = (result * base) % modulus;
|
|
357
|
+
}
|
|
358
|
+
exponent = exponent >> 1n;
|
|
359
|
+
base = (base * base) % modulus;
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import BaseProvider from './base.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 腾讯音乐平台提供者
|
|
5
|
+
*/
|
|
6
|
+
export default class TencentProvider extends BaseProvider {
|
|
7
|
+
constructor(meting) {
|
|
8
|
+
super(meting);
|
|
9
|
+
this.name = 'tencent';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 获取腾讯音乐的请求头配置
|
|
14
|
+
*/
|
|
15
|
+
getHeaders() {
|
|
16
|
+
return {
|
|
17
|
+
'Referer': 'http://y.qq.com',
|
|
18
|
+
'Cookie': 'pgv_pvi=22038528; pgv_si=s3156287488; pgv_pvid=5535248600; yplayer_open=1; ts_last=y.qq.com/portal/player.html; ts_uid=4847550686; yq_index=0; qqmusic_fromtag=66; player_exist=1',
|
|
19
|
+
'User-Agent': 'QQ%E9%9F%B3%E4%B9%90/54409 CFNetwork/901.1 Darwin/17.6.0 (x86_64)',
|
|
20
|
+
'Accept': '*/*',
|
|
21
|
+
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
|
|
22
|
+
'Connection': 'keep-alive',
|
|
23
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 搜索歌曲
|
|
29
|
+
*/
|
|
30
|
+
search(keyword, option = {}) {
|
|
31
|
+
return {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
url: 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp',
|
|
34
|
+
body: {
|
|
35
|
+
format: 'json',
|
|
36
|
+
p: option.page || 1,
|
|
37
|
+
n: option.limit || 30,
|
|
38
|
+
w: keyword,
|
|
39
|
+
aggr: 1,
|
|
40
|
+
lossless: 1,
|
|
41
|
+
cr: 1,
|
|
42
|
+
new_json: 1
|
|
43
|
+
},
|
|
44
|
+
format: 'data.song.list'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取歌曲详情
|
|
50
|
+
*/
|
|
51
|
+
song(id) {
|
|
52
|
+
return {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg',
|
|
55
|
+
body: {
|
|
56
|
+
songmid: id,
|
|
57
|
+
platform: 'yqq',
|
|
58
|
+
format: 'json'
|
|
59
|
+
},
|
|
60
|
+
format: 'data'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 获取专辑信息
|
|
66
|
+
*/
|
|
67
|
+
album(id) {
|
|
68
|
+
return {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_album_detail_cp.fcg',
|
|
71
|
+
body: {
|
|
72
|
+
albummid: id,
|
|
73
|
+
platform: 'mac',
|
|
74
|
+
format: 'json',
|
|
75
|
+
newsong: 1
|
|
76
|
+
},
|
|
77
|
+
format: 'data.getSongInfo'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 获取艺术家作品
|
|
83
|
+
*/
|
|
84
|
+
artist(id, limit = 50) {
|
|
85
|
+
return {
|
|
86
|
+
method: 'GET',
|
|
87
|
+
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg',
|
|
88
|
+
body: {
|
|
89
|
+
singermid: id,
|
|
90
|
+
begin: 0,
|
|
91
|
+
num: limit,
|
|
92
|
+
order: 'listen',
|
|
93
|
+
platform: 'mac',
|
|
94
|
+
newsong: 1
|
|
95
|
+
},
|
|
96
|
+
format: 'data.list'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 获取播放列表
|
|
102
|
+
*/
|
|
103
|
+
playlist(id) {
|
|
104
|
+
return {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_playlist_cp.fcg',
|
|
107
|
+
body: {
|
|
108
|
+
id: id,
|
|
109
|
+
format: 'json',
|
|
110
|
+
newsong: 1,
|
|
111
|
+
platform: 'jqspaframe.json'
|
|
112
|
+
},
|
|
113
|
+
format: 'data.cdlist.0.songlist'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 获取音频播放链接
|
|
119
|
+
*/
|
|
120
|
+
url(id, br = 320) {
|
|
121
|
+
return {
|
|
122
|
+
method: 'GET',
|
|
123
|
+
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg',
|
|
124
|
+
body: {
|
|
125
|
+
songmid: id,
|
|
126
|
+
platform: 'yqq',
|
|
127
|
+
format: 'json'
|
|
128
|
+
},
|
|
129
|
+
decode: 'tencent_url'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 获取歌词
|
|
135
|
+
*/
|
|
136
|
+
lyric(id) {
|
|
137
|
+
return {
|
|
138
|
+
method: 'GET',
|
|
139
|
+
url: 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg',
|
|
140
|
+
body: {
|
|
141
|
+
songmid: id,
|
|
142
|
+
g_tk: '5381'
|
|
143
|
+
},
|
|
144
|
+
decode: 'tencent_lyric'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 获取封面图片
|
|
150
|
+
*/
|
|
151
|
+
async pic(id, size = 300) {
|
|
152
|
+
const url = `https://y.gtimg.cn/music/photo_new/T002R${size}x${size}M000${id}.jpg?max_age=2592000`;
|
|
153
|
+
return JSON.stringify({ url: url });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 格式化腾讯音乐数据
|
|
158
|
+
*/
|
|
159
|
+
format(data) {
|
|
160
|
+
if (data.musicData) {
|
|
161
|
+
data = data.musicData;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = {
|
|
165
|
+
id: data.mid,
|
|
166
|
+
name: data.name,
|
|
167
|
+
artist: [],
|
|
168
|
+
album: data.album.title.trim(),
|
|
169
|
+
pic_id: data.album.mid,
|
|
170
|
+
url_id: data.mid,
|
|
171
|
+
lyric_id: data.mid,
|
|
172
|
+
source: 'tencent'
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
data.singer.forEach(singer => {
|
|
176
|
+
result.artist.push(singer.name);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 处理腾讯音乐的解码逻辑
|
|
184
|
+
*/
|
|
185
|
+
async handleDecode(decodeType, data) {
|
|
186
|
+
if (decodeType === 'tencent_url') {
|
|
187
|
+
return this.urlDecode(data);
|
|
188
|
+
} else if (decodeType === 'tencent_lyric') {
|
|
189
|
+
return this.lyricDecode(data);
|
|
190
|
+
}
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 腾讯音乐 URL 解码
|
|
196
|
+
*/
|
|
197
|
+
async urlDecode(result) {
|
|
198
|
+
const data = JSON.parse(result);
|
|
199
|
+
const guid = Math.floor(Math.random() * 10000000000);
|
|
200
|
+
|
|
201
|
+
const qualityMap = [
|
|
202
|
+
['size_flac', 999, 'F000', 'flac'],
|
|
203
|
+
['size_320mp3', 320, 'M800', 'mp3'],
|
|
204
|
+
['size_192aac', 192, 'C600', 'm4a'],
|
|
205
|
+
['size_128mp3', 128, 'M500', 'mp3'],
|
|
206
|
+
['size_96aac', 96, 'C400', 'm4a'],
|
|
207
|
+
['size_48aac', 48, 'C200', 'm4a'],
|
|
208
|
+
['size_24aac', 24, 'C100', 'm4a']
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
let uin = '0';
|
|
212
|
+
const uinMatch = this.meting.header.Cookie && this.meting.header.Cookie.match(/uin=(\d+)/);
|
|
213
|
+
if (uinMatch) {
|
|
214
|
+
uin = uinMatch[1];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const payload = {
|
|
218
|
+
req_0: {
|
|
219
|
+
module: 'vkey.GetVkeyServer',
|
|
220
|
+
method: 'CgiGetVkey',
|
|
221
|
+
param: {
|
|
222
|
+
guid: String(guid),
|
|
223
|
+
songmid: [],
|
|
224
|
+
filename: [],
|
|
225
|
+
songtype: [],
|
|
226
|
+
uin: uin,
|
|
227
|
+
loginflag: 1,
|
|
228
|
+
platform: '20'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
qualityMap.forEach(([sizeKey, br, prefix, ext]) => {
|
|
234
|
+
payload.req_0.param.songmid.push(data.data[0].mid);
|
|
235
|
+
payload.req_0.param.filename.push(`${prefix}${data.data[0].file.media_mid}.${ext}`);
|
|
236
|
+
payload.req_0.param.songtype.push(data.data[0].type);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const api = {
|
|
240
|
+
method: 'GET',
|
|
241
|
+
url: 'https://u.y.qq.com/cgi-bin/musicu.fcg',
|
|
242
|
+
body: {
|
|
243
|
+
format: 'json',
|
|
244
|
+
platform: 'yqq.json',
|
|
245
|
+
needNewCode: 0,
|
|
246
|
+
data: JSON.stringify(payload)
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const response = JSON.parse(await this.meting._exec(api));
|
|
251
|
+
const vkeys = response.req_0.data.midurlinfo;
|
|
252
|
+
|
|
253
|
+
let url;
|
|
254
|
+
for (let i = 0; i < qualityMap.length; i++) {
|
|
255
|
+
const [sizeKey, br, prefix, ext] = qualityMap[i];
|
|
256
|
+
if (data.data[0].file[sizeKey] && br <= this.meting.temp.br) {
|
|
257
|
+
if (vkeys[i].vkey) {
|
|
258
|
+
url = {
|
|
259
|
+
url: response.req_0.data.sip[0] + vkeys[i].purl,
|
|
260
|
+
size: data.data[0].file[sizeKey],
|
|
261
|
+
br: br
|
|
262
|
+
};
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!url) {
|
|
269
|
+
url = {
|
|
270
|
+
url: '',
|
|
271
|
+
size: 0,
|
|
272
|
+
br: -1
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return JSON.stringify(url);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 解码HTML实体编码
|
|
281
|
+
*/
|
|
282
|
+
decodeHtmlEntities(text) {
|
|
283
|
+
if (!text) return text;
|
|
284
|
+
|
|
285
|
+
// 常见HTML实体编码映射
|
|
286
|
+
const entityMap = {
|
|
287
|
+
''': "'",
|
|
288
|
+
'"': '"',
|
|
289
|
+
'&': '&',
|
|
290
|
+
'<': '<',
|
|
291
|
+
'>': '>',
|
|
292
|
+
' ': ' '
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// 替换命名实体
|
|
296
|
+
let decoded = text;
|
|
297
|
+
for (const [entity, char] of Object.entries(entityMap)) {
|
|
298
|
+
decoded = decoded.replace(new RegExp(entity, 'g'), char);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 替换数字实体(如 ' " 等)
|
|
302
|
+
decoded = decoded.replace(/&#(\d+);/g, (match, dec) => {
|
|
303
|
+
return String.fromCharCode(parseInt(dec, 10));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// 替换十六进制实体(如 ' 等)
|
|
307
|
+
decoded = decoded.replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => {
|
|
308
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return decoded;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 腾讯音乐歌词解码
|
|
316
|
+
*/
|
|
317
|
+
lyricDecode(result) {
|
|
318
|
+
const jsonStr = result.substring(18, result.length - 1);
|
|
319
|
+
const data = JSON.parse(jsonStr);
|
|
320
|
+
|
|
321
|
+
const lyricData = {
|
|
322
|
+
lyric: data.lyric ? this.decodeHtmlEntities(Buffer.from(data.lyric, 'base64').toString()) : '',
|
|
323
|
+
tlyric: data.trans ? this.decodeHtmlEntities(Buffer.from(data.trans, 'base64').toString()) : ''
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return JSON.stringify(lyricData);
|
|
327
|
+
}
|
|
328
|
+
}
|