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

View File

@@ -84,16 +84,36 @@ button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
<pre id="pm2Log">正在加载状态与日志...</pre>
</div>
<script>
/**
* 监控目标:本分支用 *2.py / web2 / obs2 等与「无 2」分支区分常与 Ubuntu+PM2 配套。
* 与 web2.py 中 PROCESS_MONITOR / VALID_PROCESSES 一致PM2 应用名script 为入口脚本文件名。
* 若改了 PM2 进程名,请同步改 pm2 字段与 web2.py。
*/
const PROCESS_MONITOR = [
{ pm2: 'tiktok', script: 'tiktok2.py', label: 'TikTok' },
{ pm2: 'youtube', script: 'youtube2.py', label: 'YouTube' },
{ pm2: 'obs', script: 'obs2.sh', label: 'OBS' },
];
/** 进程列表仅来自 GET /process_monitorweb2.py 启动时扫描项目根目录自动识别PM2 名 = 脚本主文件名(无扩展名)。 */
let PROCESS_MONITOR = [];
function pm2NameFromScript(script) {
const base = String(script).split(/[/\\]/).pop() || '';
const i = base.lastIndexOf('.');
return i > 0 ? base.slice(0, i) : base;
}
function isYoutubeScript(script) {
return /\.py$/i.test(script) && pm2NameFromScript(script).startsWith('youtube');
}
function isTiktokScript(script) {
return /\.py$/i.test(script) && pm2NameFromScript(script).startsWith('tiktok');
}
function isObsScript(script) {
return pm2NameFromScript(script).startsWith('obs');
}
async function loadProcessMonitor() {
const res = await fetch('/process_monitor');
if (!res.ok) throw new Error(String(res.status));
const data = await res.json();
const entries = data.entries || [];
PROCESS_MONITOR = entries.map((e) => ({
script: e.script,
label: e.label || e.script,
pm2: e.pm2 || pm2NameFromScript(e.script),
}));
}
function getEntryByPm2(name) {
return PROCESS_MONITOR.find((e) => e.pm2 === name) || null;
@@ -112,8 +132,7 @@ function fillProcessSelect() {
const logEl = document.getElementById('pm2Log');
const statusIndicator = document.getElementById('statusIndicator');
const processSelect = document.getElementById('processSelect');
fillProcessSelect();
let currentProcess = PROCESS_MONITOR[0].pm2;
let currentProcess = '';
// 各专属区域
const youtubeOnlySection = document.getElementById('youtubeOnlySection');
@@ -147,9 +166,10 @@ processSelect.addEventListener('change', () => {
function updateSectionsVisibility() {
const ent = getEntryByPm2(currentProcess);
const isYoutube = ent && ent.script === 'youtube2.py';
const isTiktok = ent && ent.script === 'tiktok2.py';
const isObs = ent && ent.script === 'obs2.sh';
const s = ent ? ent.script : '';
const isYoutube = ent && isYoutubeScript(s);
const isTiktok = ent && isTiktokScript(s);
const isObs = ent && isObsScript(s);
youtubeOnlySection.style.display = isYoutube ? 'block' : 'none';
urlConfigSection.style.display = (isYoutube || isTiktok) ? 'block' : 'none';
@@ -291,11 +311,26 @@ document.getElementById('pm2Buttons').addEventListener('click', e=>{
if(action && action !== 'status') pm2Action(action);
});
// 自动刷新 + 初始
setInterval(fetchStatus, 1000);
refreshProcessScriptNote();
fetchStatus();
updateSectionsVisibility();
(async function boot() {
try {
await loadProcessMonitor();
} catch (e) {
logEl.textContent = '无法加载 /process_monitor请用本服务打开的页面访问或检查 web2 是否已启动)\n' + (e && e.message ? e.message : '');
statusIndicator.textContent = '❌ 未连接';
return;
}
if (!PROCESS_MONITOR.length) {
logEl.textContent = '未发现任何入口脚本(项目根应有 tiktok*.py / youtube*.py / obs*.sh且 Web 为当前 web*.py';
return;
}
fillProcessSelect();
currentProcess = PROCESS_MONITOR[0].pm2;
processSelect.value = currentProcess;
refreshProcessScriptNote();
setInterval(fetchStatus, 1000);
fetchStatus();
updateSectionsVisibility();
})();
</script>
</body>
</html>

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():