koishi-plugin-bilibili-videolink-analysis 1.3.2 → 1.3.4
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 +3 -1
- package/lib/index.js +167 -22
- package/lib/utils.d.ts +14 -0
- package/package.json +26 -26
- package/src/index.ts +359 -354
- package/src/utils.ts +225 -20
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import { Schema, Logger, h, Context, Session } from "koishi";
|
|
2
2
|
import type { Config } from './index';
|
|
3
3
|
|
|
4
|
+
// 队列任务接口
|
|
5
|
+
interface QueueTask {
|
|
6
|
+
session: Session;
|
|
7
|
+
ret: string;
|
|
8
|
+
options?: { video?: boolean; audio?: boolean; link?: boolean };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 缓冲区任务接口
|
|
12
|
+
interface BufferTask {
|
|
13
|
+
session: Session;
|
|
14
|
+
ret: string;
|
|
15
|
+
options?: { video?: boolean; audio?: boolean; link?: boolean };
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Session 级别的任务接口
|
|
20
|
+
interface SessionTask {
|
|
21
|
+
session: Session;
|
|
22
|
+
sessioncontent: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export class BilibiliParser {
|
|
5
27
|
private lastProcessedUrls: Record<string, number> = {};
|
|
28
|
+
private processingQueue: QueueTask[] = []; // 待处理队列
|
|
29
|
+
private isProcessing: boolean = false; // 是否正在处理
|
|
30
|
+
private bufferQueue: BufferTask[] = []; // 缓冲队列
|
|
31
|
+
private bufferTimer: NodeJS.Timeout | null = null; // 缓冲定时器
|
|
32
|
+
|
|
33
|
+
// Session 级别的队列控制
|
|
34
|
+
private sessionQueue: SessionTask[] = []; // Session 缓冲队列
|
|
35
|
+
private sessionTimer: NodeJS.Timeout | null = null; // Session 缓冲定时器
|
|
36
|
+
private isProcessingSession: boolean = false; // 是否正在处理 Session
|
|
6
37
|
|
|
7
38
|
constructor(private ctx: Context, private config: Config, private logger: Logger) { }
|
|
8
39
|
|
|
@@ -73,8 +104,161 @@ export class BilibiliParser {
|
|
|
73
104
|
return false; // 没有处理过
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
// 添加 session 到缓冲队列(middleware 入口调用)
|
|
108
|
+
public async queueSession(session: Session, sessioncontent: string) {
|
|
109
|
+
// 将 session 加入缓冲队列
|
|
110
|
+
this.sessionQueue.push({ session, sessioncontent, timestamp: Date.now() });
|
|
111
|
+
this.logInfo(`收到消息,Session缓冲区任务数: ${this.sessionQueue.length}`);
|
|
112
|
+
|
|
113
|
+
// 清除之前的定时器
|
|
114
|
+
if (this.sessionTimer) {
|
|
115
|
+
clearTimeout(this.sessionTimer);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 设置新的定时器,等待配置的延迟时间后处理
|
|
119
|
+
this.sessionTimer = setTimeout(() => {
|
|
120
|
+
this.flushSessionBuffer();
|
|
121
|
+
}, this.config.bufferDelay * 1000);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 将 session 缓冲区的任务转移到处理队列
|
|
125
|
+
private flushSessionBuffer() {
|
|
126
|
+
if (this.sessionQueue.length === 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.logInfo(`Session缓冲时间结束,开始处理 ${this.sessionQueue.length} 个消息`);
|
|
131
|
+
|
|
132
|
+
// 启动 session 队列处理
|
|
133
|
+
if (!this.isProcessingSession) {
|
|
134
|
+
this.processSessionQueue();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 处理 session 队列中的任务
|
|
139
|
+
private async processSessionQueue() {
|
|
140
|
+
if (this.isProcessingSession || this.sessionQueue.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.isProcessingSession = true;
|
|
145
|
+
this.logInfo(`开始处理Session队列,总任务数: ${this.sessionQueue.length}`);
|
|
146
|
+
|
|
147
|
+
while (this.sessionQueue.length > 0) {
|
|
148
|
+
const task = this.sessionQueue.shift();
|
|
149
|
+
this.logInfo(`处理Session (剩余: ${this.sessionQueue.length})`);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await this.processSessionTask(task.session, task.sessioncontent);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.error('处理Session任务时发生错误:', error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.isProcessingSession = false;
|
|
159
|
+
this.logInfo('Session队列处理完成');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 实际处理单个 session 任务
|
|
163
|
+
private async processSessionTask(session: Session, sessioncontent: string) {
|
|
164
|
+
this.logInfo(`[队列] 开始处理消息: ${sessioncontent.substring(0, 50)}...`);
|
|
165
|
+
|
|
166
|
+
const links = await this.isProcessLinks(sessioncontent);
|
|
167
|
+
if (!links) {
|
|
168
|
+
this.logInfo(`[队列] 未检测到链接`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.logInfo(`[队列] 检测到 ${links.length} 个链接`);
|
|
173
|
+
|
|
174
|
+
// 逐个处理链接
|
|
175
|
+
for (let i = 0; i < links.length; i++) {
|
|
176
|
+
const link = links[i];
|
|
177
|
+
this.logInfo(`[队列] 处理第 ${i + 1}/${links.length} 个链接`);
|
|
178
|
+
|
|
179
|
+
const ret = await this.extractLinks(session, [link]);
|
|
180
|
+
if (ret && !this.isLinkProcessedRecently(ret, session.channelId)) {
|
|
181
|
+
this.logInfo(`[队列] 开始下载视频`);
|
|
182
|
+
// 直接处理,不再使用视频级别的缓冲
|
|
183
|
+
await this.processVideoTask(session, ret, { video: true });
|
|
184
|
+
this.logInfo(`[队列] 视频处理完成`);
|
|
185
|
+
} else {
|
|
186
|
+
this.logInfo(`[队列] 链接已处理过,跳过`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.logInfo(`[队列] Session 处理完成`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 添加任务到缓冲区(已废弃,保留兼容性)
|
|
76
194
|
public async processVideoFromLink(session: Session, ret: string, options: { video?: boolean; audio?: boolean; link?: boolean } = { video: true }) {
|
|
195
|
+
// 将任务加入缓冲队列
|
|
196
|
+
this.bufferQueue.push({ session, ret, options, timestamp: Date.now() });
|
|
197
|
+
this.logInfo(`收到解析请求,缓冲区任务数: ${this.bufferQueue.length}`);
|
|
198
|
+
|
|
199
|
+
// 清除之前的定时器
|
|
200
|
+
if (this.bufferTimer) {
|
|
201
|
+
clearTimeout(this.bufferTimer);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 设置新的定时器,等待配置的延迟时间后处理
|
|
205
|
+
this.bufferTimer = setTimeout(() => {
|
|
206
|
+
this.flushBuffer();
|
|
207
|
+
}, this.config.bufferDelay * 1000);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 将缓冲区的任务转移到处理队列
|
|
211
|
+
private flushBuffer() {
|
|
212
|
+
if (this.bufferQueue.length === 0) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.logInfo(`缓冲时间结束,将 ${this.bufferQueue.length} 个任务加入处理队列`);
|
|
217
|
+
|
|
218
|
+
// 将缓冲队列的任务转移到处理队列
|
|
219
|
+
while (this.bufferQueue.length > 0) {
|
|
220
|
+
const task = this.bufferQueue.shift();
|
|
221
|
+
this.processingQueue.push({
|
|
222
|
+
session: task.session,
|
|
223
|
+
ret: task.ret,
|
|
224
|
+
options: task.options
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 启动队列处理
|
|
229
|
+
if (!this.isProcessing) {
|
|
230
|
+
this.processQueue();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 处理队列中的任务
|
|
235
|
+
private async processQueue() {
|
|
236
|
+
if (this.isProcessing || this.processingQueue.length === 0) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.isProcessing = true;
|
|
241
|
+
this.logInfo(`开始处理队列,总任务数: ${this.processingQueue.length}`);
|
|
242
|
+
|
|
243
|
+
while (this.processingQueue.length > 0) {
|
|
244
|
+
const task = this.processingQueue.shift();
|
|
245
|
+
this.logInfo(`处理任务 (剩余: ${this.processingQueue.length})`);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await this.processVideoTask(task.session, task.ret, task.options);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
this.logger.error('处理视频任务时发生错误:', error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.isProcessing = false;
|
|
255
|
+
this.logInfo('队列处理完成');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 实际处理单个视频任务
|
|
259
|
+
private async processVideoTask(session: Session, ret: string, options: { video?: boolean; audio?: boolean; link?: boolean } = { video: true }) {
|
|
77
260
|
const lastretUrl = this.extractLastUrl(ret);
|
|
261
|
+
this.logInfo(`处理视频: ${lastretUrl}`);
|
|
78
262
|
|
|
79
263
|
let waitTipMsgId: string = null;
|
|
80
264
|
// 等待提示语单独发送
|
|
@@ -120,7 +304,7 @@ export class BilibiliParser {
|
|
|
120
304
|
|
|
121
305
|
// 视频/链接解析
|
|
122
306
|
if (this.config.videoParseComponents.length > 0) {
|
|
123
|
-
const fullAPIurl = `http://api.xingzhige.
|
|
307
|
+
const fullAPIurl = `http://api.xingzhige.com/API/b_parse/?url=${encodeURIComponent(lastretUrl)}`;
|
|
124
308
|
|
|
125
309
|
try {
|
|
126
310
|
const responseData: any = await this.ctx.http.get(fullAPIurl);
|
|
@@ -130,8 +314,6 @@ export class BilibiliParser {
|
|
|
130
314
|
const bilibiliUrl = `https://api.bilibili.com/x/player/playurl?fnval=80&cid=${cid}&bvid=${bvid}`;
|
|
131
315
|
const playData: any = await this.ctx.http.get(bilibiliUrl);
|
|
132
316
|
|
|
133
|
-
this.logInfo(bilibiliUrl);
|
|
134
|
-
|
|
135
317
|
if (playData.code === 0 && playData.data && playData.data.dash && playData.data.dash.duration) {
|
|
136
318
|
const videoDurationSeconds = playData.data.dash.duration;
|
|
137
319
|
const videoDurationMinutes = videoDurationSeconds / 60;
|
|
@@ -189,32 +371,55 @@ export class BilibiliParser {
|
|
|
189
371
|
}
|
|
190
372
|
} else {
|
|
191
373
|
// 视频时长在允许范围内,处理视频
|
|
192
|
-
let videoData = video.url;
|
|
193
|
-
this.logInfo(videoData);
|
|
374
|
+
let videoData = video.url;
|
|
194
375
|
|
|
195
376
|
if (this.config.filebuffer) {
|
|
196
377
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
378
|
+
// 使用 Node.js 原生 fetch 下载视频
|
|
379
|
+
const response = await fetch(video.url, {
|
|
380
|
+
headers: {
|
|
381
|
+
'User-Agent': this.config.userAgent,
|
|
382
|
+
'Referer': 'https://www.bilibili.com/'
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
throw new Error(`HTTP ${response.status}`);
|
|
388
|
+
}
|
|
199
389
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
390
|
+
// 检查文件大小
|
|
391
|
+
const contentLength = response.headers.get('content-length');
|
|
392
|
+
const fileSizeMB = contentLength ? parseInt(contentLength) / 1024 / 1024 : 0;
|
|
393
|
+
this.logInfo(`[下载] 视频大小: ${fileSizeMB.toFixed(2)}MB`);
|
|
204
394
|
|
|
205
|
-
|
|
206
|
-
|
|
395
|
+
// 检查是否超过配置的最大大小
|
|
396
|
+
const maxSize = this.config.maxFileSizeMB;
|
|
397
|
+
this.logInfo(`[下载] 配置的最大大小: ${maxSize}MB`);
|
|
207
398
|
|
|
208
|
-
|
|
399
|
+
if (maxSize > 0 && fileSizeMB > maxSize) {
|
|
400
|
+
this.logger.warn(`[下载] 文件过大 (${fileSizeMB.toFixed(2)}MB > ${maxSize}MB),使用直链模式`);
|
|
401
|
+
// 不下载,使用原始URL
|
|
402
|
+
videoData = video.url;
|
|
403
|
+
} else {
|
|
404
|
+
this.logInfo(`[下载] 开始下载并转换为Base64...`);
|
|
405
|
+
|
|
406
|
+
// 获取 MIME 类型
|
|
407
|
+
const contentType = response.headers.get('content-type');
|
|
408
|
+
const mimeType = contentType ? contentType.split(';')[0].trim() : 'video/mp4';
|
|
409
|
+
|
|
410
|
+
this.logInfo(`[下载] 读取响应体...`);
|
|
411
|
+
// 读取响应体并转换
|
|
412
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
413
|
+
this.logInfo(`[下载] 创建Buffer...`);
|
|
414
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
415
|
+
this.logInfo(`[下载] 转换为Base64...`);
|
|
209
416
|
const base64Data = buffer.toString('base64');
|
|
210
417
|
videoData = `data:${mimeType};base64,${base64Data}`;
|
|
211
418
|
|
|
212
|
-
this.logInfo(
|
|
213
|
-
} else {
|
|
214
|
-
this.logInfo("文件数据无效,使用原始URL");
|
|
419
|
+
this.logInfo(`[下载] 视频下载完成,已转换为Base64`);
|
|
215
420
|
}
|
|
216
421
|
} catch (error) {
|
|
217
|
-
this.logger.error("
|
|
422
|
+
this.logger.error("下载视频失败:", error);
|
|
218
423
|
// 出错时继续使用原始URL
|
|
219
424
|
}
|
|
220
425
|
}
|
|
@@ -227,7 +432,7 @@ export class BilibiliParser {
|
|
|
227
432
|
videoElements.push(h.audio(videoData));
|
|
228
433
|
} else {
|
|
229
434
|
if (this.config.videoParseComponents.includes('log')) {
|
|
230
|
-
this.
|
|
435
|
+
this.logInfo(video.url);
|
|
231
436
|
}
|
|
232
437
|
if (this.config.videoParseComponents.includes('link')) {
|
|
233
438
|
videoElements.push(h.text(video.url));
|
|
@@ -533,4 +738,4 @@ export class BilibiliParser {
|
|
|
533
738
|
else
|
|
534
739
|
return null;
|
|
535
740
|
}
|
|
536
|
-
}
|
|
741
|
+
}
|