From f92748d05b5edeef5027bef543ef5db5a5660955 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:14:52 +0800 Subject: [PATCH 01/11] refactor: simpify exception and download logic --- .../download/__init__.py | 65 ++++++++++-- src/nonebot_plugin_parser/download/ytdlp.py | 6 +- src/nonebot_plugin_parser/exception.py | 36 ++----- .../parsers/acfun/__init__.py | 98 ++----------------- src/nonebot_plugin_parser/parsers/base.py | 15 ++- .../parsers/bilibili/__init__.py | 77 +++------------ .../parsers/douyin/__init__.py | 11 +-- .../parsers/kuaishou/__init__.py | 3 - .../parsers/weibo/__init__.py | 1 - src/nonebot_plugin_parser/renders/base.py | 57 ++--------- tests/parsers/test_bilibili_need_ck.py | 9 +- typos.toml | 1 - 12 files changed, 111 insertions(+), 268 deletions(-) diff --git a/src/nonebot_plugin_parser/download/__init__.py b/src/nonebot_plugin_parser/download/__init__.py index 2ef0872c..461f8f46 100644 --- a/src/nonebot_plugin_parser/download/__init__.py +++ b/src/nonebot_plugin_parser/download/__init__.py @@ -2,6 +2,7 @@ from pathlib import Path from functools import partial from contextlib import contextmanager +from urllib.parse import urljoin import aiofiles from httpx import HTTPError, AsyncClient @@ -17,7 +18,7 @@ from ..utils import merge_av, safe_unlink, generate_file_name from ..config import pconfig from ..constants import COMMON_HEADER, DOWNLOAD_TIMEOUT -from ..exception import DownloadException, ZeroSizeException, SizeLimitException +from ..exception import IgnoreException, DownloadException class StreamDownloader: @@ -41,7 +42,7 @@ def rich_progress(self, desc: str, total: int | None = None): yield partial(progress.update, task_id) @auto_task - async def streamd( + async def download_file( self, url: str, *, @@ -67,11 +68,11 @@ async def streamd( if content_length == 0: logger.warning(f"媒体 url: {url}, 大小为 0, 取消下载") - raise ZeroSizeException + raise IgnoreException if (file_size := content_length / 1024 / 1024) > pconfig.max_size: - logger.warning(f"媒体 url: {url} 大小 {file_size:.2f} MB 超过 {pconfig.max_size} MB, 取消下载") - raise SizeLimitException + logger.warning(f"媒体 url: {url} 大小 {file_size:.2f} MB, 超过 {pconfig.max_size} MB, 取消下载") + raise IgnoreException with self.rich_progress(file_name, content_length) as update_progress: async with aiofiles.open(file_path, "wb") as file: @@ -97,7 +98,7 @@ async def download_video( """download video file by url with stream""" if video_name is None: video_name = generate_file_name(url, ".mp4") - return await self.streamd(url, file_name=video_name, ext_headers=ext_headers, chunk_size=1024 * 1024) + return await self.download_file(url, file_name=video_name, ext_headers=ext_headers, chunk_size=1024 * 1024) @auto_task async def download_audio( @@ -110,7 +111,7 @@ async def download_audio( """download audio file by url with stream""" if audio_name is None: audio_name = generate_file_name(url, ".mp3") - return await self.streamd(url, file_name=audio_name, ext_headers=ext_headers) + return await self.download_file(url, file_name=audio_name, ext_headers=ext_headers) @auto_task async def download_img( @@ -123,7 +124,7 @@ async def download_img( """download image file by url with stream""" if img_name is None: img_name = generate_file_name(url, ".jpg") - return await self.streamd(url, file_name=img_name, ext_headers=ext_headers) + return await self.download_file(url, file_name=img_name, ext_headers=ext_headers) @auto_task async def download_av_and_merge( @@ -155,6 +156,54 @@ async def download_imgs_without_raise( ) return [p for p in paths_or_errs if isinstance(p, Path)] + @auto_task + async def download_m3u8( + self, + m3u8_url: str, + *, + video_name: str | None = None, + ext_headers: dict[str, str] | None = None, + ) -> Path: + """download m3u8 file by url with stream""" + if video_name is None: + video_name = generate_file_name(m3u8_url, ".mp4") + + video_path = pconfig.cache_dir / video_name + + try: + async with aiofiles.open(video_path, "wb") as f: + total_size = 0 + with self.rich_progress(desc=video_name) as update_progress: + for url in await self._get_m3u8_slices(m3u8_url): + async with self.client.stream("GET", url, headers=ext_headers) as response: + async for chunk in response.aiter_bytes(chunk_size=1024 * 1024): + await f.write(chunk) + total_size += len(chunk) + update_progress(advance=len(chunk), total=total_size) + except HTTPError: + await safe_unlink(video_path) + logger.exception("m3u8 视频下载失败") + raise DownloadException("m3u8 视频下载失败") + + return video_path + + async def _get_m3u8_slices(self, m3u8_url: str): + """获取 m3u8 分片""" + + response = await self.client.get(m3u8_url) + response.raise_for_status() + + slices_text = response.text + slices: list[str] = [] + + for line in slices_text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + slices.append(urljoin(m3u8_url, line)) + + return slices + DOWNLOADER: StreamDownloader = StreamDownloader() diff --git a/src/nonebot_plugin_parser/download/ytdlp.py b/src/nonebot_plugin_parser/download/ytdlp.py index 0d747ef9..782ef818 100644 --- a/src/nonebot_plugin_parser/download/ytdlp.py +++ b/src/nonebot_plugin_parser/download/ytdlp.py @@ -5,11 +5,12 @@ import yt_dlp from msgspec import Struct, convert +from nonebot import logger from .task import auto_task from ..utils import LimitedSizeDict, generate_file_name from ..config import pconfig -from ..exception import ParseException, DurationLimitException +from ..exception import ParseException, IgnoreException class VideoInfo(Struct): @@ -95,7 +96,8 @@ async def download_video(self, url: str, cookiefile: Path | None = None) -> Path video_info = await self.extract_video_info(url, cookiefile) duration = video_info.duration if duration > pconfig.duration_maximum: - raise DurationLimitException + logger.warning(f"视频时长 {duration} 秒, 超过 {pconfig.duration_maximum} 秒, 取消下载") + raise IgnoreException video_path = pconfig.cache_dir / generate_file_name(url, ".mp4") if video_path.exists(): diff --git a/src/nonebot_plugin_parser/exception.py b/src/nonebot_plugin_parser/exception.py index 31a9549f..be89d498 100644 --- a/src/nonebot_plugin_parser/exception.py +++ b/src/nonebot_plugin_parser/exception.py @@ -1,17 +1,9 @@ class ParseException(Exception): - """异常基类""" - def __init__(self, message: str): super().__init__(message) self.message = message -class TipException(ParseException): - """提示异常""" - - pass - - class DownloadException(ParseException): """下载异常""" @@ -19,28 +11,12 @@ def __init__(self, message: str | None = None): super().__init__(message or "媒体下载失败") -class DownloadLimitException(DownloadException): - """下载超过限制异常""" - - pass - - -class SizeLimitException(DownloadLimitException): - """下载大小超过限制异常""" - - def __init__(self): - super().__init__("媒体大小超过配置限制,取消下载") - - -class DurationLimitException(DownloadLimitException): - """下载时长超过限制异常""" - - def __init__(self): - super().__init__("媒体时长超过配置限制,取消下载") +class IgnoreException(ParseException): + """可忽略异常""" + def __init__(self, message: str | None = None): + super().__init__(message or "可忽略异常") -class ZeroSizeException(DownloadException): - """下载大小为 0 异常""" - def __init__(self): - super().__init__("媒体大小为 0, 取消下载") +class TipException(ParseException): + """提示异常""" diff --git a/src/nonebot_plugin_parser/parsers/acfun/__init__.py b/src/nonebot_plugin_parser/parsers/acfun/__init__.py index 20b3feac..8bec0df7 100644 --- a/src/nonebot_plugin_parser/parsers/acfun/__init__.py +++ b/src/nonebot_plugin_parser/parsers/acfun/__init__.py @@ -1,23 +1,16 @@ import re -import asyncio from typing import ClassVar -from pathlib import Path -from urllib.parse import urljoin -import aiofiles -from httpx import HTTPError, AsyncClient +from httpx import AsyncClient from nonebot import logger from ..base import ( - DOWNLOADER, COMMON_TIMEOUT, - DOWNLOAD_TIMEOUT, Platform, BaseParser, PlatformEnum, ParseException, - DownloadException, - DurationLimitException, + IgnoreException, handle, pconfig, ) @@ -39,12 +32,13 @@ async def _parse(self, searched: re.Match[str]): video_info = await self.parse_video_info(url) author = self.create_author(video_info.name, video_info.avatar_url) - video_task = asyncio.create_task( - self.download_video( - video_info.m3u8_url, - f"acfun_{acid}.mp4", - video_info.duration, - ) + if (duration := video_info.duration) >= pconfig.duration_maximum: + logger.warning(f"视频时长 {duration} 超过最大限制 {pconfig.duration_maximum}") + raise IgnoreException + + video_task = self.downloader.download_m3u8( + video_info.m3u8_url, + video_name=f"acfun_{acid}.mp4", ) video_content = self.create_video_content(video_task, cover_url=video_info.coverUrl) @@ -58,14 +52,7 @@ async def _parse(self, searched: re.Match[str]): ) async def parse_video_info(self, url: str): - """解析acfun链接获取详细信息 - - Args: - url (str): 链接 - - Returns: - video.VideoInfo - """ + """解析 acfun 视频信息""" from . import video # 拼接查询参数 @@ -84,68 +71,3 @@ async def parse_video_info(self, url: str): raw = re.sub(r'\\{1,4}"', '"', raw) raw = raw.replace('"{', "{").replace('}"', "}") return video.decoder.decode(raw) - - async def download_video(self, m3u8_url: str, file_name: str, duration: int) -> Path: - """下载acfun视频 - - Args: - m3u8_url (str): m3u8链接 - file_name (str): 文件名 - duration (int): 视频时长(秒) - - Returns: - Path: 下载的mp4文件 - """ - - if duration >= pconfig.duration_maximum: - raise DurationLimitException - - video_file = pconfig.cache_dir / file_name - if video_file.exists(): - return video_file - - m3u8_slices = await self._get_m3u8_slices(m3u8_url) - - try: - async with ( - aiofiles.open(video_file, "wb") as f, - AsyncClient(headers=self.headers, timeout=DOWNLOAD_TIMEOUT) as client, - ): - total_size = 0 - with DOWNLOADER.rich_progress(desc=file_name) as update_progress: - for url in m3u8_slices: - async with client.stream("GET", url) as response: - async for chunk in response.aiter_bytes(chunk_size=1024 * 1024): - await f.write(chunk) - total_size += len(chunk) - update_progress(advance=len(chunk), total=total_size) - except HTTPError: - video_file.unlink(missing_ok=True) - logger.exception("视频下载失败") - raise DownloadException("视频下载失败") - return video_file - - async def _get_m3u8_slices(self, m3u8_url: str): - """拼接m3u8链接 - - Args: - m3u8_url (str): m3u8链接 - m3u8_slice (str): m3u8切片 - - Returns: - list[str]: 视频链接 - """ - async with AsyncClient(headers=self.headers, timeout=COMMON_TIMEOUT) as client: - response = await client.get(m3u8_url) - response.raise_for_status() - - slices_text = response.text - - slices: list[str] = [] - for line in slices_text.splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - slices.append(urljoin(m3u8_url, line)) - - return slices diff --git a/src/nonebot_plugin_parser/parsers/base.py b/src/nonebot_plugin_parser/parsers/base.py index 24542779..bed59792 100644 --- a/src/nonebot_plugin_parser/parsers/base.py +++ b/src/nonebot_plugin_parser/parsers/base.py @@ -1,5 +1,3 @@ -"""Parser 基类定义""" - from re import Match, Pattern, compile from abc import ABC from typing import TYPE_CHECKING, Any, TypeVar, ClassVar, cast @@ -10,16 +8,13 @@ from .data import Platform, ParseResult, ParseResultKwargs from ..config import pconfig as pconfig -from ..download import DOWNLOADER as DOWNLOADER +from ..download import DOWNLOADER from ..constants import IOS_HEADER, COMMON_HEADER, ANDROID_HEADER, COMMON_TIMEOUT from ..constants import DOWNLOAD_TIMEOUT as DOWNLOAD_TIMEOUT from ..constants import PlatformEnum as PlatformEnum -from ..exception import TipException as TipException -from ..exception import ParseException as ParseException +from ..exception import ParseException +from ..exception import IgnoreException as IgnoreException from ..exception import DownloadException as DownloadException -from ..exception import ZeroSizeException as ZeroSizeException -from ..exception import SizeLimitException as SizeLimitException -from ..exception import DurationLimitException as DurationLimitException T = TypeVar("T", bound="BaseParser") HandlerFunc = Callable[[T, Match[str]], Coroutine[Any, Any, ParseResult]] @@ -259,3 +254,7 @@ def create_graphics_content( image_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) return GraphicsContent(image_task, text, alt) + + @property + def downloader(self): + return DOWNLOADER diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 9f5c89ac..4ad7b5c1 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -12,12 +12,11 @@ from bilibili_api.login_v2 import QrCodeLogin, QrCodeLoginEvents from ..base import ( - DOWNLOADER, BaseParser, PlatformEnum, ParseException, + IgnoreException, DownloadException, - DurationLimitException, handle, pconfig, ) @@ -102,13 +101,7 @@ async def parse_video( avid: int | None = None, page_num: int = 1, ): - """解析视频信息 - - Args: - bvid (str | None): bvid - avid (int | None): avid - page_num (int): 页码 - """ + """解析视频信息""" from .video import VideoInfo, AIConclusion @@ -141,13 +134,13 @@ async def download_video(): return output_path 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 + raise IgnoreException if a_url is not None: - return await DOWNLOADER.download_av_and_merge( + return await self.downloader.download_av_and_merge( 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 self.downloader.download_file(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,11 +160,7 @@ async def download_video(): ) async def parse_dynamic_or_opus(self, dynamic_id: int): - """解析动态和图文信息 - - Args: - url (str): 动态链接 - """ + """解析动态或图文""" from bilibili_api.dynamic import Dynamic from .dynamic import DynamicData @@ -186,7 +175,7 @@ async def parse_dynamic_or_opus(self, dynamic_id: int): # 下载图片 contents: list[MediaContent] = [] for image_url in dynamic_info.image_urls: - img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) + img_task = self.downloader.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) return self.result( @@ -198,23 +187,12 @@ async def parse_dynamic_or_opus(self, dynamic_id: int): ) async def parse_opus_by_id(self, opus_id: int): - """解析图文动态信息 - - Args: - opus_id (int): 图文动态 id - """ + """解析图文动态(opus id)""" opus = Opus(opus_id, await self.credential) return await self._parse_bilibli_api_opus(opus) async def _parse_bilibli_api_opus(self, bili_opus: Opus): - """解析图文动态信息 - - Args: - opus_id (int): 图文动态 id - - Returns: - ParseResult: 解析结果 - """ + """解析图文动态(Opus)""" from .opus import OpusItem, TextNode, ImageNode @@ -246,14 +224,7 @@ async def _parse_bilibli_api_opus(self, bili_opus: Opus): ) async def parse_live(self, room_id: int): - """解析直播信息 - - Args: - room_id (int): 直播 id - - Returns: - ParseResult: 解析结果 - """ + """解析直播""" from bilibili_api.live import LiveRoom from .live import RoomData @@ -265,12 +236,12 @@ async def parse_live(self, room_id: int): contents: list[MediaContent] = [] # 下载封面 if cover := room_data.cover: - cover_task = DOWNLOADER.download_img(cover, ext_headers=self.headers) + cover_task = self.downloader.download_img(cover, ext_headers=self.headers) contents.append(ImageContent(cover_task)) # 下载关键帧 if keyframe := room_data.keyframe: - keyframe_task = DOWNLOADER.download_img(keyframe, ext_headers=self.headers) + keyframe_task = self.downloader.download_img(keyframe, ext_headers=self.headers) contents.append(ImageContent(keyframe_task)) author = self.create_author(room_data.name, room_data.avatar) @@ -285,14 +256,7 @@ async def parse_live(self, room_id: int): ) async def parse_favlist(self, fav_id: int): - """解析收藏夹信息 - - Args: - fav_id (int): 收藏夹 id - - Returns: - list[GraphicsContent]: 图文内容列表 - """ + """解析收藏夹""" from bilibili_api.favorite_list import get_video_favorite_list_content from .favlist import FavData @@ -313,12 +277,7 @@ async def parse_favlist(self, fav_id: int): ) async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video: - """解析视频信息 - - Args: - bvid (str | None): bvid - avid (int | None): avid - """ + """解析视频""" if avid: return Video(aid=avid, credential=await self.credential) elif bvid: @@ -334,13 +293,7 @@ async def extract_download_urls( avid: int | None = None, page_index: int = 0, ) -> tuple[str, str | None]: - """解析视频下载链接 - - Args: - bvid (str | None): bvid - avid (int | None): avid - page_index (int): 页索引 = 页码 - 1 - """ + """解析视频下载链接""" from bilibili_api.video import ( AudioStreamDownloadURL, diff --git a/src/nonebot_plugin_parser/parsers/douyin/__init__.py b/src/nonebot_plugin_parser/parsers/douyin/__init__.py index e451c369..b9dd15b3 100644 --- a/src/nonebot_plugin_parser/parsers/douyin/__init__.py +++ b/src/nonebot_plugin_parser/parsers/douyin/__init__.py @@ -15,7 +15,6 @@ class DouyinParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.DOUYIN, display_name="抖音") # https://v.douyin.com/_2ljF4AmKL8 @@ -31,19 +30,13 @@ async def _parse_short_link(self, searched: re.Match[str]): @handle("iesdouyin", r"iesdouyin\.com/share/(?Pslides|video|note)/(?P\d+)") @handle("m.douyin", r"m\.douyin\.com/share/(?Pslides|video|note)/(?P\d+)") # https://jingxuan.douyin.com/m/video/7574300896016862490?app=yumme&utm_source=copy_link - @handle( - "jingxuan.douyin", - r"jingxuan\.douyin.com/m/(?Pslides|video|note)/(?P\d+)", - ) + @handle("jingxuan.douyin", r"jingxuan\.douyin.com/m/(?Pslides|video|note)/(?P\d+)") async def _parse_douyin(self, searched: re.Match[str]): ty, vid = searched.group("ty"), searched.group("vid") if ty == "slides": return await self.parse_slides(vid) - for url in ( - self._build_m_douyin_url(ty, vid), - self._build_iesdouyin_url(ty, vid), - ): + for url in (self._build_m_douyin_url(ty, vid), self._build_iesdouyin_url(ty, vid)): try: return await self.parse_video(url) except ParseException as e: diff --git a/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py b/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py index bd700f4b..f5b4cbfb 100644 --- a/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py +++ b/src/nonebot_plugin_parser/parsers/kuaishou/__init__.py @@ -8,9 +8,6 @@ class KuaiShouParser(BaseParser): - """快手解析器""" - - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.KUAISHOU, display_name="快手") def __init__(self): diff --git a/src/nonebot_plugin_parser/parsers/weibo/__init__.py b/src/nonebot_plugin_parser/parsers/weibo/__init__.py index a32caae5..cb2731f5 100644 --- a/src/nonebot_plugin_parser/parsers/weibo/__init__.py +++ b/src/nonebot_plugin_parser/parsers/weibo/__init__.py @@ -12,7 +12,6 @@ class WeiBoParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.WEIBO, display_name="微博") def __init__(self): diff --git a/src/nonebot_plugin_parser/renders/base.py b/src/nonebot_plugin_parser/renders/base.py index d0235751..b284b44f 100644 --- a/src/nonebot_plugin_parser/renders/base.py +++ b/src/nonebot_plugin_parser/renders/base.py @@ -16,7 +16,7 @@ DynamicContent, GraphicsContent, ) -from ..exception import DownloadException, ZeroSizeException, DownloadLimitException +from ..exception import IgnoreException, DownloadException class BaseRenderer(ABC): @@ -27,27 +27,13 @@ class BaseRenderer(ABC): @abstractmethod async def render_messages(self, result: ParseResult) -> AsyncGenerator[UniMessage[Any], None]: - """消息生成器 - - Args: - result (ParseResult): 解析结果 - - Returns: - AsyncGenerator[UniMessage[Any], None]: 消息生成器 - """ + """渲染解析结果""" if False: yield raise NotImplementedError async def render_contents(self, result: ParseResult) -> AsyncGenerator[UniMessage[Any], None]: - """渲染媒体内容消息 - - Args: - result (ParseResult): 解析结果 - - Returns: - AsyncGenerator[UniMessage[Any], None]: 消息生成器 - """ + """渲染媒体内容""" failed_count = 0 forwardable_segs: list[ForwardNodeInner] = [] dynamic_segs: list[ForwardNodeInner] = [] @@ -55,10 +41,7 @@ async def render_contents(self, result: ParseResult) -> AsyncGenerator[UniMessag for cont in chain(result.contents, result.repost.contents if result.repost else ()): try: path = await cont.get_path() - # 继续渲染其他内容, 类似之前 gather (return_exceptions=True) 的处理 - except (DownloadLimitException, ZeroSizeException): - # 预期异常,不抛出 - # yield UniMessage(e.message) + except IgnoreException: continue except DownloadException: failed_count += 1 @@ -109,23 +92,11 @@ class ImageRenderer(BaseRenderer): @abstractmethod async def render_image(self, result: ParseResult) -> bytes: - """渲染图片 - - Args: - result (ParseResult): 解析结果 - - Returns: - bytes: 图片字节 png 格式 - """ + """渲染图片""" raise NotImplementedError @override async def render_messages(self, result: ParseResult): - """渲染消息 - - Args: - result (ParseResult): 解析结果 - """ image_seg = await self.cache_or_render_image(result) msg = UniMessage(image_seg) @@ -139,14 +110,7 @@ async def render_messages(self, result: ParseResult): yield message async def cache_or_render_image(self, result: ParseResult): - """获取缓存图片 - - Args: - result (ParseResult): 解析结果 - - Returns: - Image: 图片 Segment - """ + """获取缓存图片""" if result.render_image is None: image_raw = await self.render_image(result) image_path = await self.save_img(image_raw) @@ -158,14 +122,7 @@ async def cache_or_render_image(self, result: ParseResult): @classmethod async def save_img(cls, raw: bytes) -> Path: - """保存图片 - - Args: - raw (bytes): 图片字节 - - Returns: - Path: 图片路径 - """ + """保存图片""" import aiofiles file_name = f"{uuid.uuid4().hex}.png" diff --git a/tests/parsers/test_bilibili_need_ck.py b/tests/parsers/test_bilibili_need_ck.py index 2dc8f7fa..e6515fa0 100644 --- a/tests/parsers/test_bilibili_need_ck.py +++ b/tests/parsers/test_bilibili_need_ck.py @@ -49,23 +49,20 @@ async def test_video(): async def test_max_size_video(): from nonebot_plugin_parser.parsers import BilibiliParser from nonebot_plugin_parser.download import DOWNLOADER - from nonebot_plugin_parser.exception import ( - SizeLimitException, - DurationLimitException, - ) + from nonebot_plugin_parser.exception import IgnoreException parser = BilibiliParser() bvid = "BV1du4y1E7Nh" audio_url = None try: _, audio_url = await parser.extract_download_urls(bvid=bvid) - except DurationLimitException: + except IgnoreException: pass assert audio_url is not None try: await DOWNLOADER.download_audio(audio_url, ext_headers=parser.headers) - except SizeLimitException: + except IgnoreException: pass diff --git a/typos.toml b/typos.toml index 0e893dfa..641c5f68 100644 --- a/typos.toml +++ b/typos.toml @@ -1,5 +1,4 @@ [default.extend-words] -streamd = "streamd" detecter = "detecter" Detecter = "Detecter" mapp = "mapp" From 2b211ddf68d14f888f3c5520d5f154aa5a4276f6 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:18:31 +0800 Subject: [PATCH 02/11] tweak --- src/nonebot_plugin_parser/parsers/base.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/base.py b/src/nonebot_plugin_parser/parsers/base.py index bed59792..7fb497d8 100644 --- a/src/nonebot_plugin_parser/parsers/base.py +++ b/src/nonebot_plugin_parser/parsers/base.py @@ -90,18 +90,6 @@ def get_all_subclass(cls) -> list[type["BaseParser"]]: return cls._registry async def parse(self, keyword: str, searched: Match[str]) -> ParseResult: - """解析 URL 提取信息 - - Args: - keyword: 关键词 - searched: 正则表达式匹配对象,由平台对应的模式匹配得到 - - Returns: - ParseResult: 解析结果 - - Raises: - ParseException: 解析失败时抛出 - """ return await self._handlers[keyword](self, searched) async def parse_with_redirect( From e8e337cfea54700ae5d53bd07c3b9da22549dfbf Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:20:22 +0800 Subject: [PATCH 03/11] tweak --- src/nonebot_plugin_parser/matchers/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nonebot_plugin_parser/matchers/__init__.py b/src/nonebot_plugin_parser/matchers/__init__.py index f3c371ab..e4d019f3 100644 --- a/src/nonebot_plugin_parser/matchers/__init__.py +++ b/src/nonebot_plugin_parser/matchers/__init__.py @@ -11,7 +11,6 @@ from ..helper import UniHelper, UniMessage from ..parsers import BaseParser, ParseResult, BilibiliParser from ..renders import get_renderer -from ..download import DOWNLOADER def _get_enabled_parser_classes() -> list[type[BaseParser]]: @@ -104,7 +103,7 @@ async def _(message: Message = CommandArg()): if not audio_url: await UniMessage("未找到可下载的音频").finish() - audio_path = await DOWNLOADER.download_audio( + audio_path = await parser.downloader.download_audio( audio_url, audio_name=f"{bvid}-{page_idx}.mp3", ext_headers=parser.headers ) await UniMessage(UniHelper.record_seg(audio_path)).send() From 24c5ca5169e084891cbd650d692ffe5f85a7f9c2 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:22:31 +0800 Subject: [PATCH 04/11] tweak --- src/nonebot_plugin_parser/parsers/acfun/__init__.py | 1 - src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 1 - src/nonebot_plugin_parser/parsers/nga.py | 1 - src/nonebot_plugin_parser/parsers/tiktok.py | 1 - src/nonebot_plugin_parser/parsers/twitter.py | 1 - src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py | 1 - src/nonebot_plugin_parser/parsers/youtube/__init__.py | 1 - 7 files changed, 7 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/acfun/__init__.py b/src/nonebot_plugin_parser/parsers/acfun/__init__.py index 8bec0df7..98094304 100644 --- a/src/nonebot_plugin_parser/parsers/acfun/__init__.py +++ b/src/nonebot_plugin_parser/parsers/acfun/__init__.py @@ -17,7 +17,6 @@ class AcfunParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.ACFUN, display_name="猴山") def __init__(self): diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 4ad7b5c1..1616812f 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -31,7 +31,6 @@ class BilibiliParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.BILIBILI, display_name="哔哩哔哩") def __init__(self): diff --git a/src/nonebot_plugin_parser/parsers/nga.py b/src/nonebot_plugin_parser/parsers/nga.py index af247382..b419c104 100644 --- a/src/nonebot_plugin_parser/parsers/nga.py +++ b/src/nonebot_plugin_parser/parsers/nga.py @@ -13,7 +13,6 @@ class NGAParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.NGA, display_name="NGA") def __init__(self): diff --git a/src/nonebot_plugin_parser/parsers/tiktok.py b/src/nonebot_plugin_parser/parsers/tiktok.py index af0dd363..37e53f67 100644 --- a/src/nonebot_plugin_parser/parsers/tiktok.py +++ b/src/nonebot_plugin_parser/parsers/tiktok.py @@ -7,7 +7,6 @@ class TikTokParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.TIKTOK, display_name="TikTok") @handle("tiktok", r"(www|vt|vm)\.tiktok\.com/[A-Za-z0-9._?%&+\-=/#@]*") diff --git a/src/nonebot_plugin_parser/parsers/twitter.py b/src/nonebot_plugin_parser/parsers/twitter.py index 38be854f..cab0d966 100644 --- a/src/nonebot_plugin_parser/parsers/twitter.py +++ b/src/nonebot_plugin_parser/parsers/twitter.py @@ -10,7 +10,6 @@ class TwitterParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.TWITTER, display_name="小蓝鸟") async def _req_xdown_api(self, url: str) -> dict[str, Any]: diff --git a/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py b/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py index 6961e874..f6b2c73e 100644 --- a/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py +++ b/src/nonebot_plugin_parser/parsers/xiaohongshu/__init__.py @@ -9,7 +9,6 @@ class XiaoHongShuParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.XIAOHONGSHU, display_name="小红书") def __init__(self): diff --git a/src/nonebot_plugin_parser/parsers/youtube/__init__.py b/src/nonebot_plugin_parser/parsers/youtube/__init__.py index 7ee52f0e..529d3c6d 100644 --- a/src/nonebot_plugin_parser/parsers/youtube/__init__.py +++ b/src/nonebot_plugin_parser/parsers/youtube/__init__.py @@ -9,7 +9,6 @@ class YouTubeParser(BaseParser): - # 平台信息 platform: ClassVar[Platform] = Platform(name=PlatformEnum.YOUTUBE, display_name="油管") def __init__(self): From e7f0b830dc610de11164e5ae8be3f8f31fd4491f Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:26:08 +0800 Subject: [PATCH 05/11] tweak --- src/nonebot_plugin_parser/renders/default.py | 9 --------- src/nonebot_plugin_parser/renders/htmlrender.py | 8 -------- 2 files changed, 17 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/default.py b/src/nonebot_plugin_parser/renders/default.py index 57b8ba54..7c430bfb 100644 --- a/src/nonebot_plugin_parser/renders/default.py +++ b/src/nonebot_plugin_parser/renders/default.py @@ -11,15 +11,6 @@ class DefaultRenderer(BaseRenderer): @override async def render_messages(self, result: ParseResult): - """渲染内容消息 - - Args: - result (ParseResult): 解析结果 - - Returns: - Generator[UniMessage[Any], None, None]: 消息生成器 - """ - texts = [ result.header, result.text, diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index bcd1519c..5812b396 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -15,14 +15,6 @@ class HtmlRenderer(ImageRenderer): @override async def render_image(self, result: ParseResult) -> bytes: - """使用 HTML 绘制通用社交媒体帖子卡片 - - Args: - result: 解析结果 - - Returns: - PNG 图片的字节数据 - """ # 准备模板数据 template_data = await self._resolve_parse_result(result) From db8a916623a3b015726b61b571512fe0fd619377 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 01:30:19 +0800 Subject: [PATCH 06/11] tweak --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 1616812f..24b99659 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -133,6 +133,7 @@ async def download_video(): return output_path v_url, a_url = await self.extract_download_urls(video=video, page_index=page_info.index) if page_info.duration > pconfig.duration_maximum: + logger.warning(f"视频时长 {page_info.duration} 秒, 超过 {pconfig.duration_maximum} 秒, 取消下载") raise IgnoreException if a_url is not None: return await self.downloader.download_av_and_merge( From 112a31aac73a5f3fcb9a058bb2fb0c04482e61e5 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 09:41:02 +0800 Subject: [PATCH 07/11] tweak --- .env.test | 2 +- src/nonebot_plugin_parser/renders/common.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.env.test b/.env.test index be8f3f30..e5e1edb4 100644 --- a/.env.test +++ b/.env.test @@ -1,5 +1,5 @@ PORT=8888 -LOG_LEVEL=DEBUG +LOG_LEVEL=INFO FASTAPI_RELOAD=false LOCALSTORE_USE_CWD=true diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 7fc2667f..ecb24819 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -95,7 +95,7 @@ class RenderContext: class CommonRenderer(ImageRenderer): - """统一渲染器 - 单次遍历渲染""" + """统一渲染器""" # 布局常量 PADDING = 25 @@ -205,13 +205,15 @@ async def _create_card_image(self, result: ParseResult, not_repost: bool = True) # 裁剪到实际高度 final_height = ctx.y_pos + self.PADDING + logger.debug(f"估算高度: {estimated_height}, 画布高度: {ctx.image.height}, 最终高度: {final_height}") return ctx.image.crop((0, 0, card_width, final_height)) def _ensure_canvas_height(self, ctx: RenderContext, needed_height: int) -> None: """确保画布有足够高度,不够则扩展""" if ctx.y_pos + needed_height + self.PADDING > ctx.image.height: - # 扩展画布(每次扩展 1.5 倍或至少满足需求) - new_height = max(int(ctx.image.height * 1.5), ctx.y_pos + needed_height + self.PADDING * 2) + # 扩展画布(每次扩展 1.6 倍或至少满足需求) + new_height = max(int(ctx.image.height * 1.6), ctx.y_pos + needed_height + self.PADDING * 2) + logger.debug(f"扩展画布高度: {ctx.image.height} -> {new_height}") bg_color = self.BG_COLOR if ctx.not_repost else self.REPOST_BG_COLOR new_image = Image.new("RGB", (ctx.card_width, new_height), bg_color) new_image.paste(ctx.image, (0, 0)) @@ -222,20 +224,23 @@ def _estimate_height(self, result: ParseResult, content_width: int) -> int: """估算画布高度""" height = self.PADDING * 2 # 上下边距 - # 头部 + # 头部(头像 + 名称 + 时间) if result.author: height += self.AVATAR_SIZE + self.SECTION_SPACING - # 标题(估算) + # 标题(估算,考虑换行符) if result.title: - # 考虑换行符 lines = result.title.count("\n") + 1 + (len(result.title) * self.fontset.title.cjk_width // content_width) height += lines * self.fontset.title.line_height + self.SECTION_SPACING # 封面或图片 height += self.MAX_COVER_HEIGHT + self.SECTION_SPACING - # 文本(估算:考虑换行符) + # 图文内容 + if graphics_contents := result.graphics_contents: + height += len(graphics_contents) * self.MAX_COVER_HEIGHT + self.SECTION_SPACING + + # 正文(估算,考虑换行符) if result.text: lines = result.text.count("\n") + 1 + (len(result.text) * self.fontset.text.cjk_width // content_width) height += lines * self.fontset.text.line_height + self.SECTION_SPACING @@ -250,7 +255,7 @@ def _estimate_height(self, result: ParseResult, content_width: int) -> int: height += self.REPOST_PADDING * 2 + self.SECTION_SPACING # 增加安全余量,防止估算不足(最后会裁剪) - return height + 300 + return height async def _render_header(self, ctx: RenderContext) -> None: """渲染头部(头像 + 名称 + 时间)""" From cdd03ae032b217170b5c2f3d87476dc5ca682c79 Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 09:51:29 +0800 Subject: [PATCH 08/11] tweak --- src/nonebot_plugin_parser/utils.py | 67 +++++------------------------- 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/src/nonebot_plugin_parser/utils.py b/src/nonebot_plugin_parser/utils.py index 75247fdb..83eb29bc 100644 --- a/src/nonebot_plugin_parser/utils.py +++ b/src/nonebot_plugin_parser/utils.py @@ -7,6 +7,7 @@ from collections import OrderedDict from urllib.parse import urlparse +from anyio import Path as AnyioPath from nonebot import logger K = TypeVar("K") @@ -29,28 +30,17 @@ def __setitem__(self, key: K, value: V): def keep_zh_en_num(text: str) -> str: - """ - 保留字符串中的中英文和数字 - """ + """保留字符串中的中英文和数字""" return re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\-_]", "", text.replace(" ", "_")) async def safe_unlink(path: Path): - """ - 安全删除文件 - """ - try: - await asyncio.to_thread(path.unlink, missing_ok=True) - except Exception: - logger.warning(f"删除 {path} 失败") + """安全删除文件""" + await AnyioPath(path).unlink(missing_ok=True) async def exec_ffmpeg_cmd(cmd: list[str]) -> None: - """执行命令 - - Args: - cmd (list[str]): 命令序列 - """ + """执行 ffmpeg 命令""" try: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE @@ -71,13 +61,7 @@ async def merge_av( a_path: Path, output_path: Path, ) -> None: - """合并视频和音频 - - Args: - v_path (Path): 视频文件路径 - a_path (Path): 音频文件路径 - output_path (Path): 输出文件路径 - """ + """合并视频和音频""" logger.info(f"Merging {v_path.name} and {a_path.name} to {output_path.name}") cmd = [ @@ -107,13 +91,7 @@ async def merge_av_h264( a_path: Path, output_path: Path, ) -> None: - """合并视频和音频,并使用 H.264 编码 - - Args: - v_path (Path): 视频文件路径 - a_path (Path): 音频文件路径 - output_path (Path): 输出文件路径 - """ + """合并视频和音频,并使用 H.264 编码""" logger.info(f"Merging {v_path.name} and {a_path.name} to {output_path.name} with H.264") # 修改命令以确保视频使用 H.264 编码 @@ -147,14 +125,7 @@ async def merge_av_h264( async def encode_video_to_h264(video_path: Path) -> Path: - """将视频重新编码到 h264 - - Args: - video_path (Path): 视频路径 - - Returns: - Path: 编码后的视频路径 - """ + """将视频重新编码到 h264""" output_path = video_path.with_name(f"{video_path.stem}_h264{video_path.suffix}") if output_path.exists(): return output_path @@ -178,24 +149,13 @@ async def encode_video_to_h264(video_path: Path) -> Path: def fmt_size(file_path: Path) -> str: - """格式化文件大小 - - Args: - video_path (Path): 视频路径 - """ + """格式化文件大小""" return f"大小: {file_path.stat().st_size / 1024 / 1024:.2f} MB" def generate_file_name(url: str, default_suffix: str = "") -> str: - """根据 url 生成文件名 - - Args: - url (str): url - default_suffix (str): 默认后缀. Defaults to "". + """根据 url 生成文件名""" - Returns: - str: 文件名 - """ # 根据 url 获取文件后缀 path = Path(urlparse(url).path) suffix = path.suffix if path.suffix else default_suffix @@ -206,12 +166,7 @@ def generate_file_name(url: str, default_suffix: str = "") -> str: def write_json_to_data(data: dict[str, Any] | str, file_name: str): - """将数据写入数据目录 - - Args: - data (dict[str, Any] | str): 数据 - file_name (str): 文件名 - """ + """将数据写入数据目录""" import json from .config import pconfig From 39b0eb812a98c1458096827fbcd74a855420defa Mon Sep 17 00:00:00 2001 From: fllesser Date: Sun, 8 Feb 2026 10:11:40 +0800 Subject: [PATCH 09/11] tweak --- src/nonebot_plugin_parser/download/ytdlp.py | 29 ++---------------- src/nonebot_plugin_parser/matchers/filter.py | 1 - src/nonebot_plugin_parser/matchers/rule.py | 9 +----- src/nonebot_plugin_parser/parsers/base.py | 10 ++---- src/nonebot_plugin_parser/parsers/cookie.py | 17 ++-------- src/nonebot_plugin_parser/parsers/twitter.py | 9 +----- .../parsers/youtube/__init__.py | 10 +----- src/nonebot_plugin_parser/renders/common.py | 14 ++++----- ...unLangHeiW-1.ttf => HYSongYunLangHeiW.ttf} | Bin .../renders/resources/media_button.png | Bin 2510 -> 0 bytes .../renders/resources/play.png | Bin 0 -> 5724 bytes 11 files changed, 16 insertions(+), 83 deletions(-) rename src/nonebot_plugin_parser/renders/resources/{HYSongYunLangHeiW-1.ttf => HYSongYunLangHeiW.ttf} (100%) delete mode 100644 src/nonebot_plugin_parser/renders/resources/media_button.png create mode 100644 src/nonebot_plugin_parser/renders/resources/play.png diff --git a/src/nonebot_plugin_parser/download/ytdlp.py b/src/nonebot_plugin_parser/download/ytdlp.py index 782ef818..5f92d974 100644 --- a/src/nonebot_plugin_parser/download/ytdlp.py +++ b/src/nonebot_plugin_parser/download/ytdlp.py @@ -37,8 +37,6 @@ def author_name(self) -> str: class YtdlpDownloader: - """YtdlpDownloader class""" - def __init__(self): if TYPE_CHECKING: from yt_dlp import _Params @@ -56,15 +54,8 @@ def __init__(self): self._extract_base_opts["proxy"] = proxy async def extract_video_info(self, url: str, cookiefile: Path | None = None) -> VideoInfo: - """get video info by url - - Args: - url (str): url address - cookiefile (Path | None ): cookie file path. Defaults to None. + """Get video info by yt-dlp""" - Returns: - dict[str, str]: video info - """ video_info = self._video_info_mapping.get(url, None) if video_info: return video_info @@ -84,15 +75,8 @@ async def extract_video_info(self, url: str, cookiefile: Path | None = None) -> @auto_task async def download_video(self, url: str, cookiefile: Path | None = None) -> Path: - """download video by yt-dlp + """Download video by yt-dlp""" - Args: - url (str): url address - cookiefile (Path | None): cookie file path. Defaults to None. - - Returns: - Path: video file path - """ video_info = await self.extract_video_info(url, cookiefile) duration = video_info.duration if duration > pconfig.duration_maximum: @@ -129,15 +113,8 @@ async def download_video(self, url: str, cookiefile: Path | None = None) -> Path @auto_task async def download_audio(self, url: str, cookiefile: Path | None = None) -> Path: - """download audio by yt-dlp - - Args: - url (str): url address - cookiefile (Path | None): cookie file path. Defaults to None. + """Download audio by yt-dlp""" - Returns: - Path: audio file path - """ file_name = generate_file_name(url) audio_path = pconfig.cache_dir / f"{file_name}.flac" if audio_path.exists(): diff --git a/src/nonebot_plugin_parser/matchers/filter.py b/src/nonebot_plugin_parser/matchers/filter.py index 5abb6f7f..43b51330 100644 --- a/src/nonebot_plugin_parser/matchers/filter.py +++ b/src/nonebot_plugin_parser/matchers/filter.py @@ -37,7 +37,6 @@ def get_group_key(session: Session) -> str: return f"{session.scope}_{session.scene_path}" -# Rule def is_enabled(session: Session = UniSession()) -> bool: """判断当前会话是否在关闭解析的名单中""" if session.scene.is_private: diff --git a/src/nonebot_plugin_parser/matchers/rule.py b/src/nonebot_plugin_parser/matchers/rule.py index b6b3f761..d5730a2f 100644 --- a/src/nonebot_plugin_parser/matchers/rule.py +++ b/src/nonebot_plugin_parser/matchers/rule.py @@ -72,14 +72,7 @@ def _searched(state: T_State) -> SearchResult | None: def _extract_url(hyper: Hyper) -> str | None: - """处理 JSON 类型的消息段,提取 URL - - Args: - json_seg: JSON 类型的消息段 - - Returns: - Optional[str]: 提取的 URL, 如果提取失败则返回 None - """ + """处理 JSON 类型的消息段,提取 URL""" data = hyper.data raw_str: str | None = data.get("raw") diff --git a/src/nonebot_plugin_parser/parsers/base.py b/src/nonebot_plugin_parser/parsers/base.py index 7fb497d8..78004688 100644 --- a/src/nonebot_plugin_parser/parsers/base.py +++ b/src/nonebot_plugin_parser/parsers/base.py @@ -40,18 +40,12 @@ def decorator(func: HandlerFunc[T]) -> HandlerFunc[T]: class BaseParser: - """所有平台 Parser 的抽象基类 - - 子类必须实现: - - platform: 平台信息(包含名称和显示名称) - """ + platform: ClassVar[Platform] + """ 平台信息(包含名称和显示名称) """ _registry: ClassVar[list[type["BaseParser"]]] = [] """ 存储所有已注册的 Parser 类 """ - platform: ClassVar[Platform] - """ 平台信息(包含名称和显示名称) """ - if TYPE_CHECKING: _key_patterns: ClassVar[KeyPatterns] _handlers: ClassVar[dict[str, HandlerFunc]] diff --git a/src/nonebot_plugin_parser/parsers/cookie.py b/src/nonebot_plugin_parser/parsers/cookie.py index 6af79fcb..db96ece1 100644 --- a/src/nonebot_plugin_parser/parsers/cookie.py +++ b/src/nonebot_plugin_parser/parsers/cookie.py @@ -3,13 +3,7 @@ def save_cookies_with_netscape(cookies_str: str, file_path: Path, domain: str): - """以 netscape 格式保存 cookies - - Args: - cookies_str: cookies 字符串 - file_path: 保存的文件路径 - domain: 域名 - """ + """以 netscape 格式保存 cookies""" # 创建 MozillaCookieJar 对象 cj = cookiejar.MozillaCookieJar(file_path) @@ -43,14 +37,7 @@ def save_cookies_with_netscape(cookies_str: str, file_path: Path, domain: str): def ck2dict(cookies_str: str) -> dict[str, str]: - """将 cookies 字符串转换为字典 - - Args: - cookies_str: cookies 字符串 - - Returns: - dict[str, str]: 字典 - """ + """将 cookies 字符串转换为字典""" res = {} for cookie in cookies_str.split(";"): name, value = cookie.strip().split("=", 1) diff --git a/src/nonebot_plugin_parser/parsers/twitter.py b/src/nonebot_plugin_parser/parsers/twitter.py index cab0d966..3390e471 100644 --- a/src/nonebot_plugin_parser/parsers/twitter.py +++ b/src/nonebot_plugin_parser/parsers/twitter.py @@ -43,14 +43,7 @@ async def _parse(self, searched: re.Match[str]) -> ParseResult: return self.parse_twitter_html(html_content) def parse_twitter_html(self, html_content: str) -> ParseResult: - """解析 Twitter HTML 内容 - - Args: - html_content (str): Twitter HTML 内容 - - Returns: - ParseResult: 解析结果 - """ + """解析 Twitter HTML 内容""" from bs4 import Tag, BeautifulSoup soup = BeautifulSoup(html_content, "html.parser") diff --git a/src/nonebot_plugin_parser/parsers/youtube/__init__.py b/src/nonebot_plugin_parser/parsers/youtube/__init__.py index 529d3c6d..a2c4f674 100644 --- a/src/nonebot_plugin_parser/parsers/youtube/__init__.py +++ b/src/nonebot_plugin_parser/parsers/youtube/__init__.py @@ -52,15 +52,7 @@ async def parse_video(self, url: str): ) async def parse_audio(self, url: str): - """解析 YouTube URL 并标记为音频下载 - - Args: - url: YouTube 链接 - - Returns: - ParseResult: 解析结果(音频内容) - - """ + """解析 YouTube URL 音频""" video_info = await YTDLP_DOWNLOADER.extract_video_info(url, self.cookies_file) author = await self._fetch_author_info(video_info.channel_id) diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index ecb24819..5336ea8c 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -124,15 +124,13 @@ class CommonRenderer(ImageRenderer): REPOST_BORDER_COLOR: ClassVar[Color] = (230, 230, 230) # 资源路径 - _RESOURCES = "resources" - _EMOJIS = "emojis" - RESOURCES_DIR: ClassVar[Path] = Path(__file__).parent / _RESOURCES - DEFAULT_FONT_PATH: ClassVar[Path] = RESOURCES_DIR / "HYSongYunLangHeiW-1.ttf" - DEFAULT_VIDEO_BUTTON_PATH: ClassVar[Path] = RESOURCES_DIR / "media_button.png" + RESOURCES_DIR: ClassVar[Path] = Path(__file__).parent / "resources" + DEFAULT_FONT_PATH: ClassVar[Path] = RESOURCES_DIR / "HYSongYunLangHeiW.ttf" + DEFAULT_VIDEO_BUTTON_PATH: ClassVar[Path] = RESOURCES_DIR / "play.png" EMOJI_SOURCE: ClassVar[EmojiCDNSource] = EmojiCDNSource( base_url=pconfig.emoji_cdn, style=pconfig.emoji_style, - cache_dir=pconfig.cache_dir / _EMOJIS, + cache_dir=pconfig.cache_dir / "emojis", show_progress=True, ) @@ -152,9 +150,9 @@ def _load_fonts(cls): @classmethod def _load_video_button(cls): with Image.open(cls.DEFAULT_VIDEO_BUTTON_PATH) as img: - cls.video_button_image: PILImage = img.convert("RGBA") + cls.video_button_image: PILImage = img.convert("RGBA").resize((128, 128)) alpha = cls.video_button_image.split()[-1] - alpha = alpha.point(lambda x: int(x * 0.3)) + alpha = alpha.point(lambda x: int(x * 0.5)) cls.video_button_image.putalpha(alpha) @classmethod diff --git a/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf b/src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf similarity index 100% rename from src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW-1.ttf rename to src/nonebot_plugin_parser/renders/resources/HYSongYunLangHeiW.ttf diff --git a/src/nonebot_plugin_parser/renders/resources/media_button.png b/src/nonebot_plugin_parser/renders/resources/media_button.png deleted file mode 100644 index b57717f6d9731e39e659de08830925ef74707fa9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2510 zcmZ`*3piBk8vf_PAWS5=j&X^ujL2>?#wZ$uR8(@Qc1eb!a>=F4xI|K<3)yVi9NlPY zS7MSzlupIC+lFQax!)NxW6YSdYMyhR=RD6j@A}sFzwi5h|F{12JZr6IUG?%LsVM0v z0RU87U7YqqBjv}bfPgZOdvggIkim`~jsQH*S|vU%2WjjHm;D|9#F;{MDFBFm)QSqM{<>Nl8f*3dP38MkeFp z;(~&Lpo_}O%gY>DE-5Lowzig&`*Fh%AZ}!2B$HSy79#%s{?Pubs;Zz&qtPlVD$2{t zQ&LhqJv}`*(l!{Fas$CX?CF z(D3HXo7b;jL*)7M=WI5+wzd{>AkgXbzP`Sxsi~fx9;hL-1nSw|-VTx0)>de_ti!~_ z#Q6C5$jHd>@bLGa;BYu|b8`ZLfXn4VKD4~Ix0l6YL4^Mkyq{QDSeTuiotc@L{)x%S zNzuQtyu1uiC@(E7357!0s>Q`ci9`aKP?kX~7DE!k%6|izg$yW5|36k%q)?4mA`y!v z%Rl;v1wzrHK)A>kj!n*tPE3!CPY;hxaRx_OoT1L%{tgzmrJK#{{Pw1yncmP`UEfso z{wt0CvAn9G;Bj@{gUZ~pS9eQaX5>Ff&wCtwwJ<8ZATqt+{FVHOlzZXHdE~^~r!UR#8QxRncl{t0AbV zuhCemv38y2I&Cd&U0oerJw1IreT>1618Zn(Y;0s~vVOgZsp-azn>TIUYHp6hZQE{Z zi^tm&h>ngkMuaOsb-KD6`5@8G)4tg6sX-V^a@+=tiabCtz^S&6b!DE z?Y=pm9v*&Yyl&5HKeqKtEj$jfg_21gO|#!EsZO)%wsH@+)|VVkY;|84CD5#5t1e4B zf1Bm%dM%bmh3Qkstf5}oCO!pWlJk{l9-{}#PJ5A$&(uu8qO(7rE<0011m0HTnA@N5 z7p)$S-W@D(0frdi2B(3#B1ij)?`G=60!)qeNwD6cG{}xzwE$*n9f~L3Dar%ghcvY+t?q z{A#|wnbvN-V!nX!36oT>00Wr6aq2LD?LRKZ+_lLS2`B|92c!`ICia<@k=H$vNC5UJ zt}3i|2#Lf}{;t;!4b30$r2v5Lo~|q>5!F5cEM~ed;DD7s91dcu&Z%S&HNiaY3_1h5 z6U@&JeW{0uJUgQ_9sNP_yTk=Iz_s3WXb?V1liE8f3dm+5fbQz8-3^uCV;k#z!Wz=Gku2{!r5}cGI#I zECH?#DDI*IS$dproxfH#4M`@#;oATry`58?w)Nfq^ZM@+)rHO`w^j&#{AWYC*W~Q% zI*O3RV1SpU!)gCEX2IEJy*^+D4YqiT*3I$kHl6uU^G<9n*b6rSChdY;<^Xyu3!&Um zoA`^h;#lIMDD0ewnkWF^xX60Bju*0o zqwKhpFUekm)}ri?DAw_Omx&?(@(Bu9_r&v92{B#^xEr8Adm~KKNb;-@g*wbABt_IB z0HduDHf+IYMik7c!4vpqFJ}gU!B@4KD7mo{Ad=!J?o!!eAYZIrN}K1&eL;cjp2odi zDN}gHq%q*VTF;J+0JhF+#H<{bqtjZHW1dM%Zk8lW@M+GMCdI?UwjiZDo>lj{p;9 zQ3wC{W_Nof)h;Rvx7;r|2W*hh$G^~VE? zxvo170W?2fyQ3Sb&Kk~KoGP(D-~sS(Q>t3tL7U*-X!Fo}S4=IJiAVyqOxM(6SGV`o zB7dR&t;|2k<=)byw1&xKq+Z<=#oEYPncj8xV45~iJf#R>+!?!qkw=Caubt({_40odpbScc`WH)71Out diff --git a/src/nonebot_plugin_parser/renders/resources/play.png b/src/nonebot_plugin_parser/renders/resources/play.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b6e905fe008d6110dd2eaacbceacc4e1babe63 GIT binary patch literal 5724 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 O0000 Date: Sun, 8 Feb 2026 10:21:14 +0800 Subject: [PATCH 10/11] tweak --- src/nonebot_plugin_parser/renders/common.py | 6 +++--- .../renders/resources/play.png | Bin 5724 -> 4399 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 5336ea8c..bfd5ab7d 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -150,7 +150,7 @@ def _load_fonts(cls): @classmethod def _load_video_button(cls): with Image.open(cls.DEFAULT_VIDEO_BUTTON_PATH) as img: - cls.video_button_image: PILImage = img.convert("RGBA").resize((128, 128)) + cls.video_button_image: PILImage = img.convert("RGBA").resize((100, 100)) alpha = cls.video_button_image.split()[-1] alpha = alpha.point(lambda x: int(x * 0.5)) cls.video_button_image.putalpha(alpha) @@ -354,7 +354,7 @@ async def _render_title(self, ctx: RenderContext) -> None: async def _render_cover_or_images(self, ctx: RenderContext) -> None: """渲染封面或图片网格""" - # 尝试封面 + cover_path = await ctx.result.cover_path if cover_path and cover_path.exists(): cover = self._load_cover(cover_path, ctx.content_width) @@ -362,7 +362,7 @@ async def _render_cover_or_images(self, ctx: RenderContext) -> None: x_pos = self.PADDING ctx.image.paste(cover, (x_pos, ctx.y_pos)) # 视频按钮 - btn_size = 128 + btn_size = 100 btn_x = x_pos + (cover.width - btn_size) // 2 btn_y = ctx.y_pos + (cover.height - btn_size) // 2 ctx.image.paste(self.video_button_image, (btn_x, btn_y), self.video_button_image) diff --git a/src/nonebot_plugin_parser/renders/resources/play.png b/src/nonebot_plugin_parser/renders/resources/play.png index e3b6e905fe008d6110dd2eaacbceacc4e1babe63..cf18b8f58fea7e39818089cacc8bb3953708acce 100644 GIT binary patch literal 4399 zcmW+)c{G&m8-Cx}Vm>lNMoMo|#*!?d5U=cnv6Hnysboo(5U)XL3`UV8jGb&DQBw9L zpM)AqOv+NW5czEJ)9;VxdCs~1xX*pv=enMApX(mkM2{PP01p5Fw}HO4`R?2C_i*5L z?^4^c3;-Yi2HKjILEzjI`^QgeE2Co-@>Vx}jq^i@GCVmU@~dHi2}P?LzBJ=Ys7@V} z<3kO%4vYb>(b7J?+&|&o+pgG>h7uIc&H8bW_?yu6U;dWrH$!c<*TxeLTTCv!>izzv zK4|}|yfav?jwkZMZ*3M=HH-hWYds5}=}9nK&mXVLE_-8=+&-(Dxw-nW=X>cE^NaoT zJUN6dN^*e%XoW6f1T+_Iv`5`TLqp?jQKxUitn06NsXy#@JoBCU4GPGE5vrl8EF^_n z#WmZEmU61mD2oqZF4+=~$@@k)AGf-QAvp{uICRli*Q$*i8Wx6X=1CQlkK7wOA`-El zlv#3ju~`4Ex$t#Z7cXV|Na=U#w`CDpua*Cyccv^Yx7IKf>y*Iu`YR%g{aKGV)70ze z1r&t=#ozu_2ylm*m8midl)(^0f2{|5HDU}UqU-(=n${o-i_kWa~|iO8tO3UYHqG8<)y_D9#EW)=Tp z+w-A`2nSc?oi^sqF{G56b4imyKUb zmQ-t>uBUl64NmYn6>*FJq!bv5Ohm@rmCATHFTSp4-f&L9Pzz3<8P za|b!7(7)@=UquwGnBnk)$m=O+)Iq+c1!Xa6jVd-+|5R1&DJ2;+rX72CXSB`n{91D) z>TBLNcZLIs!^gugxm#`dB6-WV1jeQ=h}7;kL$t(VD?H!xiWYAaN?4`AN~oxQvw0n z1*UyXE{12`MeQjW(-8W8Cxm&=-|7LHa+<{*RX&^#C_7ND*_Y1Sh+{E(c2#Uwb`k3& z1$*+kmY0u&>&U}yr%&E837>i9j31CTix3v;WWDGF8W;JnHQkdEOk1PX8Hf{lCnC+f zZ|2A*41fEGA8|So<&{03vpWRQVT)o9t%*CzgoF`@63z?!5xg>dnY_6~B9U z{imk{9SxLiOHnXi|4R=f#-Uu55>Nj;Tnv9KvgU`8qe?GBKKY)Nf~6&o=bW$30Ee

iLun^8&##2rC5R#3&z36R}Z1I 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 ZqJiLf+YPl3xvIMeV4!27U3Sie`ae)Z+nN9X literal 5724 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 O0000 Date: Sun, 8 Feb 2026 10:26:19 +0800 Subject: [PATCH 11/11] tweak --- src/nonebot_plugin_parser/parsers/acfun/video.py | 2 +- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 7 ++----- src/nonebot_plugin_parser/parsers/bilibili/favlist.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/acfun/video.py b/src/nonebot_plugin_parser/parsers/acfun/video.py index 6cd8d64f..89509899 100644 --- a/src/nonebot_plugin_parser/parsers/acfun/video.py +++ b/src/nonebot_plugin_parser/parsers/acfun/video.py @@ -52,7 +52,7 @@ def avatar_url(self) -> str: @property def text(self) -> str | None: - return f"简介: {self.description}" if self.description else None + return self.description @property def timestamp(self) -> int: diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 24b99659..7db06324 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -105,11 +105,8 @@ async def parse_video( from .video import VideoInfo, AIConclusion video = await self._get_video(bvid=bvid, avid=avid) - # 转换为 msgspec struct video_info = convert(await video.get_info(), VideoInfo) - # 获取简介 - text = f"简介: {video_info.desc}" if video_info.desc else None - # up + # UP author = self.create_author(video_info.owner.name, video_info.owner.face) # 处理分 p page_info = video_info.extract_info_with_page(page_num) @@ -153,7 +150,7 @@ async def download_video(): url=url, title=page_info.title, timestamp=page_info.timestamp, - text=text, + text=video_info.desc, author=author, contents=[video_content], extra={"info": ai_summary}, diff --git a/src/nonebot_plugin_parser/parsers/bilibili/favlist.py b/src/nonebot_plugin_parser/parsers/bilibili/favlist.py index 823deca7..51a28650 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/favlist.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/favlist.py @@ -59,7 +59,7 @@ def cover(self) -> str: @property def desc(self) -> str: - return f"简介: {self.info.intro}" + return self.info.intro @property def timestamp(self) -> int: