diff --git a/src/app/(payload)/api/cron/youtube-channel-sync/route.ts b/src/app/(payload)/api/cron/youtube-channel-sync/route.ts new file mode 100644 index 0000000..1c08d49 --- /dev/null +++ b/src/app/(payload)/api/cron/youtube-channel-sync/route.ts @@ -0,0 +1,87 @@ +// src/app/(payload)/api/cron/youtube-channel-sync/route.ts +// Syncs channel-level statistics (subscribers, views, video count) for all active YouTube channels. + +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' + +import { ChannelMetricsSyncService } from '@/lib/integrations/youtube/ChannelMetricsSyncService' + +const CRON_SECRET = process.env.CRON_SECRET + +// Monitoring state +let isRunning = false +let lastRunAt: Date | null = null + +/** + * GET /api/cron/youtube-channel-sync + * Syncs channel-level metrics for all active YouTube channels. + * Scheduled daily at 04:00 UTC via Vercel Cron. + */ +export async function GET(request: NextRequest): Promise { + if (CRON_SECRET) { + const authHeader = request.headers.get('authorization') + + if (authHeader !== `Bearer ${CRON_SECRET}`) { + console.warn('[Cron] Unauthorized request to youtube-channel-sync') + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + } + + if (isRunning) { + return NextResponse.json( + { error: 'Channel metrics sync already in progress' }, + { status: 423 }, + ) + } + + isRunning = true + const startedAt = new Date() + + try { + const payload = await getPayload({ config }) + const service = new ChannelMetricsSyncService(payload) + + console.log('[Cron] Starting channel metrics sync') + + const result = await service.syncAllChannels() + + lastRunAt = startedAt + const duration = Date.now() - startedAt.getTime() + + console.log( + `[Cron] Channel metrics sync completed: ${result.channelsSynced} channels synced, ${result.errors.length} errors, ${duration}ms`, + ) + + return NextResponse.json({ + success: result.success, + channelsSynced: result.channelsSynced, + errors: result.errors, + duration, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error('[Cron] youtube-channel-sync error:', error) + + return NextResponse.json({ error: message }, { status: 500 }) + } finally { + isRunning = false + } +} + +/** + * HEAD /api/cron/youtube-channel-sync + * Status check for monitoring. + */ +export async function HEAD(): Promise { + return new NextResponse(null, { + status: isRunning ? 423 : 200, + headers: { + 'X-Running': isRunning.toString(), + 'X-Last-Run': lastRunAt?.toISOString() || 'never', + }, + }) +} + +export const dynamic = 'force-dynamic' +export const maxDuration = 120 // 2 minutes max (Vercel) diff --git a/src/app/(payload)/api/cron/youtube-metrics-sync/route.ts b/src/app/(payload)/api/cron/youtube-metrics-sync/route.ts new file mode 100644 index 0000000..68cf944 --- /dev/null +++ b/src/app/(payload)/api/cron/youtube-metrics-sync/route.ts @@ -0,0 +1,116 @@ +// src/app/(payload)/api/cron/youtube-metrics-sync/route.ts +// Syncs video-level performance metrics (views, likes, comments) for all active YouTube channels. + +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' + +import { VideoMetricsSyncService } from '@/lib/integrations/youtube/VideoMetricsSyncService' + +const CRON_SECRET = process.env.CRON_SECRET + +// Monitoring state +let isRunning = false +let lastRunAt: Date | null = null + +/** + * GET /api/cron/youtube-metrics-sync + * Syncs video performance metrics for all active YouTube channels. + * Scheduled every 6 hours via Vercel Cron. + */ +export async function GET(request: NextRequest): Promise { + if (CRON_SECRET) { + const authHeader = request.headers.get('authorization') + + if (authHeader !== `Bearer ${CRON_SECRET}`) { + console.warn('[Cron] Unauthorized request to youtube-metrics-sync') + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + } + + if (isRunning) { + return NextResponse.json( + { error: 'Video metrics sync already in progress' }, + { status: 423 }, + ) + } + + isRunning = true + const startedAt = new Date() + + try { + const payload = await getPayload({ config }) + const service = new VideoMetricsSyncService(payload) + + // Find all active YouTube channels + const channels = await payload.find({ + collection: 'youtube-channels', + where: { + status: { equals: 'active' }, + }, + limit: 0, + depth: 0, + }) + + console.log(`[Cron] Starting video metrics sync for ${channels.docs.length} channels`) + + let totalSynced = 0 + const errors: string[] = [] + + for (const channel of channels.docs) { + const channelName = (channel as any).name ?? `ID ${channel.id}` + + try { + const result = await service.syncMetrics(channel.id as number) + + totalSynced += result.syncedCount + + if (result.errors.length > 0) { + errors.push(`Channel "${channelName}": ${result.errors.join('; ')}`) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + errors.push(`Channel "${channelName}": ${message}`) + } + } + + lastRunAt = startedAt + const duration = Date.now() - startedAt.getTime() + + console.log( + `[Cron] Video metrics sync completed: ${totalSynced} videos synced, ${errors.length} errors, ${duration}ms`, + ) + + return NextResponse.json({ + success: errors.length === 0, + totalSynced, + channels: channels.docs.length, + errors, + duration, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error('[Cron] youtube-metrics-sync error:', error) + + return NextResponse.json({ error: message }, { status: 500 }) + } finally { + isRunning = false + } +} + +/** + * HEAD /api/cron/youtube-metrics-sync + * Status check for monitoring. + */ +export async function HEAD(): Promise { + return new NextResponse(null, { + status: isRunning ? 423 : 200, + headers: { + 'X-Running': isRunning.toString(), + 'X-Last-Run': lastRunAt?.toISOString() || 'never', + }, + }) +} + +export const dynamic = 'force-dynamic' +export const maxDuration = 300 // 5 minutes max (Vercel) diff --git a/vercel.json b/vercel.json index 2fb533a..bf7a58b 100644 --- a/vercel.json +++ b/vercel.json @@ -12,6 +12,14 @@ { "path": "/api/cron/send-reports", "schedule": "0 * * * *" + }, + { + "path": "/api/cron/youtube-metrics-sync", + "schedule": "0 */6 * * *" + }, + { + "path": "/api/cron/youtube-channel-sync", + "schedule": "0 4 * * *" } ] }