diff --git a/index.html b/index.html index dd0df4c..0569b39 100644 --- a/index.html +++ b/index.html @@ -84,16 +84,36 @@ button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
正在加载状态与日志...
diff --git a/web2.py b/web2.py index 1ad14c4..849cb8e 100644 --- a/web2.py +++ b/web2.py @@ -3,6 +3,7 @@ from fastapi.responses import FileResponse, JSONResponse from pathlib import Path import asyncio import re +from typing import Optional from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="多进程录制控制台") @@ -14,21 +15,80 @@ DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs" MAX_LOG_LINES = 300 -# 本分支用文件名带「2」与另一套无后缀分支区分;典型部署为 Ubuntu + PM2。 -# 与 index.html 中 PROCESS_MONITOR 保持一致:pm2=PM2 应用名,script=入口脚本 -PROCESS_MONITOR = ( - {"pm2": "tiktok", "script": "tiktok2.py", "label": "TikTok"}, - {"pm2": "youtube", "script": "youtube2.py", "label": "YouTube"}, - {"pm2": "obs", "script": "obs2.sh", "label": "OBS"}, -) + +def _label_short(prefix: str) -> str: + return {"tiktok": "TikTok", "youtube": "YouTube", "obs": "OBS", "web": "Web 控制台"}.get(prefix, prefix) + + +def discover_script_entries() -> tuple: + """扫描项目根目录自动识别入口:tiktok*.py、youtube*.py、obs*.sh,以及当前本文件(Web 控制台)。PM2 名 = 主文件名(无扩展名)。""" + base = BASE_DIR + web_path = Path(__file__).resolve() + entries: list[dict] = [] + + for path in sorted(base.glob("tiktok*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("tiktok")}) + + for path in sorted(base.glob("youtube*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("youtube")}) + + for path in sorted(base.glob("obs*.sh")): + if path.is_file(): + entries.append({"script": path.name, "label": _label_short("obs")}) + + entries.append({"script": web_path.name, "label": _label_short("web")}) + + return tuple(entries) + + +SCRIPT_ENTRIES = discover_script_entries() + + +def pm2_name_from_script(script: str) -> str: + return Path(script).stem + + +def _stem(script: str) -> str: + return Path(script).stem + + +def is_youtube_script(script: str) -> bool: + return script.endswith(".py") and _stem(script).startswith("youtube") + + +def is_tiktok_script(script: str) -> bool: + return script.endswith(".py") and _stem(script).startswith("tiktok") + + +def _build_process_monitor(): + return tuple( + { + "pm2": pm2_name_from_script(e["script"]), + "script": e["script"], + "label": e["label"], + } + for e in SCRIPT_ENTRIES + ) + + +PROCESS_MONITOR = _build_process_monitor() VALID_PROCESSES = frozenset(p["pm2"] for p in PROCESS_MONITOR) -# youtube.ini 仅对与 youtube2.py 绑定的 PM2 名开放 -YOUTUBE_PM2 = next(p["pm2"] for p in PROCESS_MONITOR if p["script"] == "youtube2.py") -# URL_config.ini:tiktok2 / youtube2 两条入口共用 + URL_CONFIG_PM2 = frozenset( - p["pm2"] for p in PROCESS_MONITOR if p["script"] in ("tiktok2.py", "youtube2.py") + pm2_name_from_script(e["script"]) + for e in SCRIPT_ENTRIES + if is_tiktok_script(e["script"]) or is_youtube_script(e["script"]) ) + +def script_for_pm2(pm2: str) -> Optional[str]: + for p in PROCESS_MONITOR: + if p["pm2"] == pm2: + return p["script"] + return None + # CORS app.add_middleware( CORSMiddleware, @@ -137,8 +197,9 @@ async def get_process_info(process: str) -> dict: # ---------------- 配置路由(youtube.ini,仅 youtube) ---------------- @app.get("/get_config") async def get_config(process: str = Query(..., description="进程名")): - if process != YOUTUBE_PM2: - return JSONResponse({"error": "仅 YouTube 入口(youtube2.py)对应进程支持此配置编辑"}, status_code=400) + 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(): @@ -151,8 +212,9 @@ async def get_config(process: str = Query(..., description="进程名")): @app.post("/save_config") async def save_config(request: Request, process: str = Query(..., description="进程名")): - if process != YOUTUBE_PM2: - return JSONResponse({"error": "仅 YouTube 入口(youtube2.py)对应进程支持此配置编辑"}, status_code=400) + 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() @@ -172,7 +234,7 @@ async def save_config(request: Request, process: str = Query(..., description=" @app.get("/get_url_config") async def get_url_config(process: str = Query(..., description="进程名")): if process not in URL_CONFIG_PM2: - return JSONResponse({"error": "仅 tiktok2.py / youtube2.py 对应进程支持 URL 配置编辑"}, status_code=400) + return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400) config_file = CONFIG_DIR / "URL_config.ini" if not config_file.exists(): @@ -186,7 +248,7 @@ async def get_url_config(process: str = Query(..., description="进程名")): @app.post("/save_url_config") async def save_url_config(request: Request, process: str = Query(..., description="进程名")): if process not in URL_CONFIG_PM2: - return JSONResponse({"error": "仅 tiktok2.py / youtube2.py 对应进程支持 URL 配置编辑"}, status_code=400) + return JSONResponse({"error": "仅 tiktok*.py / youtube*.py 对应进程支持 URL 配置编辑"}, status_code=400) try: data = await request.json() @@ -222,14 +284,15 @@ async def stop(process: str = Query(..., description="进程名")): # 第二步:等待 PM2 信号传播(通常 1~3 秒) await asyncio.sleep(2.0) - # 第三步:主动查找并杀死 ffmpeg - await force_kill_ffmpeg(process) + # 第三步:录制类进程才强杀 ffmpeg(web* 控制台不杀 ffmpeg) + if not process.startswith("web"): + await force_kill_ffmpeg(process) # 第四步:再等一小会儿,确认 await asyncio.sleep(1.0) return JSONResponse({ - "message": "已执行 stop + 强杀 ffmpeg", + "message": "已执行 stop" + ("" if process.startswith("web") else " + 强杀 ffmpeg"), "pm2_output": pm2_output, "note": "如果仍有残留,可多次点击停止或检查 pm2 logs" }) @@ -240,8 +303,8 @@ async def restart(process: str = Query(..., description="进程名")): if process not in VALID_PROCESSES: return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400) output = await run_pm2(f"restart {process}") - # 重启后也建议清理一次残留(视情况可选) - await force_kill_ffmpeg(process) + if not process.startswith("web"): + await force_kill_ffmpeg(process) return JSONResponse({"output": output}) @@ -273,6 +336,17 @@ async def status(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 + ] + } + + # ---------------- 首页 ---------------- @app.get("/") async def index():