mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 13:54:11 +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",
|
||||
"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