's'
This commit is contained in:
362
web2.py
362
web2.py
@@ -31,14 +31,22 @@ from web2_youtube_oauth import (
|
||||
from web2_client_overview import build_client_overview
|
||||
from web2_process_runtime import process_family, summarize_process_activity
|
||||
from web2_douyin_source_display import extract_douyin_source_meta
|
||||
from web2_douyin_preview import fetch_douyin_anchor_name
|
||||
from web2_config_governance import (
|
||||
list_config_backups,
|
||||
restore_config_backup,
|
||||
write_config_text,
|
||||
)
|
||||
from web2_config_insights import analyze_config_content, optimize_config_content, read_config_file
|
||||
from web2_youtube_ingest_auth import ingest_config_payload, verify_ingest_payload
|
||||
from web2_youtube_pro_runtime import sync_all_youtube_pro_runtime_files, validate_youtube_runtime_files
|
||||
from web2_relay_pro import channel_id_for_pm2_name, load_channels_document, save_channels_document
|
||||
from web2_quality_guard import build_youtube_quality_guard_report
|
||||
from web2_youtube_diagnostics import build_youtube_diagnostics_report
|
||||
from web2_relay_log import (
|
||||
close_active_relay_sessions,
|
||||
get_relay_cover_path,
|
||||
get_relay_session,
|
||||
import_relay_sessions_from_log,
|
||||
list_relay_sessions,
|
||||
relay_db_path,
|
||||
@@ -57,6 +65,7 @@ from web2_relay_pro import (
|
||||
get_single_youtube_watch_url,
|
||||
get_youtube_watch_url,
|
||||
list_channels_for_pro,
|
||||
relay_pm2_name_for_channel,
|
||||
materialize_relay_channel_youtube_ini,
|
||||
read_url_config_for_channel,
|
||||
relay_pm2_name_for_channel,
|
||||
@@ -68,7 +77,19 @@ from web2_relay_pro import (
|
||||
watch_url_for_relay_process,
|
||||
write_url_config_for_channel,
|
||||
)
|
||||
from web2_youtube_preflight import youtube_start_preflight_message
|
||||
from web2_youtube_preflight import run_youtube_preflight, youtube_start_preflight_message
|
||||
from web2_youtube_relay_heartbeat import read_relay_heartbeat
|
||||
from web2_youtube_live_store import (
|
||||
close_live_event,
|
||||
extract_youtube_key_suffix,
|
||||
get_live_event,
|
||||
ingest_browser_telemetry,
|
||||
list_live_events,
|
||||
live_db_path,
|
||||
record_control_action,
|
||||
record_process_observation,
|
||||
youtube_live_summary,
|
||||
)
|
||||
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini, validate_youtube_keys
|
||||
from web2_host_caps import gpu_status_payload, machine_summary_payload
|
||||
from web2_camera import camera_config_ready, list_capture_devices, load_camera_config, save_camera_config
|
||||
@@ -99,7 +120,10 @@ BASE_DIR = Path(__file__).parent
|
||||
CONFIG_DIR = BASE_DIR / "config"
|
||||
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs"
|
||||
RELAY_DB = relay_db_path(BASE_DIR)
|
||||
LIVE_DB = live_db_path(BASE_DIR)
|
||||
_PROCESS_STATUS_CACHE: dict[str, str] = {}
|
||||
_LIVE_OBSERVE_LAST: dict[str, float] = {}
|
||||
YOUTUBE_EVENT_STATUS_OBSERVE_SECONDS = 5.0
|
||||
_SYSTEM_METRICS_TTL_SECONDS = 10.0
|
||||
_SYSTEM_METRICS_CACHE: tuple[float, dict] | None = None
|
||||
_SYSTEM_METRICS_CACHE_LOCK = asyncio.Lock()
|
||||
@@ -107,6 +131,50 @@ _SYSTEM_METRICS_CACHE_LOCK = asyncio.Lock()
|
||||
MAX_LOG_LINES = 300
|
||||
|
||||
|
||||
def _relay_control_meta(process: str) -> dict[str, str]:
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
channel_name = ""
|
||||
if cid:
|
||||
d = get_channel_detail(CONFIG_DIR, cid)
|
||||
channel_name = str((d or {}).get("name") or "")
|
||||
cfg = _config_snapshot_for_process(process)
|
||||
return {
|
||||
"channel_id": cid or "",
|
||||
"channel_name": channel_name,
|
||||
"url_lines": cfg.get("url_lines") or _url_lines_for_process(process),
|
||||
"youtube_ini_text": cfg.get("youtube_ini_text") or "",
|
||||
"script": script_for_pm2(process) or "",
|
||||
}
|
||||
|
||||
|
||||
async def _record_youtube_control(process: str, action: str, *, note: str = "") -> None:
|
||||
if not is_youtube_script(script_for_pm2(process) or ""):
|
||||
return
|
||||
meta = _relay_control_meta(process)
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
record_control_action,
|
||||
process,
|
||||
action,
|
||||
script=meta["script"],
|
||||
url_lines=meta["url_lines"],
|
||||
youtube_ini_text=meta["youtube_ini_text"],
|
||||
channel_id=meta["channel_id"],
|
||||
channel_name=meta["channel_name"],
|
||||
note=note,
|
||||
db_path=LIVE_DB,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def _close_youtube_relay_sessions(process: str, *, reason: str) -> None:
|
||||
try:
|
||||
await asyncio.to_thread(close_active_relay_sessions, process, reason=reason, db_path=RELAY_DB)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _label_short(prefix: str) -> str:
|
||||
return {
|
||||
"tiktok": "TikTok",
|
||||
@@ -198,10 +266,16 @@ VALID_PROCESSES = frozenset(p["pm2"] for p in PROCESS_MONITOR)
|
||||
|
||||
|
||||
def relay_channel_id_from_pm2_name(name: str) -> Optional[str]:
|
||||
cid = channel_id_for_pm2_name(CONFIG_DIR, name)
|
||||
if cid:
|
||||
return cid
|
||||
prefix = "youtube2__"
|
||||
if not name.startswith(prefix):
|
||||
return None
|
||||
return name[len(prefix) :]
|
||||
legacy = name[len(prefix) :]
|
||||
if get_channel_by_id(CONFIG_DIR, legacy):
|
||||
return legacy
|
||||
return None
|
||||
|
||||
|
||||
def process_valid(name: str) -> bool:
|
||||
@@ -446,7 +520,10 @@ async def relay_pro_save_channel_keys(request: Request):
|
||||
|
||||
@app.post("/relay_pro/sync_active")
|
||||
async def relay_pro_sync_active(channel_id: str = Query(...)):
|
||||
ok, msg = sync_channel_to_legacy_files(CONFIG_DIR, channel_id)
|
||||
try:
|
||||
ok, msg = await asyncio.to_thread(sync_channel_to_legacy_files, CONFIG_DIR, channel_id)
|
||||
except Exception as exc:
|
||||
return JSONResponse({"error": str(exc)}, status_code=500)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
@@ -494,10 +571,27 @@ async def douyin_room_previews(request: Request):
|
||||
for u in urls[:16]:
|
||||
if not isinstance(u, str) or not u.strip():
|
||||
continue
|
||||
out.append(fetch_douyin_anchor_name(u.strip()))
|
||||
try:
|
||||
out.append(fetch_douyin_anchor_name(u.strip()))
|
||||
except Exception as exc:
|
||||
out.append(
|
||||
{
|
||||
"ok": False,
|
||||
"error": str(exc),
|
||||
"url": u.strip(),
|
||||
"room_id": "",
|
||||
"anchor_name": "",
|
||||
}
|
||||
)
|
||||
return {"previews": out}
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def web2_uncaught_exception_handler(request: Request, exc: Exception):
|
||||
"""未捕获异常仍返回 JSON,便于 CORS 与前端展示。"""
|
||||
return JSONResponse({"error": str(exc) or exc.__class__.__name__}, status_code=500)
|
||||
|
||||
|
||||
# ---------------- 工具函数 ----------------
|
||||
async def read_log_path(path: str, lines: int) -> str:
|
||||
if not path:
|
||||
@@ -651,6 +745,13 @@ async def _youtube_guard_rows(targets: list[str]) -> tuple[list[dict], dict[str,
|
||||
if d:
|
||||
label = str(d.get("name") or label)
|
||||
row["label"] = label
|
||||
hb = read_relay_heartbeat(proc, base_dir=BASE_DIR)
|
||||
if hb.get("state") in {"starting", "streaming", "source_failover"}:
|
||||
if hb.get("speed") is not None:
|
||||
row["ffmpeg_speed"] = hb.get("speed")
|
||||
if hb.get("frame") is not None:
|
||||
row["ffmpeg_frame"] = hb.get("frame")
|
||||
row["relay_heartbeat"] = hb
|
||||
rows.append(row)
|
||||
config_by_process[proc] = _config_snapshot_for_process(proc)
|
||||
return rows, config_by_process
|
||||
@@ -1025,7 +1126,16 @@ async def start(request: Request, process: str = Query(..., description="进程
|
||||
preflight_msg = await youtube_start_preflight_message()
|
||||
if preflight_msg:
|
||||
return JSONResponse({"output": preflight_msg}, status_code=400)
|
||||
ch_doc, _ = load_channels_document(CONFIG_DIR)
|
||||
channels = (ch_doc or {}).get("channels") or []
|
||||
runtime_errors = await asyncio.to_thread(
|
||||
validate_youtube_runtime_files, CONFIG_DIR, process, channels
|
||||
)
|
||||
if runtime_errors:
|
||||
return JSONResponse({"output": "\n".join(runtime_errors)}, status_code=400)
|
||||
await asyncio.to_thread(sync_all_youtube_pro_runtime_files, CONFIG_DIR, channels)
|
||||
output = await control_start(process)
|
||||
await _record_youtube_control(process, "start")
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
|
||||
@@ -1047,6 +1157,9 @@ async def stop(process: str = Query(..., description="进程名")):
|
||||
# 第四步:再等一小会儿,确认
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
await _close_youtube_relay_sessions(process, reason="stopped")
|
||||
await _record_youtube_control(process, "stop", note="manual stop")
|
||||
|
||||
return JSONResponse({
|
||||
"message": "已执行 stop" + ("" if process.startswith("web") else " + 强杀 ffmpeg"),
|
||||
"pm2_output": ctrl_output,
|
||||
@@ -1075,6 +1188,9 @@ async def restart(request: Request, process: str = Query(..., description="进
|
||||
await force_kill_ffmpeg_cross_platform(process)
|
||||
await asyncio.sleep(0.35)
|
||||
|
||||
await _close_youtube_relay_sessions(process, reason="restart")
|
||||
await _record_youtube_control(process, "stop", note="restart pre-start")
|
||||
|
||||
if cid:
|
||||
ready_msg = _relay_channel_ready_message(cid)
|
||||
if ready_msg:
|
||||
@@ -1091,7 +1207,19 @@ async def restart(request: Request, process: str = Query(..., description="进
|
||||
preflight_msg = await youtube_start_preflight_message()
|
||||
if preflight_msg:
|
||||
return JSONResponse({"output": f"{stop_output}\n{preflight_msg}"}, status_code=400)
|
||||
ch_doc, _ = load_channels_document(CONFIG_DIR)
|
||||
channels = (ch_doc or {}).get("channels") or []
|
||||
runtime_errors = await asyncio.to_thread(
|
||||
validate_youtube_runtime_files, CONFIG_DIR, process, channels
|
||||
)
|
||||
if runtime_errors:
|
||||
return JSONResponse(
|
||||
{"output": f"{stop_output}\n" + "\n".join(runtime_errors)},
|
||||
status_code=400,
|
||||
)
|
||||
await asyncio.to_thread(sync_all_youtube_pro_runtime_files, CONFIG_DIR, channels)
|
||||
start_output = await control_start(process)
|
||||
await _record_youtube_control(process, "restart")
|
||||
return JSONResponse({"output": f"{stop_output}\n{start_output}"})
|
||||
|
||||
|
||||
@@ -1133,10 +1261,40 @@ async def status(
|
||||
channel_name=channel_name,
|
||||
url_lines=url_lines,
|
||||
db_path=RELAY_DB,
|
||||
base_dir=BASE_DIR,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
hb = read_relay_heartbeat(process, base_dir=BASE_DIR)
|
||||
observe_payload = {
|
||||
"process_status": ps,
|
||||
"recent_log": str(payload.get("recent_log") or ""),
|
||||
"recent_error": str(payload.get("recent_error") or ""),
|
||||
"is_pushing": ps == "online" and bool(hb.get("state") in {"starting", "streaming", "source_failover"}),
|
||||
"live_status": str(hb.get("state") or ""),
|
||||
"business_status": "streaming" if hb.get("state") == "streaming" else ps,
|
||||
}
|
||||
if hb:
|
||||
observe_payload["relay_heartbeat"] = hb
|
||||
if hb.get("speed") is not None:
|
||||
observe_payload["ffmpeg_speed"] = hb.get("speed")
|
||||
if hb.get("frame") is not None:
|
||||
observe_payload["ffmpeg_frame"] = hb.get("frame")
|
||||
now_ts = time.time()
|
||||
last_ob = _LIVE_OBSERVE_LAST.get(process, 0.0)
|
||||
if now_ts - last_ob >= YOUTUBE_EVENT_STATUS_OBSERVE_SECONDS:
|
||||
_LIVE_OBSERVE_LAST[process] = now_ts
|
||||
record_process_observation(
|
||||
process,
|
||||
observe_payload,
|
||||
url_lines=url_lines,
|
||||
db_path=LIVE_DB,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return JSONResponse({
|
||||
"raw_status": full_status,
|
||||
**payload,
|
||||
@@ -1255,7 +1413,10 @@ async def youtube_quality_guard(process: str = Query("", description="进程名
|
||||
if err is not None:
|
||||
return err
|
||||
rows, config_by_process = await _youtube_guard_rows(targets)
|
||||
return build_youtube_quality_guard_report(rows, config_by_process=config_by_process)
|
||||
live = await asyncio.to_thread(youtube_live_summary, db_path=LIVE_DB)
|
||||
return build_youtube_quality_guard_report(
|
||||
rows, live_summary=live, config_by_process=config_by_process
|
||||
)
|
||||
|
||||
|
||||
@app.get("/youtube/diagnostics")
|
||||
@@ -1267,10 +1428,21 @@ async def youtube_diagnostics(process: str = Query("", description="进程名,
|
||||
return build_youtube_diagnostics_report(rows, config_by_process=config_by_process)
|
||||
|
||||
|
||||
@app.get("/youtube/preflight")
|
||||
async def youtube_preflight():
|
||||
"""开播前网络体检(Google / YouTube / 抖音 + 录制代理)。"""
|
||||
try:
|
||||
return await run_youtube_preflight()
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e), "ok": False}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/youtube/relay/sessions")
|
||||
async def youtube_relay_sessions(
|
||||
process: str = Query("", description="PM2 进程名"),
|
||||
q: str = Query("", description="搜索主播/URL/房间"),
|
||||
date_from: str = Query("", description="起始日期 YYYY-MM-DD"),
|
||||
date_to: str = Query("", description="结束日期 YYYY-MM-DD"),
|
||||
status: str = Query("", description="active | ended"),
|
||||
limit: int = Query(30, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
@@ -1278,6 +1450,8 @@ async def youtube_relay_sessions(
|
||||
return list_relay_sessions(
|
||||
process=process,
|
||||
q=q,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
status=status,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
@@ -1285,19 +1459,118 @@ async def youtube_relay_sessions(
|
||||
)
|
||||
|
||||
|
||||
@app.post("/youtube/relay/sessions/import")
|
||||
async def youtube_relay_sessions_import(process: str = Query(..., description="进程名")):
|
||||
@app.get("/youtube/relay/sessions/{session_id}")
|
||||
async def youtube_relay_session_detail(session_id: str):
|
||||
session = get_relay_session(session_id, db_path=RELAY_DB)
|
||||
if not session:
|
||||
return JSONResponse({"error": "会话不存在"}, status_code=404)
|
||||
return {"session": session}
|
||||
|
||||
|
||||
@app.get("/youtube/relay/sessions/{session_id}/cover")
|
||||
async def youtube_relay_session_cover(session_id: int):
|
||||
path = get_relay_cover_path(session_id, db_path=RELAY_DB)
|
||||
if not path:
|
||||
return JSONResponse({"error": "封面不存在"}, status_code=404)
|
||||
return FileResponse(path, media_type="image/jpeg")
|
||||
|
||||
|
||||
@app.get("/youtube/relay/heartbeat")
|
||||
async def youtube_relay_heartbeat(process: str = Query(..., description="PM2 进程名")):
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"error": "无效进程名"}, status_code=400)
|
||||
hb = read_relay_heartbeat(process, base_dir=BASE_DIR)
|
||||
return {"process": process, "heartbeat": hb, "fresh": bool(hb.get("updated_at"))}
|
||||
|
||||
|
||||
@app.get("/youtube/live/summary")
|
||||
async def youtube_live_summary_route():
|
||||
try:
|
||||
return youtube_live_summary(db_path=LIVE_DB)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/youtube/live/events")
|
||||
async def youtube_live_events(
|
||||
process: str = Query("", description="PM2 进程名"),
|
||||
status: str = Query("", description="active | ended | error"),
|
||||
limit: int = Query(30, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
return list_live_events(
|
||||
process=process,
|
||||
status=status,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
db_path=LIVE_DB,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/youtube/live/events/{event_id}")
|
||||
async def youtube_live_event_detail(event_id: str):
|
||||
event = get_live_event(event_id, db_path=LIVE_DB)
|
||||
if not event:
|
||||
return JSONResponse({"error": "事件不存在"}, status_code=404)
|
||||
return {"event": event}
|
||||
|
||||
|
||||
@app.post("/youtube/live/events/{event_id}/close")
|
||||
async def youtube_live_event_close(event_id: str, request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
reason = str(data.get("reason") or "manual")
|
||||
result = close_live_event(event_id, reason=reason, db_path=LIVE_DB)
|
||||
if not result:
|
||||
return JSONResponse({"error": "事件不存在"}, status_code=404)
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/youtube/live/ingest")
|
||||
async def youtube_live_ingest(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
if not isinstance(data, dict):
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
ok, msg = verify_ingest_payload(data)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=403)
|
||||
return ingest_browser_telemetry(data, db_path=LIVE_DB)
|
||||
|
||||
|
||||
@app.get("/youtube/live/ingest_config")
|
||||
async def youtube_live_ingest_config():
|
||||
return ingest_config_payload()
|
||||
|
||||
|
||||
@app.post("/youtube/relay/sessions/import")
|
||||
async def youtube_relay_sessions_import(request: Request, process: str = Query(..., description="进程名")):
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"error": "无效进程名"}, status_code=400)
|
||||
replace_imported = False
|
||||
log_path_override = ""
|
||||
try:
|
||||
body = await request.json()
|
||||
if isinstance(body, dict):
|
||||
replace_imported = bool(body.get("replace_imported"))
|
||||
log_path_override = str(body.get("log_path") or "").strip()
|
||||
except Exception:
|
||||
pass
|
||||
info = await control_get_process_info(process)
|
||||
log_path = info.get("out_path") or ""
|
||||
log_path = log_path_override or info.get("out_path") or ""
|
||||
if not log_path:
|
||||
return JSONResponse({"error": "无法定位 PM2 日志路径"}, status_code=400)
|
||||
result = import_relay_sessions_from_log(
|
||||
Path(log_path),
|
||||
process=process,
|
||||
url_lines=_url_lines_for_process(process),
|
||||
youtube_key_suffix=extract_youtube_key_suffix(_youtube_ini_text_for_process(process)),
|
||||
db_path=RELAY_DB,
|
||||
replace_imported=replace_imported,
|
||||
)
|
||||
if result.get("error"):
|
||||
return JSONResponse(result, status_code=400)
|
||||
@@ -1328,6 +1601,72 @@ async def config_restore(request: Request):
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.post("/config_optimize")
|
||||
async def config_optimize(request: Request):
|
||||
"""配置优化(参照 d2ypp2 config_governance.optimize_content)。"""
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
if not isinstance(data, dict):
|
||||
return JSONResponse({"error": "无效数据"}, status_code=400)
|
||||
action = str(data.get("action") or "").strip()
|
||||
content = str(data.get("content") or "")
|
||||
rel = str(data.get("file") or "").strip().replace("\\", "/").lstrip("/")
|
||||
if ".." in rel.split("/"):
|
||||
return JSONResponse({"error": "非法路径"}, status_code=400)
|
||||
if not content and rel:
|
||||
content = read_config_file(CONFIG_DIR, rel)
|
||||
try:
|
||||
result = optimize_config_content(action, rel, content)
|
||||
except ValueError as exc:
|
||||
return JSONResponse({"error": str(exc)}, status_code=400)
|
||||
if data.get("save") and rel:
|
||||
write_config_text(CONFIG_DIR, rel, result["content"], reason=action)
|
||||
result["message"] = f"{result['message']},并已保存"
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/config_insights")
|
||||
async def config_insights_get(file: str = Query(..., description="相对 config 的路径")):
|
||||
rel = file.strip().replace("\\", "/").lstrip("/")
|
||||
if ".." in rel.split("/"):
|
||||
return JSONResponse({"error": "非法路径"}, status_code=400)
|
||||
content = read_config_file(CONFIG_DIR, rel)
|
||||
if not content and not (CONFIG_DIR / rel).is_file():
|
||||
return JSONResponse({"error": "文件不存在"}, status_code=404)
|
||||
return analyze_config_content(rel, content)
|
||||
|
||||
|
||||
@app.post("/config_insights")
|
||||
async def config_insights_post(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
rel = str(data.get("file") or "").strip().replace("\\", "/").lstrip("/")
|
||||
content = str(data.get("content") or "")
|
||||
if ".." in rel.split("/"):
|
||||
return JSONResponse({"error": "非法路径"}, status_code=400)
|
||||
return analyze_config_content(rel, content)
|
||||
|
||||
|
||||
@app.post("/relay_pro/channels_save")
|
||||
async def relay_pro_channels_save(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
|
||||
channels = data.get("channels") if isinstance(data.get("channels"), list) else data
|
||||
if not isinstance(channels, list):
|
||||
return JSONResponse({"error": "channels 必须是数组"}, status_code=400)
|
||||
ok, msg = save_channels_document(CONFIG_DIR, channels)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
sync_errors = sync_all_youtube_pro_runtime_files(CONFIG_DIR, channels)
|
||||
return {"message": msg, "runtime_errors": sync_errors}
|
||||
|
||||
|
||||
def _process_monitor_family_matches(entry: dict, family: str | None) -> bool:
|
||||
target = (family or "").strip().lower()
|
||||
if not target or target == "all":
|
||||
@@ -1381,11 +1720,14 @@ async def process_monitor(
|
||||
if doc.get("ok"):
|
||||
for ch in doc.get("channels") or []:
|
||||
cid = str(ch.get("id") or "").strip()
|
||||
if not cid or not channel_configured_for_monitor(CONFIG_DIR, cid):
|
||||
if not cid:
|
||||
continue
|
||||
pm2 = relay_pm2_name_for_channel(cid, CONFIG_DIR)
|
||||
if any(e.get("pm2") == pm2 for e in entries):
|
||||
continue
|
||||
entries.append(
|
||||
{
|
||||
"pm2": f"youtube2__{cid}",
|
||||
"pm2": pm2,
|
||||
"script": "youtube2.py",
|
||||
"label": f"YouTube {ch.get('name', cid)}",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user