From 6ffe6e756cd22f73ad786cb1e606141bc4385023 Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Sat, 14 Feb 2026 12:32:31 +0000 Subject: [PATCH] feat: add bulk YouTube thumbnail download endpoint POST /api/youtube/thumbnails/bulk (Super-Admin only) Supports ?dryRun=true for preview. Downloads sequentially with 500ms delay. Co-Authored-By: Claude Opus 4.6 --- .../api/youtube/thumbnails/bulk/route.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/app/(payload)/api/youtube/thumbnails/bulk/route.ts diff --git a/src/app/(payload)/api/youtube/thumbnails/bulk/route.ts b/src/app/(payload)/api/youtube/thumbnails/bulk/route.ts new file mode 100644 index 0000000..f560dbc --- /dev/null +++ b/src/app/(payload)/api/youtube/thumbnails/bulk/route.ts @@ -0,0 +1,94 @@ +import { NextResponse, type NextRequest } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' +import { downloadAndUploadImage } from '@/lib/utils/media-download' +import { getYouTubeThumbnail } from '@/lib/utils/youtube' + +/** + * POST /api/youtube/thumbnails/bulk + * + * Bulk-Download von YouTube-Thumbnails für alle YouTubeContent-Einträge + * die noch kein Thumbnail haben. Erfordert Super-Admin-Rechte. + * + * Query-Parameter: + * - dryRun=true: Zeigt nur was passieren würde, ohne tatsächlich herunterzuladen + */ +export async function POST(req: NextRequest) { + try { + const payload = await getPayload({ config }) + + const { user } = await payload.auth({ headers: req.headers }) + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const typedUser = user as { isSuperAdmin?: boolean } + if (!typedUser.isSuperAdmin) { + return NextResponse.json({ error: 'Super-Admin erforderlich' }, { status: 403 }) + } + + const dryRun = req.nextUrl.searchParams.get('dryRun') === 'true' + + // Find all YouTubeContent without thumbnail that have a videoId + const contentWithoutThumbnail = await payload.find({ + collection: 'youtube-content', + where: { + thumbnail: { exists: false }, + 'youtube.videoId': { exists: true }, + }, + limit: 500, + depth: 0, + }) + + const results = { processed: 0, downloaded: 0, skipped: 0, errors: 0, dryRun } + + for (const doc of contentWithoutThumbnail.docs) { + results.processed++ + const videoId = doc.youtube?.videoId + if (!videoId) { + results.skipped++ + continue + } + + if (dryRun) { + results.downloaded++ + continue + } + + try { + const thumbnailUrl = getYouTubeThumbnail(videoId, 'hq') + const filename = `yt-thumb-${videoId}.jpg` + const tenantId = typeof doc.tenant === 'object' ? doc.tenant?.id : doc.tenant + + const mediaId = await downloadAndUploadImage(payload, { + url: thumbnailUrl, + filename, + alt: `Thumbnail: ${typeof doc.title === 'string' ? doc.title : doc.title?.de || videoId}`, + tenantId: tenantId || undefined, + }) + + if (mediaId) { + await payload.update({ + collection: 'youtube-content', + id: doc.id, + data: { thumbnail: mediaId }, + depth: 0, + }) + results.downloaded++ + } else { + results.errors++ + } + } catch { + results.errors++ + } + + // Rate-limit delay (500ms between downloads) + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + return NextResponse.json(results) + } catch (error) { + console.error('[yt-thumbnail-bulk] Error:', error) + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) + } +}