/** 对齐 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); } /** 从进程日志中解析「当前」抖音房间号(取文本中最后一次出现的 live.douyin.com/xxx) */ export function extractDouyinCurrentRoomFromLog(logText: string): string | null { if (!logText) return null; let last: string | null = null; for (const line of logText.split(/\r?\n/)) { const m = line.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i); if (m) last = m[1]; const m2 = line.match(/https?:\/\/[^/\s]*douyin\.com\/(\d{8,})/i); if (m2) last = m2[1]; } return last; } /** URL_config 文本中提取抖音房间线索(与 d2y electron 一致) */ export function extractDouyinRoomHints(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 m = t.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i); if (m) { out.push(m[1]); continue; } const n = t.match(/(\d{8,})/); out.push(n ? n[1] : t.slice(0, 96)); } return Array.from(new Set(out)).slice(0, 12); } 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 }; } /** 与 web.py `managed_url_config_relpath` 一致(每进程独立,避免 YouTube / TikTok 共写同一文件) */ export function managedUrlConfigRelPathForPm2(pm2: string): string { return `URL_config.${pm2.replace(/[^a-zA-Z0-9_.-]/g, "_")}.ini`; } /** 与 web.py `managed_youtube_ini_relpath` 一致 */ export function managedYoutubeIniRelPathForPm2(pm2: string): string { 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()}` : "# 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"} `; }