| 1 | import { |
| 2 | enqueueHtmlThumbnail, |
| 3 | enqueueHtmlThumbnails, |
| 4 | getFreshHtmlThumbnailPaths, |
| 5 | waitForHtmlThumbnailTask, |
| 6 | type HtmlThumbnailRequest |
| 7 | } from '../io/thumbnails/html-thumbnail-service' |
| 8 | import { TEMPLATE_THUMBNAIL_RESOURCE_TYPE } from '@shared/thumbnail' |
| 9 | |
| 10 | const TEMPLATE_COVER_VARIANT = 'cover' |
| 11 | |
| 12 | export type TemplateThumbnailEntry = { |
| 13 | templateId: string |
| 14 | pageId?: string | null |
| 15 | sourcePath?: string | null |
| 16 | width?: number |
| 17 | height?: number |
| 18 | } |
| 19 | |
| 20 | function entryToRequest(entry: TemplateThumbnailEntry): HtmlThumbnailRequest | null { |
| 21 | const templateId = entry.templateId.trim() |
| 22 | const sourcePath = typeof entry.sourcePath === 'string' ? entry.sourcePath.trim() : '' |
| 23 | if (!templateId || !sourcePath) return null |
| 24 | return { |
| 25 | resourceType: TEMPLATE_THUMBNAIL_RESOURCE_TYPE, |
| 26 | resourceId: templateId, |
| 27 | variant: TEMPLATE_COVER_VARIANT, |
| 28 | sourcePath, |
| 29 | pageId: typeof entry.pageId === 'string' ? entry.pageId.trim() : '', |
| 30 | captureWidth: entry.width, |
| 31 | captureHeight: entry.height, |
| 32 | thumbnailWidth: 640, |
| 33 | thumbnailHeight: |
| 34 | entry.width && entry.height |
| 35 | ? Math.max(64, Math.round((640 * entry.height) / entry.width)) |
| 36 | : undefined |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | function entriesToRequests(entries: TemplateThumbnailEntry[]): HtmlThumbnailRequest[] { |
| 41 | const requests: HtmlThumbnailRequest[] = [] |
| 42 | for (const entry of entries) { |
| 43 | const request = entryToRequest(entry) |
| 44 | if (request) requests.push(request) |
| 45 | } |
| 46 | return requests |
| 47 | } |
| 48 | |
| 49 | export async function captureTemplateCoverThumbnail( |
| 50 | entry: TemplateThumbnailEntry |
| 51 | ): Promise<string | null> { |
| 52 | const request = entryToRequest(entry) |
| 53 | if (!request) return null |
| 54 | try { |
| 55 | const task = await enqueueHtmlThumbnail(request) |
| 56 | if (task.status === 'completed') return task.thumbnailPath |
| 57 | const completed = await waitForHtmlThumbnailTask( |
| 58 | request.resourceType, |
| 59 | request.resourceId, |
| 60 | request.variant |
| 61 | ) |
| 62 | return completed.thumbnailPath |
| 63 | } catch (error) { |
| 64 | console.warn('[template-thumbnail] cover capture failed', error) |
| 65 | return null |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | export async function warmTemplateCoverThumbnails( |
| 70 | entries: TemplateThumbnailEntry[], |
| 71 | delayMs = 300 |
| 72 | ): Promise<Map<string, string>> { |
| 73 | const requests = entriesToRequests(entries) |
| 74 | let freshMap: Map<string, string> |
| 75 | try { |
| 76 | freshMap = await getFreshHtmlThumbnailPaths(requests) |
| 77 | } catch (error) { |
| 78 | console.warn('[template-thumbnail] fresh thumbnail lookup failed', error) |
| 79 | return new Map() |
| 80 | } |
| 81 | |
| 82 | const missing = requests.filter((request) => !freshMap.has(request.resourceId)) |
| 83 | if (missing.length > 0) { |
| 84 | void enqueueHtmlThumbnails(missing, { delayMs }).catch((error) => { |
| 85 | console.warn('[template-thumbnail] background warmup failed', error) |
| 86 | }) |
| 87 | } |
| 88 | return freshMap |
| 89 | } |
| 90 |