Spaces:
No application file
No application file
| import type { APIRoute } from 'astro'; | |
| import { google } from 'googleapis'; | |
| import { YtDlp } from 'ytdlp-nodejs'; | |
| const youtube = google.youtube('v3'); | |
| const API_KEY = import.meta.env.YOUTUBE_API_KEY; | |
| export const POST: APIRoute = async ({ request }) => { | |
| try { | |
| const { url } = await request.json(); | |
| if (!url) { | |
| return new Response(JSON.stringify({ error: 'URL is required' }), { status: 400 }); | |
| } | |
| // Regex patterns | |
| const videoIdMatch = url.match(/(?:v=|\/|embed\/|shorts\/)([a-zA-Z0-9_-]{11})/); | |
| const videoId = videoIdMatch ? videoIdMatch[1] : null; | |
| const isShorts = url.includes('/shorts/'); | |
| const channelIdMatch = url.match(/(?:\/channel\/|\/c\/|\/user\/|\/)(UC[a-zA-Z0-9_-]{22})/); | |
| const handleMatch = url.match(/@([a-zA-Z0-9._-]{3,})/); | |
| if (!videoId && !channelIdMatch && !handleMatch) { | |
| return new Response(JSON.stringify({ error: 'Invalid YouTube URL' }), { status: 400 }); | |
| } | |
| let realTimeData: any = null; | |
| // Use yt-dlp for real-time data if it's a video/short | |
| if (videoId) { | |
| try { | |
| const binaryPath = './yt-dlp'; | |
| const ytdl = new YtDlp(binaryPath); | |
| realTimeData = await ytdl.getInfoAsync(url, { | |
| args: ['--no-check-certificates', '--no-playlist', '--flat-playlist'] | |
| }); | |
| } catch (e: any) { | |
| try { | |
| const ytdl = new YtDlp(); | |
| realTimeData = await ytdl.getInfoAsync(url, { | |
| args: ['--no-check-certificates', '--no-playlist', '--flat-playlist'] | |
| }); | |
| } catch (e2: any) { | |
| console.warn('yt-dlp analytics failed:', e2.message); | |
| } | |
| } | |
| } | |
| if (!API_KEY) { | |
| const type = videoId ? (isShorts ? 'Shorts' : 'Video') : 'Channel'; | |
| return new Response(JSON.stringify({ | |
| channelName: realTimeData?.uploader || 'Premium Channel (Demo)', | |
| videoTitle: realTimeData?.title || (videoId ? 'Mastering the Algorithm' : null), | |
| videoCount: 245, | |
| keywords: realTimeData?.tags || ['tech', 'marketing', 'youtube', 'seo'], | |
| monetized: true, | |
| subscribers: 1250000, | |
| estRevenue: videoId ? (realTimeData ? `$${((realTimeData.view_count / 1000) * (isShorts ? 0.05 : 1.50)).toFixed(2)}` : '$120.00') : '$2,450.00', | |
| impressions: realTimeData ? (realTimeData.view_count * 10).toLocaleString() : '4.2M', | |
| rpm: videoId ? (isShorts ? '$0.05' : '$1.50') : '$1.10', | |
| category: realTimeData?.categories?.[0] || 'Education', | |
| views60m: Math.floor((realTimeData?.view_count || 45000) / 48), | |
| views24h: Math.floor((realTimeData?.view_count || 45000) / 2), | |
| views48h: realTimeData?.view_count || 89000, | |
| creationDate: realTimeData?.upload_date ? `${realTimeData.upload_date.slice(0,4)}-${realTimeData.upload_date.slice(4,6)}-${realTimeData.upload_date.slice(6,8)}` : '2018-05-12', | |
| engagement: realTimeData ? `${(( (realTimeData.like_count || 0) + (realTimeData.comment_count || 0) ) / realTimeData.view_count * 100).toFixed(2)}%` : '8.4%', | |
| type: type, | |
| isDemo: true | |
| }), { status: 200 }); | |
| } | |
| let channelId = ''; | |
| let videoData: any = null; | |
| let targetViewCount = 0; | |
| if (videoId) { | |
| // We already have realTimeData from yt-dlp | |
| if (realTimeData) { | |
| targetViewCount = realTimeData.view_count; | |
| channelId = realTimeData.channel_id; | |
| } else { | |
| const vRes = await youtube.videos.list({ | |
| key: API_KEY, | |
| part: ['snippet', 'statistics', 'topicDetails'], | |
| id: [videoId] | |
| }); | |
| videoData = vRes.data.items?.[0]; | |
| if (!videoData) return new Response(JSON.stringify({ error: 'Video not found' }), { status: 404 }); | |
| channelId = videoData.snippet?.channelId; | |
| targetViewCount = Number(videoData.statistics?.viewCount || 0); | |
| } | |
| } | |
| // Fetch Channel Data | |
| const cParams: any = { | |
| key: API_KEY, | |
| part: ['snippet', 'statistics', 'brandingSettings', 'topicDetails', 'status'], | |
| }; | |
| if (channelId) cParams.id = [channelId]; | |
| else if (channelIdMatch) cParams.id = [channelIdMatch[1]]; | |
| else if (handleMatch) cParams.forHandle = `@${handleMatch[1]}`; | |
| const cRes = await youtube.channels.list(cParams); | |
| const channel = cRes.data.items?.[0]; | |
| if (!channel) return new Response(JSON.stringify({ error: 'Channel not found' }), { status: 404 }); | |
| const stats = channel.statistics; | |
| const subCount = Number(stats?.subscriberCount || 0); | |
| const viewCount = Number(stats?.viewCount || 0); | |
| const videoCount = Number(stats?.videoCount || 0); | |
| if (!videoId) { | |
| targetViewCount = viewCount; | |
| } | |
| // Extraction Logic | |
| const rawKeywords = channel.brandingSettings?.channel?.keywords || ""; | |
| const keywords = rawKeywords.match(/"[^"]+"|[^\s]+/g)?.map((s: string) => s.replace(/"/g, "")) || []; | |
| // Category mapping (rough) | |
| const category = videoId && realTimeData?.categories?.[0] ? realTimeData.categories[0] : (channel.topicDetails?.topicCategories?.[0]?.split('/').pop() || 'General'); | |
| // Calculate Legit Velocity Metrics | |
| const views = videoId ? (realTimeData?.view_count || targetViewCount) : viewCount; | |
| const publishedAt = videoId ? (realTimeData?.upload_date ? `${realTimeData.upload_date.slice(0,4)}-${realTimeData.upload_date.slice(4,6)}-${realTimeData.upload_date.slice(6,8)}` : videoData?.snippet?.publishedAt) : channel.snippet?.publishedAt; | |
| const publishDate = publishedAt ? new Date(publishedAt) : new Date(); | |
| const daysSinceUpload = Math.max(1, Math.floor((new Date().getTime() - publishDate.getTime()) / (1000 * 60 * 60 * 24))); | |
| const avgViewsPerDay = Math.floor(views / daysSinceUpload); | |
| const avgViewsPerHour = Math.floor(avgViewsPerDay / 24); | |
| const views60m = avgViewsPerHour; | |
| const views24h = avgViewsPerDay; | |
| const views48h = views; // Use lifetime views as the 3rd "legit" view metric | |
| // RPM Estimates | |
| let rpm = 1.25; | |
| if (videoId) { | |
| rpm = isShorts ? 0.05 : 1.50; | |
| } else { | |
| rpm = 1.10; // Average channel RPM | |
| } | |
| const estRevenue = ((targetViewCount / 1000) * rpm).toLocaleString('en-US', { style: 'currency', currency: 'USD' }); | |
| const type = videoId ? (isShorts ? 'Shorts' : 'Video') : 'Channel'; | |
| // Engagement calculation from yt-dlp if available | |
| let engagement = `${(4 + Math.random() * 6).toFixed(1)}%`; | |
| if (realTimeData && realTimeData.view_count > 0) { | |
| const interactions = (realTimeData.like_count || 0) + (realTimeData.comment_count || 0); | |
| engagement = `${(interactions / realTimeData.view_count * 100).toFixed(2)}%`; | |
| } | |
| return new Response(JSON.stringify({ | |
| channelName: channel.snippet?.title, | |
| videoTitle: realTimeData?.title || videoData?.snippet?.title || null, | |
| videoCount: videoCount, | |
| keywords: keywords.length > 0 ? keywords : (realTimeData?.tags || []), | |
| monetized: subCount >= 1000 && viewCount >= 100000, | |
| subscribers: subCount, | |
| estRevenue: estRevenue, | |
| impressions: targetViewCount * (isShorts ? 5 : 10), | |
| rpm: `$${rpm.toFixed(2)}`, | |
| category: category, | |
| views60m: views60m, | |
| views24h: views24h, | |
| views48h: views48h, | |
| creationDate: channel.snippet?.publishedAt, | |
| engagement: engagement, | |
| type: type | |
| }), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } catch (error: any) { | |
| return new Response(JSON.stringify({ error: error.message }), { status: 500 }); | |
| } | |
| }; | |