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/lib/index.d.ts +6 -40
- package/lib/index.js +412 -377
- package/lib/utils.d.ts +63 -0
- package/package.json +1 -1
- package/src/index.ts +103 -629
- package/src/utils.ts +536 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { Schema, Logger, h, Context, Session } from "koishi";
|
|
2
|
+
import type { Config } from './index';
|
|
3
|
+
|
|
4
|
+
export class BilibiliParser {
|
|
5
|
+
private lastProcessedUrls: Record<string, number> = {};
|
|
6
|
+
|
|
7
|
+
constructor(private ctx: Context, private config: Config, private logger: Logger) { }
|
|
8
|
+
|
|
9
|
+
public logInfo(...args: any[]) {
|
|
10
|
+
if (this.config.loggerinfo) {
|
|
11
|
+
(this.logger.info as (...args: any[]) => void)(...args);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 判断是否需要解析
|
|
16
|
+
public async isProcessLinks(sessioncontent: string) {
|
|
17
|
+
// 解析内容中的链接
|
|
18
|
+
const links = this.link_type_parser(sessioncontent);
|
|
19
|
+
if (links.length === 0) {
|
|
20
|
+
return false; // 如果没有找到链接,返回 false
|
|
21
|
+
}
|
|
22
|
+
return links; // 返回解析出的链接
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//提取链接
|
|
26
|
+
public async extractLinks(session: Session, links: { type: string; id: string }[]) {
|
|
27
|
+
let ret = "";
|
|
28
|
+
if (!this.config.isfigure) {
|
|
29
|
+
ret += h("quote", { id: session.messageId });
|
|
30
|
+
}
|
|
31
|
+
let countLink = 0;
|
|
32
|
+
let tp_ret: string;
|
|
33
|
+
|
|
34
|
+
// 循环检测链接类型
|
|
35
|
+
for (const element of links) {
|
|
36
|
+
if (countLink >= 1) ret += "\n";
|
|
37
|
+
if (countLink >= this.config.parseLimit) {
|
|
38
|
+
ret += "已达到解析上限…";
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
tp_ret = await this.type_processer(element);
|
|
42
|
+
if (tp_ret == "") {
|
|
43
|
+
if (this.config.showError)
|
|
44
|
+
ret = "无法解析链接信息。可能是 ID 不存在,或该类型可能暂不支持。";
|
|
45
|
+
else
|
|
46
|
+
ret = null;
|
|
47
|
+
} else {
|
|
48
|
+
ret += tp_ret;
|
|
49
|
+
}
|
|
50
|
+
countLink++;
|
|
51
|
+
}
|
|
52
|
+
return ret;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//判断链接是否已经处理过
|
|
56
|
+
public isLinkProcessedRecently(ret: string, channelId: string) {
|
|
57
|
+
const lastretUrl = this.extractLastUrl(ret); // 提取 ret 最后一个 http 链接作为解析目标
|
|
58
|
+
const currentTime = Date.now();
|
|
59
|
+
|
|
60
|
+
// channelId 作为 key 的一部分,分频道鉴别
|
|
61
|
+
const channelKey = `${channelId}:${lastretUrl}`;
|
|
62
|
+
|
|
63
|
+
if (lastretUrl && this.lastProcessedUrls[channelKey] && (currentTime - this.lastProcessedUrls[channelKey] < this.config.MinimumTimeInterval * 1000)) {
|
|
64
|
+
this.ctx.logger.info(`重复出现,略过处理:\n ${lastretUrl} (频道 ${channelId})`);
|
|
65
|
+
|
|
66
|
+
return true; // 已经处理过
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 更新该链接的最后处理时间,使用 channelKey
|
|
70
|
+
if (lastretUrl) {
|
|
71
|
+
this.lastProcessedUrls[channelKey] = currentTime;
|
|
72
|
+
}
|
|
73
|
+
return false; // 没有处理过
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async processVideoFromLink(session: Session, ret: string, options: { video?: boolean; audio?: boolean; link?: boolean } = { video: true }) {
|
|
77
|
+
const lastretUrl = this.extractLastUrl(ret);
|
|
78
|
+
|
|
79
|
+
let waitTipMsgId: string = null;
|
|
80
|
+
// 等待提示语单独发送
|
|
81
|
+
if (this.config.waitTip_Switch) {
|
|
82
|
+
const result = await session.send(`${h.quote(session.messageId)}${this.config.waitTip_Switch}`);
|
|
83
|
+
waitTipMsgId = Array.isArray(result) ? result[0] : result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let videoElements: any[] = []; // 用于存储视频相关元素
|
|
87
|
+
let textElements: any[] = []; // 用于存储图文解析元素
|
|
88
|
+
let shouldPerformTextParsing = this.config.videoParseComponents.includes('text');
|
|
89
|
+
|
|
90
|
+
// 先进行图文解析
|
|
91
|
+
if (shouldPerformTextParsing) {
|
|
92
|
+
let fullText: string;
|
|
93
|
+
if (this.config.bVideoShowLink) {
|
|
94
|
+
fullText = ret; // 发送完整信息
|
|
95
|
+
} else {
|
|
96
|
+
// 去掉最后一个链接
|
|
97
|
+
fullText = ret.replace(lastretUrl, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 分割文本
|
|
101
|
+
const textParts = fullText.split('${~~~}');
|
|
102
|
+
|
|
103
|
+
// 循环处理每个分割后的部分
|
|
104
|
+
for (const part of textParts) {
|
|
105
|
+
const trimmedPart = part.trim(); // 去除首尾空格
|
|
106
|
+
if (trimmedPart) { // 确保不是空字符串
|
|
107
|
+
const parsedElements = h.parse(trimmedPart);
|
|
108
|
+
|
|
109
|
+
// 创建 message 元素
|
|
110
|
+
const messageElement = h('message', {
|
|
111
|
+
userId: session.userId,
|
|
112
|
+
nickname: session.author?.nickname || session.username,
|
|
113
|
+
}, parsedElements);
|
|
114
|
+
|
|
115
|
+
// 添加 message 元素到 textElements
|
|
116
|
+
textElements.push(messageElement);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 视频/链接解析
|
|
122
|
+
if (this.config.videoParseComponents.length > 0) {
|
|
123
|
+
const fullAPIurl = `http://api.xingzhige.cn/API/b_parse/?url=${encodeURIComponent(lastretUrl)}`;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const responseData: any = await this.ctx.http.get(fullAPIurl);
|
|
127
|
+
|
|
128
|
+
if (responseData.code === 0 && responseData.msg === "video" && responseData.data) {
|
|
129
|
+
const { bvid, cid, video } = responseData.data;
|
|
130
|
+
const bilibiliUrl = `https://api.bilibili.com/x/player/playurl?fnval=80&cid=${cid}&bvid=${bvid}`;
|
|
131
|
+
const playData: any = await this.ctx.http.get(bilibiliUrl);
|
|
132
|
+
|
|
133
|
+
this.logInfo(bilibiliUrl);
|
|
134
|
+
|
|
135
|
+
if (playData.code === 0 && playData.data && playData.data.dash && playData.data.dash.duration) {
|
|
136
|
+
const videoDurationSeconds = playData.data.dash.duration;
|
|
137
|
+
const videoDurationMinutes = videoDurationSeconds / 60;
|
|
138
|
+
|
|
139
|
+
// 检查视频是否太短
|
|
140
|
+
if (videoDurationMinutes < this.config.Minimumduration) {
|
|
141
|
+
|
|
142
|
+
// 根据 Minimumduration_tip 的值决定行为
|
|
143
|
+
if (this.config.Minimumduration_tip === 'return') {
|
|
144
|
+
// 不返回文字提示,直接返回
|
|
145
|
+
return;
|
|
146
|
+
} else if (typeof this.config.Minimumduration_tip === 'object' && this.config.Minimumduration_tip !== null) {
|
|
147
|
+
// 返回文字提示
|
|
148
|
+
if (this.config.Minimumduration_tip.tipcontent) {
|
|
149
|
+
if (this.config.Minimumduration_tip.tipanalysis) {
|
|
150
|
+
videoElements.push(h.text(this.config.Minimumduration_tip.tipcontent));
|
|
151
|
+
} else {
|
|
152
|
+
await session.send(this.config.Minimumduration_tip.tipcontent);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 决定是否进行图文解析
|
|
157
|
+
shouldPerformTextParsing = this.config.Minimumduration_tip.tipanalysis === true;
|
|
158
|
+
|
|
159
|
+
// 如果不进行图文解析,清空已准备的文本元素
|
|
160
|
+
if (!shouldPerformTextParsing) {
|
|
161
|
+
textElements = [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 检查视频是否太长
|
|
166
|
+
else if (videoDurationMinutes > this.config.Maximumduration) {
|
|
167
|
+
|
|
168
|
+
// 根据 Maximumduration_tip 的值决定行为
|
|
169
|
+
if (this.config.Maximumduration_tip === 'return') {
|
|
170
|
+
// 不返回文字提示,直接返回
|
|
171
|
+
return;
|
|
172
|
+
} else if (typeof this.config.Maximumduration_tip === 'object' && this.config.Maximumduration_tip !== null) {
|
|
173
|
+
// 返回文字提示
|
|
174
|
+
if (this.config.Maximumduration_tip.tipcontent) {
|
|
175
|
+
if (this.config.Maximumduration_tip.tipanalysis) {
|
|
176
|
+
videoElements.push(h.text(this.config.Maximumduration_tip.tipcontent));
|
|
177
|
+
} else {
|
|
178
|
+
await session.send(this.config.Maximumduration_tip.tipcontent);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 决定是否进行图文解析
|
|
183
|
+
shouldPerformTextParsing = this.config.Maximumduration_tip.tipanalysis === true;
|
|
184
|
+
|
|
185
|
+
// 如果不进行图文解析,清空已准备的文本元素
|
|
186
|
+
if (!shouldPerformTextParsing) {
|
|
187
|
+
textElements = [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// 视频时长在允许范围内,处理视频
|
|
192
|
+
let videoData = video.url; // 使用新变量名,避免覆盖原始URL
|
|
193
|
+
this.logInfo(videoData);
|
|
194
|
+
|
|
195
|
+
if (this.config.filebuffer) {
|
|
196
|
+
try {
|
|
197
|
+
const videoFileBuffer: any = await this.ctx.http.file(video.url);
|
|
198
|
+
this.logInfo(videoFileBuffer);
|
|
199
|
+
|
|
200
|
+
// 检查文件类型
|
|
201
|
+
if (videoFileBuffer && videoFileBuffer.data) {
|
|
202
|
+
// 将ArrayBuffer转换为Buffer
|
|
203
|
+
const buffer = Buffer.from(videoFileBuffer.data);
|
|
204
|
+
|
|
205
|
+
// 获取MIME类型
|
|
206
|
+
const mimeType = videoFileBuffer.type || videoFileBuffer.mime || 'video/mp4';
|
|
207
|
+
|
|
208
|
+
// 创建data URI
|
|
209
|
+
const base64Data = buffer.toString('base64');
|
|
210
|
+
videoData = `data:${mimeType};base64,${base64Data}`;
|
|
211
|
+
|
|
212
|
+
this.logInfo("成功使用 ctx.http.file 将视频URL 转换为data URI格式");
|
|
213
|
+
} else {
|
|
214
|
+
this.logInfo("文件数据无效,使用原始URL");
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.logger.error("获取视频文件失败:", error);
|
|
218
|
+
// 出错时继续使用原始URL
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (videoData) {
|
|
223
|
+
if (options.link) {
|
|
224
|
+
// 如果是链接选项,仍然使用原始URL
|
|
225
|
+
videoElements.push(h.text(video.url));
|
|
226
|
+
} else if (options.audio) {
|
|
227
|
+
videoElements.push(h.audio(videoData));
|
|
228
|
+
} else {
|
|
229
|
+
if (this.config.videoParseComponents.includes('log')) {
|
|
230
|
+
this.logger.info(video.url);
|
|
231
|
+
}
|
|
232
|
+
if (this.config.videoParseComponents.includes('link')) {
|
|
233
|
+
videoElements.push(h.text(video.url));
|
|
234
|
+
}
|
|
235
|
+
if (this.config.videoParseComponents.includes('video')) {
|
|
236
|
+
videoElements.push(h.video(videoData));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error("解析视频直链失败");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
throw new Error("获取播放数据失败");
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
throw new Error("解析视频信息失败或非视频类型内容");
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.logger.error("请求解析 API 失败或处理出错:", error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 准备发送的所有元素
|
|
256
|
+
let allElements = [...textElements, ...videoElements];
|
|
257
|
+
|
|
258
|
+
if (allElements.length === 0) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 合并转发处理
|
|
263
|
+
if (this.config.isfigure && (session.platform === "onebot" || session.platform === "red")) {
|
|
264
|
+
this.logInfo(`使用合并转发,正在合并消息。`);
|
|
265
|
+
|
|
266
|
+
// 创建 figure 元素
|
|
267
|
+
const figureContent = h('figure', {
|
|
268
|
+
children: allElements
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (this.config.loggerinfofulljson) {
|
|
272
|
+
this.logInfo(JSON.stringify(figureContent, null, 2));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 发送合并转发消息
|
|
276
|
+
await session.send(figureContent);
|
|
277
|
+
} else {
|
|
278
|
+
// 没有启用合并转发,按顺序发送所有元素
|
|
279
|
+
for (const element of allElements) {
|
|
280
|
+
await session.send(element);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.logInfo(`机器人已发送完整消息。`);
|
|
285
|
+
if (waitTipMsgId) {
|
|
286
|
+
await session.bot.deleteMessage(session.channelId, waitTipMsgId);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 提取最后一个URL
|
|
292
|
+
private extractLastUrl(text: string): string | null {
|
|
293
|
+
const urlPattern = /https?:\/\/[^\s]+/g;
|
|
294
|
+
const urls = text.match(urlPattern);
|
|
295
|
+
return urls ? urls.pop() : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 检测BV / AV 号并转换为URL
|
|
299
|
+
public convertBVToUrl(text: string): string[] {
|
|
300
|
+
const bvPattern = /(?:^|\s)(BV\w{10})(?:\s|$)/g;
|
|
301
|
+
const avPattern = /(?:^|\s)(av\d+)(?:\s|$)/g;
|
|
302
|
+
const matches: string[] = [];
|
|
303
|
+
let match: RegExpExecArray;
|
|
304
|
+
|
|
305
|
+
// 查找 BV 号
|
|
306
|
+
while ((match = bvPattern.exec(text)) !== null) {
|
|
307
|
+
matches.push(`https://www.bilibili.com/video/${match[1]}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 查找 AV 号
|
|
311
|
+
while ((match = avPattern.exec(text)) !== null) {
|
|
312
|
+
matches.push(`https://www.bilibili.com/video/${match[1]}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return matches;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private numeral(number: number): string | number {
|
|
319
|
+
if (this.config.useNumeral) {
|
|
320
|
+
if (number >= 10000 && number < 100000000) {
|
|
321
|
+
return (number / 10000).toFixed(1) + "万";
|
|
322
|
+
}
|
|
323
|
+
else if (number >= 100000000) {
|
|
324
|
+
return (number / 100000000).toFixed(1) + "亿";
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
return number.toString();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
return number;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 解析 ID 类型
|
|
337
|
+
* @param id 视频 ID
|
|
338
|
+
* @returns type: ID 类型, id: 视频 ID
|
|
339
|
+
*/
|
|
340
|
+
private vid_type_parse(id: string): { type: string | null; id: string | null } {
|
|
341
|
+
var idRegex = [
|
|
342
|
+
{
|
|
343
|
+
pattern: /av([0-9]+)/i,
|
|
344
|
+
type: "av",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
pattern: /bv([0-9a-zA-Z]+)/i,
|
|
348
|
+
type: "bv",
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
for (const rule of idRegex) {
|
|
352
|
+
var match = id.match(rule.pattern);
|
|
353
|
+
if (match) {
|
|
354
|
+
return {
|
|
355
|
+
type: rule.type,
|
|
356
|
+
id: match[1],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
type: null,
|
|
362
|
+
id: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 根据视频 ID 查找视频信息
|
|
368
|
+
* @param id 视频 ID
|
|
369
|
+
* @returns 视频信息 Json
|
|
370
|
+
*/
|
|
371
|
+
private async fetch_video_info(id: string): Promise<any> {
|
|
372
|
+
var ret: any;
|
|
373
|
+
const vid = this.vid_type_parse(id);
|
|
374
|
+
switch (vid["type"]) {
|
|
375
|
+
case "av":
|
|
376
|
+
ret = await this.ctx.http.get("https://api.bilibili.com/x/web-interface/view?aid=" + vid["id"], {
|
|
377
|
+
headers: {
|
|
378
|
+
"User-Agent": this.config.userAgent,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
break;
|
|
382
|
+
case "bv":
|
|
383
|
+
ret = await this.ctx.http.get("https://api.bilibili.com/x/web-interface/view?bvid=" + vid["id"], {
|
|
384
|
+
headers: {
|
|
385
|
+
"User-Agent": this.config.userAgent,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
ret = null;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
return ret;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 生成视频信息
|
|
398
|
+
* @param id 视频 ID
|
|
399
|
+
* @returns 文字视频信息
|
|
400
|
+
*/
|
|
401
|
+
private async gen_context(id: string): Promise<string | null> {
|
|
402
|
+
const info = await this.fetch_video_info(id);
|
|
403
|
+
if (!info || !info["data"])
|
|
404
|
+
return null;
|
|
405
|
+
|
|
406
|
+
let description = info["data"]["desc"];
|
|
407
|
+
// 根据配置处理简介
|
|
408
|
+
const maxLength = this.config.bVideoShowIntroductionTofixed;
|
|
409
|
+
if (description.length > maxLength) {
|
|
410
|
+
description = description.substring(0, maxLength) + '...';
|
|
411
|
+
}
|
|
412
|
+
// 定义占位符对应的数据
|
|
413
|
+
const placeholders: Record<string, string> = {
|
|
414
|
+
'${标题}': info["data"]["title"],
|
|
415
|
+
'${UP主}': info["data"]["owner"]["name"],
|
|
416
|
+
'${封面}': `<img src="${info["data"]["pic"]}"/>`,
|
|
417
|
+
'${简介}': description, // 使用处理后的简介
|
|
418
|
+
'${点赞}': `${this.numeral(info["data"]["stat"]["like"])}`,
|
|
419
|
+
'${投币}': `${this.numeral(info["data"]["stat"]["coin"])}`,
|
|
420
|
+
'${收藏}': `${this.numeral(info["data"]["stat"]["favorite"])}`,
|
|
421
|
+
'${转发}': `${this.numeral(info["data"]["stat"]["share"])}`,
|
|
422
|
+
'${观看}': `${this.numeral(info["data"]["stat"]["view"])}`,
|
|
423
|
+
'${弹幕}': `${this.numeral(info["data"]["stat"]["danmaku"])}`,
|
|
424
|
+
'${tab}': `<pre>\t</pre>`
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// 根据配置项中的格式替换占位符
|
|
428
|
+
let ret = this.config.bVideo_area;
|
|
429
|
+
for (const [placeholder, value] of Object.entries(placeholders)) {
|
|
430
|
+
ret = ret.replace(new RegExp(placeholder.replace(/\$/g, '\\$'), 'g'), value);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 根据 ID 偏好添加视频链接
|
|
434
|
+
switch (this.config.bVideoIDPreference) {
|
|
435
|
+
case "bv":
|
|
436
|
+
ret += `\nhttps://www.bilibili.com/video/${info["data"]["bvid"]}`;
|
|
437
|
+
break;
|
|
438
|
+
case "av":
|
|
439
|
+
ret += `\nhttps://www.bilibili.com/video/av${info["data"]["aid"]}`;
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return ret;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 链接类型解析
|
|
450
|
+
* @param content 传入消息
|
|
451
|
+
* @returns type: "链接类型", id :"内容ID"
|
|
452
|
+
*/
|
|
453
|
+
private link_type_parser(content: string): { type: string; id: string }[] {
|
|
454
|
+
// 先替换转义斜杠
|
|
455
|
+
content = content.replace(/\\\//g, '/');
|
|
456
|
+
var linkRegex = [
|
|
457
|
+
{
|
|
458
|
+
pattern: /bilibili\.com\/video\/([ab]v[0-9a-zA-Z]+)/gim,
|
|
459
|
+
type: "Video",
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
pattern: /b23\.tv(?:\\)?\/([0-9a-zA-Z]+)/gim,
|
|
463
|
+
type: "Short",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
pattern: /bili(?:22|23|33)\.cn\/([0-9a-zA-Z]+)/gim,
|
|
467
|
+
type: "Short",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
pattern: /bili2233\.cn\/([0-9a-zA-Z]+)/gim,
|
|
471
|
+
type: "Short",
|
|
472
|
+
},
|
|
473
|
+
];
|
|
474
|
+
var ret: { type: string; id: string }[] = [];
|
|
475
|
+
for (const rule of linkRegex) {
|
|
476
|
+
var match: RegExpExecArray;
|
|
477
|
+
let lastID: string;
|
|
478
|
+
while ((match = rule.pattern.exec(content)) !== null) {
|
|
479
|
+
if (lastID == match[1])
|
|
480
|
+
continue;
|
|
481
|
+
ret.push({
|
|
482
|
+
type: rule.type,
|
|
483
|
+
id: match[1],
|
|
484
|
+
});
|
|
485
|
+
lastID = match[1];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return ret;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 类型执行器
|
|
493
|
+
* @param element 链接列表
|
|
494
|
+
* @returns 解析来的文本
|
|
495
|
+
*/
|
|
496
|
+
private async type_processer(element: { type: string; id: string }): Promise<string> {
|
|
497
|
+
var ret = "";
|
|
498
|
+
switch (element["type"]) {
|
|
499
|
+
case "Video":
|
|
500
|
+
const video_info = await this.gen_context(element["id"]);
|
|
501
|
+
if (video_info != null)
|
|
502
|
+
ret += video_info;
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case "Short":
|
|
506
|
+
const typed_link = this.link_type_parser(await this.get_redir_url(element["id"]));
|
|
507
|
+
for (const element of typed_link) {
|
|
508
|
+
const final_info = await this.type_processer(element);
|
|
509
|
+
if (final_info != null)
|
|
510
|
+
ret += final_info;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
return ret;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 根据短链接重定向获取正常链接
|
|
520
|
+
* @param id 短链接 ID
|
|
521
|
+
* @returns 正常链接
|
|
522
|
+
*/
|
|
523
|
+
private async get_redir_url(id: string): Promise<string | null> {
|
|
524
|
+
var data = await this.ctx.http.get("https://b23.tv/" + id, {
|
|
525
|
+
redirect: "manual",
|
|
526
|
+
headers: {
|
|
527
|
+
"User-Agent": this.config.userAgent,
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
const match = data.match(/<a\s+(?:[^>]*?\s+)?href="([^"]*)"/i);
|
|
531
|
+
if (match)
|
|
532
|
+
return match[1];
|
|
533
|
+
else
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|