/**
* YouTube Data API v3 wrapper
* Provides functions for interacting with YouTube API
*/
import { google, youtube_v3 } from 'googleapis';
import {
VideoSearchResult,
VideoDetails,
ChannelInfo,
PlaylistInfo,
PlaylistVideo,
SearchOrder,
VideoOrder,
} from './types.js';
let youtubeClient: youtube_v3.Youtube | null = null;
/**
* Initializes the YouTube API client with the provided API key
* @param apiKey YouTube Data API v3 key
*/
export function initializeYouTubeClient(apiKey: string): void {
if (!apiKey) {
throw new Error(
'YouTube API key is required. Please set YOUTUBE_API_KEY environment variable.'
);
}
youtubeClient = google.youtube({
version: 'v3',
auth: apiKey,
});
}
/**
* Gets the YouTube client instance
* @returns YouTube client
*/
function getClient(): youtube_v3.Youtube {
if (!youtubeClient) {
throw new Error('YouTube client not initialized. Call initializeYouTubeClient first.');
}
return youtubeClient;
}
/**
* Searches YouTube for videos matching the query
* @param query Search term
* @param maxResults Number of results to return (default 10, max 50)
* @param order Sort order (default 'relevance')
* @returns Array of video search results
*/
export async function searchYouTube(
query: string,
maxResults: number = 10,
order: SearchOrder = 'relevance'
): Promise<VideoSearchResult[]> {
const client = getClient();
try {
const response = await client.search.list({
part: ['snippet'],
q: query,
type: ['video'],
maxResults: Math.min(maxResults, 50),
order,
});
const videoIds = response.data.items
?.map((item) => item.id?.videoId)
.filter((id): id is string => !!id);
if (!videoIds || videoIds.length === 0) {
return [];
}
// Fetch video statistics to get view counts
const statsResponse = await client.videos.list({
part: ['statistics'],
id: videoIds,
});
const statsMap = new Map(
statsResponse.data.items?.map((item) => [
item.id,
item.statistics?.viewCount,
])
);
return (
response.data.items?.map((item) => ({
videoId: item.id?.videoId || '',
title: item.snippet?.title || '',
channelName: item.snippet?.channelTitle || '',
channelId: item.snippet?.channelId || '',
thumbnailUrl: item.snippet?.thumbnails?.high?.url || '',
description: item.snippet?.description || '',
publishedAt: item.snippet?.publishedAt || '',
viewCount: statsMap.get(item.id?.videoId) || undefined,
})) || []
);
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Gets detailed information about a specific video
* @param videoId YouTube video ID
* @returns Video details object
*/
export async function getVideoDetails(videoId: string): Promise<VideoDetails> {
const client = getClient();
try {
const response = await client.videos.list({
part: ['snippet', 'contentDetails', 'statistics'],
id: [videoId],
});
const video = response.data.items?.[0];
if (!video) {
throw new Error(`Video not found: ${videoId}`);
}
return {
videoId: video.id || '',
title: video.snippet?.title || '',
description: video.snippet?.description || '',
channelName: video.snippet?.channelTitle || '',
channelId: video.snippet?.channelId || '',
duration: video.contentDetails?.duration || '',
viewCount: video.statistics?.viewCount || '0',
likeCount: video.statistics?.likeCount || '0',
commentCount: video.statistics?.commentCount || undefined,
publishedAt: video.snippet?.publishedAt || '',
tags: video.snippet?.tags || undefined,
categoryId: video.snippet?.categoryId || '',
thumbnails: {
default: video.snippet?.thumbnails?.default?.url || '',
medium: video.snippet?.thumbnails?.medium?.url || '',
high: video.snippet?.thumbnails?.high?.url || '',
standard: video.snippet?.thumbnails?.standard?.url || undefined,
maxres: video.snippet?.thumbnails?.maxres?.url || undefined,
},
};
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Gets information about a YouTube channel
* @param channelId YouTube channel ID
* @param includeVideos Whether to include recent videos (default false)
* @returns Channel information object
*/
export async function getChannelInfo(
channelId: string,
includeVideos: boolean = false
): Promise<ChannelInfo> {
const client = getClient();
try {
const response = await client.channels.list({
part: ['snippet', 'statistics', 'contentDetails'],
id: [channelId],
});
const channel = response.data.items?.[0];
if (!channel) {
throw new Error(`Channel not found: ${channelId}`);
}
const channelInfo: ChannelInfo = {
channelId: channel.id || '',
title: channel.snippet?.title || '',
description: channel.snippet?.description || '',
customUrl: channel.snippet?.customUrl || undefined,
subscriberCount: channel.statistics?.subscriberCount || '0',
viewCount: channel.statistics?.viewCount || '0',
videoCount: channel.statistics?.videoCount || '0',
thumbnailUrl: channel.snippet?.thumbnails?.high?.url || '',
publishedAt: channel.snippet?.publishedAt || '',
country: channel.snippet?.country || undefined,
};
if (includeVideos) {
channelInfo.recentVideos = await getChannelVideos(channelId, 10, 'date');
}
return channelInfo;
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Gets videos from a specific channel
* @param channelId YouTube channel ID
* @param maxResults Number of videos to return (default 25, max 50)
* @param order Sort order (default 'date')
* @returns Array of video search results
*/
export async function getChannelVideos(
channelId: string,
maxResults: number = 25,
order: VideoOrder = 'date'
): Promise<VideoSearchResult[]> {
const client = getClient();
try {
const response = await client.search.list({
part: ['snippet'],
channelId,
type: ['video'],
maxResults: Math.min(maxResults, 50),
order,
});
return (
response.data.items?.map((item) => ({
videoId: item.id?.videoId || '',
title: item.snippet?.title || '',
channelName: item.snippet?.channelTitle || '',
channelId: item.snippet?.channelId || '',
thumbnailUrl: item.snippet?.thumbnails?.high?.url || '',
description: item.snippet?.description || '',
publishedAt: item.snippet?.publishedAt || '',
})) || []
);
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Gets information about a YouTube playlist
* @param playlistId YouTube playlist ID
* @returns Playlist information object
*/
export async function getPlaylistInfo(playlistId: string): Promise<PlaylistInfo> {
const client = getClient();
try {
const response = await client.playlists.list({
part: ['snippet', 'contentDetails'],
id: [playlistId],
});
const playlist = response.data.items?.[0];
if (!playlist) {
throw new Error(`Playlist not found: ${playlistId}`);
}
return {
playlistId: playlist.id || '',
title: playlist.snippet?.title || '',
description: playlist.snippet?.description || '',
channelName: playlist.snippet?.channelTitle || '',
channelId: playlist.snippet?.channelId || '',
videoCount: playlist.contentDetails?.itemCount || 0,
thumbnailUrl: playlist.snippet?.thumbnails?.high?.url || '',
publishedAt: playlist.snippet?.publishedAt || '',
};
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Gets all videos in a playlist
* @param playlistId YouTube playlist ID
* @param maxResults Number of videos to return (default 50)
* @returns Array of playlist videos
*/
export async function getPlaylistVideos(
playlistId: string,
maxResults: number = 50
): Promise<PlaylistVideo[]> {
const client = getClient();
try {
const response = await client.playlistItems.list({
part: ['snippet', 'contentDetails'],
playlistId,
maxResults: Math.min(maxResults, 50),
});
return (
response.data.items?.map((item, index) => ({
videoId: item.contentDetails?.videoId || '',
title: item.snippet?.title || '',
channelName: item.snippet?.channelTitle || '',
channelId: item.snippet?.channelId || '',
thumbnailUrl: item.snippet?.thumbnails?.high?.url || '',
description: item.snippet?.description || '',
publishedAt: item.snippet?.publishedAt || '',
position: item.snippet?.position ?? index,
})) || []
);
} catch (error) {
handleYouTubeAPIError(error);
throw error;
}
}
/**
* Handles YouTube API errors and provides user-friendly messages
* @param error Error object from API call
*/
function handleYouTubeAPIError(error: any): void {
if (error.code === 403) {
console.error('YouTube API Error: Quota exceeded or invalid API key');
throw new Error(
'YouTube API quota exceeded or invalid API key. Please check your API key and quota limits at https://console.cloud.google.com'
);
}
if (error.code === 400) {
console.error('YouTube API Error: Invalid request', error.message);
throw new Error(`Invalid request: ${error.message}`);
}
if (error.code === 404) {
console.error('YouTube API Error: Resource not found');
throw new Error('Requested YouTube resource not found');
}
console.error('YouTube API Error:', error);
}