's'
This commit is contained in:
260
web2.py
260
web2.py
@@ -29,16 +29,25 @@ from web2_youtube_oauth import (
|
||||
from web2_wechat_chatbot import register_wechat_chatbot_routes
|
||||
from web2_client_overview import build_client_overview
|
||||
from web2_relay_pro import (
|
||||
add_relay_channel,
|
||||
build_relay_live_status,
|
||||
ensure_default_relay_config,
|
||||
first_youtube_pm2_name,
|
||||
get_channel_by_id,
|
||||
get_channel_detail,
|
||||
list_channels_for_pro,
|
||||
materialize_relay_channel_youtube_ini,
|
||||
read_url_config_for_channel,
|
||||
relay_pm2_name_for_channel,
|
||||
remove_relay_channel,
|
||||
sync_channel_to_legacy_files,
|
||||
update_channel_key,
|
||||
update_channel_keys_list,
|
||||
write_url_config_for_channel,
|
||||
)
|
||||
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini
|
||||
from web2_host_caps import gpu_status_payload
|
||||
from web2_douyin_preview import fetch_douyin_anchor_name
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -121,6 +130,22 @@ def _build_process_monitor():
|
||||
PROCESS_MONITOR = _build_process_monitor()
|
||||
VALID_PROCESSES = frozenset(p["pm2"] for p in PROCESS_MONITOR)
|
||||
|
||||
|
||||
def relay_channel_id_from_pm2_name(name: str) -> Optional[str]:
|
||||
prefix = "youtube2__"
|
||||
if not name.startswith(prefix):
|
||||
return None
|
||||
return name[len(prefix) :]
|
||||
|
||||
|
||||
def process_valid(name: str) -> bool:
|
||||
if name in VALID_PROCESSES:
|
||||
return True
|
||||
cid = relay_channel_id_from_pm2_name(name)
|
||||
if not cid:
|
||||
return False
|
||||
return get_channel_by_id(CONFIG_DIR, cid) is not None
|
||||
|
||||
URL_CONFIG_PM2 = frozenset(
|
||||
pm2_name_from_script(e["script"])
|
||||
for e in SCRIPT_ENTRIES
|
||||
@@ -129,6 +154,8 @@ URL_CONFIG_PM2 = frozenset(
|
||||
|
||||
|
||||
def script_for_pm2(pm2: str) -> Optional[str]:
|
||||
if relay_channel_id_from_pm2_name(pm2):
|
||||
return "youtube2.py"
|
||||
for p in PROCESS_MONITOR:
|
||||
if p["pm2"] == pm2:
|
||||
return p["script"]
|
||||
@@ -147,18 +174,24 @@ def get_builtin_supervisor() -> BuiltinSupervisor:
|
||||
|
||||
|
||||
async def control_start(process: str) -> str:
|
||||
if relay_channel_id_from_pm2_name(process) and not use_builtin():
|
||||
return "多频道独立进程仅支持内置进程管理(请勿使用 WEB2_SUPERVISOR=pm2)。"
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().start(process)
|
||||
return await run_pm2_shell(f"start {process}")
|
||||
|
||||
|
||||
async def control_stop(process: str) -> str:
|
||||
if relay_channel_id_from_pm2_name(process) and not use_builtin():
|
||||
return "多频道独立进程仅支持内置进程管理(请勿使用 WEB2_SUPERVISOR=pm2)。"
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().stop(process)
|
||||
return await run_pm2_shell(f"stop {process}")
|
||||
|
||||
|
||||
async def control_restart(process: str) -> str:
|
||||
if relay_channel_id_from_pm2_name(process) and not use_builtin():
|
||||
return "多频道独立进程仅支持内置进程管理(请勿使用 WEB2_SUPERVISOR=pm2)。"
|
||||
if use_builtin():
|
||||
return await get_builtin_supervisor().restart(process)
|
||||
return await run_pm2_shell(f"restart {process}")
|
||||
@@ -222,6 +255,39 @@ async def relay_pro_channels():
|
||||
return list_channels_for_pro(CONFIG_DIR)
|
||||
|
||||
|
||||
@app.post("/relay_pro/channel_add")
|
||||
async def relay_pro_channel_add(request: Request):
|
||||
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)
|
||||
name = str(data.get("name") or "")
|
||||
key = str(data.get("youtubeKey") or data.get("key") or "")
|
||||
ok, msg, new_id = add_relay_channel(CONFIG_DIR, name, key)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg, "channelId": new_id}
|
||||
|
||||
|
||||
@app.post("/relay_pro/channel_delete")
|
||||
async def relay_pro_channel_delete(request: Request):
|
||||
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)
|
||||
cid = str(data.get("channelId") or data.get("channel_id") or "").strip()
|
||||
if not cid:
|
||||
return JSONResponse({"error": "缺少 channelId"}, status_code=400)
|
||||
ok, msg = remove_relay_channel(CONFIG_DIR, cid)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.get("/relay_pro/channel_detail")
|
||||
async def relay_pro_channel_detail(channel_id: str = Query(..., description="频道 id,如 ch_0")):
|
||||
d = get_channel_detail(CONFIG_DIR, channel_id)
|
||||
@@ -266,6 +332,30 @@ async def relay_pro_save_channel_key(request: Request):
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@app.post("/relay_pro/channel_keys")
|
||||
async def relay_pro_save_channel_keys(request: Request):
|
||||
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)
|
||||
cid = str(data.get("channelId") or data.get("channel_id") or "").strip()
|
||||
keys = data.get("keys")
|
||||
if not cid:
|
||||
return JSONResponse({"error": "缺少 channelId"}, status_code=400)
|
||||
if not isinstance(keys, list):
|
||||
return JSONResponse({"error": "keys 须为数组"}, status_code=400)
|
||||
try:
|
||||
ai = int(data.get("activeIndex", 0))
|
||||
except Exception:
|
||||
ai = 0
|
||||
ok, msg = update_channel_keys_list(CONFIG_DIR, cid, [str(x) for x in keys], ai)
|
||||
if not ok:
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
return {"message": msg}
|
||||
|
||||
|
||||
@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)
|
||||
@@ -290,6 +380,30 @@ async def relay_pro_live_status():
|
||||
)
|
||||
|
||||
|
||||
@app.get("/host/gpu_status")
|
||||
async def host_gpu_status():
|
||||
return gpu_status_payload()
|
||||
|
||||
|
||||
@app.post("/douyin/room_previews")
|
||||
async def douyin_room_previews(request: Request):
|
||||
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)
|
||||
urls = data.get("urls")
|
||||
if not isinstance(urls, list):
|
||||
return JSONResponse({"error": "urls 须为数组"}, status_code=400)
|
||||
out = []
|
||||
for u in urls[:16]:
|
||||
if not isinstance(u, str) or not u.strip():
|
||||
continue
|
||||
out.append(fetch_douyin_anchor_name(u.strip()))
|
||||
return {"previews": out}
|
||||
|
||||
|
||||
# ---------------- 工具函数 ----------------
|
||||
async def read_log_path(path: str, lines: int) -> str:
|
||||
if not path:
|
||||
@@ -343,6 +457,58 @@ async def save_config(request: Request, process: str = Query(..., description="
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/youtube/ini_model")
|
||||
async def youtube_ini_model_get(process: str = Query(..., description="进程名")):
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini"}, status_code=400)
|
||||
config_file = CONFIG_DIR / "youtube.ini"
|
||||
if not config_file.exists():
|
||||
return {"keys": [""], "activeIndex": 0, "options": {}, "header": ""}
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"读取失败: {e}"}, status_code=500)
|
||||
return parse_youtube_ini(content)
|
||||
|
||||
|
||||
@app.post("/youtube/ini_model")
|
||||
async def youtube_ini_model_post(request: Request, process: str = Query(..., description="进程名")):
|
||||
sc = script_for_pm2(process)
|
||||
if not sc or not is_youtube_script(sc):
|
||||
return JSONResponse({"error": "仅 youtube*.py 对应进程支持 youtube.ini"}, status_code=400)
|
||||
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)
|
||||
keys = data.get("keys")
|
||||
if not isinstance(keys, list):
|
||||
keys = [""]
|
||||
keys = [str(x) for x in keys]
|
||||
try:
|
||||
active_index = int(data.get("activeIndex", 0))
|
||||
except Exception:
|
||||
active_index = 0
|
||||
options = data.get("options")
|
||||
if not isinstance(options, dict):
|
||||
options = {}
|
||||
else:
|
||||
options = {str(k): str(v) for k, v in options.items()}
|
||||
header = data.get("header")
|
||||
if not isinstance(header, str):
|
||||
header = ""
|
||||
content = serialize_youtube_ini(keys, active_index, options, header)
|
||||
config_file = CONFIG_DIR / "youtube.ini"
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
return {"message": "配置保存成功"}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
# ---------------- 配置路由(URL_config.ini,youtube 和 tiktok 共享) ----------------
|
||||
@app.get("/get_url_config")
|
||||
async def get_url_config(process: str = Query(..., description="进程名")):
|
||||
@@ -377,18 +543,73 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
def _script_entries_payload() -> list[dict]:
|
||||
return [{"script": e["script"]} for e in SCRIPT_ENTRIES]
|
||||
|
||||
|
||||
async def _relay_youtube_online_pm2_names() -> list[str]:
|
||||
"""已启动的多路转播进程 youtube2__*(与单进程 youtube2 互斥)。"""
|
||||
doc = list_channels_for_pro(CONFIG_DIR)
|
||||
if not doc.get("ok"):
|
||||
return []
|
||||
out: list[str] = []
|
||||
for ch in doc.get("channels") or []:
|
||||
cid = str(ch.get("id") or "").strip()
|
||||
if not cid:
|
||||
continue
|
||||
pnm = relay_pm2_name_for_channel(cid)
|
||||
info = await control_get_process_info(pnm)
|
||||
if str(info.get("process_status") or "") == "online":
|
||||
out.append(pnm)
|
||||
return out
|
||||
|
||||
|
||||
async def _stop_legacy_youtube_if_online() -> None:
|
||||
"""多路转播启动前必须停掉单进程 youtube2,否则会读全局 URL_config.ini,等同「所有线路一起推」。"""
|
||||
if not use_builtin():
|
||||
return
|
||||
legacy = first_youtube_pm2_name(_script_entries_payload())
|
||||
if not legacy:
|
||||
return
|
||||
info = await control_get_process_info(legacy)
|
||||
if str(info.get("process_status") or "") != "online":
|
||||
return
|
||||
await control_stop(legacy)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
# ---------------- PM2 控制路由 ----------------
|
||||
@app.get("/start")
|
||||
async def start(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
sc = script_for_pm2(process)
|
||||
legacy_pm2 = first_youtube_pm2_name(_script_entries_payload())
|
||||
if not cid and sc == "youtube2.py" and process == legacy_pm2:
|
||||
online_relays = await _relay_youtube_online_pm2_names()
|
||||
if online_relays:
|
||||
return JSONResponse(
|
||||
{
|
||||
"output": (
|
||||
"当前有多路转播进程在运行,与单进程 youtube2 互斥(单进程会读全局 URL_config.ini)。"
|
||||
f"请先停止:{', '.join(online_relays)}"
|
||||
)
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
if cid:
|
||||
ok, msg = materialize_relay_channel_youtube_ini(CONFIG_DIR, cid)
|
||||
if not ok:
|
||||
return JSONResponse({"output": msg}, status_code=400)
|
||||
await _stop_legacy_youtube_if_online()
|
||||
output = await control_start(process)
|
||||
return JSONResponse({"output": output})
|
||||
|
||||
|
||||
@app.get("/stop")
|
||||
async def stop(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
|
||||
# 第一步:尝试优雅停止
|
||||
@@ -413,8 +634,14 @@ async def stop(process: str = Query(..., description="进程名")):
|
||||
|
||||
@app.get("/restart")
|
||||
async def restart(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
if not process_valid(process):
|
||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||
cid = relay_channel_id_from_pm2_name(process)
|
||||
if cid:
|
||||
ok, msg = materialize_relay_channel_youtube_ini(CONFIG_DIR, cid)
|
||||
if not ok:
|
||||
return JSONResponse({"output": msg}, status_code=400)
|
||||
await _stop_legacy_youtube_if_online()
|
||||
output = await control_restart(process)
|
||||
if not process.startswith("web"):
|
||||
await force_kill_ffmpeg_cross_platform(process)
|
||||
@@ -423,7 +650,7 @@ async def restart(process: str = Query(..., description="进程名")):
|
||||
|
||||
@app.get("/status")
|
||||
async def status(process: str = Query(..., description="进程名")):
|
||||
if process not in VALID_PROCESSES:
|
||||
if not process_valid(process):
|
||||
return JSONResponse({
|
||||
"raw_status": "",
|
||||
"process_status": "invalid",
|
||||
@@ -551,12 +778,25 @@ async def youtube_stream_fetch_key(process: str = Query(..., description="进程
|
||||
# ---------------- 进程列表(供前端同步,由 discover_script_entries 自动扫描) ----------------
|
||||
@app.get("/process_monitor")
|
||||
async def process_monitor():
|
||||
return {
|
||||
"entries": [
|
||||
{"pm2": p["pm2"], "script": p["script"], "label": p["label"]}
|
||||
for p in PROCESS_MONITOR
|
||||
]
|
||||
}
|
||||
ensure_default_relay_config(CONFIG_DIR)
|
||||
entries = [
|
||||
{"pm2": p["pm2"], "script": p["script"], "label": p["label"]}
|
||||
for p in PROCESS_MONITOR
|
||||
]
|
||||
doc = list_channels_for_pro(CONFIG_DIR)
|
||||
if doc.get("ok"):
|
||||
for ch in doc.get("channels") or []:
|
||||
cid = str(ch.get("id") or "").strip()
|
||||
if not cid:
|
||||
continue
|
||||
entries.append(
|
||||
{
|
||||
"pm2": f"youtube2__{cid}",
|
||||
"script": "youtube2.py",
|
||||
"label": f"YouTube {ch.get('name', cid)}",
|
||||
}
|
||||
)
|
||||
return {"entries": entries}
|
||||
|
||||
|
||||
# ---------------- 首页 ----------------
|
||||
|
||||
Reference in New Issue
Block a user