koishi-plugin-bilibili-videolink-analysis 1.1.2 → 1.1.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.
Files changed (2) hide show
  1. package/lib/index.js +200 -252
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -54,6 +54,7 @@ exports.usage = `
54
54
 
55
55
  exports.Config = Schema.intersect([
56
56
  Schema.object({
57
+ demand: Schema.boolean().default(true).description("开启点播指令功能"),
57
58
  timeout: Schema.number().role('slider').min(1).max(300).step(1).default(60).description('指定播放视频的输入时限。`单位 秒`'),
58
59
  point: Schema.tuple([Number, Number]).description('序号标注位置。分别表示`距离顶部 距离左侧`的百分比').default([50, 50]),
59
60
  enable: Schema.boolean().description('是否开启自动解析`选择对应视频 会自动解析视频内容`').default(true),
@@ -126,95 +127,232 @@ function apply(ctx, config) {
126
127
  }
127
128
  return next();
128
129
  });
130
+ if (config.demand) {
131
+ ctx.command('B站点播')
132
+ ctx.command('B站点播/退出登录', '退出B站账号')
133
+ .action(async ({ session }) => {
134
+ const page = await ctx.puppeteer.page();
135
+ await page.goto('https://www.bilibili.com/', { waitUntil: 'networkidle2' });
129
136
 
130
- ctx.command('B站点播')
131
- ctx.command('B站点播/退出登录', '退出B站账号')
132
- .action(async ({ session }) => {
133
- const page = await ctx.puppeteer.page();
134
- await page.goto('https://www.bilibili.com/', { waitUntil: 'networkidle2' });
135
-
136
- const loginButtonSelector = '.right-entry__outside.go-login-btn';
137
- const isLoggedIn = await page.$(loginButtonSelector) === null;
137
+ const loginButtonSelector = '.right-entry__outside.go-login-btn';
138
+ const isLoggedIn = await page.$(loginButtonSelector) === null;
138
139
 
139
- if (!isLoggedIn) {
140
- await page.close();
141
- await session.send(h.text('您尚未登录。'))
142
- return;
143
- }
140
+ if (!isLoggedIn) {
141
+ await page.close();
142
+ await session.send(h.text('您尚未登录。'))
143
+ return;
144
+ }
144
145
 
145
- const avatarLinkSelector = '.header-entry-mini';
146
- const logoutButtonSelector = '.logout-item';
146
+ const avatarLinkSelector = '.header-entry-mini';
147
+ const logoutButtonSelector = '.logout-item';
147
148
 
148
- try {
149
- const avatarElement = await page.$(avatarLinkSelector);
150
- if (avatarElement) {
151
- await avatarElement.hover();
152
- await page.waitForSelector(logoutButtonSelector, { visible: true });
149
+ try {
150
+ const avatarElement = await page.$(avatarLinkSelector);
151
+ if (avatarElement) {
152
+ await avatarElement.hover();
153
+ await page.waitForSelector(logoutButtonSelector, { visible: true });
153
154
 
154
- await page.click(logoutButtonSelector);
155
+ await page.click(logoutButtonSelector);
155
156
 
156
- await new Promise(resolve => setTimeout(resolve, 1000));
157
+ await new Promise(resolve => setTimeout(resolve, 1000));
157
158
 
159
+ await page.close();
160
+ await session.send(h.text('已成功退出登录。'))
161
+ return;
162
+ } else {
163
+ await page.close();
164
+ await session.send(h.text('找不到用户头像,无法退出登录。'))
165
+ return;
166
+ }
167
+ } catch (error) {
158
168
  await page.close();
159
- await session.send(h.text('已成功退出登录。'))
169
+ logger.error('Error during logout:', error);
170
+ await session.send(h.text('退出登录时出错。'))
160
171
  return;
161
- } else {
172
+ }
173
+ });
174
+
175
+ ctx.command('B站点播/登录', '登录B站账号')
176
+ .alias("登陆")
177
+ .action(async ({ session }) => {
178
+ const page = await ctx.puppeteer.page();
179
+ await page.goto('https://www.bilibili.com/', { waitUntil: 'networkidle2' });
180
+
181
+ const loginButtonSelector = '.right-entry__outside.go-login-btn';
182
+ const isLoggedIn = await page.$(loginButtonSelector) === null;
183
+
184
+ if (isLoggedIn) {
162
185
  await page.close();
163
- await session.send(h.text('找不到用户头像,无法退出登录。'))
186
+ await session.send(h.text('您已经登录了。'))
164
187
  return;
165
188
  }
166
- } catch (error) {
167
- await page.close();
168
- logger.error('Error during logout:', error);
169
- await session.send(h.text('退出登录时出错。'))
170
- return;
171
- }
172
- });
173
189
 
174
- ctx.command('B站点播/登录', '登录B站账号')
175
- .alias("登陆")
176
- .action(async ({ session }) => {
177
- const page = await ctx.puppeteer.page();
178
- await page.goto('https://www.bilibili.com/', { waitUntil: 'networkidle2' });
190
+ await page.click(loginButtonSelector);
191
+
192
+ const qrCodeSelector = '.login-scan-box img';
193
+ await page.waitForSelector(qrCodeSelector);
194
+ const qrCodeUrl = await page.$eval(qrCodeSelector, img => img.src);
195
+
196
+ await session.send(h.image(qrCodeUrl, 'image/png'));
197
+ await session.send('请扫描二维码进行登录。');
198
+
199
+ let attempts = 0;
200
+ let loginSuccessful = false;
201
+
202
+ while (attempts < 6) {
203
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Wait
204
+ const isStillLoggedIn = await page.$(loginButtonSelector) === null;
205
+
206
+ if (isStillLoggedIn) {
207
+ loginSuccessful = true;
208
+ break;
209
+ }
179
210
 
180
- const loginButtonSelector = '.right-entry__outside.go-login-btn';
181
- const isLoggedIn = await page.$(loginButtonSelector) === null;
211
+ attempts++;
212
+ }
182
213
 
183
- if (isLoggedIn) {
184
214
  await page.close();
185
- await session.send(h.text('您已经登录了。'))
215
+ await session.send(h.text(loginSuccessful ? '登录成功!' : '登录失败,请重试。'))
186
216
  return;
187
- }
217
+ });
218
+
219
+ ctx.command('B站点播/点播 [keyword]', '点播B站视频')
220
+ .option('video', '-v 解析返回视频')
221
+ .option('audio', '-a 解析返回语音')
222
+ .option('link', '-l 解析返回链接')
223
+ .option('page', '-p <page:number> 指定页数', { fallback: '1' })
224
+ .example('点播 遠い空へ -v')
225
+ .action(async ({ options, session }, keyword) => {
226
+ if (!keyword) {
227
+ await session.execute('点播 -h')
228
+ await session.send(h.text('没输入点播内容'))
229
+ return
230
+ }
188
231
 
189
- await page.click(loginButtonSelector);
190
232
 
191
- const qrCodeSelector = '.login-scan-box img';
192
- await page.waitForSelector(qrCodeSelector);
193
- const qrCodeUrl = await page.$eval(qrCodeSelector, img => img.src);
233
+ const url = `https://search.bilibili.com/video?keyword=${encodeURIComponent(keyword)}&page=${options.page}&o=30`
234
+ const page = await ctx.puppeteer.page()
235
+
236
+ await page.goto(url, {
237
+ waitUntil: 'networkidle2'
238
+ })
239
+
240
+ await page.addStyleTag({
241
+ content: `
242
+ div.bili-header,
243
+ div.login-tip,
244
+ div.v-popover,
245
+ div.right-entry__outside {
246
+ display: none !important;
247
+ }
248
+ `
249
+ })
250
+ // 获取视频列表并为每个视频元素添加序号
251
+ const videos = await page.evaluate((point) => {
252
+ const items = Array.from(document.querySelectorAll('.video-list-item:not([style*="display: none"])'))
253
+ return items.map((item, index) => {
254
+ const link = item.querySelector('a')
255
+ const href = link?.getAttribute('href') || ''
256
+ const idMatch = href.match(/\/video\/(BV\w+)\//)
257
+ const id = idMatch ? idMatch[1] : ''
194
258
 
195
- await session.send(h.image(qrCodeUrl, 'image/png'));
196
- await session.send('请扫描二维码进行登录。');
259
+ if (!id) {
260
+ // 如果没有提取到视频ID,隐藏这个元素
261
+ //const htmlElement = item as HTMLElement
262
+ const htmlElement = item
263
+ htmlElement.style.display = 'none'
264
+ } else {
265
+ // 创建一个包含序号的元素,并将其插入到视频元素的正中央
266
+ const overlay = document.createElement('div')
267
+ overlay.style.position = 'absolute'
268
+ overlay.style.top = `${point[0]}%`
269
+ overlay.style.left = `${point[1]}%`
270
+ overlay.style.transform = 'translate(-50%, -50%)'
271
+ overlay.style.fontSize = '48px'
272
+ overlay.style.fontWeight = 'bold'
273
+ overlay.style.color = 'black'
274
+ overlay.style.zIndex = '10'
275
+ overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.7)' // 半透明白色背景,确保数字清晰可见
276
+ overlay.style.padding = '10px'
277
+ overlay.style.borderRadius = '8px'
278
+ overlay.textContent = `${index + 1}` // 序号
279
+
280
+ // 确保父元素有 `position: relative` 以正确定位
281
+ //const videoElement = item as HTMLElement
282
+ const videoElement = item
283
+ videoElement.style.position = 'relative'
284
+ videoElement.appendChild(overlay)
285
+ }
197
286
 
198
- let attempts = 0;
199
- let loginSuccessful = false;
287
+ return { id }
288
+ }).filter(video => video.id)
289
+ }, config.point) // 传递配置的 point 参数
200
290
 
201
- while (attempts < 6) {
202
- await new Promise(resolve => setTimeout(resolve, 5000)); // Wait
203
- const isStillLoggedIn = await page.$(loginButtonSelector) === null;
291
+ // 如果开启了日志调试模式,打印获取到的视频信息
292
+ if (config.loggerinfo) {
293
+ ctx.logger.info(options)
294
+ ctx.logger.info(`共找到 ${videos.length} 个视频:`)
295
+ videos.forEach((video, index) => {
296
+ ctx.logger.info(`序号 ${index + 1}: ID - ${video.id}`)
297
+ })
298
+ }
204
299
 
205
- if (isStillLoggedIn) {
206
- loginSuccessful = true;
207
- break;
300
+ if (videos.length === 0) {
301
+ await page.close()
302
+ await session.send(h.text('未找到相关视频。'))
303
+ return
208
304
  }
209
305
 
210
- attempts++;
211
- }
306
+ // 动态调整窗口大小以适应视频数量
307
+ const viewportHeight = 200 + videos.length * 100
308
+ await page.setViewport({
309
+ width: 1440,
310
+ height: viewportHeight
311
+ })
312
+ let msg;
313
+ // 截图
314
+ const videoListElement = await page.$('.video-list.row')
315
+ if (videoListElement) {
316
+ const imgBuf = await videoListElement.screenshot({
317
+ captureBeyondViewport: false
318
+ })
319
+ msg = h.image(imgBuf, 'image/png')
320
+ }
212
321
 
213
- await page.close();
214
- await session.send(h.text(loginSuccessful ? '登录成功!' : '登录失败,请重试。'))
215
- return;
216
- });
322
+ await page.close()
217
323
 
324
+ // 发送截图
325
+ await session.send(msg)
326
+
327
+ // 提示用户输入
328
+ await session.send(`请选择视频的序号:`)
329
+
330
+ // 等待用户输入
331
+ const userChoice = await session.prompt(config.timeout * 1000)
332
+ const choiceIndex = parseInt(userChoice) - 1
333
+ if (isNaN(choiceIndex) || choiceIndex < 0 || choiceIndex >= videos.length) {
334
+ await session.send(h.text('输入无效,请输入正确的序号。'))
335
+ return
336
+ }
337
+
338
+ // 返回用户选择的视频ID
339
+ const chosenVideo = videos[choiceIndex]
340
+
341
+ // 如果开启了日志调试模式,打印用户选择的视频信息
342
+ if (config.loggerinfo) {
343
+ ctx.logger.info(`渲染序号设置\noverlay.style.top = ${config.point[0]}% \noverlay.style.left = ${config.point[1]}%`)
344
+ ctx.logger.info(`用户选择了序号 ${choiceIndex + 1}: ID - ${chosenVideo.id}`)
345
+ }
346
+
347
+ if (config.enable) { // 开启自动解析了
348
+ session.content = `https://www.bilibili.com/video/${chosenVideo.id}`
349
+ const ret = await extractLinks(session, config, ctx, lastProcessedUrls, logger); // 提取链接
350
+ if (ret && !isLinkProcessedRecently(ret, lastProcessedUrls, config, logger)) {
351
+ await processVideoFromLink(session, config, ctx, lastProcessedUrls, logger, ret, options); // 解析视频并返回
352
+ }
353
+ }
354
+ })
355
+ }
218
356
  if (config.loggerinfo) {
219
357
  ctx.command('B站点播/调试点播 [keyword]', '调试时点播B站视频')
220
358
  .option('video', '-v 解析返回视频')
@@ -322,196 +460,6 @@ function apply(ctx, config) {
322
460
  }
323
461
  });
324
462
  }
325
- ctx.command('B站点播/点播 [keyword]', '点播B站视频')
326
- .option('video', '-v 解析返回视频')
327
- .option('audio', '-a 解析返回语音')
328
- .option('link', '-l 解析返回链接')
329
- .option('page', '-p <page:number> 指定页数', { fallback: '1' })
330
- .example('点播 遠い空へ -v')
331
- .action(async ({ options, session }, keyword) => {
332
- if (!keyword) {
333
- await session.execute('点播 -h')
334
- await session.send(h.text('没输入点播内容'))
335
- return
336
- }
337
-
338
-
339
- const url = `https://search.bilibili.com/video?keyword=${encodeURIComponent(keyword)}&page=${options.page}&o=30`
340
- const page = await ctx.puppeteer.page()
341
-
342
- await page.goto(url, {
343
- waitUntil: 'networkidle2'
344
- })
345
-
346
- await page.addStyleTag({
347
- content: `
348
- div.bili-header,
349
- div.login-tip,
350
- div.v-popover,
351
- div.right-entry__outside {
352
- display: none !important;
353
- }
354
- `
355
- })
356
- // 获取视频列表并为每个视频元素添加序号
357
- const videos = await page.evaluate((point) => {
358
- const items = Array.from(document.querySelectorAll('.video-list-item:not([style*="display: none"])'))
359
- return items.map((item, index) => {
360
- const link = item.querySelector('a')
361
- const href = link?.getAttribute('href') || ''
362
- const idMatch = href.match(/\/video\/(BV\w+)\//)
363
- const id = idMatch ? idMatch[1] : ''
364
-
365
- if (!id) {
366
- // 如果没有提取到视频ID,隐藏这个元素
367
- //const htmlElement = item as HTMLElement
368
- const htmlElement = item
369
- htmlElement.style.display = 'none'
370
- } else {
371
- // 创建一个包含序号的元素,并将其插入到视频元素的正中央
372
- const overlay = document.createElement('div')
373
- overlay.style.position = 'absolute'
374
- overlay.style.top = `${point[0]}%`
375
- overlay.style.left = `${point[1]}%`
376
- overlay.style.transform = 'translate(-50%, -50%)'
377
- overlay.style.fontSize = '48px'
378
- overlay.style.fontWeight = 'bold'
379
- overlay.style.color = 'black'
380
- overlay.style.zIndex = '10'
381
- overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.7)' // 半透明白色背景,确保数字清晰可见
382
- overlay.style.padding = '10px'
383
- overlay.style.borderRadius = '8px'
384
- overlay.textContent = `${index + 1}` // 序号
385
-
386
- // 确保父元素有 `position: relative` 以正确定位
387
- //const videoElement = item as HTMLElement
388
- const videoElement = item
389
- videoElement.style.position = 'relative'
390
- videoElement.appendChild(overlay)
391
- }
392
-
393
- return { id }
394
- }).filter(video => video.id)
395
- }, config.point) // 传递配置的 point 参数
396
-
397
- // 如果开启了日志调试模式,打印获取到的视频信息
398
- if (config.loggerinfo) {
399
- ctx.logger.info(options)
400
- ctx.logger.info(`共找到 ${videos.length} 个视频:`)
401
- videos.forEach((video, index) => {
402
- ctx.logger.info(`序号 ${index + 1}: ID - ${video.id}`)
403
- })
404
- }
405
-
406
- if (videos.length === 0) {
407
- await page.close()
408
- await session.send(h.text('未找到相关视频。'))
409
- return
410
- }
411
-
412
- // 动态调整窗口大小以适应视频数量
413
- const viewportHeight = 200 + videos.length * 100
414
- await page.setViewport({
415
- width: 1440,
416
- height: viewportHeight
417
- })
418
- let msg;
419
- // 截图
420
- const videoListElement = await page.$('.video-list.row')
421
- if (videoListElement) {
422
- const imgBuf = await videoListElement.screenshot({
423
- captureBeyondViewport: false
424
- })
425
- msg = h.image(imgBuf, 'image/png')
426
- }
427
-
428
- await page.close()
429
-
430
- // 发送截图
431
- await session.send(msg)
432
-
433
- // 提示用户输入
434
- await session.send(`请选择视频的序号:`)
435
-
436
- // 等待用户输入
437
- const userChoice = await session.prompt(config.timeout * 1000)
438
- const choiceIndex = parseInt(userChoice) - 1
439
- if (isNaN(choiceIndex) || choiceIndex < 0 || choiceIndex >= videos.length) {
440
- await session.send(h.text('输入无效,请输入正确的序号。'))
441
- return
442
- }
443
-
444
- // 返回用户选择的视频ID
445
- const chosenVideo = videos[choiceIndex]
446
-
447
- // 如果开启了日志调试模式,打印用户选择的视频信息
448
- if (config.loggerinfo) {
449
- ctx.logger.info(`渲染序号设置\noverlay.style.top = ${config.point[0]}% \noverlay.style.left = ${config.point[1]}%`)
450
- ctx.logger.info(`用户选择了序号 ${choiceIndex + 1}: ID - ${chosenVideo.id}`)
451
- }
452
-
453
- if (config.enable) { // 开启自动解析了
454
- session.content = `https://www.bilibili.com/video/${chosenVideo.id}`
455
- const ret = await extractLinks(session, config, ctx, lastProcessedUrls, logger); // 提取链接
456
- if (ret && !isLinkProcessedRecently(ret, lastProcessedUrls, config, logger)) {
457
- await processVideoFromLink(session, config, ctx, lastProcessedUrls, logger, ret, options); // 解析视频并返回
458
- }
459
- }
460
- })
461
- /*async function handleBilibiliMedia(config, session, lastretUrl) {
462
- const fullAPIurl = `https://api.xingzhige.com/API/b_parse/?url=${encodeURIComponent(lastretUrl)}`;
463
-
464
- try {
465
- // 发起请求,解析 Bilibili 视频信息
466
- const responseData = await ctx.http.get(fullAPIurl);
467
-
468
- // 检查返回状态码是否为0且为视频内容
469
- if (responseData.code === 0 && responseData.msg === "video" && responseData.data) {
470
- const { bvid, cid } = responseData.data;
471
-
472
- // 请求 Bilibili 播放 URL,获取视频信息
473
- const bilibiliUrl = `https://api.bilibili.com/x/player/playurl?fnval=80&cid=${cid}&bvid=${bvid}`;
474
- const playData = await ctx.http.get(bilibiliUrl);
475
- //////
476
- ctx.logger.info(bilibiliUrl)
477
- // 检查返回的状态码是否为0,表示请求成功
478
- if (playData.code === 0 && playData.data && playData.data.dash.duration) {
479
- const videoDurationSeconds = playData.data.dash.duration; // 视频时长,单位为秒
480
- const videoDurationMinutes = videoDurationSeconds / 60; // 转换为分钟
481
-
482
- // 检查视频时长是否超过配置的最大允许时长
483
- if (videoDurationMinutes > config.Maximumduration) {
484
- // 视频时长超过最大限制,返回提示
485
- if (config.Maximumduration_tip !== '不返回文字提示') {
486
- await session.send(config.Maximumduration_tip)
487
- return next()
488
- } else {
489
- return null; // 不返回提示信息
490
- }
491
- }
492
-
493
- // 视频时长符合要求,继续解析并返回视频直链
494
- const videoUrl = responseData.data.video.url;
495
- //////
496
- ctx.logger.info(videoUrl)
497
- if (videoUrl) {
498
- return videoUrl; // 返回视频直链
499
- } else {
500
- throw new Error("解析视频直链失败");
501
- }
502
- } else {
503
- throw new Error("获取播放数据失败");
504
- }
505
- } else {
506
- throw new Error("解析视频信息失败或非视频类型内容");
507
- }
508
- } catch (error) {
509
- logger.error("请求解析 API 失败或处理出错:", error);
510
- return null;
511
- }
512
- }*/
513
-
514
-
515
463
  //判断是否需要解析
516
464
  async function isProcessLinks(session, config, ctx, lastProcessedUrls, logger) {
517
465
  // 解析内容中的链接
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "koishi-plugin-bilibili-videolink-analysis",
3
3
  "description": "[<ruby>Bilibili视频解析<rp>(</rp><rt>点我查看食用方法</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/koishi-plugin-bilibili-videolink-analysis)解析B站链接(支持小程序卡片)支持搜索点播功能!灵感来自完美的 [bili-parser](/market?keyword=bili-parser) !",
4
4
  "license": "MIT",
5
- "version": "1.1.2",
5
+ "version": "1.1.3",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",
8
8
  "files": [