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 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 lru_cache_1 = require("lru-cache");
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 lru_cache_1.LRUCache({
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(/&amp;/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, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
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, config.tempDir || './temp_videos', config.videoDownloadTimeout || 120000, config.maxVideoSize || 0);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/微博/小红书/剪映/YouTube/TikTok等20+平台",
4
- "version": "1.1.3",
4
+ "version": "1.1.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
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
  |--------|------|--------|------|