From aa24f2f10eec7fe269f3982ff8be78d0b070280c Mon Sep 17 00:00:00 2001 From: grepfs17 Date: Mon, 30 Mar 2026 04:28:11 +0200 Subject: [PATCH] Refactor YouTube API calls to use URLSearchParams for query parameters and improve error handling --- src/components/AppContainer.tsx | 10 +++- src/components/ChatApp.tsx | 10 +++- src/components/YouTubeChat.tsx | 12 +++- src/pages/api/youtube-chat.ts | 26 ++++++--- src/pages/api/youtube-live.ts | 84 +++++++++++++++++++++++++--- src/pages/api/youtube-stream-chat.ts | 30 ++++++++-- 6 files changed, 146 insertions(+), 26 deletions(-) diff --git a/src/components/AppContainer.tsx b/src/components/AppContainer.tsx index 8c46a2f..f01b558 100644 --- a/src/components/AppContainer.tsx +++ b/src/components/AppContainer.tsx @@ -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); diff --git a/src/components/ChatApp.tsx b/src/components/ChatApp.tsx index e3d7eae..247d307 100644 --- a/src/components/ChatApp.tsx +++ b/src/components/ChatApp.tsx @@ -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; diff --git a/src/components/YouTubeChat.tsx b/src/components/YouTubeChat.tsx index ac0c50a..590bf8e 100644 --- a/src/components/YouTubeChat.tsx +++ b/src/components/YouTubeChat.tsx @@ -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(() => { diff --git a/src/pages/api/youtube-chat.ts b/src/pages/api/youtube-chat.ts index 9348fec..9e19761 100644 --- a/src/pages/api/youtube-chat.ts +++ b/src/pages/api/youtube-chat.ts @@ -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 }, ); diff --git a/src/pages/api/youtube-live.ts b/src/pages/api/youtube-live.ts index 9ba093a..66ed6cb 100644 --- a/src/pages/api/youtube-live.ts +++ b/src/pages/api/youtube-live.ts @@ -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( @@ -29,29 +30,98 @@ export const GET: APIRoute = async ({ url }) => { } const yt = await getInnertube(); + + // 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 + } + } - // Use getChannel which handles handles, URLs, and IDs - const channel = await yt.getChannel(channelUrl); + // 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 }, ); diff --git a/src/pages/api/youtube-stream-chat.ts b/src/pages/api/youtube-stream-chat.ts index 14f3b10..e4f87b3 100644 --- a/src/pages/api/youtube-stream-chat.ts +++ b/src/pages/api/youtube-stream-chat.ts @@ -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(); 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): 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 }; }