Skip to main content
Glama

YouTube Knowledge MCP

by efikuta
trending.ts11.7 kB
import { Logger } from 'winston'; import { YouTubeClient } from '../youtube-client.js'; import { CacheManager } from '../utils/cache.js'; import { QuotaManager } from '../utils/quota.js'; import { GetTrendingVideosParams, TrendingVideosResult, GetTrendingVideosSchema, QuotaExceededError } from '../types.js'; export class TrendingVideosTool { constructor( private youtubeClient: YouTubeClient, private cache: CacheManager, private quotaManager: QuotaManager, private logger: Logger ) {} async execute(args: unknown): Promise<TrendingVideosResult> { // Validate input parameters const params = GetTrendingVideosSchema.parse(args); this.logger.info(`Getting trending videos for region: ${params.region}, category: ${params.category || 'all'}`); // Generate cache key const cacheKey = this.cache.getTrendingKey(params.region, params.category); // Check cache first const cached = await this.cache.get<TrendingVideosResult>(cacheKey); if (cached) { this.logger.info(`Returning cached trending videos for region: ${params.region}`); return cached; } // Check quota before making API call const operationCost = QuotaManager.getOperationCost('trending'); if (!this.quotaManager.canPerformOperation(operationCost)) { throw new QuotaExceededError('Insufficient quota for trending videos operation'); } // Optimize operation based on quota availability const optimizedMaxResults = this.quotaManager.optimizeOperation('trending', params.maxResults); if (optimizedMaxResults === 0) { throw new QuotaExceededError('Cannot perform trending operation due to quota constraints'); } const optimizedParams: GetTrendingVideosParams = { ...params, maxResults: optimizedMaxResults }; try { // Get trending videos from YouTube API const result = await this.youtubeClient.getTrendingVideos(optimizedParams); // Record quota usage await this.quotaManager.recordUsage(operationCost, 'trending'); // Enhance the result with additional metadata const enhancedResult = await this.enhanceTrendingData(result); // Cache the result (trending data changes frequently, so shorter TTL) await this.cache.set(cacheKey, enhancedResult, 1800); // 30 minutes TTL this.logger.info(`Retrieved ${result.videos.length} trending videos for region: ${params.region}`); return enhancedResult; } catch (error) { this.logger.error(`Failed to get trending videos for region ${params.region}:`, error); // Try to return cached data if quota exceeded if (error instanceof QuotaExceededError) { const fallbackCache = await this.getFallbackTrendingData(params.region); if (fallbackCache) { this.logger.warn('Returning fallback trending data due to quota limits'); return fallbackCache; } } throw error; } } /** * Enhance trending data with additional analysis and metadata */ private async enhanceTrendingData(result: TrendingVideosResult): Promise<TrendingVideosResult> { const enhanced = { ...result }; // Add trending analysis const analysis = this.analyzeTrendingPatterns(result.videos); return { ...enhanced, analysis, metadata: { retrievedAt: new Date().toISOString(), totalVideos: result.videos.length, categories: this.getUniqueCategories(result.videos), avgViews: this.calculateAverageViews(result.videos), topChannels: this.getTopChannels(result.videos) } } as any; } /** * Analyze patterns in trending videos */ private analyzeTrendingPatterns(videos: any[]): { popularTopics: string[]; avgDuration: string; mostActiveChannels: Array<{ name: string; videoCount: number }>; viewDistribution: { min: number; max: number; avg: number }; publishTimePatterns: Record<string, number>; } { if (videos.length === 0) { return { popularTopics: [], avgDuration: '0', mostActiveChannels: [], viewDistribution: { min: 0, max: 0, avg: 0 }, publishTimePatterns: {} }; } // Extract topics from titles and descriptions const allText = videos .map(video => `${video.title} ${video.description}`) .join(' ') .toLowerCase(); const words = allText.split(/\s+/) .filter(word => word.length > 3) .filter(word => !this.isStopWord(word)); const wordCount: Record<string, number> = {}; words.forEach(word => { wordCount[word] = (wordCount[word] || 0) + 1; }); const popularTopics = Object.entries(wordCount) .sort(([, a], [, b]) => b - a) .slice(0, 10) .map(([word]) => word); // Calculate average duration (simplified) const avgDurationMs = videos.reduce((sum, video) => { // Parse ISO 8601 duration (PT1H2M3S format) const duration = video.duration || 'PT0S'; const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/); if (match) { const hours = parseInt(match[1] || '0'); const minutes = parseInt(match[2] || '0'); const seconds = parseInt(match[3] || '0'); return sum + (hours * 3600 + minutes * 60 + seconds); } return sum; }, 0) / videos.length; const avgDuration = this.formatDuration(Math.round(avgDurationMs)); // Find most active channels const channelCount: Record<string, number> = {}; videos.forEach(video => { channelCount[video.channelTitle] = (channelCount[video.channelTitle] || 0) + 1; }); const mostActiveChannels = Object.entries(channelCount) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([name, videoCount]) => ({ name, videoCount })); // View distribution const views = videos.map(video => parseInt(video.viewCount || '0')); const viewDistribution = { min: Math.min(...views), max: Math.max(...views), avg: Math.round(views.reduce((sum, v) => sum + v, 0) / views.length) }; // Publish time patterns (hour of day) const publishTimePatterns: Record<string, number> = {}; videos.forEach(video => { const hour = new Date(video.publishedAt).getHours(); const hourRange = `${hour}:00-${hour + 1}:00`; publishTimePatterns[hourRange] = (publishTimePatterns[hourRange] || 0) + 1; }); return { popularTopics, avgDuration, mostActiveChannels, viewDistribution, publishTimePatterns }; } /** * Get trending videos by category */ async getTrendingByCategory( category: string, region: string = 'US', maxResults: number = 25 ): Promise<TrendingVideosResult> { // Map common category names to YouTube category IDs const categoryMap: Record<string, string> = { 'music': '10', 'gaming': '20', 'entertainment': '24', 'news': '25', 'education': '27', 'science': '28', 'technology': '28', 'sports': '17', 'travel': '19', 'comedy': '23' }; const categoryId = categoryMap[category.toLowerCase()] || category; return this.execute({ category: categoryId, region, maxResults }); } /** * Get trending videos from multiple regions */ async getGlobalTrending(regions: string[] = ['US', 'GB', 'CA', 'AU']): Promise<{ byRegion: Record<string, TrendingVideosResult>; globalTrends: { commonVideos: any[]; regionalDifferences: Record<string, string[]>; }; }> { const results: Record<string, TrendingVideosResult> = {}; // Get trending videos for each region for (const region of regions) { try { results[region] = await this.execute({ region, maxResults: 10 }); } catch (error) { this.logger.warn(`Failed to get trending videos for ${region}:`, error); } } // Analyze global trends const allVideos = Object.values(results).flatMap(result => result.videos); const videoMap = new Map(); allVideos.forEach(video => { if (videoMap.has(video.id)) { videoMap.get(video.id).regions.push(video.region || 'unknown'); } else { videoMap.set(video.id, { ...video, regions: [video.region || 'unknown'] }); } }); const commonVideos = Array.from(videoMap.values()) .filter(video => video.regions.length > 1) .sort((a, b) => b.regions.length - a.regions.length); // Find regional differences const regionalDifferences: Record<string, string[]> = {}; regions.forEach(region => { if (results[region]) { const uniqueToRegion = results[region].videos .filter(video => !commonVideos.some(common => common.id === video.id)) .map(video => video.title); regionalDifferences[region] = uniqueToRegion.slice(0, 5); } }); return { byRegion: results, globalTrends: { commonVideos: commonVideos.slice(0, 10), regionalDifferences } }; } /** * Get fallback trending data from cache (even if expired) */ private async getFallbackTrendingData(region: string): Promise<TrendingVideosResult | null> { try { // Try different cache keys as fallback const fallbackKeys = [ this.cache.getTrendingKey(region), this.cache.getTrendingKey('US'), // Fallback to US this.cache.getTrendingKey('GB') // Fallback to GB ]; for (const key of fallbackKeys) { const cached = await this.cache.get<TrendingVideosResult>(key); if (cached) { return { ...cached, metadata: { ...((cached as any).metadata || {}), fallback: true, originalRegion: region } } as any; } } return null; } catch (error) { this.logger.error('Failed to get fallback trending data:', error); return null; } } /** * Helper methods */ private getUniqueCategories(videos: any[]): string[] { const categories = videos .map(video => video.categoryId) .filter(Boolean); return [...new Set(categories)]; } private calculateAverageViews(videos: any[]): number { const totalViews = videos.reduce((sum, video) => { return sum + parseInt(video.viewCount || '0'); }, 0); return Math.round(totalViews / videos.length); } private getTopChannels(videos: any[]): Array<{ name: string; videoCount: number }> { const channelCount: Record<string, number> = {}; videos.forEach(video => { channelCount[video.channelTitle] = (channelCount[video.channelTitle] || 0) + 1; }); return Object.entries(channelCount) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([name, videoCount]) => ({ name, videoCount })); } private formatDuration(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}h ${minutes}m ${secs}s`; } else if (minutes > 0) { return `${minutes}m ${secs}s`; } else { return `${secs}s`; } } private isStopWord(word: string): boolean { const stopWords = new Set([ 'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'i', 'it', 'for', 'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at', 'this', 'but', 'his', 'by', 'from' ]); return stopWords.has(word.toLowerCase()); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/efikuta/youtube-knowledge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server