117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
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";
|
|
|
|
export function parseYoutubeProChannelList(raw: unknown): YoutubeProChannel[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw
|
|
.map((x) => migrateRow(x as Record<string, unknown>))
|
|
.filter((c): c is YoutubeProChannel => c !== null);
|
|
}
|
|
|
|
function migrateRow(x: Record<string, unknown>): 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<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));
|
|
}
|
|
|
|
const YOUTUBE_PRO_SYNC_PATHS = ["/hub/youtube_pro_channels", "/youtube_pro_channels"] as const;
|
|
|
|
/** 从服务器拉多频道列表(依次尝试 /hub 与根路径,兼容反代未转发 /hub 的情况) */
|
|
export async function fetchYoutubeProChannelsRemote(
|
|
fetchJson: <T,>(path: string, init?: RequestInit) => Promise<T>,
|
|
): Promise<YoutubeProChannel[]> {
|
|
for (const path of YOUTUBE_PRO_SYNC_PATHS) {
|
|
try {
|
|
const data = await fetchJson<{ channels?: unknown }>(path);
|
|
const fromServer = parseYoutubeProChannelList(data.channels);
|
|
if (fromServer.length) return fromServer;
|
|
} catch {
|
|
/* try next */
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export async function pushYoutubeProChannelsRemote(
|
|
fetchJson: <T,>(path: string, init?: RequestInit) => Promise<T>,
|
|
channels: YoutubeProChannel[],
|
|
): Promise<void> {
|
|
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__${channelId}`;
|
|
}
|