's'
This commit is contained in:
105
web-console/lib/youtubeConfigUtils.ts
Normal file
105
web-console/lib/youtubeConfigUtils.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/** 对齐 d2yexeapp electron `youtubeConfigUtils.ts` 的浏览器侧解析(无 OAuth) */
|
||||
|
||||
export function parseYoutubeStreamKeySuffix(iniOrKeyLine: string): string {
|
||||
for (const line of iniOrKeyLine.split(/\r?\n/)) {
|
||||
const t = line.trim();
|
||||
if (t.startsWith("#") || !t) continue;
|
||||
const m = t.match(/^(?:key|stream_key)\s*=\s*(.+)$/i);
|
||||
if (m) {
|
||||
const v = m[1].trim().split(/[#;]/)[0].trim();
|
||||
if (v && !v.toLowerCase().startsWith("rtmp")) return v.length > 8 ? v.slice(-8) : v;
|
||||
if (v.includes("live2/")) {
|
||||
const parts = v.split("/");
|
||||
const last = parts[parts.length - 1] || "";
|
||||
return last.length > 8 ? last.slice(-8) : last;
|
||||
}
|
||||
}
|
||||
}
|
||||
const one = iniOrKeyLine.trim();
|
||||
if (one && !one.includes("\n") && !one.toLowerCase().startsWith("rtmp"))
|
||||
return one.length > 8 ? one.slice(-8) : one;
|
||||
return "";
|
||||
}
|
||||
|
||||
export function extractDouyinLiveUrls(urlIni: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const line of urlIni.split(/\r?\n/)) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith("#")) continue;
|
||||
if (!/douyin\.com|v\.douyin\.com/i.test(t)) continue;
|
||||
const full = t.match(/(https?:\/\/live\.douyin\.com\/[a-zA-Z0-9_-]+)/i);
|
||||
if (full) {
|
||||
out.push(full[1].replace(/^http:\/\//i, "https://"));
|
||||
continue;
|
||||
}
|
||||
const rel = t.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);
|
||||
if (rel) {
|
||||
out.push(`https://live.douyin.com/${rel[1]}`);
|
||||
continue;
|
||||
}
|
||||
const n = t.match(/(\d{8,})/);
|
||||
if (n) out.push(`https://live.douyin.com/${n[1]}`);
|
||||
}
|
||||
return Array.from(new Set(out)).slice(0, 16);
|
||||
}
|
||||
|
||||
export type YoutubeIniFields = {
|
||||
key: string;
|
||||
rtmps: boolean;
|
||||
bitrate: string;
|
||||
fastAudio: boolean;
|
||||
};
|
||||
|
||||
export function parseYoutubeIniFields(content: string): YoutubeIniFields {
|
||||
let key = "";
|
||||
let rtmps = true;
|
||||
let bitrate = "";
|
||||
let fastAudio = false;
|
||||
const lines = content.split(/\r?\n/);
|
||||
let inYoutube = false;
|
||||
for (const line of lines) {
|
||||
const t = line.trim();
|
||||
if (/^\[youtube\]/i.test(t)) {
|
||||
inYoutube = true;
|
||||
continue;
|
||||
}
|
||||
if (/^\[/.test(t)) {
|
||||
inYoutube = false;
|
||||
continue;
|
||||
}
|
||||
if (!inYoutube) continue;
|
||||
if (!t || t.startsWith("#") || t.startsWith(";")) continue;
|
||||
const eq = t.indexOf("=");
|
||||
if (eq < 0) continue;
|
||||
const k = t.slice(0, eq).trim().toLowerCase();
|
||||
const v = t
|
||||
.slice(eq + 1)
|
||||
.trim()
|
||||
.split(/[#;]/)[0]
|
||||
.trim();
|
||||
if (k === "key" || k === "stream_key") key = v;
|
||||
else if (k === "rtmps") rtmps = !/^(0|false|否)/i.test(v);
|
||||
else if (k === "bitrate") bitrate = v;
|
||||
else if (k === "fast_audio") fastAudio = /^(1|true|yes|是|开|on)/i.test(v);
|
||||
}
|
||||
return { key, rtmps, bitrate, fastAudio };
|
||||
}
|
||||
|
||||
export function buildYoutubeIniFromFields(f: YoutubeIniFields): string {
|
||||
const bitrateLine = f.bitrate.trim()
|
||||
? `bitrate = ${f.bitrate.trim()}`
|
||||
: "# bitrate = 4000";
|
||||
return `[youtube]
|
||||
# YouTube 推流 key(工作室 → 直播 → 串流密钥)
|
||||
key = ${f.key.trim()}
|
||||
|
||||
# 使用 RTMPS;填 否 则 RTMP
|
||||
rtmps = ${f.rtmps ? "是" : "否"}
|
||||
|
||||
# 视频比特率 kbps(可选)
|
||||
${bitrateLine}
|
||||
|
||||
# 轻量音频链,减轻 CPU:1 / 是
|
||||
fast_audio = ${f.fastAudio ? "1" : "0"}
|
||||
`;
|
||||
}
|
||||
71
web-console/lib/youtubeProChannels.ts
Normal file
71
web-console/lib/youtubeProChannels.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export type YoutubeProChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
/** 备忘尾码(旧版) */
|
||||
streamKeySuffix?: string;
|
||||
/** 完整串流密钥或 rtmp(s) URL */
|
||||
streamKey?: string;
|
||||
/** 本线路 URL_config 草稿(多路本地编排) */
|
||||
urlLines?: string;
|
||||
/** 建议 PM2 名,如 youtube2__xxx */
|
||||
pm2Name?: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "live-hub-youtube-pro-v2";
|
||||
|
||||
function migrateRow(x: Record<string, unknown>): YoutubeProChannel | null {
|
||||
if (!x || typeof x !== "object") return null;
|
||||
const id = String(x.id || "").trim();
|
||||
const name = String(x.name || "").trim();
|
||||
if (!id || !name) return null;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
streamKeySuffix: typeof x.streamKeySuffix === "string" ? x.streamKeySuffix : undefined,
|
||||
streamKey: typeof x.streamKey === "string" ? x.streamKey : undefined,
|
||||
urlLines: typeof x.urlLines === "string" ? x.urlLines : undefined,
|
||||
pm2Name: typeof x.pm2Name === "string" ? x.pm2Name : undefined,
|
||||
notes: typeof x.notes === "string" ? x.notes : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function safeParse(raw: string | null): YoutubeProChannel[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const v = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(v)) return [];
|
||||
return v.map((x) => migrateRow(x as Record<string, unknown>)).filter((c): c is YoutubeProChannel => c !== null);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function loadYoutubeProChannels(): YoutubeProChannel[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
let raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
raw = window.localStorage.getItem("live-hub-youtube-pro-v1");
|
||||
if (raw) {
|
||||
const legacy = safeParse(raw);
|
||||
if (legacy.length) {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(legacy));
|
||||
return legacy;
|
||||
}
|
||||
}
|
||||
}
|
||||
return safeParse(raw);
|
||||
}
|
||||
|
||||
export function saveYoutubeProChannels(channels: YoutubeProChannel[]): void {
|
||||
if (typeof window === "undefined") return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(channels));
|
||||
}
|
||||
|
||||
export function newChannelId(): string {
|
||||
return `ch_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export function defaultPm2NameForChannel(channelId: string): string {
|
||||
return `youtube2__${channelId}`;
|
||||
}
|
||||
Reference in New Issue
Block a user