koishi-plugin-video-parser-all 1.2.2 → 1.2.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.
Files changed (2) hide show
  1. package/lib/index.js +75 -47
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -11,8 +11,35 @@ const promises_1 = __importDefault(require("fs/promises"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = require("fs");
13
13
  const promises_2 = require("stream/promises");
14
- const LruCacheModule = require('lru-cache');
15
- const LRUCache = LruCacheModule.LRUCache || LruCacheModule;
14
+ const crypto_1 = require("crypto");
15
+ class SimpleLRUCache {
16
+ constructor(max, ttlMs) {
17
+ this.max = max;
18
+ this.ttlMs = ttlMs;
19
+ this.map = new Map();
20
+ }
21
+ get(key) {
22
+ const entry = this.map.get(key);
23
+ if (!entry)
24
+ return undefined;
25
+ if (Date.now() > entry.expireAt) {
26
+ this.map.delete(key);
27
+ return undefined;
28
+ }
29
+ return entry.value;
30
+ }
31
+ set(key, value) {
32
+ this.map.delete(key);
33
+ while (this.map.size >= this.max) {
34
+ const k = this.map.keys().next().value;
35
+ if (k === undefined)
36
+ break;
37
+ this.map.delete(k);
38
+ }
39
+ this.map.set(key, { value, expireAt: Date.now() + this.ttlMs });
40
+ }
41
+ clear() { this.map.clear(); }
42
+ }
16
43
  exports.name = 'video-parser-all';
17
44
  exports.Config = koishi_1.Schema.intersect([
18
45
  koishi_1.Schema.object({
@@ -121,44 +148,40 @@ function debugLog(level, ...args) {
121
148
  }).join(' ')}`;
122
149
  logger.info(message);
123
150
  }
124
- const urlCache = new LRUCache({
125
- max: 500,
126
- ttl: 10 * 60 * 1000,
127
- updateAgeOnGet: false,
128
- });
151
+ const urlCache = new SimpleLRUCache(500, 10 * 60 * 1000);
152
+ const LINK_RULES = [
153
+ { pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
154
+ { pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
155
+ { pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
156
+ { pattern: /https?:\/\/(?:www\.)?douyin\.com\/video\/\d{10,}/gi, type: 'douyin' },
157
+ { pattern: /https?:\/\/v\.douyin\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'douyin' },
158
+ { pattern: /https?:\/\/(?:www\.)?kuaishou\.com\/short-video\/[0-9a-zA-Z_-]{10,}/gi, type: 'kuaishou' },
159
+ { pattern: /https?:\/\/v\.kuaishou\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'kuaishou' },
160
+ { pattern: /https?:\/\/(?:www\.)?xiaohongshu\.com\/discovery\/item\/[0-9a-zA-Z_-]{10,}/gi, type: 'xiaohongshu' },
161
+ { pattern: /https?:\/\/xhslink\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'xiaohongshu' },
162
+ { pattern: /https?:\/\/weibo\.com\/\d+\/[0-9a-zA-Z_-]{10,}/gi, type: 'weibo' },
163
+ { pattern: /https?:\/\/video\.weibo\.com\/show\?fid=[0-9a-zA-Z_-]{10,}/gi, type: 'weibo' },
164
+ { pattern: /https?:\/\/(?:www\.)?ixigua\.com\/\d{10,}/gi, type: 'xigua' },
165
+ { pattern: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}/gi, type: 'youtube' },
166
+ { pattern: /https?:\/\/youtu\.be\/[a-zA-Z0-9_-]{11}/gi, type: 'youtube' },
167
+ { pattern: /https?:\/\/(?:www\.)?tiktok\.com\/@[\w.]+\/video\/\d{10,}/gi, type: 'tiktok' },
168
+ { pattern: /https?:\/\/vm\.tiktok\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'tiktok' },
169
+ { pattern: /https?:\/\/(?:www\.)?acfun\.cn\/v\/ac\d{10,}/gi, type: 'acfun' },
170
+ { pattern: /https?:\/\/(?:www\.)?zhihu\.com\/video\/\d{10,}/gi, type: 'zhihu' },
171
+ { pattern: /https?:\/\/weishi\.qq\.com\/weishi\/feed\/[0-9a-zA-Z_-]{10,}/gi, type: 'weishi' },
172
+ { pattern: /https?:\/\/(?:www\.)?huya\.com\/video\/[0-9a-zA-Z_-]{10,}/gi, type: 'huya' },
173
+ { pattern: /https?:\/\/haokan\.baidu\.com\/v\?vid=[0-9a-zA-Z_-]{10,}/gi, type: 'haokan' },
174
+ { pattern: /https?:\/\/(?:www\.)?meipai\.com\/media\/\d{10,}/gi, type: 'meipai' },
175
+ { pattern: /https?:\/\/twitter\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
176
+ { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
177
+ { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
178
+ { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
179
+ ];
129
180
  function linkTypeParser(content) {
130
181
  content = content.replace(/\\\//g, '/');
131
- const rules = [
132
- { pattern: /https?:\/\/(?:www\.)?bilibili\.com\/video\/([ab]v[0-9a-zA-Z_-]+)/gi, type: 'bilibili' },
133
- { pattern: /https?:\/\/b23\.tv\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
134
- { pattern: /https?:\/\/bili\d+\.cn\/[0-9a-zA-Z_-]{5,}/gi, type: 'bilibili' },
135
- { pattern: /https?:\/\/(?:www\.)?douyin\.com\/video\/\d{10,}/gi, type: 'douyin' },
136
- { pattern: /https?:\/\/v\.douyin\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'douyin' },
137
- { pattern: /https?:\/\/(?:www\.)?kuaishou\.com\/short-video\/[0-9a-zA-Z_-]{10,}/gi, type: 'kuaishou' },
138
- { pattern: /https?:\/\/v\.kuaishou\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'kuaishou' },
139
- { pattern: /https?:\/\/(?:www\.)?xiaohongshu\.com\/discovery\/item\/[0-9a-zA-Z_-]{10,}/gi, type: 'xiaohongshu' },
140
- { pattern: /https?:\/\/xhslink\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'xiaohongshu' },
141
- { pattern: /https?:\/\/weibo\.com\/\d+\/[0-9a-zA-Z_-]{10,}/gi, type: 'weibo' },
142
- { pattern: /https?:\/\/video\.weibo\.com\/show\?fid=[0-9a-zA-Z_-]{10,}/gi, type: 'weibo' },
143
- { pattern: /https?:\/\/(?:www\.)?ixigua\.com\/\d{10,}/gi, type: 'xigua' },
144
- { pattern: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}/gi, type: 'youtube' },
145
- { pattern: /https?:\/\/youtu\.be\/[a-zA-Z0-9_-]{11}/gi, type: 'youtube' },
146
- { pattern: /https?:\/\/(?:www\.)?tiktok\.com\/@[\w.]+\/video\/\d{10,}/gi, type: 'tiktok' },
147
- { pattern: /https?:\/\/vm\.tiktok\.com\/[0-9a-zA-Z_-]{8,}/gi, type: 'tiktok' },
148
- { pattern: /https?:\/\/(?:www\.)?acfun\.cn\/v\/ac\d{10,}/gi, type: 'acfun' },
149
- { pattern: /https?:\/\/(?:www\.)?zhihu\.com\/video\/\d{10,}/gi, type: 'zhihu' },
150
- { pattern: /https?:\/\/weishi\.qq\.com\/weishi\/feed\/[0-9a-zA-Z_-]{10,}/gi, type: 'weishi' },
151
- { pattern: /https?:\/\/(?:www\.)?huya\.com\/video\/[0-9a-zA-Z_-]{10,}/gi, type: 'huya' },
152
- { pattern: /https?:\/\/haokan\.baidu\.com\/v\?vid=[0-9a-zA-Z_-]{10,}/gi, type: 'haokan' },
153
- { pattern: /https?:\/\/(?:www\.)?meipai\.com\/media\/\d{10,}/gi, type: 'meipai' },
154
- { pattern: /https?:\/\/twitter\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
155
- { pattern: /https?:\/\/x\.com\/\w+\/status\/\d{10,}/gi, type: 'twitter' },
156
- { pattern: /https?:\/\/(?:www\.)?instagram\.com\/p\/[0-9a-zA-Z_-]{10,}/gi, type: 'instagram' },
157
- { pattern: /https?:\/\/(?:www\.)?doubao\.com\/video\/\d{10,}/gi, type: 'doubao' },
158
- ];
159
182
  const matches = [];
160
183
  const seen = new Set();
161
- for (const rule of rules) {
184
+ for (const rule of LINK_RULES) {
162
185
  let match;
163
186
  rule.pattern.lastIndex = 0;
164
187
  while ((match = rule.pattern.exec(content)) !== null) {
@@ -372,6 +395,7 @@ function parseApiResponse(raw, maxDescLen) {
372
395
  duration, publishTime
373
396
  };
374
397
  }
398
+ const formatVarRegex = /\$\{([^}]+)\}/g;
375
399
  function generateFormattedText(p, format) {
376
400
  const imageCount = p.images.length || p.live_photo.length;
377
401
  const vars = {
@@ -390,14 +414,18 @@ function generateFormattedText(p, format) {
390
414
  '封面': p.cover,
391
415
  '视频链接': p.video,
392
416
  };
417
+ const varReplacements = Object.entries(vars).map(([key, val]) => ({
418
+ regex: new RegExp(`\\$\\{${key}\\}`, 'g'),
419
+ value: val,
420
+ }));
393
421
  const lines = format.split('\n');
394
422
  const resultLines = [];
395
423
  for (const line of lines) {
396
- const varMatches = line.match(/\$\{([^}]+)\}/g);
424
+ const varMatches = line.match(formatVarRegex);
397
425
  if (varMatches) {
398
426
  let allEmpty = true;
399
427
  for (const match of varMatches) {
400
- const varName = match.replace(/\$\{|\}/g, '');
428
+ const varName = match.slice(2, -1);
401
429
  const val = vars[varName];
402
430
  if (val && val !== '0') {
403
431
  allEmpty = false;
@@ -408,8 +436,8 @@ function generateFormattedText(p, format) {
408
436
  continue;
409
437
  }
410
438
  let newLine = line;
411
- for (const [key, value] of Object.entries(vars)) {
412
- newLine = newLine.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
439
+ for (const { regex, value } of varReplacements) {
440
+ newLine = newLine.replace(regex, value);
413
441
  }
414
442
  resultLines.push(newLine);
415
443
  }
@@ -434,15 +462,14 @@ function buildForwardNode(session, content, botName) {
434
462
  function getErrorMessage(error) {
435
463
  if (error instanceof Error)
436
464
  return error.message;
465
+ if (error && typeof error === 'object' && 'message' in error)
466
+ return String(error.message);
437
467
  return String(error);
438
468
  }
439
469
  function apply(ctx, config) {
440
470
  debugEnabled = config.debug || false;
441
471
  debugLog('INFO', '插件初始化开始');
442
- const dedupCache = new LRUCache({
443
- max: 1000,
444
- ttl: config.deduplicationInterval * 1000,
445
- });
472
+ const dedupCache = new SimpleLRUCache(1000, config.deduplicationInterval * 1000);
446
473
  const texts = {
447
474
  waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
448
475
  unsupportedPlatformText: config.unsupportedPlatformText || '不支持该平台链接',
@@ -506,7 +533,7 @@ function apply(ctx, config) {
506
533
  throw new Error('视频链接为空');
507
534
  const tempDir = config.tempDir || './temp_videos';
508
535
  await promises_1.default.mkdir(tempDir, { recursive: true });
509
- const fileName = `video_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.mp4`;
536
+ const fileName = `video_${Date.now()}_${(0, crypto_1.randomBytes)(4).toString('hex')}.mp4`;
510
537
  const filePath = path_1.default.resolve(tempDir, fileName);
511
538
  debugLog('INFO', `开始下载视频: ${videoUrl.substring(0, 100)}...`);
512
539
  debugLog('INFO', `临时文件路径: ${filePath}`);
@@ -522,6 +549,7 @@ function apply(ctx, config) {
522
549
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
523
550
  'Referer': 'https://www.bilibili.com/',
524
551
  },
552
+ maxRedirects: 5,
525
553
  validateStatus: (status) => status >= 200 && status < 300,
526
554
  });
527
555
  }
@@ -530,7 +558,7 @@ function apply(ctx, config) {
530
558
  await promises_1.default.unlink(filePath).catch(() => { });
531
559
  throw new Error(`下载视频失败: ${getErrorMessage(e)}`);
532
560
  }
533
- const maxSizeBytes = (config.maxVideoSize || 0) * 1024 * 1024;
561
+ const maxSizeBytes = (config.maxVideoSize ?? 0) * 1024 * 1024;
534
562
  const contentLength = Number(response.headers['content-length'] || 0);
535
563
  if (maxSizeBytes > 0 && contentLength > maxSizeBytes) {
536
564
  writer.destroy();
@@ -904,7 +932,7 @@ function apply(ctx, config) {
904
932
  dedupCache.clear();
905
933
  debugLog('INFO', '插件已卸载,资源已清理');
906
934
  });
907
- process.on('exit', async () => {
935
+ process.on('beforeExit', async () => {
908
936
  try {
909
937
  const tempDir = config.tempDir || './temp_videos';
910
938
  const files = await promises_1.default.readdir(tempDir);
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.2.2",
4
+ "version": "1.2.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [