Update Live-streaming code (auto-daily features)
This commit is contained in:
@@ -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)
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user