import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; // YouTube category IDs mapping const categoryIds: Record = { 'gaming': '20', 'music': '10', 'education': '27', 'entertainment': '24', 'technology': '28', 'sports': '17', 'lifestyle': '22', 'news': '25', 'travel': '19', 'food': '26', 'pets': '15', 'autos': '2', }; // Country codes const validCountries = [ 'US', 'GB', 'IN', 'CA', 'BR', 'DE', 'JP', 'AU', 'FR', 'ES', 'TR', 'PH', 'MX', 'KR', 'RU', 'IT', 'NL', 'SE', 'PL', 'ID' ]; interface ChannelData { id: string; title: string; description: string; customUrl: string; thumbnails: any; country?: string; publishedAt: string; subscriberCount: number; viewCount: number; videoCount: number; subscriberGrowthIndicator?: string; category?: string; } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { const YOUTUBE_API_KEY = Deno.env.get('YOUTUBE_API_KEY'); if (!YOUTUBE_API_KEY) { throw new Error('YouTube API key not configured'); } const { category, country, limit = 50, rankBy = 'subscribers', searchQuery } = await req.json(); console.log(`Fetching top channels - Category: ${category || 'all'}, Country: ${country || 'all'}, Limit: ${limit}, Search: ${searchQuery || 'none'}`); let channelIds: string[] = []; // If search query is provided, search for channels directly if (searchQuery && searchQuery.trim()) { const searchUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=channel&q=${encodeURIComponent(searchQuery)}&maxResults=${Math.min(limit, 50)}&key=${YOUTUBE_API_KEY}`; console.log('Searching channels with query:', searchQuery); const searchResponse = await fetch(searchUrl); if (!searchResponse.ok) { const errorText = await searchResponse.text(); console.error('YouTube search API error:', errorText); throw new Error('Failed to search YouTube channels'); } const searchData = await searchResponse.json(); channelIds = searchData.items?.map((item: any) => item.id.channelId || item.snippet.channelId).filter(Boolean) || []; } else { // For browsing top channels, use mostPopular videos endpoint then extract channels let videosUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&chart=mostPopular&maxResults=50&key=${YOUTUBE_API_KEY}`; if (category && categoryIds[category]) { videosUrl += `&videoCategoryId=${categoryIds[category]}`; } if (country && validCountries.includes(country)) { videosUrl += `®ionCode=${country}`; } console.log('Fetching popular videos to extract channels'); const videosResponse = await fetch(videosUrl); if (!videosResponse.ok) { const errorText = await videosResponse.text(); console.error('YouTube videos API error:', errorText); throw new Error('Failed to fetch popular videos'); } const videosData = await videosResponse.json(); // Extract unique channel IDs from popular videos const uniqueChannelIds = new Set (); videosData.items?.forEach((video: any) => { if (video.snippet.channelId) { uniqueChannelIds.add(video.snippet.channelId); } }); channelIds = Array.from(uniqueChannelIds); console.log(`Extracted ${channelIds.length} unique channels from popular videos`); } if (channelIds.length === 0) { console.log('No channels found'); return new Response(JSON.stringify({ channels: [], total: 0 }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // Fetch detailed channel statistics (API allows max 50 per request) const statsUrl = `https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,brandingSettings&id=${channelIds.slice(0, 50).join(',')}&key=${YOUTUBE_API_KEY}`; console.log('Fetching channel statistics for', channelIds.length, 'channels'); const statsResponse = await fetch(statsUrl); if (!statsResponse.ok) { const errorText = await statsResponse.text(); console.error('YouTube channels API error:', errorText); throw new Error('Failed to fetch channel statistics'); } const statsData = await statsResponse.json(); // Process and format channel data const channels: ChannelData[] = statsData.items?.map((channel: any) => { const subscriberCount = parseInt(channel.statistics.subscriberCount) || 0; const viewCount = parseInt(channel.statistics.viewCount) || 0; const videoCount = parseInt(channel.statistics.videoCount) || 0; // Calculate growth indicator based on views per video const viewsPerVideo = videoCount > 0 ? viewCount / videoCount : 0; let growthIndicator = 'stable'; if (viewsPerVideo > 1000000) growthIndicator = 'explosive'; else if (viewsPerVideo > 100000) growthIndicator = 'high'; else if (viewsPerVideo > 10000) growthIndicator = 'moderate'; else if (viewsPerVideo < 1000) growthIndicator = 'low'; return { id: channel.id, title: channel.snippet.title, description: channel.snippet.description?.substring(0, 200) || '', customUrl: channel.snippet.customUrl || '', thumbnails: channel.snippet.thumbnails, country: channel.snippet.country || channel.brandingSettings?.channel?.country || 'Unknown', publishedAt: channel.snippet.publishedAt, subscriberCount, viewCount, videoCount, subscriberGrowthIndicator: growthIndicator, category: category || 'general' }; }) || []; // Sort channels by selected metric channels.sort((a, b) => { switch (rankBy) { case 'views': return b.viewCount - a.viewCount; case 'videos': return b.videoCount - a.videoCount; default: return b.subscriberCount - a.subscriberCount; } }); // Add rank positions const rankedChannels = channels.map((channel, index) => ({ ...channel, rank: index + 1 })); console.log(`Returning ${rankedChannels.length} channels`); return new Response(JSON.stringify({ channels: rankedChannels, total: rankedChannels.length, filters: { category, country, rankBy, searchQuery } }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } catch (error) { console.error('Top channels error:', error); return new Response(JSON.stringify({ error: error instanceof Error ? error.message : 'Failed to fetch top channels' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } });