ErisPulse-BiliParser 1.0.0__tar.gz

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.
@@ -0,0 +1,549 @@
1
+ import re
2
+ import time
3
+ from typing import Optional, Dict, List, Tuple
4
+
5
+ import aiohttp
6
+ from bilibili_api import video, comment
7
+
8
+ from ErisPulse import sdk
9
+ from ErisPulse.Core.Bases import BaseModule
10
+ from ErisPulse.Core.Event import command, message
11
+
12
+
13
+ _BILI_LINK_REGEX = re.compile(
14
+ r'(?:https?://(?:www\.)?bilibili\.com/video/((?:BV[\w]+)|(?:av\d+))'
15
+ r'|https?://b23\.tv/([\w]+)'
16
+ r'|(?<!\w)((?:BV[\w]{6,12})|(?:av\d+))(?!\w))',
17
+ re.IGNORECASE
18
+ )
19
+
20
+
21
+ def _format_count(n: int) -> str:
22
+ if n >= 100_000_000:
23
+ return f"{n / 100_000_000:.1f}亿"
24
+ if n >= 10_000:
25
+ return f"{n / 10_000:.1f}万"
26
+ return str(n)
27
+
28
+
29
+ def _format_duration(seconds: int) -> str:
30
+ m, s = divmod(seconds, 60)
31
+ h, m = divmod(m, 60)
32
+ if h > 0:
33
+ return f"{h}:{m:02d}:{s:02d}"
34
+ return f"{m}:{s:02d}"
35
+
36
+
37
+ class BiliTemplates:
38
+ PRIMARY_COLOR = "#fb7299"
39
+ PRIMARY_BG = "rgba(251, 114, 153, 0.05)"
40
+ SECONDARY_COLOR = "#666"
41
+
42
+ @classmethod
43
+ def build_video_card(cls, info: dict, config: dict) -> Dict[str, str]:
44
+ comments_text = info.get("_comments_text", "")
45
+
46
+ html = cls._build_html(info, config, comments_text)
47
+ markdown = cls._build_markdown(info, config, comments_text)
48
+ text = cls._build_text(info, config, comments_text)
49
+
50
+ return {"html": html, "markdown": markdown, "text": text}
51
+
52
+ @classmethod
53
+ def _build_html(cls, info: dict, config: dict, comments_text: str) -> str:
54
+ stat = info.get("stat", {})
55
+ owner = info.get("owner", {})
56
+ bvid = info.get("bvid", "")
57
+
58
+ stat_items = (
59
+ f'<span style="margin-right: 12px;">播放 {_format_count(stat.get("view", 0))}</span>'
60
+ f'<span style="margin-right: 12px;">弹幕 {_format_count(stat.get("danmaku", 0))}</span>'
61
+ f'<span>点赞 {_format_count(stat.get("like", 0))}</span>'
62
+ )
63
+
64
+ interact_items = (
65
+ f'<span style="margin-right: 12px;">投币 {_format_count(stat.get("coin", 0))}</span>'
66
+ f'<span style="margin-right: 12px;">收藏 {_format_count(stat.get("favorite", 0))}</span>'
67
+ f'<span>分享 {_format_count(stat.get("share", 0))}</span>'
68
+ )
69
+
70
+ duration_line = ""
71
+ if info.get("duration") and info["duration"] != "0:00":
72
+ duration_line = f'<div style="font-size: 12px; color: {cls.SECONDARY_COLOR}; margin-bottom: 4px;">时长: {info["duration"]}</div>'
73
+
74
+ pages_line = ""
75
+ if info.get("videos", 1) > 1:
76
+ pages_line = f'<div style="font-size: 12px; color: {cls.SECONDARY_COLOR}; margin-bottom: 4px;">共 {info["videos"]} P</div>'
77
+
78
+ tags_line = ""
79
+ if info.get("tags"):
80
+ tags_html = " ".join(
81
+ f'<code style="font-size: 11px; background: rgba(0,0,0,0.04); padding: 1px 5px; border-radius: 3px;">{t}</code>'
82
+ for t in info["tags"]
83
+ )
84
+ tags_line = f'<div style="font-size: 12px; margin-top: 6px;">{tags_html}</div>'
85
+
86
+ desc_section = ""
87
+ if config.get("show_description", False) and info.get("description"):
88
+ max_len = config.get("max_desc_length", 100)
89
+ desc = info["description"][:max_len]
90
+ if len(info["description"]) > max_len:
91
+ desc += "..."
92
+ desc_section = (
93
+ f'<details style="margin-top: 8px;">'
94
+ f'<summary style="cursor: pointer; font-size: 12px; color: {cls.SECONDARY_COLOR};">简介</summary>'
95
+ f'<div style="padding: 6px; font-size: 12px; color: {cls.SECONDARY_COLOR};">{desc}</div>'
96
+ f'</details>'
97
+ )
98
+
99
+ comments_section = ""
100
+ if comments_text:
101
+ comments_section = (
102
+ f'<div style="margin-top: 8px; border-top: 1px solid rgba(0,0,0,0.06); padding-top: 8px;">'
103
+ f'<div style="font-size: 13px; font-weight: bold; color: {cls.PRIMARY_COLOR}; margin-bottom: 6px;">热门评论</div>'
104
+ f'{comments_text}'
105
+ f'</div>'
106
+ )
107
+
108
+ link_line = ""
109
+ if bvid:
110
+ link_line = (
111
+ f'<div style="margin-top: 8px;">'
112
+ f'<a href="https://www.bilibili.com/video/{bvid}" style="font-size: 12px; color: {cls.PRIMARY_COLOR};">https://www.bilibili.com/video/{bvid}</a>'
113
+ f'</div>'
114
+ )
115
+
116
+ return (
117
+ f'<div style="padding: 12px; border-radius: 8px;">'
118
+ f'<div style="color: {cls.PRIMARY_COLOR}; font-size: 15px; font-weight: bold; margin-bottom: 8px;">{info["title"]}</div>'
119
+ f'<div style="font-size: 13px; margin-bottom: 10px;">UP主: <span style="color: {cls.PRIMARY_COLOR}; font-weight: bold;">{owner.get("name", "未知")}</span></div>'
120
+ f'<div style="padding: 8px; background: {cls.PRIMARY_BG}; border-radius: 6px; margin-bottom: 8px;">'
121
+ f'<div style="font-size: 13px; margin-bottom: 4px;">{stat_items}</div>'
122
+ f'<div style="font-size: 13px;">{interact_items}</div>'
123
+ f'{duration_line}{pages_line}'
124
+ f'{tags_line}'
125
+ f'</div>'
126
+ f'{desc_section}'
127
+ f'{comments_section}'
128
+ f'{link_line}'
129
+ f'</div>'
130
+ )
131
+
132
+ @classmethod
133
+ def _build_markdown(cls, info: dict, config: dict, comments_text: str) -> str:
134
+ stat = info.get("stat", {})
135
+ owner = info.get("owner", {})
136
+ bvid = info.get("bvid", "")
137
+
138
+ lines = [
139
+ f'**{info["title"]}**',
140
+ f'UP主: {owner.get("name", "未知")}',
141
+ '',
142
+ f'播放: {_format_count(stat.get("view", 0))} | '
143
+ f'弹幕: {_format_count(stat.get("danmaku", 0))} | '
144
+ f'点赞: {_format_count(stat.get("like", 0))}',
145
+ f'投币: {_format_count(stat.get("coin", 0))} | '
146
+ f'收藏: {_format_count(stat.get("favorite", 0))} | '
147
+ f'分享: {_format_count(stat.get("share", 0))}',
148
+ ]
149
+
150
+ if info.get("duration") and info["duration"] != "0:00":
151
+ lines.append(f'时长: {info["duration"]}')
152
+ if info.get("videos", 1) > 1:
153
+ lines.append(f'共 {info["videos"]} P')
154
+ if info.get("tags"):
155
+ lines.append(f'标签: {" | ".join(info["tags"])}')
156
+
157
+ if config.get("show_description", False) and info.get("description"):
158
+ max_len = config.get("max_desc_length", 100)
159
+ desc = info["description"][:max_len]
160
+ if len(info["description"]) > max_len:
161
+ desc += "..."
162
+ lines.extend(['', f'> {desc}'])
163
+
164
+ if comments_text:
165
+ lines.extend(['', '**热门评论**', comments_text])
166
+
167
+ if bvid:
168
+ lines.extend(['', f'[查看原视频](https://www.bilibili.com/video/{bvid})'])
169
+
170
+ return '\n'.join(lines)
171
+
172
+ @classmethod
173
+ def _build_text(cls, info: dict, config: dict, comments_text: str) -> str:
174
+ stat = info.get("stat", {})
175
+ owner = info.get("owner", {})
176
+ bvid = info.get("bvid", "")
177
+
178
+ lines = [
179
+ info["title"],
180
+ f'UP主: {owner.get("name", "未知")}',
181
+ '----------',
182
+ f'播放: {_format_count(stat.get("view", 0))} '
183
+ f'弹幕: {_format_count(stat.get("danmaku", 0))} '
184
+ f'点赞: {_format_count(stat.get("like", 0))}',
185
+ f'投币: {_format_count(stat.get("coin", 0))} '
186
+ f'收藏: {_format_count(stat.get("favorite", 0))} '
187
+ f'分享: {_format_count(stat.get("share", 0))}',
188
+ ]
189
+
190
+ if info.get("duration") and info["duration"] != "0:00":
191
+ lines.append(f'时长: {info["duration"]}')
192
+ if info.get("videos", 1) > 1:
193
+ lines.append(f'共 {info["videos"]} P')
194
+ if info.get("tags"):
195
+ lines.append(f'标签: {" | ".join(info["tags"])}')
196
+
197
+ if config.get("show_description", False) and info.get("description"):
198
+ max_len = config.get("max_desc_length", 100)
199
+ desc = info["description"][:max_len]
200
+ if len(info["description"]) > max_len:
201
+ desc += "..."
202
+ lines.extend(['', desc])
203
+
204
+ if comments_text:
205
+ lines.extend(['', '── 热门评论 ──', comments_text])
206
+
207
+ if bvid:
208
+ lines.extend(['', f'https://www.bilibili.com/video/{bvid}'])
209
+
210
+ return '\n'.join(lines)
211
+
212
+ @classmethod
213
+ def build_comments_html(cls, comments: list) -> str:
214
+ items = []
215
+ for i, c in enumerate(comments, 1):
216
+ content = c["content"]
217
+ if len(content) > 80:
218
+ content = content[:80] + "..."
219
+ like_text = f' <span style="color: {cls.SECONDARY_COLOR};">({_format_count(c["like"])})</span>' if c["like"] > 0 else ""
220
+ items.append(
221
+ f'<div style="margin-bottom: 4px; font-size: 12px;">'
222
+ f'<span style="font-weight: bold;">{i}. {c["user"]}</span>: '
223
+ f'{content}{like_text}'
224
+ f'</div>'
225
+ )
226
+ return ''.join(items)
227
+
228
+ @classmethod
229
+ def build_comments_markdown(cls, comments: list) -> str:
230
+ lines = []
231
+ for i, c in enumerate(comments, 1):
232
+ content = c["content"]
233
+ if len(content) > 80:
234
+ content = content[:80] + "..."
235
+ like_text = f' ({_format_count(c["like"])})' if c["like"] > 0 else ""
236
+ lines.append(f'{i}. **{c["user"]}**: {content}{like_text}')
237
+ return '\n'.join(lines)
238
+
239
+ @classmethod
240
+ def build_comments_text(cls, comments: list) -> str:
241
+ lines = []
242
+ for i, c in enumerate(comments, 1):
243
+ content = c["content"]
244
+ if len(content) > 80:
245
+ content = content[:80] + "..."
246
+ like_text = f' ({_format_count(c["like"])})' if c["like"] > 0 else ""
247
+ lines.append(f'{i}. {c["user"]}: {content}{like_text}')
248
+ return '\n'.join(lines)
249
+
250
+
251
+ class BiliVideoParser:
252
+ def __init__(self, logger, config: dict):
253
+ self.logger = logger
254
+ self.config = config
255
+ self._cache: Dict[str, Tuple[dict, float]] = {}
256
+ self._cache_ttl = config.get("cache_ttl", 600)
257
+
258
+ async def resolve_short_url(self, short_code: str) -> Optional[str]:
259
+ url = f"https://b23.tv/{short_code}"
260
+ try:
261
+ async with aiohttp.ClientSession(
262
+ allow_redirects=False,
263
+ timeout=aiohttp.ClientTimeout(total=10),
264
+ ) as session:
265
+ async with session.get(url) as resp:
266
+ if resp.status in (301, 302):
267
+ redirect_url = resp.headers.get("Location", "")
268
+ match = re.search(
269
+ r'(?:BV[\w]+|av\d+)', redirect_url
270
+ )
271
+ if match:
272
+ return match.group(0)
273
+ return None
274
+ except Exception as e:
275
+ self.logger.warning(f"解析短链接失败: {url} - {e}")
276
+ return None
277
+
278
+ def extract_ids(self, text: str) -> List:
279
+ results = []
280
+ seen = set()
281
+ for match in _BILI_LINK_REGEX.finditer(text):
282
+ bv_or_av = match.group(1) or match.group(3)
283
+ short_code = match.group(2)
284
+ if bv_or_av:
285
+ if bv_or_av[:2].lower() == "bv":
286
+ key = "BV" + bv_or_av[2:]
287
+ else:
288
+ key = bv_or_av
289
+ if key not in seen:
290
+ seen.add(key)
291
+ results.append(key)
292
+ elif short_code and short_code not in seen:
293
+ seen.add(short_code)
294
+ results.append(("short", short_code))
295
+ return results
296
+
297
+ def _get_cache(self, key: str) -> Optional[dict]:
298
+ if key in self._cache:
299
+ data, ts = self._cache[key]
300
+ if time.time() - ts < self._cache_ttl:
301
+ return data
302
+ del self._cache[key]
303
+ return None
304
+
305
+ def _set_cache(self, key: str, data: dict):
306
+ self._cache[key] = (data, time.time())
307
+ now = time.time()
308
+ expired = [k for k, (_, ts) in self._cache.items() if now - ts > self._cache_ttl]
309
+ for k in expired:
310
+ del self._cache[k]
311
+
312
+ async def parse_video(self, video_id: str) -> Optional[dict]:
313
+ cached = self._get_cache(video_id)
314
+ if cached:
315
+ return cached
316
+
317
+ try:
318
+ if video_id[:2].upper() == "BV":
319
+ v = video.Video(bvid="BV" + video_id[2:])
320
+ elif video_id.startswith("av"):
321
+ v = video.Video(aid=int(video_id[2:]))
322
+ else:
323
+ return None
324
+
325
+ info = await v.get_info()
326
+ stat = info.get("stat", {})
327
+
328
+ result = {
329
+ "bvid": info.get("bvid", ""),
330
+ "aid": info.get("aid", 0),
331
+ "title": info.get("title", "未知标题"),
332
+ "cover": info.get("pic", ""),
333
+ "duration": _format_duration(info.get("duration", 0)),
334
+ "description": info.get("desc", ""),
335
+ "pubdate": info.get("pubdate", 0),
336
+ "owner": {
337
+ "name": info.get("owner", {}).get("name", "未知UP主"),
338
+ "face": info.get("owner", {}).get("face", ""),
339
+ "mid": info.get("owner", {}).get("mid", 0),
340
+ },
341
+ "stat": {
342
+ "view": stat.get("view", 0),
343
+ "danmaku": stat.get("danmaku", 0),
344
+ "like": stat.get("like", 0),
345
+ "coin": stat.get("coin", 0),
346
+ "favorite": stat.get("favorite", 0),
347
+ "share": stat.get("share", 0),
348
+ "reply": stat.get("reply", 0),
349
+ },
350
+ "tid": info.get("tid", 0),
351
+ "videos": info.get("videos", 1),
352
+ }
353
+
354
+ try:
355
+ tag_result = await v.get_tags()
356
+ if isinstance(tag_result, list):
357
+ result["tags"] = [t.get("tag_name", "") for t in tag_result[:5]]
358
+ else:
359
+ result["tags"] = []
360
+ except Exception:
361
+ result["tags"] = []
362
+
363
+ self._set_cache(video_id, result)
364
+ return result
365
+
366
+ except Exception as e:
367
+ self.logger.error(f"解析视频 {video_id} 失败: {e}")
368
+ return None
369
+
370
+ async def get_hot_comments(
371
+ self, aid: int, count: int = 3
372
+ ) -> List[dict]:
373
+ cache_key = f"comments_{aid}"
374
+ cached = self._get_cache(cache_key)
375
+ if cached:
376
+ return cached[:count]
377
+
378
+ try:
379
+ comments_data = await comment.get_comments(
380
+ oid=aid,
381
+ type_=comment.CommentResourceType.VIDEO,
382
+ order=comment.OrderType.LIKE,
383
+ )
384
+
385
+ comments = []
386
+ for c in comments_data.get("replies", []) or []:
387
+ member = c.get("member", {})
388
+ content = c.get("content", {})
389
+ comments.append({
390
+ "user": member.get("uname", "匿名"),
391
+ "content": content.get("message", ""),
392
+ "like": c.get("like", 0),
393
+ })
394
+ if len(comments) >= 10:
395
+ break
396
+
397
+ self._set_cache(cache_key, comments)
398
+ return comments[:count]
399
+
400
+ except Exception as e:
401
+ self.logger.warning(f"获取视频 av{aid} 评论失败: {e}")
402
+ return []
403
+
404
+
405
+ class Main(BaseModule):
406
+ def __init__(self):
407
+ self.sdk = sdk
408
+ self.logger = sdk.logger.get_child("BiliParser")
409
+ self.config = self._load_config()
410
+ self.parser = BiliVideoParser(self.logger, self.config)
411
+
412
+ @staticmethod
413
+ def get_load_strategy():
414
+ from ErisPulse.loaders import ModuleLoadStrategy
415
+ return ModuleLoadStrategy(
416
+ lazy_load=False,
417
+ priority=0,
418
+ )
419
+
420
+ def _load_config(self) -> dict:
421
+ config = sdk.config.getConfig("BiliParser")
422
+ if not config:
423
+ default_config = {
424
+ "auto_parse": True,
425
+ "show_cover": True,
426
+ "show_comments": True,
427
+ "comment_count": 3,
428
+ "show_description": False,
429
+ "max_desc_length": 100,
430
+ "cache_ttl": 600,
431
+ "max_videos_per_message": 3,
432
+ }
433
+ sdk.config.setConfig("BiliParser", default_config, immediate=True)
434
+ self.logger.info("已创建默认配置")
435
+ return default_config
436
+ return config
437
+
438
+ async def on_load(self, event):
439
+ self._register_commands()
440
+
441
+ if self.config.get("auto_parse", True):
442
+ self._register_auto_parse()
443
+
444
+ self.logger.info("BiliParser 模块已加载")
445
+
446
+ async def on_unload(self, event):
447
+ self.logger.info("BiliParser 模块已卸载")
448
+
449
+ def _register_commands(self):
450
+ @command("bili", help="解析B站视频链接")
451
+ async def bili_cmd(event):
452
+ args = event.get_command_args()
453
+ if not args:
454
+ await event.reply("用法: /bili <BV号/AV号/链接>")
455
+ return
456
+
457
+ text = " ".join(args)
458
+ ids = self.parser.extract_ids(text)
459
+
460
+ resolved_ids = await self._resolve_all_ids(ids)
461
+ if not resolved_ids:
462
+ await event.reply("未找到有效的B站视频链接")
463
+ return
464
+
465
+ await self._send_video_info(event, resolved_ids[0])
466
+
467
+ def _register_auto_parse(self):
468
+ @message.on_message(priority=50)
469
+ async def auto_parse_handler(event):
470
+ if event.is_command():
471
+ return
472
+
473
+ text = event.get_text()
474
+ if not text:
475
+ return
476
+
477
+ ids = self.parser.extract_ids(text)
478
+ if not ids:
479
+ return
480
+
481
+ resolved_ids = await self._resolve_all_ids(ids)
482
+ max_count = self.config.get("max_videos_per_message", 3)
483
+
484
+ for vid in resolved_ids[:max_count]:
485
+ await self._send_video_info(event, vid)
486
+
487
+ async def _resolve_all_ids(self, ids: list) -> List[str]:
488
+ resolved = []
489
+ for item in ids:
490
+ if isinstance(item, tuple) and item[0] == "short":
491
+ real_id = await self.parser.resolve_short_url(item[1])
492
+ if real_id:
493
+ resolved.append(real_id)
494
+ else:
495
+ resolved.append(item)
496
+ return resolved
497
+
498
+ def _select_best_format(self, platform: str, templates: Dict[str, str]) -> tuple:
499
+ try:
500
+ supported_methods = sdk.adapter.list_sends(platform)
501
+ if "Html" in supported_methods:
502
+ return ("Html", templates["html"])
503
+ elif "Markdown" in supported_methods:
504
+ return ("Markdown", templates["markdown"])
505
+ else:
506
+ return ("Text", templates["text"])
507
+ except Exception:
508
+ return ("Text", templates["text"])
509
+
510
+ async def _send_video_info(self, event, video_id: str):
511
+ info = await self.parser.parse_video(video_id)
512
+ if not info:
513
+ return
514
+
515
+ cover_url = info.get("cover", "")
516
+ show_cover = self.config.get("show_cover", True)
517
+
518
+ if show_cover and cover_url:
519
+ try:
520
+ await event.reply(cover_url, method="Image")
521
+ except Exception as e:
522
+ self.logger.debug(f"发送封面图失败: {e}")
523
+
524
+ comments = []
525
+ show_comments = self.config.get("show_comments", True)
526
+ comment_count = self.config.get("comment_count", 3)
527
+
528
+ if show_comments and info.get("aid"):
529
+ comments = await self.parser.get_hot_comments(
530
+ info["aid"], comment_count
531
+ )
532
+
533
+ comments_html = BiliTemplates.build_comments_html(comments) if comments else ""
534
+ comments_md = BiliTemplates.build_comments_markdown(comments) if comments else ""
535
+ comments_text = BiliTemplates.build_comments_text(comments) if comments else ""
536
+
537
+ templates_set = {
538
+ "html": BiliTemplates._build_html(info, self.config, comments_html),
539
+ "markdown": BiliTemplates._build_markdown(info, self.config, comments_md),
540
+ "text": BiliTemplates._build_text(info, self.config, comments_text),
541
+ }
542
+
543
+ platform = event.get_platform()
544
+ fmt_name, content = self._select_best_format(platform, templates_set)
545
+
546
+ try:
547
+ await event.reply(content, method=fmt_name)
548
+ except Exception:
549
+ await event.reply(templates_set["text"])
@@ -0,0 +1 @@
1
+ from .Core import Main
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-BiliParser
3
+ Version: 1.0.0
4
+ Summary: B站视频解析模块,自动解析视频链接并展示详细信息、热门评论与弹幕
5
+ Author-email: wsu2059 <wsu2059@qq.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: bilibili-api-python>=16.0.0
9
+
10
+ # ErisPulse-BiliParser
11
+
12
+ B站视频解析模块,自动解析消息中的B站视频链接并展示详细信息。
13
+
14
+ ## 功能
15
+
16
+ - 自动检测消息中的B站视频链接(支持 BV号、AV号、完整链接、b23.tv短链接)
17
+ - 手动 `/bili` 命令解析
18
+ - 输出封面图 + 视频详情(标题、UP主、播放量、弹幕、点赞、投币、收藏、分享)
19
+ - 热门评论展示
20
+ - 多平台富文本适配(HTML > Markdown > 纯文本自动回退)
21
+ - 解析结果缓存
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ epsdk install BiliParser
27
+ ```
28
+
29
+ ## 配置
30
+
31
+ 在 `config.toml` 中添加:
32
+
33
+ ```toml
34
+ [BiliParser]
35
+ auto_parse = true # 自动解析消息中的B站链接
36
+ show_cover = true # 发送封面图
37
+ show_comments = true # 显示热门评论
38
+ comment_count = 3 # 显示评论数量
39
+ show_description = false # 显示视频简介
40
+ max_desc_length = 100 # 简介最大长度
41
+ cache_ttl = 600 # 缓存过期时间(秒)
42
+ max_videos_per_message = 3 # 单条消息最多解析视频数
43
+ ```
44
+
45
+ ## 使用
46
+
47
+ ### 自动解析
48
+
49
+ 在群聊或私聊中发送包含B站链接的消息,模块会自动解析:
50
+
51
+ ```
52
+ 看看这个视频 https://www.bilibili.com/video/BV1xx411c7mD
53
+ ```
54
+
55
+ ### 手动命令
56
+
57
+ ```
58
+ /bili BV1xx411c7mD
59
+ /bili av2
60
+ /bili https://b23.tv/xxxxx
61
+ ```
62
+
63
+ ## 支持的链接格式
64
+
65
+ | 格式 | 示例 |
66
+ |------|------|
67
+ | BV号 | `BV1xx411c7mD` |
68
+ | AV号 | `av2` |
69
+ | 完整链接 | `https://www.bilibili.com/video/BV1xx411c7mD` |
70
+ | 短链接 | `https://b23.tv/xxxxx` |
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ BiliParser/Core.py
4
+ BiliParser/__init__.py
5
+ ErisPulse_BiliParser.egg-info/PKG-INFO
6
+ ErisPulse_BiliParser.egg-info/SOURCES.txt
7
+ ErisPulse_BiliParser.egg-info/dependency_links.txt
8
+ ErisPulse_BiliParser.egg-info/entry_points.txt
9
+ ErisPulse_BiliParser.egg-info/requires.txt
10
+ ErisPulse_BiliParser.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [erispulse.module]
2
+ BiliParser = BiliParser:Main
@@ -0,0 +1 @@
1
+ bilibili-api-python>=16.0.0
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-BiliParser
3
+ Version: 1.0.0
4
+ Summary: B站视频解析模块,自动解析视频链接并展示详细信息、热门评论与弹幕
5
+ Author-email: wsu2059 <wsu2059@qq.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: bilibili-api-python>=16.0.0
9
+
10
+ # ErisPulse-BiliParser
11
+
12
+ B站视频解析模块,自动解析消息中的B站视频链接并展示详细信息。
13
+
14
+ ## 功能
15
+
16
+ - 自动检测消息中的B站视频链接(支持 BV号、AV号、完整链接、b23.tv短链接)
17
+ - 手动 `/bili` 命令解析
18
+ - 输出封面图 + 视频详情(标题、UP主、播放量、弹幕、点赞、投币、收藏、分享)
19
+ - 热门评论展示
20
+ - 多平台富文本适配(HTML > Markdown > 纯文本自动回退)
21
+ - 解析结果缓存
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ epsdk install BiliParser
27
+ ```
28
+
29
+ ## 配置
30
+
31
+ 在 `config.toml` 中添加:
32
+
33
+ ```toml
34
+ [BiliParser]
35
+ auto_parse = true # 自动解析消息中的B站链接
36
+ show_cover = true # 发送封面图
37
+ show_comments = true # 显示热门评论
38
+ comment_count = 3 # 显示评论数量
39
+ show_description = false # 显示视频简介
40
+ max_desc_length = 100 # 简介最大长度
41
+ cache_ttl = 600 # 缓存过期时间(秒)
42
+ max_videos_per_message = 3 # 单条消息最多解析视频数
43
+ ```
44
+
45
+ ## 使用
46
+
47
+ ### 自动解析
48
+
49
+ 在群聊或私聊中发送包含B站链接的消息,模块会自动解析:
50
+
51
+ ```
52
+ 看看这个视频 https://www.bilibili.com/video/BV1xx411c7mD
53
+ ```
54
+
55
+ ### 手动命令
56
+
57
+ ```
58
+ /bili BV1xx411c7mD
59
+ /bili av2
60
+ /bili https://b23.tv/xxxxx
61
+ ```
62
+
63
+ ## 支持的链接格式
64
+
65
+ | 格式 | 示例 |
66
+ |------|------|
67
+ | BV号 | `BV1xx411c7mD` |
68
+ | AV号 | `av2` |
69
+ | 完整链接 | `https://www.bilibili.com/video/BV1xx411c7mD` |
70
+ | 短链接 | `https://b23.tv/xxxxx` |
@@ -0,0 +1,61 @@
1
+ # ErisPulse-BiliParser
2
+
3
+ B站视频解析模块,自动解析消息中的B站视频链接并展示详细信息。
4
+
5
+ ## 功能
6
+
7
+ - 自动检测消息中的B站视频链接(支持 BV号、AV号、完整链接、b23.tv短链接)
8
+ - 手动 `/bili` 命令解析
9
+ - 输出封面图 + 视频详情(标题、UP主、播放量、弹幕、点赞、投币、收藏、分享)
10
+ - 热门评论展示
11
+ - 多平台富文本适配(HTML > Markdown > 纯文本自动回退)
12
+ - 解析结果缓存
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ epsdk install BiliParser
18
+ ```
19
+
20
+ ## 配置
21
+
22
+ 在 `config.toml` 中添加:
23
+
24
+ ```toml
25
+ [BiliParser]
26
+ auto_parse = true # 自动解析消息中的B站链接
27
+ show_cover = true # 发送封面图
28
+ show_comments = true # 显示热门评论
29
+ comment_count = 3 # 显示评论数量
30
+ show_description = false # 显示视频简介
31
+ max_desc_length = 100 # 简介最大长度
32
+ cache_ttl = 600 # 缓存过期时间(秒)
33
+ max_videos_per_message = 3 # 单条消息最多解析视频数
34
+ ```
35
+
36
+ ## 使用
37
+
38
+ ### 自动解析
39
+
40
+ 在群聊或私聊中发送包含B站链接的消息,模块会自动解析:
41
+
42
+ ```
43
+ 看看这个视频 https://www.bilibili.com/video/BV1xx411c7mD
44
+ ```
45
+
46
+ ### 手动命令
47
+
48
+ ```
49
+ /bili BV1xx411c7mD
50
+ /bili av2
51
+ /bili https://b23.tv/xxxxx
52
+ ```
53
+
54
+ ## 支持的链接格式
55
+
56
+ | 格式 | 示例 |
57
+ |------|------|
58
+ | BV号 | `BV1xx411c7mD` |
59
+ | AV号 | `av2` |
60
+ | 完整链接 | `https://www.bilibili.com/video/BV1xx411c7mD` |
61
+ | 短链接 | `https://b23.tv/xxxxx` |
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "ErisPulse-BiliParser"
3
+ version = "1.0.0"
4
+ description = "B站视频解析模块,自动解析视频链接并展示详细信息、热门评论与弹幕"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [ { name = "wsu2059", email = "wsu2059@qq.com" } ]
9
+
10
+ dependencies = [
11
+ "bilibili-api-python>=16.0.0",
12
+ ]
13
+
14
+ [project.entry-points."erispulse.module"]
15
+ "BiliParser" = "BiliParser:Main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+