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.
- erispulse_biliparser-1.0.0/BiliParser/Core.py +549 -0
- erispulse_biliparser-1.0.0/BiliParser/__init__.py +1 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/PKG-INFO +70 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/SOURCES.txt +10 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/dependency_links.txt +1 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/entry_points.txt +2 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/requires.txt +1 -0
- erispulse_biliparser-1.0.0/ErisPulse_BiliParser.egg-info/top_level.txt +1 -0
- erispulse_biliparser-1.0.0/PKG-INFO +70 -0
- erispulse_biliparser-1.0.0/README.md +61 -0
- erispulse_biliparser-1.0.0/pyproject.toml +15 -0
- erispulse_biliparser-1.0.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bilibili-api-python>=16.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BiliParser
|
|
@@ -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"
|