s
This commit is contained in:
@@ -1 +1 @@
|
|||||||
https://live.douyin.com/35932729226,主播: 内向小学生
|
https://live.douyin.com/945495200134,主播: 小张摆摊vlog
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[youtube]
|
[youtube]
|
||||||
# 只写 stream key(推荐方式)
|
# 只写 stream key(推荐方式)
|
||||||
key = qxvb-r47b-r5ju-6ud3-6k7z
|
key = frcc-7914-kxhz-rbux-4zuh
|
||||||
170
index.html
170
index.html
@@ -13,16 +13,17 @@ h1{margin:0 0 24px;font-size:1.6em;display:flex;align-items:center;gap:12px}
|
|||||||
#processSelect{padding:8px;border-radius:8px;border:1px solid #ddd;font-size:1em}
|
#processSelect{padding:8px;border-radius:8px;border:1px solid #ddd;font-size:1em}
|
||||||
#statusIndicator{font-size:1.2em;font-weight:600}
|
#statusIndicator{font-size:1.2em;font-weight:600}
|
||||||
h2{margin:24px 0 12px;font-size:1.4em}
|
h2{margin:24px 0 12px;font-size:1.4em}
|
||||||
|
h3{margin:16px 0 8px;font-size:1.2em}
|
||||||
input,button,textarea{width:100%;padding:12px;margin:8px 0;border-radius:8px;border:none;box-sizing:border-box;font-size:1em}
|
input,button,textarea{width:100%;padding:12px;margin:8px 0;border-radius:8px;border:none;box-sizing:border-box;font-size:1em}
|
||||||
input,textarea{background:#f5f5f5;color:#000;border:1px solid #ddd;font-family:monospace}
|
input,textarea{background:#f5f5f5;color:#000;border:1px solid #ddd;font-family:monospace;overflow:hidden;resize:none;min-height:100px}
|
||||||
button{background:var(--accent);color:#fff;cursor:pointer;font-weight:600;transition:.2s}
|
button{background:var(--accent);color:#fff;cursor:pointer;font-weight:600;transition:.2s}
|
||||||
button:hover{background:#1a237e}
|
button:hover{background:#1a237e}
|
||||||
button.loading{opacity:.7;pointer-events:none}
|
button.loading{opacity:.7;pointer-events:none}
|
||||||
button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
|
button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
|
||||||
#pm2Buttons{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;margin:12px 0}
|
#pm2Buttons{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;margin:12px 0}
|
||||||
#pm2Log{background:#000;color:#0f0;padding:12px;height:320px;overflow-y:auto;font-family:monospace;font-size:.9em;border-radius:8px;white-space:pre-wrap;word-break:break-all}
|
#pm2Log{background:#000;color:#0f0;padding:12px;height:320px;overflow-y:auto;font-family:monospace;font-size:.9em;border-radius:8px;white-space:pre-wrap;word-break:break-all}
|
||||||
#configButtons{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin:12px 0}
|
.configButtons{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin:12px 0}
|
||||||
#configStatus{color:#0f0;margin-top:8px;font-family:monospace}
|
.configStatus{color:#0f0;margin-top:8px;font-family:monospace;font-size:0.9em}
|
||||||
#obsAddress{background:#f0f0f0;padding:12px;border-radius:8px;font-family:monospace;font-size:1.1em;margin:8px 0;word-break:break-all}
|
#obsAddress{background:#f0f0f0;padding:12px;border-radius:8px;font-family:monospace;font-size:1.1em;margin:8px 0;word-break:break-all}
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
</style>
|
</style>
|
||||||
@@ -39,15 +40,32 @@ button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
|
|||||||
<span id="statusIndicator">⚪ 加载中...</span>
|
<span id="statusIndicator">⚪ 加载中...</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- YouTube 配置编辑区(仅 youtube 时显示) -->
|
<!-- YouTube 专属配置(仅 youtube 时显示) -->
|
||||||
<div id="configSection" style="display:none;">
|
<div id="youtubeOnlySection" style="display:none;">
|
||||||
<h2>YouTube 配置编辑 (config/youtube.ini)</h2>
|
<h2>YouTube 配置编辑 (youtube.ini)</h2>
|
||||||
<textarea id="youtubeConfig" rows="20"></textarea>
|
<div class="configEditor">
|
||||||
<div id="configButtons">
|
<h3>youtube.ini</h3>
|
||||||
<button id="loadConfigBtn">加载配置</button>
|
<textarea id="youtubeConfig"></textarea>
|
||||||
<button id="saveConfigBtn">保存配置</button>
|
<div class="configButtons">
|
||||||
|
<button id="loadYoutubeBtn">加载配置</button>
|
||||||
|
<button id="saveYoutubeBtn">保存配置</button>
|
||||||
|
</div>
|
||||||
|
<pre class="configStatus" id="youtubeStatus">就绪</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL_config.ini 共享配置(youtube 或 tiktok 时显示) -->
|
||||||
|
<div id="urlConfigSection" style="display:none;">
|
||||||
|
<h2>URL 配置编辑 (URL_config.ini)</h2>
|
||||||
|
<div class="configEditor">
|
||||||
|
<h3>URL_config.ini</h3>
|
||||||
|
<textarea id="urlConfig"></textarea>
|
||||||
|
<div class="configButtons">
|
||||||
|
<button id="loadUrlBtn">加载配置</button>
|
||||||
|
<button id="saveUrlBtn">保存配置</button>
|
||||||
|
</div>
|
||||||
|
<pre class="configStatus" id="urlStatus">就绪</pre>
|
||||||
</div>
|
</div>
|
||||||
<pre id="configStatus">就绪</pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OBS 推流地址区(仅 obs 时显示) -->
|
<!-- OBS 推流地址区(仅 obs 时显示) -->
|
||||||
@@ -75,12 +93,21 @@ const processSelect = document.getElementById('processSelect');
|
|||||||
let currentProcess = 'tiktok';
|
let currentProcess = 'tiktok';
|
||||||
|
|
||||||
// 各专属区域
|
// 各专属区域
|
||||||
const configSection = document.getElementById('configSection');
|
const youtubeOnlySection = document.getElementById('youtubeOnlySection');
|
||||||
|
const urlConfigSection = document.getElementById('urlConfigSection');
|
||||||
const obsSection = document.getElementById('obsSection');
|
const obsSection = document.getElementById('obsSection');
|
||||||
|
|
||||||
|
// youtube.ini 编辑(youtube 专属)
|
||||||
const youtubeConfig = document.getElementById('youtubeConfig');
|
const youtubeConfig = document.getElementById('youtubeConfig');
|
||||||
const loadConfigBtn = document.getElementById('loadConfigBtn');
|
const loadYoutubeBtn = document.getElementById('loadYoutubeBtn');
|
||||||
const saveConfigBtn = document.getElementById('saveConfigBtn');
|
const saveYoutubeBtn = document.getElementById('saveYoutubeBtn');
|
||||||
const configStatus = document.getElementById('configStatus');
|
const youtubeStatus = document.getElementById('youtubeStatus');
|
||||||
|
|
||||||
|
// URL_config.ini 编辑(youtube 和 tiktok 共享)
|
||||||
|
const urlConfig = document.getElementById('urlConfig');
|
||||||
|
const loadUrlBtn = document.getElementById('loadUrlBtn');
|
||||||
|
const saveUrlBtn = document.getElementById('saveUrlBtn');
|
||||||
|
const urlStatus = document.getElementById('urlStatus');
|
||||||
|
|
||||||
processSelect.addEventListener('change', () => {
|
processSelect.addEventListener('change', () => {
|
||||||
currentProcess = processSelect.value;
|
currentProcess = processSelect.value;
|
||||||
@@ -88,16 +115,81 @@ processSelect.addEventListener('change', () => {
|
|||||||
updateSectionsVisibility();
|
updateSectionsVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据当前进程显示/隐藏对应区域
|
|
||||||
function updateSectionsVisibility() {
|
function updateSectionsVisibility() {
|
||||||
configSection.style.display = currentProcess === 'youtube' ? 'block' : 'none';
|
youtubeOnlySection.style.display = currentProcess === 'youtube' ? 'block' : 'none';
|
||||||
|
urlConfigSection.style.display = (currentProcess === 'youtube' || currentProcess === 'tiktok') ? 'block' : 'none';
|
||||||
obsSection.style.display = currentProcess === 'obs' ? 'block' : 'none';
|
obsSection.style.display = currentProcess === 'obs' ? 'block' : 'none';
|
||||||
|
|
||||||
if (currentProcess === 'youtube') {
|
if (currentProcess === 'youtube') {
|
||||||
loadConfig();
|
loadYoutubeConfig();
|
||||||
|
}
|
||||||
|
if (currentProcess === 'youtube' || currentProcess === 'tiktok') {
|
||||||
|
loadUrlConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// textarea 高度自适应
|
||||||
|
function autoResize(textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = (textarea.scrollHeight) + 'px';
|
||||||
|
}
|
||||||
|
[youtubeConfig, urlConfig].forEach(ta => {
|
||||||
|
ta.addEventListener('input', () => autoResize(ta));
|
||||||
|
autoResize(ta);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用加载/保存
|
||||||
|
async function loadConfig(endpoint, textarea, statusEl) {
|
||||||
|
statusEl.textContent = '加载中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${endpoint}?process=${currentProcess}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
textarea.value = data.content || '';
|
||||||
|
autoResize(textarea);
|
||||||
|
statusEl.textContent = '加载成功';
|
||||||
|
} catch (err) {
|
||||||
|
statusEl.textContent = `加载失败: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig(endpoint, textarea, statusEl, saveBtn) {
|
||||||
|
statusEl.textContent = '保存中...';
|
||||||
|
const content = textarea.value;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${endpoint}?process=${currentProcess}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({content})
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
statusEl.textContent = data.message || '保存成功';
|
||||||
|
} catch (err) {
|
||||||
|
statusEl.textContent = `保存失败: ${err.message}`;
|
||||||
|
} finally {
|
||||||
|
if (saveBtn) saveBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// youtube.ini(仅 youtube)
|
||||||
|
function loadYoutubeConfig() { loadConfig('/get_config', youtubeConfig, youtubeStatus); }
|
||||||
|
function saveYoutubeConfig() {
|
||||||
|
saveYoutubeBtn.classList.add('loading');
|
||||||
|
saveConfig('/save_config', youtubeConfig, youtubeStatus, saveYoutubeBtn);
|
||||||
|
}
|
||||||
|
loadYoutubeBtn.onclick = loadYoutubeConfig;
|
||||||
|
saveYoutubeBtn.onclick = saveYoutubeConfig;
|
||||||
|
|
||||||
|
// URL_config.ini(youtube 和 tiktok 共享)
|
||||||
|
function loadUrlConfig() { loadConfig('/get_url_config', urlConfig, urlStatus); }
|
||||||
|
function saveUrlConfig() {
|
||||||
|
saveUrlBtn.classList.add('loading');
|
||||||
|
saveConfig('/save_url_config', urlConfig, urlStatus, saveUrlBtn);
|
||||||
|
}
|
||||||
|
loadUrlBtn.onclick = loadUrlConfig;
|
||||||
|
saveUrlBtn.onclick = saveUrlConfig;
|
||||||
|
|
||||||
function scrollLog(){logEl.scrollTop = logEl.scrollHeight}
|
function scrollLog(){logEl.scrollTop = logEl.scrollHeight}
|
||||||
|
|
||||||
function updateStatus(process_status){
|
function updateStatus(process_status){
|
||||||
@@ -156,44 +248,6 @@ async function pm2Action(action){
|
|||||||
|
|
||||||
async function fetchStatus(){ pm2Action('status'); }
|
async function fetchStatus(){ pm2Action('status'); }
|
||||||
|
|
||||||
// 配置加载
|
|
||||||
async function loadConfig(){
|
|
||||||
configStatus.textContent = '加载中...';
|
|
||||||
try{
|
|
||||||
const res = await fetch(`/get_config?process=${currentProcess}`);
|
|
||||||
if(!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
youtubeConfig.value = data.content || '';
|
|
||||||
configStatus.textContent = '加载成功';
|
|
||||||
}catch(err){
|
|
||||||
configStatus.textContent = `加载失败: ${err.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置保存
|
|
||||||
async function saveConfig(){
|
|
||||||
configStatus.textContent = '保存中...';
|
|
||||||
const content = youtubeConfig.value;
|
|
||||||
try{
|
|
||||||
const res = await fetch(`/save_config?process=${currentProcess}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({content})
|
|
||||||
});
|
|
||||||
if(!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
configStatus.textContent = data.message || '保存成功';
|
|
||||||
}catch(err){
|
|
||||||
configStatus.textContent = `保存失败: ${err.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadConfigBtn.onclick = loadConfig;
|
|
||||||
saveConfigBtn.onclick = () => {
|
|
||||||
saveConfigBtn.classList.add('loading');
|
|
||||||
saveConfig().finally(() => saveConfigBtn.classList.remove('loading'));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 按钮事件委托
|
// 按钮事件委托
|
||||||
document.getElementById('pm2Buttons').addEventListener('click', e=>{
|
document.getElementById('pm2Buttons').addEventListener('click', e=>{
|
||||||
const btn = e.target.closest('button');
|
const btn = e.target.closest('button');
|
||||||
@@ -202,10 +256,8 @@ document.getElementById('pm2Buttons').addEventListener('click', e=>{
|
|||||||
if(action && action !== 'status') pm2Action(action);
|
if(action && action !== 'status') pm2Action(action);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动刷新状态与日志
|
// 自动刷新 + 初始
|
||||||
setInterval(fetchStatus, 1000);
|
setInterval(fetchStatus, 1000);
|
||||||
|
|
||||||
// 初始加载
|
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
updateSectionsVisibility();
|
updateSectionsVisibility();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
53
web.py
53
web.py
@@ -7,17 +7,17 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
|
|
||||||
app = FastAPI(title="多进程录制控制台")
|
app = FastAPI(title="多进程录制控制台")
|
||||||
|
|
||||||
# ---------------- 配置(修正为根目录结构) ----------------
|
# ---------------- 配置(根目录结构) ----------------
|
||||||
BASE_DIR = Path(__file__).parent
|
BASE_DIR = Path(__file__).parent
|
||||||
CONFIG_DIR = BASE_DIR / "config" # 直接在根目录下
|
CONFIG_DIR = BASE_DIR / "config" # config 文件夹在项目根目录
|
||||||
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs" # 直接在根目录下
|
DEFAULT_LOG_DIR = BASE_DIR / ".pm2" / "logs" # .pm2/logs 在项目根目录
|
||||||
|
|
||||||
MAX_LOG_LINES = 300
|
MAX_LOG_LINES = 300
|
||||||
|
|
||||||
# 支持的进程列表
|
# 支持的进程列表
|
||||||
VALID_PROCESSES = {"tiktok", "youtube", "obs"}
|
VALID_PROCESSES = {"tiktok", "youtube", "obs"}
|
||||||
|
|
||||||
# CORS(支持 POST)
|
# CORS(支持 GET 和 POST)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
@@ -86,7 +86,7 @@ async def get_process_info(process: str) -> dict:
|
|||||||
out_path = value
|
out_path = value
|
||||||
elif key == "err_log_path":
|
elif key == "err_log_path":
|
||||||
err_path = value
|
err_path = value
|
||||||
# 备用默认路径(修正为根目录)
|
# 备用默认路径
|
||||||
if not out_path:
|
if not out_path:
|
||||||
out_path = str(DEFAULT_LOG_DIR / f"{process}-out.log")
|
out_path = str(DEFAULT_LOG_DIR / f"{process}-out.log")
|
||||||
if not err_path:
|
if not err_path:
|
||||||
@@ -97,11 +97,11 @@ async def get_process_info(process: str) -> dict:
|
|||||||
"err_path": err_path,
|
"err_path": err_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------- 配置路由 ----------------
|
# ---------------- 配置路由(youtube.ini,仅 youtube) ----------------
|
||||||
@app.get("/get_config")
|
@app.get("/get_config")
|
||||||
async def get_config(process: str = Query(..., description="进程名")):
|
async def get_config(process: str = Query(..., description="进程名")):
|
||||||
if process != "youtube":
|
if process != "youtube":
|
||||||
return JSONResponse({"error": "仅 youtube 支持配置编辑"}, status_code=400)
|
return JSONResponse({"error": "仅 youtube 支持此配置编辑"}, status_code=400)
|
||||||
|
|
||||||
config_file = CONFIG_DIR / "youtube.ini"
|
config_file = CONFIG_DIR / "youtube.ini"
|
||||||
if not config_file.exists():
|
if not config_file.exists():
|
||||||
@@ -115,7 +115,7 @@ async def get_config(process: str = Query(..., description="进程名")):
|
|||||||
@app.post("/save_config")
|
@app.post("/save_config")
|
||||||
async def save_config(request: Request, process: str = Query(..., description="进程名")):
|
async def save_config(request: Request, process: str = Query(..., description="进程名")):
|
||||||
if process != "youtube":
|
if process != "youtube":
|
||||||
return JSONResponse({"error": "仅 youtube 支持配置编辑"}, status_code=400)
|
return JSONResponse({"error": "仅 youtube 支持此配置编辑"}, status_code=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
@@ -131,7 +131,41 @@ async def save_config(request: Request, process: str = Query(..., description="
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||||
|
|
||||||
# ---------------- 原有路由 ----------------
|
# ---------------- 配置路由(URL_config.ini,youtube 和 tiktok 共享) ----------------
|
||||||
|
@app.get("/get_url_config")
|
||||||
|
async def get_url_config(process: str = Query(..., description="进程名")):
|
||||||
|
if process not in ["youtube", "tiktok"]:
|
||||||
|
return JSONResponse({"error": "仅 youtube 和 tiktok 支持此配置编辑"}, status_code=400)
|
||||||
|
|
||||||
|
config_file = CONFIG_DIR / "URL_config.ini"
|
||||||
|
if not config_file.exists():
|
||||||
|
return {"content": "# URL_config.ini 文件不存在,将创建空文件\n"}
|
||||||
|
try:
|
||||||
|
content = config_file.read_text(encoding="utf-8")
|
||||||
|
return {"content": content}
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse({"error": f"读取失败: {str(e)}"}, status_code=500)
|
||||||
|
|
||||||
|
@app.post("/save_url_config")
|
||||||
|
async def save_url_config(request: Request, process: str = Query(..., description="进程名")):
|
||||||
|
if process not in ["youtube", "tiktok"]:
|
||||||
|
return JSONResponse({"error": "仅 youtube 和 tiktok 支持此配置编辑"}, status_code=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
content = data.get("content", "")
|
||||||
|
except:
|
||||||
|
return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400)
|
||||||
|
|
||||||
|
config_file = CONFIG_DIR / "URL_config.ini"
|
||||||
|
try:
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_file.write_text(content, encoding="utf-8")
|
||||||
|
return {"message": "配置保存成功"}
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||||
|
|
||||||
|
# ---------------- PM2 控制路由 ----------------
|
||||||
@app.get("/start")
|
@app.get("/start")
|
||||||
async def start(process: str = Query(..., description="进程名")):
|
async def start(process: str = Query(..., description="进程名")):
|
||||||
if process not in VALID_PROCESSES:
|
if process not in VALID_PROCESSES:
|
||||||
@@ -177,6 +211,7 @@ async def status(process: str = Query(..., description="进程名")):
|
|||||||
"recent_error": recent_error,
|
"recent_error": recent_error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ---------------- 首页 ----------------
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def index():
|
async def index():
|
||||||
return FileResponse(BASE_DIR / "index.html")
|
return FileResponse(BASE_DIR / "index.html")
|
||||||
|
|||||||
Reference in New Issue
Block a user