koishi-plugin-bilibili-videolink-analysis 1.2.0 → 1.3.0

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/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Schema, Logger, h, Context, Session } from "koishi";
2
2
  import { } from "koishi-plugin-puppeteer";
3
+ import { BilibiliParser } from "./utils";
3
4
 
4
5
  const logger = new Logger('bilibili-videolink-analysis');
5
6
 
@@ -57,9 +58,9 @@ export interface Config {
57
58
  point?: [number, number];
58
59
  enable?: boolean;
59
60
  enablebilianalysis: boolean;
61
+ videoParseMode: string[];
60
62
  waitTip_Switch?: string | null;
61
- linktextParsing: boolean;
62
- VideoParsing_ToLink: '1' | '2' | '3' | '4' | '5';
63
+ videoParseComponents: string[];
63
64
  BVnumberParsing: boolean;
64
65
  MinimumTimeInterval: number;
65
66
  Minimumduration: number;
@@ -103,76 +104,70 @@ export const Config = Schema.intersect([
103
104
  }).description('视频解析 - 功能开关'),
104
105
  Schema.union([
105
106
  Schema.object({
106
- enablebilianalysis: Schema.const(false).required(),
107
- }),
108
- Schema.intersect([
109
- Schema.object({
110
- enablebilianalysis: Schema.const(true),
111
- // @ts-ignore // 摸了摸了
112
- waitTip_Switch: Schema.union([
113
- Schema.const(null).description('不返回文字提示'),
114
- Schema.string().description('返回文字提示(请在右侧填写文字内容)').default('正在解析B站链接...'),
115
- ]).description("是否返回等待提示。开启后,会发送`等待提示语`"),
116
- linktextParsing: Schema.boolean().default(true).description("是否返回 视频图文数据 `开启后,才发送视频数据的图文解析。`"),
117
- VideoParsing_ToLink: Schema.union([
118
- Schema.const('1').description('不返回视频/视频直链'),
119
- Schema.const('2').description('仅返回视频'),
120
- Schema.const('3').description('仅返回视频直链'),
121
- Schema.const('4').description('返回视频和视频直链'),
122
- Schema.const('5').description('返回视频,仅在日志记录视频直链'),
123
- ]).role('radio').default('2').description("是否返回` 视频/视频直链 `"),
124
- BVnumberParsing: Schema.boolean().default(true).description("是否允许根据`独立的BV、AV号`解析视频 `开启后,可以通过视频的BV、AV号解析视频。` <br> [触发说明见README](https://www.npmjs.com/package/koishi-plugin-bilibili-videolink-analysis)"),
125
- MinimumTimeInterval: Schema.number().default(180).description("若干`秒`内 不再处理相同链接 `防止多bot互相触发 导致的刷屏/性能浪费`").min(1),
126
- }),
127
-
128
- Schema.object({
129
- enablebilianalysis: Schema.const(true),
130
- Minimumduration: Schema.number().default(0).description("允许解析的视频最小时长(分钟)`低于这个时长 就不会发视频内容`").min(0),
131
- Minimumduration_tip: Schema.union([
132
- Schema.const('return').description('不返回文字提示'),
133
- Schema.object({
134
- tipcontent: Schema.string().default('视频太短啦!不看不看~').description("文字提示内容"),
135
- tipanalysis: Schema.boolean().default(true).description("是否进行图文解析(不会返回视频链接)"),
136
- }).description('返回文字提示'),
137
- Schema.const(null),
138
- ]).description("对`过短视频`的文字提示内容").default(null),
139
- Maximumduration: Schema.number().default(25).description("允许解析的视频最大时长(分钟)`超过这个时长 就不会发视频内容`").min(1),
140
- Maximumduration_tip: Schema.union([
141
- Schema.const('return').description('不返回文字提示'),
142
- Schema.object({
143
- tipcontent: Schema.string().default('视频太长啦!内容还是去B站看吧~').description("文字提示内容"),
144
- tipanalysis: Schema.boolean().default(true).description("是否进行图文解析(不会返回视频链接)"),
145
- }).description('返回文字提示'),
146
- Schema.const(null),
147
- ]).description("对`过长视频`的文字提示内容").default(null),
148
- }).description("视频解析 - 内容限制"),
149
-
150
- Schema.object({
151
- parseLimit: Schema.number().default(3).description("单对话多链接解析上限").hidden(),
152
- useNumeral: Schema.boolean().default(true).description("使用格式化数字").hidden(),
153
- showError: Schema.boolean().default(false).description("当链接不正确时提醒发送者").hidden(),
154
- bVideoIDPreference: Schema.union([
155
- Schema.const("bv").description("BV "),
156
- Schema.const("av").description("AV 号"),
157
- ]).default("bv").description("ID 偏好").hidden(),
158
-
159
- bVideo_area: Schema.string().role('textarea', { rows: [8, 16] })
160
- .default("${标题} --- ${UP主}\n${简介}\n点赞:${点赞} --- 投币:${投币}\n收藏:${收藏} --- 转发:${转发}\n观看:${观看} --- 弹幕:${弹幕}\n${~~~}\n${封面}")
161
- .description(`图文解析的返回格式<br>
107
+ enablebilianalysis: Schema.const(true),
108
+ waitTip_Switch: Schema.union([
109
+ Schema.const(null).description('不返回文字提示'),
110
+ Schema.string().description('返回文字提示(请在右侧填写文字内容)').default('正在解析B站链接...'),
111
+ ]).description("是否返回等待提示。开启后,会发送`等待提示语`"),
112
+ videoParseMode: Schema.array(Schema.union([
113
+ Schema.const('link').description('解析链接'),
114
+ Schema.const('card').description('解析哔哩哔哩分享卡片'),
115
+ ]))
116
+ .default(['link', 'card'])
117
+ .role('checkbox')
118
+ .description('选择解析来源'),
119
+ videoParseComponents: Schema.array(Schema.union([
120
+ Schema.const('log').description('记录日志'),
121
+ Schema.const('text').description('返回图文'),
122
+ Schema.const('link').description('返回视频直链'),
123
+ Schema.const('video').description('返回视频'),
124
+ ]))
125
+ .default(['text', 'video'])
126
+ .role('checkbox')
127
+ .description('选择要返回的内容组件'),
128
+ BVnumberParsing: Schema.boolean().default(true).description("是否允许根据`独立的BV、AV号`解析视频 `开启后,可以通过视频的BV、AV号解析视频。` <br> [触发说明见README](https://www.npmjs.com/package/koishi-plugin-bilibili-videolink-analysis)"),
129
+ MinimumTimeInterval: Schema.number().default(180).description("若干`秒`内 不再处理相同链接 `防止多bot互相触发 导致的刷屏/性能浪费`").min(1),
130
+ Minimumduration: Schema.number().default(0).description("允许解析的视频最小时长(分钟)`低于这个时长 就不会发视频内容`").min(0),
131
+ Minimumduration_tip: Schema.union([
132
+ Schema.const('return').description('不返回文字提示'),
133
+ Schema.object({
134
+ tipcontent: Schema.string().default('视频太短啦!不看不看~').description("文字提示内容"),
135
+ tipanalysis: Schema.boolean().default(true).description("是否进行图文解析(不会返回视频链接)"),
136
+ }).description('返回文字提示'),
137
+ Schema.const(null),
138
+ ]).description("对`过短视频`的文字提示内容").default(null),
139
+ Maximumduration: Schema.number().default(25).description("允许解析的视频最大时长(分钟)`超过这个时长 就不会发视频内容`").min(1),
140
+ Maximumduration_tip: Schema.union([
141
+ Schema.const('return').description('不返回文字提示'),
142
+ Schema.object({
143
+ tipcontent: Schema.string().default('视频太长啦!内容还是去B站看吧~').description("文字提示内容"),
144
+ tipanalysis: Schema.boolean().default(true).description("是否进行图文解析(不会返回视频链接)"),
145
+ }).description('返回文字提示'),
146
+ Schema.const(null),
147
+ ]).description("对`过长视频`的文字提示内容").default(null),
148
+ parseLimit: Schema.number().default(3).description("单对话多链接解析上限").hidden(),
149
+ useNumeral: Schema.boolean().default(true).description("使用格式化数字").hidden(),
150
+ showError: Schema.boolean().default(false).description("当链接不正确时提醒发送者").hidden(),
151
+ bVideoIDPreference: Schema.union([
152
+ Schema.const("bv").description("BV 号"),
153
+ Schema.const("av").description("AV 号"),
154
+ ]).default("bv").description("ID 偏好").hidden(),
155
+ bVideo_area: Schema.string().role('textarea', { rows: [8, 16] })
156
+ .default("${标题} ${tab} ${UP主}\n${简介}\n点赞:${点赞} ${tab} 投币:${投币}\n收藏:${收藏} ${tab} 转发:${转发}\n观看:${观看} ${tab} 弹幕:${弹幕}\n${~~~}\n${封面}")
157
+ .description(`图文解析的返回格式<br>
162
158
  注意变量格式,以及变量名称。<br>比如 \`\${标题}\` 不可以变成\`\${标题123}\`,你可以直接删掉但是不能修改变量名称哦<br>
163
159
  当然变量也不能无中生有,下面的默认值内容 就是所有变量了,你仅可以删去变量 或者修改变量之外的格式。<br>
164
160
  · 特殊变量\`\${~~~}\`表示分割线,会把上下内容分为两个信息单独发送。\`\${tab}\`表示制表符。`),
165
- bVideoShowLink: Schema.boolean().default(false).description("在末尾显示视频的链接地址 `开启可能会导致其他bot循环解析`"),
166
- bVideoShowIntroductionTofixed: Schema.number().default(50).description("视频的`简介`最大的字符长度<br>超出部分会使用 `...` 代替"),
167
- }).description("链接的图文解析设置"),
168
-
169
- Schema.object({
170
- isfigure: Schema.boolean().default(false).description("是否开启合并转发 `仅支持 onebot 适配器` 其他平台开启 无效").experimental(),
171
- filebuffer: Schema.boolean().default(true).description("是否将视频链接下载后再发送 (以解决部分onebot协议端的问题)<br>否则使用视频直链发送").experimental(),
172
- middleware: Schema.boolean().default(false).description("前置中间件模式"),
173
- userAgent: Schema.string().description("所有 API 请求所用的 User-Agent").default("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"),
174
- }).description("调试设置"),
175
- ]),
161
+ bVideoShowLink: Schema.boolean().default(false).description("在末尾显示视频的链接地址 `开启可能会导致其他bot循环解析`"),
162
+ bVideoShowIntroductionTofixed: Schema.number().default(50).description("视频的`简介`最大的字符长度<br>超出部分会使用 `...` 代替"),
163
+ isfigure: Schema.boolean().default(true).description("是否开启合并转发 `仅支持 onebot 适配器` 其他平台开启 无效").experimental(),
164
+ filebuffer: Schema.boolean().default(true).description("是否将视频链接下载后再发送 (以解决部分onebot协议端的问题)<br>否则使用视频直链发送").experimental(),
165
+ middleware: Schema.boolean().default(false).description("前置中间件模式"),
166
+ userAgent: Schema.string().description("所有 API 请求所用的 User-Agent").default("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"),
167
+ }),
168
+ Schema.object({
169
+ enablebilianalysis: Schema.const(false).required(),
170
+ }),
176
171
  ]),
