diff --git a/blivedm/clients/wbi.py b/blivedm/clients/wbi.py new file mode 100644 index 0000000..75f7e01 --- /dev/null +++ b/blivedm/clients/wbi.py @@ -0,0 +1,102 @@ +import hashlib +import logging +import time +import urllib.parse +from typing import Any, Dict + +import aiohttp + +from ..utils import USER_AGENT + +logger = logging.getLogger("blivedm") + +UID_INIT_URL = "https://api.bilibili.com/x/web-interface/nav" + +WTS = "wts" +W_RID = "w_rid" + +KEY_LENGTH = 32 + +# fmt: off +KEY_MAP = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, + 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, + 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, + 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52, +] +# fmt: on + +INVALID_CHARS = ["!", "'", "(", ")", "*"] + + +def filtered_string(s: str) -> str: + return "".join(c for c in s if c not in INVALID_CHARS) + + +def extract_key_part(url: str) -> str: + slash = url.rfind("/") + if slash == -1: + raise ValueError("missing url slash") + dot = url[slash:].find(".") + if dot == -1: + raise ValueError("missing url dot") + return url[slash + 1 : slash + dot] + + +def sign_content_with_key(content: str, key: str) -> str: + hasher = hashlib.md5() + hasher.update(f"{content}{key}".encode()) + return hasher.hexdigest() + + +async def get_wbi_key(session: aiohttp.ClientSession) -> str: + async with session.get( + UID_INIT_URL, + headers={"User-Agent": USER_AGENT}, + ) as response: + data = await response.json() + + wbi_img = data["data"]["wbi_img"] + img = extract_key_part(wbi_img["img_url"]) + sub = extract_key_part(wbi_img["sub_url"]) + + if not img or not sub: + raise ValueError("missing wbi key") + + full = img + sub + key_chars = ["\0"] * KEY_LENGTH + for i, index in enumerate(KEY_MAP[:KEY_LENGTH]): + key_chars[i] = full[index] if index < len(full) else "\0" + + return "".join(key_chars) + + +async def signed_query( + session: aiohttp.ClientSession, query: Dict[str, Any] +) -> Dict[str, str]: + ts = str(int(time.time())) + + filtered_query = [] + for k, v in query.items(): + filtered_query.append((k, filtered_string(str(v)))) + + filtered_query.append((WTS, ts)) + filtered_query.sort(key=lambda x: x[0]) + + content = urllib.parse.urlencode(filtered_query) + + try: + key = await get_wbi_key(session) + except (aiohttp.ClientConnectionError, aiohttp.ClientResponseError, ValueError): + logger.exception("get_wbi_key() failed:") + return query + + query_sign = sign_content_with_key(content, key) + + signed_query = { + **query, + WTS: ts, + W_RID: query_sign, + } + + return signed_query \ No newline at end of file diff --git a/blivedm/clients/web.py b/blivedm/clients/web.py index 5ff71aa..6145ffe 100644 --- a/blivedm/clients/web.py +++ b/blivedm/clients/web.py @@ -6,8 +6,9 @@ from typing import * import aiohttp import yarl -from . import ws_base from .. import utils +from . import ws_base +from .wbi import UID_INIT_URL, signed_query __all__ = ( 'BLiveClient', @@ -15,7 +16,6 @@ __all__ = ( logger = logging.getLogger('blivedm') -UID_INIT_URL = 'https://api.bilibili.com/x/web-interface/nav' BUVID_INIT_URL = 'https://www.bilibili.com/' ROOM_INIT_URL = 'https://api.live.bilibili.com/room/v1/Room/get_info' DANMAKU_SERVER_CONF_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo' @@ -35,12 +35,12 @@ class BLiveClient(ws_base.WebSocketClientBase): """ def __init__( - self, - room_id: int, - *, - uid: Optional[int] = None, - session: Optional[aiohttp.ClientSession] = None, - heartbeat_interval=30, + self, + room_id: int, + *, + uid: Optional[int] = None, + session: Optional[aiohttp.ClientSession] = None, + heartbeat_interval=30, ): super().__init__(session, heartbeat_interval) @@ -50,8 +50,6 @@ class BLiveClient(ws_base.WebSocketClientBase): # 在调用init_room后初始化的字段 self._room_owner_uid: Optional[int] = None - self.live_status: Optional[int] = None - self.live_start_time: Optional[int] = None """主播用户ID""" self._host_server_list: Optional[List[dict]] = None """ @@ -122,8 +120,8 @@ class BLiveClient(ws_base.WebSocketClientBase): try: async with self._session.get( - UID_INIT_URL, - headers={'User-Agent': utils.USER_AGENT}, + UID_INIT_URL, + headers={'User-Agent': utils.USER_AGENT}, ) as res: if res.status != 200: logger.warning('room=%d _init_uid() failed, status=%d, reason=%s', self._tmp_room_id, @@ -160,8 +158,8 @@ class BLiveClient(ws_base.WebSocketClientBase): async def _init_buvid(self): try: async with self._session.get( - BUVID_INIT_URL, - headers={'User-Agent': utils.USER_AGENT}, + BUVID_INIT_URL, + headers={'User-Agent': utils.USER_AGENT}, ) as res: if res.status != 200: logger.warning('room=%d _init_buvid() status error, status=%d, reason=%s', @@ -173,11 +171,11 @@ class BLiveClient(ws_base.WebSocketClientBase): async def _init_room_id_and_owner(self): try: async with self._session.get( - ROOM_INIT_URL, - headers={'User-Agent': utils.USER_AGENT}, - params={ - 'room_id': self._tmp_room_id - }, + ROOM_INIT_URL, + headers={'User-Agent': utils.USER_AGENT}, + params={ + 'room_id': self._tmp_room_id + }, ) as res: if res.status != 200: logger.warning('room=%d _init_room_id_and_owner() failed, status=%d, reason=%s', self._tmp_room_id, @@ -203,12 +201,12 @@ class BLiveClient(ws_base.WebSocketClientBase): async def _init_host_server(self): try: async with self._session.get( - DANMAKU_SERVER_CONF_URL, - headers={'User-Agent': utils.USER_AGENT}, - params={ - 'id': self._room_id, - 'type': 0 - }, + DANMAKU_SERVER_CONF_URL, + headers={'User-Agent': utils.USER_AGENT}, + params=await signed_query(self._session, { + 'id': self._room_id, + 'type': 0 + }), ) as res: if res.status != 200: logger.warning('room=%d _init_host_server() failed, status=%d, reason=%s', self._room_id, @@ -264,4 +262,4 @@ class BLiveClient(ws_base.WebSocketClientBase): } if self._host_server_token is not None: auth_params['key'] = self._host_server_token - await self._websocket.send_bytes(self._make_packet(auth_params, ws_base.Operation.AUTH)) + await self._websocket.send_bytes(self._make_packet(auth_params, ws_base.Operation.AUTH)) \ No newline at end of file diff --git a/main.py b/main.py index d0b39d7..8eff1e4 100644 --- a/main.py +++ b/main.py @@ -111,8 +111,8 @@ async def connect_room(room_id): room_status_log.write(json.dumps( { "room_id": room_id, - "live_status": client.live_status, - "live_start_time": client.live_start_time, + # "live_status": client.live_status, + # "live_start_time": client.live_start_time, }, separators=(",", ":"), ensure_ascii=False,