's'
This commit is contained in:
75
index.html
75
index.html
@@ -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_monitor(web2.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
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