136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
/** 치지직 라이브/채널 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<T>(url: string): Promise<T> {
|
|
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<ChannelInfo> {
|
|
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<LiveChatInfo> {
|
|
// 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<string> {
|
|
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`;
|
|
}
|