s
This commit is contained in:
@@ -776,6 +776,7 @@ ensure_runtime_dirs_owned_by_live() {
|
||||
backup_config \
|
||||
.pm2 \
|
||||
.process_runner \
|
||||
web-console \
|
||||
web-console/out \
|
||||
web-console/.next \
|
||||
web-console/node_modules \
|
||||
@@ -784,6 +785,7 @@ ensure_runtime_dirs_owned_by_live() {
|
||||
path="$ROOT_DIR/$rel"
|
||||
mkdir -p "$path"
|
||||
chown -R live:live "$path" 2>/dev/null || true
|
||||
chmod -R u+rwX,g+rwX "$path" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
@@ -952,11 +954,14 @@ StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
User=live
|
||||
PermissionsStartOnly=true
|
||||
WorkingDirectory=$ROOT_DIR
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=PORT=$PORT
|
||||
Environment=LIVE_NOFILE_SOFT=$LIVE_NOFILE_SOFT
|
||||
LimitNOFILE=$LIVE_NOFILE_SOFT
|
||||
ExecStartPre=/usr/bin/install -d -o live -g live $ROOT_DIR/config $ROOT_DIR/logs $ROOT_DIR/downloads $ROOT_DIR/backup $ROOT_DIR/backup_config $ROOT_DIR/.pm2 $ROOT_DIR/.process_runner $ROOT_DIR/web-console $ROOT_DIR/services/neko/data
|
||||
ExecStartPre=/bin/chown -R live:live $ROOT_DIR/config $ROOT_DIR/logs $ROOT_DIR/downloads $ROOT_DIR/backup $ROOT_DIR/backup_config $ROOT_DIR/.pm2 $ROOT_DIR/.process_runner $ROOT_DIR/web-console $ROOT_DIR/services/neko/data
|
||||
ExecStart=$ROOT_DIR/start.sh
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
@@ -276,6 +277,11 @@ async def hub_youtube_pro_channels_write(body: YoutubeProChannelsBody) -> dict[s
|
||||
set_youtube_pro_channels(body.channels)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"多频道状态数据库写入失败: {exc}; 请确认 config 目录归 live 用户可写",
|
||||
) from exc
|
||||
capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, _repo_root, body.channels)
|
||||
return {"message": "ok", "neko": capacity}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import ast
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -19,6 +20,15 @@ class _JsonRequest:
|
||||
return self._payload
|
||||
|
||||
|
||||
class _ReadRequest:
|
||||
headers: dict[str, str] = {}
|
||||
|
||||
class _Client:
|
||||
host = "127.0.0.1"
|
||||
|
||||
client = _Client()
|
||||
|
||||
|
||||
def _json_body(resp) -> dict[str, object]: # noqa: ANN001
|
||||
return json.loads(resp.body)
|
||||
|
||||
@@ -180,7 +190,7 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
with patch("web.allows_url_config", return_value=True):
|
||||
with patch("web.managed_url_config_relpath", return_value=rel):
|
||||
with patch("web._sync_youtube_pro_runtime_files", side_effect=AssertionError("unexpected resync")):
|
||||
payload = asyncio.run(web.get_url_config("youtube"))
|
||||
payload = asyncio.run(web.get_url_config(_ReadRequest(), process="youtube"))
|
||||
|
||||
self.assertEqual(_json_body(payload)["content"], "https://live.douyin.com/new\n")
|
||||
self.assertEqual(payload.headers.get("cache-control"), "no-store, max-age=0, must-revalidate")
|
||||
@@ -204,6 +214,26 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
|
||||
|
||||
def test_save_url_config_returns_json_when_channel_store_readonly(self) -> None:
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "urlLines": "old\n"}]
|
||||
|
||||
with patch("web.allows_url_config", return_value=True):
|
||||
with patch("web.managed_url_config_relpath", return_value="URL_config.youtube2__lane1.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=sqlite3.OperationalError("attempt to write a readonly database")):
|
||||
payload = asyncio.run(
|
||||
web.save_url_config(
|
||||
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
|
||||
process="youtube2__lane1",
|
||||
)
|
||||
)
|
||||
|
||||
body = _json_body(payload)
|
||||
self.assertEqual(payload.status_code, 500)
|
||||
self.assertIn("多频道状态数据库写入失败", body["error"])
|
||||
self.assertIn("config", body["error"])
|
||||
|
||||
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "streamKey": "old-key"}]
|
||||
|
||||
@@ -370,6 +370,7 @@ export function LiveYoutubeUnmannedView({
|
||||
const urlListDirtyRef = useRef(false);
|
||||
const singleUrlDraftRef = useRef(urlConfig);
|
||||
const proUrlDraftRef = useRef(proUrlDraft);
|
||||
const channelsRef = useRef<YoutubeProChannel[]>([]);
|
||||
const currentProcRef = useRef(currentProc);
|
||||
const urlRequestSeqRef = useRef(0);
|
||||
const ytIniRequestSeqRef = useRef(0);
|
||||
@@ -403,6 +404,27 @@ export function LiveYoutubeUnmannedView({
|
||||
proUrlDraftRef.current = proUrlDraft;
|
||||
}, [proUrlDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
channelsRef.current = channels;
|
||||
}, [channels]);
|
||||
|
||||
const updateActiveChannelUrlLines = useCallback((channelId: string, nextUrlLines: string) => {
|
||||
if (!channelId) return;
|
||||
setChannels((prev) => {
|
||||
let changed = false;
|
||||
const next = prev.map((row) => {
|
||||
if (row.id !== channelId) return row;
|
||||
if ((row.urlLines ?? "") === nextUrlLines) return row;
|
||||
changed = true;
|
||||
return { ...row, urlLines: nextUrlLines };
|
||||
});
|
||||
if (!changed) return prev;
|
||||
channelsRef.current = next;
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const pullUrlConfigFromServer = useCallback(async () => {
|
||||
const targetProc = currentProcRef.current.trim();
|
||||
if (!targetProc) return;
|
||||
@@ -418,24 +440,18 @@ export function LiveYoutubeUnmannedView({
|
||||
if (urlListDirtyRef.current) return;
|
||||
if (mode === "single") applySingleUrlDraft(c);
|
||||
else {
|
||||
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
|
||||
const localDraft = channelsRef.current.find((row) => row.id === activeCh)?.urlLines ?? "";
|
||||
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
|
||||
applyProUrlDraft(localDraft);
|
||||
return;
|
||||
}
|
||||
applyProUrlDraft(c);
|
||||
if (activeCh) {
|
||||
setChannels((prev) => {
|
||||
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
updateActiveChannelUrlLines(activeCh, c);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [activeCh, applyProUrlDraft, applySingleUrlDraft, channels, fetchJson, mode]);
|
||||
}, [activeCh, applyProUrlDraft, applySingleUrlDraft, fetchJson, mode, updateActiveChannelUrlLines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc || !urlAutoRefresh) return;
|
||||
@@ -467,27 +483,21 @@ export function LiveYoutubeUnmannedView({
|
||||
}
|
||||
if (mode === "single") applySingleUrlDraft(c);
|
||||
else {
|
||||
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
|
||||
const localDraft = channelsRef.current.find((row) => row.id === activeCh)?.urlLines ?? "";
|
||||
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
|
||||
applyProUrlDraft(localDraft);
|
||||
setUrlFetchNote(t("服务器暂无已保存 URL,已保留本地草稿", "Server has no saved URLs, kept the local draft"));
|
||||
return;
|
||||
}
|
||||
applyProUrlDraft(c);
|
||||
if (activeCh) {
|
||||
setChannels((prev) => {
|
||||
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
updateActiveChannelUrlLines(activeCh, c);
|
||||
}
|
||||
setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server"));
|
||||
} catch (e) {
|
||||
if (requestId !== urlRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
|
||||
setUrlFetchNote((e as Error).message);
|
||||
} finally {
|
||||
if (requestId === urlRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
|
||||
if (requestId === urlRequestSeqRef.current) {
|
||||
setUrlReloading(false);
|
||||
}
|
||||
}
|
||||
@@ -512,6 +522,7 @@ export function LiveYoutubeUnmannedView({
|
||||
try {
|
||||
const remote = await fetchYoutubeProChannelsRemote(fetchJson);
|
||||
if (remote.fetched && remote.channels.length && !cancelled) {
|
||||
channelsRef.current = remote.channels;
|
||||
setChannels(remote.channels);
|
||||
saveYoutubeProChannels(remote.channels);
|
||||
return;
|
||||
@@ -537,6 +548,7 @@ export function LiveYoutubeUnmannedView({
|
||||
},
|
||||
];
|
||||
})();
|
||||
channelsRef.current = initial;
|
||||
setChannels(initial);
|
||||
if (!list.length) saveYoutubeProChannels(initial);
|
||||
})();
|
||||
@@ -574,6 +586,7 @@ export function LiveYoutubeUnmannedView({
|
||||
}, [currentProc, currentProcLooksYoutube, mode, preferredSingleProc, setCurrentProc]);
|
||||
|
||||
const applyChannelsLocal = useCallback((next: YoutubeProChannel[]) => {
|
||||
channelsRef.current = next;
|
||||
setChannels(next);
|
||||
saveYoutubeProChannels(next);
|
||||
}, []);
|
||||
@@ -786,7 +799,7 @@ export function LiveYoutubeUnmannedView({
|
||||
if (requestId !== ytIniRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
|
||||
setYtIniStatus((e as Error).message);
|
||||
} finally {
|
||||
if (requestId === ytIniRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
|
||||
if (requestId === ytIniRequestSeqRef.current) {
|
||||
setYtIniLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"built_at": 1779182151.9524395,
|
||||
"source_mtime": 1779180451.7832174,
|
||||
"built_at": 1779184553.6568675,
|
||||
"source_mtime": 1779183169.5914183,
|
||||
"toolchain": "local",
|
||||
"node_major": 20
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,6 +2,6 @@
|
||||
3:I[3946,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","357","static/chunks/app/android/page-84550dabda18d658.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,6 @@
|
||||
3:I[6367,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","443","static/chunks/app/console/page-75d851a154677dbc.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-3c5cf076d3a5e7c5.js"],"default",1]
|
||||
3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-a84b3c1d3686233c.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,6 @@
|
||||
3:I[3398,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","6","static/chunks/app/shellcrash/page-3d8615ae919b9119.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
|
||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
|
||||
@@ -6,7 +6,9 @@ import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
@@ -362,6 +364,10 @@ def _persist_youtube_pro_channel_edits(
|
||||
return updated
|
||||
|
||||
|
||||
def _youtube_pro_store_error(exc: sqlite3.Error) -> str:
|
||||
return f"多频道状态数据库写入失败: {exc}; 请确认 config 目录归 live 用户可写"
|
||||
|
||||
|
||||
def _validate_youtube_runtime_files(process: str) -> list[str]:
|
||||
return validate_youtube_runtime_files(BASE_DIR, CONFIG_DIR, process, get_youtube_pro_channels())
|
||||
|
||||
@@ -553,6 +559,8 @@ async def alias_youtube_pro_channels_post(body: YoutubeProChannelsBody):
|
||||
set_youtube_pro_channels(body.channels)
|
||||
except ValueError as exc:
|
||||
return JSONResponse({"error": str(exc)}, status_code=400)
|
||||
except sqlite3.Error as exc:
|
||||
return JSONResponse({"error": _youtube_pro_store_error(exc)}, status_code=500)
|
||||
capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, BASE_DIR, body.channels)
|
||||
return {"message": "ok", "neko": capacity}
|
||||
|
||||
@@ -570,6 +578,35 @@ _NO_STORE_HEADERS = {
|
||||
"Expires": "0",
|
||||
}
|
||||
|
||||
_READ_CONFIG_MIN_INTERVAL_SECONDS = 0.45
|
||||
_READ_CONFIG_LAST_SEEN: dict[tuple[str, str, str], float] = {}
|
||||
|
||||
|
||||
def _client_host(request: Request) -> str:
|
||||
forwarded = (request.headers.get("x-forwarded-for") or "").split(",", 1)[0].strip()
|
||||
if forwarded:
|
||||
return forwarded
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
def _reject_config_read_storm(request: Request, endpoint: str, process: str) -> JSONResponse | None:
|
||||
now = time.monotonic()
|
||||
if len(_READ_CONFIG_LAST_SEEN) > 2048:
|
||||
cutoff = now - 30.0
|
||||
for key, seen_at in list(_READ_CONFIG_LAST_SEEN.items()):
|
||||
if seen_at < cutoff:
|
||||
_READ_CONFIG_LAST_SEEN.pop(key, None)
|
||||
key = (_client_host(request), endpoint, (process or "").strip())
|
||||
last = _READ_CONFIG_LAST_SEEN.get(key, 0.0)
|
||||
_READ_CONFIG_LAST_SEEN[key] = now
|
||||
if now - last >= _READ_CONFIG_MIN_INTERVAL_SECONDS:
|
||||
return None
|
||||
return JSONResponse(
|
||||
{"error": "刷新过快,请稍后重试"},
|
||||
status_code=429,
|
||||
headers={**_NO_STORE_HEADERS, "Retry-After": "1"},
|
||||
)
|
||||
|
||||
|
||||
class ProcessNameBody(BaseModel):
|
||||
process: str = Field(..., min_length=1, max_length=160)
|
||||
@@ -1112,7 +1149,10 @@ async def get_system_snapshot():
|
||||
|
||||
# ---------------- 配置路由(youtube.ini,仅 youtube) ----------------
|
||||
@app.get("/get_config")
|
||||
async def get_config(process: str = Query(..., description="进程名")):
|
||||
async def get_config(request: Request, process: str = Query(..., description="进程名")):
|
||||
storm = _reject_config_read_storm(request, "get_config", process)
|
||||
if storm is not None:
|
||||
return storm
|
||||
rel = managed_youtube_ini_relpath(process)
|
||||
if not rel:
|
||||
return JSONResponse(
|
||||
@@ -1156,13 +1196,18 @@ async def save_config(request: Request, process: str = Query(..., description="
|
||||
governed_save_managed_file("app-config", rel, content)
|
||||
_persist_youtube_pro_channel_edits(process, youtube_ini_content=content)
|
||||
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
|
||||
except sqlite3.Error as e:
|
||||
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
|
||||
except OSError as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
|
||||
# ---------------- 配置路由(URL_config.ini) ----------------
|
||||
@app.get("/get_url_config")
|
||||
async def get_url_config(process: str = Query(..., description="进程名")):
|
||||
async def get_url_config(request: Request, process: str = Query(..., description="进程名")):
|
||||
storm = _reject_config_read_storm(request, "get_url_config", process)
|
||||
if storm is not None:
|
||||
return storm
|
||||
if not allows_url_config(process):
|
||||
return JSONResponse(
|
||||
{"error": "仅 tiktok*.py / youtube*.py / douyin_youtube*.py 对应进程支持 URL 配置编辑"},
|
||||
@@ -1210,6 +1255,8 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
|
||||
governed_save_managed_file("app-config", rel, content)
|
||||
_persist_youtube_pro_channel_edits(process, url_content=content)
|
||||
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
|
||||
except sqlite3.Error as e:
|
||||
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
|
||||
except OSError as e:
|
||||
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user