's'
This commit is contained in:
118
web2.py
118
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():
|
||||
|
||||
Reference in New Issue
Block a user