koishi-plugin-video-parser-all 0.2.3 → 0.2.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,7 +3,6 @@ export declare const name = "video-parser-all";
3
3
  export interface Config {
4
4
  enable: boolean;
5
5
  showWaitingTip: boolean;
6
- revokeWaitingTip: boolean;
7
6
  waitingTipText: string;
8
7
  sameLinkInterval: number;
9
8
  imageParseFormat: string;
@@ -20,8 +19,11 @@ export interface Config {
20
19
  messageBufferDelay: number;
21
20
  retryTimes: number;
22
21
  retryInterval: number;
23
- apiUrl: string;
22
+ apiMode: 'builtin' | 'custom';
23
+ builtinApiUrl: string;
24
+ customApiUrl: string;
24
25
  videoSendTimeout: number;
26
+ autoClearCacheInterval: number;
25
27
  }
26
28
  export declare const Config: Schema<Config>;
27
29
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -16,11 +16,10 @@ exports.name = 'video-parser-all';
16
16
  exports.Config = koishi_1.Schema.object({
17
17
  enable: koishi_1.Schema.boolean().default(true).description('启用插件'),
18
18
  showWaitingTip: koishi_1.Schema.boolean().default(true).description('解析时显示等待提示'),
19
- revokeWaitingTip: koishi_1.Schema.boolean().default(true).description('解析完成后撤回等待提示'),
20
19
  waitingTipText: koishi_1.Schema.string().default('正在解析视频,请稍候...').description('等待提示文本'),
21
20
  sameLinkInterval: koishi_1.Schema.number().default(0).min(0).description('相同链接解析间隔(秒)'),
22
- imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题}\n${~~~}\n${UP主}').description(`解析结果格式
23
- 支持变量:\${标题} \${UP主} \${简介} \${点赞} \${投币} \${收藏} \${转发} \${观看} \${弹幕} \${tab} \${~~~}`),
21
+ imageParseFormat: koishi_1.Schema.string().role('textarea').default('${标题}\n${UP主}').description(`解析结果格式
22
+ 支持变量:\${标题} \${UP主} \${简介} \${tab} \${~~~}`),
24
23
  returnContent: koishi_1.Schema.object({
25
24
  showImageText: koishi_1.Schema.boolean().default(true).description('显示文本与封面'),
26
25
  showVideoUrl: koishi_1.Schema.boolean().default(false).description('显示无水印链接'),
@@ -34,7 +33,11 @@ exports.Config = koishi_1.Schema.object({
34
33
  messageBufferDelay: koishi_1.Schema.number().default(0).min(0).description('消息缓冲延迟(秒)'),
35
34
  retryTimes: koishi_1.Schema.number().default(0).min(0).description('接口重试次数'),
36
35
  retryInterval: koishi_1.Schema.number().default(0).min(0).description('重试间隔(毫秒)'),
37
- apiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description(`本插件使用 BugPk-Api 公共接口
36
+ apiMode: koishi_1.Schema.union([
37
+ koishi_1.Schema.const('builtin').description('使用内置API'),
38
+ koishi_1.Schema.const('custom').description('使用自定义API')
39
+ ]).default('builtin').description('API使用模式'),
40
+ builtinApiUrl: koishi_1.Schema.string().default('https://api.bugpk.com/api/short_videos').description(`内置API地址
38
41
  解析失败可能原因:
39
42
  1. 视频为私密/付费/下架/限制
40
43
  2. 接口限流或维护
@@ -42,10 +45,12 @@ exports.Config = koishi_1.Schema.object({
42
45
  4. 平台链接格式更新
43
46
  注意事项:
44
47
  1. 勿频繁解析相同链接
45
- 2. 仅支持快手/B站公开视频
48
+ 2. 仅支持快手/B站/小红书/微博公开视频
46
49
  3. 公共接口不保证100%成功
47
50
  4. 失败可检查链接或稍后重试`),
51
+ customApiUrl: koishi_1.Schema.string().default('').description('自定义API地址(优先使用)'),
48
52
  videoSendTimeout: koishi_1.Schema.number().default(0).min(0).description('视频发送超时(毫秒)'),
53
+ autoClearCacheInterval: koishi_1.Schema.number().default(0).min(0).description('自动清理缓存间隔(分钟),0表示不自动清理'),
49
54
  });
50
55
  if (!worker_threads_1.isMainThread) {
51
56
  const { url, filePath } = worker_threads_1.workerData;
@@ -76,7 +81,9 @@ const processed = new Map();
76
81
  const linkBuffer = new Map();
77
82
  const PLATFORM_KEYWORDS = {
78
83
  bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com'],
79
- kuaishou: ['kuaishou', '快手', 'v.kuishou.com', 'www.kuaishou.com', 'kwimgs.com']
84
+ kuaishou: ['kuaishou', '快手', 'v.kuishou.com', 'www.kuishou.com', 'kwimgs.com'],
85
+ xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com'],
86
+ weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'svproxy.168299.xyz']
80
87
  };
81
88
  function extractUrl(content) {
82
89
  const urlMatches = content.match(/https?:\/\/[^\s]+/gi) || [];
@@ -95,6 +102,10 @@ function getPlatformType(url) {
95
102
  return 'kuaishou';
96
103
  if (PLATFORM_KEYWORDS.bilibili.some(k => lower.includes(k)))
97
104
  return 'bilibili';
105
+ if (PLATFORM_KEYWORDS.xiaohongshu.some(k => lower.includes(k)))
106
+ return 'xiaohongshu';
107
+ if (PLATFORM_KEYWORDS.weibo.some(k => lower.includes(k)))
108
+ return 'weibo';
98
109
  return null;
99
110
  }
100
111
  async function shortUrl(url) {
@@ -121,7 +132,7 @@ async function downloadVideoWithThreads(url, filename) {
121
132
  function parseData(data, maxDescLength) {
122
133
  const type = data.type || 'video';
123
134
  const title = data.title || data.desc || '无标题';
124
- const author = data.author || data.auther || data.user?.name || '未知作者';
135
+ const author = data.author?.name || data.author || data.auther || data.user?.name || '未知作者';
125
136
  const desc = (data.desc || data.description || title).slice(0, maxDescLength);
126
137
  const cover = data.cover || data.imgurl || data.pic || '';
127
138
  const images = data.images || [];
@@ -153,6 +164,12 @@ function apply(ctx, config) {
153
164
  if (!worker_threads_1.isMainThread)
154
165
  return;
155
166
  clearAllCache();
167
+ const getApiUrl = () => {
168
+ if (config.apiMode === 'custom' && config.customApiUrl.trim()) {
169
+ return config.customApiUrl.trim();
170
+ }
171
+ return config.builtinApiUrl;
172
+ };
156
173
  const http = axios_1.default.create({
157
174
  timeout: config.timeout,
158
175
  headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
@@ -163,7 +180,7 @@ function apply(ctx, config) {
163
180
  return { data: null, msg: '不支持该平台链接' };
164
181
  for (let i = 0; i <= config.retryTimes; i++) {
165
182
  try {
166
- const res = await http.get(config.apiUrl, { params: { url } });
183
+ const res = await http.get(getApiUrl(), { params: { url } });
167
184
  if ((res.data.code === 200 || res.data.code === 0) && res.data.data) {
168
185
  return { data: parseData(res.data.data, config.maxDescLength), msg: '解析成功' };
169
186
  }
@@ -194,26 +211,10 @@ function apply(ctx, config) {
194
211
  .replace(/\${标题}/g, d.title)
195
212
  .replace(/\${UP主}/g, d.author)
196
213
  .replace(/\${简介}/g, d.desc)
197
- .replace(/\${点赞}/g, '0')
198
- .replace(/\${投币}/g, '0')
199
- .replace(/\${收藏}/g, '0')
200
- .replace(/\${转发}/g, '0')
201
- .replace(/\${观看}/g, '0')
202
- .replace(/\${弹幕}/g, '0')
203
- .replace(/\${tab}/g, '\t').replace(/\${~~~}/g, '\n');
214
+ .replace(/\${tab}/g, '\t')
215
+ .replace(/\${~~~}/g, '\n');
204
216
  return { data: { text, cover: d.cover, images: d.images, video: d.video, type: d.type }, msg: 'ok' };
205
217
  }
206
- async function revokeTip(session, key) {
207
- if (!config.revokeWaitingTip || session.platform !== 'onebot')
208
- return;
209
- const buf = linkBuffer.get(key);
210
- if (!buf?.tipMsgId)
211
- return;
212
- try {
213
- await session.bot.deleteMessage(session.channelId, buf.tipMsgId.toString());
214
- }
215
- catch { }
216
- }
217
218
  async function sendTimeout(session, c) {
218
219
  if (config.videoSendTimeout <= 0)
219
220
  return session.send(c).catch(() => null);
@@ -226,7 +227,6 @@ function apply(ctx, config) {
226
227
  if (buf) {
227
228
  clearTimeout(buf.timer);
228
229
  linkBuffer.delete(key);
229
- await revokeTip(session, key);
230
230
  }
231
231
  const items = [];
232
232
  const errs = [];
@@ -234,44 +234,111 @@ function apply(ctx, config) {
234
234
  const one = await processSingleUrl(session, u);
235
235
  one.data ? items.push(one.data) : errs.push(`【${u.slice(0, 22)}...】:${one.msg}`);
236
236
  }
237
- if (items.length === 0) {
238
- await sendTimeout(session, `❌ 全部解析失败\n${errs.join('\n')}`);
239
- return;
240
- }
237
+ const forwardMessages = [];
238
+ const botName = '视频解析机器人';
241
239
  if (errs.length) {
242
- await sendTimeout(session, `⚠️ 部分解析失败\n${errs.join('\n')}`);
243
- await delay(600);
240
+ const errorMsg = `⚠️ 部分解析失败\n${errs.join('\n')}`;
241
+ if (config.enableForward && session.platform === 'onebot') {
242
+ forwardMessages.push((0, koishi_1.h)('message', [
243
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
244
+ errorMsg
245
+ ]));
246
+ }
247
+ else {
248
+ await sendTimeout(session, errorMsg);
249
+ await delay(600);
250
+ }
244
251
  }
245
- for (const it of items) {
246
- await sendTimeout(session, it.text);
247
- await delay(300);
248
- if (it.type === 'image' && it.images?.length) {
249
- const msg = (0, koishi_1.h)('message', ...it.images.map(u => koishi_1.h.image(u)));
250
- await sendTimeout(session, msg);
252
+ if (items.length === 0) {
253
+ const failMsg = `❌ 全部解析失败\n${errs.join('\n')}`;
254
+ if (config.enableForward && session.platform === 'onebot') {
255
+ forwardMessages.push((0, koishi_1.h)('message', [
256
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
257
+ failMsg
258
+ ]));
251
259
  }
252
260
  else {
261
+ await sendTimeout(session, failMsg);
262
+ }
263
+ return;
264
+ }
265
+ for (const it of items) {
266
+ if (config.enableForward && session.platform === 'onebot') {
267
+ forwardMessages.push((0, koishi_1.h)('message', [
268
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
269
+ it.text
270
+ ]));
253
271
  if (it.cover) {
254
- await sendTimeout(session, koishi_1.h.image(it.cover));
255
- await delay(300);
272
+ forwardMessages.push((0, koishi_1.h)('message', [
273
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
274
+ koishi_1.h.image(it.cover)
275
+ ]));
256
276
  }
257
277
  if (it.video && config.returnContent.showVideoFile) {
258
278
  let vid = koishi_1.h.video(it.video);
259
- if (config.downloadVideoBeforeSend && session.platform === 'onebot') {
279
+ if (config.downloadVideoBeforeSend) {
260
280
  try {
261
281
  const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
262
282
  vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
263
283
  }
264
284
  catch { }
265
285
  }
266
- await sendTimeout(session, vid);
286
+ forwardMessages.push((0, koishi_1.h)('message', [
287
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
288
+ vid
289
+ ]));
267
290
  }
268
291
  if (it.video && config.returnContent.showVideoUrl) {
269
- await delay(300);
270
292
  const s = await shortUrl(it.video);
271
- await sendTimeout(session, `🔗 无水印:${s}`);
293
+ forwardMessages.push((0, koishi_1.h)('message', [
294
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
295
+ `🔗 无水印:${s}`
296
+ ]));
297
+ }
298
+ if (it.type === 'image' && it.images?.length) {
299
+ it.images.forEach(imgUrl => {
300
+ forwardMessages.push((0, koishi_1.h)('message', [
301
+ (0, koishi_1.h)('author', { id: session.selfId, name: botName }),
302
+ koishi_1.h.image(imgUrl)
303
+ ]));
304
+ });
272
305
  }
273
306
  }
274
- await delay(1000);
307
+ else {
308
+ await sendTimeout(session, it.text);
309
+ await delay(300);
310
+ if (it.type === 'image' && it.images?.length) {
311
+ const msg = (0, koishi_1.h)('message', ...it.images.map(u => koishi_1.h.image(u)));
312
+ await sendTimeout(session, msg);
313
+ }
314
+ else {
315
+ if (it.cover) {
316
+ await sendTimeout(session, koishi_1.h.image(it.cover));
317
+ await delay(300);
318
+ }
319
+ if (it.video && config.returnContent.showVideoFile) {
320
+ let vid = koishi_1.h.video(it.video);
321
+ if (config.downloadVideoBeforeSend) {
322
+ try {
323
+ const name = crypto_1.default.createHash('md5').update(it.video).digest('hex');
324
+ vid = koishi_1.h.file(await downloadVideoWithThreads(it.video, name));
325
+ }
326
+ catch { }
327
+ }
328
+ await sendTimeout(session, vid);
329
+ }
330
+ if (it.video && config.returnContent.showVideoUrl) {
331
+ await delay(300);
332
+ const s = await shortUrl(it.video);
333
+ await sendTimeout(session, `🔗 无水印:${s}`);
334
+ }
335
+ }
336
+ await delay(1000);
337
+ }
338
+ }
339
+ if (config.enableForward && session.platform === 'onebot' && forwardMessages.length) {
340
+ const forwardMsg = (0, koishi_1.h)('message', { forward: true }, forwardMessages);
341
+ await sendTimeout(session, forwardMsg);
275
342
  }
276
343
  }
277
344
  ctx.on('message', async (session) => {
@@ -334,6 +401,12 @@ function apply(ctx, config) {
334
401
  catch { }
335
402
  });
336
403
  }, 1800000);
404
+ if (config.autoClearCacheInterval > 0) {
405
+ setInterval(() => {
406
+ clearAllCache();
407
+ ctx.logger.info('自动清理缓存完成');
408
+ }, config.autoClearCacheInterval * 60000);
409
+ }
337
410
  process.on('exit', clearAllCache);
338
411
  ctx.logger.info('视频解析插件已加载');
339
412
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
- "description": "Koishi 视频解析插件,支持抖音/快手/B站链接解析",
4
- "version": "0.2.3",
3
+ "description": "Koishi 视频解析插件,支持抖音/快手/B站/小红书/微博视频链接解析",
4
+ "version": "0.2.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -35,5 +35,13 @@
35
35
  "peerDependencies": {
36
36
  "@koishijs/plugin-console": "^5.30.4",
37
37
  "koishi": "^4.18.7"
38
- }
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/Minecraft-1314/koishi-plugin-video-parser-all.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues"
45
+ },
46
+ "homepage": "https://github.com/Minecraft-1314/koishi-plugin-video-parser-all#readme"
39
47
  }
package/readme.md CHANGED
@@ -1,5 +1,49 @@
1
- # koishi-plugin-video-parser
1
+ # koishi-plugin-video-parser-all
2
2
 
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-video-parser?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-video-parser)
3
+ ## 项目介绍 (Project Introduction)
4
4
 
5
- video-parser
5
+ ### 中文
6
+ 这是一个为 Koishi 机器人框架开发的**多平台视频解析插件**,支持自动识别并解析抖音、快手、B站、小红书、微博等主流短视频平台链接。核心特性:
7
+ - 自动识别多平台视频链接,无需手动指定平台
8
+ - 自定义解析结果格式、返回内容类型(封面/链接/视频文件)
9
+ - 内置防重复解析、接口重试、自动缓存清理等实用功能
10
+ - 支持 OneBot 平台消息合并转发,优化展示体验
11
+ - 可切换内置 API 或自定义 API,适配不同网络环境
12
+
13
+ ### English
14
+ A multi-platform video parsing plugin for the Koishi bot framework, supporting automatic recognition and parsing of video links from Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo and other mainstream platforms. Core features:
15
+ - Auto-detect video links across platforms without manual specification
16
+ - Customize result formatting and output types (cover/link/video file)
17
+ - Built-in duplicate prevention, retry logic, auto cache cleanup
18
+ - Support OneBot message forwarding for better display experience
19
+ - Switchable between built-in and custom API for different network environments
20
+
21
+ ## 项目仓库 (Repository)
22
+ - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
23
+ - Issues: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all/issues`
24
+
25
+ ## 核心指令 (Core Commands)
26
+
27
+ parse (手动解析)(Manual parsing)
28
+
29
+ clear-cache (清理缓存)(Clear cache)
30
+
31
+ ## 项目贡献者 (Contributors)
32
+
33
+ | 贡献者 (Contributor) | 贡献内容 (Contribution) |
34
+ |----------------------|-------------------------|
35
+ | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
36
+ | JH-Ahua | BugPk-Api |
37
+ | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
38
+
39
+ ## 许可协议 (License)
40
+
41
+ 本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
42
+
43
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
44
+
45
+ ## 支持我们 (Support Us)
46
+
47
+ 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
48
+
49
+ If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us, which will be the greatest encouragement to all contributors!