koishi-plugin-bilibili-videolink-analysis 1.3.2 → 1.3.3

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/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.logger.info(`[队列] 开始处理消息: ${sessioncontent.substring(0, 50)}...`);
165
+
166
+ const links = await this.isProcessLinks(sessioncontent);
167
+ if (!links) {
168
+ this.logger.info(`[队列] 未检测到链接`);
169
+ return;
170
+ }
171
+
172
+ this.logger.info(`[队列] 检测到 ${links.length} 个链接`);
173
+
174
+ // 逐个处理链接
175
+ for (let i = 0; i < links.length; i++) {
176
+ const link = links[i];
177
+ this.logger.info(`[队列] 处理第 ${i + 1}/${links.length} 个链接`);
178
+
179
+ const ret = await this.extractLinks(session, [link]);
180
+ if (ret && !this.isLinkProcessedRecently(ret, session.channelId)) {
181
+ this.logger.info(`[队列] 开始下载视频`);
182
+ // 直接处理,不再使用视频级别的缓冲
183
+ await this.processVideoTask(session, ret, { video: true });
184
+ this.logger.info(`[队列] 视频处理完成`);
185
+ } else {
186
+ this.logger.info(`[队列] 链接已处理过,跳过`);
187
+ }
188
+ }
189
+
190
+ this.logger.info(`[队列] 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
  // 等待提示语单独发送
@@ -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; // 使用新变量名,避免覆盖原始URL
193
- this.logInfo(videoData);
374
+ let videoData = video.url;
194
375
 
195
376
  if (this.config.filebuffer) {
196
377
  try {
197
- const videoFileBuffer: any = await this.ctx.http.file(video.url);
198
- this.logInfo(videoFileBuffer);
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
- if (videoFileBuffer && videoFileBuffer.data) {
202
- // 将ArrayBuffer转换为Buffer
203
- const buffer = Buffer.from(videoFileBuffer.data);
390
+ // 检查文件大小
391
+ const contentLength = response.headers.get('content-length');
392
+ const fileSizeMB = contentLength ? parseInt(contentLength) / 1024 / 1024 : 0;
393
+ this.logger.info(`[下载] 视频大小: ${fileSizeMB.toFixed(2)}MB`);
204
394
 
205
- // 获取MIME类型
206
- const mimeType = videoFileBuffer.type || videoFileBuffer.mime || 'video/mp4';
395
+ // 检查是否超过配置的最大大小
396
+ const maxSize = this.config.maxFileSizeMB;
397
+ this.logger.info(`[下载] 配置的最大大小: ${maxSize}MB`);
207
398
 
208
- // 创建data URI
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.logger.info(`[下载] 开始下载并转换为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.logger.info(`[下载] 读取响应体...`);
411
+ // 读取响应体并转换
412
+ const arrayBuffer = await response.arrayBuffer();
413
+ this.logger.info(`[下载] 创建Buffer...`);
414
+ const buffer = Buffer.from(arrayBuffer);
415
+ this.logger.info(`[下载] 转换为Base64...`);
209
416
  const base64Data = buffer.toString('base64');
210
417
  videoData = `data:${mimeType};base64,${base64Data}`;
211
418
 
212
- this.logInfo("成功使用 ctx.http.file 将视频URL 转换为data URI格式");
213
- } else {
214
- this.logInfo("文件数据无效,使用原始URL");
419
+ this.logger.info(`[下载] 视频下载完成,已转换为Base64`);
215
420
  }
216
421
  } catch (error) {
217
- this.logger.error("获取视频文件失败:", error);
422
+ this.logger.error("下载视频失败:", error);
218
423
  // 出错时继续使用原始URL
219
424
  }
220
425
  }
@@ -533,4 +738,4 @@ export class BilibiliParser {
533
738
  else
534
739
  return null;
535
740
  }
536
- }
741
+ }