/* RFC-0912: builds VideoObject JSON-LD nodes from SemanticBlock.video data for opted-in content videos. Reads variant-manifest-derived data populated by buildSemanticPageModelWith. Do not read the variant manifest directly — buildSemanticPageModelWith populates SemanticBlock.video before buildJsonLd runs. Do not emit VideoObject for blocks without the seo.videoObject opt-in. RFC-0912: initial implementation. */ import type { JsonLdContext } from "./context.ts"; import type { JsonLdNode } from "./types.ts"; function formatDuration(seconds: number): string { const totalSec = Math.round(seconds); const hours = Math.floor(totalSec / 3600); const minutes = Math.floor((totalSec % 3600) / 60); const secs = totalSec % 60; const parts = ["PT"]; if (hours > 0) parts.push(`${hours}H`); if (minutes > 0) parts.push(`${minutes}M`); parts.push(`${secs}S`); return parts.join(""); } export function buildVideoObjectNodes(context: JsonLdContext): JsonLdNode[] { const { page, webpageId } = context; const nodes: JsonLdNode[] = []; for (const block of page.blocks) { if (!block.video) continue; const { seo, manifest } = block.video; const nodeId = `${webpageId.replace("#/schema/webpage", "#/schema/video")}/${block.id}`; const node: JsonLdNode = { "@type": "VideoObject", "@id": nodeId, name: seo.name, description: seo.description, uploadDate: seo.uploadDate, thumbnailUrl: manifest.posterUrl, contentUrl: manifest.contentUrl, ...(manifest.durationSec != null ? { duration: formatDuration(manifest.durationSec) } : {}), }; nodes.push(node); } return nodes; }