치지직 채팅창 인식 추가 및 주사위 횟수 증가 추가.
This commit is contained in:
135
lib/chzzk/channel.ts
Normal file
135
lib/chzzk/channel.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/** 치지직 라이브/채널 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user