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;
|
let cancelled = false;
|
||||||
const fetchId = async () => {
|
const fetchId = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/youtube-live?channelUrl=${encodeURIComponent(linkedYoutubeUrl)}`, {
|
const params = new URLSearchParams();
|
||||||
headers: sessions.youtube?.accessToken ? { 'Authorization': `Bearer ${sessions.youtube.accessToken}` } : undefined
|
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();
|
const data = await res.json();
|
||||||
if (!cancelled && res.ok && data.videoId) {
|
if (!cancelled && res.ok && data.videoId) {
|
||||||
setYoutubeStreamVideoId(data.videoId);
|
setYoutubeStreamVideoId(data.videoId);
|
||||||
|
|||||||
@@ -130,9 +130,13 @@ export default function ChatApp({
|
|||||||
// the YouTube iframe to unmount and remount every poll. Only update
|
// the YouTube iframe to unmount and remount every poll. Only update
|
||||||
// `autoVideoId` after we receive a result, and only if it changed.
|
// `autoVideoId` after we receive a result, and only if it changed.
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/youtube-live?channelUrl=${encodeURIComponent(linkedYoutubeUrl)}`, {
|
const params = new URLSearchParams();
|
||||||
headers: youtubeAccessToken ? { 'Authorization': `Bearer ${youtubeAccessToken}` } : undefined
|
params.append('channelUrl', linkedYoutubeUrl);
|
||||||
});
|
if (youtubeAccessToken) {
|
||||||
|
params.append('accessToken', youtubeAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`/api/youtube-live?${params.toString()}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,13 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
|||||||
try {
|
try {
|
||||||
setError(null);
|
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) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => ({}));
|
const errorData = await res.json().catch(() => ({}));
|
||||||
@@ -94,6 +100,8 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
|||||||
|
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
console.log(`[YouTubeChat] Received response: ${data.messageCount} messages, ${(data.emotes || []).length} emotes`);
|
||||||
|
|
||||||
if (data.success && data.messages) {
|
if (data.success && data.messages) {
|
||||||
const fetchedMessages = (data.messages as any[]).map((msg: any) => ({
|
const fetchedMessages = (data.messages as any[]).map((msg: any) => ({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
@@ -163,7 +171,7 @@ export default function YouTubeChat({ videoId, accessToken, session, onEmotesDis
|
|||||||
clearInterval(pollingIntervalRef.current);
|
clearInterval(pollingIntervalRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [videoId, isConnected]);
|
}, [videoId, accessToken, isConnected]);
|
||||||
|
|
||||||
// Keep the view pinned to the newest message
|
// Keep the view pinned to the newest message
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -26,11 +26,17 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
// Update session credentials for this request
|
// Update session credentials for this request
|
||||||
// This allows us to use an existing accessToken from the client
|
// This allows us to use an existing accessToken from the client
|
||||||
(yt.session as any).signIn({
|
try {
|
||||||
access_token: accessToken,
|
await (yt.session as any).signIn({
|
||||||
refresh_token: '', // Not needed for a one-off send
|
access_token: accessToken,
|
||||||
expiry_date: new Date(Date.now() + 3600 * 1000).toISOString()
|
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 info = await yt.getInfo(videoId);
|
||||||
const liveChat = info.getLiveChat();
|
const liveChat = info.getLiveChat();
|
||||||
@@ -49,10 +55,16 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: error instanceof Error ? error.message : "Internal server error",
|
error: errorMessage,
|
||||||
|
details: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||||
}),
|
}),
|
||||||
{ status: 500, headers },
|
{ status: 500, headers },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const channelUrl = url.searchParams.get("channelUrl");
|
const channelUrl = url.searchParams.get("channelUrl");
|
||||||
|
const accessToken = url.searchParams.get("accessToken");
|
||||||
|
|
||||||
if (!channelUrl) {
|
if (!channelUrl) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -30,28 +31,97 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
|
|
||||||
const yt = await getInnertube();
|
const yt = await getInnertube();
|
||||||
|
|
||||||
// Use getChannel which handles handles, URLs, and IDs
|
// If accessToken is provided, use it to authenticate the session
|
||||||
const channel = await yt.getChannel(channelUrl);
|
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
|
// Check if the channel is currently live
|
||||||
const liveStreams = await (channel as any).getLiveStreams();
|
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 });
|
liveVideoCache.set(channelUrl, { videoId, expires: now + LIVE_CACHE_TTL });
|
||||||
|
|
||||||
// Try to get channel ID from metadata or basic_info
|
// 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,
|
status: 200,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} 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(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: error instanceof Error ? error.message : "Internal server error",
|
error: errorMessage,
|
||||||
|
details: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||||
}),
|
}),
|
||||||
{ status: 500, headers },
|
{ status: 500, headers },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const activeLiveChats = new Map<
|
|||||||
|
|
||||||
export const GET: APIRoute = async ({ url }) => {
|
export const GET: APIRoute = async ({ url }) => {
|
||||||
const videoId = url.searchParams.get('videoId');
|
const videoId = url.searchParams.get('videoId');
|
||||||
|
const accessToken = url.searchParams.get('accessToken');
|
||||||
|
|
||||||
if (!videoId) {
|
if (!videoId) {
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -66,6 +67,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
const emoteMap = new Map<string, YTEmoteEntry>();
|
const emoteMap = new Map<string, YTEmoteEntry>();
|
||||||
|
|
||||||
chat.on('chat-update', (action: any) => {
|
chat.on('chat-update', (action: any) => {
|
||||||
|
console.log(`[YouTube Chat] Received chat-update action:`, action.type);
|
||||||
if (action.type === 'AddChatItemAction') {
|
if (action.type === 'AddChatItemAction') {
|
||||||
const item = action.item;
|
const item = action.item;
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
|
|
||||||
let chatMsg: ChatMessage | null = null;
|
let chatMsg: ChatMessage | null = null;
|
||||||
|
|
||||||
if (item.type === 'LiveChatMessage') {
|
if (item.type === 'LiveChatMessage' || item.type === 'LiveChatTextMessage') {
|
||||||
chatMsg = parseLiveChatMessage(item, emoteMap);
|
chatMsg = parseLiveChatMessage(item, emoteMap);
|
||||||
} else if (item.type === 'LiveChatPaidMessageItem') {
|
} else if (item.type === 'LiveChatPaidMessageItem') {
|
||||||
chatMsg = parsePaidMessage(item, emoteMap);
|
chatMsg = parsePaidMessage(item, emoteMap);
|
||||||
@@ -100,6 +102,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[YouTube Chat] Starting live chat for video ${videoId}`);
|
||||||
await chat.start();
|
await chat.start();
|
||||||
|
|
||||||
// Populate emote map with initial channel emojis
|
// Populate emote map with initial channel emojis
|
||||||
@@ -130,6 +133,10 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
|
|
||||||
chatSession.lastUpdate = Date.now();
|
chatSession.lastUpdate = Date.now();
|
||||||
|
|
||||||
|
if (chatSession.messages.length > 0) {
|
||||||
|
console.log(`[YouTube Chat] Returning ${chatSession.messages.length} messages for video ${videoId}`);
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -147,10 +154,13 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} 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(
|
return new Response(
|
||||||
JSON.stringify({
|
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' } }
|
{ 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 {
|
return {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
author: msg.author.name.toString(),
|
author: msg.author.name.toString(),
|
||||||
authorAvatar: msg.author.thumbnails?.[0]?.url,
|
authorAvatar: msg.author.thumbnails?.[0]?.url,
|
||||||
badges,
|
badges,
|
||||||
parts,
|
parts,
|
||||||
timestamp: new Date(parseInt(msg.timestamp) / 1000)
|
timestamp
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user