koishi-plugin-music-parser-all 0.0.1 → 0.0.2

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/lib/index.d.ts CHANGED
@@ -28,7 +28,6 @@ export declare const Config: Schema<{
28
28
  forceDownloadMusicVoice?: boolean | null | undefined;
29
29
  forceDownloadImage?: boolean | null | undefined;
30
30
  } & {
31
- maxLyricLength?: number | null | undefined;
32
31
  maxConcurrent?: number | null | undefined;
33
32
  downloadConcurrency?: number | null | undefined;
34
33
  mediaDownloadTimeout?: number | null | undefined;
@@ -65,6 +64,44 @@ export declare const Config: Schema<{
65
64
  deduplicationInterval?: number | null | undefined;
66
65
  cacheTTL?: number | null | undefined;
67
66
  cacheDir?: string | null | undefined;
67
+ } & {
68
+ primaryApiUrl?: string | null | undefined;
69
+ backupApiUrl?: string | null | undefined;
70
+ platformDedicatedFirst?: ({
71
+ netease?: boolean | null | undefined;
72
+ kuwo?: boolean | null | undefined;
73
+ qqmusic?: boolean | null | undefined;
74
+ qishui?: boolean | null | undefined;
75
+ } & import("cosmokit").Dict) | null | undefined;
76
+ customApis?: ({
77
+ platform?: "netease" | "kuwo" | "qqmusic" | "qishui" | null | undefined;
78
+ apiUrl?: string | null | undefined;
79
+ apiKey?: string | null | undefined;
80
+ authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
81
+ customHeaderName?: string | null | undefined;
82
+ fieldMapping?: string | null | undefined;
83
+ } & import("cosmokit").Dict)[] | null | undefined;
84
+ customPlatforms?: ({
85
+ name?: string | null | undefined;
86
+ exampleUrl?: string | null | undefined;
87
+ keywords?: string | null | undefined;
88
+ apiUrl?: string | null | undefined;
89
+ apiKey?: string | null | undefined;
90
+ authHeaderType?: "Bearer" | "X-API-Key" | "Custom" | null | undefined;
91
+ customHeaderName?: string | null | undefined;
92
+ fieldMapping?: string | null | undefined;
93
+ proxy?: ({
94
+ enabled?: boolean | null | undefined;
95
+ protocol?: "http" | "https" | null | undefined;
96
+ host?: string | null | undefined;
97
+ port?: number | null | undefined;
98
+ auth?: ({
99
+ username?: string | null | undefined;
100
+ password?: string | null | undefined;
101
+ } & import("cosmokit").Dict) | null | undefined;
102
+ } & import("cosmokit").Dict) | null | undefined;
103
+ } & import("cosmokit").Dict)[] | null | undefined;
104
+ globalFieldMapping?: string | null | undefined;
68
105
  } & {
69
106
  waitingTipText?: string | null | undefined;
70
107
  unsupportedPlatformText?: string | null | undefined;
@@ -92,7 +129,6 @@ export declare const Config: Schema<{
92
129
  forceDownloadMusicVoice: boolean;
93
130
  forceDownloadImage: boolean;
94
131
  } & {
95
- maxLyricLength: number;
96
132
  maxConcurrent: number;
97
133
  downloadConcurrency: number;
98
134
  mediaDownloadTimeout: number;
@@ -132,6 +168,59 @@ export declare const Config: Schema<{
132
168
  deduplicationInterval: number;
133
169
  cacheTTL: number;
134
170
  cacheDir: string;
171
+ } & {
172
+ primaryApiUrl: string;
173
+ backupApiUrl: string;
174
+ platformDedicatedFirst: Schemastery.ObjectT<{
175
+ netease: Schema<boolean, boolean>;
176
+ kuwo: Schema<boolean, boolean>;
177
+ qqmusic: Schema<boolean, boolean>;
178
+ qishui: Schema<boolean, boolean>;
179
+ }>;
180
+ customApis: Schemastery.ObjectT<{
181
+ platform: Schema<"netease" | "kuwo" | "qqmusic" | "qishui", "netease" | "kuwo" | "qqmusic" | "qishui">;
182
+ apiUrl: Schema<string, string>;
183
+ apiKey: Schema<string, string>;
184
+ authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
185
+ customHeaderName: Schema<string, string>;
186
+ fieldMapping: Schema<string, string>;
187
+ }>[];
188
+ customPlatforms: Schemastery.ObjectT<{
189
+ name: Schema<string, string>;
190
+ exampleUrl: Schema<string, string>;
191
+ keywords: Schema<string, string>;
192
+ apiUrl: Schema<string, string>;
193
+ apiKey: Schema<string, string>;
194
+ authHeaderType: Schema<"Bearer" | "X-API-Key" | "Custom", "Bearer" | "X-API-Key" | "Custom">;
195
+ customHeaderName: Schema<string, string>;
196
+ fieldMapping: Schema<string, string>;
197
+ proxy: Schema<Schemastery.ObjectS<{
198
+ enabled: Schema<boolean, boolean>;
199
+ protocol: Schema<"http" | "https", "http" | "https">;
200
+ host: Schema<string, string>;
201
+ port: Schema<number, number>;
202
+ auth: Schema<Schemastery.ObjectS<{
203
+ username: Schema<string, string>;
204
+ password: Schema<string, string>;
205
+ }>, Schemastery.ObjectT<{
206
+ username: Schema<string, string>;
207
+ password: Schema<string, string>;
208
+ }>>;
209
+ }>, Schemastery.ObjectT<{
210
+ enabled: Schema<boolean, boolean>;
211
+ protocol: Schema<"http" | "https", "http" | "https">;
212
+ host: Schema<string, string>;
213
+ port: Schema<number, number>;
214
+ auth: Schema<Schemastery.ObjectS<{
215
+ username: Schema<string, string>;
216
+ password: Schema<string, string>;
217
+ }>, Schemastery.ObjectT<{
218
+ username: Schema<string, string>;
219
+ password: Schema<string, string>;
220
+ }>>;
221
+ }>>;
222
+ }>[];
223
+ globalFieldMapping: string;
135
224
  } & {
136
225
  waitingTipText: string;
137
226
  unsupportedPlatformText: string;
package/lib/index.js CHANGED
@@ -82,18 +82,17 @@ exports.Config = koishi_1.Schema.intersect([
82
82
  }).description('各平台解析开关'),
83
83
  }).description('基本设置'),
84
84
  koishi_1.Schema.object({
85
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('歌名:${name}\n歌手:${artist}\n专辑:${album}\n音质:${level}\n歌词:${lyric}').description('文字格式,支持变量,空行自动隐藏'),
85
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('歌名:${name}\n歌手:${artist}\n专辑:${album}\n音质:${level}\n大小:${size}').description('文字格式,支持变量,空行自动隐藏'),
86
86
  }).description('消息格式'),
87
87
  koishi_1.Schema.object({
88
88
  showMusicText: koishi_1.Schema.boolean().default(true).description('发送文字内容'),
89
89
  showCoverImage: koishi_1.Schema.boolean().default(true).description('发送封面图片'),
90
90
  showMusicVoice: koishi_1.Schema.boolean().default(false).description('音乐链接以语音形式发送'),
91
91
  showMusicVoiceFile: koishi_1.Schema.boolean().default(true).description('音乐链接是否以语音形式发送(关闭则只发送链接)'),
92
- forceDownloadMusicVoice: koishi_1.Schema.boolean().default(false).description('强制下载音乐语音'),
92
+ forceDownloadMusicVoice: koishi_1.Schema.boolean().default(true).description('强制下载音乐语音(推荐开启,避免链接失效)'),
93
93
  forceDownloadImage: koishi_1.Schema.boolean().default(false).description('强制下载封面图片'),
94
94
  }).description('媒体发送与音乐语音'),
95
95
  koishi_1.Schema.object({
96
- maxLyricLength: koishi_1.Schema.number().min(0).step(1).default(500).description('歌词长度上限(0不限制)'),
97
96
  maxConcurrent: koishi_1.Schema.number().min(1).step(1).default(3).description('解析最大并发数'),
98
97
  downloadConcurrency: koishi_1.Schema.number().min(1).step(1).default(3).description('下载线程数'),
99
98
  mediaDownloadTimeout: koishi_1.Schema.number().min(0).step(1).default(120000).description('统一下载超时 (ms)'),
@@ -141,6 +140,69 @@ exports.Config = koishi_1.Schema.intersect([
141
140
  cacheTTL: koishi_1.Schema.number().min(0).step(1).default(600).description('缓存时间 (s)'),
142
141
  cacheDir: koishi_1.Schema.string().default('./temp_cache_music').description('统一临时目录'),
143
142
  }).description('缓存与临时文件'),
143
+ koishi_1.Schema.object({
144
+ primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/163_music').hidden(),
145
+ backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/music').hidden(),
146
+ platformDedicatedFirst: koishi_1.Schema.object({
147
+ netease: koishi_1.Schema.boolean().default(false).description('网易云音乐'),
148
+ kuwo: koishi_1.Schema.boolean().default(false).description('酷我音乐'),
149
+ qqmusic: koishi_1.Schema.boolean().default(false).description('QQ音乐'),
150
+ qishui: koishi_1.Schema.boolean().default(false).description('汽水音乐'),
151
+ }).description('优先使用专属 API'),
152
+ customApis: koishi_1.Schema.array(koishi_1.Schema.object({
153
+ platform: koishi_1.Schema.union([
154
+ koishi_1.Schema.const('netease').description('网易云音乐'),
155
+ koishi_1.Schema.const('kuwo').description('酷我音乐'),
156
+ koishi_1.Schema.const('qqmusic').description('QQ音乐'),
157
+ koishi_1.Schema.const('qishui').description('汽水音乐'),
158
+ ]).description('平台'),
159
+ apiUrl: koishi_1.Schema.string().description('API 地址'),
160
+ apiKey: koishi_1.Schema.string().description('API Key').default(''),
161
+ authHeaderType: koishi_1.Schema.union([
162
+ koishi_1.Schema.const('Bearer').description('Bearer'),
163
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
164
+ koishi_1.Schema.const('Custom').description('自定义'),
165
+ ]).default('Bearer').description('认证头类型'),
166
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
167
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
168
+ })).default([]).description('覆盖内置平台 API'),
169
+ customPlatforms: koishi_1.Schema.array(koishi_1.Schema.object({
170
+ name: koishi_1.Schema.string().required().description('平台名称'),
171
+ exampleUrl: koishi_1.Schema.string().description('示例链接'),
172
+ keywords: koishi_1.Schema.string().required().description('关键词(逗号分隔)'),
173
+ apiUrl: koishi_1.Schema.string().required().description('解析 API'),
174
+ apiKey: koishi_1.Schema.string().default('').description('API Key'),
175
+ authHeaderType: koishi_1.Schema.union([
176
+ koishi_1.Schema.const('Bearer').description('Bearer'),
177
+ koishi_1.Schema.const('X-API-Key').description('X-API-Key'),
178
+ koishi_1.Schema.const('Custom').description('自定义'),
179
+ ]).default('Bearer').description('认证头类型'),
180
+ customHeaderName: koishi_1.Schema.string().default('X-API-Key').description('自定义头名称'),
181
+ fieldMapping: koishi_1.Schema.string().role('textarea').default('{}').description('字段映射 JSON'),
182
+ proxy: koishi_1.Schema.object({
183
+ enabled: koishi_1.Schema.boolean().default(false).description('启用独立代理'),
184
+ protocol: koishi_1.Schema.union([
185
+ koishi_1.Schema.const('http').description('HTTP'),
186
+ koishi_1.Schema.const('https').description('HTTPS'),
187
+ ]).default('http').description('协议'),
188
+ host: koishi_1.Schema.string().default('127.0.0.1').description('地址'),
189
+ port: koishi_1.Schema.number().default(7890).description('端口'),
190
+ auth: koishi_1.Schema.object({
191
+ username: koishi_1.Schema.string().default('').description('用户名'),
192
+ password: koishi_1.Schema.string().default('').description('密码'),
193
+ }).description('认证'),
194
+ }).description('独立代理(覆盖全局代理)'),
195
+ })).default([]).description('自定义新平台'),
196
+ globalFieldMapping: koishi_1.Schema.string().role('textarea').default('{\n' +
197
+ ' "name": "data.name",\n' +
198
+ ' "artist": "data.ar_name",\n' +
199
+ ' "album": "data.al_name",\n' +
200
+ ' "cover": "data.pic",\n' +
201
+ ' "musicUrl": "data.url",\n' +
202
+ ' "level": "data.level",\n' +
203
+ ' "size": "data.size"\n' +
204
+ '}').description('全局字段映射 JSON'),
205
+ }).description('API 与平台'),
144
206
  koishi_1.Schema.object({
145
207
  waitingTipText: koishi_1.Schema.string().default('正在解析音乐,请稍候...').description('等待提示'),
146
208
  unsupportedPlatformText: koishi_1.Schema.string().default('暂不支持该平台音乐链接').description('不支持提示'),
@@ -157,13 +219,28 @@ function debugLog(level, ...args) {
157
219
  logger.info(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`);
158
220
  }
159
221
  const BUILTIN_LINK_RULES = [
160
- { pattern: /https?:\/\/(?:music\.163\.com\/(?:#\/)?song\?id=\d{3,}|163cn\.tv\/[A-Za-z0-9]+|y\.music\.163\.com\/m\/song\?id=\d{3,})[^\s]*/gi, type: 'netease' },
161
- { pattern: /https?:\/\/www\.kuwo\.cn\/play_detail\/\d+[^\s]*/gi, type: 'kuwo' },
162
- { pattern: /https?:\/\/y\.qq\.com\/n\/ryqq\/songDetail\/[A-Za-z0-9]+[^\s]*/gi, type: 'qqmusic' },
163
- { pattern: /https?:\/\/i\.y\.qq\.com\/v8\/playsong\.html\?songid=\d+[^\s]*/gi, type: 'qqmusic' },
164
- { pattern: /https?:\/\/qishui\.douyin\.com\/s\/[A-Za-z0-9]+[^\s]*/gi, type: 'qishui' },
222
+ { pattern: /https?:\/\/(?:music\.163\.com\/(?:#\/)?song\?id=(\d{3,})|163cn\.tv\/[A-Za-z0-9]+|y\.music\.163\.com\/m\/song\?id=(\d{3,}))/gi, type: 'netease' },
223
+ { pattern: /https?:\/\/www\.kuwo\.cn\/play_detail\/(\d+)/gi, type: 'kuwo' },
224
+ { pattern: /https?:\/\/y\.qq\.com\/n\/ryqq\/songDetail\/([A-Za-z0-9]+)/gi, type: 'qqmusic' },
225
+ { pattern: /https?:\/\/i\.y\.qq\.com\/v8\/playsong\.html\?songid=(\d+)/gi, type: 'qqmusic' },
226
+ { pattern: /https?:\/\/qishui\.douyin\.com\/s\/([A-Za-z0-9]+)/gi, type: 'qishui' },
165
227
  ];
166
- function linkTypeParser(content, customRules = []) {
228
+ function buildCustomLinkRules(customPlatforms) {
229
+ if (!Array.isArray(customPlatforms) || customPlatforms.length === 0)
230
+ return [];
231
+ return customPlatforms
232
+ .filter(p => p.keywords)
233
+ .map(p => {
234
+ const keywords = p.keywords.split(',').map((s) => s.trim()).filter(Boolean);
235
+ if (keywords.length === 0)
236
+ return null;
237
+ const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
238
+ const pattern = new RegExp('https?://[^/\\s]*(' + escaped.join('|') + ')[^\\s]*', 'gi');
239
+ return { pattern, type: `custom_${p.name}` };
240
+ })
241
+ .filter(Boolean);
242
+ }
243
+ function linkTypeParser(content, customRules) {
167
244
  content = content.replace(/\\\//g, '/');
168
245
  const allRules = [...BUILTIN_LINK_RULES, ...customRules];
169
246
  const matches = [];
@@ -176,7 +253,8 @@ function linkTypeParser(content, customRules = []) {
176
253
  if (seen.has(url))
177
254
  continue;
178
255
  seen.add(url);
179
- matches.push({ type: rule.type, url, id: match[1] || url });
256
+ const id = match[1] || match[2] || url;
257
+ matches.push({ type: rule.type, url, id });
180
258
  }
181
259
  }
182
260
  return matches;
@@ -237,80 +315,105 @@ function cleanUrl(url) {
237
315
  return url.replace(/&amp;/g, '&').replace(/\?.*/, '');
238
316
  }
239
317
  }
240
- const API_ENDPOINTS = {
241
- netease: 'https://api.bugpk.com/api/163_music',
242
- kuwo: 'https://api.bugpk.com/api/kuwo',
243
- qqmusic: 'https://api.bugpk.com/api/qqmusic',
244
- qishui: 'https://api.bugpk.com/api/qsmusic',
245
- };
246
- async function fetchMusicApi(type, url, config, http) {
247
- const apiUrl = API_ENDPOINTS[type];
248
- if (!apiUrl)
249
- throw new Error(`不支持的平台: ${type}`);
250
- const params = { url: cleanUrl(url) };
251
- if (type === 'netease')
252
- params.type = 'json';
253
- const res = await http.get(apiUrl, { params });
254
- if (res.data && (res.data.code === 200 || res.data.code === 0)) {
255
- return res.data.data;
318
+ function parseFieldMapping(mappingStr) {
319
+ if (!mappingStr || mappingStr.trim() === '{}' || mappingStr.trim() === '')
320
+ return undefined;
321
+ try {
322
+ const obj = JSON.parse(mappingStr);
323
+ if (typeof obj === 'object' && !Array.isArray(obj))
324
+ return obj;
325
+ return undefined;
326
+ }
327
+ catch {
328
+ return undefined;
256
329
  }
257
- throw new Error(res.data?.msg || `API返回错误码: ${res.data?.code}`);
258
330
  }
259
- function parseMusicResponse(rawData, type, maxLyricLen) {
260
- debugLog('DEBUG', `解析原始数据 [${type}]:`, rawData);
261
- switch (type) {
262
- case 'netease':
263
- return {
264
- type: 'netease',
265
- name: rawData.name || '',
266
- artist: rawData.ar_name || '',
267
- album: rawData.al_name || '',
268
- cover: rawData.pic || '',
269
- musicUrl: rawData.url || '',
270
- lyric: (rawData.lyric || '').slice(0, maxLyricLen),
271
- level: rawData.level || '',
272
- size: rawData.size || '',
273
- };
274
- case 'kuwo':
275
- return {
276
- type: 'kuwo',
277
- name: rawData.title || '',
278
- artist: rawData.artist || '',
279
- album: rawData.album || '',
280
- cover: rawData.pic || rawData.albumpic || '',
281
- musicUrl: rawData.music_url || '',
282
- lyric: (rawData.lyrics_url || '').slice(0, maxLyricLen),
283
- level: '',
284
- size: '',
285
- };
286
- case 'qqmusic':
287
- return {
288
- type: 'qqmusic',
289
- name: rawData.name || '',
290
- artist: rawData.author || '',
291
- album: rawData.album || '',
292
- cover: rawData.cover || '',
293
- musicUrl: rawData.url || '',
294
- lyric: (rawData.lrc_data || '').slice(0, maxLyricLen),
295
- level: '',
296
- size: '',
297
- };
298
- case 'qishui':
299
- const cover = Array.isArray(rawData.artistsmedium_avatar_url) ? rawData.artistsmedium_avatar_url[0] : '';
300
- return {
301
- type: 'qishui',
302
- name: rawData.albumname || '',
303
- artist: rawData.artistsname || '',
304
- album: rawData.albumname || '',
305
- cover: cover,
306
- musicUrl: rawData.url || '',
307
- lyric: (rawData.lyric || '').slice(0, maxLyricLen),
308
- level: rawData.Format || '',
309
- size: rawData.Size || '',
310
- };
311
- default:
312
- throw new Error('未知平台类型');
331
+ function getNestedValue(obj, path) {
332
+ if (!path)
333
+ return obj;
334
+ const keys = path.split('.');
335
+ let current = obj;
336
+ for (const key of keys) {
337
+ if (current === null || current === undefined)
338
+ return undefined;
339
+ current = current[key];
313
340
  }
341
+ return current;
342
+ }
343
+ function parseApiResponse(raw, type, fieldMapping) {
344
+ debugLog('DEBUG', 'API raw response', raw);
345
+ const data = raw?.data || raw;
346
+ const mapField = (name, fallback) => {
347
+ if (fieldMapping && fieldMapping[name]) {
348
+ const value = getNestedValue(raw, fieldMapping[name]);
349
+ if (value !== undefined)
350
+ return value;
351
+ }
352
+ return fallback();
353
+ };
354
+ const name = mapField('name', () => {
355
+ switch (type) {
356
+ case 'netease': return data.name || data.ar_name || '';
357
+ case 'kuwo': return data.title || '';
358
+ case 'qqmusic': return data.name || '';
359
+ case 'qishui': return data.albumname || '';
360
+ default: return data.name || data.title || data.albumname || '';
361
+ }
362
+ });
363
+ const artist = mapField('artist', () => {
364
+ switch (type) {
365
+ case 'netease': return data.ar_name || '';
366
+ case 'kuwo': return data.artist || '';
367
+ case 'qqmusic': return data.author || '';
368
+ case 'qishui': return data.artistsname || '';
369
+ default: return data.ar_name || data.artist || data.author || data.artistsname || '';
370
+ }
371
+ });
372
+ const album = mapField('album', () => {
373
+ switch (type) {
374
+ case 'netease': return data.al_name || '';
375
+ case 'kuwo': return data.album || '';
376
+ case 'qqmusic': return data.album || '';
377
+ case 'qishui': return data.albumname || '';
378
+ default: return data.al_name || data.album || data.albumname || '';
379
+ }
380
+ });
381
+ const cover = mapField('cover', () => {
382
+ switch (type) {
383
+ case 'netease': return data.pic || '';
384
+ case 'kuwo': return data.pic || data.albumpic || '';
385
+ case 'qqmusic': return data.cover || '';
386
+ case 'qishui': {
387
+ const avatars = data.artistsmedium_avatar_url;
388
+ return Array.isArray(avatars) ? avatars[0] || '' : '';
389
+ }
390
+ default: return data.pic || data.cover || '';
391
+ }
392
+ });
393
+ const musicUrl = mapField('musicUrl', () => {
394
+ switch (type) {
395
+ case 'netease': return data.url || '';
396
+ case 'kuwo': return data.music_url || '';
397
+ case 'qqmusic': return data.url || '';
398
+ case 'qishui': return data.url || '';
399
+ default: return data.url || data.music_url || '';
400
+ }
401
+ });
402
+ const level = mapField('level', () => {
403
+ switch (type) {
404
+ case 'netease': return data.level || '';
405
+ case 'qishui': return data.Format || '';
406
+ default: return data.level || data.Format || '';
407
+ }
408
+ });
409
+ const size = mapField('size', () => {
410
+ switch (type) {
411
+ case 'netease': return data.size || '';
412
+ case 'qishui': return data.Size || '';
413
+ default: return data.size || data.Size || '';
414
+ }
415
+ });
416
+ return { type, name, artist, album, cover, musicUrl, level, size };
314
417
  }
315
418
  function generateFormattedText(p, format) {
316
419
  const vars = {
@@ -319,7 +422,6 @@ function generateFormattedText(p, format) {
319
422
  album: p.album,
320
423
  cover: p.cover,
321
424
  music_url: p.musicUrl,
322
- lyric: p.lyric,
323
425
  level: p.level || '未知',
324
426
  size: p.size || '未知',
325
427
  };
@@ -367,6 +469,17 @@ function getErrorMessage(error) {
367
469
  return String(error.message);
368
470
  return String(error);
369
471
  }
472
+ function buildAuthHeaders(apiKey, authHeaderType, customHeaderName) {
473
+ if (!apiKey)
474
+ return {};
475
+ if (authHeaderType === 'Bearer')
476
+ return { 'Authorization': `Bearer ${apiKey}` };
477
+ if (authHeaderType === 'X-API-Key')
478
+ return { 'X-API-Key': apiKey };
479
+ if (authHeaderType === 'Custom' && customHeaderName)
480
+ return { [customHeaderName]: apiKey };
481
+ return {};
482
+ }
370
483
  function apply(ctx, config) {
371
484
  debugEnabled = config.debug || false;
372
485
  debugLog('INFO', '音乐解析插件启动');
@@ -408,6 +521,58 @@ function apply(ctx, config) {
408
521
  logger.warn('aria2 连接失败,回退到内置下载');
409
522
  }
410
523
  }
524
+ const customPlatforms = (config.customPlatforms || []).map((p) => ({
525
+ name: p.name,
526
+ apiUrl: p.apiUrl,
527
+ apiKey: p.apiKey || '',
528
+ authHeaderType: p.authHeaderType || 'Bearer',
529
+ customHeaderName: p.customHeaderName || 'X-API-Key',
530
+ fieldMapping: parseFieldMapping(p.fieldMapping),
531
+ proxy: p.proxy || null
532
+ }));
533
+ function getPlatformConfig(type) {
534
+ if (type.startsWith('custom_')) {
535
+ const name = type.slice(7);
536
+ const custom = customPlatforms.find(p => p.name === name);
537
+ if (custom) {
538
+ return {
539
+ apiUrl: custom.apiUrl,
540
+ dedicatedFirst: true,
541
+ apiKey: custom.apiKey || '',
542
+ authHeaderType: custom.authHeaderType,
543
+ customHeaderName: custom.customHeaderName,
544
+ fieldMapping: custom.fieldMapping,
545
+ customProxy: custom.proxy
546
+ };
547
+ }
548
+ return { apiUrl: null, dedicatedFirst: false, apiKey: '', authHeaderType: 'Bearer', customHeaderName: 'X-API-Key' };
549
+ }
550
+ const custom = config.customApis?.find((item) => item.platform === type);
551
+ const defaultDedicatedApis = {
552
+ netease: 'https://api.bugpk.com/api/163_music',
553
+ kuwo: 'https://api.bugpk.com/api/kuwo',
554
+ qqmusic: 'https://api.bugpk.com/api/qqmusic',
555
+ qishui: 'https://api.bugpk.com/api/qsmusic',
556
+ };
557
+ let apiUrl = defaultDedicatedApis[type] || null;
558
+ let apiKey = '';
559
+ let authHeaderType = 'Bearer';
560
+ let customHeaderName = 'X-API-Key';
561
+ let fieldMapping = undefined;
562
+ if (custom && custom.apiUrl) {
563
+ apiUrl = custom.apiUrl;
564
+ apiKey = custom.apiKey || '';
565
+ authHeaderType = custom.authHeaderType || 'Bearer';
566
+ customHeaderName = custom.customHeaderName || 'X-API-Key';
567
+ fieldMapping = parseFieldMapping(custom.fieldMapping);
568
+ }
569
+ const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
570
+ if (!fieldMapping) {
571
+ fieldMapping = parseFieldMapping(config.globalFieldMapping);
572
+ }
573
+ return { apiUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, fieldMapping };
574
+ }
575
+ const BACKUP_AGGREGATE_API = config.backupApiUrl || 'https://api.bugpk.com/api/music';
411
576
  async function downloadFile(url, timeout, maxSize, filePrefix, fileExts) {
412
577
  if (!url)
413
578
  throw new Error('链接为空');
@@ -517,7 +682,7 @@ function apply(ctx, config) {
517
682
  const sendLink = async () => { await sendWithTimeout(session, `${type === 'audio' ? '音乐' : '封面'}链接:${url}`).catch(() => { }); };
518
683
  const extMap = {
519
684
  image: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
520
- audio: ['mp3', 'm4a', 'flac', 'wav', 'ogg', 'aac']
685
+ audio: ['mp3', 'm4a', 'flac', 'wav', 'ogg', 'aac', 'opus', 'wma', 'ape', 'wv', 'alac']
521
686
  };
522
687
  const prefixMap = { image: 'img', audio: 'music' };
523
688
  const sendFunc = type === 'audio' ? koishi_1.h.audio : koishi_1.h.image;
@@ -569,6 +734,100 @@ function apply(ctx, config) {
569
734
  downloadLimiter.release();
570
735
  }
571
736
  }
737
+ async function fetchApi(url, type, matchId, fieldMapping, platformConf) {
738
+ const cacheKey = url;
739
+ const cached = urlCacheLocal.get(cacheKey);
740
+ if (cached && cached.expire > Date.now())
741
+ return cached.data;
742
+ const { apiUrl: dedicatedUrl, dedicatedFirst, apiKey, authHeaderType, customHeaderName, customProxy } = platformConf || getPlatformConfig(type);
743
+ const primaryApi = dedicatedUrl;
744
+ const backupApis = [];
745
+ if (type === 'netease' || type === 'qqmusic') {
746
+ backupApis.push({ url: BACKUP_AGGREGATE_API, label: '聚合备用API', fieldMapping });
747
+ }
748
+ const apiList = [];
749
+ if (dedicatedFirst && primaryApi) {
750
+ apiList.push({ url: primaryApi, label: `专属API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
751
+ apiList.push(...backupApis);
752
+ }
753
+ else {
754
+ if (primaryApi)
755
+ apiList.push({ url: primaryApi, label: `默认API(${type})`, apiKey, authHeaderType, customHeaderName, fieldMapping });
756
+ apiList.push(...backupApis);
757
+ }
758
+ const customHeaders = config.customHeaders || [];
759
+ let lastError = null;
760
+ for (const api of apiList) {
761
+ for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
762
+ try {
763
+ const headers = {
764
+ 'User-Agent': config.userAgent,
765
+ 'Referer': 'https://www.baidu.com/',
766
+ };
767
+ for (const h of customHeaders) {
768
+ if (h.name && h.value)
769
+ headers[h.name] = h.value;
770
+ }
771
+ if (api.apiKey) {
772
+ const authHeaders = buildAuthHeaders(api.apiKey, api.authHeaderType || 'Bearer', api.customHeaderName || 'X-API-Key');
773
+ Object.assign(headers, authHeaders);
774
+ }
775
+ let apiUrl = api.url;
776
+ let params = {};
777
+ if (apiUrl === BACKUP_AGGREGATE_API) {
778
+ const media = type === 'netease' ? 'netease' : 'tencent';
779
+ params = { id: matchId, media, type: 'song' };
780
+ }
781
+ else {
782
+ params = { url: cleanUrl(url) };
783
+ if (type === 'netease' && apiUrl.includes('163_music'))
784
+ params.type = 'json';
785
+ }
786
+ const proxyToUse = customProxy && customProxy.enabled ? customProxy : (proxyConfig.enabled ? proxyConfig : undefined);
787
+ const axiosConfigLocal = {
788
+ params,
789
+ timeout: config.timeout,
790
+ headers,
791
+ proxy: proxyToUse && proxyToUse.host ? {
792
+ protocol: proxyToUse.protocol || 'http',
793
+ host: proxyToUse.host,
794
+ port: proxyToUse.port || 7890,
795
+ auth: proxyToUse.auth?.username ? { username: proxyToUse.auth.username, password: proxyToUse.auth.password || '' } : undefined
796
+ } : undefined
797
+ };
798
+ const res = await http.get(apiUrl, axiosConfigLocal);
799
+ const rawData = res.data;
800
+ if (rawData && (rawData.code === 200 || rawData.code === 0 || (apiUrl === BACKUP_AGGREGATE_API && rawData.url))) {
801
+ const parsed = parseApiResponse(rawData, type, api.fieldMapping);
802
+ urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
803
+ return parsed;
804
+ }
805
+ throw new Error(rawData?.msg || `API返回错误码: ${rawData?.code}`);
806
+ }
807
+ catch (error) {
808
+ lastError = error instanceof Error ? error : new Error(String(error));
809
+ debugLog('ERROR', `${api.label} attempt ${attempt + 1} failed: ${lastError.message}`);
810
+ if (attempt < config.retryTimes) {
811
+ await delay(config.retryInterval);
812
+ continue;
813
+ }
814
+ break;
815
+ }
816
+ }
817
+ debugLog('WARN', `${api.label} all retries failed`);
818
+ }
819
+ throw lastError || new Error('所有API请求全部失败');
820
+ }
821
+ async function processSingleUrl(url, type, matchId, fieldMapping, platformConf) {
822
+ try {
823
+ const parsed = await fetchApi(url, type, matchId, fieldMapping, platformConf);
824
+ const text = generateFormattedText(parsed, config.unifiedMessageFormat);
825
+ return { success: true, data: { text, parsed } };
826
+ }
827
+ catch (error) {
828
+ return { success: false, msg: getErrorMessage(error), url };
829
+ }
830
+ }
572
831
  async function flush(session, matches) {
573
832
  debugLog('INFO', `开始解析 ${matches.length} 个链接`);
574
833
  const items = [];
@@ -578,7 +837,7 @@ function apply(ctx, config) {
578
837
  await limiter.acquire();
579
838
  try {
580
839
  const platformEnabled = config.platformEnabled?.[match.type] ?? true;
581
- if (!platformEnabled) {
840
+ if (!platformEnabled && !match.type.startsWith('custom_')) {
582
841
  debugLog('INFO', `平台 ${match.type} 已禁用,跳过链接: ${match.url}`);
583
842
  return;
584
843
  }
@@ -586,13 +845,12 @@ function apply(ctx, config) {
586
845
  const lastTime = dedupCache.get(match.url);
587
846
  if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
588
847
  debugLog('INFO', `跳过重复链接: ${match.url}`);
589
- const shortUrl = match.url.length > 50 ? match.url.slice(0, 50) + '...' : match.url;
590
- await sendWithTimeout(session, `链接 ${shortUrl} 在最近 ${config.deduplicationInterval} 秒内已解析过,已跳过。`).catch(() => { });
591
848
  return;
592
849
  }
593
850
  }
594
851
  debugLog('INFO', `解析链接: ${match.url} (${match.type})`);
595
- const result = await processSingleUrl(match.url, match.type);
852
+ const platformConf = getPlatformConfig(match.type);
853
+ const result = await processSingleUrl(match.url, match.type, match.id, platformConf.fieldMapping, platformConf);
596
854
  if (result.success) {
597
855
  if (config.deduplicationInterval > 0) {
598
856
  const fp = contentFingerprint(result.data.parsed);
@@ -667,32 +925,6 @@ function apply(ctx, config) {
667
925
  }
668
926
  debugLog('INFO', '处理完成');
669
927
  }
670
- async function processSingleUrl(url, type) {
671
- const cacheKey = url;
672
- const cached = urlCacheLocal.get(cacheKey);
673
- if (cached && cached.expire > Date.now()) {
674
- const text = generateFormattedText(cached.data, config.unifiedMessageFormat);
675
- return { success: true, data: { text, parsed: cached.data } };
676
- }
677
- for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
678
- try {
679
- const data = await fetchMusicApi(type, url, config, http);
680
- const parsed = parseMusicResponse(data, type, config.maxLyricLength);
681
- urlCacheLocal.set(cacheKey, { data: parsed, expire: Date.now() + cacheTTL });
682
- const text = generateFormattedText(parsed, config.unifiedMessageFormat);
683
- return { success: true, data: { text, parsed } };
684
- }
685
- catch (error) {
686
- const errMsg = getErrorMessage(error);
687
- debugLog('ERROR', `解析尝试 ${attempt + 1} 失败: ${errMsg}`);
688
- if (attempt < config.retryTimes)
689
- await delay(config.retryInterval);
690
- else
691
- return { success: false, msg: errMsg, url };
692
- }
693
- }
694
- return { success: false, msg: texts.unsupportedPlatformText, url };
695
- }
696
928
  async function sendWithTimeout(session, content, customRetries) {
697
929
  const maxRetries = customRetries ?? config.retryTimes ?? 3;
698
930
  const retryDelay = config.retryInterval || 1000;
@@ -718,6 +950,7 @@ function apply(ctx, config) {
718
950
  }
719
951
  return null;
720
952
  }
953
+ const customRules = buildCustomLinkRules(config.customPlatforms || []);
721
954
  const axiosConfig = {
722
955
  timeout: config.timeout,
723
956
  headers: {
@@ -756,7 +989,7 @@ function apply(ctx, config) {
756
989
  return;
757
990
  if (session.selfId === session.userId)
758
991
  return;
759
- const matches = extractAllUrlsFromMessage(session, []);
992
+ const matches = extractAllUrlsFromMessage(session, customRules);
760
993
  if (!matches.length)
761
994
  return;
762
995
  debugLog('INFO', `检测到 ${matches.length} 个音乐链接`);
@@ -775,7 +1008,7 @@ function apply(ctx, config) {
775
1008
  await sendWithTimeout(session, texts.invalidLinkText);
776
1009
  return;
777
1010
  }
778
- const matches = linkTypeParser(url, []);
1011
+ const matches = linkTypeParser(url, customRules);
779
1012
  if (!matches.length) {
780
1013
  await sendWithTimeout(session, texts.invalidLinkText);
781
1014
  return;
@@ -794,7 +1027,7 @@ function apply(ctx, config) {
794
1027
  const now = Date.now();
795
1028
  for (const file of files) {
796
1029
  if ((file.startsWith('music_') || file.startsWith('img_')) &&
797
- (file.match(/\.(mp3|m4a|flac|wav|ogg|aac|png|jpg|jpeg|gif|webp)$/i))) {
1030
+ (file.match(/\.(mp3|m4a|flac|wav|ogg|aac|opus|wma|ape|wv|alac|png|jpg|jpeg|gif|webp)$/i))) {
798
1031
  const filePath = path_1.default.join(cacheDir, file);
799
1032
  const stats = await promises_1.default.stat(filePath);
800
1033
  if (now - stats.mtimeMs > 3600000) {
@@ -821,7 +1054,7 @@ function apply(ctx, config) {
821
1054
  const files = await promises_1.default.readdir(cacheDir);
822
1055
  for (const file of files) {
823
1056
  if ((file.startsWith('music_') || file.startsWith('img_')) &&
824
- (file.match(/\.(mp3|m4a|flac|wav|ogg|aac|png|jpg|jpeg|gif|webp)$/i))) {
1057
+ (file.match(/\.(mp3|m4a|flac|wav|ogg|aac|opus|wma|ape|wv|alac|png|jpg|jpeg|gif|webp)$/i))) {
825
1058
  await promises_1.default.unlink(path_1.default.join(cacheDir, file)).catch(() => { });
826
1059
  }
827
1060
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-music-parser-all",
3
3
  "description": "Koishi 全平台音乐解析插件,支持网易云音乐/酷我音乐/QQ音乐/汽水音乐等平台",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -1,22 +1,22 @@
1
- # koishi-plugin-video-parser-all
1
+ # koishi-plugin-music-parser-all
2
2
 
3
3
  ## 项目介绍 (Project Introduction)
4
4
 
5
5
  ### 中文
6
- 这是一个为 Koishi 机器人框架开发的**全平台视频/图集解析插件**,使用统一API接口,支持自动识别并解析抖音、快手、B站、小红书、微博、YouTube、TikTok、剪映、AcFun、知乎、虎牙、绿洲、视频号等20+主流平台的短视频/图集/实况链接。
6
+ 这是一个为 Koishi 机器人框架开发的**全平台音乐解析插件**,使用统一API接口,支持自动识别并解析网易云音乐、酷我音乐、QQ音乐、汽水音乐等主流音乐平台的歌曲链接,获取歌名、歌手、专辑、封面及高音质直链,并支持以语音消息发送音乐。内置独立接口优先、聚合接口备用的解析策略,同时支持自定义 API、平台扩展与字段映射。
7
7
 
8
8
  ### English
9
- This is a **multi-platform video/image parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse short video/image/live photo links from 20+ mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, YouTube, TikTok, Jianying, AcFun, Zhihu, Huya, Oasis, WeChat Channels and more.
9
+ This is a **multi-platform music parsing plugin** developed for the Koishi bot framework, using a unified API interface to automatically recognize and parse music links from major platforms such as NetEase Cloud Music, Kuwo Music, QQ Music, Qishui Music and more, obtaining track info, cover, and high-quality direct links, with optional voice message playback. It features dedicated API priority with aggregate API fallback, and supports custom APIs, platform extensions, and field mapping.
10
10
 
11
11
  ## 项目仓库 (Repository)
12
- - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
13
- - Issues: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues`
12
+ - GitHub: https://github.com/Minecraft-1314/koishi-plugin-music-parser-all
13
+ - Issues: https://github.com/Minecraft-1314/koishi-plugin-music-parser-all/issues
14
14
 
15
15
  ## 核心指令 (Core Commands)
16
16
 
17
- | 指令 (Command) | 说明 (Description) | 示例 (Example) |
18
- |----------------|--------------------|----------------|
19
- | `parse <url>` | 手动解析指定的视频/图集链接 | `parse https://v.douyin.com/xxxx/` |
17
+ | 指令 | 说明 | 示例 |
18
+ |------|------|------|
19
+ | `music <url>` | 手动解析指定的音乐链接 | `music https://music.163.com/song?id=865632948` |
20
20
 
21
21
  ## 配置项说明 (Configuration)
22
22
 
@@ -24,38 +24,29 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
24
24
  | 配置项 | 类型 | 默认值 | 说明 |
25
25
  |--------|------|--------|------|
26
26
  | `enable` | boolean | true | 启用插件 |
27
- | `botName` | string | 视频解析机器人 | 合并转发中的昵称 |
27
+ | `botName` | string | 音乐解析机器人 | 合并转发中的昵称 |
28
28
  | `showWaitingTip` | boolean | true | 显示等待提示 |
29
29
  | `debug` | boolean | false | Debug 日志 |
30
- | `platformEnabled` | object | 全开 | 各平台开关 |
30
+ | `platformEnabled` | object | 全开 | 各平台开关(netease/kuwo/qqmusic/qishui) |
31
31
 
32
32
  ### 消息格式
33
33
  | 配置项 | 类型 | 默认值 | 说明 |
34
34
  |--------|------|--------|------|
35
35
  | `unifiedMessageFormat` | string | 见预设 | 文字格式,支持变量,空行自动隐藏 |
36
36
 
37
- ### 媒体发送
37
+ ### 媒体发送与音乐语音
38
38
  | 配置项 | 类型 | 默认值 | 说明 |
39
39
  |--------|------|--------|------|
40
- | `showImageText` | boolean | true | 发送文字内容 |
40
+ | `showMusicText` | boolean | true | 发送文字内容 |
41
41
  | `showCoverImage` | boolean | true | 发送封面图片 |
42
- | `showMusicCover` | boolean | true | 发送音乐封面 |
43
- | `showImageFile` | boolean | true | 封面/图片是否以图片形式发送(关闭则只发送链接) |
44
- | `showVideoFile` | boolean | true | 视频是否以视频形式发送(关闭则只发送链接) |
45
- | `forceDownloadImage` | boolean | false | 强制下载封面/图片 |
46
- | `forceDownloadVideo` | boolean | false | 强制下载视频 |
47
-
48
- ### 音乐语音(需 silk 和 ffmpeg)
49
- | 配置项 | 类型 | 默认值 | 说明 |
50
- |--------|------|--------|------|
51
- | `showMusicVoice` | boolean | false | 音乐链接以语音发送 |
42
+ | `showMusicVoice` | boolean | false | 音乐链接以语音形式发送 |
52
43
  | `showMusicVoiceFile` | boolean | true | 音乐链接是否以语音形式发送(关闭则只发送链接) |
53
- | `forceDownloadMusicVoice` | boolean | false | 强制下载音乐语音 |
44
+ | `forceDownloadMusicVoice` | boolean | true | 强制下载音乐语音(推荐开启,避免直链失效) |
45
+ | `forceDownloadImage` | boolean | false | 强制下载封面图片 |
54
46
 
55
47
  ### 性能与限制
56
48
  | 配置项 | 类型 | 默认值 | 说明 |
57
49
  |--------|------|--------|------|
58
- | `maxDescLength` | number | 200 | 简介长度上限 |
59
50
  | `maxConcurrent` | number | 3 | 解析最大并发数 |
60
51
  | `downloadConcurrency` | number | 3 | 下载线程数 |
61
52
  | `mediaDownloadTimeout` | number | 120000 | 统一下载超时 (ms) |
@@ -63,16 +54,16 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
63
54
  | `downloadEngine` | string | internal | 下载引擎(internal / aria2 / downloads) |
64
55
  | `aria2Host` | string | 127.0.0.1 | aria2 RPC 地址 |
65
56
  | `aria2Port` | number | 6800 | aria2 RPC 端口 |
66
- | `aria2Secret` | string | | aria2 RPC 密钥 |
57
+ | `aria2Secret` | string | | aria2 RPC 密钥 |
67
58
  | `resumeDownload` | boolean | true | 启用断点续传(仅 aria2) |
68
59
 
69
60
  ### 网络与请求
70
61
  | 配置项 | 类型 | 默认值 | 说明 |
71
62
  |--------|------|--------|------|
72
63
  | `timeout` | number | 180000 | API 超时 (ms) |
73
- | `videoSendTimeout` | number | 180000 | 发送超时 (ms) |
64
+ | `videoSendTimeout` | number | 180000 | 消息发送超时 (ms) |
74
65
  | `userAgent` | string | 见预设 | User-Agent |
75
- | `proxy` | object | ... | HTTP/HTTPS 代理 |
66
+ | `proxy` | object | | HTTP/HTTPS 代理 |
76
67
  | `customHeaders` | array | [] | 自定义请求头 |
77
68
 
78
69
  ### 发送与重试
@@ -88,112 +79,81 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
88
79
  |--------|------|--------|------|
89
80
  | `deduplicationInterval` | number | 180 | 去重间隔 (s) |
90
81
  | `cacheTTL` | number | 600 | 缓存时间 (s) |
91
- | `cacheDir` | string | ./temp_cache | 统一临时目录 |
82
+ | `cacheDir` | string | ./temp_cache_music | 统一临时目录 |
92
83
 
93
- ### API 与平台
84
+ ### API 与平台(新增)
94
85
  | 配置项 | 类型 | 默认值 | 说明 |
95
86
  |--------|------|--------|------|
96
- | `platformDedicatedFirst` | object | 全关 | 优先专属 API |
97
- | `customApis` | array | [] | 覆盖内置平台 API |
98
- | `customPlatforms` | array | [] | 自定义新平台 |
99
- | `globalFieldMapping` | string | 预设 | 全局字段映射 JSON |
87
+ | `platformDedicatedFirst` | object | 全关 | 优先使用专属 API(每个平台可单独设定) |
88
+ | `customApis` | array | [] | 覆盖内置平台 API(支持自定义 URL、API Key 认证、字段映射) |
89
+ | `customPlatforms` | array | [] | 自定义新平台(可设定关键词匹配、独立代理、字段映射) |
90
+ | `globalFieldMapping` | string | 预设 | 全局字段映射 JSON,适配不同 API 返回结构 |
100
91
 
101
92
  ### 界面文本
102
93
  | 配置项 | 类型 | 默认值 | 说明 |
103
94
  |--------|------|--------|------|
104
- | `waitingTipText` | string | 正在解析... | 等待提示 |
105
- | `unsupportedPlatformText` | string | 不支持该平台 | 不支持提示 |
106
- | `invalidLinkText` | string | 无效链接 | 无效链接提示 |
95
+ | `waitingTipText` | string | 正在解析音乐,请稍候... | 等待提示 |
96
+ | `unsupportedPlatformText` | string | 暂不支持该平台音乐链接 | 不支持提示 |
97
+ | `invalidLinkText` | string | 无效的音乐链接 | 无效链接提示 |
107
98
  | `parseErrorPrefix` | string | ❌ 解析失败: | 错误前缀 |
108
- | `parseErrorItemFormat` | string | ... | 错误格式 |
99
+ | `parseErrorItemFormat` | string | 见预设 | 错误格式 |
109
100
 
110
101
  ## 支持的变量 (Supported Variables)
111
102
  在 `unifiedMessageFormat` 中可使用以下变量,空行自动隐藏:
112
103
 
113
104
  | 变量名 | 说明 |
114
105
  |--------|------|
115
- | `${标题}` | 视频/图集标题 |
116
- | `${作者}` | 作者名称 |
117
- | `${简介}` | 内容简介 |
118
- | `${视频时长}` | 视频时长(时:分:秒) |
119
- | `${点赞数}` | 点赞数量 |
120
- | `${收藏数}` | 收藏数量 |
121
- | `${转发数}` | 转发/分享数量 |
122
- | `${播放数}` | 播放量 |
123
- | `${评论数}` | 评论数量 |
124
- | `${发布时间}` | 发布时间(格式化) |
125
- | `${图片数量}` | 图集/实况图片数量 |
126
- | `${作者ID}` | 作者唯一标识ID |
127
- | `${视频链接}` | 视频原始链接 |
128
- | `${音乐标题}` | 音乐标题 |
129
- | `${音乐作者}` | 音乐作者 |
106
+ | `${name}` | 歌曲名称 |
107
+ | `${artist}` | 歌手名称 |
108
+ | `${album}` | 专辑名称 |
109
+ | `${cover}` | 封面图片链接 |
110
+ | `${music_url}` | 音乐直链 |
111
+ | `${level}` | 音质等级 |
112
+ | `${size}` | 文件大小 |
113
+
114
+ ## 解析策略 (Parsing Strategy)
115
+ 1. 独立接口优先:每个平台默认使用专属接口(如网易云、酷我、QQ、汽水)
116
+ 2. 聚合接口备用:当解析网易云或 QQ 音乐失败时,自动调用统一聚合接口 `https://api.bugpk.com/api/music` 进行补提
117
+ 3. 自定义优先:若配置了自定义 API 并开启“优先使用专属 API”,则会优先尝试自定义地址
130
118
 
131
119
  ## 依赖说明 (Dependencies)
132
120
  ### 音乐语音(可选)
133
- 若启用 `showMusicVoice`,请安装:
121
+ 若启用 `showMusicVoice`,推荐安装以下可选插件以获得更好的语音格式支持:
134
122
  - `koishi-plugin-silk`:silk 编解码
135
123
  - `koishi-plugin-ffmpeg`:音频重采样
124
+ 未安装时仍可尝试直接发送音频链接,但可能受平台限制。
125
+
136
126
  ### aria2 下载引擎(可选)
137
127
  若启用 `downloadEngine: 'aria2'`,请安装并启动 aria2 服务,并安装 npm 包 `aria2`:
138
128
  - 安装 aria2 服务端:https://github.com/aria2/aria2
139
129
  - 安装 npm 客户端:`npm install aria2`
140
130
  - 启动 RPC:`aria2c --enable-rpc --rpc-listen-all=true --rpc-allow-origin-all`
141
131
  未满足条件时自动降级为内置下载,不影响正常使用。
132
+
142
133
  ### downloads 服务(可选)
143
134
  若启用 `downloadEngine: 'downloads'`,请安装可选依赖 `koishi-plugin-downloads`,失败时回退到内置下载。
144
135
 
145
136
  ## 支持的平台 (Supported Platforms)
146
137
  | 平台名称 | 关键词识别 | 解析能力 |
147
138
  |----------|------------|----------|
148
- | 哔哩哔哩 (B站) | bilibili, b23.tv, bilibili.com | 视频 |
149
- | 抖音 | douyin, v.douyin.com | 短视频、图集、实况 |
150
- | 快手 | kuaishou, v.kuaishou.com | 短视频、图集 |
151
- | 小红书 | xiaohongshu, xhslink.com | 图文、视频 |
152
- | 微博 | weibo, video.weibo.com | 视频、图集 |
153
- | 剪映 / 即梦 | jianying, jimeng.jianying.com | 视频模板 |
154
- | 今日头条 / 西瓜视频 | toutiao, ixigua.com | 短视频 |
155
- | AcFun(A站) | acfun, acfun.cn | 视频 |
156
- | 知乎 | zhihu, zhihu.com | 视频、回答 |
157
- | 微视 | weishi, weishi.qq.com | 短视频 |
158
- | 虎牙 | huya, huya.com | 直播、视频 |
159
- | YouTube(油管) | youtube, youtu.be | 视频 |
160
- | TikTok(国际版抖音) | tiktok, tiktok.com | 短视频 |
161
- | 好看视频 | haokan, haokan.baidu.com | 短视频 |
162
- | 美拍 | meipai, meipai.com | 短视频 |
163
- | Twitter / X | twitter, x.com | 视频、图文 |
164
- | Instagram | instagram, instagram.com | 图文、Reels |
165
- | 豆包 | doubao (doubao.com/video) | 视频 |
166
- | 皮皮搞笑 | pipigx, h5.pipigx.com | 短视频 |
167
- | 皮皮虾 | pipixia, h5.pipix.com | 短视频 |
168
- | 最右 | zuiyou, xiaochuankeji.cn | 短视频 |
169
- | 梨视频 | video.li, pearvideo.com | 短视频 |
170
- | 全民直播 | quanmin (quanmin.tv) | 直播 |
171
- | 绿洲 (Oasis) | oasis.weibo.com | 视频、图文 |
172
- | 视频号 (WeChat Channels) | channels.weixin.qq.com, weixin.qq.com/sph/ | 短视频 |
173
- | 🔧 自定义平台 | 通过 `customPlatforms` 添加 | 取决于 API |
139
+ | 网易云音乐 | music.163.com, 163cn.tv | 歌曲信息、SVIP高音质直链、封面 |
140
+ | 酷我音乐 | kuwo.cn | 歌曲信息、直链、封面 |
141
+ | QQ音乐 | y.qq.com, i.y.qq.com | 歌曲信息、直链、封面 |
142
+ | 汽水音乐 | qishui.douyin.com | 歌曲信息、高音质直链、封面 |
143
+ | 自定义平台 | 通过 `customPlatforms` 添加 | 取决于 API |
174
144
 
175
145
  ## 项目贡献者 (Contributors)
176
-
177
- | 贡献者 (Contributor) | 贡献内容 (Contribution) |
178
- |----------------------|-------------------------|
179
- | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
180
- | ShiraiKuroko003 | 修复消息格式设置问题并且PR-1.2.5版本已修复 |
181
- | cyavb | 提交功能建议-给自定义API添加KEY认证-已修复 |
182
- | Keep785 | 提交Bug-无法正常关闭发送封面-已修复<br>提交Bug-解析问题-已修复 |
183
- | dzt2008 + Apricityx | 提交Bug-会对非支持视频平台URL进行误解析-已修复 |
146
+ | 贡献者 | 贡献内容 |
147
+ |--------|----------|
148
+ | Minecraft-1314 | 插件完整开发 |
184
149
  | JH-Ahua | BugPk-Api 支持 |
185
- | shangxue | 灵感来源 |
186
150
 
187
151
  (欢迎通过 Issues 或 PR 加入贡献者列表)
188
152
 
189
153
  ## 许可协议 (License)
190
-
191
154
  本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
192
-
193
155
  This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
194
156
 
195
157
  ## 支持我们 (Support Us)
196
-
197
158
  如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们!
198
-
199
- If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us!
159
+ If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us!