koishi-plugin-video-parser-all 0.1.2 → 0.1.3
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 +3 -0
- package/lib/index.js +171 -64
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export interface Config {
|
|
|
15
15
|
bugpkBilibiliApi: string;
|
|
16
16
|
timeout: number;
|
|
17
17
|
ignoreSendError: boolean;
|
|
18
|
+
enableForward: boolean;
|
|
19
|
+
downloadVideoBeforeSend: boolean;
|
|
20
|
+
messageBufferDelay: number;
|
|
18
21
|
}
|
|
19
22
|
export declare const Config: Schema<Config>;
|
|
20
23
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -8,37 +8,48 @@ exports.apply = apply;
|
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const promises_1 = require("stream/promises");
|
|
11
14
|
exports.name = 'video-parser-all';
|
|
12
15
|
exports.Config = koishi_1.Schema.object({
|
|
13
|
-
enable: koishi_1.Schema.boolean().default(true),
|
|
14
|
-
showWaitingTip: koishi_1.Schema.boolean().default(true),
|
|
15
|
-
waitingTipText: koishi_1.Schema.string().default('正在解析视频…'),
|
|
16
|
-
sameLinkInterval: koishi_1.Schema.number().default(180),
|
|
16
|
+
enable: koishi_1.Schema.boolean().default(true).description('是否启用插件'),
|
|
17
|
+
showWaitingTip: koishi_1.Schema.boolean().default(true).description('是否显示解析等待提示'),
|
|
18
|
+
waitingTipText: koishi_1.Schema.string().default('正在解析视频…').description('解析等待提示的文本内容'),
|
|
19
|
+
sameLinkInterval: koishi_1.Schema.number().default(180).description('相同链接重复解析间隔(秒),避免频繁解析同一链接'),
|
|
17
20
|
imageParseFormat: koishi_1.Schema.string()
|
|
18
21
|
.role('textarea')
|
|
19
22
|
.default(`\${标题} \${tab} \${UP主}
|
|
20
23
|
\${简介}
|
|
21
24
|
\${~~~}
|
|
22
|
-
\${封面}`)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
\${封面}`)
|
|
26
|
+
.description('解析结果的输出格式,支持占位符:${标题}、${UP主}、${简介}、${点赞}、${投币}、${收藏}、${转发}、${观看}、${弹幕}、${tab}、${~~~}、${封面}'),
|
|
27
|
+
showVideoUrl: koishi_1.Schema.boolean().default(false).description('是否在消息中显示无水印视频链接(false则直接发送视频文件)'),
|
|
28
|
+
maxDescLength: koishi_1.Schema.number().default(200).description('简介内容的最大长度,超出部分会被截断'),
|
|
29
|
+
bugpkUniversalApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('通用视频解析API地址(优先调用)'),
|
|
30
|
+
bugpkDouyinMainApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/douyin').description('抖音主解析API地址(备用)'),
|
|
31
|
+
bugpkDouyinBackupApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/dyjx').description('抖音备用解析API地址(备用)'),
|
|
32
|
+
bugpkKuaishouApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/ksjx').description('快手解析API地址(备用)'),
|
|
33
|
+
bugpkBilibiliApi: koishi_1.Schema.string().default('https://api.bugpk.com/api/bilibili').description('B站解析API地址(备用)'),
|
|
34
|
+
timeout: koishi_1.Schema.number().default(15000).description('API请求超时时间(毫秒)'),
|
|
35
|
+
ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略消息发送失败的错误,避免日志刷屏'),
|
|
36
|
+
enableForward: koishi_1.Schema.boolean().default(false).description('是否开启合并转发 仅支持 onebot 适配器 其他平台开启 无效'),
|
|
37
|
+
downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('是否将视频链接下载后再发送 (以解决部分onebot协议端的问题)否则使用视频直链发送'),
|
|
38
|
+
messageBufferDelay: koishi_1.Schema.number().default(1).description('消息接收缓冲延迟(秒)收到链接后等待指定时间,收集同时发送的多个链接后再逐个处理'),
|
|
32
39
|
});
|
|
33
40
|
const processed = new Map();
|
|
41
|
+
const linkBuffer = new Map();
|
|
34
42
|
const PLATFORM_KEYWORDS = {
|
|
35
43
|
bilibili: ['bilibili', 'b23', 'B站'],
|
|
36
44
|
kuaishou: ['kuaishou', '快手'],
|
|
37
45
|
douyin: ['douyin', '抖音']
|
|
38
46
|
};
|
|
39
47
|
function extractUrl(content) {
|
|
40
|
-
const
|
|
41
|
-
return
|
|
48
|
+
const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
|
|
49
|
+
return urlMatches.filter(url => {
|
|
50
|
+
const lowerUrl = url.toLowerCase();
|
|
51
|
+
return Object.values(PLATFORM_KEYWORDS).some(keywords => keywords.some(keyword => lowerUrl.includes(keyword.toLowerCase())));
|
|
52
|
+
});
|
|
42
53
|
}
|
|
43
54
|
function hasPlatformKeyword(content) {
|
|
44
55
|
const lowerContent = content.toLowerCase();
|
|
@@ -57,6 +68,21 @@ function getPlatformType(url) {
|
|
|
57
68
|
return 'bilibili';
|
|
58
69
|
return null;
|
|
59
70
|
}
|
|
71
|
+
async function downloadVideo(url, filename) {
|
|
72
|
+
const downloadPath = path_1.default.join(process.cwd(), 'temp_videos');
|
|
73
|
+
if (!fs_1.default.existsSync(downloadPath)) {
|
|
74
|
+
fs_1.default.mkdirSync(downloadPath, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
const filePath = path_1.default.join(downloadPath, `${filename}.mp4`);
|
|
77
|
+
const response = await (0, axios_1.default)({
|
|
78
|
+
url,
|
|
79
|
+
method: 'GET',
|
|
80
|
+
responseType: 'stream',
|
|
81
|
+
timeout: 30000
|
|
82
|
+
});
|
|
83
|
+
await (0, promises_1.pipeline)(response.data, fs_1.default.createWriteStream(filePath));
|
|
84
|
+
return filePath;
|
|
85
|
+
}
|
|
60
86
|
function parseUniversalApiData(data) {
|
|
61
87
|
return {
|
|
62
88
|
title: data.title || '无标题',
|
|
@@ -112,6 +138,10 @@ function parseBilibili(data) {
|
|
|
112
138
|
videoUrl = data.play;
|
|
113
139
|
else if (data.durl && Array.isArray(data.durl))
|
|
114
140
|
videoUrl = data.durl[0]?.url || '';
|
|
141
|
+
else if (data.hd_url)
|
|
142
|
+
videoUrl = data.hd_url;
|
|
143
|
+
else if (data.sd_url)
|
|
144
|
+
videoUrl = data.sd_url;
|
|
115
145
|
const digg = data.like || data.digg || 0;
|
|
116
146
|
const coin = data.coin || 0;
|
|
117
147
|
const collect = data.favorite || data.collect || 0;
|
|
@@ -200,50 +230,112 @@ function apply(ctx, config) {
|
|
|
200
230
|
cover: '', video: ''
|
|
201
231
|
};
|
|
202
232
|
}
|
|
203
|
-
async function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
async function processSingleUrl(session, url) {
|
|
234
|
+
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
|
|
237
|
+
return;
|
|
238
|
+
processed.set(hash, now);
|
|
239
|
+
const data = await parseVideo(url);
|
|
240
|
+
let text = config.imageParseFormat
|
|
241
|
+
.replace(/\${标题}/g, data.title)
|
|
242
|
+
.replace(/\${UP主}/g, data.author)
|
|
243
|
+
.replace(/\${简介}/g, data.desc.slice(0, config.maxDescLength))
|
|
244
|
+
.replace(/\${点赞}/g, data.digg.toString())
|
|
245
|
+
.replace(/\${投币}/g, data.coin.toString())
|
|
246
|
+
.replace(/\${收藏}/g, data.collect.toString())
|
|
247
|
+
.replace(/\${转发}/g, data.share.toString())
|
|
248
|
+
.replace(/\${观看}/g, data.play.toString())
|
|
249
|
+
.replace(/\${弹幕}/g, data.danmaku.toString())
|
|
250
|
+
.replace(/\${tab}/g, '\t')
|
|
251
|
+
.replace(/\${~~~}/g, '——————————————');
|
|
252
|
+
const [beforeCover, afterCover] = text.split('\${封面}');
|
|
253
|
+
const msgParts = [];
|
|
254
|
+
if (beforeCover && beforeCover.trim())
|
|
255
|
+
msgParts.push(beforeCover.trim());
|
|
256
|
+
if (data.cover)
|
|
257
|
+
msgParts.push(koishi_1.h.image(data.cover));
|
|
258
|
+
if (afterCover && afterCover.trim())
|
|
259
|
+
msgParts.push(afterCover.trim());
|
|
260
|
+
if (data.video && config.showVideoUrl)
|
|
261
|
+
msgParts.push(`🔗 无水印链接:${data.video}`);
|
|
262
|
+
let videoMsg = '';
|
|
263
|
+
if (data.video && !config.showVideoUrl) {
|
|
264
|
+
if (config.downloadVideoBeforeSend && data.video) {
|
|
234
265
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
266
|
+
const filename = crypto_1.default.createHash('md5').update(data.video).digest('hex');
|
|
267
|
+
const filePath = await downloadVideo(data.video, filename);
|
|
268
|
+
videoMsg = koishi_1.h.video(`file://${filePath}`);
|
|
238
269
|
}
|
|
239
270
|
catch (e) {
|
|
240
|
-
|
|
271
|
+
ctx.logger.error(`视频下载失败: ${e.message}`);
|
|
272
|
+
videoMsg = `📥 无水印视频:${data.video}`;
|
|
241
273
|
}
|
|
242
274
|
}
|
|
275
|
+
else {
|
|
276
|
+
videoMsg = koishi_1.h.video(data.video);
|
|
277
|
+
}
|
|
243
278
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
279
|
+
return {
|
|
280
|
+
content: msgParts.join('\n'),
|
|
281
|
+
video: videoMsg,
|
|
282
|
+
data
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function processBufferedUrls(session) {
|
|
286
|
+
const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
287
|
+
const bufferData = linkBuffer.get(sessionKey);
|
|
288
|
+
if (!bufferData)
|
|
289
|
+
return;
|
|
290
|
+
clearTimeout(bufferData.timer);
|
|
291
|
+
linkBuffer.delete(sessionKey);
|
|
292
|
+
const results = [];
|
|
293
|
+
for (const url of bufferData.urls) {
|
|
294
|
+
const result = await processSingleUrl(session, url);
|
|
295
|
+
if (result)
|
|
296
|
+
results.push(result);
|
|
297
|
+
}
|
|
298
|
+
if (results.length === 0)
|
|
299
|
+
return;
|
|
300
|
+
// 修复:通过平台判断是否为onebot,而非直接访问session.adapter
|
|
301
|
+
if (config.enableForward && session.platform === 'onebot') {
|
|
302
|
+
const forwardMessages = results.map(result => {
|
|
303
|
+
const messages = [
|
|
304
|
+
(0, koishi_1.h)('message', { user_id: session.selfId }, [result.content]),
|
|
305
|
+
];
|
|
306
|
+
if (result.video)
|
|
307
|
+
messages.push((0, koishi_1.h)('message', { user_id: session.selfId }, [result.video]));
|
|
308
|
+
return messages;
|
|
309
|
+
}).flat();
|
|
310
|
+
await session.send((0, koishi_1.h)('forward', {
|
|
311
|
+
messages: forwardMessages,
|
|
312
|
+
title: '视频解析结果',
|
|
313
|
+
brief: `共解析${results.length}个视频链接`,
|
|
314
|
+
source: '视频解析插件',
|
|
315
|
+
preview: [results[0].content.substring(0, 10) + '...'],
|
|
316
|
+
summary: `查看${results.length}条解析结果`
|
|
317
|
+
})).catch(e => {
|
|
318
|
+
if (!config.ignoreSendError)
|
|
319
|
+
ctx.logger.warn(`合并转发发送失败: ${e.message}`);
|
|
320
|
+
sendIndividualMessages(session, results);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
sendIndividualMessages(session, results);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function sendIndividualMessages(session, results) {
|
|
328
|
+
for (const result of results) {
|
|
329
|
+
try {
|
|
330
|
+
if (result.content)
|
|
331
|
+
await session.send(result.content);
|
|
332
|
+
if (result.video)
|
|
333
|
+
await session.send(result.video);
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
if (!config.ignoreSendError)
|
|
337
|
+
ctx.logger.warn(`单条消息发送失败: ${e.message}`);
|
|
338
|
+
}
|
|
247
339
|
}
|
|
248
340
|
}
|
|
249
341
|
ctx.on('message', async (session) => {
|
|
@@ -253,22 +345,27 @@ function apply(ctx, config) {
|
|
|
253
345
|
const content = session.content.trim();
|
|
254
346
|
if (!hasPlatformKeyword(content))
|
|
255
347
|
return;
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
return;
|
|
259
|
-
const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
|
|
260
|
-
const now = Date.now();
|
|
261
|
-
if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000)
|
|
348
|
+
const urls = extractUrl(content);
|
|
349
|
+
if (urls.length === 0)
|
|
262
350
|
return;
|
|
263
|
-
|
|
264
|
-
if (config.showWaitingTip) {
|
|
351
|
+
if (config.showWaitingTip && !linkBuffer.has(`${session.platform}:${session.userId}:${session.channelId}`)) {
|
|
265
352
|
await session.send(config.waitingTipText).catch(e => {
|
|
266
353
|
if (!config.ignoreSendError)
|
|
267
354
|
ctx.logger.warn(`发送等待提示失败: ${e.message}`);
|
|
268
355
|
});
|
|
269
356
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
357
|
+
const sessionKey = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
358
|
+
if (linkBuffer.has(sessionKey)) {
|
|
359
|
+
const bufferData = linkBuffer.get(sessionKey);
|
|
360
|
+
bufferData.urls.push(...urls);
|
|
361
|
+
clearTimeout(bufferData.timer);
|
|
362
|
+
bufferData.timer = setTimeout(() => processBufferedUrls(session), config.messageBufferDelay * 1000);
|
|
363
|
+
linkBuffer.set(sessionKey, bufferData);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const timer = setTimeout(() => processBufferedUrls(session), config.messageBufferDelay * 1000);
|
|
367
|
+
linkBuffer.set(sessionKey, { urls, timer });
|
|
368
|
+
}
|
|
272
369
|
}
|
|
273
370
|
catch (e) {
|
|
274
371
|
ctx.logger.error(`消息处理异常: ${e.message}`);
|
|
@@ -277,6 +374,16 @@ function apply(ctx, config) {
|
|
|
277
374
|
setInterval(() => {
|
|
278
375
|
const now = Date.now();
|
|
279
376
|
processed.forEach((t, k) => now - t > 86400000 && processed.delete(k));
|
|
377
|
+
const tempPath = path_1.default.join(process.cwd(), 'temp_videos');
|
|
378
|
+
if (fs_1.default.existsSync(tempPath)) {
|
|
379
|
+
fs_1.default.readdirSync(tempPath).forEach(file => {
|
|
380
|
+
const filePath = path_1.default.join(tempPath, file);
|
|
381
|
+
const stat = fs_1.default.statSync(filePath);
|
|
382
|
+
if (now - stat.ctimeMs > 3600000) {
|
|
383
|
+
fs_1.default.unlinkSync(filePath);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
280
387
|
}, 3600000);
|
|
281
388
|
ctx.logger.info('视频解析插件加载完成');
|
|
282
389
|
}
|