This commit is contained in:
eric
2026-03-22 23:29:04 -05:00
parent a4b7edd153
commit cc552d49f0
2 changed files with 151 additions and 42 deletions

118
web2.py
View File

@@ -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.initiktok2 / 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)
# 第三步:录制类进程才强杀 ffmpegweb* 控制台不杀 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():