From a9e3bbbb3a4133803957327ea99f8657912eb56b Mon Sep 17 00:00:00 2001 From: molanp <104612722+molanp@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:08:54 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(bilibili):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=93=94=E5=93=A9=E5=93=94=E5=93=A9=E5=9B=BE=E6=96=87=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=A7=A3=E6=9E=90&=E4=BF=AE=E5=A4=8D=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=8D=A0=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持解析哔哩哔哩图文动态内容,并将动态和图文信息合并为统 一接口。同时修复了代码格式化问题,优化了部分函数参数 换行以提高可读性。 - Bug: 字体资源被持续占用未释放 Fixes #433 - Bug: B站动态解析缺少图片资源 Fixes #443 - Bug: 动态解析错误 Fixes #442 --- .../parsers/bilibili/__init__.py | 68 +++++--- .../parsers/bilibili/dynamic.py | 10 +- src/nonebot_plugin_parser/renders/common.py | 152 ++++++++++++++---- tests/parsers/test_bilibili.py | 4 +- 4 files changed, 177 insertions(+), 57 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index b3457717..719ce9d9 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -33,7 +33,9 @@ class BilibiliParser(BaseParser): # 平台信息 - platform: ClassVar[Platform] = Platform(name=PlatformEnum.BILIBILI, display_name="哔哩哔哩") + platform: ClassVar[Platform] = Platform( + name=PlatformEnum.BILIBILI, display_name="哔哩哔哩" + ) def __init__(self): self.headers = HEADERS.copy() @@ -48,7 +50,10 @@ async def _parse_short_link(self, searched: Match[str]): return await self.parse_with_redirect(url) @handle("BV", r"^(?PBV[0-9a-zA-Z]{10})(?:\s)?(?P\d{1,3})?$") - @handle("/BV", r"bilibili\.com(?:/video)?/(?PBV[0-9a-zA-Z]{10})(?:\?p=(?P\d{1,3}))?") + @handle( + "/BV", + r"bilibili\.com(?:/video)?/(?PBV[0-9a-zA-Z]{10})(?:\?p=(?P\d{1,3}))?", + ) async def _parse_bv(self, searched: Match[str]): """解析视频信息""" bvid = str(searched.group("bvid")) @@ -57,7 +62,10 @@ async def _parse_bv(self, searched: Match[str]): return await self.parse_video(bvid=bvid, page_num=page_num) @handle("av", r"^av(?P\d{6,})(?:\s)?(?P\d{1,3})?$") - @handle("/av", r"bilibili\.com(?:/video)?/av(?P\d{6,})(?:\?p=(?P\d{1,3}))?") + @handle( + "/av", + r"bilibili\.com(?:/video)?/av(?P\d{6,})(?:\?p=(?P\d{1,3}))?", + ) async def _parse_av(self, searched: Match[str]): """解析视频信息""" avid = int(searched.group("avid")) @@ -67,10 +75,11 @@ async def _parse_av(self, searched: Match[str]): @handle("/dynamic/", r"bilibili\.com/dynamic/(?P\d+)") @handle("t.bili", r"t\.bilibili\.com/(?P\d+)") + @handle("/opus/", r"bilibili\.com/opus/(?P\d+)") async def _parse_dynamic(self, searched: Match[str]): """解析动态信息""" dynamic_id = int(searched.group("dynamic_id")) - return await self.parse_dynamic(dynamic_id) + return await self.parse_dynamic_or_opus(dynamic_id) @handle("live.bili", r"live\.bilibili\.com/(?P\d+)") async def _parse_live(self, searched: Match[str]): @@ -88,13 +97,7 @@ async def _parse_favlist(self, searched: Match[str]): async def _parse_read(self, searched: Match[str]): """解析专栏信息""" read_id = int(searched.group("read_id")) - return await self.parse_read_with_opus(read_id) - - @handle("/opus/", r"bilibili\.com/opus/(?P\d+)") - async def _parse_opus(self, searched: Match[str]): - """解析图文动态信息""" - opus_id = int(searched.group("opus_id")) - return await self.parse_opus(opus_id) + return await self.parse_read(read_id) async def parse_video( self, @@ -140,7 +143,9 @@ async def download_video(): output_path = pconfig.cache_dir / f"{video_info.bvid}-{page_num}.mp4" if output_path.exists(): return output_path - v_url, a_url = await self.extract_download_urls(video=video, page_index=page_info.index) + v_url, a_url = await self.extract_download_urls( + video=video, page_index=page_info.index + ) if page_info.duration > pconfig.duration_maximum: raise DurationLimitException if a_url is not None: @@ -148,7 +153,9 @@ async def download_video(): v_url, a_url, output_path=output_path, ext_headers=self.headers ) else: - return await DOWNLOADER.streamd(v_url, file_name=output_path.name, ext_headers=self.headers) + return await DOWNLOADER.streamd( + v_url, file_name=output_path.name, ext_headers=self.headers + ) video_task = asyncio.create_task(download_video()) video_content = self.create_video_content( @@ -167,8 +174,8 @@ async def download_video(): extra={"info": ai_summary}, ) - async def parse_dynamic(self, dynamic_id: int): - """解析动态信息 + async def parse_dynamic_or_opus(self, dynamic_id: int): + """解析动态和图文信息 Args: url (str): 动态链接 @@ -178,6 +185,8 @@ async def parse_dynamic(self, dynamic_id: int): from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) + if await dynamic.is_article(): + return await self._parse_opus_obj(dynamic.turn_to_opus()) dynamic_info = convert(await dynamic.get_info(), DynamicData).item author = self.create_author(dynamic_info.name, dynamic_info.avatar) @@ -205,7 +214,7 @@ async def parse_opus(self, opus_id: int): opus = Opus(opus_id, await self.credential) return await self._parse_opus_obj(opus) - async def parse_read_with_opus(self, read_id: int): + async def parse_read(self, read_id: int): """解析专栏信息, 使用 Opus 接口 Args: @@ -242,7 +251,11 @@ async def _parse_opus_obj(self, bili_opus: Opus): for node in opus_data.gen_text_img(): if isinstance(node, ImageNode): - contents.append(self.create_graphics_content(node.url, current_text.strip(), node.alt)) + contents.append( + self.create_graphics_content( + node.url, current_text.strip(), node.alt + ) + ) current_text = "" elif isinstance(node, TextNode): current_text += node.text @@ -319,10 +332,15 @@ async def parse_favlist(self, fav_id: int): title=favdata.title, timestamp=favdata.timestamp, author=self.create_author(favdata.info.upper.name, favdata.info.upper.face), - contents=[self.create_graphics_content(fav.cover, fav.desc) for fav in favdata.medias], + contents=[ + self.create_graphics_content(fav.cover, fav.desc) + for fav in favdata.medias + ], ) - async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video: + async def _get_video( + self, *, bvid: str | None = None, avid: int | None = None + ) -> Video: """解析视频信息 Args: @@ -373,7 +391,9 @@ async def extract_download_urls( video_stream = streams[0] if not isinstance(video_stream, VideoStreamDownloadURL): raise DownloadException("未找到可下载的视频流") - logger.debug(f"视频流质量: {video_stream.video_quality.name}, 编码: {video_stream.video_codecs}") + logger.debug( + f"视频流质量: {video_stream.video_quality.name}, 编码: {video_stream.video_codecs}" + ) audio_stream = streams[1] if not isinstance(audio_stream, AudioStreamDownloadURL): @@ -393,7 +413,9 @@ def _load_credential(self): if not self._cookies_file.exists(): return - self._credential = Credential.from_cookies(json.loads(self._cookies_file.read_text())) + self._credential = Credential.from_cookies( + json.loads(self._cookies_file.read_text()) + ) async def login_with_qrcode(self) -> bytes: """通过二维码登录获取哔哩哔哩登录凭证""" @@ -460,6 +482,8 @@ async def credential(self) -> Credential | None: logger.info(f"哔哩哔哩凭证刷新成功, 保存到 {self._cookies_file}") self._save_credential() else: - logger.warning("哔哩哔哩凭证刷新需要包含 `SESSDATA`, `ac_time_value` 项") + logger.warning( + "哔哩哔哩凭证刷新需要包含 `SESSDATA`, `ac_time_value` 项" + ) return self._credential diff --git a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py index 4a5c84df..ebf8944f 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py @@ -63,9 +63,10 @@ class OpusContent(Struct): class DynamicMajor(Struct): """动态主要内容""" - type: str + type: str | None = None archive: VideoArchive | None = None opus: OpusContent | None = None + desc: OpusSummary | None = None @property def title(self) -> str | None: @@ -81,6 +82,8 @@ def text(self) -> str | None: return self.archive.desc elif self.type == "MAJOR_TYPE_OPUS" and self.opus: return self.opus.summary.text + elif self.desc: + return self.desc.text return None @property @@ -126,7 +129,10 @@ def pub_ts(self) -> int: def major_info(self) -> dict[str, Any] | None: """获取主要内容信息""" if self.module_dynamic: - return self.module_dynamic.get("major") + if major:= self.module_dynamic.get("major"): + return major + # 转发类型动态没有 major + return self.module_dynamic return None diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index fff98147..1d0a689a 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -130,7 +130,9 @@ class FontSet: def new(cls, font_path: Path): font_infos: dict[str, FontInfo] = {} for name, size, fill in cls._FONT_INFOS: - font = ImageFont.truetype(font_path, size) + with open(font_path, "rb") as f: + font_bytes = BytesIO(f.read()) + font = ImageFont.truetype(font_bytes, size, encoding="utf-8") height = get_font_height(font) font_infos[name] = FontInfo( font=font, @@ -360,10 +362,23 @@ async def text( ) -> int: """绘制文本""" if emosvg is not None: - emosvg.text(ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height) + emosvg.text( + ctx.image, + xy, + lines, + font.font, + fill=font.fill, + line_height=font.line_height, + ) else: await Apilmoji.text( - ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height, source=cls.EMOJI_SOURCE + ctx.image, + xy, + lines, + font.font, + fill=font.fill, + line_height=font.line_height, + source=cls.EMOJI_SOURCE, ) return font.line_height * len(lines) @@ -534,7 +549,9 @@ def _load_and_process_avatar(self, avatar: Path | None) -> PILImage | None: return output_avatar - async def _calculate_sections(self, result: ParseResult, content_width: int) -> list[SectionData]: + async def _calculate_sections( + self, result: ParseResult, content_width: int + ) -> list[SectionData]: """计算各部分内容的高度和数据""" sections: list[SectionData] = [] @@ -558,7 +575,9 @@ async def _calculate_sections(self, result: ParseResult, content_width: int) -> await result.cover_path, content_width=content_width, ): - sections.append(CoverSectionData(height=cover_img.height, cover_img=cover_img)) + sections.append( + CoverSectionData(height=cover_img.height, cover_img=cover_img) + ) elif result.img_contents: # 如果没有封面但有图片,处理图片列表 img_grid_section = await self._calculate_image_grid_section( @@ -633,7 +652,9 @@ async def _calculate_graphics_section( ) # 计算总高度:文本高度 + 图片高度 + alt文本高度 + 间距 - text_height = len(text_lines) * self.fontset.text.line_height if text_lines else 0 + text_height = ( + len(text_lines) * self.fontset.text.line_height if text_lines else 0 + ) alt_height = self.fontset.extra.line_height if graphics_content.alt else 0 total_height = text_height + image.height + alt_height if text_lines: @@ -657,7 +678,9 @@ async def _calculate_header_section( return None # 加载头像 - avatar_img = self._load_and_process_avatar(await result.author.get_avatar_path()) + avatar_img = self._load_and_process_avatar( + await result.author.get_avatar_path() + ) text_height = self.fontset.name.line_height time = result.formartted_datetime @@ -715,7 +738,9 @@ async def _calculate_image_grid_section( for img_content in img_contents: img_path = await img_content.get_path() # 使用装饰器保护的方法,失败会返回 None - img = await self._load_and_process_grid_image(img_path, content_width, img_count) + img = await self._load_and_process_grid_image( + img_path, content_width, img_count + ) if img is not None: processed_images.append(img) @@ -743,7 +768,9 @@ async def _calculate_image_grid_section( grid_height = max_img_height else: # 多张图片:上间距 + (图片 + 间距) * 行数 - grid_height = self.IMAGE_GRID_SPACING + rows * (max_img_height + self.IMAGE_GRID_SPACING) + grid_height = self.IMAGE_GRID_SPACING + rows * ( + max_img_height + self.IMAGE_GRID_SPACING + ) return ImageGridSectionData( height=grid_height, @@ -804,7 +831,9 @@ async def _load_and_process_grid_image( else: # 多张图片,使用3列布局 num_gaps = self.IMAGE_GRID_COLS + 1 - max_size = (content_width - self.IMAGE_GRID_SPACING * num_gaps) // self.IMAGE_GRID_COLS + max_size = ( + content_width - self.IMAGE_GRID_SPACING * num_gaps + ) // self.IMAGE_GRID_COLS max_size = min(max_size, self.IMAGE_3_GRID_SIZE) # 调整多张图片的尺寸 @@ -836,7 +865,9 @@ def _crop_to_square(self, img: PILImage) -> PILImage: bottom = top + width return img.crop((0, top, width, bottom)) - async def _draw_sections(self, ctx: RenderContext, sections: list[SectionData]) -> None: + async def _draw_sections( + self, ctx: RenderContext, sections: list[SectionData] + ) -> None: """绘制所有内容到画布上""" for section in sections: match section: @@ -920,7 +951,9 @@ def _create_avatar_placeholder(self) -> PILImage: placeholder.putalpha(mask) return placeholder - async def _draw_header(self, ctx: RenderContext, section: HeaderSectionData) -> None: + async def _draw_header( + self, ctx: RenderContext, section: HeaderSectionData + ) -> None: """绘制 header 部分""" x_pos = self.PADDING @@ -1006,7 +1039,9 @@ async def _draw_text(self, ctx: RenderContext, lines: list[str]) -> None: ) ctx.y_pos += self.SECTION_SPACING - async def _draw_graphics(self, ctx: RenderContext, section: GraphicsSectionData) -> None: + async def _draw_graphics( + self, ctx: RenderContext, section: GraphicsSectionData + ) -> None: """绘制图文内容""" # 绘制文本内容(如果有) if section.text_lines: @@ -1085,7 +1120,9 @@ def _draw_repost(self, ctx: RenderContext, section: RepostSectionData) -> None: ctx.y_pos += repost_height + self.SECTION_SPACING - def _draw_image_grid(self, ctx: RenderContext, section: ImageGridSectionData) -> None: + def _draw_image_grid( + self, ctx: RenderContext, section: ImageGridSectionData + ) -> None: """绘制图片网格""" images = section.images cols = section.cols @@ -1108,7 +1145,9 @@ def _draw_image_grid(self, ctx: RenderContext, section: ImageGridSectionData) -> # 多张图片,统一使用间距计算,确保所有间距相同 num_gaps = cols + 1 # 2列有3个间距,3列有4个间距 calculated_size = (available_width - img_spacing * num_gaps) // cols - max_img_size = self.IMAGE_2_GRID_SIZE if cols == 2 else self.IMAGE_3_GRID_SIZE + max_img_size = ( + self.IMAGE_2_GRID_SIZE if cols == 2 else self.IMAGE_3_GRID_SIZE + ) max_img_size = min(calculated_size, max_img_size) current_y = ctx.y_pos @@ -1133,7 +1172,12 @@ def _draw_image_grid(self, ctx: RenderContext, section: ImageGridSectionData) -> ctx.image.paste(img, (img_x, img_y + y_offset)) # 如果是最后一张图片且有更多图片,绘制+N效果 - if has_more and row == rows - 1 and i == len(row_images) - 1 and len(images) == self.MAX_IMAGES_DISPLAY: + if ( + has_more + and row == rows - 1 + and i == len(row_images) - 1 + and len(images) == self.MAX_IMAGES_DISPLAY + ): self._draw_more_indicator( ctx.image, img_x, @@ -1162,7 +1206,9 @@ def _draw_more_indicator( # 创建半透明黑色遮罩(透明度 1/4) overlay = Image.new("RGBA", (img_width, img_height), (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) - overlay_draw.rectangle((0, 0, img_width - 1, img_height - 1), fill=(0, 0, 0, 100)) + overlay_draw.rectangle( + (0, 0, img_width - 1, img_height - 1), fill=(0, 0, 0, 100) + ) # 将遮罩贴到图片上 image.paste(overlay, (img_x, img_y), overlay) @@ -1176,7 +1222,9 @@ def _draw_more_indicator( text_y = img_y + (img_height - indicator_font.line_height) // 2 # 绘制50%透明白色文字 - draw.text((text_x, text_y), text, fill=indicator_font.fill, font=indicator_font.font) + draw.text( + (text_x, text_y), text, fill=indicator_font.fill, font=indicator_font.font + ) def _draw_rounded_rectangle( self, @@ -1194,10 +1242,18 @@ def _draw_rounded_rectangle( draw.rectangle((x1, y1 + radius, x2, y2 - radius), fill=fill_color) # 绘制四个圆角 - draw.pieslice((x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=fill_color) - draw.pieslice((x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=fill_color) - draw.pieslice((x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=fill_color) - draw.pieslice((x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=fill_color) + draw.pieslice( + (x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=fill_color + ) + draw.pieslice( + (x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=fill_color + ) + draw.pieslice( + (x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=fill_color + ) + draw.pieslice( + (x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=fill_color + ) def _draw_rounded_rectangle_border( self, @@ -1211,16 +1267,48 @@ def _draw_rounded_rectangle_border( x1, y1, x2, y2 = bbox # 绘制主体边框 - draw.rectangle((x1 + radius, y1, x2 - radius, y1 + width), fill=border_color) # 上 - draw.rectangle((x1 + radius, y2 - width, x2 - radius, y2), fill=border_color) # 下 - draw.rectangle((x1, y1 + radius, x1 + width, y2 - radius), fill=border_color) # 左 - draw.rectangle((x2 - width, y1 + radius, x2, y2 - radius), fill=border_color) # 右 + draw.rectangle( + (x1 + radius, y1, x2 - radius, y1 + width), fill=border_color + ) # 上 + draw.rectangle( + (x1 + radius, y2 - width, x2 - radius, y2), fill=border_color + ) # 下 + draw.rectangle( + (x1, y1 + radius, x1 + width, y2 - radius), fill=border_color + ) # 左 + draw.rectangle( + (x2 - width, y1 + radius, x2, y2 - radius), fill=border_color + ) # 右 # 绘制四个圆角边框 - draw.arc((x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=border_color, width=width) - draw.arc((x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=border_color, width=width) - draw.arc((x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=border_color, width=width) - draw.arc((x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=border_color, width=width) + draw.arc( + (x1, y1, x1 + 2 * radius, y1 + 2 * radius), + 180, + 270, + fill=border_color, + width=width, + ) + draw.arc( + (x2 - 2 * radius, y1, x2, y1 + 2 * radius), + 270, + 360, + fill=border_color, + width=width, + ) + draw.arc( + (x1, y2 - 2 * radius, x1 + 2 * radius, y2), + 90, + 180, + fill=border_color, + width=width, + ) + draw.arc( + (x2 - 2 * radius, y2 - 2 * radius, x2, y2), + 0, + 90, + fill=border_color, + width=width, + ) def _wrap_text(self, text: str, max_width: int, font_info: FontInfo) -> list[str]: """使用 emoji.emoji_list 优化的文本自动换行算法,正确处理组合 emoji @@ -1300,7 +1388,9 @@ def is_punctuation(char: str) -> bool: return lines - def _wrap_text_old(self, text: str, max_width: int, font_info: FontInfo) -> list[str]: + def _wrap_text_old( + self, text: str, max_width: int, font_info: FontInfo + ) -> list[str]: """优化的文本自动换行算法,考虑中英文字符宽度相同 Args: diff --git a/tests/parsers/test_bilibili.py b/tests/parsers/test_bilibili.py index 4cc8a70d..f0b7f860 100644 --- a/tests/parsers/test_bilibili.py +++ b/tests/parsers/test_bilibili.py @@ -43,7 +43,7 @@ async def test_read(): parser = BilibiliParser() _, searched = parser.search_url(url) read_id = int(searched.group("read_id")) - result = await parser.parse_read_with_opus(read_id) + result = await parser.parse_read(read_id) logger.debug(f"result: {result}") assert result.title, "标题为空" assert result.author, "作者为空" @@ -110,7 +110,7 @@ async def test_dynamic(): async def test_parse_dynamic(dynamic_url: str) -> None: _, searched = parser.search_url(dynamic_url) dynamic_id = int(searched.group("dynamic_id")) - result = await parser.parse_dynamic(dynamic_id) + result = await parser.parse_dynamic_or_opus(dynamic_id) assert result.title, "标题为空" assert result.author, "作者为空" avatar_path = await result.author.get_avatar_path() From 82c55afa00732c3a930fadae09ea96ba3068dae5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:09:35 +0000 Subject: [PATCH 2/8] chore: auto fix by pre-commit hooks --- .../parsers/bilibili/__init__.py | 39 ++------ .../parsers/bilibili/dynamic.py | 2 +- src/nonebot_plugin_parser/renders/common.py | 99 +++++-------------- 3 files changed, 34 insertions(+), 106 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 719ce9d9..6f15346d 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -33,9 +33,7 @@ class BilibiliParser(BaseParser): # 平台信息 - platform: ClassVar[Platform] = Platform( - name=PlatformEnum.BILIBILI, display_name="哔哩哔哩" - ) + platform: ClassVar[Platform] = Platform(name=PlatformEnum.BILIBILI, display_name="哔哩哔哩") def __init__(self): self.headers = HEADERS.copy() @@ -143,9 +141,7 @@ async def download_video(): output_path = pconfig.cache_dir / f"{video_info.bvid}-{page_num}.mp4" if output_path.exists(): return output_path - v_url, a_url = await self.extract_download_urls( - video=video, page_index=page_info.index - ) + v_url, a_url = await self.extract_download_urls(video=video, page_index=page_info.index) if page_info.duration > pconfig.duration_maximum: raise DurationLimitException if a_url is not None: @@ -153,9 +149,7 @@ async def download_video(): v_url, a_url, output_path=output_path, ext_headers=self.headers ) else: - return await DOWNLOADER.streamd( - v_url, file_name=output_path.name, ext_headers=self.headers - ) + return await DOWNLOADER.streamd(v_url, file_name=output_path.name, ext_headers=self.headers) video_task = asyncio.create_task(download_video()) video_content = self.create_video_content( @@ -251,11 +245,7 @@ async def _parse_opus_obj(self, bili_opus: Opus): for node in opus_data.gen_text_img(): if isinstance(node, ImageNode): - contents.append( - self.create_graphics_content( - node.url, current_text.strip(), node.alt - ) - ) + contents.append(self.create_graphics_content(node.url, current_text.strip(), node.alt)) current_text = "" elif isinstance(node, TextNode): current_text += node.text @@ -332,15 +322,10 @@ async def parse_favlist(self, fav_id: int): title=favdata.title, timestamp=favdata.timestamp, author=self.create_author(favdata.info.upper.name, favdata.info.upper.face), - contents=[ - self.create_graphics_content(fav.cover, fav.desc) - for fav in favdata.medias - ], + contents=[self.create_graphics_content(fav.cover, fav.desc) for fav in favdata.medias], ) - async def _get_video( - self, *, bvid: str | None = None, avid: int | None = None - ) -> Video: + async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video: """解析视频信息 Args: @@ -391,9 +376,7 @@ async def extract_download_urls( video_stream = streams[0] if not isinstance(video_stream, VideoStreamDownloadURL): raise DownloadException("未找到可下载的视频流") - logger.debug( - f"视频流质量: {video_stream.video_quality.name}, 编码: {video_stream.video_codecs}" - ) + logger.debug(f"视频流质量: {video_stream.video_quality.name}, 编码: {video_stream.video_codecs}") audio_stream = streams[1] if not isinstance(audio_stream, AudioStreamDownloadURL): @@ -413,9 +396,7 @@ def _load_credential(self): if not self._cookies_file.exists(): return - self._credential = Credential.from_cookies( - json.loads(self._cookies_file.read_text()) - ) + self._credential = Credential.from_cookies(json.loads(self._cookies_file.read_text())) async def login_with_qrcode(self) -> bytes: """通过二维码登录获取哔哩哔哩登录凭证""" @@ -482,8 +463,6 @@ async def credential(self) -> Credential | None: logger.info(f"哔哩哔哩凭证刷新成功, 保存到 {self._cookies_file}") self._save_credential() else: - logger.warning( - "哔哩哔哩凭证刷新需要包含 `SESSDATA`, `ac_time_value` 项" - ) + logger.warning("哔哩哔哩凭证刷新需要包含 `SESSDATA`, `ac_time_value` 项") return self._credential diff --git a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py index ebf8944f..0737f92d 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py @@ -129,7 +129,7 @@ def pub_ts(self) -> int: def major_info(self) -> dict[str, Any] | None: """获取主要内容信息""" if self.module_dynamic: - if major:= self.module_dynamic.get("major"): + if major := self.module_dynamic.get("major"): return major # 转发类型动态没有 major return self.module_dynamic diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 1d0a689a..692b5743 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -549,9 +549,7 @@ def _load_and_process_avatar(self, avatar: Path | None) -> PILImage | None: return output_avatar - async def _calculate_sections( - self, result: ParseResult, content_width: int - ) -> list[SectionData]: + async def _calculate_sections(self, result: ParseResult, content_width: int) -> list[SectionData]: """计算各部分内容的高度和数据""" sections: list[SectionData] = [] @@ -575,9 +573,7 @@ async def _calculate_sections( await result.cover_path, content_width=content_width, ): - sections.append( - CoverSectionData(height=cover_img.height, cover_img=cover_img) - ) + sections.append(CoverSectionData(height=cover_img.height, cover_img=cover_img)) elif result.img_contents: # 如果没有封面但有图片,处理图片列表 img_grid_section = await self._calculate_image_grid_section( @@ -652,9 +648,7 @@ async def _calculate_graphics_section( ) # 计算总高度:文本高度 + 图片高度 + alt文本高度 + 间距 - text_height = ( - len(text_lines) * self.fontset.text.line_height if text_lines else 0 - ) + text_height = len(text_lines) * self.fontset.text.line_height if text_lines else 0 alt_height = self.fontset.extra.line_height if graphics_content.alt else 0 total_height = text_height + image.height + alt_height if text_lines: @@ -678,9 +672,7 @@ async def _calculate_header_section( return None # 加载头像 - avatar_img = self._load_and_process_avatar( - await result.author.get_avatar_path() - ) + avatar_img = self._load_and_process_avatar(await result.author.get_avatar_path()) text_height = self.fontset.name.line_height time = result.formartted_datetime @@ -738,9 +730,7 @@ async def _calculate_image_grid_section( for img_content in img_contents: img_path = await img_content.get_path() # 使用装饰器保护的方法,失败会返回 None - img = await self._load_and_process_grid_image( - img_path, content_width, img_count - ) + img = await self._load_and_process_grid_image(img_path, content_width, img_count) if img is not None: processed_images.append(img) @@ -768,9 +758,7 @@ async def _calculate_image_grid_section( grid_height = max_img_height else: # 多张图片:上间距 + (图片 + 间距) * 行数 - grid_height = self.IMAGE_GRID_SPACING + rows * ( - max_img_height + self.IMAGE_GRID_SPACING - ) + grid_height = self.IMAGE_GRID_SPACING + rows * (max_img_height + self.IMAGE_GRID_SPACING) return ImageGridSectionData( height=grid_height, @@ -831,9 +819,7 @@ async def _load_and_process_grid_image( else: # 多张图片,使用3列布局 num_gaps = self.IMAGE_GRID_COLS + 1 - max_size = ( - content_width - self.IMAGE_GRID_SPACING * num_gaps - ) // self.IMAGE_GRID_COLS + max_size = (content_width - self.IMAGE_GRID_SPACING * num_gaps) // self.IMAGE_GRID_COLS max_size = min(max_size, self.IMAGE_3_GRID_SIZE) # 调整多张图片的尺寸 @@ -865,9 +851,7 @@ def _crop_to_square(self, img: PILImage) -> PILImage: bottom = top + width return img.crop((0, top, width, bottom)) - async def _draw_sections( - self, ctx: RenderContext, sections: list[SectionData] - ) -> None: + async def _draw_sections(self, ctx: RenderContext, sections: list[SectionData]) -> None: """绘制所有内容到画布上""" for section in sections: match section: @@ -951,9 +935,7 @@ def _create_avatar_placeholder(self) -> PILImage: placeholder.putalpha(mask) return placeholder - async def _draw_header( - self, ctx: RenderContext, section: HeaderSectionData - ) -> None: + async def _draw_header(self, ctx: RenderContext, section: HeaderSectionData) -> None: """绘制 header 部分""" x_pos = self.PADDING @@ -1039,9 +1021,7 @@ async def _draw_text(self, ctx: RenderContext, lines: list[str]) -> None: ) ctx.y_pos += self.SECTION_SPACING - async def _draw_graphics( - self, ctx: RenderContext, section: GraphicsSectionData - ) -> None: + async def _draw_graphics(self, ctx: RenderContext, section: GraphicsSectionData) -> None: """绘制图文内容""" # 绘制文本内容(如果有) if section.text_lines: @@ -1120,9 +1100,7 @@ def _draw_repost(self, ctx: RenderContext, section: RepostSectionData) -> None: ctx.y_pos += repost_height + self.SECTION_SPACING - def _draw_image_grid( - self, ctx: RenderContext, section: ImageGridSectionData - ) -> None: + def _draw_image_grid(self, ctx: RenderContext, section: ImageGridSectionData) -> None: """绘制图片网格""" images = section.images cols = section.cols @@ -1145,9 +1123,7 @@ def _draw_image_grid( # 多张图片,统一使用间距计算,确保所有间距相同 num_gaps = cols + 1 # 2列有3个间距,3列有4个间距 calculated_size = (available_width - img_spacing * num_gaps) // cols - max_img_size = ( - self.IMAGE_2_GRID_SIZE if cols == 2 else self.IMAGE_3_GRID_SIZE - ) + max_img_size = self.IMAGE_2_GRID_SIZE if cols == 2 else self.IMAGE_3_GRID_SIZE max_img_size = min(calculated_size, max_img_size) current_y = ctx.y_pos @@ -1172,12 +1148,7 @@ def _draw_image_grid( ctx.image.paste(img, (img_x, img_y + y_offset)) # 如果是最后一张图片且有更多图片,绘制+N效果 - if ( - has_more - and row == rows - 1 - and i == len(row_images) - 1 - and len(images) == self.MAX_IMAGES_DISPLAY - ): + if has_more and row == rows - 1 and i == len(row_images) - 1 and len(images) == self.MAX_IMAGES_DISPLAY: self._draw_more_indicator( ctx.image, img_x, @@ -1206,9 +1177,7 @@ def _draw_more_indicator( # 创建半透明黑色遮罩(透明度 1/4) overlay = Image.new("RGBA", (img_width, img_height), (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) - overlay_draw.rectangle( - (0, 0, img_width - 1, img_height - 1), fill=(0, 0, 0, 100) - ) + overlay_draw.rectangle((0, 0, img_width - 1, img_height - 1), fill=(0, 0, 0, 100)) # 将遮罩贴到图片上 image.paste(overlay, (img_x, img_y), overlay) @@ -1222,9 +1191,7 @@ def _draw_more_indicator( text_y = img_y + (img_height - indicator_font.line_height) // 2 # 绘制50%透明白色文字 - draw.text( - (text_x, text_y), text, fill=indicator_font.fill, font=indicator_font.font - ) + draw.text((text_x, text_y), text, fill=indicator_font.fill, font=indicator_font.font) def _draw_rounded_rectangle( self, @@ -1242,18 +1209,10 @@ def _draw_rounded_rectangle( draw.rectangle((x1, y1 + radius, x2, y2 - radius), fill=fill_color) # 绘制四个圆角 - draw.pieslice( - (x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=fill_color - ) - draw.pieslice( - (x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=fill_color - ) - draw.pieslice( - (x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=fill_color - ) - draw.pieslice( - (x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=fill_color - ) + draw.pieslice((x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=fill_color) + draw.pieslice((x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=fill_color) + draw.pieslice((x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=fill_color) + draw.pieslice((x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=fill_color) def _draw_rounded_rectangle_border( self, @@ -1267,18 +1226,10 @@ def _draw_rounded_rectangle_border( x1, y1, x2, y2 = bbox # 绘制主体边框 - draw.rectangle( - (x1 + radius, y1, x2 - radius, y1 + width), fill=border_color - ) # 上 - draw.rectangle( - (x1 + radius, y2 - width, x2 - radius, y2), fill=border_color - ) # 下 - draw.rectangle( - (x1, y1 + radius, x1 + width, y2 - radius), fill=border_color - ) # 左 - draw.rectangle( - (x2 - width, y1 + radius, x2, y2 - radius), fill=border_color - ) # 右 + draw.rectangle((x1 + radius, y1, x2 - radius, y1 + width), fill=border_color) # 上 + draw.rectangle((x1 + radius, y2 - width, x2 - radius, y2), fill=border_color) # 下 + draw.rectangle((x1, y1 + radius, x1 + width, y2 - radius), fill=border_color) # 左 + draw.rectangle((x2 - width, y1 + radius, x2, y2 - radius), fill=border_color) # 右 # 绘制四个圆角边框 draw.arc( @@ -1388,9 +1339,7 @@ def is_punctuation(char: str) -> bool: return lines - def _wrap_text_old( - self, text: str, max_width: int, font_info: FontInfo - ) -> list[str]: + def _wrap_text_old(self, text: str, max_width: int, font_info: FontInfo) -> list[str]: """优化的文本自动换行算法,考虑中英文字符宽度相同 Args: From cfc958dc56c7e39b9ca2afbba0bd4f96ed87d9e9 Mon Sep 17 00:00:00 2001 From: molanp <104612722+molanp@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:15:20 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(tests):=20=E4=BF=AE=E5=A4=8Dbilibili?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除重复的opus测试函数,将测试数据合并到dynamic测试中, 统一测试逻辑并优化代码结构,提高测试效率。 --- tests/parsers/test_bilibili.py | 48 +++++----------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/tests/parsers/test_bilibili.py b/tests/parsers/test_bilibili.py index f0b7f860..0a5c826d 100644 --- a/tests/parsers/test_bilibili.py +++ b/tests/parsers/test_bilibili.py @@ -60,53 +60,17 @@ async def test_read(): @pytest.mark.asyncio -async def test_opus(): +async def test_dynamic(): from nonebot_plugin_parser.parsers import BilibiliParser - opus_urls = [ + dynamic_urls = [ + "https://t.bilibili.com/1120105154190770281", "https://www.bilibili.com/opus/998440765151510535", - "https://www.bilibili.com/opus/1040093151889457152", + "https://www.bilibili.com/opus/1040093151889457152" ] parser = BilibiliParser() - async def test_parse_opus(opus_url: str) -> None: - _, searched = parser.search_url(opus_url) - opus_id = int(searched.group("opus_id")) - try: - result = await parser.parse_opus(opus_id) - except Exception as e: - pytest.skip(f"{opus_url} | opus 解析失败: {e} (风控)") - - assert result.contents, "内容为空" - for content in result.contents: - path = await content.get_path() - assert path.exists(), "内容不存在" - - assert result.author, "作者为空" - avatar_path = await result.author.get_avatar_path() - assert avatar_path, "头像不存在" - assert avatar_path.exists(), "头像不存在" - - graphics_contents = result.graphics_contents - assert graphics_contents, "图文内容为空" - - for graphics_content in graphics_contents: - path = await graphics_content.get_path() - assert path.exists(), "图文内容不存在" - - await asyncio.gather(*[test_parse_opus(opus_url) for opus_url in opus_urls]) - logger.success("B站动态解析成功") - - -@pytest.mark.asyncio -async def test_dynamic(): - from nonebot_plugin_parser.parsers import BilibiliParser - - dynamic_urls = ["https://t.bilibili.com/1120105154190770281"] - - parser = BilibiliParser() - async def test_parse_dynamic(dynamic_url: str) -> None: _, searched = parser.search_url(dynamic_url) dynamic_id = int(searched.group("dynamic_id")) @@ -122,5 +86,7 @@ async def test_parse_dynamic(dynamic_url: str) -> None: path = await img_content.get_path() assert path.exists(), "图片不存在" - await asyncio.gather(*[test_parse_dynamic(dynamic_url) for dynamic_url in dynamic_urls]) + await asyncio.gather( + *[test_parse_dynamic(dynamic_url) for dynamic_url in dynamic_urls] + ) logger.success("B站动态解析成功") From 44d456f4f8d4aae2afac6c0d1e85d6ba3ed5abaa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:15:40 +0000 Subject: [PATCH 4/8] chore: auto fix by pre-commit hooks --- tests/parsers/test_bilibili.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/parsers/test_bilibili.py b/tests/parsers/test_bilibili.py index 0a5c826d..0d7e07b5 100644 --- a/tests/parsers/test_bilibili.py +++ b/tests/parsers/test_bilibili.py @@ -66,7 +66,7 @@ async def test_dynamic(): dynamic_urls = [ "https://t.bilibili.com/1120105154190770281", "https://www.bilibili.com/opus/998440765151510535", - "https://www.bilibili.com/opus/1040093151889457152" + "https://www.bilibili.com/opus/1040093151889457152", ] parser = BilibiliParser() @@ -86,7 +86,5 @@ async def test_parse_dynamic(dynamic_url: str) -> None: path = await img_content.get_path() assert path.exists(), "图片不存在" - await asyncio.gather( - *[test_parse_dynamic(dynamic_url) for dynamic_url in dynamic_urls] - ) + await asyncio.gather(*[test_parse_dynamic(dynamic_url) for dynamic_url in dynamic_urls]) logger.success("B站动态解析成功") From 3c8e1960bfe5bbbe8209cc57dd2fc56caa55ec24 Mon Sep 17 00:00:00 2001 From: molanp <104612722+molanp@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:21:13 +0800 Subject: [PATCH 5/8] =?UTF-8?q?test(bilibili):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=A7=A3=E6=9E=90=E6=B5=8B=E8=AF=95=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=A0=87=E9=A2=98=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了对动态解析结果中标题字段的非空断言, 保留了作者字段的断言以确保基本功能正常 --- tests/parsers/test_bilibili.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/parsers/test_bilibili.py b/tests/parsers/test_bilibili.py index 0d7e07b5..b3ddd762 100644 --- a/tests/parsers/test_bilibili.py +++ b/tests/parsers/test_bilibili.py @@ -75,7 +75,6 @@ async def test_parse_dynamic(dynamic_url: str) -> None: _, searched = parser.search_url(dynamic_url) dynamic_id = int(searched.group("dynamic_id")) result = await parser.parse_dynamic_or_opus(dynamic_id) - assert result.title, "标题为空" assert result.author, "作者为空" avatar_path = await result.author.get_avatar_path() assert avatar_path, "头像不存在" From 4d60f93c91f2fcb95e2e2b0f95b1b348108fd2f1 Mon Sep 17 00:00:00 2001 From: molanp <104612722+molanp@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:55:18 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E7=9A=84=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 6f15346d..d8d42143 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -48,10 +48,7 @@ async def _parse_short_link(self, searched: Match[str]): return await self.parse_with_redirect(url) @handle("BV", r"^(?PBV[0-9a-zA-Z]{10})(?:\s)?(?P\d{1,3})?$") - @handle( - "/BV", - r"bilibili\.com(?:/video)?/(?PBV[0-9a-zA-Z]{10})(?:\?p=(?P\d{1,3}))?", - ) + @handle("/BV", r"bilibili\.com(?:/video)?/(?PBV[0-9a-zA-Z]{10})(?:\?p=(?P\d{1,3}))?") async def _parse_bv(self, searched: Match[str]): """解析视频信息""" bvid = str(searched.group("bvid")) @@ -60,10 +57,7 @@ async def _parse_bv(self, searched: Match[str]): return await self.parse_video(bvid=bvid, page_num=page_num) @handle("av", r"^av(?P\d{6,})(?:\s)?(?P\d{1,3})?$") - @handle( - "/av", - r"bilibili\.com(?:/video)?/av(?P\d{6,})(?:\?p=(?P\d{1,3}))?", - ) + @handle("/av", r"bilibili\.com(?:/video)?/av(?P\d{6,})(?:\?p=(?P\d{1,3}))?") async def _parse_av(self, searched: Match[str]): """解析视频信息""" avid = int(searched.group("avid")) From 4da618c52ed1772637e11efcd19c639aa6a346c2 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sat, 31 Jan 2026 17:09:33 +0800 Subject: [PATCH 7/8] Revert common --- src/nonebot_plugin_parser/renders/common.py | 53 +++------------------ 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 692b5743..fff98147 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -130,9 +130,7 @@ class FontSet: def new(cls, font_path: Path): font_infos: dict[str, FontInfo] = {} for name, size, fill in cls._FONT_INFOS: - with open(font_path, "rb") as f: - font_bytes = BytesIO(f.read()) - font = ImageFont.truetype(font_bytes, size, encoding="utf-8") + font = ImageFont.truetype(font_path, size) height = get_font_height(font) font_infos[name] = FontInfo( font=font, @@ -362,23 +360,10 @@ async def text( ) -> int: """绘制文本""" if emosvg is not None: - emosvg.text( - ctx.image, - xy, - lines, - font.font, - fill=font.fill, - line_height=font.line_height, - ) + emosvg.text(ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height) else: await Apilmoji.text( - ctx.image, - xy, - lines, - font.font, - fill=font.fill, - line_height=font.line_height, - source=cls.EMOJI_SOURCE, + ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height, source=cls.EMOJI_SOURCE ) return font.line_height * len(lines) @@ -1232,34 +1217,10 @@ def _draw_rounded_rectangle_border( draw.rectangle((x2 - width, y1 + radius, x2, y2 - radius), fill=border_color) # 右 # 绘制四个圆角边框 - draw.arc( - (x1, y1, x1 + 2 * radius, y1 + 2 * radius), - 180, - 270, - fill=border_color, - width=width, - ) - draw.arc( - (x2 - 2 * radius, y1, x2, y1 + 2 * radius), - 270, - 360, - fill=border_color, - width=width, - ) - draw.arc( - (x1, y2 - 2 * radius, x1 + 2 * radius, y2), - 90, - 180, - fill=border_color, - width=width, - ) - draw.arc( - (x2 - 2 * radius, y2 - 2 * radius, x2, y2), - 0, - 90, - fill=border_color, - width=width, - ) + draw.arc((x1, y1, x1 + 2 * radius, y1 + 2 * radius), 180, 270, fill=border_color, width=width) + draw.arc((x2 - 2 * radius, y1, x2, y1 + 2 * radius), 270, 360, fill=border_color, width=width) + draw.arc((x1, y2 - 2 * radius, x1 + 2 * radius, y2), 90, 180, fill=border_color, width=width) + draw.arc((x2 - 2 * radius, y2 - 2 * radius, x2, y2), 0, 90, fill=border_color, width=width) def _wrap_text(self, text: str, max_width: int, font_info: FontInfo) -> list[str]: """使用 emoji.emoji_list 优化的文本自动换行算法,正确处理组合 emoji From 2bace281cd985baf8de69059cccd1fec5cc44ffd Mon Sep 17 00:00:00 2001 From: fllesser Date: Sat, 31 Jan 2026 17:19:42 +0800 Subject: [PATCH 8/8] Tweaks --- .../parsers/bilibili/__init__.py | 27 +++++++------------ tests/parsers/test_bilibili.py | 3 +-- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index d8d42143..2598c698 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -88,8 +88,12 @@ async def _parse_favlist(self, searched: Match[str]): @handle("/read/", r"bilibili\.com/read/cv(?P\d+)") async def _parse_read(self, searched: Match[str]): """解析专栏信息""" + from bilibili_api.article import Article + read_id = int(searched.group("read_id")) - return await self.parse_read(read_id) + article = Article(read_id) + opus = await article.turn_to_opus() + return await self._parse_bilibli_api_opus(opus) async def parse_video( self, @@ -174,9 +178,9 @@ async def parse_dynamic_or_opus(self, dynamic_id: int): dynamic = Dynamic(dynamic_id, await self.credential) if await dynamic.is_article(): - return await self._parse_opus_obj(dynamic.turn_to_opus()) - dynamic_info = convert(await dynamic.get_info(), DynamicData).item + return await self._parse_bilibli_api_opus(dynamic.turn_to_opus()) + dynamic_info = convert(await dynamic.get_info(), DynamicData).item author = self.create_author(dynamic_info.name, dynamic_info.avatar) # 下载图片 @@ -193,27 +197,16 @@ async def parse_dynamic_or_opus(self, dynamic_id: int): contents=contents, ) - async def parse_opus(self, opus_id: int): + async def parse_opus_by_id(self, opus_id: int): """解析图文动态信息 Args: opus_id (int): 图文动态 id """ opus = Opus(opus_id, await self.credential) - return await self._parse_opus_obj(opus) - - async def parse_read(self, read_id: int): - """解析专栏信息, 使用 Opus 接口 - - Args: - read_id (int): 专栏 id - """ - from bilibili_api.article import Article - - article = Article(read_id) - return await self._parse_opus_obj(await article.turn_to_opus()) + return await self._parse_bilibli_api_opus(opus) - async def _parse_opus_obj(self, bili_opus: Opus): + async def _parse_bilibli_api_opus(self, bili_opus: Opus): """解析图文动态信息 Args: diff --git a/tests/parsers/test_bilibili.py b/tests/parsers/test_bilibili.py index b3ddd762..c1fa4fe0 100644 --- a/tests/parsers/test_bilibili.py +++ b/tests/parsers/test_bilibili.py @@ -42,8 +42,7 @@ async def test_read(): url = "https://www.bilibili.com/read/cv523868" parser = BilibiliParser() _, searched = parser.search_url(url) - read_id = int(searched.group("read_id")) - result = await parser.parse_read(read_id) + result = await parser._parse_read(searched) logger.debug(f"result: {result}") assert result.title, "标题为空" assert result.author, "作者为空"