最低支持的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
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)

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

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