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"; const PM2_SANITIZE_RE = /[^a-zA-Z0-9_.-]+/g; export function sanitizeYoutubePm2Name(value: string): string { return (value || "").trim().replace(PM2_SANITIZE_RE, "_"); } export function resolveYoutubeProChannelPm2(channel: Pick): string { const raw = sanitizeYoutubePm2Name(String(channel.pm2Name || "")); return raw || defaultPm2NameForChannel(String(channel.id || "")); } export function parseYoutubeProChannelList(raw: unknown): YoutubeProChannel[] { if (!Array.isArray(raw)) return []; return raw .map((x) => migrateRow(x as Record)) .filter((c): c is YoutubeProChannel => c !== null); } function migrateRow(x: Record): YoutubeProChannel | null { if (!x || typeof x !== "object") return null; const id = String(x.id || "").trim(); if (!id) return null; const name = String(x.name || "").trim() || id; 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)).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)); } const YOUTUBE_PRO_SYNC_PATHS = ["/hub/youtube_pro_channels", "/youtube_pro_channels"] as const; export type YoutubeProChannelsRemoteResult = { fetched: boolean; channels: YoutubeProChannel[]; }; /** 从服务器拉多频道列表(依次尝试 /hub 与根路径,兼容反代未转发 /hub 的情况) */ export async function fetchYoutubeProChannelsRemote( fetchJson: (path: string, init?: RequestInit) => Promise, ): Promise { for (const path of YOUTUBE_PRO_SYNC_PATHS) { try { const data = await fetchJson<{ channels?: unknown }>(path); return { fetched: true, channels: parseYoutubeProChannelList(data.channels) }; } catch { /* try next */ } } return { fetched: false, channels: [] }; } export async function pushYoutubeProChannelsRemote( fetchJson: (path: string, init?: RequestInit) => Promise, channels: YoutubeProChannel[], ): Promise { let last: Error | null = null; for (const path of YOUTUBE_PRO_SYNC_PATHS) { try { await fetchJson(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ channels }), }); return; } catch (e) { last = e instanceof Error ? e : new Error(String(e)); } } throw last ?? new Error("youtube_pro_channels sync failed"); } 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__${sanitizeYoutubePm2Name(channelId) || "channel"}`; }