"""Bilibili Live Danmaku Handler. Connects to a Bilibili live room and puts incoming danmaku messages into an asyncio queue. Supports cookie-based login with browser header emulation. """ import asyncio import logging from typing import Optional import blivedm from blivedm.models.web import DanmakuMessage from bili_login import build_danmaku_session logger = logging.getLogger(__name__) DANMAKU_FORMAT = "{uname}\u8bf4\u3001 {msg}" class DanmakuHandler(blivedm.BaseHandler): """Handles incoming danmaku from a Bilibili live room.""" def __init__(self, queue: asyncio.Queue, message_format: str = DANMAKU_FORMAT): super().__init__() self._queue = queue self._message_format = message_format self._count = 0 def _on_danmaku(self, client: blivedm.BLiveClient, message: DanmakuMessage) -> None: if not message.msg.strip(): return text = self._message_format.format( uname=message.uname, msg=message.msg, uid=message.uid, ) self._count += 1 if self._count % 20 == 1: logger.info("Danmaku #%d: %s", self._count, text) else: logger.debug("Danmaku #%d: %s", self._count, text) try: self._queue.put_nowait(text) except asyncio.QueueFull: logger.warning("Danmaku queue full, dropping: %s", text) def _on_gift(self, client: blivedm.BLiveClient, message) -> None: logger.debug("Gift: %s x%d from %s", message.gift_name, message.num, message.uname) def _on_interact_word_v2(self, client: blivedm.BLiveClient, message) -> None: logger.debug("Interact: %s (type=%d)", message.uname, message.msg_type) def on_client_stopped(self, client, exception: Optional[Exception]) -> None: if exception is not None: logger.error("Client stopped with exception: %s", exception) else: logger.info("Client stopped normally.") class DanmakuClient: """Manages the BLiveClient connection for a single room.""" def __init__( self, room_id: int, queue: asyncio.Queue, cookies: Optional[dict[str, str]] = None, message_format: str = DANMAKU_FORMAT, ): self._room_id = room_id self._queue = queue self._cookies = cookies self._message_format = message_format self._client: Optional[blivedm.BLiveClient] = None self._session: Optional["aiohttp.ClientSession"] = None async def start(self) -> None: import aiohttp self._session = await build_danmaku_session(self._cookies) self._client = blivedm.BLiveClient(self._room_id, session=self._session) handler = DanmakuHandler(self._queue, self._message_format) self._client.set_handler(handler) self._client.start() logger.info("Connected to room %d", self._room_id) async def stop(self) -> None: if self._client is not None: logger.info("Disconnecting from room %d...", self._room_id) await self._client.stop_and_close() self._client = None if self._session is not None: await self._session.close() self._session = None @property def room_id(self) -> int: return self._room_id