mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
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:
parent
5ddcd5ab45
commit
13507d1361
3 changed files with 211 additions and 0 deletions
87
src/app/(payload)/api/cron/youtube-channel-sync/route.ts
Normal file
87
src/app/(payload)/api/cron/youtube-channel-sync/route.ts
Normal 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)
|
||||||
116
src/app/(payload)/api/cron/youtube-metrics-sync/route.ts
Normal file
116
src/app/(payload)/api/cron/youtube-metrics-sync/route.ts
Normal 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)
|
||||||
|
|
@ -12,6 +12,14 @@
|
||||||
{
|
{
|
||||||
"path": "/api/cron/send-reports",
|
"path": "/api/cron/send-reports",
|
||||||
"schedule": "0 * * * *"
|
"schedule": "0 * * * *"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/api/cron/youtube-metrics-sync",
|
||||||
|
"schedule": "0 */6 * * *"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/api/cron/youtube-channel-sync",
|
||||||
|
"schedule": "0 4 * * *"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue