@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.
@@ -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
+ '&apos;': "'",
288
+ '&quot;': '"',
289
+ '&amp;': '&',
290
+ '&lt;': '<',
291
+ '&gt;': '>',
292
+ '&nbsp;': ' '
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
+ // 替换数字实体(如 &#39; &#34; 等)
302
+ decoded = decoded.replace(/&#(\d+);/g, (match, dec) => {
303
+ return String.fromCharCode(parseInt(dec, 10));
304
+ });
305
+
306
+ // 替换十六进制实体(如 &#x27; 等)
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
+ }