'init'
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled

This commit is contained in:
eric
2026-03-24 14:46:41 -05:00
parent c5a1a9a404
commit b77a8cadb2
164 changed files with 57060 additions and 58 deletions

255
web2_client_overview.py Normal file
View File

@@ -0,0 +1,255 @@
"""
工作台 /client/overviewYouTube 频道摘要、微信绑定摘要EasyTier 节点数由 Electron 主进程补充)。
多路转播语义:每条线路为「一个抖音直播间 ↔ 一个 YouTube 串流」一对一;
禁止同一抖音直播间同时向多个 YouTube 频道推流;禁止多条线路共用同一串流密钥(避免串台)。
"""
from __future__ import annotations
import configparser
import json
import re
from pathlib import Path
from typing import Any
from web2_wechat_chatbot import build_client, load_wechat_chatbot_config, load_wechat_state
def _mask_key(key: str) -> str:
k = (key or "").strip()
if not k:
return ""
if len(k) <= 8:
return "****"
return f"{k[:4]}{k[-4:]}"
def _short_douyin_hint(url: str) -> str:
u = (url or "").strip()
if not u:
return ""
if len(u) <= 56:
return u
return f"{u[:28]}{u[-16:]}"
def _normalize_douyin_url(url: str) -> str:
"""用于去重:去空白、小写 host、去尾部斜杠。"""
u = (url or "").strip()
if not u:
return ""
u = re.sub(r"#.*$", "", u).strip()
return u.rstrip("/").lower()
def _normalize_stream_key(key: str) -> str:
k = (key or "").strip()
if not k:
return ""
if k.startswith("rtmp://") or k.startswith("rtmps://"):
parts = k.rstrip("/").split("/")
k = parts[-1] if parts else k
return k.strip().lower()
def _parse_youtube_ini_key(ini_path: Path) -> str:
if not ini_path.exists():
return ""
cp = configparser.ConfigParser()
cp.read(ini_path, encoding="utf-8")
if not cp.has_section("youtube"):
return ""
for name, val in cp.items("youtube"):
if name in ("key", "stream_key"):
v = (val or "").strip()
if v and not v.startswith("#"):
return v
return ""
def _first_douyin_line_from_url_config(config_dir: Path) -> str:
"""单路 legacy从 URL_config.ini 取首条抖音链接作展示提示(仍可能多 URL 共单 key与多路一对一模型不同"""
p = config_dir / "URL_config.ini"
if not p.exists():
return ""
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
if "douyin" not in line.lower():
continue
if "," in line:
line = line.split(",")[-1].strip()
return line.strip()
return ""
def _parse_youtube_channels_json(path: Path) -> tuple[list[dict[str, str]], list[str]]:
"""
解析 youtube_channels.json展示层包含「待填写」线路仅对同时填写两项的条目做一对一冲突校验。
不完整条目不标为错误,由客户端自动维护文件,用户只需在录制页填写即可。
"""
errors: list[str] = []
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except Exception as e:
return [], [f"youtube_channels.json 解析失败: {e}"]
arr = raw.get("channels") if isinstance(raw, dict) else None
if not isinstance(arr, list):
return [], ["youtube_channels.json 缺少 channels 数组"]
parsed: list[tuple[str, str, str]] = []
for item in arr:
if not isinstance(item, dict):
continue
name = str(item.get("name") or item.get("id") or "").strip() or "未命名"
durl = str(
item.get("douyinUrl") or item.get("douyin_url") or item.get("douyin") or item.get("url") or ""
).strip()
key = str(item.get("youtubeKey") or item.get("key") or item.get("stream_key") or "").strip()
parsed.append((name, durl, key))
# 仅对「两项均已填写」的条目做冲突与规范化校验
strict: list[tuple[str, str, str]] = []
for name, durl, key in parsed:
if durl and key:
strict.append((name, durl, key))
seen_d: dict[str, str] = {}
seen_k: dict[str, str] = {}
ok_strict: set[str] = set()
for name, durl, key in strict:
nd = _normalize_douyin_url(durl)
nk = _normalize_stream_key(key)
if not nd or not nk:
errors.append(f"条目「{name}」地址或密钥无效")
continue
if nd in seen_d:
errors.append(
f"禁止重复使用同一抖音直播间绑定多条线路(冲突:「{seen_d[nd]}」与「{name}」)"
)
continue
if nk in seen_k:
errors.append(
f"禁止多条抖音线路共用同一 YouTube 串流密钥(冲突:「{seen_k[nk]}」与「{name}」)"
)
continue
seen_d[nd] = name
seen_k[nk] = name
ok_strict.add(name)
out: list[dict[str, str]] = []
for name, durl, key in parsed:
if not durl and not key:
out.append(
{
"name": name,
"douyinPreview": "(待填写)",
"keyPreview": "(待填写)",
}
)
continue
if not durl or not key:
out.append(
{
"name": name,
"douyinPreview": _short_douyin_hint(durl) if durl else "(待填写)",
"keyPreview": _mask_key(key) if key else "(待填写)",
}
)
continue
if name in ok_strict:
out.append(
{
"name": name,
"douyinPreview": _short_douyin_hint(durl),
"keyPreview": _mask_key(key),
}
)
else:
out.append(
{
"name": name,
"douyinPreview": _short_douyin_hint(durl) if durl else "(待填写)",
"keyPreview": _mask_key(key) if key else "(待填写)",
}
)
return out, errors
def _youtube_channels_list(config_dir: Path) -> tuple[list[dict[str, str]], list[str], list[str]]:
"""
返回:(频道行, 校验错误, 说明性备注)。
错误用于重复抖音/重复密钥等;备注用于单路 legacy 提示,不应标红为「错误」。
"""
path = config_dir / "youtube_channels.json"
if path.exists():
pairs, errs = _parse_youtube_channels_json(path)
if pairs:
return pairs, errs, []
if errs:
return [], errs, []
return (
[],
[],
[
"转播线路文件已就绪,请在客户端「录制」页 Pro 转播中填写抖音地址与 YouTube 密钥,将自动保存。",
],
)
key = _parse_youtube_ini_key(config_dir / "youtube.ini")
if not key:
return [], [], []
hint = _first_douyin_line_from_url_config(config_dir)
row: dict[str, str] = {
"name": "默认(单路)",
"keyPreview": _mask_key(key),
}
if hint:
row["douyinPreview"] = _short_douyin_hint(hint)
else:
row["douyinPreview"] = "(请在 URL_config.ini 填写抖音地址)"
notes = [
"当前为单路 legacyyoutube.ini + URL_config.ini多路一对一由客户端自动生成 youtube_channels.json。",
]
return [row], [], notes
def build_client_overview(config_dir: Path, base_dir: Path) -> dict[str, Any]:
channels, relay_errors, relay_notes = _youtube_channels_list(config_dir)
cfg = load_wechat_chatbot_config(base_dir)
st = load_wechat_state(base_dir)
wechat: dict[str, Any] = {
"configured": bool(cfg.get("proxyUrl") and cfg.get("apiKey")),
"nickName": st.get("nickName"),
"wcId": st.get("wcId"),
"online": False,
"proxyError": None,
}
if wechat["configured"]:
try:
c = build_client(cfg)
ps = c.get_status()
wechat["online"] = bool(ps.get("isLoggedIn")) or bool(ps.get("wcId"))
if ps.get("nickName"):
wechat["nickName"] = ps.get("nickName")
if ps.get("wcId"):
wechat["wcId"] = ps.get("wcId")
except Exception as e:
wechat["proxyError"] = str(e)[:300]
return {
"youtubeChannelCount": len(channels),
"youtubeChannels": channels,
"relayPairingPolicy": "one_douyin_one_youtube",
"relayValidationErrors": relay_errors,
"relayNotes": relay_notes,
"wechat": wechat,
"easyTierPeersNote": "peer 数量由 Electron 调用 easytier-cli peer list 获取",
}