177
172
 
178
173
  Schema.object({
@@ -183,24 +178,43 @@ export const Config = Schema.intersect([
183
178
  ]);
184
179
 
185
180
  export function apply(ctx: Context, config: Config) {
186
-
187
- // 记录上次处理链接的时间
188
- const lastProcessedUrls: Record<string, number> = {};
181
+ const bilibiliParser = new BilibiliParser(ctx, config, logger);
189
182
 
190
183
  if (config.enablebilianalysis) {
191
184
  ctx.middleware(async (session, next) => {
185
+ // 尝试解析JSON卡片
186
+ let isCard = false;
187
+ try {
188
+ if (session.stripped.content.startsWith('<json data=')) {
189
+ isCard = true;
190
+ }
191
+ } catch (e) {
192
+ // Not a valid JSON card
193
+ }
194
+
195
+ if (isCard) {
196
+ if (!config.videoParseMode.includes('card')) {
197
+ return next();
198
+ }
199
+ } else {
200
+ if (!config.videoParseMode.includes('link')) {
201
+ return next();
202
+ }
203
+ }
204
+
192
205
  let sessioncontent = session.stripped.content;
206
+ ctx.logger.info(sessioncontent)
193
207
  if (config.BVnumberParsing) {
194
- const bvUrls = convertBVToUrl(sessioncontent);
208
+ const bvUrls = bilibiliParser.convertBVToUrl(sessioncontent);
195
209
  if (bvUrls.length > 0) {
196
210
  sessioncontent += '\n' + bvUrls.join('\n');
197
211
  }
198
212
  }
199
- const links = await isProcessLinks(sessioncontent); // 判断是否需要解析
213
+ const links = await bilibiliParser.isProcessLinks(sessioncontent); // 判断是否需要解析
200
214
  if (links) {
201
- const ret = await extractLinks(session, links); // 提取链接
202
- if (ret && !isLinkProcessedRecently(ret, session.channelId)) {
203
- await processVideoFromLink(session, ret); // 解析视频并返回
215
+ const ret = await bilibiliParser.extractLinks(session, links); // 提取链接
216
+ if (ret && !bilibiliParser.isLinkProcessedRecently(ret, session.channelId)) {
217
+ await bilibiliParser.processVideoFromLink(session, ret); // 解析视频并返回
204
218
  }
205
219
  }
206
220
  return next();
@@ -275,10 +289,10 @@ display: none !important;
275
289
  }).filter(video => video.id)
276
290
  }, config.point) // 传递配置的 point 参数
277
291
 
278
- logInfo(options)
279
- logInfo(`共找到 ${videos.length} 个视频:`)
292
+ bilibiliParser.logInfo(options)
293
+ bilibiliParser.logInfo(`共找到 ${videos.length} 个视频:`)
280
294
  videos.forEach((video: any, index: number) => {
281
- logInfo(`序号 ${index + 1}: ID - ${video.id}`)
295
+ bilibiliParser.logInfo(`序号 ${index + 1}: ID - ${video.id}`)
282
296
  })
283
297
 
284
298
 
@@ -294,11 +308,11 @@ display: none !important;
294
308
  width: 1440,
295
309
  height: viewportHeight
296
310
  })
297
- logInfo("窗口:宽度:")
298
- logInfo(1440)
311
+ bilibiliParser.logInfo("窗口:宽度:")
312
+ bilibiliParser.logInfo(1440)
299
313
 
300
- logInfo("窗口:高度:")
301
- logInfo(viewportHeight)
314
+ bilibiliParser.logInfo("窗口:高度:")
315
+ bilibiliParser.logInfo(viewportHeight)
302
316
  let msg: any;
303
317
 
304
318
  // 截图
@@ -326,556 +340,16 @@ display: none !important;
326
340
  // 返回用户选择的视频ID
327
341
  const chosenVideo = videos[choiceIndex]
328
342
 
329
- logInfo(`渲染序号设置\noverlay.style.top = ${config.point[0]}% \noverlay.style.left = ${config.point[1]}%`)
330
- logInfo(`用户选择了序号 ${choiceIndex + 1}: ID - ${chosenVideo.id}`)
343
+ bilibiliParser.logInfo(`渲染序号设置\noverlay.style.top = ${config.point[0]}% \noverlay.style.left = ${config.point[1]}%`)
344
+ bilibiliParser.logInfo(`用户选择了序号 ${choiceIndex + 1}: ID - ${chosenVideo.id}`)
331
345
 
332
346
  // 开启自动解析了
333
347
  if (config.enable) {
334
- const ret = await extractLinks(session, [{ type: 'Video', id: chosenVideo.id }]); // 提取链接
335
- if (ret && !isLinkProcessedRecently(ret, session.channelId)) {
336
- await processVideoFromLink(session, ret, options); // 解析视频并返回
348
+ const ret = await bilibiliParser.extractLinks(session, [{ type: 'Video', id: chosenVideo.id }]); // 提取链接
349
+ if (ret && !bilibiliParser.isLinkProcessedRecently(ret, session.channelId)) {
350
+ await bilibiliParser.processVideoFromLink(session, ret, options); // 解析视频并返回
337
351
  }
338
352
  }
339
353
  })
