koishi-plugin-bilitester 1.0.0 → 1.2.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.
- package/README.md +11 -2
- package/lib/index.js +243 -25
- package/lib/services/auth.service.d.ts +43 -0
- package/lib/services/auth.service.js +101 -0
- package/lib/services/bilibili.service.d.ts +98 -0
- package/lib/services/bilibili.service.js +99 -0
- package/lib/services/database.service.d.ts +27 -0
- package/lib/services/database.service.js +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- Cookie 管理:自动存储和管理登录 Cookie
|
|
9
9
|
- 账号信息查询:查看当前登录账号的详细信息
|
|
10
10
|
- 视频信息查询:获取哔哩哔哩视频的详细信息
|
|
11
|
+
- 视频搜索:搜索哔哩哔哩视频
|
|
11
12
|
- 动态查询:获取用户的最新动态
|
|
12
13
|
- 直播间信息:获取直播间状态和信息
|
|
13
14
|
- 账号信息刷新:更新缓存的账号信息
|
|
@@ -70,10 +71,18 @@ bilitester logout
|
|
|
70
71
|
### 查询视频信息
|
|
71
72
|
|
|
72
73
|
```
|
|
73
|
-
|
|
74
|
+
bilitester video <BV号>
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
例如:`
|
|
77
|
+
例如:`bilitester video BV1xx411c7mD`
|
|
78
|
+
|
|
79
|
+
### 搜索视频
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
bilitester search <关键词>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
例如:`bilitester search 动画`
|
|
77
86
|
|
|
78
87
|
### 查询动态
|
|
79
88
|
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,105 @@ function apply(ctx, config) {
|
|
|
52
52
|
const getCookieString = (sessdata, biliJct, dedeUserId, dedeUserIdCkMd5) => {
|
|
53
53
|
return `SESSDATA=${sessdata}; bili_jct=${biliJct}; DedeUserID=${dedeUserId}; DedeUserID__ckMd5=${dedeUserIdCkMd5}`;
|
|
54
54
|
};
|
|
55
|
+
const formatNumber = (num) => {
|
|
56
|
+
if (num >= 100000000) {
|
|
57
|
+
return (num / 100000000).toFixed(1) + '亿';
|
|
58
|
+
}
|
|
59
|
+
else if (num >= 10000) {
|
|
60
|
+
return (num / 10000).toFixed(1) + '万';
|
|
61
|
+
}
|
|
62
|
+
return num.toString();
|
|
63
|
+
};
|
|
64
|
+
const formatDuration = (seconds) => {
|
|
65
|
+
const hours = Math.floor(seconds / 3600);
|
|
66
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
67
|
+
const secs = seconds % 60;
|
|
68
|
+
if (hours > 0) {
|
|
69
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const parseVideoId = (input) => {
|
|
76
|
+
const avMatch = input.match(/(?:^|[\s/])(av\d+)(?:[\s/]|$)/i);
|
|
77
|
+
if (avMatch) {
|
|
78
|
+
return { type: 'av', id: avMatch[1] };
|
|
79
|
+
}
|
|
80
|
+
const bvMatch = input.match(/(?:^|[\s/])(BV[\w]+)(?:[\s/]|$)/i);
|
|
81
|
+
if (bvMatch) {
|
|
82
|
+
return { type: 'bv', id: bvMatch[1] };
|
|
83
|
+
}
|
|
84
|
+
return { type: null, id: null };
|
|
85
|
+
};
|
|
86
|
+
const parseVideoLinks = (content) => {
|
|
87
|
+
const links = [];
|
|
88
|
+
const fullLinkPattern = /bilibili\.com\/video\/([ab]v[\w]+)/gi;
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = fullLinkPattern.exec(content)) !== null) {
|
|
91
|
+
links.push({ type: 'Video', id: match[1] });
|
|
92
|
+
}
|
|
93
|
+
const shortLinkPatterns = [
|
|
94
|
+
/b23\.tv\/([\w]+)/gi,
|
|
95
|
+
/bili22\.cn\/([\w]+)/gi,
|
|
96
|
+
/bili23\.cn\/([\w]+)/gi,
|
|
97
|
+
/bili33\.cn\/([\w]+)/gi,
|
|
98
|
+
/bili2233\.cn\/([\w]+)/gi,
|
|
99
|
+
];
|
|
100
|
+
for (const pattern of shortLinkPatterns) {
|
|
101
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
102
|
+
links.push({ type: 'Short', id: match[1] });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return links;
|
|
106
|
+
};
|
|
107
|
+
const resolveShortLink = async (shortId) => {
|
|
108
|
+
try {
|
|
109
|
+
const response = await axios_1.default.get(`https://b23.tv/${shortId}`, {
|
|
110
|
+
maxRedirects: 0,
|
|
111
|
+
validateStatus: (status) => status >= 300 && status < 400,
|
|
112
|
+
});
|
|
113
|
+
const location = response.headers['location'];
|
|
114
|
+
if (location) {
|
|
115
|
+
return location;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.response && error.response.headers && error.response.headers.location) {
|
|
121
|
+
return error.response.headers.location;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const fetchVideoInfo = async (videoId) => {
|
|
127
|
+
const parsed = parseVideoId(videoId);
|
|
128
|
+
if (!parsed.type || !parsed.id) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
let url;
|
|
133
|
+
if (parsed.type === 'av') {
|
|
134
|
+
url = `https://api.bilibili.com/x/web-interface/view?aid=${parsed.id.replace('av', '')}`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
url = `https://api.bilibili.com/x/web-interface/view?bvid=${parsed.id}`;
|
|
138
|
+
}
|
|
139
|
+
const response = await axios_1.default.get(url, {
|
|
140
|
+
headers: {
|
|
141
|
+
'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',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (response.data.code === 0) {
|
|
145
|
+
return response.data;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error('获取视频信息失败:', error);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
55
154
|
const pollLoginStatus = async (qrcodeKey, userId) => {
|
|
56
155
|
const session = loginSessions.get(qrcodeKey);
|
|
57
156
|
if (!session)
|
|
@@ -260,7 +359,7 @@ function apply(ctx, config) {
|
|
|
260
359
|
return '刷新账号信息失败,请稍后重试';
|
|
261
360
|
}
|
|
262
361
|
});
|
|
263
|
-
cmd.subcommand('video <bvid>', '
|
|
362
|
+
cmd.subcommand('video <bvid>', '获取B站视频信息')
|
|
264
363
|
.action(async ({ session }, bvid) => {
|
|
265
364
|
if (!session) {
|
|
266
365
|
return '无法获取会话信息';
|
|
@@ -269,39 +368,99 @@ function apply(ctx, config) {
|
|
|
269
368
|
try {
|
|
270
369
|
const accounts = await ctx.database.get('bilibili', { userId });
|
|
271
370
|
if (!accounts || accounts.length === 0) {
|
|
272
|
-
return '
|
|
371
|
+
return '您还未登录B站账号,请使用 "bilitester login" 进行登录';
|
|
273
372
|
}
|
|
274
373
|
const acc = accounts[0];
|
|
275
374
|
const cookie = getCookieString(acc.sessdata, acc.biliJct, acc.dedeUserId, acc.dedeUserIdCkMd5);
|
|
276
375
|
const axiosInstance = createAxiosInstance(cookie);
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (response.data.code === 0) {
|
|
281
|
-
const video = response.data.data;
|
|
282
|
-
const duration = Math.floor(video.duration / 60) + ':' + (video.duration % 60).toString().padStart(2, '0');
|
|
283
|
-
const pubdate = new Date(video.pubdate * 1000).toLocaleString('zh-CN');
|
|
284
|
-
return `视频信息:\n` +
|
|
285
|
-
`标题: ${video.title}\n` +
|
|
286
|
-
`BV号: ${video.bvid}\n` +
|
|
287
|
-
`AV号: ${video.aid}\n` +
|
|
288
|
-
`UP主: ${video.owner.name}\n` +
|
|
289
|
-
`时长: ${duration}\n` +
|
|
290
|
-
`发布时间: ${pubdate}\n` +
|
|
291
|
-
`播放: ${video.stat.view} | 弹幕: ${video.stat.danmaku} | 点赞: ${video.stat.like}\n` +
|
|
292
|
-
`投币: ${video.stat.coin} | 收藏: ${video.stat.favorite} | 分享: ${video.stat.share}\n` +
|
|
293
|
-
`简介: ${video.desc.substring(0, 100)}${video.desc.length > 100 ? '...' : ''}\n` +
|
|
294
|
-
`${koishi_1.h.image(video.pic)}`;
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
return '获取视频信息失败,请检查BV号是否正确';
|
|
376
|
+
const videoInfo = await fetchVideoInfo(bvid);
|
|
377
|
+
if (!videoInfo) {
|
|
378
|
+
return '获取视频信息失败,请检查BV/AV号是否正确';
|
|
298
379
|
}
|
|
380
|
+
const video = videoInfo.data;
|
|
381
|
+
const duration = formatDuration(video.duration);
|
|
382
|
+
const pubdate = new Date(video.pubdate * 1000).toLocaleString('zh-CN');
|
|
383
|
+
return `视频信息:\n` +
|
|
384
|
+
`标题: ${video.title}\n` +
|
|
385
|
+
`BV号: ${video.bvid}\n` +
|
|
386
|
+
`AV号: ${video.aid}\n` +
|
|
387
|
+
`UP主: ${video.owner.name}\n` +
|
|
388
|
+
`时长: ${duration}\n` +
|
|
389
|
+
`发布时间: ${pubdate}\n` +
|
|
390
|
+
`播放: ${formatNumber(video.stat.view)} | 弹幕: ${formatNumber(video.stat.danmaku)} | 点赞: ${formatNumber(video.stat.like)}\n` +
|
|
391
|
+
`投币: ${formatNumber(video.stat.coin)} | 收藏: ${formatNumber(video.stat.favorite)} | 分享: ${formatNumber(video.stat.share)}\n` +
|
|
392
|
+
`简介: ${video.desc.substring(0, 100)}${video.desc.length > 100 ? '...' : ''}\n` +
|
|
393
|
+
`${koishi_1.h.image(video.pic)}`;
|
|
299
394
|
}
|
|
300
395
|
catch (error) {
|
|
301
396
|
console.error('获取视频信息失败:', error);
|
|
302
397
|
return '获取视频信息失败,请稍后重试';
|
|
303
398
|
}
|
|
304
399
|
});
|
|
400
|
+
cmd.subcommand('parse <link>', '解析B站视频链接')
|
|
401
|
+
.action(async ({ session }, link) => {
|
|
402
|
+
if (!session) {
|
|
403
|
+
return '无法获取会话信息';
|
|
404
|
+
}
|
|
405
|
+
const userId = session.userId || session.guildId || 'unknown';
|
|
406
|
+
try {
|
|
407
|
+
const accounts = await ctx.database.get('bilibili', { userId });
|
|
408
|
+
if (!accounts || accounts.length === 0) {
|
|
409
|
+
return '您还未登录B站账号,请使用 "bilitester login" 进行登录';
|
|
410
|
+
}
|
|
411
|
+
const acc = accounts[0];
|
|
412
|
+
const cookie = getCookieString(acc.sessdata, acc.biliJct, acc.dedeUserId, acc.dedeUserIdCkMd5);
|
|
413
|
+
const axiosInstance = createAxiosInstance(cookie);
|
|
414
|
+
let videoId = null;
|
|
415
|
+
const parsed = parseVideoId(link);
|
|
416
|
+
if (parsed.type && parsed.id) {
|
|
417
|
+
videoId = parsed.id;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
const links = parseVideoLinks(link);
|
|
421
|
+
if (links.length > 0) {
|
|
422
|
+
const videoLink = links[0];
|
|
423
|
+
if (videoLink.type === 'Short') {
|
|
424
|
+
const resolvedLink = await resolveShortLink(videoLink.id);
|
|
425
|
+
if (resolvedLink) {
|
|
426
|
+
const resolvedParsed = parseVideoId(resolvedLink);
|
|
427
|
+
if (resolvedParsed.type && resolvedParsed.id) {
|
|
428
|
+
videoId = resolvedParsed.id;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
videoId = videoLink.id;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!videoId) {
|
|
438
|
+
return '无法解析视频链接,请检查链接格式是否正确';
|
|
439
|
+
}
|
|
440
|
+
const videoInfo = await fetchVideoInfo(videoId);
|
|
441
|
+
if (!videoInfo) {
|
|
442
|
+
return '获取视频信息失败,请检查链接是否正确';
|
|
443
|
+
}
|
|
444
|
+
const video = videoInfo.data;
|
|
445
|
+
const duration = formatDuration(video.duration);
|
|
446
|
+
const pubdate = new Date(video.pubdate * 1000).toLocaleString('zh-CN');
|
|
447
|
+
return `视频信息:\n` +
|
|
448
|
+
`标题: ${video.title}\n` +
|
|
449
|
+
`BV号: ${video.bvid}\n` +
|
|
450
|
+
`AV号: ${video.aid}\n` +
|
|
451
|
+
`UP主: ${video.owner.name}\n` +
|
|
452
|
+
`时长: ${duration}\n` +
|
|
453
|
+
`发布时间: ${pubdate}\n` +
|
|
454
|
+
`播放: ${formatNumber(video.stat.view)} | 弹幕: ${formatNumber(video.stat.danmaku)} | 点赞: ${formatNumber(video.stat.like)}\n` +
|
|
455
|
+
`投币: ${formatNumber(video.stat.coin)} | 收藏: ${formatNumber(video.stat.favorite)} | 分享: ${formatNumber(video.stat.share)}\n` +
|
|
456
|
+
`简介: ${video.desc.substring(0, 100)}${video.desc.length > 100 ? '...' : ''}\n` +
|
|
457
|
+
`${koishi_1.h.image(video.pic)}`;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
console.error('解析视频链接失败:', error);
|
|
461
|
+
return '解析视频链接失败,请稍后重试';
|
|
462
|
+
}
|
|
463
|
+
});
|
|
305
464
|
cmd.subcommand('dynamic', '获取哔哩哔哩动态')
|
|
306
465
|
.action(async ({ session }) => {
|
|
307
466
|
if (!session) {
|
|
@@ -353,7 +512,7 @@ function apply(ctx, config) {
|
|
|
353
512
|
return '获取动态失败,请稍后重试';
|
|
354
513
|
}
|
|
355
514
|
});
|
|
356
|
-
cmd.subcommand('live <roomId>', '
|
|
515
|
+
cmd.subcommand('live <roomId>', '获取B站直播间信息')
|
|
357
516
|
.action(async ({ session }, roomId) => {
|
|
358
517
|
if (!session) {
|
|
359
518
|
return '无法获取会话信息';
|
|
@@ -362,7 +521,7 @@ function apply(ctx, config) {
|
|
|
362
521
|
try {
|
|
363
522
|
const accounts = await ctx.database.get('bilibili', { userId });
|
|
364
523
|
if (!accounts || accounts.length === 0) {
|
|
365
|
-
return '
|
|
524
|
+
return '您还未登录B站账号,请使用 "bilitester login" 进行登录';
|
|
366
525
|
}
|
|
367
526
|
const acc = accounts[0];
|
|
368
527
|
const cookie = getCookieString(acc.sessdata, acc.biliJct, acc.dedeUserId, acc.dedeUserIdCkMd5);
|
|
@@ -391,6 +550,65 @@ function apply(ctx, config) {
|
|
|
391
550
|
return '获取直播间信息失败,请稍后重试';
|
|
392
551
|
}
|
|
393
552
|
});
|
|
553
|
+
cmd.subcommand('search <keyword>', '搜索B站视频')
|
|
554
|
+
.action(async ({ session }, keyword) => {
|
|
555
|
+
if (!session) {
|
|
556
|
+
return '无法获取会话信息';
|
|
557
|
+
}
|
|
558
|
+
const userId = session.userId || session.guildId || 'unknown';
|
|
559
|
+
try {
|
|
560
|
+
const accounts = await ctx.database.get('bilibili', { userId });
|
|
561
|
+
if (!accounts || accounts.length === 0) {
|
|
562
|
+
return '您还未登录B站账号,请使用 "bilitester login" 进行登录';
|
|
563
|
+
}
|
|
564
|
+
const acc = accounts[0];
|
|
565
|
+
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
|
+
}
|
|
573
|
+
});
|
|
574
|
+
if (response.data.code === 0) {
|
|
575
|
+
const { data } = response.data;
|
|
576
|
+
const videos = data.result[0]?.data || [];
|
|
577
|
+
if (videos.length === 0) {
|
|
578
|
+
return `未找到与"${keyword}"相关的视频`;
|
|
579
|
+
}
|
|
580
|
+
let result = `搜索结果:${keyword}\n`;
|
|
581
|
+
result += `共找到 ${data.numResults} 个视频,第 ${data.page}/${data.numPages} 页\n`;
|
|
582
|
+
result += '─'.repeat(40) + '\n';
|
|
583
|
+
for (let i = 0; i < Math.min(videos.length, 5); i++) {
|
|
584
|
+
const video = videos[i];
|
|
585
|
+
const duration = video.duration ? video.duration : '未知';
|
|
586
|
+
const pubdate = new Date(video.pubdate * 1000).toLocaleString('zh-CN');
|
|
587
|
+
result += `${i + 1}. ${video.title}\n`;
|
|
588
|
+
result += ` UP主: ${video.author}\n`;
|
|
589
|
+
result += ` BV号: ${video.bvid}\n`;
|
|
590
|
+
result += ` 时长: ${duration}\n`;
|
|
591
|
+
result += ` 播放: ${video.play} | 弹幕: ${video.video_review} | 收藏: ${video.favorites}\n`;
|
|
592
|
+
result += ` 发布: ${pubdate}\n`;
|
|
593
|
+
result += '─'.repeat(40) + '\n';
|
|
594
|
+
}
|
|
595
|
+
if (videos.length > 5) {
|
|
596
|
+
result += `还有 ${videos.length - 5} 个结果未显示...\n`;
|
|
597
|
+
}
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
else if (response.data.code === -412) {
|
|
601
|
+
return '搜索被拦截,请稍后重试';
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
return '搜索失败,请稍后重试';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
console.error('搜索视频失败:', error);
|
|
609
|
+
return '搜索视频失败,请稍后重试';
|
|
610
|
+
}
|
|
611
|
+
});
|
|
394
612
|
ctx.on('dispose', () => {
|
|
395
613
|
for (const session of loginSessions.values()) {
|
|
396
614
|
if (session.timer) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface QRCodeResponse {
|
|
2
|
+
code: number;
|
|
3
|
+
message: string;
|
|
4
|
+
ttl: number;
|
|
5
|
+
data: {
|
|
6
|
+
url: string;
|
|
7
|
+
qrcode_key: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface PollResponse {
|
|
11
|
+
code: number;
|
|
12
|
+
message: string;
|
|
13
|
+
ttl: number;
|
|
14
|
+
data: {
|
|
15
|
+
url: string;
|
|
16
|
+
refresh_token: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface LoginResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
sessdata?: string;
|
|
25
|
+
biliJct?: string;
|
|
26
|
+
dedeUserId?: string;
|
|
27
|
+
dedeUserIdCkMd5?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface LoginSession {
|
|
30
|
+
qrcodeKey: string;
|
|
31
|
+
userId: string;
|
|
32
|
+
startTime: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class AuthService {
|
|
35
|
+
private static readonly POLL_INTERVAL;
|
|
36
|
+
private static readonly QR_CODE_TIMEOUT;
|
|
37
|
+
static generateQRCode(): Promise<{
|
|
38
|
+
url: string;
|
|
39
|
+
qrcodeKey: string;
|
|
40
|
+
} | null>;
|
|
41
|
+
static pollLoginStatus(qrcodeKey: string, onLogin: (result: LoginResult) => void, onTimeout: () => void, onError: (error: string) => void): Promise<NodeJS.Timeout>;
|
|
42
|
+
static getCookieString(sessdata: string, biliJct: string, dedeUserId: string, dedeUserIdCkMd5: string): string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AuthService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class AuthService {
|
|
9
|
+
static async generateQRCode() {
|
|
10
|
+
try {
|
|
11
|
+
const response = await axios_1.default.get('https://passport.bilibili.com/x/passport-login/web/qrcode/generate', {
|
|
12
|
+
headers: {
|
|
13
|
+
'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',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const { code, data } = response.data;
|
|
17
|
+
if (code === 0 && data) {
|
|
18
|
+
return {
|
|
19
|
+
url: data.url,
|
|
20
|
+
qrcodeKey: data.qrcode_key,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('生成二维码失败:', error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
static async pollLoginStatus(qrcodeKey, onLogin, onTimeout, onError) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const timer = setInterval(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await axios_1.default.get('https://passport.bilibili.com/x/passport-login/web/qrcode/poll', {
|
|
35
|
+
params: { qrcode_key: qrcodeKey },
|
|
36
|
+
headers: {
|
|
37
|
+
'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',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const { code, data } = response.data;
|
|
41
|
+
if (code === 0) {
|
|
42
|
+
if (data.code === 0) {
|
|
43
|
+
const cookies = response.headers['set-cookie'] || [];
|
|
44
|
+
let sessdata = '';
|
|
45
|
+
let biliJct = '';
|
|
46
|
+
let dedeUserId = '';
|
|
47
|
+
let dedeUserIdCkMd5 = '';
|
|
48
|
+
for (const cookie of cookies) {
|
|
49
|
+
if (cookie.includes('SESSDATA=')) {
|
|
50
|
+
sessdata = cookie.match(/SESSDATA=([^;]+)/)?.[1] || '';
|
|
51
|
+
}
|
|
52
|
+
if (cookie.includes('bili_jct=')) {
|
|
53
|
+
biliJct = cookie.match(/bili_jct=([^;]+)/)?.[1] || '';
|
|
54
|
+
}
|
|
55
|
+
if (cookie.includes('DedeUserID=')) {
|
|
56
|
+
dedeUserId = cookie.match(/DedeUserID=([^;]+)/)?.[1] || '';
|
|
57
|
+
}
|
|
58
|
+
if (cookie.includes('DedeUserID__ckMd5=')) {
|
|
59
|
+
dedeUserIdCkMd5 = cookie.match(/DedeUserID__ckMd5=([^;]+)/)?.[1] || '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (sessdata && biliJct && dedeUserId) {
|
|
63
|
+
onLogin({
|
|
64
|
+
success: true,
|
|
65
|
+
sessdata,
|
|
66
|
+
biliJct,
|
|
67
|
+
dedeUserId,
|
|
68
|
+
dedeUserIdCkMd5,
|
|
69
|
+
});
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
resolve(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (data.code === 86038) {
|
|
75
|
+
onError('二维码已失效');
|
|
76
|
+
clearInterval(timer);
|
|
77
|
+
resolve(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('轮询登录状态失败:', error);
|
|
83
|
+
onError('网络错误,请稍后重试');
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
resolve(timer);
|
|
86
|
+
}
|
|
87
|
+
}, this.POLL_INTERVAL);
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
onTimeout();
|
|
91
|
+
resolve(timer);
|
|
92
|
+
}, this.QR_CODE_TIMEOUT * 1000);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
static getCookieString(sessdata, biliJct, dedeUserId, dedeUserIdCkMd5) {
|
|
96
|
+
return `SESSDATA=${sessdata}; bili_jct=${biliJct}; DedeUserID=${dedeUserId}; DedeUserID__ckMd5=${dedeUserIdCkMd5}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.AuthService = AuthService;
|
|
100
|
+
AuthService.POLL_INTERVAL = 2000;
|
|
101
|
+
AuthService.QR_CODE_TIMEOUT = 180;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface VideoInfo {
|
|
2
|
+
bvid: string;
|
|
3
|
+
aid: number;
|
|
4
|
+
title: string;
|
|
5
|
+
desc: string;
|
|
6
|
+
pic: string;
|
|
7
|
+
owner: {
|
|
8
|
+
mid: string;
|
|
9
|
+
name: string;
|
|
10
|
+
face: string;
|
|
11
|
+
};
|
|
12
|
+
stat: {
|
|
13
|
+
view: number;
|
|
14
|
+
danmaku: number;
|
|
15
|
+
reply: number;
|
|
16
|
+
favorite: number;
|
|
17
|
+
coin: number;
|
|
18
|
+
share: number;
|
|
19
|
+
like: number;
|
|
20
|
+
};
|
|
21
|
+
duration: number;
|
|
22
|
+
pubdate: number;
|
|
23
|
+
}
|
|
24
|
+
export interface VideoSearchResult {
|
|
25
|
+
bvid: string;
|
|
26
|
+
aid: number;
|
|
27
|
+
title: string;
|
|
28
|
+
pic: string;
|
|
29
|
+
description: string;
|
|
30
|
+
owner: {
|
|
31
|
+
mid: string;
|
|
32
|
+
name: string;
|
|
33
|
+
face: string;
|
|
34
|
+
};
|
|
35
|
+
stat: {
|
|
36
|
+
view: number;
|
|
37
|
+
danmaku: number;
|
|
38
|
+
reply: number;
|
|
39
|
+
favorite: number;
|
|
40
|
+
coin: number;
|
|
41
|
+
share: number;
|
|
42
|
+
like: number;
|
|
43
|
+
};
|
|
44
|
+
duration: number;
|
|
45
|
+
pubdate: number;
|
|
46
|
+
}
|
|
47
|
+
export interface UserInfo {
|
|
48
|
+
isLogin: boolean;
|
|
49
|
+
mid: string;
|
|
50
|
+
uname: string;
|
|
51
|
+
face: string;
|
|
52
|
+
vipStatus: number;
|
|
53
|
+
vipType: number;
|
|
54
|
+
vipDueDate: number;
|
|
55
|
+
level_info: {
|
|
56
|
+
current_level: number;
|
|
57
|
+
current_exp: number;
|
|
58
|
+
next_exp: string | number;
|
|
59
|
+
};
|
|
60
|
+
money: number;
|
|
61
|
+
}
|
|
62
|
+
export interface DynamicResponse {
|
|
63
|
+
cards: Array<{
|
|
64
|
+
card: string;
|
|
65
|
+
desc: {
|
|
66
|
+
type: number;
|
|
67
|
+
dynamic_id: number;
|
|
68
|
+
timestamp: number;
|
|
69
|
+
};
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
export interface LiveRoomInfo {
|
|
73
|
+
roomid: number;
|
|
74
|
+
uid: number;
|
|
75
|
+
title: string;
|
|
76
|
+
live_status: number;
|
|
77
|
+
keyframe: string;
|
|
78
|
+
cover: string;
|
|
79
|
+
online: number;
|
|
80
|
+
area_name: string;
|
|
81
|
+
parent_area_name: string;
|
|
82
|
+
}
|
|
83
|
+
export interface SearchResponse {
|
|
84
|
+
code: number;
|
|
85
|
+
message: string;
|
|
86
|
+
data: {
|
|
87
|
+
result: VideoSearchResult[];
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export declare class BilibiliService {
|
|
91
|
+
private axiosInstance;
|
|
92
|
+
constructor(cookie: string);
|
|
93
|
+
getUserInfo(): Promise<UserInfo | null>;
|
|
94
|
+
getVideoInfo(bvid: string): Promise<VideoInfo | null>;
|
|
95
|
+
searchVideos(keyword: string, page?: number): Promise<VideoSearchResult[] | null>;
|
|
96
|
+
getDynamics(uid: string): Promise<DynamicResponse | null>;
|
|
97
|
+
getLiveRoomInfo(roomId: number): Promise<LiveRoomInfo | null>;
|
|
98
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BilibiliService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class BilibiliService {
|
|
9
|
+
constructor(cookie) {
|
|
10
|
+
this.axiosInstance = axios_1.default.create({
|
|
11
|
+
headers: {
|
|
12
|
+
'Cookie': cookie,
|
|
13
|
+
'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',
|
|
14
|
+
'Referer': 'https://www.bilibili.com',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async getUserInfo() {
|
|
19
|
+
try {
|
|
20
|
+
const response = await this.axiosInstance.get('https://api.bilibili.com/x/web-interface/nav');
|
|
21
|
+
if (response.data.code === 0 && response.data.data.isLogin) {
|
|
22
|
+
return response.data.data;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error('获取用户信息失败:', error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async getVideoInfo(bvid) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await this.axiosInstance.get('https://api.bilibili.com/x/web-interface/view', {
|
|
34
|
+
params: { bvid }
|
|
35
|
+
});
|
|
36
|
+
if (response.data.code === 0) {
|
|
37
|
+
return response.data.data;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('获取视频信息失败:', error);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async searchVideos(keyword, page = 1) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await this.axiosInstance.get('https://api.bilibili.com/x/web-interface/search/all', {
|
|
49
|
+
params: {
|
|
50
|
+
keyword,
|
|
51
|
+
page,
|
|
52
|
+
search_type: 'video',
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (response.data.code === 0) {
|
|
56
|
+
return response.data.data.result;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('搜索视频失败:', error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async getDynamics(uid) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await this.axiosInstance.get('https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new', {
|
|
68
|
+
params: {
|
|
69
|
+
uid,
|
|
70
|
+
type_list: '8',
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (response.data.code === 0) {
|
|
74
|
+
return response.data.data;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('获取动态失败:', error);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async getLiveRoomInfo(roomId) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await this.axiosInstance.get('https://api.live.bilibili.com/room/v1/Room/get_info', {
|
|
86
|
+
params: { room_id: roomId }
|
|
87
|
+
});
|
|
88
|
+
if (response.data.code === 0) {
|
|
89
|
+
return response.data.data;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error('获取直播间信息失败:', error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.BilibiliService = BilibiliService;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
export interface BilibiliAccount {
|
|
3
|
+
id: number;
|
|
4
|
+
userId: string;
|
|
5
|
+
sessdata: string;
|
|
6
|
+
biliJct: string;
|
|
7
|
+
dedeUserId: string;
|
|
8
|
+
dedeUserIdCkMd5: string;
|
|
9
|
+
mid: string;
|
|
10
|
+
name: string;
|
|
11
|
+
face: string;
|
|
12
|
+
vipStatus: number;
|
|
13
|
+
vipType: number;
|
|
14
|
+
vipDueDate: number;
|
|
15
|
+
level: number;
|
|
16
|
+
coins: number;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
updatedAt: Date;
|
|
19
|
+
}
|
|
20
|
+
export declare class DatabaseService {
|
|
21
|
+
private ctx;
|
|
22
|
+
constructor(ctx: Context);
|
|
23
|
+
createAccount(account: Omit<BilibiliAccount, 'id'>): Promise<BilibiliAccount>;
|
|
24
|
+
getAccountByUserId(userId: string): Promise<BilibiliAccount | null>;
|
|
25
|
+
updateAccount(id: number, data: Partial<Omit<BilibiliAccount, 'id' | 'userId' | 'createdAt'>>): Promise<boolean>;
|
|
26
|
+
deleteAccountByUserId(userId: string): Promise<boolean>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatabaseService = void 0;
|
|
4
|
+
class DatabaseService {
|
|
5
|
+
constructor(ctx) {
|
|
6
|
+
this.ctx = ctx;
|
|
7
|
+
this.ctx.model.extend('bilibili', {
|
|
8
|
+
id: 'unsigned',
|
|
9
|
+
userId: 'text',
|
|
10
|
+
sessdata: 'text',
|
|
11
|
+
biliJct: 'text',
|
|
12
|
+
dedeUserId: 'text',
|
|
13
|
+
dedeUserIdCkMd5: 'text',
|
|
14
|
+
mid: 'text',
|
|
15
|
+
name: 'text',
|
|
16
|
+
face: 'text',
|
|
17
|
+
vipStatus: 'integer',
|
|
18
|
+
vipType: 'integer',
|
|
19
|
+
vipDueDate: 'integer',
|
|
20
|
+
level: 'integer',
|
|
21
|
+
coins: 'float',
|
|
22
|
+
createdAt: 'timestamp',
|
|
23
|
+
updatedAt: 'timestamp',
|
|
24
|
+
}, {
|
|
25
|
+
autoInc: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async createAccount(account) {
|
|
29
|
+
const newAccount = {
|
|
30
|
+
...account,
|
|
31
|
+
id: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
await this.ctx.database.create('bilibili', newAccount);
|
|
34
|
+
return newAccount;
|
|
35
|
+
}
|
|
36
|
+
async getAccountByUserId(userId) {
|
|
37
|
+
const accounts = await this.ctx.database.get('bilibili', { userId });
|
|
38
|
+
if (!accounts || accounts.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return accounts[0];
|
|
42
|
+
}
|
|
43
|
+
async updateAccount(id, data) {
|
|
44
|
+
try {
|
|
45
|
+
await this.ctx.database.set('bilibili', { id }, {
|
|
46
|
+
...data,
|
|
47
|
+
updatedAt: new Date(),
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error('更新账号信息失败:', error);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async deleteAccountByUserId(userId) {
|
|
57
|
+
try {
|
|
58
|
+
await this.ctx.database.remove('bilibili', { userId });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('删除账号信息失败:', error);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.DatabaseService = DatabaseService;
|