feat(youtube): add metrics sync cron endpoints

Add two cron endpoints for automated YouTube metrics syncing:
- /api/cron/youtube-metrics-sync (every 6 hours): syncs video performance
  metrics (views, likes, comments) for all active channels
- /api/cron/youtube-channel-sync (daily at 04:00 UTC): syncs channel-level
  statistics (subscribers, total views, video count)

Both endpoints follow the established cron pattern with CRON_SECRET auth,
concurrent execution guards, HEAD monitoring, and structured logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-02-14 13:27:05 +00:00
parent 5ddcd5ab45
commit 13507d1361
3 changed files with 211 additions and 0 deletions

View file

@ -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<NextResponse> {
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<NextResponse> {
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)

View file

@ -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<NextResponse> {
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<NextResponse> {
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)

View file

@ -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 * * *"
}
]
}