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 +91 -2
- package/lib/index.js +350 -117
- package/package.json +1 -1
- package/readme.md +53 -93
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
|
|
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(
|
|
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
|
|
161
|
-
{ pattern: /https?:\/\/www\.kuwo\.cn\/play_detail
|
|
162
|
-
{ pattern: /https?:\/\/y\.qq\.com\/n\/ryqq\/songDetail\/[A-Za-z0-9]+
|
|
163
|
-
{ pattern: /https?:\/\/i\.y\.qq\.com\/v8\/playsong\.html\?songid
|
|
164
|
-
{ pattern: /https?:\/\/qishui\.douyin\.com\/s\/[A-Za-z0-9]+
|
|
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
|
|
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
|
-
|
|
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(/&/g, '&').replace(/\?.*/, '');
|
|
238
316
|
}
|
|
239
317
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
package/readme.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# koishi-plugin-
|
|
1
|
+
# koishi-plugin-music-parser-all
|
|
2
2
|
|
|
3
3
|
## 项目介绍 (Project Introduction)
|
|
4
4
|
|
|
5
5
|
### 中文
|
|
6
|
-
这是一个为 Koishi
|
|
6
|
+
这是一个为 Koishi 机器人框架开发的**全平台音乐解析插件**,使用统一API接口,支持自动识别并解析网易云音乐、酷我音乐、QQ音乐、汽水音乐等主流音乐平台的歌曲链接,获取歌名、歌手、专辑、封面及高音质直链,并支持以语音消息发送音乐。内置独立接口优先、聚合接口备用的解析策略,同时支持自定义 API、平台扩展与字段映射。
|
|
7
7
|
|
|
8
8
|
### English
|
|
9
|
-
This is a **multi-platform
|
|
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:
|
|
13
|
-
- 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
|
-
| 指令
|
|
18
|
-
|
|
19
|
-
| `
|
|
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
|
-
| `
|
|
40
|
+
| `showMusicText` | boolean | true | 发送文字内容 |
|
|
41
41
|
| `showCoverImage` | boolean | true | 发送封面图片 |
|
|
42
|
-
| `
|
|
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 |
|
|
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 |
|
|
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 |
|
|
64
|
+
| `videoSendTimeout` | number | 180000 | 消息发送超时 (ms) |
|
|
74
65
|
| `userAgent` | string | 见预设 | User-Agent |
|
|
75
|
-
| `proxy` | object |
|
|
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 | ./
|
|
82
|
+
| `cacheDir` | string | ./temp_cache_music | 统一临时目录 |
|
|
92
83
|
|
|
93
|
-
### API
|
|
84
|
+
### API 与平台(新增)
|
|
94
85
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
95
86
|
|--------|------|--------|------|
|
|
96
|
-
| `platformDedicatedFirst` | object | 全关 |
|
|
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
|
-
|
|
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
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
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
|
-
|
|
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!
|