koishi-plugin-video-parser-all 0.8.4 → 0.8.6

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
@@ -3,8 +3,9 @@ export declare const name = "video-parser-all";
3
3
  export declare const Config: Schema<{
4
4
  enable?: boolean | null | undefined;
5
5
  botName?: string | null | undefined;
6
+ showWaitingTip?: boolean | null | undefined;
7
+ sameLinkInterval?: number | null | undefined;
6
8
  debug?: boolean | null | undefined;
7
- debugFile?: boolean | null | undefined;
8
9
  } & import("cosmokit").Dict & {
9
10
  unifiedMessageFormat?: string | null | undefined;
10
11
  } & {
@@ -22,9 +23,6 @@ export declare const Config: Schema<{
22
23
  retryInterval?: number | null | undefined;
23
24
  } & {
24
25
  enableForward?: boolean | null | undefined;
25
- downloadVideoBeforeSend?: boolean | null | undefined;
26
- maxVideoSize?: number | null | undefined;
27
- downloadThreads?: number | null | undefined;
28
26
  } & {
29
27
  messageBufferDelay?: number | null | undefined;
30
28
  } & {
@@ -40,8 +38,9 @@ export declare const Config: Schema<{
40
38
  }, {
41
39
  enable: boolean;
42
40
  botName: string;
41
+ showWaitingTip: boolean;
42
+ sameLinkInterval: number;
43
43
  debug: boolean;
44
- debugFile: boolean;
45
44
  } & import("cosmokit").Dict & {
46
45
  unifiedMessageFormat: string;
47
46
  } & {
@@ -59,9 +58,6 @@ export declare const Config: Schema<{
59
58
  retryInterval: number;
60
59
  } & {
61
60
  enableForward: boolean;
62
- downloadVideoBeforeSend: boolean;
63
- maxVideoSize: number;
64
- downloadThreads: number;
65
61
  } & {
66
62
  messageBufferDelay: number;
67
63
  } & {
package/lib/index.js CHANGED
@@ -10,19 +10,17 @@ const axios_1 = __importDefault(require("axios"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
- const url_1 = require("url");
14
- const promises_1 = require("stream/promises");
15
- const worker_threads_1 = require("worker_threads");
16
13
  exports.name = 'video-parser-all';
17
14
  exports.Config = koishi_1.Schema.intersect([
18
15
  koishi_1.Schema.object({
19
16
  enable: koishi_1.Schema.boolean().default(true).description('是否启用视频解析插件'),
20
17
  botName: koishi_1.Schema.string().default('视频解析机器人').description('合并转发消息中显示的机器人名称'),
21
- debug: koishi_1.Schema.boolean().default(false).description('开启调试模式(详细日志输出至控制台)'),
22
- debugFile: koishi_1.Schema.boolean().default(false).description('调试日志同时写入本地 debug.log 文件'),
18
+ showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
19
+ sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
20
+ debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,在控制台输出详细日志'),
23
21
  }).description('基础设置'),
24
22
  koishi_1.Schema.object({
25
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`标题:\${标题}\n作者:\${作者}\n点赞:\${点赞数}\n视频链接:\${视频链接}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${视频时长} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${发布时间} ${图片数量} ${作者ID} ${视频链接} ${封面} ${音乐作者} ${音乐标题}'),
23
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default(`\${标题}\n\${作者}\n\${简介}\n点赞:\${点赞数}\n收藏:\${收藏数}\n转发:\${转发数}\n播放:\${播放数}\n评论:\${评论数}`).description('统一消息格式,可用变量:${标题} ${作者} ${简介} ${点赞数} ${收藏数} ${转发数} ${播放数} ${评论数} ${视频时长} ${发布时间} ${图片数量} ${作者ID} ${视频链接} ${封面} ${音乐作者} ${音乐标题}'),
26
24
  }).description('消息格式设置'),
27
25
  koishi_1.Schema.object({
28
26
  showImageText: koishi_1.Schema.boolean().default(true).description('是否发送解析后的文字内容'),
@@ -42,9 +40,6 @@ exports.Config = koishi_1.Schema.intersect([
42
40
  }).description('错误与重试设置'),
43
41
  koishi_1.Schema.object({
44
42
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅 OneBot 平台)'),
45
- downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频到本地'),
46
- maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('视频下载大小限制(MB,0 为不限制)'),
47
- downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0 为单线程)'),
48
43
  }).description('发送方式设置'),
49
44
  koishi_1.Schema.object({
50
45
  messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒)'),
@@ -66,26 +61,12 @@ const processed = new Map();
66
61
  const linkBuffer = new Map();
67
62
  const logger = new koishi_1.Logger(exports.name);
68
63
  let debugEnabled = false;
69
- let debugFileEnabled = false;
70
- let debugStream = null;
71
64
  function debugLog(level, ...args) {
72
65
  if (!debugEnabled)
73
66
  return;
74
67
  const timestamp = new Date().toISOString();
75
68
  const message = `[${timestamp}] [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}`;
76
69
  logger.info(message);
77
- if (debugFileEnabled && debugStream) {
78
- debugStream.write(message + '\n');
79
- }
80
- }
81
- function initDebug(enabled, fileEnabled) {
82
- debugEnabled = enabled;
83
- debugFileEnabled = fileEnabled;
84
- if (fileEnabled && enabled) {
85
- const logPath = path_1.default.join(process.cwd(), 'debug.log');
86
- debugStream = fs_1.default.createWriteStream(logPath, { flags: 'a' });
87
- debugStream.write(`\n=== Debug session started at ${new Date().toISOString()} ===\n`);
88
- }
89
70
  }
90
71
  const PLATFORM_KEYWORDS = {
91
72
  bilibili: ['bilibili', 'b23', 'www.bilibili.com', 'm.bilibili.com', 'b23.tv', 't.bilibili.com', 'bilibili.com/video', 'bilibili.com/opus', 'bilibili.com/bangumi'],
@@ -119,103 +100,6 @@ function getErrorMessage(error) {
119
100
  return error.message;
120
101
  return String(error);
121
102
  }
122
- async function getFileSize(url, userAgent) {
123
- try {
124
- const response = await axios_1.default.head(url, {
125
- timeout: 10000,
126
- headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
127
- });
128
- const contentLength = response.headers['content-length'];
129
- if (contentLength)
130
- return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
131
- }
132
- catch (error) { }
133
- return 0;
134
- }
135
- async function downloadVideoThread(workerData) {
136
- return new Promise((resolve, reject) => {
137
- const worker = new worker_threads_1.Worker(__filename, { workerData });
138
- worker.on('message', resolve);
139
- worker.on('error', reject);
140
- worker.on('exit', (code) => {
141
- if (code !== 0)
142
- reject(new Error(`下载线程异常退出,代码:${code}`));
143
- });
144
- });
145
- }
146
- if (!worker_threads_1.isMainThread) {
147
- const { url, start, end, filename, userAgent } = worker_threads_1.workerData;
148
- const filePath = path_1.default.join(process.cwd(), 'temp_videos', `${filename}_${start}_${end}.part`);
149
- (0, axios_1.default)({
150
- url,
151
- method: 'GET',
152
- responseType: 'stream',
153
- timeout: 60000,
154
- headers: {
155
- 'User-Agent': userAgent,
156
- 'Range': `bytes=${start}-${end}`
157
- }
158
- }).then(response => {
159
- const writeStream = fs_1.default.createWriteStream(filePath);
160
- response.data.pipe(writeStream);
161
- writeStream.on('finish', () => worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end }));
162
- writeStream.on('error', (error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
163
- }).catch((error) => worker_threads_1.parentPort?.postMessage({ success: false, error: error.message }));
164
- }
165
- async function downloadVideo(url, filename, userAgent, maxSize, threads) {
166
- const dir = path_1.default.join(process.cwd(), 'temp_videos');
167
- if (!fs_1.default.existsSync(dir))
168
- fs_1.default.mkdirSync(dir, { recursive: true });
169
- const filePath = path_1.default.join(dir, `${filename}.mp4`);
170
- try {
171
- if (url.endsWith('.m4a') || url.endsWith('.mp3'))
172
- return { filePath: '', success: false };
173
- const fileSize = await getFileSize(url, userAgent);
174
- if (maxSize > 0 && fileSize > maxSize)
175
- return { filePath: '', success: false };
176
- if (threads <= 0 || fileSize === 0) {
177
- const response = await (0, axios_1.default)({
178
- url,
179
- method: 'GET',
180
- responseType: 'stream',
181
- timeout: 60000,
182
- headers: { 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
183
- });
184
- const writeStream = fs_1.default.createWriteStream(filePath);
185
- await (0, promises_1.pipeline)(response.data, writeStream);
186
- return { filePath, success: true };
187
- }
188
- const totalSize = fileSize * 1024 * 1024;
189
- const chunkSize = Math.ceil(totalSize / threads);
190
- const promises = [];
191
- for (let i = 0; i < threads; i++) {
192
- const start = i * chunkSize;
193
- const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
194
- promises.push(downloadVideoThread({ url, start, end, filename, userAgent }));
195
- }
196
- const results = await Promise.all(promises);
197
- const writeStream = fs_1.default.createWriteStream(filePath);
198
- for (const result of results) {
199
- if (!result.success)
200
- throw new Error(result.error);
201
- const readStream = fs_1.default.createReadStream(result.filePath);
202
- await (0, promises_1.pipeline)(readStream, writeStream, { end: false });
203
- fs_1.default.unlinkSync(result.filePath);
204
- }
205
- writeStream.end();
206
- return { filePath, success: true };
207
- }
208
- catch (error) {
209
- if (fs_1.default.existsSync(filePath))
210
- fs_1.default.unlinkSync(filePath);
211
- const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
212
- partFiles.forEach(file => { try {
213
- fs_1.default.unlinkSync(path_1.default.join(dir, file));
214
- }
215
- catch (e) { } });
216
- return { filePath: '', success: false };
217
- }
218
- }
219
103
  function extractUrl(content) {
220
104
  const urlMatches = content.match(/https?:\/\/[^\s\"\'\>]+/gi) || [];
221
105
  return urlMatches.filter(url => {
@@ -425,10 +309,9 @@ function buildForwardNode(session, content, botName) {
425
309
  return (0, koishi_1.h)('node', { user: { nickname: botName.substring(0, 15), user_id: session.selfId } }, messageContent);
426
310
  }
427
311
  function apply(ctx, config) {
428
- initDebug(config.debug, config.debugFile);
312
+ debugEnabled = config.debug || false;
429
313
  debugLog('INFO', '插件初始化开始');
430
314
  debugLog('INFO', '当前配置:', config);
431
- // 合并默认文案
432
315
  const texts = {
433
316
  waitingTipText: config.waitingTipText || '正在解析视频,请稍候...',
434
317
  duplicateLinkText: config.duplicateLinkText || '请勿重复解析相同链接',
@@ -472,37 +355,30 @@ function apply(ctx, config) {
472
355
  }
473
356
  async function parseUrl(url) {
474
357
  debugLog('INFO', `开始解析链接: ${url}`);
475
- // 尝试短链接解析
476
358
  const realUrl = await resolveShortUrl(url);
477
359
  debugLog('DEBUG', `重定向后的URL: ${realUrl}`);
478
360
  const platform = getPlatformType(realUrl);
479
361
  if (!platform) {
480
- debugLog('WARN', `不支持的平台: ${realUrl}`);
481
362
  return { success: false, msg: texts.unsupportedPlatformText };
482
363
  }
483
- // 优先使用原始短链接尝试,若失败再用重定向后的URL
484
364
  const candidates = [url, realUrl];
485
365
  let lastError = null;
486
366
  for (const candidate of candidates) {
487
367
  try {
488
368
  const info = await fetchApi(candidate);
489
- debugLog('INFO', `解析成功: ${info.title}`);
490
369
  return { success: true, data: info };
491
370
  }
492
371
  catch (error) {
493
372
  lastError = getErrorMessage(error);
494
- debugLog('ERROR', `候选链接解析失败: ${candidate} => ${lastError}`);
495
373
  }
496
374
  }
497
375
  return { success: false, msg: lastError || '解析失败' };
498
376
  }
499
377
  async function processSingleUrl(session, url) {
500
- debugLog('INFO', `处理单个URL: ${url}, 用户: ${session.userId}`);
501
378
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
502
379
  const now = Date.now();
503
380
  const last = processed.get(hash);
504
381
  if (last && (now - last) < config.sameLinkInterval * 1000) {
505
- debugLog('WARN', `重复解析: ${url}`);
506
382
  return { success: false, msg: texts.duplicateLinkText };
507
383
  }
508
384
  processed.set(hash, now);
@@ -513,13 +389,11 @@ function apply(ctx, config) {
513
389
  return { success: true, data: { text, parsed: result.data } };
514
390
  }
515
391
  async function sendWithTimeout(session, content) {
516
- debugLog('DEBUG', `发送消息: ${JSON.stringify(content)}`);
517
392
  if (config.videoSendTimeout <= 0) {
518
393
  try {
519
394
  return await session.send(content);
520
395
  }
521
396
  catch (err) {
522
- debugLog('ERROR', `发送消息失败: ${getErrorMessage(err)}`);
523
397
  if (!config.ignoreSendError)
524
398
  throw err;
525
399
  return null;
@@ -532,7 +406,6 @@ function apply(ctx, config) {
532
406
  ]);
533
407
  }
534
408
  catch (err) {
535
- debugLog('ERROR', `发送消息超时或失败: ${getErrorMessage(err)}`);
536
409
  if (!config.ignoreSendError)
537
410
  throw err;
538
411
  return null;
@@ -572,9 +445,7 @@ function apply(ctx, config) {
572
445
  for (const item of items) {
573
446
  const p = item.parsed;
574
447
  const text = item.text;
575
- debugLog('INFO', `开始发送内容,类型: ${p.type}, 标题: ${p.title}`);
576
448
  if (text && config.showImageText) {
577
- debugLog('DEBUG', '发送文本消息');
578
449
  if (enableForward)
579
450
  forwardMessages.push(buildForwardNode(session, text, botName));
580
451
  else {
@@ -583,7 +454,6 @@ function apply(ctx, config) {
583
454
  }
584
455
  }
585
456
  if (p.cover && p.type !== 'live_photo') {
586
- debugLog('DEBUG', '发送封面图片:', p.cover);
587
457
  if (enableForward)
588
458
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(p.cover), botName));
589
459
  else {
@@ -592,42 +462,21 @@ function apply(ctx, config) {
592
462
  }
593
463
  }
594
464
  if (p.video && config.showVideoFile && (p.type === 'video' || p.type === 'live')) {
595
- const sendVideo = async () => {
596
- if (config.downloadVideoBeforeSend) {
597
- const fname = crypto_1.default.createHash('md5').update(p.video).digest('hex');
598
- const dl = await downloadVideo(p.video, fname, config.userAgent, config.maxVideoSize, config.downloadThreads);
599
- if (dl.success) {
600
- const fileUrl = (0, url_1.pathToFileURL)(dl.filePath).href;
601
- debugLog('INFO', `视频下载成功,发送文件: ${fileUrl}`);
602
- return koishi_1.h.file(fileUrl);
603
- }
604
- }
605
- debugLog('INFO', `发送视频链接: ${p.video}`);
606
- return koishi_1.h.video(p.video);
607
- };
465
+ const videoMsg = koishi_1.h.video(p.video);
608
466
  if (enableForward) {
609
- const vMsg = await sendVideo();
610
- forwardMessages.push(buildForwardNode(session, vMsg, botName));
467
+ forwardMessages.push(buildForwardNode(session, videoMsg, botName));
611
468
  }
612
469
  else {
613
470
  try {
614
- const vMsg = await sendVideo();
615
- await sendWithTimeout(session, vMsg);
616
- }
617
- catch (e) {
618
- debugLog('ERROR', `发送视频失败: ${getErrorMessage(e)},尝试直接发送链接`);
619
- try {
620
- await sendWithTimeout(session, koishi_1.h.video(p.video));
621
- }
622
- catch { }
471
+ await sendWithTimeout(session, videoMsg);
623
472
  }
473
+ catch { }
624
474
  await delay(500);
625
475
  }
626
476
  }
627
477
  if (p.type === 'image' || p.type === 'live_photo') {
628
478
  const mediaList = [];
629
479
  if (p.type === 'live_photo' && p.live_photo?.length) {
630
- debugLog('INFO', `发送实况图集,共 ${p.live_photo.length} 张`);
631
480
  for (const lp of p.live_photo) {
632
481
  if (lp.image)
633
482
  mediaList.push({ type: 'image', url: lp.image });
@@ -636,7 +485,6 @@ function apply(ctx, config) {
636
485
  }
637
486
  }
638
487
  else if (p.images?.length) {
639
- debugLog('INFO', `发送图集,共 ${p.images.length} 张`);
640
488
  p.images.forEach(url => mediaList.push({ type: 'image', url }));
641
489
  }
642
490
  if (enableForward) {
@@ -647,25 +495,20 @@ function apply(ctx, config) {
647
495
  }
648
496
  else {
649
497
  for (const m of mediaList) {
650
- debugLog('DEBUG', `发送${m.type}: ${m.url}`);
651
498
  try {
652
499
  await sendWithTimeout(session, m.type === 'image' ? koishi_1.h.image(m.url) : koishi_1.h.video(m.url));
653
500
  await delay(200);
654
501
  }
655
- catch (e) {
656
- debugLog('ERROR', `发送${m.type}失败: ${getErrorMessage(e)}`);
657
- }
502
+ catch { }
658
503
  }
659
504
  }
660
505
  }
661
506
  }
662
507
  if (enableForward && forwardMessages.length) {
663
- debugLog('INFO', `合并转发消息,共 ${forwardMessages.length} 条`);
664
508
  try {
665
509
  await sendWithTimeout(session, (0, koishi_1.h)('message', { forward: true }, forwardMessages.slice(0, 100)));
666
510
  }
667
- catch (e) {
668
- debugLog('ERROR', `合并转发失败,降级逐条发送: ${getErrorMessage(e)}`);
511
+ catch {
669
512
  for (const node of forwardMessages) {
670
513
  try {
671
514
  await sendWithTimeout(session, node.data.content);
@@ -680,13 +523,9 @@ function apply(ctx, config) {
680
523
  if (!config.enable)
681
524
  return;
682
525
  const content = session.content?.trim() || '';
683
- debugLog('INFO', `收到消息: "${content}"`);
684
526
  const urls = extractUrl(content);
685
- if (!urls.length) {
686
- debugLog('DEBUG', '消息中未检测到平台链接');
527
+ if (!urls.length)
687
528
  return;
688
- }
689
- debugLog('INFO', '检测到链接:', urls);
690
529
  if (config.showWaitingTip) {
691
530
  try {
692
531
  await sendWithTimeout(session, texts.waitingTipText);
@@ -696,7 +535,6 @@ function apply(ctx, config) {
696
535
  await flush(session, urls);
697
536
  });
698
537
  ctx.command('parse <url>', '手动解析视频').action(async ({ session }, url) => {
699
- debugLog('INFO', `手动解析指令: ${url}`);
700
538
  const us = extractUrl(url);
701
539
  if (!us.length) {
702
540
  await sendWithTimeout(session, texts.invalidLinkText);
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": "0.8.4",
4
+ "version": "0.8.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [