修复直播测试与 TTS 预热
This commit is contained in:
+303
-25
@@ -61,6 +61,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
MIHOYO_SDK_REGISTRY_SUBKEY = r"Software\miHoYoSDK"
|
||||
MANUAL_LIVE_TEST_DURATION_SECONDS = 30 * 60
|
||||
BILIBILI_LIVE_STATUS_POLL_TIMEOUT_SECONDS = 45.0
|
||||
BILIBILI_LIVE_STATUS_POLL_INTERVAL_SECONDS = 2.0
|
||||
BILIBILI_LIVE_STATUS_REFRESH_SECONDS = 60.0
|
||||
|
||||
|
||||
def delete_mihoyo_sdk_registry(logger: logging.Logger, subkey: str = MIHOYO_SDK_REGISTRY_SUBKEY) -> dict[str, Any]:
|
||||
@@ -511,6 +515,7 @@ class Config:
|
||||
tts_all["faster-qwen3-tts"].setdefault("chunk_size", 8)
|
||||
tts_all["faster-qwen3-tts"].setdefault("append_silence", True)
|
||||
tts_all["faster-qwen3-tts"].setdefault("streaming", True)
|
||||
tts_all["faster-qwen3-tts"].setdefault("startup_timeout_seconds", 900)
|
||||
self.data.setdefault("frontend", {})
|
||||
if legacy_frontend:
|
||||
self.data["frontend"].update(legacy_frontend)
|
||||
@@ -2450,6 +2455,10 @@ class FasterQwen3TTSEngine:
|
||||
self.cpu_threads = max(1, min(8, int(self.cfg.get("cpu_threads", 4) or 4)))
|
||||
self.cpu_affinity_count = max(0, int(self.cfg.get("cpu_affinity_count", 8) or 0))
|
||||
self.process_priority = str(self.cfg.get("process_priority", "below_normal") or "below_normal")
|
||||
self.startup_timeout_seconds = max(
|
||||
60,
|
||||
int(self.cfg.get("startup_timeout_seconds", 900) or 900),
|
||||
)
|
||||
self.logger = logger
|
||||
self._parent = parent
|
||||
self._worker_ready = False
|
||||
@@ -2473,6 +2482,7 @@ class FasterQwen3TTSEngine:
|
||||
},
|
||||
logger,
|
||||
synthesis_timeout_seconds=120,
|
||||
startup_timeout_seconds=self.startup_timeout_seconds,
|
||||
)
|
||||
if bool(self.cfg.get("streaming", False)):
|
||||
self.logger.warning("[FasterQwenTTS] 独立 worker 仅使用非流式模式,已忽略 streaming=true")
|
||||
@@ -2720,6 +2730,7 @@ class Broadcaster:
|
||||
self._tts_sequence = 0
|
||||
self._tts_started = False
|
||||
self._tts_warmup_done = asyncio.Event()
|
||||
self._tts_warmup_task: asyncio.Task | None = None
|
||||
self._broadcast_channels: dict[str, dict[str, str]] = {}
|
||||
self._stop = False
|
||||
# 弹幕会话已登录标志: 启动时ping一下 nav API, -101 表示 SESSDATA 失效, 关闭弹幕发送
|
||||
@@ -2762,9 +2773,14 @@ class Broadcaster:
|
||||
self.logger.warning("[TTS] 配置热更新时无法调度旧 worker 关闭")
|
||||
self.logger.info(f"[配置] 播报配置已热更新: danmu={self.enable_danmu}, tts={self.config.tts_provider}")
|
||||
|
||||
async def _warmup_tts_in_background(self, tts: TTSEngine):
|
||||
try:
|
||||
await tts.warmup()
|
||||
finally:
|
||||
self._tts_warmup_done.set()
|
||||
|
||||
async def start(self):
|
||||
if self._tts_started:
|
||||
await self._tts_warmup_done.wait()
|
||||
return
|
||||
self._tts_started = True
|
||||
if not self.tts.enabled:
|
||||
@@ -2777,10 +2793,16 @@ class Broadcaster:
|
||||
for task in (synth_task, play_task):
|
||||
task.add_done_callback(self._tts_worker_tasks.discard)
|
||||
|
||||
try:
|
||||
if bool(self._tts_queue_cfg.get("warmup_on_start", True)):
|
||||
await self.tts.warmup()
|
||||
finally:
|
||||
if bool(self._tts_queue_cfg.get("warmup_on_start", True)):
|
||||
warmup_task = asyncio.create_task(
|
||||
self._warmup_tts_in_background(self.tts),
|
||||
name="TTS后台预热",
|
||||
)
|
||||
self._tts_warmup_task = warmup_task
|
||||
self._tts_worker_tasks.add(warmup_task)
|
||||
warmup_task.add_done_callback(self._tts_worker_tasks.discard)
|
||||
self.logger.info("[TTS队列] 后台预热已启动,不阻塞弹幕监听")
|
||||
else:
|
||||
self._tts_warmup_done.set()
|
||||
self.logger.info(
|
||||
f"[TTS队列] 流水线启动, pending={self._tts_pending.maxsize}, "
|
||||
@@ -5285,6 +5307,11 @@ class CommandHandler:
|
||||
)
|
||||
|
||||
def _is_live_time(self) -> bool:
|
||||
if self.system and (
|
||||
self.system.manual_live_override_active()
|
||||
or self.system.bilibili_live_active()
|
||||
):
|
||||
return True
|
||||
return is_within_live_time(self.config.system_cfg)
|
||||
|
||||
def _rule_matches(self, rule: dict, text: str) -> bool:
|
||||
@@ -6867,6 +6894,23 @@ def get_real_room_id(room_id: int, cookie: str = "") -> int:
|
||||
return real_room_id
|
||||
|
||||
|
||||
def get_bilibili_live_status(room_id: int, cookie: str = "") -> int:
|
||||
"""返回 B站房间直播状态:0=未开播,1=直播中,其他值按接口原样返回。"""
|
||||
data = _request_bilibili_json(
|
||||
f"https://api.live.bilibili.com/room/v1/Room/get_info?room_id={int(room_id)}",
|
||||
cookie,
|
||||
f"https://live.bilibili.com/{int(room_id)}",
|
||||
)
|
||||
if data.get("code") != 0:
|
||||
raise RuntimeError(
|
||||
f"查询直播状态失败: code={data.get('code')} message={data.get('message', '')}"
|
||||
)
|
||||
try:
|
||||
return int((data.get("data") or {}).get("live_status", -1))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError("直播状态接口未返回有效 live_status") from exc
|
||||
|
||||
|
||||
def get_danmu_server(room_id: int, cookie: str = "") -> dict:
|
||||
"""使用登录 Cookie 获取弹幕服务器和专用 token;token 不得回退为 SESSDATA。"""
|
||||
url = f"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id={int(room_id)}"
|
||||
@@ -7241,6 +7285,9 @@ class SystemScheduler:
|
||||
self._live_session_started_at: datetime | None = None
|
||||
self._startup_compensation_task: asyncio.Task | None = None
|
||||
self._startup_compensation_window_key: str | None = None
|
||||
self._last_live_action_error = ""
|
||||
self._last_bilibili_live_status: int | None = None
|
||||
self._last_live_status_refresh_monotonic = 0.0
|
||||
|
||||
@property
|
||||
def cfg(self) -> dict:
|
||||
@@ -7452,6 +7499,89 @@ class SystemScheduler:
|
||||
user32.EnumWindows(EnumWindowsProc(callback), 0)
|
||||
return hwnd_found[0] if hwnd_found else None
|
||||
|
||||
@property
|
||||
def last_live_action_error(self) -> str:
|
||||
return self._last_live_action_error
|
||||
|
||||
@staticmethod
|
||||
def _force_foreground_window(hwnd: int) -> bool:
|
||||
"""在同一登录会话中临时合并输入线程,可靠地前置目标窗口。"""
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
current_thread = int(kernel32.GetCurrentThreadId())
|
||||
foreground = int(user32.GetForegroundWindow() or 0)
|
||||
target_pid = ctypes.wintypes.DWORD()
|
||||
target_thread = int(user32.GetWindowThreadProcessId(hwnd, ctypes.byref(target_pid)) or 0)
|
||||
foreground_thread = 0
|
||||
if foreground:
|
||||
foreground_pid = ctypes.wintypes.DWORD()
|
||||
foreground_thread = int(
|
||||
user32.GetWindowThreadProcessId(foreground, ctypes.byref(foreground_pid)) or 0
|
||||
)
|
||||
|
||||
attached: list[int] = []
|
||||
try:
|
||||
for thread_id in {target_thread, foreground_thread}:
|
||||
if thread_id and thread_id != current_thread:
|
||||
if user32.AttachThreadInput(current_thread, thread_id, True):
|
||||
attached.append(thread_id)
|
||||
if user32.IsIconic(hwnd):
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
else:
|
||||
user32.ShowWindow(hwnd, 5) # SW_SHOW
|
||||
user32.BringWindowToTop(hwnd)
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
user32.SetActiveWindow(hwnd)
|
||||
user32.SetFocus(hwnd)
|
||||
finally:
|
||||
for thread_id in reversed(attached):
|
||||
user32.AttachThreadInput(current_thread, thread_id, False)
|
||||
return int(user32.GetForegroundWindow() or 0) == int(hwnd)
|
||||
|
||||
async def _get_bilibili_live_status(self, *, record_error: bool = True) -> int | None:
|
||||
try:
|
||||
status = await asyncio.to_thread(
|
||||
get_bilibili_live_status,
|
||||
int(self.config.room_id),
|
||||
self.config.bilibili_cookie,
|
||||
)
|
||||
self._last_bilibili_live_status = status
|
||||
if self.system:
|
||||
self.system.set_bilibili_live_status(status)
|
||||
return status
|
||||
except Exception as exc:
|
||||
message = f"查询 B站直播状态失败: {exc}"
|
||||
if record_error:
|
||||
self._last_live_action_error = message
|
||||
self.logger.warning(f"[系统定时] {message}")
|
||||
else:
|
||||
self.logger.debug(f"[系统定时] {message}")
|
||||
return None
|
||||
|
||||
async def _wait_for_bilibili_live_status(
|
||||
self,
|
||||
expected_status: int,
|
||||
*,
|
||||
timeout_seconds: float = BILIBILI_LIVE_STATUS_POLL_TIMEOUT_SECONDS,
|
||||
) -> bool:
|
||||
effective_timeout = max(1.0, float(timeout_seconds))
|
||||
deadline = time.monotonic() + effective_timeout
|
||||
while True:
|
||||
status = await self._get_bilibili_live_status()
|
||||
if status == expected_status:
|
||||
self._last_live_action_error = ""
|
||||
return True
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
state_name = "开播" if expected_status == 1 else "关播"
|
||||
self._last_live_action_error = (
|
||||
f"已点击直播姬,但 B站在 {int(effective_timeout)} 秒内未确认{state_name}"
|
||||
f"(live_status={status})"
|
||||
)
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
await asyncio.sleep(min(BILIBILI_LIVE_STATUS_POLL_INTERVAL_SECONDS, remaining))
|
||||
|
||||
async def _click_bilibili_live(
|
||||
self,
|
||||
*,
|
||||
@@ -7460,57 +7590,137 @@ class SystemScheduler:
|
||||
action_name: str,
|
||||
confirm_enter: bool = False,
|
||||
) -> bool:
|
||||
"""把直播姬窗口前置,在指定比例坐标点击;可选按 Enter 确认弹窗。"""
|
||||
"""确认直播姬获得前台后,在恢复后的窗口坐标内点击。"""
|
||||
self._last_live_action_error = ""
|
||||
if os.name != "nt":
|
||||
self.logger.warning(f"[系统定时] 当前系统不支持自动{action_name}")
|
||||
self._last_live_action_error = f"当前系统不支持自动{action_name}"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
keyword = self.cfg.get("bilibili_push_window_keyword", "直播姬")
|
||||
hwnd = self._find_window_by_keyword(keyword)
|
||||
if not hwnd:
|
||||
self.logger.warning(f"[系统定时] 未找到直播姬窗口,关键字: {keyword}")
|
||||
self._last_live_action_error = f"未找到直播姬窗口,关键字: {keyword}"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
user32 = ctypes.windll.user32
|
||||
rect = ctypes.wintypes.RECT()
|
||||
if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||||
self.logger.warning("[系统定时] 获取直播姬窗口位置失败")
|
||||
return False
|
||||
x_ratio = float(self.cfg.get(x_ratio_key, 0.741))
|
||||
y_ratio = float(self.cfg.get(y_ratio_key, 0.907))
|
||||
x = int(rect.left + max(0.0, min(1.0, x_ratio)) * (rect.right - rect.left))
|
||||
y = int(rect.top + max(0.0, min(1.0, y_ratio)) * (rect.bottom - rect.top))
|
||||
previous_dpi_context = None
|
||||
set_thread_dpi_context = getattr(user32, "SetThreadDpiAwarenessContext", None)
|
||||
try:
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
user32.SetForegroundWindow(hwnd)
|
||||
if set_thread_dpi_context:
|
||||
set_thread_dpi_context.restype = ctypes.c_void_p
|
||||
previous_dpi_context = set_thread_dpi_context(ctypes.c_void_p(-4))
|
||||
|
||||
if user32.IsIconic(hwnd):
|
||||
user32.ShowWindow(hwnd, 9) # SW_RESTORE
|
||||
else:
|
||||
user32.ShowWindow(hwnd, 5) # SW_SHOW
|
||||
await asyncio.sleep(0.5)
|
||||
user32.SetCursorPos(x, y)
|
||||
if not self._force_foreground_window(hwnd):
|
||||
await asyncio.sleep(0.3)
|
||||
if int(user32.GetForegroundWindow() or 0) != int(hwnd):
|
||||
self._last_live_action_error = "直播姬窗口无法获得前台,已取消点击以避免误操作其他窗口"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
|
||||
rect = ctypes.wintypes.RECT()
|
||||
if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||||
self._last_live_action_error = "获取直播姬窗口位置失败"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
width = int(rect.right - rect.left)
|
||||
height = int(rect.bottom - rect.top)
|
||||
if width < 200 or height < 150:
|
||||
self._last_live_action_error = (
|
||||
f"直播姬窗口尺寸异常: rect=({rect.left},{rect.top},{rect.right},{rect.bottom})"
|
||||
)
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
|
||||
x_ratio = float(self.cfg.get(x_ratio_key, 0.741))
|
||||
y_ratio = float(self.cfg.get(y_ratio_key, 0.907))
|
||||
x = int(rect.left + max(0.0, min(1.0, x_ratio)) * width)
|
||||
y = int(rect.top + max(0.0, min(1.0, y_ratio)) * height)
|
||||
virtual_left = int(user32.GetSystemMetrics(76))
|
||||
virtual_top = int(user32.GetSystemMetrics(77))
|
||||
virtual_right = virtual_left + int(user32.GetSystemMetrics(78))
|
||||
virtual_bottom = virtual_top + int(user32.GetSystemMetrics(79))
|
||||
if not (virtual_left <= x < virtual_right and virtual_top <= y < virtual_bottom):
|
||||
self._last_live_action_error = (
|
||||
f"直播姬点击坐标超出屏幕: x={x}, y={y}, "
|
||||
f"screen=({virtual_left},{virtual_top},{virtual_right},{virtual_bottom})"
|
||||
)
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
|
||||
previous_cursor = ctypes.wintypes.POINT()
|
||||
has_previous_cursor = bool(user32.GetCursorPos(ctypes.byref(previous_cursor)))
|
||||
if not user32.SetCursorPos(x, y):
|
||||
self._last_live_action_error = (
|
||||
f"无法移动鼠标到直播姬按钮位置: x={x}, y={y}。"
|
||||
"请通过桌面上可见的 run.bat 控制台启动服务后重试"
|
||||
)
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
await asyncio.sleep(0.1)
|
||||
if int(user32.GetForegroundWindow() or 0) != int(hwnd):
|
||||
self._last_live_action_error = "点击前直播姬失去前台,已取消操作"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
if has_previous_cursor:
|
||||
user32.SetCursorPos(previous_cursor.x, previous_cursor.y)
|
||||
return False
|
||||
user32.mouse_event(0x0002, 0, 0, 0, 0) # LEFTDOWN
|
||||
await asyncio.sleep(0.05)
|
||||
user32.mouse_event(0x0004, 0, 0, 0, 0) # LEFTUP
|
||||
if has_previous_cursor:
|
||||
user32.SetCursorPos(previous_cursor.x, previous_cursor.y)
|
||||
if confirm_enter:
|
||||
await asyncio.sleep(0.8)
|
||||
user32.keybd_event(0x0D, 0, 0, 0) # VK_RETURN key down
|
||||
user32.keybd_event(0x0D, 0, 0x0002, 0) # key up
|
||||
self.logger.info(f"[系统定时] 已点击直播姬{action_name}: hwnd={hwnd}, x={x}, y={y}")
|
||||
self.logger.info(
|
||||
f"[系统定时] 已点击直播姬{action_name}: hwnd={hwnd}, x={x}, y={y}, "
|
||||
f"rect=({rect.left},{rect.top},{rect.right},{rect.bottom})"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.warning(f"[系统定时] 点击直播姬{action_name}失败: {e}")
|
||||
self._last_live_action_error = f"点击直播姬{action_name}失败: {e}"
|
||||
self.logger.warning(f"[系统定时] {self._last_live_action_error}")
|
||||
return False
|
||||
finally:
|
||||
if set_thread_dpi_context and previous_dpi_context:
|
||||
try:
|
||||
set_thread_dpi_context(ctypes.c_void_p(previous_dpi_context))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def push_bilibili_live(self) -> bool:
|
||||
return await self._click_bilibili_live(
|
||||
self._last_live_action_error = ""
|
||||
if await self._get_bilibili_live_status() == 1:
|
||||
self.logger.info("[系统定时] B站已处于开播状态,跳过重复点击")
|
||||
return True
|
||||
clicked = await self._click_bilibili_live(
|
||||
x_ratio_key="bilibili_push_click_x_ratio",
|
||||
y_ratio_key="bilibili_push_click_y_ratio",
|
||||
action_name="开启推流按钮",
|
||||
)
|
||||
if not clicked:
|
||||
return False
|
||||
return await self._wait_for_bilibili_live_status(1)
|
||||
|
||||
async def stop_bilibili_live(self) -> bool:
|
||||
return await self._click_bilibili_live(
|
||||
self._last_live_action_error = ""
|
||||
if await self._get_bilibili_live_status() == 0:
|
||||
self.logger.info("[系统定时] B站已处于未开播状态,跳过重复关播点击")
|
||||
return True
|
||||
clicked = await self._click_bilibili_live(
|
||||
x_ratio_key="bilibili_stop_push_click_x_ratio",
|
||||
y_ratio_key="bilibili_stop_push_click_y_ratio",
|
||||
action_name="关闭推流按钮",
|
||||
confirm_enter=bool(self.cfg.get("bilibili_stop_push_confirm_enter", True)),
|
||||
)
|
||||
if not clicked:
|
||||
return False
|
||||
return await self._wait_for_bilibili_live_status(0)
|
||||
|
||||
def _schedule_reboot_after_stop(self, stopped_at: datetime) -> bool:
|
||||
if not self._is_enabled("reboot_after_stop_enabled"):
|
||||
@@ -7677,6 +7887,13 @@ class SystemScheduler:
|
||||
now = datetime.now().replace(second=0, microsecond=0)
|
||||
today = date.today().isoformat()
|
||||
try:
|
||||
monotonic_now = time.monotonic()
|
||||
if (
|
||||
monotonic_now - self._last_live_status_refresh_monotonic
|
||||
>= BILIBILI_LIVE_STATUS_REFRESH_SECONDS
|
||||
):
|
||||
self._last_live_status_refresh_monotonic = monotonic_now
|
||||
await self._get_bilibili_live_status(record_error=False)
|
||||
for event, when in self._live_occurrences(now):
|
||||
key = self._event_key(event, when)
|
||||
if key in self._triggered_events or not self._event_due(now, when):
|
||||
@@ -7744,6 +7961,11 @@ class QueueSystem:
|
||||
self.stats_store = stats_store
|
||||
self.health = ServiceRegistry(stats_store)
|
||||
self.live_client = None
|
||||
self._manual_live_override_until_monotonic = 0.0
|
||||
self._manual_live_override_until_wall = 0.0
|
||||
self._bilibili_live_status: int | None = None
|
||||
self._bilibili_live_status_updated_monotonic = 0.0
|
||||
self._bilibili_live_status_updated_wall = 0.0
|
||||
self.health.set("主程序", ServiceRegistry.STARTING, "初始化")
|
||||
self.health.set("配置", ServiceRegistry.RUNNING, f"revision={config.revision}")
|
||||
self.user_mgr = UserManager(
|
||||
@@ -7792,6 +8014,54 @@ class QueueSystem:
|
||||
self.handler.login_monitor = self.login_monitor
|
||||
self.health.set("主程序", ServiceRegistry.RUNNING, "初始化完成")
|
||||
|
||||
def enable_manual_live_override(
|
||||
self,
|
||||
duration_seconds: int = MANUAL_LIVE_TEST_DURATION_SECONDS,
|
||||
) -> float:
|
||||
duration = max(60, min(int(duration_seconds), 6 * 60 * 60))
|
||||
self._manual_live_override_until_monotonic = time.monotonic() + duration
|
||||
self._manual_live_override_until_wall = time.time() + duration
|
||||
self.logger.info(f"[直播测试] 已临时开放弹幕指令 {duration // 60} 分钟")
|
||||
return self._manual_live_override_until_wall
|
||||
|
||||
def clear_manual_live_override(self) -> None:
|
||||
was_active = self.manual_live_override_active()
|
||||
self._manual_live_override_until_monotonic = 0.0
|
||||
self._manual_live_override_until_wall = 0.0
|
||||
if was_active:
|
||||
self.logger.info("[直播测试] 已关闭手动直播指令时段")
|
||||
|
||||
def manual_live_override_active(self) -> bool:
|
||||
if self._manual_live_override_until_monotonic <= time.monotonic():
|
||||
self._manual_live_override_until_monotonic = 0.0
|
||||
self._manual_live_override_until_wall = 0.0
|
||||
return False
|
||||
return True
|
||||
|
||||
def manual_live_override_snapshot(self) -> dict[str, Any]:
|
||||
active = self.manual_live_override_active()
|
||||
return {
|
||||
"active": active,
|
||||
"until": self._manual_live_override_until_wall if active else 0.0,
|
||||
}
|
||||
|
||||
def set_bilibili_live_status(self, status: int) -> None:
|
||||
self._bilibili_live_status = int(status)
|
||||
self._bilibili_live_status_updated_monotonic = time.monotonic()
|
||||
self._bilibili_live_status_updated_wall = time.time()
|
||||
|
||||
def bilibili_live_active(self, max_age_seconds: float = 120.0) -> bool:
|
||||
status = getattr(self, "_bilibili_live_status", None)
|
||||
updated = float(getattr(self, "_bilibili_live_status_updated_monotonic", 0.0) or 0.0)
|
||||
return status == 1 and time.monotonic() - updated <= max(1.0, max_age_seconds)
|
||||
|
||||
def bilibili_live_status_snapshot(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": getattr(self, "_bilibili_live_status", None),
|
||||
"updated_at": float(getattr(self, "_bilibili_live_status_updated_wall", 0.0) or 0.0),
|
||||
"active": self.bilibili_live_active(),
|
||||
}
|
||||
|
||||
def apply_config(self):
|
||||
self.config.apply_runtime_settings()
|
||||
self.user_mgr.config = self.config
|
||||
@@ -9223,6 +9493,8 @@ class WebServer:
|
||||
"active_song_request": song_state.get("active"),
|
||||
"recent_danmu": list(getattr(self.system.handler, "recent_danmu", [])[-25:]),
|
||||
"recent_gifts": list(getattr(self.system.handler, "recent_gifts", [])[-25:]),
|
||||
"manual_live_test": self.system.manual_live_override_snapshot(),
|
||||
"bilibili_live": self.system.bilibili_live_status_snapshot(),
|
||||
})
|
||||
return state
|
||||
|
||||
@@ -10284,13 +10556,19 @@ class WebServer:
|
||||
elif action == "test_bilibili_push":
|
||||
ok = await self.scheduler.push_bilibili_live()
|
||||
if not ok:
|
||||
return web.json_response({"success": False, "error": "未找到直播姬窗口或点击失败"}, status=400)
|
||||
error = self.scheduler.last_live_action_error or "直播姬开播测试失败"
|
||||
return web.json_response({"success": False, "error": error}, status=400)
|
||||
manual_until = self.system.enable_manual_live_override()
|
||||
audit_target = "bilibili_push"
|
||||
audit_detail = f"live confirmed manual_until={manual_until:.0f}"
|
||||
elif action == "test_bilibili_stop_push":
|
||||
ok = await self.scheduler.stop_bilibili_live()
|
||||
if not ok:
|
||||
return web.json_response({"success": False, "error": "未找到直播姬窗口或关闭推流点击失败"}, status=400)
|
||||
error = self.scheduler.last_live_action_error or "直播姬关播测试失败"
|
||||
return web.json_response({"success": False, "error": error}, status=400)
|
||||
self.system.clear_manual_live_override()
|
||||
audit_target = "bilibili_stop_push"
|
||||
audit_detail = "offline confirmed"
|
||||
elif action == "save_system_schedule_config":
|
||||
data = await request.json()
|
||||
self.config.reload()
|
||||
|
||||
Reference in New Issue
Block a user