- Add web UI panel (dark theme) at / with REST API - Add Bilibili QR code scan login with browser header emulation - Add wanakana for English -> katakana conversion - Add volume control (0%-200%) - Add message format with '说、' pause marker - PyInstaller --onedir build for portable exe - Rate-limit danmaku logging (1/20) - Fix aiohttp.access log spam - Add README.md
99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
"""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
|