koishi-plugin-video-parser-all 1.1.3 → 1.1.5
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 +4 -0
- package/lib/index.js +85 -78
- package/package.json +1 -1
- package/readme.md +5 -0
package/lib/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare const Config: Schema<{
|
|
|
25
25
|
retryInterval?: number | null | undefined;
|
|
26
26
|
} & {
|
|
27
27
|
enableForward?: boolean | null | undefined;
|
|
28
|
+
} & {
|
|
29
|
+
deduplicationInterval?: number | null | undefined;
|
|
28
30
|
} & {
|
|
29
31
|
primaryApiUrl?: string | null | undefined;
|
|
30
32
|
backupApiUrl?: string | null | undefined;
|
|
@@ -82,6 +84,8 @@ export declare const Config: Schema<{
|
|
|
82
84
|
retryInterval: number;
|
|
83
85
|
} & {
|
|
84
86
|
enableForward: boolean;
|
|
87
|
+
} & {
|
|
88
|
+
deduplicationInterval: number;
|
|
85
89
|
} & {
|
|
86
90
|
primaryApiUrl: string;
|
|
87
91
|
backupApiUrl: string;
|
package/lib/index.js
CHANGED
|
@@ -6,12 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Config = exports.name = void 0;
|
|
7
7
|
exports.apply = apply;
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
|
-
const axios_1 = __importDefault(require("axios"));
|
|
10
9
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
10
|
const path_1 = __importDefault(require("path"));
|
|
12
11
|
const fs_1 = require("fs");
|
|
13
12
|
const promises_2 = require("stream/promises");
|
|
14
|
-
const
|
|
13
|
+
const LRUCache = require("lru-cache");
|
|
15
14
|
exports.name = 'video-parser-all';
|
|
16
15
|
exports.Config = koishi_1.Schema.intersect([
|
|
17
16
|
koishi_1.Schema.object({
|
|
@@ -45,6 +44,9 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
45
44
|
koishi_1.Schema.object({
|
|
46
45
|
enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
|
|
47
46
|
}).description('发送方式设置'),
|
|
47
|
+
koishi_1.Schema.object({
|
|
48
|
+
deduplicationInterval: koishi_1.Schema.number().min(0).step(1).default(180).description('禁止重复解析时间间隔(秒),0 为不限制'),
|
|
49
|
+
}).description('去重设置'),
|
|
48
50
|
koishi_1.Schema.object({
|
|
49
51
|
primaryApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description('主 API 地址'),
|
|
50
52
|
backupApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/svparse').description('备用主 API 地址(仅支持抖音/小红书/ins/即梦)'),
|
|
@@ -117,7 +119,7 @@ function debugLog(level, ...args) {
|
|
|
117
119
|
}).join(' ')}`;
|
|
118
120
|
logger.info(message);
|
|
119
121
|
}
|
|
120
|
-
const urlCache = new
|
|
122
|
+
const urlCache = new LRUCache({
|
|
121
123
|
max: 500,
|
|
122
124
|
ttl: 10 * 60 * 1000,
|
|
123
125
|
updateAgeOnGet: false,
|
|
@@ -236,25 +238,6 @@ function cleanUrl(url) {
|
|
|
236
238
|
return url.replace(/&/g, '&').replace(/\?.*/, '');
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
|
-
async function resolveShortUrl(url) {
|
|
240
|
-
try {
|
|
241
|
-
const res = await axios_1.default.get(url, {
|
|
242
|
-
timeout: 10000,
|
|
243
|
-
maxRedirects: 10,
|
|
244
|
-
headers: {
|
|
245
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
246
|
-
'Referer': 'https://www.baidu.com/',
|
|
247
|
-
},
|
|
248
|
-
validateStatus: (status) => status >= 200 && status < 400,
|
|
249
|
-
});
|
|
250
|
-
const finalUrl = res.request?.res?.responseUrl || url;
|
|
251
|
-
return cleanUrl(finalUrl);
|
|
252
|
-
}
|
|
253
|
-
catch (e) {
|
|
254
|
-
debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
|
|
255
|
-
return cleanUrl(url);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
241
|
function formatDuration(seconds) {
|
|
259
242
|
if (!seconds || seconds <= 0)
|
|
260
243
|
return '';
|
|
@@ -446,51 +429,6 @@ function buildForwardNode(session, content, botName) {
|
|
|
446
429
|
}
|
|
447
430
|
}, messageContent);
|
|
448
431
|
}
|
|
449
|
-
async function downloadVideoFile(videoUrl, tempDir, timeout, maxSizeMB) {
|
|
450
|
-
if (!videoUrl)
|
|
451
|
-
throw new Error('视频链接为空');
|
|
452
|
-
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
453
|
-
const fileName = `video_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.mp4`;
|
|
454
|
-
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
455
|
-
debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
|
|
456
|
-
debugLog('INFO', `临时文件路径: ${filePath}`);
|
|
457
|
-
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
458
|
-
let response;
|
|
459
|
-
try {
|
|
460
|
-
response = await (0, axios_1.default)({
|
|
461
|
-
method: 'GET',
|
|
462
|
-
url: videoUrl,
|
|
463
|
-
responseType: 'stream',
|
|
464
|
-
timeout: timeout,
|
|
465
|
-
headers: {
|
|
466
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
467
|
-
'Referer': 'https://www.bilibili.com/',
|
|
468
|
-
},
|
|
469
|
-
validateStatus: (status) => status >= 200 && status < 300,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
catch (e) {
|
|
473
|
-
writer.destroy();
|
|
474
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
475
|
-
throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
|
|
476
|
-
}
|
|
477
|
-
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
478
|
-
const contentLength = Number(response.headers['content-length'] || 0);
|
|
479
|
-
if (maxSizeMB > 0 && contentLength > maxSizeBytes) {
|
|
480
|
-
writer.destroy();
|
|
481
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
482
|
-
throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${maxSizeMB}MB)`);
|
|
483
|
-
}
|
|
484
|
-
try {
|
|
485
|
-
await (0, promises_2.pipeline)(response.data, writer);
|
|
486
|
-
debugLog('INFO', `视频下载完成`);
|
|
487
|
-
return filePath;
|
|
488
|
-
}
|
|
489
|
-
catch (e) {
|
|
490
|
-
await promises_1.default.unlink(filePath).catch(() => { });
|
|
491
|
-
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
432
|
function getErrorMessage(error) {
|
|
495
433
|
if (error instanceof Error)
|
|
496
434
|
return error.message;
|
|
@@ -499,6 +437,10 @@ function getErrorMessage(error) {
|
|
|
499
437
|
function apply(ctx, config) {
|
|
500
438
|
debugEnabled = config.debug || false;
|
|
501
439
|
debugLog('INFO', '插件初始化开始');
|
|
440
|
+
const dedupCache = new LRUCache({
|
|
441
|
+
max: 1000,
|
|
442
|
+
ttl: config.deduplicationInterval * 1000,
|
|
443
|
+
});
|
|
502
444
|
const texts = {
|
|
503
445
|
waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
|
|
504
446
|
unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
|
|
@@ -506,14 +448,6 @@ function apply(ctx, config) {
|
|
|
506
448
|
parseErrorPrefix: config.parseErrorPrefix || '❌ 解析失败:',
|
|
507
449
|
parseErrorItemFormat: config.parseErrorItemFormat || '【${url}】: ${msg}',
|
|
508
450
|
};
|
|
509
|
-
const http = axios_1.default.create({
|
|
510
|
-
timeout: config.timeout,
|
|
511
|
-
headers: {
|
|
512
|
-
'User-Agent': config.userAgent,
|
|
513
|
-
'Referer': 'https://www.baidu.com/',
|
|
514
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
451
|
const defaultDedicatedApis = {
|
|
518
452
|
bilibili: 'https://api.bugpk.com/api/bilibili',
|
|
519
453
|
douyin: 'https://api.bugpk.com/api/douyin',
|
|
@@ -538,6 +472,68 @@ function apply(ctx, config) {
|
|
|
538
472
|
const dedicatedFirst = config.platformDedicatedFirst?.[type] ?? false;
|
|
539
473
|
return { apiUrl, dedicatedFirst };
|
|
540
474
|
}
|
|
475
|
+
async function resolveShortUrl(url) {
|
|
476
|
+
try {
|
|
477
|
+
const res = await ctx.http.get(url, {
|
|
478
|
+
timeout: 10000,
|
|
479
|
+
headers: {
|
|
480
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
481
|
+
'Referer': 'https://www.baidu.com/',
|
|
482
|
+
},
|
|
483
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
484
|
+
});
|
|
485
|
+
const finalUrl = res.request?.res?.responseUrl || url;
|
|
486
|
+
return cleanUrl(finalUrl);
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
debugLog('WARN', '解析短链接失败:', e, '原始URL:', url);
|
|
490
|
+
return cleanUrl(url);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function downloadVideoFile(videoUrl) {
|
|
494
|
+
if (!videoUrl)
|
|
495
|
+
throw new Error('视频链接为空');
|
|
496
|
+
const tempDir = config.tempDir || './temp_videos';
|
|
497
|
+
await promises_1.default.mkdir(tempDir, { recursive: true });
|
|
498
|
+
const fileName = `video_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.mp4`;
|
|
499
|
+
const filePath = path_1.default.resolve(tempDir, fileName);
|
|
500
|
+
debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
|
|
501
|
+
debugLog('INFO', `临时文件路径: ${filePath}`);
|
|
502
|
+
const writer = (0, fs_1.createWriteStream)(filePath);
|
|
503
|
+
let response;
|
|
504
|
+
try {
|
|
505
|
+
response = await ctx.http.get(videoUrl, {
|
|
506
|
+
responseType: 'stream',
|
|
507
|
+
timeout: config.videoDownloadTimeout || 120000,
|
|
508
|
+
headers: {
|
|
509
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
510
|
+
'Referer': 'https://www.bilibili.com/',
|
|
511
|
+
},
|
|
512
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
516
|
+
writer.destroy();
|
|
517
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
518
|
+
throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
|
|
519
|
+
}
|
|
520
|
+
const maxSizeBytes = (config.maxVideoSize || 0) * 1024 * 1024;
|
|
521
|
+
const contentLength = Number(response.headers?.['content-length'] || 0);
|
|
522
|
+
if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
|
|
523
|
+
writer.destroy();
|
|
524
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
525
|
+
throw new Error(`视频文件过大(${Math.round(contentLength / 1024 / 1024)}MB),超过限制(${config.maxVideoSize}MB)`);
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
await (0, promises_2.pipeline)(response.data, writer);
|
|
529
|
+
debugLog('INFO', `视频下载完成`);
|
|
530
|
+
return filePath;
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
await promises_1.default.unlink(filePath).catch(() => { });
|
|
534
|
+
throw new Error(`写入视频文件失败: ${getErrorMessage(e)}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
541
537
|
async function fetchApi(url, type) {
|
|
542
538
|
const cacheKey = url;
|
|
543
539
|
const cached = urlCache.get(cacheKey);
|
|
@@ -567,7 +563,7 @@ function apply(ctx, config) {
|
|
|
567
563
|
for (const api of apiList) {
|
|
568
564
|
for (let attempt = 0; attempt <= config.retryTimes; attempt++) {
|
|
569
565
|
try {
|
|
570
|
-
const res = await http.get(api.url, {
|
|
566
|
+
const res = await ctx.http.get(api.url, {
|
|
571
567
|
params: { url },
|
|
572
568
|
timeout: config.timeout
|
|
573
569
|
});
|
|
@@ -670,7 +666,7 @@ function apply(ctx, config) {
|
|
|
670
666
|
};
|
|
671
667
|
if (config.forceDownloadVideo) {
|
|
672
668
|
try {
|
|
673
|
-
const tempFilePath = await downloadVideoFile(videoUrl
|
|
669
|
+
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
674
670
|
const localFile = `file://${tempFilePath}`;
|
|
675
671
|
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
676
672
|
return;
|
|
@@ -696,7 +692,7 @@ function apply(ctx, config) {
|
|
|
696
692
|
catch (urlErr) {
|
|
697
693
|
debugLog('ERROR', '直接发送URL失败,尝试下载:', getErrorMessage(urlErr));
|
|
698
694
|
try {
|
|
699
|
-
const tempFilePath = await downloadVideoFile(videoUrl
|
|
695
|
+
const tempFilePath = await downloadVideoFile(videoUrl);
|
|
700
696
|
const localFile = `file://${tempFilePath}`;
|
|
701
697
|
await sendWithTimeout(session, koishi_1.h.video(localFile));
|
|
702
698
|
return;
|
|
@@ -713,10 +709,20 @@ function apply(ctx, config) {
|
|
|
713
709
|
const errors = [];
|
|
714
710
|
for (let i = 0; i < matches.length; i++) {
|
|
715
711
|
const match = matches[i];
|
|
712
|
+
if (config.deduplicationInterval > 0) {
|
|
713
|
+
const lastTime = dedupCache.get(match.url);
|
|
714
|
+
if (lastTime && (Date.now() - lastTime < config.deduplicationInterval * 1000)) {
|
|
715
|
+
debugLog('INFO', `跳过重复链接: ${match.url}`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
716
719
|
debugLog('INFO', `正在解析第 ${i + 1}/${matches.length} 个链接: ${match.url} (平台: ${match.type})`);
|
|
717
720
|
const result = await processSingleUrl(match.url, match.type);
|
|
718
721
|
if (result.success) {
|
|
719
722
|
items.push(result.data);
|
|
723
|
+
if (config.deduplicationInterval > 0) {
|
|
724
|
+
dedupCache.set(match.url, Date.now());
|
|
725
|
+
}
|
|
720
726
|
}
|
|
721
727
|
else {
|
|
722
728
|
const item = texts.parseErrorItemFormat
|
|
@@ -879,6 +885,7 @@ function apply(ctx, config) {
|
|
|
879
885
|
ctx.on('dispose', () => {
|
|
880
886
|
clearInterval(tempCleanupInterval);
|
|
881
887
|
urlCache.clear();
|
|
888
|
+
dedupCache.clear();
|
|
882
889
|
debugLog('INFO', '插件已卸载,资源已清理');
|
|
883
890
|
});
|
|
884
891
|
process.on('exit', async () => {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -71,6 +71,11 @@ This is a **multi-platform video/image parsing plugin** developed for the Koishi
|
|
|
71
71
|
|--------|------|--------|------|
|
|
72
72
|
| `enableForward` | boolean | false | 是否启用合并转发(仅 OneBot 平台),启用后视频与图文将整合进同一条合并消息 |
|
|
73
73
|
|
|
74
|
+
### 去重设置
|
|
75
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
76
|
+
|--------|------|--------|------|
|
|
77
|
+
| `deduplicationInterval` | number | 180 | 禁止重复解析时间间隔(秒),0 为不限制。同一个链接在间隔内不会重复解析。 |
|
|
78
|
+
|
|
74
79
|
### 界面文字设置
|
|
75
80
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
76
81
|
|--------|------|--------|------|
|