最低支持的Python版本升级到3.8

This commit is contained in:
John Smith 2023-03-26 00:51:50 +08:00
parent 61e6825d4e
commit 924b99eed6
5 changed files with 204 additions and 300 deletions

View File

@ -1,15 +1,18 @@
# blivedm # blivedm
Python 3获取bilibili直播弹幕使用websocket协议
Python获取bilibili直播弹幕的库使用WebSocket协议
[协议解释](https://blog.csdn.net/xfgryujk/article/details/80306776)(有点过时了,总体是没错的) [协议解释](https://blog.csdn.net/xfgryujk/article/details/80306776)(有点过时了,总体是没错的)
基于本库开发的一个应用:[blivechat](https://github.com/xfgryujk/blivechat) 基于本库开发的一个应用:[blivechat](https://github.com/xfgryujk/blivechat)
## 使用说明 ## 使用说明
1. 需要Python 3.6及以上版本
1. 需要Python 3.8及以上版本
2. 安装依赖 2. 安装依赖
```sh ```sh
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. 例程看[sample.py](./sample.py) 3. 例程看[sample.py](./sample.py)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio import asyncio
import collections
import enum import enum
import json import json
import logging import logging
@ -26,7 +25,14 @@ DEFAULT_DANMAKU_SERVER_LIST = [
] ]
HEADER_STRUCT = struct.Struct('>I2H2I') 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 # WS_BODY_PROTOCOL_VERSION
@ -95,8 +101,8 @@ class BLiveClient:
heartbeat_interval=30, heartbeat_interval=30,
ssl: Union[bool, ssl_.SSLContext] = True, ssl: Union[bool, ssl_.SSLContext] = True,
): ):
# 用来init_room的临时房间ID可以用短ID
self._tmp_room_id = room_id self._tmp_room_id = room_id
"""用来init_room的临时房间ID可以用短ID"""
self._uid = uid self._uid = uid
if session is None: if session is None:
@ -110,29 +116,31 @@ class BLiveClient:
self._heartbeat_interval = heartbeat_interval self._heartbeat_interval = heartbeat_interval
self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa
# 消息处理器,可动态增删
self._handlers: List[handlers.HandlerInterface] = [] self._handlers: List[handlers.HandlerInterface] = []
"""消息处理器,可动态增删"""
# 在调用init_room后初始化的字段 # 在调用init_room后初始化的字段
# 真实房间ID
self._room_id = None self._room_id = None
# 房间短ID没有则为0 """真实房间ID"""
self._room_short_id = None self._room_short_id = None
# 主播用户ID """房间短ID没有则为0"""
self._room_owner_uid = None self._room_owner_uid = None
# 弹幕服务器列表 """主播用户ID"""
# [{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...]
self._host_server_list: Optional[List[dict]] = None 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 self._host_server_token = None
"""连接弹幕服务器用的token"""
# 在运行时初始化的字段 # 在运行时初始化的字段
# websocket连接
self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
# 网络协程的future """WebSocket连接"""
self._network_future: Optional[asyncio.Future] = None self._network_future: Optional[asyncio.Future] = None
# 发心跳包定时器的handle """网络协程的future"""
self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
"""发心跳包定时器的handle"""
@property @property
def is_running(self) -> bool: def is_running(self) -> bool:
@ -192,7 +200,7 @@ class BLiveClient:
logger.warning('room=%s client is running, cannot start() again', self.room_id) logger.warning('room=%s client is running, cannot start() again', self.room_id)
return return
self._network_future = asyncio.ensure_future(self._network_coroutine_wrapper()) self._network_future = asyncio.create_task(self._network_coroutine_wrapper())
def stop(self): def stop(self):
""" """
@ -355,7 +363,7 @@ class BLiveClient:
except asyncio.CancelledError: except asyncio.CancelledError:
# 正常停止 # 正常停止
pass pass
except Exception as e: # noqa except Exception: # noqa
logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id) logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id)
finally: finally:
logger.debug('room=%s _network_coroutine() finished', self.room_id) logger.debug('room=%s _network_coroutine() finished', self.room_id)
@ -416,7 +424,7 @@ class BLiveClient:
async def _on_ws_connect(self): async def _on_ws_connect(self):
""" """
websocket连接成功 WebSocket连接成功
""" """
await self._send_auth() await self._send_auth()
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
@ -425,7 +433,7 @@ class BLiveClient:
async def _on_ws_close(self): async def _on_ws_close(self):
""" """
websocket连接断开 WebSocket连接断开
""" """
if self._heartbeat_timer_handle is not None: if self._heartbeat_timer_handle is not None:
self._heartbeat_timer_handle.cancel() self._heartbeat_timer_handle.cancel()
@ -457,7 +465,7 @@ class BLiveClient:
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
self._heartbeat_interval, self._on_send_heartbeat self._heartbeat_interval, self._on_send_heartbeat
) )
asyncio.ensure_future(self._send_heartbeat()) asyncio.create_task(self._send_heartbeat())
async def _send_heartbeat(self): async def _send_heartbeat(self):
""" """
@ -475,9 +483,9 @@ class BLiveClient:
async def _on_ws_message(self, message: aiohttp.WSMessage): async def _on_ws_message(self, message: aiohttp.WSMessage):
""" """
收到websocket消息 收到WebSocket消息
:param message: websocket消息 :param message: WebSocket消息
""" """
if message.type != aiohttp.WSMsgType.BINARY: if message.type != aiohttp.WSMsgType.BINARY:
logger.warning('room=%d unknown websocket message type=%s, data=%s', self.room_id, 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): async def _parse_ws_message(self, data: bytes):
""" """
解析websocket消息 解析WebSocket消息
:param data: websocket消息数据 :param data: WebSocket消息数据
""" """
offset = 0 offset = 0
try: try:

View File

@ -12,7 +12,6 @@ __all__ = (
logger = logging.getLogger('blivedm') logger = logging.getLogger('blivedm')
# 常见可忽略的cmd
IGNORED_CMDS = ( IGNORED_CMDS = (
'COMBO_SEND', 'COMBO_SEND',
'ENTRY_EFFECT', 'ENTRY_EFFECT',
@ -38,9 +37,10 @@ IGNORED_CMDS = (
'SUPER_CHAT_MESSAGE_JPN', 'SUPER_CHAT_MESSAGE_JPN',
'WIDGET_BANNER', 'WIDGET_BANNER',
) )
"""常见可忽略的cmd"""
# 已打日志的未知cmd
logged_unknown_cmds = set() logged_unknown_cmds = set()
"""已打日志的未知cmd"""
class HandlerInterface: class HandlerInterface:
@ -75,7 +75,6 @@ class BaseHandler(HandlerInterface):
def __super_chat_message_delete_callback(self, client: client_.BLiveClient, command: dict): 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'])) return self._on_super_chat_delete(client, models.SuperChatDeleteMessage.from_command(command['data']))
# cmd -> 处理回调
_CMD_CALLBACK_DICT: Dict[ _CMD_CALLBACK_DICT: Dict[
str, str,
Optional[Callable[ Optional[Callable[
@ -97,6 +96,7 @@ class BaseHandler(HandlerInterface):
# 删除醒目留言 # 删除醒目留言
'SUPER_CHAT_MESSAGE_DELETE': __super_chat_message_delete_callback, 'SUPER_CHAT_MESSAGE_DELETE': __super_chat_message_delete_callback,
} }
"""cmd -> 处理回调"""
# 忽略其他常见cmd # 忽略其他常见cmd
for cmd in IGNORED_CMDS: for cmd in IGNORED_CMDS:
_CMD_CALLBACK_DICT[cmd] = None _CMD_CALLBACK_DICT[cmd] = None

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import dataclasses
import json import json
from typing import * from typing import *
@ -12,18 +13,14 @@ __all__ = (
) )
@dataclasses.dataclass
class HeartbeatMessage: class HeartbeatMessage:
""" """
心跳消息 心跳消息
:param popularity: 人气值
""" """
def __init__( popularity: int = None
self, """人气值"""
popularity: int = None,
):
self.popularity: int = popularity
@classmethod @classmethod
def from_command(cls, data: dict): def from_command(cls, data: dict):
@ -32,132 +29,84 @@ class HeartbeatMessage:
) )
@dataclasses.dataclass
class DanmakuMessage: 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__( mode: int = None
self, """弹幕显示模式(滚动、顶部、底部)"""
mode: int = None, font_size: int = None
font_size: int = None, """字体尺寸"""
color: int = None, color: int = None
timestamp: int = None, """颜色"""
rnd: int = None, timestamp: int = None
uid_crc32: str = None, """时间戳(毫秒)"""
msg_type: int = None, rnd: int = None
bubble: int = None, """随机数前端叫作弹幕ID可能是去重用的"""
dm_type: int = None, uid_crc32: str = None
emoticon_options: Union[dict, str] = None, """用户ID文本的CRC32"""
voice_config: Union[dict, str] = None, msg_type: int = None
mode_info: dict = 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, uid: int = None
uname: str = None, """用户ID"""
admin: int = None, uname: str = None
vip: int = None, """用户名"""
svip: int = None, admin: int = None
urank: int = None, """是否房管"""
mobile_verify: int = None, vip: int = None
uname_color: str = None, """是否月费老爷"""
svip: int = None
"""是否年费老爷"""
urank: int = None
"""用户身份用来判断是否正式会员猜测非正式会员为5000正式会员为10000"""
mobile_verify: int = None
"""是否绑定手机"""
uname_color: str = None
"""用户名颜色"""
medal_level: str = None, medal_level: str = None
medal_name: str = None, """勋章等级"""
runame: str = None, medal_name: str = None
medal_room_id: int = None, """勋章名"""
mcolor: int = None, runame: str = None
special_medal: str = None, """勋章房间主播名"""
medal_room_id: int = None
"""勋章房间ID"""
mcolor: int = None
"""勋章颜色"""
special_medal: str = None
"""特殊勋章"""
user_level: int = None, user_level: int = None
ulevel_color: int = None, """用户等级"""
ulevel_rank: str = None, ulevel_color: int = None
"""用户等级颜色"""
ulevel_rank: str = None
"""用户等级排名,>50000时为'>50000'"""
old_title: str = None, old_title: str = None
title: str = None, """旧头衔"""
title: str = None
"""头衔"""
privilege_type: int = None, privilege_type: int = None
): """舰队类型0非舰队1总督2提督3舰长"""
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
@classmethod @classmethod
def from_command(cls, info: dict): def from_command(cls, info: dict):
@ -250,60 +199,42 @@ class DanmakuMessage:
return {} return {}
@dataclasses.dataclass
class GiftMessage: 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__( gift_name: str = None
self, """礼物名"""
gift_name: str = None, num: int = None
num: int = None, """数量"""
uname: str = None, uname: str = None
face: str = None, """用户名"""
guard_level: int = None, face: str = None
uid: int = None, """用户头像URL"""
timestamp: int = None, guard_level: int = None
gift_id: int = None, """舰队等级0非舰队1总督2提督3舰长"""
gift_type: int = None, uid: int = None
action: str = None, """用户ID"""
price: int = None, timestamp: int = None
rnd: str = None, """时间戳"""
coin_type: str = None, gift_id: int = None
total_coin: int = None, """礼物ID"""
tid: str = None, gift_type: int = None
): """礼物类型(未知)"""
self.gift_name = gift_name action: str = None
self.num = num """目前遇到的有'喂食''赠送'"""
self.uname = uname price: int = None
self.face = face """礼物单价瓜子数"""
self.guard_level = guard_level rnd: str = None
self.uid = uid """随机数,可能是去重用的。有时是时间戳+去重ID有时是UUID"""
self.timestamp = timestamp coin_type: str = None
self.gift_id = gift_id """瓜子类型,'silver''gold'1000金瓜子 = 1元"""
self.gift_type = gift_type total_coin: int = None
self.action = action """总瓜子数"""
self.price = price tid: str = None
self.rnd = rnd """可能是事务ID有时和rnd相同"""
self.coin_type = coin_type
self.total_coin = total_coin
self.tid = tid
@classmethod @classmethod
def from_command(cls, data: dict): def from_command(cls, data: dict):
@ -326,42 +257,30 @@ class GiftMessage:
) )
@dataclasses.dataclass
class GuardBuyMessage: 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__( uid: int = None
self, """用户ID"""
uid: int = None, username: str = None
username: str = None, """用户名"""
guard_level: int = None, guard_level: int = None
num: int = None, """舰队等级0非舰队1总督2提督3舰长"""
price: int = None, num: int = None
gift_id: int = None, """数量"""
gift_name: str = None, price: int = None
start_time: int = None, """单价金瓜子数"""
end_time: int = None, gift_id: int = None
): """礼物ID"""
self.uid: int = uid gift_name: str = None
self.username: str = username """礼物名"""
self.guard_level: int = guard_level start_time: int = None
self.num: int = num """开始时间戳,和结束时间戳相同"""
self.price: int = price end_time: int = None
self.gift_id: int = gift_id """结束时间戳,和开始时间戳相同"""
self.gift_name: str = gift_name
self.start_time: int = start_time
self.end_time: int = end_time
@classmethod @classmethod
def from_command(cls, data: dict): def from_command(cls, data: dict):
@ -378,72 +297,50 @@ class GuardBuyMessage:
) )
@dataclasses.dataclass
class SuperChatMessage: 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__( price: int = None
self, """价格(人民币)"""
price: int = None, message: str = None
message: str = None, """消息"""
message_trans: str = None, message_trans: str = None
start_time: int = None, """消息日文翻译目前只出现在SUPER_CHAT_MESSAGE_JPN"""
end_time: int = None, start_time: int = None
time: int = None, """开始时间戳"""
id_: int = None, end_time: int = None
gift_id: int = None, """结束时间戳"""
gift_name: str = None, time: int = None
uid: int = None, """剩余时间(约等于 结束时间戳 - 开始时间戳)"""
uname: str = None, id: int = None
face: str = None, """醒目留言ID删除时用"""
guard_level: int = None, gift_id: int = None
user_level: int = None, """礼物ID"""
background_bottom_color: str = None, gift_name: str = None
background_color: str = None, """礼物名"""
background_icon: str = None, uid: int = None
background_image: str = None, """用户ID"""
background_price_color: str = None, uname: str = None
): """用户名"""
self.price: int = price face: str = None
self.message: str = message """用户头像URL"""
self.message_trans: str = message_trans guard_level: int = None
self.start_time: int = start_time """舰队等级0非舰队1总督2提督3舰长"""
self.end_time: int = end_time user_level: int = None
self.time: int = time """用户等级"""
self.id: int = id_ background_bottom_color: str = None
self.gift_id: int = gift_id """底部背景色,'#rrggbb'"""
self.gift_name: str = gift_name background_color: str = None
self.uid: int = uid """背景色,'#rrggbb'"""
self.uname: str = uname background_icon: str = None
self.face: str = face """背景图标"""
self.guard_level: int = guard_level background_image: str = None
self.user_level: int = user_level """背景图URL"""
self.background_bottom_color: str = background_bottom_color background_price_color: str = None
self.background_color: str = background_color """背景价格颜色,'#rrggbb'"""
self.background_icon: str = background_icon
self.background_image: str = background_image
self.background_price_color: str = background_price_color
@classmethod @classmethod
def from_command(cls, data: dict): def from_command(cls, data: dict):
@ -454,7 +351,7 @@ class SuperChatMessage:
start_time=data['start_time'], start_time=data['start_time'],
end_time=data['end_time'], end_time=data['end_time'],
time=data['time'], time=data['time'],
id_=data['id'], id=data['id'],
gift_id=data['gift']['gift_id'], gift_id=data['gift']['gift_id'],
gift_name=data['gift']['gift_name'], gift_name=data['gift']['gift_name'],
uid=data['uid'], uid=data['uid'],
@ -470,18 +367,14 @@ class SuperChatMessage:
) )
@dataclasses.dataclass
class SuperChatDeleteMessage: class SuperChatDeleteMessage:
""" """
删除醒目留言消息 删除醒目留言消息
:param ids: 醒目留言ID数组
""" """
def __init__( ids: List[int] = None
self, """醒目留言ID数组"""
ids: List[int] = None,
):
self.ids: List[int] = ids
@classmethod @classmethod
def from_command(cls, data: dict): def from_command(cls, data: dict):

View File

@ -88,4 +88,4 @@ class MyHandler(blivedm.BaseHandler):
if __name__ == '__main__': if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main()) asyncio.run(main())