From 924b99eed6f952a9563f849869cf3a1c98ef171f Mon Sep 17 00:00:00 2001 From: John Smith Date: Sun, 26 Mar 2023 00:51:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E4=BD=8E=E6=94=AF=E6=8C=81=E7=9A=84Py?= =?UTF-8?q?thon=E7=89=88=E6=9C=AC=E5=8D=87=E7=BA=A7=E5=88=B03.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +- blivedm/client.py | 52 +++--- blivedm/handlers.py | 6 +- blivedm/models.py | 435 +++++++++++++++++--------------------------- sample.py | 2 +- 5 files changed, 204 insertions(+), 300 deletions(-) diff --git a/README.md b/README.md index 0a8878c..726ff6d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ # blivedm -Python 3获取bilibili直播弹幕,使用websocket协议 + +Python获取bilibili直播弹幕的库,使用WebSocket协议 [协议解释](https://blog.csdn.net/xfgryujk/article/details/80306776)(有点过时了,总体是没错的) 基于本库开发的一个应用:[blivechat](https://github.com/xfgryujk/blivechat) - ## 使用说明 -1. 需要Python 3.6及以上版本 + +1. 需要Python 3.8及以上版本 2. 安装依赖 + ```sh pip install -r requirements.txt ``` + 3. 例程看[sample.py](./sample.py) diff --git a/blivedm/client.py b/blivedm/client.py index 91bb4b9..dc167c7 100644 --- a/blivedm/client.py +++ b/blivedm/client.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import asyncio -import collections import enum import json import logging @@ -26,7 +25,14 @@ DEFAULT_DANMAKU_SERVER_LIST = [ ] HEADER_STRUCT = struct.Struct('>I2H2I') -HeaderTuple = collections.namedtuple('HeaderTuple', ('pack_len', 'raw_header_size', 'ver', 'operation', 'seq_id')) + + +class HeaderTuple(NamedTuple): + pack_len: int + raw_header_size: int + ver: int + operation: int + seq_id: int # WS_BODY_PROTOCOL_VERSION @@ -95,8 +101,8 @@ class BLiveClient: heartbeat_interval=30, ssl: Union[bool, ssl_.SSLContext] = True, ): - # 用来init_room的临时房间ID,可以用短ID self._tmp_room_id = room_id + """用来init_room的临时房间ID,可以用短ID""" self._uid = uid if session is None: @@ -110,29 +116,31 @@ class BLiveClient: self._heartbeat_interval = heartbeat_interval self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa - # 消息处理器,可动态增删 self._handlers: List[handlers.HandlerInterface] = [] + """消息处理器,可动态增删""" # 在调用init_room后初始化的字段 - # 真实房间ID self._room_id = None - # 房间短ID,没有则为0 + """真实房间ID""" self._room_short_id = None - # 主播用户ID + """房间短ID,没有则为0""" self._room_owner_uid = None - # 弹幕服务器列表 - # [{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...] + """主播用户ID""" self._host_server_list: Optional[List[dict]] = None - # 连接弹幕服务器用的token + """ + 弹幕服务器列表 + [{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...] + """ self._host_server_token = None + """连接弹幕服务器用的token""" # 在运行时初始化的字段 - # websocket连接 self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None - # 网络协程的future + """WebSocket连接""" self._network_future: Optional[asyncio.Future] = None - # 发心跳包定时器的handle + """网络协程的future""" self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None + """发心跳包定时器的handle""" @property def is_running(self) -> bool: @@ -192,7 +200,7 @@ class BLiveClient: logger.warning('room=%s client is running, cannot start() again', self.room_id) return - self._network_future = asyncio.ensure_future(self._network_coroutine_wrapper()) + self._network_future = asyncio.create_task(self._network_coroutine_wrapper()) def stop(self): """ @@ -355,7 +363,7 @@ class BLiveClient: except asyncio.CancelledError: # 正常停止 pass - except Exception as e: # noqa + except Exception: # noqa logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id) finally: logger.debug('room=%s _network_coroutine() finished', self.room_id) @@ -416,7 +424,7 @@ class BLiveClient: async def _on_ws_connect(self): """ - websocket连接成功 + WebSocket连接成功 """ await self._send_auth() self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( @@ -425,7 +433,7 @@ class BLiveClient: async def _on_ws_close(self): """ - websocket连接断开 + WebSocket连接断开 """ if self._heartbeat_timer_handle is not None: self._heartbeat_timer_handle.cancel() @@ -457,7 +465,7 @@ class BLiveClient: self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( self._heartbeat_interval, self._on_send_heartbeat ) - asyncio.ensure_future(self._send_heartbeat()) + asyncio.create_task(self._send_heartbeat()) async def _send_heartbeat(self): """ @@ -475,9 +483,9 @@ class BLiveClient: async def _on_ws_message(self, message: aiohttp.WSMessage): """ - 收到websocket消息 + 收到WebSocket消息 - :param message: websocket消息 + :param message: WebSocket消息 """ if message.type != aiohttp.WSMsgType.BINARY: logger.warning('room=%d unknown websocket message type=%s, data=%s', self.room_id, @@ -494,9 +502,9 @@ class BLiveClient: async def _parse_ws_message(self, data: bytes): """ - 解析websocket消息 + 解析WebSocket消息 - :param data: websocket消息数据 + :param data: WebSocket消息数据 """ offset = 0 try: diff --git a/blivedm/handlers.py b/blivedm/handlers.py index 7db1102..c4dd583 100644 --- a/blivedm/handlers.py +++ b/blivedm/handlers.py @@ -12,7 +12,6 @@ __all__ = ( logger = logging.getLogger('blivedm') -# 常见可忽略的cmd IGNORED_CMDS = ( 'COMBO_SEND', 'ENTRY_EFFECT', @@ -38,9 +37,10 @@ IGNORED_CMDS = ( 'SUPER_CHAT_MESSAGE_JPN', 'WIDGET_BANNER', ) +"""常见可忽略的cmd""" -# 已打日志的未知cmd logged_unknown_cmds = set() +"""已打日志的未知cmd""" class HandlerInterface: @@ -75,7 +75,6 @@ class BaseHandler(HandlerInterface): def __super_chat_message_delete_callback(self, client: client_.BLiveClient, command: dict): return self._on_super_chat_delete(client, models.SuperChatDeleteMessage.from_command(command['data'])) - # cmd -> 处理回调 _CMD_CALLBACK_DICT: Dict[ str, Optional[Callable[ @@ -97,6 +96,7 @@ class BaseHandler(HandlerInterface): # 删除醒目留言 'SUPER_CHAT_MESSAGE_DELETE': __super_chat_message_delete_callback, } + """cmd -> 处理回调""" # 忽略其他常见cmd for cmd in IGNORED_CMDS: _CMD_CALLBACK_DICT[cmd] = None diff --git a/blivedm/models.py b/blivedm/models.py index f1931d4..7bf372e 100644 --- a/blivedm/models.py +++ b/blivedm/models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import dataclasses import json from typing import * @@ -12,18 +13,14 @@ __all__ = ( ) +@dataclasses.dataclass class HeartbeatMessage: """ 心跳消息 - - :param popularity: 人气值 """ - def __init__( - self, - popularity: int = None, - ): - self.popularity: int = popularity + popularity: int = None + """人气值""" @classmethod def from_command(cls, data: dict): @@ -32,132 +29,84 @@ class HeartbeatMessage: ) +@dataclasses.dataclass class DanmakuMessage: """ 弹幕消息 - - :param mode: 弹幕显示模式(滚动、顶部、底部) - :param font_size: 字体尺寸 - :param color: 颜色 - :param timestamp: 时间戳(毫秒) - :param rnd: 随机数,前端叫作弹幕ID,可能是去重用的 - :param uid_crc32: 用户ID文本的CRC32 - :param msg_type: 是否礼物弹幕(节奏风暴) - :param bubble: 右侧评论栏气泡 - :param dm_type: 弹幕类型,0文本,1表情,2语音 - :param emoticon_options: 表情参数 - :param voice_config: 语音参数 - :param mode_info: 一些附加参数 - - :param msg: 弹幕内容 - - :param uid: 用户ID - :param uname: 用户名 - :param admin: 是否房管 - :param vip: 是否月费老爷 - :param svip: 是否年费老爷 - :param urank: 用户身份,用来判断是否正式会员,猜测非正式会员为5000,正式会员为10000 - :param mobile_verify: 是否绑定手机 - :param uname_color: 用户名颜色 - - :param medal_level: 勋章等级 - :param medal_name: 勋章名 - :param runame: 勋章房间主播名 - :param medal_room_id: 勋章房间ID - :param mcolor: 勋章颜色 - :param special_medal: 特殊勋章 - - :param user_level: 用户等级 - :param ulevel_color: 用户等级颜色 - :param ulevel_rank: 用户等级排名,>50000时为'>50000' - - :param old_title: 旧头衔 - :param title: 头衔 - - :param privilege_type: 舰队类型,0非舰队,1总督,2提督,3舰长 """ - def __init__( - self, - mode: int = None, - font_size: int = None, - color: int = None, - timestamp: int = None, - rnd: int = None, - uid_crc32: str = None, - msg_type: int = None, - bubble: int = None, - dm_type: int = None, - emoticon_options: Union[dict, str] = None, - voice_config: Union[dict, str] = None, - mode_info: dict = None, + mode: int = None + """弹幕显示模式(滚动、顶部、底部)""" + font_size: int = None + """字体尺寸""" + color: int = None + """颜色""" + timestamp: int = None + """时间戳(毫秒)""" + rnd: int = None + """随机数,前端叫作弹幕ID,可能是去重用的""" + uid_crc32: str = None + """用户ID文本的CRC32""" + msg_type: int = None + """是否礼物弹幕(节奏风暴)""" + bubble: int = None + """右侧评论栏气泡""" + dm_type: int = None + """弹幕类型,0文本,1表情,2语音""" + emoticon_options: Union[dict, str] = None + """表情参数""" + voice_config: Union[dict, str] = None + """语音参数""" + mode_info: dict = None + """一些附加参数""" - msg: str = None, + msg: str = None + """弹幕内容""" - uid: int = None, - uname: str = None, - admin: int = None, - vip: int = None, - svip: int = None, - urank: int = None, - mobile_verify: int = None, - uname_color: str = None, + uid: int = None + """用户ID""" + uname: str = None + """用户名""" + admin: int = None + """是否房管""" + vip: int = None + """是否月费老爷""" + svip: int = None + """是否年费老爷""" + urank: int = None + """用户身份,用来判断是否正式会员,猜测非正式会员为5000,正式会员为10000""" + mobile_verify: int = None + """是否绑定手机""" + uname_color: str = None + """用户名颜色""" - medal_level: str = None, - medal_name: str = None, - runame: str = None, - medal_room_id: int = None, - mcolor: int = None, - special_medal: str = None, + medal_level: str = None + """勋章等级""" + medal_name: str = None + """勋章名""" + runame: str = None + """勋章房间主播名""" + medal_room_id: int = None + """勋章房间ID""" + mcolor: int = None + """勋章颜色""" + special_medal: str = None + """特殊勋章""" - user_level: int = None, - ulevel_color: int = None, - ulevel_rank: str = None, + user_level: int = None + """用户等级""" + ulevel_color: int = None + """用户等级颜色""" + ulevel_rank: str = None + """用户等级排名,>50000时为'>50000'""" - old_title: str = None, - title: str = None, + old_title: str = None + """旧头衔""" + title: str = None + """头衔""" - privilege_type: int = None, - ): - self.mode: int = mode - self.font_size: int = font_size - self.color: int = color - self.timestamp: int = timestamp - self.rnd: int = rnd - self.uid_crc32: str = uid_crc32 - self.msg_type: int = msg_type - self.bubble: int = bubble - self.dm_type: int = dm_type - self.emoticon_options: Union[dict, str] = emoticon_options - self.voice_config: Union[dict, str] = voice_config - self.mode_info: dict = mode_info - - self.msg: str = msg - - self.uid: int = uid - self.uname: str = uname - self.admin: int = admin - self.vip: int = vip - self.svip: int = svip - self.urank: int = urank - self.mobile_verify: int = mobile_verify - self.uname_color: str = uname_color - - self.medal_level: str = medal_level - self.medal_name: str = medal_name - self.runame: str = runame - self.medal_room_id: int = medal_room_id - self.mcolor: int = mcolor - self.special_medal: str = special_medal - - self.user_level: int = user_level - self.ulevel_color: int = ulevel_color - self.ulevel_rank: str = ulevel_rank - - self.old_title: str = old_title - self.title: str = title - - self.privilege_type: int = privilege_type + privilege_type: int = None + """舰队类型,0非舰队,1总督,2提督,3舰长""" @classmethod def from_command(cls, info: dict): @@ -250,60 +199,42 @@ class DanmakuMessage: return {} +@dataclasses.dataclass class GiftMessage: """ 礼物消息 - - :param gift_name: 礼物名 - :param num: 数量 - :param uname: 用户名 - :param face: 用户头像URL - :param guard_level: 舰队等级,0非舰队,1总督,2提督,3舰长 - :param uid: 用户ID - :param timestamp: 时间戳 - :param gift_id: 礼物ID - :param gift_type: 礼物类型(未知) - :param action: 目前遇到的有'喂食'、'赠送' - :param price: 礼物单价瓜子数 - :param rnd: 随机数,可能是去重用的。有时是时间戳+去重ID,有时是UUID - :param coin_type: 瓜子类型,'silver'或'gold',1000金瓜子 = 1元 - :param total_coin: 总瓜子数 - :param tid: 可能是事务ID,有时和rnd相同 """ - def __init__( - self, - gift_name: str = None, - num: int = None, - uname: str = None, - face: str = None, - guard_level: int = None, - uid: int = None, - timestamp: int = None, - gift_id: int = None, - gift_type: int = None, - action: str = None, - price: int = None, - rnd: str = None, - coin_type: str = None, - total_coin: int = None, - tid: str = None, - ): - self.gift_name = gift_name - self.num = num - self.uname = uname - self.face = face - self.guard_level = guard_level - self.uid = uid - self.timestamp = timestamp - self.gift_id = gift_id - self.gift_type = gift_type - self.action = action - self.price = price - self.rnd = rnd - self.coin_type = coin_type - self.total_coin = total_coin - self.tid = tid + gift_name: str = None + """礼物名""" + num: int = None + """数量""" + uname: str = None + """用户名""" + face: str = None + """用户头像URL""" + guard_level: int = None + """舰队等级,0非舰队,1总督,2提督,3舰长""" + uid: int = None + """用户ID""" + timestamp: int = None + """时间戳""" + gift_id: int = None + """礼物ID""" + gift_type: int = None + """礼物类型(未知)""" + action: str = None + """目前遇到的有'喂食'、'赠送'""" + price: int = None + """礼物单价瓜子数""" + rnd: str = None + """随机数,可能是去重用的。有时是时间戳+去重ID,有时是UUID""" + coin_type: str = None + """瓜子类型,'silver'或'gold',1000金瓜子 = 1元""" + total_coin: int = None + """总瓜子数""" + tid: str = None + """可能是事务ID,有时和rnd相同""" @classmethod def from_command(cls, data: dict): @@ -326,42 +257,30 @@ class GiftMessage: ) +@dataclasses.dataclass class GuardBuyMessage: """ 上舰消息 - - :param uid: 用户ID - :param username: 用户名 - :param guard_level: 舰队等级,0非舰队,1总督,2提督,3舰长 - :param num: 数量 - :param price: 单价金瓜子数 - :param gift_id: 礼物ID - :param gift_name: 礼物名 - :param start_time: 开始时间戳,和结束时间戳相同 - :param end_time: 结束时间戳,和开始时间戳相同 """ - def __init__( - self, - uid: int = None, - username: str = None, - guard_level: int = None, - num: int = None, - price: int = None, - gift_id: int = None, - gift_name: str = None, - start_time: int = None, - end_time: int = None, - ): - self.uid: int = uid - self.username: str = username - self.guard_level: int = guard_level - self.num: int = num - self.price: int = price - self.gift_id: int = gift_id - self.gift_name: str = gift_name - self.start_time: int = start_time - self.end_time: int = end_time + uid: int = None + """用户ID""" + username: str = None + """用户名""" + guard_level: int = None + """舰队等级,0非舰队,1总督,2提督,3舰长""" + num: int = None + """数量""" + price: int = None + """单价金瓜子数""" + gift_id: int = None + """礼物ID""" + gift_name: str = None + """礼物名""" + start_time: int = None + """开始时间戳,和结束时间戳相同""" + end_time: int = None + """结束时间戳,和开始时间戳相同""" @classmethod def from_command(cls, data: dict): @@ -378,72 +297,50 @@ class GuardBuyMessage: ) +@dataclasses.dataclass class SuperChatMessage: """ 醒目留言消息 - - :param price: 价格(人民币) - :param message: 消息 - :param message_trans: 消息日文翻译(目前只出现在SUPER_CHAT_MESSAGE_JPN) - :param start_time: 开始时间戳 - :param end_time: 结束时间戳 - :param time: 剩余时间(约等于 结束时间戳 - 开始时间戳) - :param id_: str,醒目留言ID,删除时用 - :param gift_id: 礼物ID - :param gift_name: 礼物名 - :param uid: 用户ID - :param uname: 用户名 - :param face: 用户头像URL - :param guard_level: 舰队等级,0非舰队,1总督,2提督,3舰长 - :param user_level: 用户等级 - :param background_bottom_color: 底部背景色,'#rrggbb' - :param background_color: 背景色,'#rrggbb' - :param background_icon: 背景图标 - :param background_image: 背景图URL - :param background_price_color: 背景价格颜色,'#rrggbb' """ - def __init__( - self, - price: int = None, - message: str = None, - message_trans: str = None, - start_time: int = None, - end_time: int = None, - time: int = None, - id_: int = None, - gift_id: int = None, - gift_name: str = None, - uid: int = None, - uname: str = None, - face: str = None, - guard_level: int = None, - user_level: int = None, - background_bottom_color: str = None, - background_color: str = None, - background_icon: str = None, - background_image: str = None, - background_price_color: str = None, - ): - self.price: int = price - self.message: str = message - self.message_trans: str = message_trans - self.start_time: int = start_time - self.end_time: int = end_time - self.time: int = time - self.id: int = id_ - self.gift_id: int = gift_id - self.gift_name: str = gift_name - self.uid: int = uid - self.uname: str = uname - self.face: str = face - self.guard_level: int = guard_level - self.user_level: int = user_level - self.background_bottom_color: str = background_bottom_color - self.background_color: str = background_color - self.background_icon: str = background_icon - self.background_image: str = background_image - self.background_price_color: str = background_price_color + price: int = None + """价格(人民币)""" + message: str = None + """消息""" + message_trans: str = None + """消息日文翻译(目前只出现在SUPER_CHAT_MESSAGE_JPN)""" + start_time: int = None + """开始时间戳""" + end_time: int = None + """结束时间戳""" + time: int = None + """剩余时间(约等于 结束时间戳 - 开始时间戳)""" + id: int = None + """醒目留言ID,删除时用""" + gift_id: int = None + """礼物ID""" + gift_name: str = None + """礼物名""" + uid: int = None + """用户ID""" + uname: str = None + """用户名""" + face: str = None + """用户头像URL""" + guard_level: int = None + """舰队等级,0非舰队,1总督,2提督,3舰长""" + user_level: int = None + """用户等级""" + background_bottom_color: str = None + """底部背景色,'#rrggbb'""" + background_color: str = None + """背景色,'#rrggbb'""" + background_icon: str = None + """背景图标""" + background_image: str = None + """背景图URL""" + background_price_color: str = None + """背景价格颜色,'#rrggbb'""" @classmethod def from_command(cls, data: dict): @@ -454,7 +351,7 @@ class SuperChatMessage: start_time=data['start_time'], end_time=data['end_time'], time=data['time'], - id_=data['id'], + id=data['id'], gift_id=data['gift']['gift_id'], gift_name=data['gift']['gift_name'], uid=data['uid'], @@ -470,18 +367,14 @@ class SuperChatMessage: ) +@dataclasses.dataclass class SuperChatDeleteMessage: """ 删除醒目留言消息 - - :param ids: 醒目留言ID数组 """ - def __init__( - self, - ids: List[int] = None, - ): - self.ids: List[int] = ids + ids: List[int] = None + """醒目留言ID数组""" @classmethod def from_command(cls, data: dict): diff --git a/sample.py b/sample.py index 0ee567d..4e014d4 100644 --- a/sample.py +++ b/sample.py @@ -88,4 +88,4 @@ class MyHandler(blivedm.BaseHandler): if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) + asyncio.run(main())