koishi-plugin-bilibili-videolink-analysis 1.1.2 → 1.1.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.
Files changed (3) hide show
  1. package/LICENSE.txt +21 -0
  2. package/lib/index.js +199 -252
  3. package/package.json +1 -1
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 shangxue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,231 @@ function apply(ctx, config) {
126
127
  }
127
128
  return next();
128
129
  });
130
+ if (config.demand) {
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' });
129
135
 
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;
136
+ const loginButtonSelector = '.right-entry__outside.go-login-btn';
137
+ const isLoggedIn = await page.$(loginButtonSelector) === null;
138
138
 
139
- if (!isLoggedIn) {
140
- await page.close();
141
- await session.send(h.text('您尚未登录。'))
142
- return;
143
- }
139
+ if (!isLoggedIn) {
140
+ await page.close();
141
+ await session.send(h.text('您尚未登录。'))
142
+ return;
143
+ }
144
144
 
145
- const avatarLinkSelector = '.header-entry-mini';
146
- const logoutButtonSelector = '.logout-item';
145
+ const avatarLinkSelector = '.header-entry-mini';
146
+ const logoutButtonSelector = '.logout-item';
147
147
 
148
- try {
149
- const avatarElement = await page.$(avatarLinkSelector);
150
- if (avatarElement) {
151
- await avatarElement.hover();
152
- await page.waitForSelector(logoutButtonSelector, { visible: true });
148
+ try {
149
+ const avatarElement = await page.$(avatarLinkSelector);
150
+ if (avatarElement) {
151
+ await avatarElement.hover();
152
+ await page.waitForSelector(logoutButtonSelector, { visible: true });
153
153
 
154
- await page.click(logoutButtonSelector);
154
+ await page.click(logoutButtonSelector);
155
155
 
156
- await new Promise(resolve => setTimeout(resolve, 1000));
156
+ await new Promise(resolve => setTimeout(resolve, 1000));
157
157
 
158
+ await page.close();
159
+ await session.send(h.text('已成功退出登录。'))
160
+ return;
161
+ } else {
162
+ await page.close();
163
+ await session.send(h.text('找不到用户头像,无法退出登录。'))
164
+ return;
165
+ }
166
+ } catch (error) {
158
167
  await page.close();
159
- await session.send(h.text('已成功退出登录。'))
168
+ logger.error('Error during logout:', error);
169
+ await session.send(h.text('退出登录时出错。'))
160
170
  return;
161
- } else {
171
+ }
172
+ });
173
+
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' });
179
+
180
+ const loginButtonSelector = '.right-entry__outside.go-login-btn';
181
+ const isLoggedIn = await page.$(loginButtonSelector) === null;
182
+
183
+ if (isLoggedIn) {
162
184
  await page.close();
163
- await session.send(h.text('找不到用户头像,无法退出登录。'))
185
+ await session.send(h.text('您已经登录了。'))
164
186
  return;
165
187
  }
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
188
 
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' });
189
+ await page.click(loginButtonSelector);
190
+
191
+ const qrCodeSelector = '.login-scan-box img';
192
+ await page.waitForSelector(qrCodeSelector);
193
+ const qrCodeUrl = await page.$eval(qrCodeSelector, img => img.src);
194
+
195
+ await session.send(h.image(qrCodeUrl, 'image/png'));
196
+ await session.send('请扫描二维码进行登录。');
197
+
198
+ let attempts = 0;
199
+ let loginSuccessful = false;
200
+
201
+ while (attempts < 6) {
202
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Wait
203
+ const isStillLoggedIn = await page.$(loginButtonSelector) === null;
204
+
205
+ if (isStillLoggedIn) {
206
+ loginSuccessful = true;
207
+ break;
208
+ }
179
209
 
180
- const loginButtonSelector = '.right-entry__outside.go-login-btn';
181
- const isLoggedIn = await page.$(loginButtonSelector) === null;
210
+ attempts++;
211
+ }
182
212
 
183
- if (isLoggedIn) {
184
213
  await page.close();
185
- await session.send(h.text('您已经登录了。'))
214
+ await session.send(h.text(loginSuccessful ? '登录成功!' : '登录失败,请重试。'))
186
215
  return;
187
- }
216
+ });
217
+
218
+ ctx.command('B站点播 [keyword]', '点播B站视频')
219
+ .option('video', '-v 解析返回视频')
220
+ .option('audio', '-a 解析返回语音')
221
+ .option('link', '-l 解析返回链接')
222
+ .option('page', '-p <page:number> 指定页数', { fallback: '1' })
223
+ .example('点播 遠い空へ -v')
224
+ .action(async ({ options, session }, keyword) => {
225
+ if (!keyword) {
226
+ await session.execute('点播 -h')
227
+ await session.send(h.text('没输入点播内容'))
228
+ return
229
+ }
188
230
 
189
- await page.click(loginButtonSelector);
190
231
 
191
- const qrCodeSelector = '.login-scan-box img';
192
- await page.waitForSelector(qrCodeSelector);
193
- const qrCodeUrl = await page.$eval(qrCodeSelector, img => img.src);
232
+ const url = `https://search.bilibili.com/video?keyword=${encodeURIComponent(keyword)}&page=${options.page}&o=30`
233
+ const page = await ctx.puppeteer.page()
234
+
235
+ await page.goto(url, {
236
+ waitUntil: 'networkidle2'
237
+ })
238
+
239
+ await page.addStyleTag({
240
+ content: `
241
+ div.bili-header,
242
+ div.login-tip,
243
+ div.v-popover,
244
+ div.right-entry__outside {
245
+ display: none !important;
246
+ }
247
+ `
248
+ })
249
+ // 获取视频列表并为每个视频元素添加序号
250
+ const videos = await page.evaluate((point) => {
251
+ const items = Array.from(document.querySelectorAll('.video-list-item:not([style*="display: none"])'))
252
+ return items.map((item, index) => {
253
+ const link = item.querySelector('a')
254
+ const href = link?.getAttribute('href') || ''
255
+ const idMatch = href.match(/\/video\/(BV\w+)\//)
256
+ const id = idMatch ? idMatch[1] : ''
194
257
 
195
- await session.send(h.image(qrCodeUrl, 'image/png'));
196
- await session.send('请扫描二维码进行登录。');
258
+ if (!id) {
259
+ // 如果没有提取到视频ID,隐藏这个元素
260
+ //const htmlElement = item as HTMLElement
261
+ const htmlElement = item
262
+ htmlElement.style.display = 'none'
263
+ } else {
264
+ // 创建一个包含序号的元素,并将其插入到视频元素的正中央
265
+ const overlay = document.createElement('div')
266
+ overlay.style.position = 'absolute'
267
+ overlay.style.top = `${point[0]}%`
268
+ overlay.style.left = `${point[1]}%`
269
+ overlay.style.transform = 'translate(-50%, -50%)'
270
+ overlay.style.fontSize = '48px'
271
+ overlay.style.fontWeight = 'bold'
272
+ overlay.style.color = 'black'
273
+ overlay.style.zIndex = '10'
274
+ overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.7)' // 半透明白色背景,确保数字清晰可见
275
+ overlay.style.padding = '10px'
276
+ overlay.style.borderRadius = '8px'
277
+ overlay.textContent = `${index + 1}` // 序号
278
+
279
+ // 确保父元素有 `position: relative` 以正确定位
280
+ //const videoElement = item as HTMLElement
281
+ const videoElement = item
282
+ videoElement.style.position = 'relative'
283
+ videoElement.appendChild(overlay)
284
+ }
197
285
 
198
- let attempts = 0;
199
- let loginSuccessful = false;
286
+ return { id }
287
+ }).filter(video => video.id)
288
+ }, config.point) // 传递配置的 point 参数
200
289
 
201
- while (attempts < 6) {
202
- await new Promise(resolve => setTimeout(resolve, 5000)); // Wait
203
- const isStillLoggedIn = await page.$(loginButtonSelector) === null;
290
+ // 如果开启了日志调试模式,打印获取到的视频信息
291
+ if (config.loggerinfo) {
292
+ ctx.logger.info(options)
293
+ ctx.logger.info(`共找到 ${videos.length} 个视频:`)
294
+ videos.forEach((video, index) => {
295
+ ctx.logger.info(`序号 ${index + 1}: ID - ${video.id}`)
296
+ })
297
+ }
204
298
 
205
- if (isStillLoggedIn) {
206
- loginSuccessful = true;
207
- break;
299
+ if (videos.length === 0) {
300
+ await page.close()
301
+ await session.send(h.text('未找到相关视频。'))
302
+ return
208
303
  }
209
304
 
210
- attempts++;
211
- }
305
+ // 动态调整窗口大小以适应视频数量
306
+ const viewportHeight = 200 + videos.length * 100
307
+ await page.setViewport({
308
+ width: 1440,
309
+ height: viewportHeight
310
+ })
311
+ let msg;
312
+ // 截图
313
+ const videoListElement = await page.$('.video-list.row')
314
+ if (videoListElement) {
315
+ const imgBuf = await videoListElement.screenshot({
316
+ captureBeyondViewport: false
317
+ })
318
+ msg = h.image(imgBuf, 'image/png')
319
+ }
212
320
 
213
- await page.close();
214
- await session.send(h.text(loginSuccessful ? '登录成功!' : '登录失败,请重试。'))
215
- return;
216
- });
321
+ await page.close()
217
322
 
323
+ // 发送截图
324
+ await session.send(msg)
325
+
326
+ // 提示用户输入
327
+ await session.send(`请选择视频的序号:`)
328
+
329
+ // 等待用户输入
330
+ const userChoice = await session.prompt(config.timeout * 1000)
331
+ const choiceIndex = parseInt(userChoice) - 1
332
+ if (isNaN(choiceIndex) || choiceIndex < 0 || choiceIndex >= videos.length) {
333
+ await session.send(h.text('输入无效,请输入正确的序号。'))
334
+ return
335
+ }
336
+
337
+ // 返回用户选择的视频ID
338
+ const chosenVideo = videos[choiceIndex]
339
+
340
+ // 如果开启了日志调试模式,打印用户选择的视频信息
341
+ if (config.loggerinfo) {
342
+ ctx.logger.info(`渲染序号设置\noverlay.style.top = ${config.point[0]}% \noverlay.style.left = ${config.point[1]}%`)
343
+ ctx.logger.info(`用户选择了序号 ${choiceIndex + 1}: ID - ${chosenVideo.id}`)
344
+ }
345
+
346
+ if (config.enable) { // 开启自动解析了
347
+ session.content = `https://www.bilibili.com/video/${chosenVideo.id}`
348
+ const ret = await extractLinks(session, config, ctx, lastProcessedUrls, logger); // 提取链接
349
+ if (ret && !isLinkProcessedRecently(ret, lastProcessedUrls, config, logger)) {
350
+ await processVideoFromLink(session, config, ctx, lastProcessedUrls, logger, ret, options); // 解析视频并返回
351
+ }
352
+ }
353
+ })
354
+ }
218
355
  if (config.loggerinfo) {
219
356
  ctx.command('B站点播/调试点播 [keyword]', '调试时点播B站视频')
220
357
  .option('video', '-v 解析返回视频')
@@ -322,196 +459,6 @@ function apply(ctx, config) {
322
459
  }
323
460
  });
324
461
  }
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
462
  //判断是否需要解析
516
463
  async function isProcessLinks(session, config, ctx, lastProcessedUrls, logger) {
517
464
  // 解析内容中的链接
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.4",
6
6
  "main": "lib/index.js",
7
7
  "typings": "lib/index.d.ts",
8
8
  "files": [