feat: web UI + QR login + portable build

- 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
This commit is contained in:
2026-08-08 13:22:58 +08:00
parent 9b277fe2e0
commit fd5c732d8a
13 changed files with 1470 additions and 206 deletions
+18 -21
View File
@@ -1,6 +1,7 @@
"""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
@@ -10,9 +11,11 @@ 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}: {msg}"
DANMAKU_FORMAT = "{uname}\u8bf4\u3001 {msg}"
class DanmakuHandler(blivedm.BaseHandler):
@@ -22,6 +25,7 @@ class DanmakuHandler(blivedm.BaseHandler):
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():
@@ -31,7 +35,11 @@ class DanmakuHandler(blivedm.BaseHandler):
msg=message.msg,
uid=message.uid,
)
logger.info("Danmaku: %s", text)
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:
@@ -57,44 +65,33 @@ class DanmakuClient:
self,
room_id: int,
queue: asyncio.Queue,
sessdata: str = "",
cookies: Optional[dict[str, str]] = None,
message_format: str = DANMAKU_FORMAT,
):
self._room_id = room_id
self._queue = queue
self._sessdata = sessdata
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:
"""Connect to the live room and start receiving danmaku."""
import http.cookies
import aiohttp
session = aiohttp.ClientSession()
if self._sessdata:
cookies = http.cookies.SimpleCookie()
cookies["SESSDATA"] = self._sessdata
cookies["SESSDATA"]["domain"] = "bilibili.com"
session.cookie_jar.update_cookies(cookies)
self._client = blivedm.BLiveClient(
self._room_id,
session=session,
)
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:
"""Stop the client and clean up."""
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: