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