340
354
  }
341
-
342
- function logInfo(...args: any[]) {
343
- if (config.loggerinfo) {
344
- (logger.info as (...args: any[]) => void)(...args);
345
- }
346
- }
347
-
348
- // 判断是否需要解析
349
- async function isProcessLinks(sessioncontent: string) {
350
- // 解析内容中的链接
351
- const links = link_type_parser(sessioncontent);
352
- if (links.length === 0) {
353
- return false; // 如果没有找到链接,返回 false
354
- }
355
- return links; // 返回解析出的链接
356
- }
357
-
358
- //提取链接
359
- async function extractLinks(session: Session, links: { type: string; id: string }[]) {
360
- let ret = "";
361
- if (!config.isfigure) {
362
- ret += h("quote", { id: session.messageId });
363
- }
364
- let countLink = 0;
365
- let tp_ret: string;
366
-
367
- // 循环检测链接类型
368
- for (const element of links) {
369
- if (countLink >= 1) ret += "\n";
370
- if (countLink >= config.parseLimit) {
371
- ret += "已达到解析上限…";
372
- break;
373
- }
374
- tp_ret = await type_processer(element);
375
- if (tp_ret == "") {
376
- if (config.showError)
377
- ret = "无法解析链接信息。可能是 ID 不存在,或该类型可能暂不支持。";
378
- else
379
- ret = null;
380
- } else {
381
- ret += tp_ret;
382
- }
383
- countLink++;
384
- }
385
- return ret;
386
- }
387
-
388
- //判断链接是否已经处理过
389
- function isLinkProcessedRecently(ret: string, channelId: string) {
390
- const lastretUrl = extractLastUrl(ret); // 提取 ret 最后一个 http 链接作为解析目标
391
- const currentTime = Date.now();
392
-
393
- // channelId 作为 key 的一部分,分频道鉴别
394
- const channelKey = `${channelId}:${lastretUrl}`;
395
-
396
- if (lastretUrl && lastProcessedUrls[channelKey] && (currentTime - lastProcessedUrls[channelKey] < config.MinimumTimeInterval * 1000)) {
397
- ctx.logger.info(`重复出现,略过处理:\n ${lastretUrl} (频道 ${channelId})`);
398
-
399
- return true; // 已经处理过
400
- }
401
-
402
- // 更新该链接的最后处理时间,使用 channelKey
403
- if (lastretUrl) {
404
- lastProcessedUrls[channelKey] = currentTime;
405
- }
406
- return false; // 没有处理过
407
- }
408
-
409
- async function processVideoFromLink(session: Session, ret: string, options: { video?: boolean; audio?: boolean; link?: boolean } = { video: true }) {
410
- const lastretUrl = extractLastUrl(ret);
411
-
412
- let waitTipMsgId: string = null;
413
- // 等待提示语单独发送
414
- if (config.waitTip_Switch) {
415
- const result = await session.send(`${h.quote(session.messageId)}${config.waitTip_Switch}`);
416
- waitTipMsgId = Array.isArray(result) ? result[0] : result;
417
- }
418
-
419
- let videoElements: any[] = []; // 用于存储视频相关元素
420
- let textElements: any[] = []; // 用于存储图文解析元素
421
- let shouldPerformTextParsing = config.linktextParsing;
422
-
423
- // 先进行图文解析
424
- if (shouldPerformTextParsing) {
425
- let fullText: string;
426
- if (config.bVideoShowLink) {
427
- fullText = ret; // 发送完整信息
428
- } else {
429
- // 去掉最后一个链接
430
- fullText = ret.replace(lastretUrl, '');
431
- }
432
-
433
- // 分割文本
434
- const textParts = fullText.split('${~~~}');
435
-
436
- // 循环处理每个分割后的部分
437
- for (const part of textParts) {
438
- const trimmedPart = part.trim(); // 去除首尾空格
439
- if (trimmedPart) { // 确保不是空字符串
440
- const parsedElements = h.parse(trimmedPart);
441
-
442
- // 创建 message 元素
443
- const messageElement = h('message', {
444
- userId: session.userId,
445
- nickname: session.author?.nickname || session.username,
446
- }, parsedElements);
447
-
448
- // 添加 message 元素到 textElements
449
- textElements.push(messageElement);
450
- }
451
- }
452
- }
453
-
454
- // 视频/链接解析
455
- if (config.VideoParsing_ToLink) {
456
- const fullAPIurl = `http://api.xingzhige.cn/API/b_parse/?url=${encodeURIComponent(lastretUrl)}`;
457
-
458
- try {
459
- const responseData: any = await ctx.http.get(fullAPIurl);
460
-
461
- if (responseData.code === 0 && responseData.msg === "video" && responseData.data) {
462
- const { bvid, cid, video } = responseData.data;
463
- const bilibiliUrl = `https://api.bilibili.com/x/player/playurl?fnval=80&cid=${cid}&bvid=${bvid}`;
464
- const playData: any = await ctx.http.get(bilibiliUrl);
465
-
466
- logInfo(bilibiliUrl);
467
-
468
- if (playData.code === 0 && playData.data && playData.data.dash && playData.data.dash.duration) {
469
- const videoDurationSeconds = playData.data.dash.duration;
470
- const videoDurationMinutes = videoDurationSeconds / 60;
471
-
472
- // 检查视频是否太短
473
- if (videoDurationMinutes < config.Minimumduration) {
474
-
475
- // 根据 Minimumduration_tip 的值决定行为
476
- if (config.Minimumduration_tip === 'return') {
477
- // 不返回文字提示,直接返回
478
- return;
479
- } else if (typeof config.Minimumduration_tip === 'object' && config.Minimumduration_tip !== null) {
480
- // 返回文字提示
481
- if (config.Minimumduration_tip.tipcontent) {
482
- if (config.Minimumduration_tip.tipanalysis) {
483
- videoElements.push(h.text(config.Minimumduration_tip.tipcontent));
484
- } else {
485
- await session.send(config.Minimumduration_tip.tipcontent);
486
- }
487
- }
488
-
489
- // 决定是否进行图文解析
490
- shouldPerformTextParsing = config.Minimumduration_tip.tipanalysis === true;
491
-
492
- // 如果不进行图文解析,清空已准备的文本元素
493
- if (!shouldPerformTextParsing) {
494
- textElements = [];
495
- }
496
- }
497
- }
498
- // 检查视频是否太长
499
- else if (videoDurationMinutes > config.Maximumduration) {
500
-
501
- // 根据 Maximumduration_tip 的值决定行为
502
- if (config.Maximumduration_tip === 'return') {
503
- // 不返回文字提示,直接返回
504
- return;
505
- } else if (typeof config.Maximumduration_tip === 'object' && config.Maximumduration_tip !== null) {
506
- // 返回文字提示
507
- if (config.Maximumduration_tip.tipcontent) {
508
- if (config.Maximumduration_tip.tipanalysis) {
509
- videoElements.push(h.text(config.Maximumduration_tip.tipcontent));
510
- } else {
511
- await session.send(config.Maximumduration_tip.tipcontent);
512
- }
513
- }
514
-
515
- // 决定是否进行图文解析
516
- shouldPerformTextParsing = config.Maximumduration_tip.tipanalysis === true;
517
-
518
- // 如果不进行图文解析,清空已准备的文本元素
519
- if (!shouldPerformTextParsing) {
520
- textElements = [];
521
- }
522
- }
523
- } else {
524
- // 视频时长在允许范围内,处理视频
525
- let videoData = video.url; // 使用新变量名,避免覆盖原始URL
526
- logInfo(videoData);
527
-
528
- if (config.filebuffer) {
529
- try {
530
- const videoFileBuffer: any = await ctx.http.file(video.url);
531
- logInfo(videoFileBuffer);
532
-
533
- // 检查文件类型
534
- if (videoFileBuffer && videoFileBuffer.data) {
535
- // 将ArrayBuffer转换为Buffer
536
- const buffer = Buffer.from(videoFileBuffer.data);
537
-
538
- // 获取MIME类型
539
- const mimeType = videoFileBuffer.type || videoFileBuffer.mime || 'video/mp4';
540
-
541
- // 创建data URI
542
- const base64Data = buffer.toString('base64');
543
- videoData = `data:${mimeType};base64,${base64Data}`;
544
-
545
- logInfo("成功使用 ctx.http.file 将视频URL 转换为data URI格式");
546
- } else {
547
- logInfo("文件数据无效,使用原始URL");
548
- }
549
- } catch (error) {
550
- logger.error("获取视频文件失败:", error);
551
- // 出错时继续使用原始URL
552
- }
553
- }
554
-
555
- if (videoData) {
556
- if (options.link) {
557
- // 如果是链接选项,仍然使用原始URL
558
- videoElements.push(h.text(video.url));
559
- } else if (options.audio) {
560
- videoElements.push(h.audio(videoData));
561
- } else {
562
- switch (config.VideoParsing_ToLink) {
563
- case '1':
564
- break;
565
- case '2':
566
- videoElements.push(h.video(videoData));
567
- break;
568
- case '3':
569
- videoElements.push(h.text(video.url));
570
- break;
571
- case '4':
572
- videoElements.push(h.text(video.url));
573
- videoElements.push(h.video(videoData));
574
- break;
575
- case '5':
576
- logger.info(video.url);
577
- videoElements.push(h.video(videoData));
578
- break;
579
- default:
580
- break;
581
- }
582
- }
583
- } else {
584
- throw new Error("解析视频直链失败");
585
- }
586
-
587
- }
588
- } else {
589
- throw new Error("获取播放数据失败");
590
- }
591
- } else {
592
- throw new Error("解析视频信息失败或非视频类型内容");
593
- }
594
- } catch (error) {
595
- logger.error("请求解析 API 失败或处理出错:", error);
596
- }
597
- }
598
-
599
- // 准备发送的所有元素
600
- let allElements = [...textElements, ...videoElements];
601
-
602
- if (allElements.length === 0) {
603
- return;
604
- }
605
-
606
- // 合并转发处理
607
- if (config.isfigure && (session.platform === "onebot" || session.platform === "red")) {
608
- logInfo(`使用合并转发,正在合并消息。`);
609
-
610
- // 创建 figure 元素
611
- const figureContent = h('figure', {
612
- children: allElements
613
- });
614
-
615
- if (config.loggerinfofulljson) {
616
- logInfo(JSON.stringify(figureContent, null, 2));
617
- }
618
-
619
- // 发送合并转发消息
620
- await session.send(figureContent);
621
- } else {
622
- // 没有启用合并转发,按顺序发送所有元素
623
- for (const element of allElements) {
624
- await session.send(element);
625
- }
626
- }
627
-
628
- logInfo(`机器人已发送完整消息。`);
629
- if (waitTipMsgId) {
630
- await session.bot.deleteMessage(session.channelId, waitTipMsgId);
631
- }
632
- return;
633
- }
634
-
635
- // 提取最后一个URL
636
- function extractLastUrl(text: string): string | null {
637
- const urlPattern = /https?:\/\/[^\s]+/g;
638
- const urls = text.match(urlPattern);
639
- return urls ? urls.pop() : null;
640
- }
641
-
642
- // 检测BV / AV 号并转换为URL
643
- function convertBVToUrl(text: string): string[] {
644
- const bvPattern = /(?:^|\s)(BV\w{10})(?:\s|$)/g;
645
- const avPattern = /(?:^|\s)(av\d+)(?:\s|$)/g;
646
- const matches: string[] = [];
647
- let match: RegExpExecArray;
648
-
649
- // 查找 BV 号
650
- while ((match = bvPattern.exec(text)) !== null) {
651
- matches.push(`https://www.bilibili.com/video/${match[1]}`);
652
- }
653
-
654
- // 查找 AV 号
655
- while ((match = avPattern.exec(text)) !== null) {
656
- matches.push(`https://www.bilibili.com/video/${match[1]}`);
657
- }
658
-
659
- return matches;
660
- }
661
-
662
- function numeral(number: number): string | number {
663
- if (config.useNumeral) {
664
- if (number >= 10000 && number < 100000000) {
665
- return (number / 10000).toFixed(1) + "万";
666
- }
667
- else if (number >= 100000000) {
668
- return (number / 100000000).toFixed(1) + "亿";
669
- }
670
- else {
671
- return number.toString();
672
- }
673
- }
674
- else {
675
- return number;
676
- }
677
- }
678
-
679
- /**
680
- * 解析 ID 类型
681
- * @param id 视频 ID
682
- * @returns type: ID 类型, id: 视频 ID
683
- */
684
- function vid_type_parse(id: string): { type: string | null; id: string | null } {
685
- var idRegex = [
686
- {
687
- pattern: /av([0-9]+)/i,
688
- type: "av",
689
- },
690
- {
691
- pattern: /bv([0-9a-zA-Z]+)/i,
692
- type: "bv",
693
- },
694
- ];
695
- for (const rule of idRegex) {
696
- var match = id.match(rule.pattern);
697
- if (match) {
698
- return {
699
- type: rule.type,
700
- id: match[1],
701
- };
702
- }
703
- }
704
- return {
705
- type: null,
706
- id: null,
707
- };
708
- }
709
-
710
- /**
711
- * 根据视频 ID 查找视频信息
712
- * @param id 视频 ID
713
- * @returns 视频信息 Json
714
- */
715
- async function fetch_video_info(id: string): Promise<any> {
716
- var ret: any;
717
- const vid = vid_type_parse(id);
718
- switch (vid["type"]) {
719
- case "av":
720
- ret = await ctx.http.get("https://api.bilibili.com/x/web-interface/view?aid=" + vid["id"], {
721
- headers: {
722
- "User-Agent": config.userAgent,
723
- },
724
- });
725
- break;
726
- case "bv":
727
- ret = await ctx.http.get("https://api.bilibili.com/x/web-interface/view?bvid=" + vid["id"], {
728
- headers: {
729
- "User-Agent": config.userAgent,
730
- },
731
- });
732
- break;
733
- default:
734
- ret = null;
735
- break;
736
- }
737
- return ret;
738
- }
739
-
740
- /**
741
- * 生成视频信息
742
- * @param id 视频 ID
743
- * @returns 文字视频信息
744
- */
745
- async function gen_context(id: string): Promise<string | null> {
746
- const info = await fetch_video_info(id);
747
- if (!info || !info["data"])
748
- return null;
749
-
750
- let description = info["data"]["desc"];
751
- // 根据配置处理简介
752
- const maxLength = config.bVideoShowIntroductionTofixed;
753
- if (description.length > maxLength) {
754
- description = description.substring(0, maxLength) + '...';
755
- }
756
- // 定义占位符对应的数据
757
- const placeholders: Record<string, string> = {
758
- '${标题}': info["data"]["title"],
759
- '${UP主}': info["data"]["owner"]["name"],
760
- '${封面}': `<img src="${info["data"]["pic"]}"/>`,
761
- '${简介}': description, // 使用处理后的简介
762
- '${点赞}': `${numeral(info["data"]["stat"]["like"])}`,
763
- '${投币}': `${numeral(info["data"]["stat"]["coin"])}`,
764
- '${收藏}': `${numeral(info["data"]["stat"]["favorite"])}`,
765
- '${转发}': `${numeral(info["data"]["stat"]["share"])}`,
766
- '${观看}': `${numeral(info["data"]["stat"]["view"])}`,
767
- '${弹幕}': `${numeral(info["data"]["stat"]["danmaku"])}`,
768
- '${tab}': `<pre>\t</pre>`
769
- };
770
-
771
- // 根据配置项中的格式替换占位符
772
- let ret = config.bVideo_area;
773
- for (const [placeholder, value] of Object.entries(placeholders)) {
774
- ret = ret.replace(new RegExp(placeholder.replace(/\$/g, '\\$'), 'g'), value);
775
- }
776
-
777
- // 根据 ID 偏好添加视频链接
778
- switch (config.bVideoIDPreference) {
779
- case "bv":
780
- ret += `\nhttps://www.bilibili.com/video/${info["data"]["bvid"]}`;
781
- break;
782
- case "av":
783
- ret += `\nhttps://www.bilibili.com/video/av${info["data"]["aid"]}`;
784
- break;
785
- default:
786
- break;
787
- }
788
-
789
- return ret;
790
- }
791
-
792
- /**
793
- * 链接类型解析
794
- * @param content 传入消息
795
- * @returns type: "链接类型", id :"内容ID"
796
- */
797
- function link_type_parser(content: string): { type: string; id: string }[] {
798
- // 先替换转义斜杠
799
- content = content.replace(/\\\//g, '/');
800
- var linkRegex = [
801
- {
802
- pattern: /bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/gim,
803
- type: "Video",
804
- },
805
- {
806
- pattern: /b23\.tv(?:\\)?\/([0-9a-zA-Z]+)/gim,
807
- type: "Short",
808
- },
809
- {
810
- pattern: /bili(?:22|23|33)\.cn\/([0-9a-zA-Z]+)/gim,
811
- type: "Short",
812
- },
813
- {
814
- pattern: /bili2233\.cn\/([0-9a-zA-Z]+)/gim,
815
- type: "Short",
816
- },
817
- ];
818
- var ret: { type: string; id: string }[] = [];
819
- for (const rule of linkRegex) {
820
- var match: RegExpExecArray;
821
- let lastID: string;
822
- while ((match = rule.pattern.exec(content)) !== null) {
823
- if (lastID == match[1])
824
- continue;
825
- ret.push({
826
- type: rule.type,
827
- id: match[1],
828
- });
829
- lastID = match[1];
830
- }
831
- }
832
- return ret;
833
- }
834
-
835
- /**
836
- * 类型执行器
837
- * @param element 链接列表
838
- * @returns 解析来的文本
839
- */
840
- async function type_processer(element: { type: string; id: string }): Promise<string> {
841
- var ret = "";
842
- switch (element["type"]) {
843
- case "Video":
844
- const video_info = await gen_context(element["id"]);
845
- if (video_info != null)
846
- ret += video_info;
847
- break;
848
-
849
- case "Short":
850
- const typed_link = link_type_parser(await get_redir_url(element["id"]));
851
- for (const element of typed_link) {
852
- const final_info = await type_processer(element);
853
- if (final_info != null)
854
- ret += final_info;
855
- break;
856
- }
857
- break;
858
- }
859
- return ret;
860
- }
861
-
862
- /**
863
- * 根据短链接重定向获取正常链接
864
- * @param id 短链接 ID
865
- * @returns 正常链接
866
- */
867
- async function get_redir_url(id: string): Promise<string | null> {
868
- var data = await ctx.http.get("https://b23.tv/" + id, {
869
- redirect: "manual",
870
- headers: {
871
- "User-Agent": config.userAgent,
872
- },
873
- });
874
- const match = data.match(/<a\s+(?:[^>]*?\s+)?href="([^"]*)"/i);
875
- if (match)
876
- return match[1];
877
- else
878
- return null;
879
- }
880
-
881
355
  }