Files

216 lines
8.2 KiB
Python
Executable File

import asyncio
import json
import time
import logging
from collections import deque
from typing import Set, Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
# ==================== 配置日志 ====================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('danmaku.log', encoding='utf-8')
]
)
logger = logging.getLogger("danmaku")
MSG_NO_LOGIN = "未登录,无法加入弹幕"
MSG_BAD_TOKEN = "登录已失效,请刷新页面重试"
NICK_GUEST = "观众"
# ==================== 弹幕登录校验(live token -> live-api) ====================
import os as _os
VERIFY_URL = 'https://api.rrrosmontis.icu/v1/live/api/auth/verify'
try:
_cfg = json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'danmaku_auth.json'), encoding='utf-8'))
VERIFY_URL = _cfg.get('verify_url', VERIFY_URL)
except Exception:
pass
def _verify_token_sync(token):
try:
import urllib.request as _ur
req = _ur.Request(VERIFY_URL, data=json.dumps({'token': token}).encode('utf-8'), headers={'Content-Type': 'application/json'})
with _ur.urlopen(req, timeout=6) as resp:
d = json.loads(resp.read().decode('utf-8'))
return d.get('user') if d.get('ok') else None
except Exception:
return None
async def _verify_token(token):
return await asyncio.to_thread(_verify_token_sync, token)
# ==================== 直播间类 ====================
class LiveRoom:
def __init__(self, room_id: int):
self.room_id = room_id
self.start_time = time.time()
self.timeline: list = []
self.timeline_lock = asyncio.Lock()
self.recent_danmaku = deque(maxlen=20)
self.connections: Set[WebSocket] = set()
self.connections_lock = asyncio.Lock()
logger.info(f"创建新房间 {room_id},开始时间: {self.start_time}")
# ==================== 全局房间管理 ====================
rooms: Dict[int, LiveRoom] = {}
rooms_lock = asyncio.Lock()
async def get_or_create_room(room_id: int) -> LiveRoom:
async with rooms_lock:
if room_id not in rooms:
rooms[room_id] = LiveRoom(room_id)
return rooms[room_id]
async def remove_connection_from_room(websocket: WebSocket):
async with rooms_lock:
for room in rooms.values():
async with room.connections_lock:
if websocket in room.connections:
room.connections.remove(websocket)
logger.info(f"从房间 {room.room_id} 移除连接,当前连接数: {len(room.connections)}")
break
# ==================== FastAPI 应用 ====================
app = FastAPI()
@app.websocket("/danmaku")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
client_host = websocket.client.host if websocket.client else "unknown"
token = (websocket.query_params.get('token') or '').strip()
logger.info(f"新WebSocket连接: {client_host}")
current_room: LiveRoom = None
current_user: str = None
try:
async for message in websocket.iter_text():
logger.info(f"收到消息 from {client_host}: {message[:200]}")
data = json.loads(message)
msg_type = data.get("type")
if msg_type == "join":
# 房间号处理
room_id = data.get("room")
try:
room_id = int(room_id)
except (TypeError, ValueError):
await websocket.send_json({"type": "error", "message": "房间号必须是整数"})
continue
# 如果已经加入过房间,先从原房间移除
if current_room:
async with current_room.connections_lock:
current_room.connections.discard(websocket)
# 加入新房间
# 登录校验: 弹幕连接必须携带有效 live token (?token=)
if not token:
await websocket.send_json({"type": "error", "message": MSG_NO_LOGIN})
await websocket.close(code=4401)
return
user_info = await _verify_token(token)
if not user_info:
await websocket.send_json({"type": "error", "message": MSG_BAD_TOKEN})
await websocket.close(code=4401)
return
current_room = await get_or_create_room(room_id)
current_user = user_info.get("nickname") or NICK_GUEST
async with current_room.connections_lock:
current_room.connections.add(websocket)
logger.info(f"客户端 {client_host} 加入房间 {room_id},用户名 {current_user}")
# 发送 init 消息(历史弹幕等)
elapsed = time.time() - current_room.start_time
init_msg = {
"type": "init",
"start_time": current_room.start_time,
"elapsed": elapsed,
"recent_danmaku": list(current_room.recent_danmaku)
}
await websocket.send_json(init_msg)
continue
elif msg_type == "danmaku":
if current_room is None:
await websocket.send_json({"type": "error", "message": "请先发送 join 消息加入房间"})
continue
text = data.get("text", "")
user = current_user or NICK_GUEST
danmaku = {
"text": text,
"user": user,
"time": time.time()
}
# 记录到房间的最近弹幕和 timeline
current_room.recent_danmaku.append(danmaku)
async with current_room.timeline_lock:
current_room.timeline.append({
"time": time.time() - current_room.start_time,
"type": "danmaku",
"user": danmaku["user"],
"text": danmaku["text"]
})
logger.info(f"弹幕 (房间 {current_room.room_id}): {danmaku['user']}: {danmaku['text']}")
# 广播给同一房间的所有客户端
await broadcast_danmaku(current_room, danmaku)
continue
else:
logger.warning(f"未知消息类型: {msg_type}")
await websocket.send_json({"type": "error", "message": f"未知消息类型: {msg_type}"})
except WebSocketDisconnect:
logger.info(f"WebSocket断开: {client_host}")
except Exception as e:
logger.error(f"WebSocket处理异常: {e}", exc_info=True)
finally:
if current_room:
async with current_room.connections_lock:
current_room.connections.discard(websocket)
logger.info(f"从房间 {current_room.room_id} 移除连接,当前连接数: {len(current_room.connections)}")
else:
await remove_connection_from_room(websocket)
logger.info(f"当前所有房间连接状态: {[(rid, len(room.connections)) for rid, room in rooms.items()]}")
async def broadcast_danmaku(room: LiveRoom, danmaku: dict):
async with room.connections_lock:
for conn in room.connections:
try:
await conn.send_json({"type": "danmaku", "data": danmaku})
except:
pass
logger.info(f"广播弹幕到房间 {room.room_id}: {danmaku['user']}: {danmaku['text']}")
@app.get("/")
async def root():
return {
"status": "running",
"rooms": [
{
"room_id": rid,
"connections": len(room.connections),
"timeline_count": len(room.timeline),
"start_time": room.start_time
} for rid, room in rooms.items()
]
}
# ==================== 启动服务 ====================
if __name__ == "__main__":
import uvicorn
logger.info("启动 uvicorn 服务器,端口 55556")
uvicorn.run(app, host="0.0.0.0", port=55556)