koishi-plugin-video-parser-all 0.5.0 → 0.5.1

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
@@ -24,6 +24,8 @@ export declare const Config: Schema<{
24
24
  } & {
25
25
  enableForward?: boolean | null | undefined;
26
26
  downloadVideoBeforeSend?: boolean | null | undefined;
27
+ maxVideoSize?: number | null | undefined;
28
+ downloadThreads?: number | null | undefined;
27
29
  } & {
28
30
  messageBufferDelay?: number | null | undefined;
29
31
  } & {
@@ -52,6 +54,8 @@ export declare const Config: Schema<{
52
54
  } & {
53
55
  enableForward: boolean;
54
56
  downloadVideoBeforeSend: boolean;
57
+ maxVideoSize: number;
58
+ downloadThreads: number;
55
59
  } & {
56
60
  messageBufferDelay: number;
57
61
  } & {
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@ const crypto_1 = __importDefault(require("crypto"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const promises_1 = require("stream/promises");
14
+ const worker_threads_1 = require("worker_threads");
14
15
  exports.name = 'video-parser-all';
15
16
  exports.Config = koishi_1.Schema.intersect([
16
17
  koishi_1.Schema.object({
@@ -21,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
21
22
  sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
22
23
  }).description('基础设置'),
23
24
  koishi_1.Schema.object({
24
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n视频大小:${视频大小}MB').description('统一消息格式(B站会显示投币,其他平台自动隐藏)'),
25
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}').description('统一消息格式(B站会显示投币,其他平台自动隐藏;无法获取的变量会自动隐藏)'),
25
26
  }).description('统一消息格式'),
26
27
  koishi_1.Schema.object({
27
28
  showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
@@ -43,13 +44,15 @@ exports.Config = koishi_1.Schema.intersect([
43
44
  koishi_1.Schema.object({
44
45
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
45
46
  downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(避免链接失效)'),
46
- }).description('发送方式设置'),
47
+ maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
48
+ downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
49
+ }).description('发送方式设置(说明:视频大小超过限制将只发送链接;多线程下载可提升速度但可能增加服务器负载)'),
47
50
  koishi_1.Schema.object({
48
51
  messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
49
52
  }).description('消息处理设置'),
50
53
  koishi_1.Schema.object({
51
54
  autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
52
- }).description('缓存清理设置'),
55
+ }).description('缓存清理设置(说明:开启自动清理可定期删除过期的临时视频文件和解析缓存)'),
53
56
  ]);
54
57
  const processed = new Map();
55
58
  const linkBuffer = new Map();
@@ -67,8 +70,8 @@ const PLATFORM_KEYWORDS = {
67
70
  };
68
71
  const API_CONFIG = {
69
72
  bilibili: 'https://api.xingzhige.com/API/b_parse/',
70
- douyin: 'https://api.xingzhige.com/API/douyin/',
71
- kuaishou: 'https://api.xingzhige.com/API/kuaishou/',
73
+ douyin: 'https://api.bugpk.com/api/short_videos',
74
+ kuaishou: 'https://api.bugpk.com/api/short_videos',
72
75
  xiaohongshu: 'https://api.bugpk.com/api/short_videos',
73
76
  weibo: 'https://api.bugpk.com/api/weibo',
74
77
  zuiyou: 'https://api.bugpk.com/api/short_videos',
@@ -81,7 +84,59 @@ function getErrorMessage(error) {
81
84
  return error.message;
82
85
  return String(error);
83
86
  }
84
- async function downloadVideo(url, filename, userAgent) {
87
+ async function getFileSize(url, userAgent) {
88
+ try {
89
+ const response = await axios_1.default.head(url, {
90
+ timeout: 10000,
91
+ headers: {
92
+ 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
93
+ }
94
+ });
95
+ const contentLength = response.headers['content-length'];
96
+ if (contentLength) {
97
+ return Math.round(Number(contentLength) / 1024 / 1024 * 100) / 100;
98
+ }
99
+ }
100
+ catch (error) { }
101
+ return 0;
102
+ }
103
+ async function downloadVideoThread(workerData) {
104
+ return new Promise((resolve, reject) => {
105
+ const worker = new worker_threads_1.Worker(__filename, { workerData });
106
+ worker.on('message', resolve);
107
+ worker.on('error', reject);
108
+ worker.on('exit', (code) => {
109
+ if (code !== 0)
110
+ reject(new Error(`Worker stopped with exit code ${code}`));
111
+ });
112
+ });
113
+ }
114
+ if (!worker_threads_1.isMainThread) {
115
+ const { url, start, end, filename, userAgent } = worker_threads_1.workerData;
116
+ const filePath = path_1.default.join(process.cwd(), 'temp_videos', `${filename}_${start}_${end}.part`);
117
+ (0, axios_1.default)({
118
+ url,
119
+ method: 'GET',
120
+ responseType: 'stream',
121
+ timeout: 60000,
122
+ headers: {
123
+ 'User-Agent': userAgent,
124
+ 'Range': `bytes=${start}-${end}`
125
+ }
126
+ }).then(response => {
127
+ const writeStream = fs_1.default.createWriteStream(filePath);
128
+ response.data.pipe(writeStream);
129
+ writeStream.on('finish', () => {
130
+ worker_threads_1.parentPort?.postMessage({ success: true, filePath, start, end });
131
+ });
132
+ writeStream.on('error', (error) => {
133
+ worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
134
+ });
135
+ }).catch(error => {
136
+ worker_threads_1.parentPort?.postMessage({ success: false, error: error.message });
137
+ });
138
+ }
139
+ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
85
140
  const dir = path_1.default.join(process.cwd(), 'temp_videos');
86
141
  if (!fs_1.default.existsSync(dir))
87
142
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -90,23 +145,61 @@ async function downloadVideo(url, filename, userAgent) {
90
145
  if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
91
146
  throw new Error('不支持音频');
92
147
  }
93
- const response = await (0, axios_1.default)({
94
- url,
95
- method: 'GET',
96
- responseType: 'stream',
97
- timeout: 60000,
98
- headers: {
99
- 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
100
- }
101
- });
148
+ const fileSize = await getFileSize(url, userAgent);
149
+ if (maxSize > 0 && fileSize > maxSize) {
150
+ throw new Error(`视频大小${fileSize}MB超过限制${maxSize}MB`);
151
+ }
152
+ if (threads <= 0 || fileSize === 0) {
153
+ const response = await (0, axios_1.default)({
154
+ url,
155
+ method: 'GET',
156
+ responseType: 'stream',
157
+ timeout: 60000,
158
+ headers: {
159
+ 'User-Agent': userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
160
+ }
161
+ });
162
+ const writeStream = fs_1.default.createWriteStream(filePath);
163
+ await (0, promises_1.pipeline)(response.data, writeStream);
164
+ return filePath;
165
+ }
166
+ const totalSize = fileSize * 1024 * 1024;
167
+ const chunkSize = Math.ceil(totalSize / threads);
168
+ const promises = [];
169
+ for (let i = 0; i < threads; i++) {
170
+ const start = i * chunkSize;
171
+ const end = i === threads - 1 ? totalSize - 1 : start + chunkSize - 1;
172
+ promises.push(downloadVideoThread({
173
+ url,
174
+ start,
175
+ end,
176
+ filename,
177
+ userAgent
178
+ }));
179
+ }
180
+ const results = await Promise.all(promises);
102
181
  const writeStream = fs_1.default.createWriteStream(filePath);
103
- await (0, promises_1.pipeline)(response.data, writeStream);
182
+ for (const result of results) {
183
+ if (!result.success)
184
+ throw new Error(result.error);
185
+ const readStream = fs_1.default.createReadStream(result.filePath);
186
+ await (0, promises_1.pipeline)(readStream, writeStream, { end: false });
187
+ fs_1.default.unlinkSync(result.filePath);
188
+ }
189
+ writeStream.end();
104
190
  return filePath;
105
191
  }
106
192
  catch (error) {
107
193
  if (fs_1.default.existsSync(filePath)) {
108
194
  fs_1.default.unlinkSync(filePath);
109
195
  }
196
+ const partFiles = fs_1.default.readdirSync(dir).filter(file => file.startsWith(`${filename}_`) && file.endsWith('.part'));
197
+ partFiles.forEach(file => {
198
+ try {
199
+ fs_1.default.unlinkSync(path_1.default.join(dir, file));
200
+ }
201
+ catch (e) { }
202
+ });
110
203
  throw error;
111
204
  }
112
205
  }
@@ -124,46 +217,11 @@ function parseXingzhigeData(resData, platform) {
124
217
  favorite: 0,
125
218
  share: 0,
126
219
  view: 0,
127
- size: 0,
128
220
  duration: '00:00'
129
221
  },
130
222
  type: 'video'
131
223
  };
132
- if (platform === 'kuaishou' && resData.jx && resData.jx.length > 0) {
133
- const item = resData.jx[0];
134
- result.title = item.title || '';
135
- result.cover = item.cover || '';
136
- result.video = item.url || item.video || '';
137
- result.images = item.images || [];
138
- result.stat = {
139
- like: resData.stat?.like || 0,
140
- coin: 0,
141
- favorite: 0,
142
- share: resData.stat?.share || 0,
143
- view: resData.stat?.view || 0,
144
- size: 0,
145
- duration: '00:00'
146
- };
147
- result.type = result.images.length > 0 ? 'image' : 'video';
148
- }
149
- else if (platform === 'douyin' && resData.jx && resData.jx.length > 0) {
150
- const item = resData.jx[0];
151
- result.title = item.title || '';
152
- result.cover = item.cover || '';
153
- result.video = item.url || '';
154
- result.images = item.images || [];
155
- result.stat = {
156
- like: resData.stat?.like || 0,
157
- coin: 0,
158
- favorite: resData.stat?.collect || 0,
159
- share: resData.stat?.share || 0,
160
- view: 0,
161
- size: 0,
162
- duration: '00:00'
163
- };
164
- result.type = result.images.length > 0 ? 'image' : 'video';
165
- }
166
- else if (platform === 'bilibili') {
224
+ if (platform === 'bilibili') {
167
225
  const d = resData.data || resData;
168
226
  result.title = d.video?.title || d.title || '';
169
227
  result.author = d.owner?.name || d.name || '未知UP主';
@@ -176,7 +234,6 @@ function parseXingzhigeData(resData, platform) {
176
234
  like: d.stat?.like || 0,
177
235
  coin: d.stat?.coin || 0,
178
236
  share: d.stat?.share || 0,
179
- size: 0,
180
237
  duration: formatDuration(d.duration || 0)
181
238
  };
182
239
  result.duration = d.duration || 0;
@@ -256,7 +313,6 @@ function parseData(data, maxDescLength, platform) {
256
313
  favorite: 0,
257
314
  share: 0,
258
315
  view: 0,
259
- size: 0,
260
316
  duration: '00:00'
261
317
  };
262
318
  const durationFormatted = formatDuration(duration);
@@ -289,7 +345,6 @@ function parseData(data, maxDescLength, platform) {
289
345
  favorite: stat.favorite || 0,
290
346
  share: stat.share || 0,
291
347
  view: stat.view || 0,
292
- size: stat.size || 0,
293
348
  duration: durationFormatted
294
349
  }
295
350
  };
@@ -299,17 +354,29 @@ function generateFormattedText(platform, parseData, config) {
299
354
  if (platform !== 'bilibili') {
300
355
  format = format.replace(/投币:\$\{投币数\}\n?/g, '');
301
356
  }
302
- return format
303
- .replace(/\${标题}/g, parseData.title)
304
- .replace(/\${作者}/g, parseData.author)
305
- .replace(/\${简介}/g, parseData.desc)
306
- .replace(/\${视频时长}/g, parseData.stat.duration)
307
- .replace(/\${点赞数}/g, parseData.stat.like)
308
- .replace(/\${投币数}/g, parseData.stat.coin)
309
- .replace(/\${收藏数}/g, parseData.stat.favorite)
310
- .replace(/\${转发数}/g, parseData.stat.share)
311
- .replace(/\${播放数}/g, parseData.stat.view)
312
- .replace(/\${视频大小}/g, parseData.stat.size);
357
+ const variables = {
358
+ '标题': parseData.title || '',
359
+ '作者': parseData.author || '',
360
+ '简介': parseData.desc || '',
361
+ '视频时长': parseData.stat.duration || '',
362
+ '点赞数': parseData.stat.like > 0 ? parseData.stat.like : '',
363
+ '投币数': parseData.stat.coin > 0 ? parseData.stat.coin : '',
364
+ '收藏数': parseData.stat.favorite > 0 ? parseData.stat.favorite : '',
365
+ '转发数': parseData.stat.share > 0 ? parseData.stat.share : '',
366
+ '播放数': parseData.stat.view > 0 ? parseData.stat.view : ''
367
+ };
368
+ let result = format;
369
+ Object.entries(variables).forEach(([key, value]) => {
370
+ const regex = new RegExp(`${key}:\\$\\{${key}\\\}([\\n]?)`, 'g');
371
+ if (!value) {
372
+ result = result.replace(regex, '');
373
+ }
374
+ else {
375
+ result = result.replace(`$\{${key}\}`, value);
376
+ }
377
+ });
378
+ result = result.replace(/\n+/g, '\n').trim();
379
+ return result;
313
380
  }
314
381
  function clearAllCache() {
315
382
  processed.clear();
@@ -366,7 +433,7 @@ function apply(ctx, config) {
366
433
  try {
367
434
  const res = await http.get(apiUrl, { params: { url: realUrl } });
368
435
  let parseResult = null;
369
- if (['bilibili', 'douyin', 'kuaishou'].includes(platform)) {
436
+ if (platform === 'bilibili') {
370
437
  const xgData = parseXingzhigeData(res.data, platform);
371
438
  parseResult = parseData(xgData, config.maxDescLength, platform);
372
439
  }
@@ -483,21 +550,27 @@ function apply(ctx, config) {
483
550
  }
484
551
  if (item.video && config.showVideoFile && forwardMessages.length < 100) {
485
552
  let videoElem;
486
- if (config.downloadVideoBeforeSend) {
487
- try {
553
+ try {
554
+ if (config.downloadVideoBeforeSend) {
488
555
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
489
- const filePath = await downloadVideo(item.video, filename, config.userAgent);
556
+ const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
490
557
  videoElem = koishi_1.h.file(filePath);
491
558
  }
492
- catch (error) {
493
- logger.error(`视频下载失败: ${getErrorMessage(error)}`);
494
- videoElem = koishi_1.h.video(item.video);
559
+ else {
560
+ const fileSize = await getFileSize(item.video, config.userAgent);
561
+ if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
562
+ videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
563
+ }
564
+ else {
565
+ videoElem = koishi_1.h.video(item.video);
566
+ }
495
567
  }
568
+ forwardMessages.push(buildForwardNode(session, videoElem, botName));
496
569
  }
497
- else {
498
- videoElem = koishi_1.h.video(item.video);
570
+ catch (error) {
571
+ logger.error(`视频处理失败: ${getErrorMessage(error)}`);
572
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`), botName));
499
573
  }
500
- forwardMessages.push(buildForwardNode(session, videoElem, botName));
501
574
  }
502
575
  }
503
576
  else {
@@ -515,22 +588,28 @@ function apply(ctx, config) {
515
588
  await delay(300);
516
589
  }
517
590
  if (item.video && config.showVideoFile) {
518
- let videoElem;
519
- if (config.downloadVideoBeforeSend) {
520
- try {
591
+ try {
592
+ let videoElem;
593
+ if (config.downloadVideoBeforeSend) {
521
594
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
522
- const filePath = await downloadVideo(item.video, filename, config.userAgent);
595
+ const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
523
596
  videoElem = koishi_1.h.file(filePath);
524
597
  }
525
- catch (error) {
526
- logger.error(`视频下载失败: ${getErrorMessage(error)}`);
527
- videoElem = koishi_1.h.video(item.video);
598
+ else {
599
+ const fileSize = await getFileSize(item.video, config.userAgent);
600
+ if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
601
+ videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
602
+ }
603
+ else {
604
+ videoElem = koishi_1.h.video(item.video);
605
+ }
528
606
  }
607
+ await sendTimeout(session, videoElem);
529
608
  }
530
- else {
531
- videoElem = koishi_1.h.video(item.video);
609
+ catch (error) {
610
+ logger.error(`视频处理失败: ${getErrorMessage(error)}`);
611
+ await sendTimeout(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`));
532
612
  }
533
- await sendTimeout(session, videoElem);
534
613
  }
535
614
  }
536
615
  await delay(1000);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/右视频链接解析",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [