koishi-plugin-bilitester 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.
Files changed (2) hide show
  1. package/lib/index.js +139 -8
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ exports.name = exports.Config = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
10
+ const crypto_1 = __importDefault(require("crypto"));
10
11
  exports.Config = koishi_1.Schema.object({
11
12
  pollInterval: koishi_1.Schema.number()
12
13
  .description('轮询扫码状态的时间间隔(毫秒)')
@@ -16,7 +17,64 @@ exports.Config = koishi_1.Schema.object({
16
17
  .default(180),
17
18
  });
18
19
  exports.name = 'bilitester';
20
+ const searchResultsCache = new Map();
19
21
  const loginSessions = new Map();
22
+ const MIXIN_KEY_ENC_TAB = [
23
+ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
24
+ 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
25
+ 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
26
+ 36, 20, 34, 44, 52
27
+ ];
28
+ const getMixinKey = (orig) => {
29
+ return MIXIN_KEY_ENC_TAB.map(n => orig[n]).join('').slice(0, 32);
30
+ };
31
+ let cachedWbiKeys = null;
32
+ let wbiKeysExpireTime = 0;
33
+ const getWbiKeys = async (cookie) => {
34
+ const now = Date.now();
35
+ if (cachedWbiKeys && now - wbiKeysExpireTime < 3600000) {
36
+ return cachedWbiKeys;
37
+ }
38
+ try {
39
+ const response = await axios_1.default.get('https://api.bilibili.com/x/web-interface/nav', {
40
+ headers: {
41
+ 'Cookie': cookie,
42
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
43
+ 'Referer': 'https://www.bilibili.com/',
44
+ },
45
+ });
46
+ const { data } = response.data;
47
+ if (!data || !data.wbi_img) {
48
+ throw new Error('Failed to get WBI keys');
49
+ }
50
+ const img_url = data.wbi_img.img_url;
51
+ const sub_url = data.wbi_img.sub_url;
52
+ const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.'));
53
+ const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.'));
54
+ cachedWbiKeys = { imgKey, subKey };
55
+ wbiKeysExpireTime = now;
56
+ return { imgKey, subKey };
57
+ }
58
+ catch (error) {
59
+ console.error('获取WBI keys失败:', error);
60
+ throw error;
61
+ }
62
+ };
63
+ const encWbi = (params, imgKey, subKey) => {
64
+ const mixinKey = getMixinKey(imgKey + subKey);
65
+ const currTime = Math.round(Date.now() / 1000);
66
+ const chrFilter = /[!'()*]/g;
67
+ Object.assign(params, { wts: currTime });
68
+ const sortedKeys = Object.keys(params).sort();
69
+ const query = sortedKeys
70
+ .map(key => {
71
+ const value = params[key].toString().replace(chrFilter, '');
72
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
73
+ })
74
+ .join('&');
75
+ const wbiSign = crypto_1.default.createHash('md5').update(query + mixinKey).digest('hex');
76
+ return query + `&w_rid=${wbiSign}`;
77
+ };
20
78
  function apply(ctx, config) {
21
79
  const pollInterval = config.pollInterval || 2000;
22
80
  const qrCodeTimeout = config.qrCodeTimeout || 180;
@@ -563,13 +621,17 @@ function apply(ctx, config) {
563
621
  }
564
622
  const acc = accounts[0];
565
623
  const cookie = getCookieString(acc.sessdata, acc.biliJct, acc.dedeUserId, acc.dedeUserIdCkMd5);
566
- const axiosInstance = createAxiosInstance(cookie);
567
- const response = await axiosInstance.get('https://api.bilibili.com/x/web-interface/search/type', {
568
- params: {
569
- search_type: 'video',
570
- keyword: keyword,
571
- page: 1,
572
- }
624
+ const wbiKeys = await getWbiKeys(cookie);
625
+ const wbiSign = encWbi({
626
+ keyword: keyword,
627
+ page: 1,
628
+ }, wbiKeys.imgKey, wbiKeys.subKey);
629
+ const response = await axios_1.default.get(`https://api.bilibili.com/x/web-interface/search/type?search_type=video&keyword=${encodeURIComponent(keyword)}&page=1&${wbiSign}`, {
630
+ headers: {
631
+ 'Cookie': cookie,
632
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
633
+ 'Referer': 'https://www.bilibili.com/',
634
+ },
573
635
  });
574
636
  if (response.data.code === 0) {
575
637
  const { data } = response.data;
@@ -577,6 +639,8 @@ function apply(ctx, config) {
577
639
  if (videos.length === 0) {
578
640
  return `未找到与"${keyword}"相关的视频`;
579
641
  }
642
+ const cacheKey = `search_${userId}_${keyword}_1`;
643
+ searchResultsCache.set(cacheKey, videos);
580
644
  let result = `搜索结果:${keyword}\n`;
581
645
  result += `共找到 ${data.numResults} 个视频,第 ${data.page}/${data.numPages} 页\n`;
582
646
  result += '─'.repeat(40) + '\n';
@@ -588,13 +652,14 @@ function apply(ctx, config) {
588
652
  result += ` UP主: ${video.author}\n`;
589
653
  result += ` BV号: ${video.bvid}\n`;
590
654
  result += ` 时长: ${duration}\n`;
591
- result += ` 播放: ${video.play} | 弹幕: ${video.video_review} | 收藏: ${video.favorites}\n`;
655
+ result += ` 播放: ${formatNumber(video.play)} | 弹幕: ${formatNumber(video.video_review)} | 收藏: ${formatNumber(video.favorites)}\n`;
592
656
  result += ` 发布: ${pubdate}\n`;
593
657
  result += '─'.repeat(40) + '\n';
594
658
  }
595
659
  if (videos.length > 5) {
596
660
  result += `还有 ${videos.length - 5} 个结果未显示...\n`;
597
661
  }
662
+ result += `\n使用 "bilitester select <序号>" 选择视频`;
598
663
  return result;
599
664
  }
600
665
  else if (response.data.code === -412) {
@@ -609,6 +674,72 @@ function apply(ctx, config) {
609
674
  return '搜索视频失败,请稍后重试';
610
675
  }
611
676
  });
677
+ cmd.subcommand('select <index>', '选择搜索结果中的视频')
678
+ .action(async ({ session }, index) => {
679
+ if (!session) {
680
+ return '无法获取会话信息';
681
+ }
682
+ const userId = session.userId || session.guildId || 'unknown';
683
+ try {
684
+ const cachedResults = Array.from(searchResultsCache.entries()).find(([key]) => key.startsWith(`search_${userId}_`));
685
+ if (!cachedResults || cachedResults[1].length === 0) {
686
+ return '请先使用 "bilitester search <关键词>" 搜索视频';
687
+ }
688
+ const videos = cachedResults[1];
689
+ const idx = parseInt(index);
690
+ if (isNaN(idx) || idx < 1 || idx > videos.length) {
691
+ return `请输入有效的视频序号 (1-${videos.length})`;
692
+ }
693
+ const video = videos[idx - 1];
694
+ const accounts = await ctx.database.get('bilibili', { userId });
695
+ if (!accounts || accounts.length === 0) {
696
+ return '您还未登录B站账号,请使用 "bilitester login" 进行登录';
697
+ }
698
+ const acc = accounts[0];
699
+ const cookie = getCookieString(acc.sessdata, acc.biliJct, acc.dedeUserId, acc.dedeUserIdCkMd5);
700
+ const axiosInstance = createAxiosInstance(cookie);
701
+ const playUrlResponse = await axiosInstance.get(`https://api.bilibili.com/x/player/playurl`, {
702
+ params: {
703
+ bvid: video.bvid,
704
+ fnval: 80,
705
+ }
706
+ });
707
+ if (playUrlResponse.data.code === 0 && playUrlResponse.data.data && playUrlResponse.data.data.dash) {
708
+ const dash = playUrlResponse.data.data.dash;
709
+ const videoStream = dash.video?.[0];
710
+ const audioStream = dash.audio?.[0];
711
+ if (videoStream && audioStream) {
712
+ const duration = formatDuration(dash.duration);
713
+ const pubdate = new Date(video.pubdate * 1000).toLocaleString('zh-CN');
714
+ let result = `视频信息:\n`;
715
+ result += `标题: ${video.title}\n`;
716
+ result += `BV号: ${video.bvid}\n`;
717
+ result += `AV号: ${video.aid}\n`;
718
+ result += `UP主: ${video.author}\n`;
719
+ result += `时长: ${duration}\n`;
720
+ result += `发布时间: ${pubdate}\n`;
721
+ result += `播放: ${formatNumber(video.play)} | 弹幕: ${formatNumber(video.video_review)} | 收藏: ${formatNumber(video.favorites)}\n`;
722
+ result += `简介: ${video.description.substring(0, 100)}${video.description.length > 100 ? '...' : ''}\n`;
723
+ result += `${koishi_1.h.image(video.pic)}\n`;
724
+ const videoUrl = videoStream.baseUrl + videoStream.segment_base_url + videoStream.segments[0].url;
725
+ const audioUrl = audioStream.baseUrl + audioStream.segment_base_url + audioStream.segments[0].url;
726
+ result += `\n视频链接:\n${videoUrl}\n`;
727
+ result += `\n音频链接:\n${audioUrl}`;
728
+ return result;
729
+ }
730
+ else {
731
+ return '获取视频播放链接失败,可能是大会员专属视频';
732
+ }
733
+ }
734
+ else {
735
+ return '获取视频播放链接失败,请稍后重试';
736
+ }
737
+ }
738
+ catch (error) {
739
+ console.error('获取视频播放链接失败:', error);
740
+ return '获取视频播放链接失败,请稍后重试';
741
+ }
742
+ });
612
743
  ctx.on('dispose', () => {
613
744
  for (const session of loginSessions.values()) {
614
745
  if (session.timer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-bilitester",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "哔哩哔哩登录和API调用插件,支持二维码登录和Cookie管理",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",