From 1e58ad78fc9a4694d6e3e1d204f73a2801ce08f9 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 13 May 2018 21:57:36 +0800
Subject: [PATCH] Initial commit

---
 .gitattributes |   2 +
 .gitignore     | 102 +++++++++++++++++++++++++++++++++++
 LICENSE        |  21 ++++++++
 README.md      |   3 ++
 blivedm.py     | 144 +++++++++++++++++++++++++++++++++++++++++++++++++
 sample.py      |  23 ++++++++
 6 files changed, 295 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 blivedm.py
 create mode 100644 sample.py

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()