's'
This commit is contained in:
@@ -6,6 +6,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
buildYoutubeIniFromFields,
|
||||
extractDouyinLiveUrls,
|
||||
managedUrlConfigRelPathForPm2,
|
||||
managedYoutubeIniRelPathForPm2,
|
||||
parseYoutubeIniFields,
|
||||
parseYoutubeStreamKeySuffix,
|
||||
type YoutubeIniFields,
|
||||
@@ -191,11 +193,20 @@ export function LiveYoutubeUnmannedView({
|
||||
);
|
||||
const c = data.content ?? "";
|
||||
if (mode === "single") setUrlConfig(c);
|
||||
else setProUrlDraft(c);
|
||||
else {
|
||||
setProUrlDraft(c);
|
||||
if (activeCh) {
|
||||
setChannels((prev) => {
|
||||
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [currentProc, fetchJson, mode, setUrlConfig]);
|
||||
}, [activeCh, currentProc, fetchJson, mode, setUrlConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc || !urlAutoRefresh) return;
|
||||
@@ -217,7 +228,16 @@ export function LiveYoutubeUnmannedView({
|
||||
);
|
||||
const c = data.content ?? "";
|
||||
if (mode === "single") setUrlConfig(c);
|
||||
else setProUrlDraft(c);
|
||||
else {
|
||||
setProUrlDraft(c);
|
||||
if (activeCh) {
|
||||
setChannels((prev) => {
|
||||
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server"));
|
||||
} catch (e) {
|
||||
setUrlFetchNote((e as Error).message);
|
||||
@@ -284,11 +304,6 @@ export function LiveYoutubeUnmannedView({
|
||||
|
||||
const active = channels.find((c) => c.id === activeCh) || channels[0];
|
||||
|
||||
const proLanesWithKey = useMemo(
|
||||
() => channels.filter((ch) => (ch.streamKey ?? "").trim().length > 0),
|
||||
[channels],
|
||||
);
|
||||
|
||||
const selectProChannel = useCallback(
|
||||
(c: YoutubeProChannel) => {
|
||||
if (active && mode === "pro") {
|
||||
@@ -306,8 +321,34 @@ export function LiveYoutubeUnmannedView({
|
||||
const ch = channels.find((x) => x.id === activeCh);
|
||||
if (!ch) return;
|
||||
const want = (ch.pm2Name ?? "").trim() || defaultPm2NameForChannel(ch.id);
|
||||
if (processOptions.some((e) => e.pm2 === want)) setCurrentProc(want);
|
||||
}, [mode, activeCh, channels, processOptions, setCurrentProc]);
|
||||
setCurrentProc(want);
|
||||
}, [mode, activeCh, channels, setCurrentProc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "pro" || !currentProc || !activeCh) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const data = await fetchJson<{ content?: string }>(
|
||||
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
|
||||
);
|
||||
if (cancelled) return;
|
||||
const c = data.content ?? "";
|
||||
setProUrlDraft(c);
|
||||
urlListDirtyRef.current = false;
|
||||
setChannels((prev) => {
|
||||
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
|
||||
saveYoutubeProChannels(next);
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
if (!cancelled) setProUrlDraft("");
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mode, currentProc, activeCh, fetchJson]);
|
||||
|
||||
const loadYoutubeIni = useCallback(async () => {
|
||||
if (!currentProc) return;
|
||||
@@ -375,7 +416,7 @@ export function LiveYoutubeUnmannedView({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
root: "app-config",
|
||||
path: "youtube.ini",
|
||||
path: managedYoutubeIniRelPathForPm2(currentProc),
|
||||
content: source,
|
||||
action: "youtube_balanced_preset",
|
||||
}),
|
||||
@@ -433,7 +474,7 @@ export function LiveYoutubeUnmannedView({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
root: "app-config",
|
||||
path: "URL_config.ini",
|
||||
path: managedUrlConfigRelPathForPm2(currentProc),
|
||||
content: source,
|
||||
action: "dedupe_urls",
|
||||
}),
|
||||
@@ -507,7 +548,7 @@ export function LiveYoutubeUnmannedView({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<div className="rounded-2xl border border-violet-500/25 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/80 to-cyan-500/5 p-6 shadow-xl shadow-violet-900/10">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -620,9 +661,9 @@ export function LiveYoutubeUnmannedView({
|
||||
{t("Pro 转播实况", "Pro relay status")}
|
||||
</h4>
|
||||
<p className="mt-1 text-[11px] text-zinc-500">
|
||||
{t("对齐桌面端工作台:每路密钥、源站、进程状态一览;点行可切换编辑。", "Like the desktop workspace: per-lane key, source, PM2 status.")}
|
||||
{t("对齐桌面端:每路独立 URL / youtube 配置文件;点左侧信息切换编辑,右侧控制进程。", "Per-lane config files like desktop; click row info to edit, use right-side controls for PM2.")}
|
||||
</p>
|
||||
<div className="mt-3 max-h-52 space-y-2 overflow-y-auto pr-1">
|
||||
<div className="mt-3 max-h-[min(24rem,55vh)] space-y-2 overflow-y-auto pr-1">
|
||||
{channels.map((c) => {
|
||||
const pm = (c.pm2Name ?? "").trim() || defaultPm2NameForChannel(c.id);
|
||||
const st = statusForPm2(pm);
|
||||
@@ -630,10 +671,11 @@ export function LiveYoutubeUnmannedView({
|
||||
const dUrl = extractDouyinLiveUrls((c.urlLines ?? "").trim())[0];
|
||||
const suf = parseYoutubeStreamKeySuffix(`key = ${c.streamKey ?? ""}`);
|
||||
const sel = activeCh === c.id;
|
||||
const rowOnline = pm2StatusOnline(statusForPm2(pm));
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
className={`flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-left text-xs transition ${
|
||||
className={`flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2.5 text-left text-xs transition sm:flex-nowrap ${
|
||||
sel ? "border-violet-500/45 bg-violet-500/[0.1]" : "border-white/10 bg-black/30 hover:border-white/18"
|
||||
}`}
|
||||
>
|
||||
@@ -642,131 +684,92 @@ export function LiveYoutubeUnmannedView({
|
||||
className="min-w-0 flex-1 text-left"
|
||||
onClick={() => selectProChannel(c)}
|
||||
>
|
||||
<span className="font-medium text-zinc-100">{c.name}</span>
|
||||
<span className="ml-2 font-mono text-[10px] text-zinc-500">
|
||||
key …{suf || "—"}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-zinc-100">{c.name}</span>
|
||||
<span
|
||||
className={`shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ring-1 ${
|
||||
on
|
||||
? "bg-emerald-500/15 text-emerald-200 ring-emerald-500/30"
|
||||
: "bg-zinc-600/20 text-zinc-400 ring-zinc-500/25"
|
||||
}`}
|
||||
>
|
||||
{on ? t("运行中", "Live") : st || t("未运行", "Idle")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 font-mono text-[10px] text-zinc-500">
|
||||
<span>
|
||||
key …{suf || "—"}
|
||||
</span>
|
||||
<span className="text-zinc-600">·</span>
|
||||
<span className="truncate" title={pm}>
|
||||
{pm}
|
||||
</span>
|
||||
</div>
|
||||
{dUrl ? (
|
||||
<a
|
||||
href={dUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-1 block max-w-full truncate text-cyan-300/90 underline-offset-2 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={dUrl}
|
||||
>
|
||||
{dUrl.replace(/^https?:\/\//, "")}
|
||||
</a>
|
||||
) : (
|
||||
<span className="mt-1 block text-zinc-600">—</span>
|
||||
)}
|
||||
</button>
|
||||
<span
|
||||
className={`shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ring-1 ${
|
||||
on
|
||||
? "bg-emerald-500/15 text-emerald-200 ring-emerald-500/30"
|
||||
: "bg-zinc-600/20 text-zinc-400 ring-zinc-500/25"
|
||||
}`}
|
||||
<div
|
||||
className="flex w-full shrink-0 flex-wrap items-center justify-end gap-1.5 sm:w-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{on ? t("运行中", "Live") : st || t("未运行", "Idle")}
|
||||
</span>
|
||||
{dUrl ? (
|
||||
<a
|
||||
href={dUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="max-w-[200px] truncate text-cyan-300/90 underline-offset-2 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={dUrl}
|
||||
>
|
||||
{dUrl.replace(/^https?:\/\//, "")}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-zinc-600">—</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-xs text-zinc-500">
|
||||
{t("点选一行即可编辑该线路;下方列表为已保存密钥的线路控制。", "Pick a row above to edit; the list below is lanes with a saved key.")}
|
||||
</p>
|
||||
{proLanesWithKey.length === 0 ? (
|
||||
<p className="mt-4 rounded-xl border border-white/10 bg-black/30 px-4 py-3 text-sm text-zinc-400">
|
||||
{channels.length > 0
|
||||
? t("请先在下方填写串流密钥并点「保存」。", "Enter a stream key below and tap Save.")
|
||||
: t("请先新增线路。", "Add a lane first.")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-4 space-y-2">
|
||||
{proLanesWithKey.map((c) => {
|
||||
const pm = (c.pm2Name ?? "").trim() || defaultPm2NameForChannel(c.id);
|
||||
const suffix = parseYoutubeStreamKeySuffix(`key = ${c.streamKey ?? ""}`);
|
||||
const sel = activeCh === c.id;
|
||||
const rowOnline = pm2StatusOnline(statusForPm2(pm));
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => selectProChannel(c)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
selectProChannel(c);
|
||||
}
|
||||
}}
|
||||
className={`rounded-xl border p-3 text-left transition ${
|
||||
sel ? "border-violet-500/45 bg-violet-500/[0.08]" : "border-white/10 bg-black/30 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex flex-1 flex-wrap items-center gap-2">
|
||||
<code className="shrink-0 font-mono text-xs text-cyan-300/90">{suffix || "—"}</code>
|
||||
<span className="truncate text-sm text-zinc-200">{c.name}</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex shrink-0 flex-wrap gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<LiveStreamActionBtn
|
||||
kind="start"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("开始", "Start")}
|
||||
className="rounded-lg bg-emerald-500/20 text-emerald-100 ring-emerald-500/30"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("start", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
<LiveStreamActionBtn
|
||||
kind="stop"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("停止", "Stop")}
|
||||
className="rounded-lg bg-rose-500/15 text-rose-100 ring-rose-500/25"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("stop", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
<LiveStreamActionBtn
|
||||
kind="restart"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("重新开始", "Restart")}
|
||||
className="rounded-lg bg-amber-500/15 text-amber-100 ring-amber-500/25"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("restart", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<LiveStreamActionBtn
|
||||
kind="start"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("开始", "Start")}
|
||||
className="rounded-lg bg-emerald-500/20 text-emerald-100 ring-emerald-500/30"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("start", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
<LiveStreamActionBtn
|
||||
kind="stop"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("停止", "Stop")}
|
||||
className="rounded-lg bg-rose-500/15 text-rose-100 ring-rose-500/25"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("stop", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
<LiveStreamActionBtn
|
||||
kind="restart"
|
||||
targetPm2={pm}
|
||||
busy={busy}
|
||||
online={rowOnline}
|
||||
label={t("重新开始", "Restart")}
|
||||
className="rounded-lg bg-amber-500/15 text-amber-100 ring-amber-500/25"
|
||||
baseLock={ytIniLoading || urlReloading || urlOptimizing}
|
||||
onRun={() => onRunProcess("restart", pm)}
|
||||
notify={notify}
|
||||
t={t}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-3 text-center font-mono text-[11px] text-zinc-600">
|
||||
{t("当前线路", "Current lane")}: {active?.name ?? "—"} · key {keySuffixUi || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -882,7 +885,19 @@ export function LiveYoutubeUnmannedView({
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold text-white">{t("当前线路", "Current lane")}</h3>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
{t("当前线路", "Current lane")}
|
||||
{active ? (
|
||||
<span className="ml-2 font-normal text-zinc-400">· {active.name}</span>
|
||||
) : null}
|
||||
</h3>
|
||||
{active ? (
|
||||
<p className="mt-0.5 font-mono text-[11px] text-zinc-500">
|
||||
PM2: {(active.pm2Name ?? "").trim() || defaultPm2NameForChannel(active.id)} · key {keySuffixUi || "—"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={lock}
|
||||
@@ -906,21 +921,6 @@ export function LiveYoutubeUnmannedView({
|
||||
{t("新增线路", "Add lane")}
|
||||
</button>
|
||||
</div>
|
||||
<label className="mt-3 block text-xs font-medium text-zinc-500">{t("切换线路", "Switch lane")}</label>
|
||||
<select
|
||||
className="mt-1 w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
|
||||
value={activeCh}
|
||||
onChange={(e) => {
|
||||
const c = channels.find((x) => x.id === e.target.value);
|
||||
if (c) selectProChannel(c);
|
||||
}}
|
||||
>
|
||||
{channels.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{active ? (
|
||||
<div className="mt-4 space-y-3 border-t border-white/5 pt-4">
|
||||
@@ -982,7 +982,12 @@ export function LiveYoutubeUnmannedView({
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
|
||||
<h3 className="text-sm font-semibold text-white">{t("当前线路 · 直播地址列表", "URL list (this lane)")}</h3>
|
||||
<h3 className="text-sm font-semibold text-white">{t("直播地址列表", "URL list")}</h3>
|
||||
<p className="mt-1 font-mono text-[10px] text-zinc-600">
|
||||
{currentProc
|
||||
? managedUrlConfigRelPathForPm2(currentProc)
|
||||
: "URL_config.ini"}
|
||||
</p>
|
||||
{urlToolbar}
|
||||
{urlFetchNote ? <p className="mt-2 text-xs text-zinc-500">{urlFetchNote}</p> : null}
|
||||
<textarea
|
||||
@@ -1021,39 +1026,33 @@ export function LiveYoutubeUnmannedView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
|
||||
<h3 className="text-sm font-semibold text-white">{t("直播地址列表 · URL_config.ini", "URL_config.ini")}</h3>
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
{mode === "pro"
|
||||
? t("Pro 模式在上方「当前线路」里编辑本线路地址。", "Pro: edit this lane’s URLs in the lane panel above.")
|
||||
: t("一行一个地址;可自动刷新或手动重新读取。", "One URL per line; auto-refresh or reload.")}
|
||||
</p>
|
||||
{mode === "single" ? (
|
||||
<>
|
||||
{urlToolbar}
|
||||
{urlFetchNote ? <p className="mt-2 text-xs text-zinc-500">{urlFetchNote}</p> : null}
|
||||
<textarea
|
||||
className="mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300"
|
||||
value={urlConfig}
|
||||
onChange={(e) => {
|
||||
urlListDirtyRef.current = true;
|
||||
setUrlConfig(e.target.value);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={lock}
|
||||
onClick={() => void saveSingleUrlAndClearDirty()}
|
||||
className="mt-3 rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30"
|
||||
>
|
||||
{t("保存 URL 配置", "Save URL config")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-4 text-xs text-zinc-600">{t("Pro 模式:请使用「当前线路」中的地址列表。", "Use lane URL list in Pro mode.")}</p>
|
||||
)}
|
||||
</div>
|
||||
{mode === "single" ? (
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6">
|
||||
<h3 className="text-sm font-semibold text-white">{t("直播地址列表 · URL_config.ini", "URL_config.ini")}</h3>
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
{t("一行一个地址;可自动刷新或手动重新读取。", "One URL per line; auto-refresh or reload.")}
|
||||
</p>
|
||||
{urlToolbar}
|
||||
{urlFetchNote ? <p className="mt-2 text-xs text-zinc-500">{urlFetchNote}</p> : null}
|
||||
<textarea
|
||||
className="mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300"
|
||||
value={urlConfig}
|
||||
onChange={(e) => {
|
||||
urlListDirtyRef.current = true;
|
||||
setUrlConfig(e.target.value);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={lock}
|
||||
onClick={() => void saveSingleUrlAndClearDirty()}
|
||||
className="mt-3 rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30"
|
||||
>
|
||||
{t("保存 URL 配置", "Save URL config")}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<LiveProcessLiveLogCard
|
||||
t={t}
|
||||
|
||||
@@ -116,6 +116,18 @@ export function parseYoutubeIniFields(content: string): YoutubeIniFields {
|
||||
return { key, rtmps, bitrate, fastAudio };
|
||||
}
|
||||
|
||||
/** 与 web.py `managed_url_config_relpath` 一致(app-config 相对路径) */
|
||||
export function managedUrlConfigRelPathForPm2(pm2: string): string {
|
||||
if (!pm2.includes("__")) return "URL_config.ini";
|
||||
return `URL_config.${pm2.replace(/[^a-zA-Z0-9_.-]/g, "_")}.ini`;
|
||||
}
|
||||
|
||||
/** 与 web.py `managed_youtube_ini_relpath` 一致 */
|
||||
export function managedYoutubeIniRelPathForPm2(pm2: string): string {
|
||||
if (!pm2.includes("__")) return "youtube.ini";
|
||||
return `youtube.${pm2.replace(/[^a-zA-Z0-9_.-]/g, "_")}.ini`;
|
||||
}
|
||||
|
||||
export function buildYoutubeIniFromFields(f: YoutubeIniFields): string {
|
||||
const bitrateLine = f.bitrate.trim()
|
||||
? `bitrate = ${f.bitrate.trim()}`
|
||||
|
||||
Reference in New Issue
Block a user