commit 1e58ad78fc9a4694d6e3e1d204f73a2801ce08f9 Author: John Smith Date: Sun May 13 21:57:36 2018 +0800 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..101adc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,102 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + + +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd776f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 xfgryujk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d47fd50 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# blivedm + +获取bilibili直播弹幕,使用websocket协议 diff --git a/blivedm.py b/blivedm.py new file mode 100644 index 0000000..69ba24a --- /dev/null +++ b/blivedm.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +import json +import struct +from collections import namedtuple +from enum import IntEnum + +import requests +import websockets + + +class Operation(IntEnum): + SEND_HEARTBEAT = 2 + POPULARITY = 3 + COMMAND = 5 + AUTH = 7 + RECV_HEARTBEAT = 8 + + +class BLiveClient: + ROOM_INIT_URL = 'https://api.live.bilibili.com/room/v1/Room/room_init' + WEBSOCKET_URL = 'wss://broadcastlv.chat.bilibili.com:2245/sub' + + HEADER_STRUCT = struct.Struct('>I2H2I') + HeaderTuple = namedtuple('HeaderTuple', ('total_len', 'header_len', 'proto_ver', 'operation', 'sequence')) + + def __init__(self, room_id): + """ + :param room_id: URL中的房间ID + """ + self._short_id = room_id + self._room_id = None + self._websocket = None + # 未登录 + self._uid = 0 + + async def start(self): + # 获取房间ID + if self._room_id is None: + res = requests.get(self.ROOM_INIT_URL, {'id': self._short_id}) + if res.status_code != 200: + raise ConnectionError() + else: + self._room_id = res.json()['data']['room_id'] + + # 连接 + async with websockets.connect(self.WEBSOCKET_URL) as websocket: + self._websocket = websocket + await self._send_auth() + + # 处理消息 + async for message in websocket: + await self._handle_message(message) + + def _make_packet(self, data, operation): + body = json.dumps(data).encode('utf-8') + header = self.HEADER_STRUCT.pack( + self.HEADER_STRUCT.size + len(body), + self.HEADER_STRUCT.size, + 1, + operation, + 1 + ) + return header + body + + async def _send_auth(self): + auth_params = { + 'uid': self._uid, + 'roomid': self._room_id, + 'protover': 1, + 'platform': 'web', + 'clientver': '1.4.0' + } + await self._websocket.send(self._make_packet(auth_params, Operation.AUTH)) + + async def _send_heartbeat(self): + self._websocket.send(self._make_packet({}, Operation.SEND_HEARTBEAT)) + # TODO 每30s调用 + + async def _handle_message(self, message): + offset = 0 + while offset < len(message): + try: + header = self.HeaderTuple(*self.HEADER_STRUCT.unpack_from(message, offset)) + except struct.error: + break + + if header.operation == Operation.POPULARITY: + popularity = int.from_bytes(message[offset + self.HEADER_STRUCT.size: + offset + self.HEADER_STRUCT.size + 4] + , 'big') + await self._on_get_popularity(popularity) + + elif header.operation == Operation.COMMAND: + body = message[offset + self.HEADER_STRUCT.size: offset + header.total_len] + body = json.loads(body.decode('utf-8')) + await self._handle_command(body) + + elif header.operation == Operation.RECV_HEARTBEAT: + await self._send_heartbeat() + + offset += header.total_len + + async def _handle_command(self, command): + if isinstance(command, list): + for one_command in command: + await self._handle_command(one_command) + return + + cmd = command['cmd'] + # print(command) + + if cmd == 'DANMU_MSG': # 收到弹幕 + await self._on_get_danmaku(command['info'][1], command['info'][2][1]) + + elif cmd == 'SEND_GIFT': # 送礼物 + pass + + elif cmd == 'WELCOME': # 欢迎 + pass + + elif cmd == 'PREPARING': # 房主准备中 + pass + + elif cmd == 'LIVE': # 直播开始 + pass + + else: + print('未知命令:', command) + + async def _on_get_popularity(self, popularity): + """ + 获取到人气值 + :param popularity: 人气值 + """ + pass + + async def _on_get_danmaku(self, content, user_name): + """ + 获取到弹幕 + :param content: 弹幕内容 + :param user_name: 弹幕作者 + """ + pass diff --git a/sample.py b/sample.py new file mode 100644 index 0000000..4e88be2 --- /dev/null +++ b/sample.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from asyncio import get_event_loop + +from blivedm import BLiveClient + + +class MyBLiveClient(BLiveClient): + + async def _on_get_popularity(self, popularity): + print('当前人气值:', popularity) + + async def _on_get_danmaku(self, content, user_name): + print(user_name, '说:', content) + + +def main(): + client = MyBLiveClient(6) + get_event_loop().run_until_complete(client.start()) + + +if __name__ == '__main__': + main()