/** 치지직 라이브/채널 URL에서 channelId 추출 */ export function parseChzzkChannelId(input: string): string | null { const raw = input.trim(); if (!raw) return null; // 해시만 붙여 넣은 경우 if (/^[a-f0-9]{32}$/i.test(raw)) return raw.toLowerCase(); try { const url = new URL(raw.startsWith('http') ? raw : `https://${raw}`); if (!url.hostname.includes('chzzk.naver.com')) return null; const parts = url.pathname.split('/').filter(Boolean); // /live/{id} | /{id} | /channel/{id} const liveIdx = parts.indexOf('live'); if (liveIdx >= 0 && parts[liveIdx + 1]) { const id = parts[liveIdx + 1]; return /^[a-f0-9]{32}$/i.test(id) ? id.toLowerCase() : null; } const channelIdx = parts.indexOf('channel'); if (channelIdx >= 0 && parts[channelIdx + 1]) { const id = parts[channelIdx + 1]; return /^[a-f0-9]{32}$/i.test(id) ? id.toLowerCase() : null; } if (parts[0] && /^[a-f0-9]{32}$/i.test(parts[0])) { return parts[0].toLowerCase(); } } catch { return null; } return null; } const CHZZK_API = 'https://api.chzzk.naver.com'; const GAME_API = 'https://comm-api.game.naver.com/nng_main'; const DEFAULT_HEADERS = { Accept: 'application/json', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', Origin: 'https://chzzk.naver.com', Referer: 'https://chzzk.naver.com/', }; async function getJson(url: string): Promise { const res = await fetch(url, { headers: DEFAULT_HEADERS, cache: 'no-store', }); if (!res.ok) { throw new Error(`치지직 API 오류 (${res.status})`); } return (await res.json()) as T; } export type ChannelInfo = { channelId: string; channelName: string | null; }; export async function fetchChannelInfo(channelId: string): Promise { const data = await getJson<{ content?: { channelId?: string; channelName?: string }; }>(`${CHZZK_API}/service/v1/channels/${channelId}`); return { channelId: data.content?.channelId || channelId, channelName: data.content?.channelName ?? null, }; } export type LiveChatInfo = { chatChannelId: string; liveTitle: string | null; status: string | null; }; /** 방송 중일 때 chatChannelId를 얻는다. */ export async function fetchLiveChatInfo(channelId: string): Promise { // polling live-status 우선 try { const status = await getJson<{ content?: { chatChannelId?: string; status?: string; liveTitle?: string }; }>(`${CHZZK_API}/polling/v2/channels/${channelId}/live-status`); if (status.content?.chatChannelId) { return { chatChannelId: status.content.chatChannelId, liveTitle: status.content.liveTitle ?? null, status: status.content.status ?? null, }; } } catch { // fallback below } const detail = await getJson<{ content?: { chatChannelId?: string; liveTitle?: string; status?: string }; }>(`${CHZZK_API}/service/v3/channels/${channelId}/live-detail`); if (!detail.content?.chatChannelId) { throw new Error('방송이 꺼져 있거나 chatChannelId를 찾지 못했습니다.'); } return { chatChannelId: detail.content.chatChannelId, liveTitle: detail.content.liveTitle ?? null, status: detail.content.status ?? null, }; } export async function fetchChatAccessToken(chatChannelId: string): Promise { const data = await getJson<{ code?: number; message?: string; content?: { accessToken?: string }; }>( `${GAME_API}/v1/chats/access-token?channelId=${encodeURIComponent(chatChannelId)}&chatType=STREAMING`, ); if (data.code === 42601) { throw new Error('연령 제한 방송이라 익명 채팅 토큰을 받을 수 없습니다.'); } const token = data.content?.accessToken; if (!token) { throw new Error(data.message || '채팅 accessToken을 받지 못했습니다.'); } return token; } export function chatServerUrl(chatChannelId: string) { const sum = chatChannelId .split('') .map((c) => c.charCodeAt(0)) .reduce((a, b) => a + b, 0); const serverId = (Math.abs(sum) % 9) + 1; return `wss://kr-ss${serverId}.chat.naver.com/chat`; }