Refactor YouTube API calls to use URLSearchParams for query parameters and improve error handling
This commit is contained in:
@@ -380,9 +380,13 @@ export default function AppContainer() {
|
||||
let cancelled = false;
|
||||
const fetchId = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/youtube-live?channelUrl=${encodeURIComponent(linkedYoutubeUrl)}`, {
|
||||
headers: sessions.youtube?.accessToken ? { 'Authorization': `Bearer ${sessions.youtube.accessToken}` } : undefined
|
||||
});
|
||||
const params = new URLSearchParams();
|
||||
params.append('channelUrl', linkedYoutubeUrl);
|
||||
if (sessions.youtube?.accessToken) {
|
||||
params.append('accessToken', sessions.youtube.accessToken);
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/youtube-live?${params.toString()}`);
|
||||
const data = await res.json();
|
||||
if (!cancelled && res.ok && data.videoId) {
|
||||
setYoutubeStreamVideoId(data.videoId);
|
||||
|
||||
@@ -130,9 +130,13 @@ export default function ChatApp({
|
||||
// the YouTube iframe to unmount and remount every poll. Only update
|
||||
// `autoVideoId` after we receive a result, and only if it changed.
|
||||
try {
|
||||
const res = await fetch(`/api/youtube-live?channelUrl=${encodeURIComponent(linkedYoutubeUrl)}`, {
|
||||
headers: youtubeAccessToken ? { 'Authorization': `Bearer ${youtubeAccessToken}` } : undefined
|
||||
});
|
||||
const params = new URLSearchParams();
|
||||
params.append('channelUrl', linkedYoutubeUrl);
|
||||
if (youtubeAccessToken) {
|
||||
params.append('accessToken', youtubeAccessToken);
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/youtube-live?${params.toString()}`);
|
||||
const data = await res.json();
|
||||
if (cancelled) return;
|
||||
|
||||
|
||||
@@ -82,7 +82,13 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const res = await fetch(`/api/youtube-stream-chat?videoId=${encodeURIComponent(videoId)}`);
|
||||
const params = new URLSearchParams();
|
||||
params.append('videoId', videoId);
|
||||
if (accessToken) {
|
||||
params.append('accessToken', accessToken);
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/youtube-stream-chat?${params.toString()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
@@ -94,6 +100,8 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
console.log(`[YouTubeChat] Received response: ${data.messageCount} messages, ${(data.emotes || []).length} emotes`);
|
||||
|
||||
if (data.success && data.messages) {
|
||||
const fetchedMessages = (data.messages as any[]).map((msg: any) => ({
|
||||
id: msg.id,
|
||||
@@ -163,7 +171,7 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [videoId, isConnected]);
|
||||
}, [videoId, accessToken, isConnected]);
|
||||
|
||||
// Keep the view pinned to the newest message
|
||||
useEffect(() => {
|
||||
|
||||
@@ -26,11 +26,17 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
// Update session credentials for this request
|
||||
// This allows us to use an existing accessToken from the client
|
||||
(yt.session as any).signIn({
|
||||
access_token: accessToken,
|
||||
refresh_token: '', // Not needed for a one-off send
|
||||
expiry_date: new Date(Date.now() + 3600 * 1000).toISOString()
|
||||
});
|
||||
try {
|
||||
await (yt.session as any).signIn({
|
||||
access_token: accessToken,
|
||||
expiry_date: Date.now() + 3600 * 1000
|
||||
});
|
||||
} catch (tokenError) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Invalid YouTube token: ${tokenError instanceof Error ? tokenError.message : 'Unknown error'}` }),
|
||||
{ status: 401, headers }
|
||||
);
|
||||
}
|
||||
|
||||
const info = await yt.getInfo(videoId);
|
||||
const liveChat = info.getLiveChat();
|
||||
@@ -49,10 +55,16 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("youtube-chat API error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
console.error("youtube-chat API error:", {
|
||||
message: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : "Internal server error",
|
||||
error: errorMessage,
|
||||
details: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
}),
|
||||
{ status: 500, headers },
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
|
||||
try {
|
||||
const channelUrl = url.searchParams.get("channelUrl");
|
||||
const accessToken = url.searchParams.get("accessToken");
|
||||
|
||||
if (!channelUrl) {
|
||||
return new Response(
|
||||
@@ -30,28 +31,97 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
|
||||
const yt = await getInnertube();
|
||||
|
||||
// Use getChannel which handles handles, URLs, and IDs
|
||||
const channel = await yt.getChannel(channelUrl);
|
||||
// If accessToken is provided, use it to authenticate the session
|
||||
if (accessToken) {
|
||||
try {
|
||||
await (yt.session as any).signIn({
|
||||
access_token: accessToken,
|
||||
expiry_date: Date.now() + 3600 * 1000
|
||||
});
|
||||
} catch (tokenError) {
|
||||
console.error('Failed to authenticate with provided token:', tokenError);
|
||||
// Continue without authentication - some channels may be public
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the URL to get the channel ID/endpoint
|
||||
let channelId = channelUrl;
|
||||
console.log(`[YouTube Live] Looking up channel: ${channelUrl}`);
|
||||
if (channelUrl.includes('youtube.com') || channelUrl.includes('youtu.be')) {
|
||||
try {
|
||||
const endpoint = await (yt as any).resolveURL(channelUrl);
|
||||
console.log(`[YouTube Live] Resolved URL to endpoint:`, endpoint?.payload);
|
||||
if (endpoint?.payload?.browseId) {
|
||||
channelId = endpoint.payload.browseId;
|
||||
console.log(`[YouTube Live] Using channel ID: ${channelId}`);
|
||||
}
|
||||
} catch (urlError) {
|
||||
console.warn('Failed to resolve URL, will try direct lookup:', urlError);
|
||||
}
|
||||
}
|
||||
|
||||
// Use getChannel with the resolved ID
|
||||
const channel = await yt.getChannel(channelId);
|
||||
console.log(`[YouTube Live] Retrieved channel info`);
|
||||
|
||||
// Check if the channel is currently live
|
||||
const liveStreams = await (channel as any).getLiveStreams();
|
||||
const videoId = liveStreams.videos && liveStreams.videos.length > 0 ? liveStreams.videos[0].id : null;
|
||||
console.log(`[YouTube Live] getLiveStreams returned:`, {
|
||||
hasVideos: !!liveStreams.videos,
|
||||
videoCount: liveStreams.videos?.length || 0,
|
||||
});
|
||||
let videoId = liveStreams.videos && liveStreams.videos.length > 0 ? liveStreams.videos[0].id : null;
|
||||
|
||||
// If getLiveStreams didn't find anything, try checking the home tab
|
||||
if (!videoId) {
|
||||
console.log(`[YouTube Live] No live streams found via getLiveStreams, trying home feed...`);
|
||||
try {
|
||||
const home = await (channel as any).getHome();
|
||||
if (home?.contents) {
|
||||
// Look for a live stream section in the home feed
|
||||
const sections = home.contents.tabContents?.sectionListRenderer?.contents || [];
|
||||
for (const section of sections) {
|
||||
const items = section.itemSectionRenderer?.contents || [];
|
||||
for (const item of items) {
|
||||
if (item.videoRenderer?.isLiveContent || item.videoRenderer?.badges?.some((b: any) => b.metadataBadgeRenderer?.label === 'LIVE')) {
|
||||
videoId = item.videoRenderer?.videoId;
|
||||
if (videoId) {
|
||||
console.log(`[YouTube Live] Found live video in home feed: ${videoId}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (videoId) break;
|
||||
}
|
||||
}
|
||||
} catch (homeError) {
|
||||
console.warn('[YouTube Live] Failed to check home feed:', homeError);
|
||||
}
|
||||
}
|
||||
console.log(`[YouTube Live] Final videoId: ${videoId}`);
|
||||
|
||||
liveVideoCache.set(channelUrl, { videoId, expires: now + LIVE_CACHE_TTL });
|
||||
|
||||
// Try to get channel ID from metadata or basic_info
|
||||
const channelId = (channel as any).basic_info?.id || (channel as any).header?.author?.id || "";
|
||||
const returnChannelId = (channel as any).basic_info?.id || (channel as any).header?.author?.id || "";
|
||||
|
||||
return new Response(JSON.stringify({ videoId, channelId }), {
|
||||
return new Response(JSON.stringify({ videoId, channelId: returnChannelId }), {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("youtube-live API error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
console.error("youtube-live API error:", {
|
||||
message: errorMessage,
|
||||
stack: errorStack,
|
||||
channelUrl: url.searchParams.get("channelUrl"),
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : "Internal server error",
|
||||
error: errorMessage,
|
||||
details: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
}),
|
||||
{ status: 500, headers },
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ const activeLiveChats = new Map<
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
const videoId = url.searchParams.get('videoId');
|
||||
const accessToken = url.searchParams.get('accessToken');
|
||||
|
||||
if (!videoId) {
|
||||
return new Response(
|
||||
@@ -66,6 +67,7 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
const emoteMap = new Map<string, YTEmoteEntry>();
|
||||
|
||||
chat.on('chat-update', (action: any) => {
|
||||
console.log(`[YouTube Chat] Received chat-update action:`, action.type);
|
||||
if (action.type === 'AddChatItemAction') {
|
||||
const item = action.item;
|
||||
|
||||
@@ -73,7 +75,7 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
|
||||
let chatMsg: ChatMessage | null = null;
|
||||
|
||||
if (item.type === 'LiveChatMessage') {
|
||||
if (item.type === 'LiveChatMessage' || item.type === 'LiveChatTextMessage') {
|
||||
chatMsg = parseLiveChatMessage(item, emoteMap);
|
||||
} else if (item.type === 'LiveChatPaidMessageItem') {
|
||||
chatMsg = parsePaidMessage(item, emoteMap);
|
||||
@@ -100,6 +102,7 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
console.log(`[YouTube Chat] Starting live chat for video ${videoId}`);
|
||||
await chat.start();
|
||||
|
||||
// Populate emote map with initial channel emojis
|
||||
@@ -130,6 +133,10 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
|
||||
chatSession.lastUpdate = Date.now();
|
||||
|
||||
if (chatSession.messages.length > 0) {
|
||||
console.log(`[YouTube Chat] Returning ${chatSession.messages.length} messages for video ${videoId}`);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
@@ -147,10 +154,13 @@ export const GET: APIRoute = async ({ url }) => {
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('YouTube stream chat API error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
activeLiveChats.delete(videoId);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
error: errorMessage,
|
||||
details: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
}),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
@@ -197,13 +207,25 @@ function parseLiveChatMessage(msg: any, emoteMap: Map<string, YTEmoteEntry>): Ch
|
||||
}
|
||||
}
|
||||
|
||||
// YouTube timestamps might already be in milliseconds
|
||||
// Test both formats to see which one matches server time
|
||||
const rawTimestamp = parseInt(msg.timestamp);
|
||||
const asMillisecondsDirectly = new Date(rawTimestamp);
|
||||
const asUsToMs = new Date(rawTimestamp / 1000);
|
||||
|
||||
// Use the one that seems more reasonable (closer to now)
|
||||
const now = new Date();
|
||||
const msTimeDiff = Math.abs(now.getTime() - asMillisecondsDirectly.getTime());
|
||||
const usTimeDiff = Math.abs(now.getTime() - asUsToMs.getTime());
|
||||
const timestamp = msTimeDiff < usTimeDiff ? asMillisecondsDirectly : asUsToMs;
|
||||
|
||||
return {
|
||||
id: msg.id,
|
||||
author: msg.author.name.toString(),
|
||||
authorAvatar: msg.author.thumbnails?.[0]?.url,
|
||||
badges,
|
||||
parts,
|
||||
timestamp: new Date(parseInt(msg.timestamp) / 1000)
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user