147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
/** 对齐 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"}
|
||
`;
|
||
}
|