Update Live-streaming code (auto-daily features)

This commit is contained in:
2026-08-15 16:49:17 +08:00
commit 06ff69e945
350 changed files with 62343 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
import winreg
SUBKEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
NAME = "BetterGI弹幕排队系统"
VALUE = 'cmd.exe /d /k ""C:\\Users\\Administrator\\Desktop\\Live-streaming\\run.bat""'
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, SUBKEY, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(key, NAME, 0, winreg.REG_SZ, VALUE)
winreg.CloseKey(key)
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, SUBKEY, 0, winreg.KEY_READ)
value, _ = winreg.QueryValueEx(key, NAME)
winreg.CloseKey(key)
print("WINREG WRITE+READ OK:", value)
+212
View File
@@ -0,0 +1,212 @@
[CmdletBinding()]
param(
[switch]$SkipInstall,
[switch]$FullTts,
[switch]$Lite,
[switch]$NoAdmin
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$AdminRoot = Join-Path $Root "frontend\admin"
$DistRoot = Join-Path $Root "dist"
$AppDist = Join-Path $DistRoot "LiveStreaming"
$BuildRoot = Join-Path $Root "build"
$PyBuild = Join-Path $BuildRoot "pyinstaller"
function Write-Step([string]$Message) {
Write-Host ""
Write-Host "==> $Message" -ForegroundColor Cyan
}
function Invoke-Checked([string]$File, [string[]]$Arguments) {
& $File @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE."
}
}
function Get-ProjectPython {
$candidates = @(
$env:LIVE_STREAMING_PYTHON,
(Join-Path $Root ".venv-tts\Scripts\python.exe"),
(Join-Path $Root ".venv\Scripts\python.exe"),
"E:\Programs\Anaconda3\envs\Live-streaming\python.exe",
"python"
) | Where-Object { $_ }
foreach ($candidate in $candidates) {
try {
$version = & $candidate -c "import sys; print(sys.version.split()[0])" 2>$null
if ($LASTEXITCODE -eq 0 -and $version) {
Write-Host "Python: $candidate ($version)"
return $candidate
}
} catch {
}
}
throw "No usable Python found. Create .venv, install Python 3.10+, or set LIVE_STREAMING_PYTHON."
}
$Python = Get-ProjectPython
$PythonPrefix = (& $Python -c "import sys; print(sys.prefix)").Trim()
if ($LASTEXITCODE -ne 0 -or -not $PythonPrefix) {
throw "Unable to resolve Python prefix from $Python."
}
$CondaBin = Join-Path $PythonPrefix "Library\bin"
if (Test-Path $CondaBin) {
$env:PATH = "$CondaBin;$env:PATH"
}
if (-not $SkipInstall) {
Write-Step "Install Python runtime/build dependencies"
Invoke-Checked $Python @("-m", "pip", "install", "-r", (Join-Path $Root "requirements.txt"))
}
Write-Step "Build Vue admin dist"
if (-not (Test-Path (Join-Path $AdminRoot "node_modules"))) {
Invoke-Checked "npm" @("--prefix", $AdminRoot, "install")
}
Invoke-Checked "npm" @("--prefix", $AdminRoot, "run", "build")
Write-Step "Prepare PyInstaller output"
# 确保没有残留进程占用 dist 目录
$killed = Get-Process -Name "LiveStreaming" -ErrorAction SilentlyContinue
if ($killed) {
$killed | Stop-Process -Force -ErrorAction SilentlyContinue
$killed | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Write-Host " Stopped LiveStreaming.exe before build"
}
function Remove-WithRetry([string]$Path) {
if (-not (Test-Path $Path)) { return }
$retries = 3
while ($retries -gt 0) {
try {
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
return
} catch {
$retries--
if ($retries -eq 0) {
Write-Host " WARNING: Cannot remove $Path, skipping" -ForegroundColor Yellow
} else {
Write-Host " Retry remove $Path ($retries left)..." -ForegroundColor Yellow
Start-Sleep -Seconds 2
}
}
}
}
Remove-WithRetry $AppDist
Remove-WithRetry $PyBuild
New-Item -ItemType Directory -Force -Path $DistRoot | Out-Null
New-Item -ItemType Directory -Force -Path $PyBuild | Out-Null
$excludeModules = @(
"tkinter",
"pytest",
"IPython",
"jupyter",
"notebook",
"matplotlib.tests",
"numpy.tests",
"pandas.tests",
"scipy.tests",
"torch.testing"
)
if ($Lite) {
$excludeModules += @(
"torch",
"torchaudio",
"torchvision",
"transformers",
"faster_qwen3_tts",
"dots_tts"
)
} else {
$excludeModules += @(
"torch.distributed",
"torch.utils.tensorboard",
"torchvision",
"torchaudio._internal",
"torch.utils.benchmark",
"torch.testing",
"torch.onnx",
"tensorboard",
"tensorboardX"
)
}
$pyiArgs = @(
"--noconfirm",
"--onedir",
"--name", "LiveStreaming",
"--distpath", $DistRoot,
"--workpath", $PyBuild,
"--specpath", $BuildRoot,
"--paths", (Join-Path $Root "app"),
"--hidden-import", "danmu_queue",
"--hidden-import", "music_monitor",
"--hidden-import", "tts_monitor",
"--hidden-import", "core.runtime_paths"
)
foreach ($module in $excludeModules) {
$pyiArgs += @("--exclude-module", $module)
}
$condaDlls = @("ffi.dll", "liblzma.dll", "libbz2.dll", "libexpat.dll")
if (Test-Path $CondaBin) {
foreach ($dll in $condaDlls) {
$dllPath = Join-Path $CondaBin $dll
if (Test-Path $dllPath) {
$pyiArgs += @("--add-binary", "$dllPath;.")
}
}
}
if (-not $NoAdmin) {
$pyiArgs += "--uac-admin"
}
$pyiArgs += (Join-Path $Root "app\main.py")
Write-Step "Package LiveStreaming.exe"
# PyInstaller 隔离模式与 torch DLL 冲突,设置环境变量跳过 CUDA 加载
$env:CUDA_VISIBLE_DEVICES = ""
$env:PYTORCH_NVFUSER_DISABLE = "1"
$SpecFile = Join-Path $BuildRoot "LiveStreaming.spec"
if (Test-Path $SpecFile) {
Write-Host " Reusing spec (incremental) ..."
Invoke-Checked $Python @("-m", "PyInstaller", "--noconfirm", $SpecFile)
} else {
Invoke-Checked $Python (@("-m", "PyInstaller") + $pyiArgs)
}
Write-Step "Create runtime directories"
$ExePath = Join-Path $AppDist "LiveStreaming.exe"
if (-not (Test-Path $ExePath)) {
throw "PyInstaller finished without creating $ExePath."
}
New-Item -ItemType Directory -Force -Path (Join-Path $AppDist "data") | Out-Null
New-Item -ItemType Directory -Force -Path (Join-Path $AppDist "logs") | Out-Null
Copy-Item -LiteralPath (Join-Path $Root "web") -Destination (Join-Path $AppDist "web") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "config") -Destination (Join-Path $AppDist "config") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "integrations") -Destination (Join-Path $AppDist "integrations") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "docs") -Destination (Join-Path $AppDist "docs") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "vendor") -Destination (Join-Path $AppDist "vendor") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "README.md") -Destination (Join-Path $AppDist "README.md") -Force
Write-Host ""
Write-Host "Build complete: $AppDist\LiveStreaming.exe" -ForegroundColor Green
if ($Lite) {
Write-Host "Lite build: TTS/GPU dependencies excluded."
} else {
Write-Host "Full build with TTS support."
}
+275
View File
@@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
"""
补充处理之前跳过的项目(有子目录的多作者/多版本食材与特产)。
规则:
- 子目录名含"效率版/高效率/中效率/效率路线/效率" → 用该效率版子目录的 json 创建配置组
- 其余 → 创建空配置组(projects=[]),用户手动添加路线 json
用法:
python fill_skipped_groups.py
"""
import os
import json
import sys
BASE = r"C:\Program Files\BetterGI\BetterGI\User"
AUTOPATHING = os.path.join(BASE, "AutoPathing")
SCRIPTGROUP = os.path.join(BASE, "ScriptGroup")
IGNORE_NAMES = {"desktop.ini", "icon.ico", "Thumbs.db"}
REGION_ORDER = ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "挪德卡莱"]
CATEGORIES = ["地方特产", "食材与炼金"]
def region_sort_key(name):
try:
return (REGION_ORDER.index(name), name)
except ValueError:
return (len(REGION_ORDER), name)
def load_config_template():
template_path = os.path.join(SCRIPTGROUP, "子探测单元.json")
with open(template_path, "r", encoding="utf-8") as f:
return json.load(f)["config"]
def get_next_group_index():
max_index = 0
for fn in os.listdir(SCRIPTGROUP):
if not fn.lower().endswith(".json"):
continue
fp = os.path.join(SCRIPTGROUP, fn)
try:
with open(fp, "r", encoding="utf-8") as f:
data = json.load(f)
idx = data.get("index", 0)
if isinstance(idx, int) and idx > max_index:
max_index = idx
except Exception:
continue
return max_index + 1
def detect_structure(category_path):
has_any_direct_json = False
for entry in os.listdir(category_path):
entry_path = os.path.join(category_path, entry)
if not os.path.isdir(entry_path):
continue
sub_entries = [e for e in os.listdir(entry_path) if e not in IGNORE_NAMES]
has_json = any(
fn.lower().endswith(".json") and os.path.isfile(os.path.join(entry_path, fn))
for fn in sub_entries
)
if has_json:
has_any_direct_json = True
break
return "two" if has_any_direct_json else "three"
def list_items(category_path, structure):
result = []
if structure == "two":
for item in os.listdir(category_path):
ip = os.path.join(category_path, item)
if not os.path.isdir(ip):
continue
result.append((item, item, ip))
result.sort(key=lambda x: x[0])
else:
for region in os.listdir(category_path):
rp = os.path.join(category_path, region)
if not os.path.isdir(rp):
continue
for item in os.listdir(rp):
ip = os.path.join(rp, item)
if not os.path.isdir(ip):
continue
result.append(((region, item), item, ip))
result.sort(key=lambda x: (region_sort_key(x[0][0]), x[0][1]))
return result
def analyze_item(item_path):
entries = os.listdir(item_path)
subdirs = [e for e in entries
if os.path.isdir(os.path.join(item_path, e)) and e not in IGNORE_NAMES]
json_files = [e for e in entries
if e.lower().endswith(".json")
and os.path.isfile(os.path.join(item_path, e))
and e not in IGNORE_NAMES]
return subdirs, sorted(json_files)
def list_jsons_in_dir(dir_path):
"""列出某目录下直接包含的 json 文件(排除系统文件)。"""
if not os.path.isdir(dir_path):
return []
result = []
for e in os.listdir(dir_path):
if e in IGNORE_NAMES:
continue
if e.lower().endswith(".json") and os.path.isfile(os.path.join(dir_path, e)):
result.append(e)
return sorted(result)
def pick_efficiency_subdir(subdirs):
"""按优先级选取效率版子目录,返回子目录名或 None。"""
priorities = ["效率版", "高效率", "中效率", "效率路线", "效率"]
for keyword in priorities:
for sd in subdirs:
if keyword in sd:
return sd
return None
def build_group(group_index, name, folder_name, json_files, config_template):
projects = []
for i, jf in enumerate(json_files, 1):
projects.append({
"name": jf,
"folderName": folder_name,
"jsScriptSettingsObject": None,
"index": i,
"type": "Pathing",
"status": "Enabled",
"schedule": "Daily",
"runNum": 1,
"allowJsNotification": True,
"allowJsHTTPHash": ""
})
return {
"index": group_index,
"name": name,
"config": config_template,
"projects": projects
}
def main():
config_template = load_config_template()
report_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"补充报告_跳过项目.txt"
)
eff_created = [] # (category, group_key, name, eff_subdir, route_count, file_path)
empty_created = [] # (category, group_key, name, subdirs, file_path)
write_errors = []
group_index = get_next_group_index()
print(f"[信息] 起始 group index: {group_index}")
for category in CATEGORIES:
category_path = os.path.join(AUTOPATHING, category)
if not os.path.isdir(category_path):
continue
structure = detect_structure(category_path)
items = list_items(category_path, structure)
for group_key, name, item_path in items:
subdirs, json_files = analyze_item(item_path)
if not subdirs:
continue # 无子目录,之前已创建,跳过
# 构造 folderName
if structure == "two":
folder_name = f"{category}\\{name}"
else:
region, _ = group_key
folder_name = f"{category}\\{region}\\{name}"
# 检查是否已存在同名配置组(避免覆盖之前创建的)
out_path = os.path.join(SCRIPTGROUP, f"{name}.json")
if os.path.exists(out_path):
# 已存在,跳过
continue
eff_subdir = pick_efficiency_subdir(subdirs)
if eff_subdir:
# 用效率版子目录的 json
eff_path = os.path.join(item_path, eff_subdir)
# 效率版子目录可能直接含 json,也可能再含子目录(一般直接含)
eff_jsons = list_jsons_in_dir(eff_path)
if not eff_jsons:
# 效率版子目录下无直接 json,创建空配置组
group = build_group(group_index, name, folder_name, [], config_template)
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(group, f, ensure_ascii=False, indent=2)
empty_created.append((category, group_key, name, subdirs, out_path))
group_index += 1
except Exception as e:
write_errors.append((name, str(e)))
continue
group = build_group(group_index, name, folder_name, eff_jsons, config_template)
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(group, f, ensure_ascii=False, indent=2)
eff_created.append((category, group_key, name, eff_subdir, len(eff_jsons), out_path))
group_index += 1
except Exception as e:
write_errors.append((name, str(e)))
else:
# 无效率版 → 创建空配置组
group = build_group(group_index, name, folder_name, [], config_template)
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(group, f, ensure_ascii=False, indent=2)
empty_created.append((category, group_key, name, subdirs, out_path))
group_index += 1
except Exception as e:
write_errors.append((name, str(e)))
# 生成报告
lines = []
lines.append("=" * 70)
lines.append("BetterGI 跳过项目补充处理报告")
lines.append("=" * 70)
lines.append("")
lines.append(f"【使用效率版创建】共 {len(eff_created)}")
lines.append("-" * 70)
cur_cat = None
for category, gk, name, eff_sd, cnt, fp in eff_created:
if category != cur_cat:
cur_cat = category
lines.append(f"\n[{category}]")
lines.append(f" {name}")
lines.append(f" 效率版: {eff_sd} ({cnt} 条路线)")
lines.append("")
lines.append(f"【创建空配置组(需手动添加路线)】共 {len(empty_created)}")
lines.append("-" * 70)
cur_cat = None
for category, gk, name, subdirs, fp in empty_created:
if category != cur_cat:
cur_cat = category
lines.append(f"\n[{category}]")
lines.append(f" {name}")
lines.append(f" 候选子目录: {subdirs}")
lines.append("")
if write_errors:
lines.append("【写入错误】")
lines.append("-" * 70)
for name, err in write_errors:
lines.append(f" {name}: {err}")
lines.append("")
lines.append("=" * 70)
report = "\n".join(lines)
with open(report_path, "w", encoding="utf-8") as f:
f.write(report)
print(report)
print(f"\n报告已保存: {report_path}")
if __name__ == "__main__":
main()
+313
View File
@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
"""
为 BetterGI AutoPathing 分类目录批量生成 ScriptGroup 配置组。
支持两类结构:
- 两层(分类/食材):如"食材与炼金/<食材>/xxx.json" folderName = "<分类>\\<食材>"
- 三层(分类/地区/特产):如"地方特产/<地区>/<特产>/xxx.json" folderName = "<分类>\\<地区>\\<特产>"
规则:
- 食材/特产目录下若无子目录(json 直接平铺)→ 单一来源路线 → 生成配置组
- 食材/特产目录下若有子目录(多作者/多版本路线)→ 跳过,记录到未加入清单
用法:
python generate_scriptgroups.py <分类名>
例: python generate_scriptgroups.py 食材与炼金
python generate_scriptgroups.py 地方特产
输出:
- 在 ScriptGroup 目录下为每个可创建的项目生成 <名称>.json
- 在项目目录下生成 生成报告_<分类名>.txt
"""
import os
import json
import sys
BASE = r"C:\Program Files\BetterGI\BetterGI\User"
AUTOPATHING = os.path.join(BASE, "AutoPathing")
SCRIPTGROUP = os.path.join(BASE, "ScriptGroup")
# 非路线文件,扫描时排除
IGNORE_NAMES = {"desktop.ini", "icon.ico", "Thumbs.db"}
# 地区显示顺序(仅用于排序,地方特产用)
REGION_ORDER = ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "挪德卡莱"]
def region_sort_key(name):
try:
return (REGION_ORDER.index(name), name)
except ValueError:
return (len(REGION_ORDER), name)
def load_config_template():
"""读取 子探测单元.json 作为 config 模板。"""
template_path = os.path.join(SCRIPTGROUP, "子探测单元.json")
if not os.path.exists(template_path):
print(f"[错误] 找不到模板文件: {template_path}")
sys.exit(1)
with open(template_path, "r", encoding="utf-8") as f:
return json.load(f)["config"]
def get_next_group_index():
"""扫描 ScriptGroup 下所有 json,返回下一个可用 index。"""
max_index = 0
if not os.path.isdir(SCRIPTGROUP):
return 1
for fn in os.listdir(SCRIPTGROUP):
if not fn.lower().endswith(".json"):
continue
fp = os.path.join(SCRIPTGROUP, fn)
try:
with open(fp, "r", encoding="utf-8") as f:
data = json.load(f)
idx = data.get("index", 0)
if isinstance(idx, int) and idx > max_index:
max_index = idx
except Exception:
continue
return max_index + 1
def detect_structure(category_path):
"""检测目录结构:返回 'two' (分类/食材) 或 'three' (分类/地区/特产)。
判定规则:扫描所有第二层目录,只要有一个直接包含 json 文件,
就判为两层结构(单作者食材直接放 json)。只有当所有第二层目录
都只含子目录(无直接 json)时,才判为三层(地区→特产)。
"""
has_any_direct_json = False
for entry in os.listdir(category_path):
entry_path = os.path.join(category_path, entry)
if not os.path.isdir(entry_path):
continue
sub_entries = [e for e in os.listdir(entry_path) if e not in IGNORE_NAMES]
has_json = any(
fn.lower().endswith(".json") and os.path.isfile(os.path.join(entry_path, fn))
for fn in sub_entries
)
if has_json:
has_any_direct_json = True
break
return "two" if has_any_direct_json else "three"
def list_items(category_path, structure):
"""返回 [(group_key, item_name, item_path)] 列表。
- two: group_key=item_name, item_path=分类/食材
- three: group_key=(region, item_name), item_path=分类/地区/特产
"""
result = []
if structure == "two":
for item in os.listdir(category_path):
ip = os.path.join(category_path, item)
if not os.path.isdir(ip):
continue
result.append((item, item, ip))
result.sort(key=lambda x: x[0])
else:
for region in os.listdir(category_path):
rp = os.path.join(category_path, region)
if not os.path.isdir(rp):
continue
for item in os.listdir(rp):
ip = os.path.join(rp, item)
if not os.path.isdir(ip):
continue
result.append(((region, item), item, ip))
result.sort(key=lambda x: (region_sort_key(x[0][0]), x[0][1]))
return result
def analyze_item(item_path):
"""分析项目目录,返回 (subdirs, json_files)。"""
entries = os.listdir(item_path)
subdirs = [e for e in entries
if os.path.isdir(os.path.join(item_path, e)) and e not in IGNORE_NAMES]
json_files = [e for e in entries
if e.lower().endswith(".json")
and os.path.isfile(os.path.join(item_path, e))
and e not in IGNORE_NAMES]
return subdirs, sorted(json_files)
def classify_subdirs(subdirs):
"""对有子目录的项目分类,返回 (类型, 说明)。"""
authors = []
for sd in subdirs:
if "@" in sd:
author = sd.split("@", 1)[1]
authors.append(author)
else:
authors.append(None)
unique_authors = {a for a in authors if a is not None}
has_unmarked = any(a is None for a in authors)
if len(unique_authors) == 0:
return "版本说明类", "所有子目录均无作者标识(版本/路线说明)"
if has_unmarked:
return "混合多路线", f"含无作者标识的版本目录;作者: {sorted(unique_authors)}"
if len(unique_authors) == 1:
return "同一作者多版本", f"作者: {next(iter(unique_authors))}"
return "多作者", f"作者: {sorted(unique_authors)}"
def build_group(group_index, name, folder_name, json_files, config_template):
"""构造一个配置组 dict。"""
projects = []
for i, jf in enumerate(json_files, 1):
projects.append({
"name": jf,
"folderName": folder_name,
"jsScriptSettingsObject": None,
"index": i,
"type": "Pathing",
"status": "Enabled",
"schedule": "Daily",
"runNum": 1,
"allowJsNotification": True,
"allowJsHTTPHash": ""
})
return {
"index": group_index,
"name": name,
"config": config_template,
"projects": projects
}
def main():
if len(sys.argv) < 2:
print("用法: python generate_scriptgroups.py <分类名>")
print("例: python generate_scriptgroups.py 食材与炼金")
sys.exit(1)
category = sys.argv[1]
category_path = os.path.join(AUTOPATHING, category)
if not os.path.isdir(category_path):
print(f"[错误] 分类目录不存在: {category_path}")
sys.exit(1)
config_template = load_config_template()
report_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
f"生成报告_{category}.txt"
)
structure = detect_structure(category_path)
print(f"[信息] 分类 '{category}' 结构: {'两层(分类/食材)' if structure == 'two' else '三层(分类/地区/特产)'}")
items = list_items(category_path, structure)
print(f"[信息] 共扫描到 {len(items)} 个项目")
created = [] # (group_key, name, route_count, file_path)
skipped = [] # (group_key, name, subdirs, 类型, 说明)
write_errors = [] # (name, error)
group_index = get_next_group_index()
print(f"[信息] 起始 group index: {group_index}")
for group_key, name, item_path in items:
subdirs, json_files = analyze_item(item_path)
if not subdirs:
# 无子目录 → 创建配置组
if not json_files:
skipped.append((group_key, name, [], "空目录", "目录下无路线文件"))
continue
# 构造 folderName
if structure == "two":
folder_name = f"{category}\\{name}"
else:
region, _ = group_key
folder_name = f"{category}\\{region}\\{name}"
group = build_group(group_index, name, folder_name, json_files, config_template)
out_path = os.path.join(SCRIPTGROUP, f"{name}.json")
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(group, f, ensure_ascii=False, indent=2)
created.append((group_key, name, len(json_files), out_path))
group_index += 1
except PermissionError as e:
write_errors.append((name, f"权限不足: {e}"))
except Exception as e:
write_errors.append((name, str(e)))
else:
# 有子目录 → 跳过
kind, desc = classify_subdirs(subdirs)
skipped.append((group_key, name, subdirs, kind, desc))
# 生成报告
lines = []
lines.append("=" * 70)
lines.append(f"BetterGI {category} 配置组生成报告")
lines.append("=" * 70)
lines.append("")
lines.append(f"【已创建配置组】共 {len(created)}")
lines.append("-" * 70)
def group_label(gk):
if structure == "two":
return gk
else:
return f"{gk[0]} / {gk[1]}"
# 按分组打印已创建
if structure == "two":
for gk, name, cnt, fp in created:
lines.append(f" {name} ({cnt} 条路线) -> {os.path.basename(fp)}")
else:
cur_region = None
for gk, name, cnt, fp in created:
region = gk[0]
if region != cur_region:
cur_region = region
lines.append(f"\n[{region}]")
lines.append(f" {name} ({cnt} 条路线) -> {os.path.basename(fp)}")
lines.append("")
lines.append(f"【未加入配置组(需手动添加)】共 {len(skipped)}")
lines.append("-" * 70)
if structure == "two":
for gk, name, subdirs, kind, desc in skipped:
lines.append(f" {name}")
lines.append(f" 类型: {kind}")
lines.append(f" 说明: {desc}")
if subdirs:
lines.append(f" 子目录: {subdirs}")
lines.append("")
else:
cur_region = None
for gk, name, subdirs, kind, desc in skipped:
region = gk[0]
if region != cur_region:
cur_region = region
lines.append(f"\n[{region}]")
lines.append(f" {name}")
lines.append(f" 类型: {kind}")
lines.append(f" 说明: {desc}")
if subdirs:
lines.append(f" 子目录: {subdirs}")
lines.append("")
if write_errors:
lines.append("【写入错误】")
lines.append("-" * 70)
for name, err in write_errors:
lines.append(f" {name}: {err}")
lines.append("")
lines.append("=" * 70)
report = "\n".join(lines)
with open(report_path, "w", encoding="utf-8") as f:
f.write(report)
print(report)
print(f"\n报告已保存: {report_path}")
if __name__ == "__main__":
main()
+127
View File
@@ -0,0 +1,127 @@
"""Preview the built admin UI against a fake local backend.
This script does not connect to Bilibili or BetterGI. It is only for checking
the admin page in a local browser while developing the Vue UI.
"""
from __future__ import annotations
import asyncio
import logging
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "app"))
from danmu_queue import Config, ServiceRegistry, WebServer # noqa: E402
class FakeRunner:
def is_bgi_running(self):
return False
async def kill_bgi(self):
return None
class FakeQueueManager:
def __init__(self):
self.state = {
"queue": [10001, 10002],
"current_admin_uid": 10001,
"current_group": "薄荷",
"group_start_time": None,
"admin_window_end": None,
"default_running": False,
"login_status": "confirming",
}
def _save(self):
return None
def leave_queue(self, uid):
if uid in self.state["queue"]:
self.state["queue"].remove(uid)
return {"success": True, "was_running": False}
class FakeUserManager:
def __init__(self):
self.users = {
"10001": {"uname": "测试用户A", "points": 18, "last_signin_date": ""},
"10002": {"uname": "测试用户B", "points": 7, "last_signin_date": ""},
}
async def _save(self):
return None
async def add_points(self, uid, points):
user = self.users.setdefault(str(uid), {"uname": f"用户{uid}", "points": 0})
user["points"] += int(points)
return user["points"]
class FakeSongRequestManager:
def __init__(self):
self.state = {
"queue": [
{"id": "1", "name": "测试歌曲", "artist": "测试歌手", "uname": "测试用户A"}
]
}
def remove_request(self, index=None, song_id=""):
queue = self.state.setdefault("queue", [])
if queue:
return {"success": True, "removed": queue.pop(0)}
return {"success": False, "msg": "empty"}
def clear_requests(self):
count = len(self.state.get("queue", []))
self.state["queue"] = []
return count
class FakeLogMonitor:
def set_current_group(self, group):
return None
class FakeSystem:
def __init__(self):
self.runner = FakeRunner()
self.queue_mgr = FakeQueueManager()
self.user_mgr = FakeUserManager()
self.song_request_mgr = FakeSongRequestManager()
self.log_monitor = FakeLogMonitor()
self.handler = type("FakeHandler", (), {"recent_danmu": [
{"uname": "测试用户A", "text": "排队"},
{"uname": "测试用户B", "text": "点歌 测试歌曲"},
]})()
self.broadcaster = None
self.health = ServiceRegistry()
self.health.set("主程序", ServiceRegistry.RUNNING, "preview")
self.health.set("Web后台服务", ServiceRegistry.RUNNING, "preview")
self.health.set("直播监听", ServiceRegistry.RECONNECTING, "preview reconnect")
def apply_config(self):
self.health.set("配置", ServiceRegistry.RUNNING, "preview save")
async def _start_default_group(self):
return None
async def main():
cfg = Config(str(ROOT / "config" / "config_queue.json"))
logger = logging.getLogger("preview_admin")
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))
server = WebServer(FakeSystem(), cfg, logger, port=5190, host="127.0.0.1")
server_task = asyncio.create_task(server.start())
await asyncio.sleep(0.2)
print("Preview: http://127.0.0.1:5190/admin")
await server_task
if __name__ == "__main__":
asyncio.run(main())