import { google, youtube_v3 } from 'googleapis';
export interface VideoResult {
videoId: string;
title: string;
channelTitle: string;
description: string;
thumbnailUrl: string;
publishedAt: string;
}
export interface SearchOptions {
query: string;
maxResults?: number;
type?: 'video' | 'playlist' | 'channel';
}
class YouTubeClient {
private youtube: youtube_v3.Youtube;
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('YouTube API key is required. Set YOUTUBE_API_KEY environment variable.');
}
this.youtube = google.youtube({
version: 'v3',
auth: apiKey,
});
}
async searchVideos(options: SearchOptions): Promise<VideoResult[]> {
const { query, maxResults = 10, type = 'video' } = options;
try {
const response = await this.youtube.search.list({
part: ['snippet'],
q: query,
type: [type],
maxResults: Math.min(maxResults, 50), // YouTube API max is 50
order: 'relevance',
});
const items = response.data.items || [];
return items
.filter((item): item is youtube_v3.Schema$SearchResult =>
item.id?.videoId !== undefined
)
.map((item) => ({
videoId: item.id!.videoId!,
title: item.snippet?.title || 'Untitled',
channelTitle: item.snippet?.channelTitle || 'Unknown Channel',
description: item.snippet?.description || '',
thumbnailUrl: item.snippet?.thumbnails?.high?.url ||
item.snippet?.thumbnails?.default?.url || '',
publishedAt: item.snippet?.publishedAt || '',
}));
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('quota')) {
throw new Error('YouTube API quota exceeded. Please try again later or check your API key limits.');
}
if (error.message.includes('403')) {
throw new Error('YouTube API access forbidden. Please check your API key permissions.');
}
throw new Error(`YouTube API error: ${error.message}`);
}
throw error;
}
}
generatePlaylistUrl(videoIds: string[]): string {
if (videoIds.length === 0) {
throw new Error('At least one video ID is required to create a playlist URL.');
}
if (videoIds.length === 1) {
return `https://www.youtube.com/watch?v=${videoIds[0]}`;
}
// YouTube watch_videos endpoint creates a temporary playlist
return `https://www.youtube.com/watch_videos?video_ids=${videoIds.join(',')}`;
}
}
let clientInstance: YouTubeClient | null = null;
export function getYouTubeClient(): YouTubeClient {
if (!clientInstance) {
const apiKey = process.env.YOUTUBE_API_KEY;
if (!apiKey) {
throw new Error('YOUTUBE_API_KEY environment variable is not set.');
}
clientInstance = new YouTubeClient(apiKey);
}
return clientInstance;
}
export { YouTubeClient };