cms.c2sgmbh/src/hooks/processFeaturedVideo.ts
Martin Porwoll 913897c87c feat: add comprehensive video feature with collections, hooks, and tests
Video Feature Implementation:
- Add Videos and VideoCategories collections with multi-tenant support
- Extend VideoBlock with library/upload/embed sources and playback options
- Add featuredVideo group to Posts collection with processed embed URLs

Hooks & Validation:
- Add processFeaturedVideo hook for URL parsing and privacy mode embedding
- Add createSlugValidationHook for tenant-scoped slug uniqueness
- Add video-utils library (parseVideoUrl, generateEmbedUrl, formatDuration)

Testing:
- Add 84 unit tests for video-utils (URL parsing, duration, embed generation)
- Add 14 integration tests for Videos collection CRUD and slug validation

Database:
- Migration for videos, video_categories tables with locales
- Migration for Posts featuredVideo processed fields
- Update payload internal tables for new collections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 10:48:33 +00:00

88 lines
2.3 KiB
TypeScript

/**
* Featured Video Processing Hook
*
* Verarbeitet featuredVideo.embedUrl in Posts:
* - Extrahiert Video-ID aus URL
* - Generiert normalisierte Embed-URL mit Privacy-Mode
*/
import type { CollectionBeforeChangeHook } from 'payload'
import { parseVideoUrl, generateEmbedUrl } from '../lib/video'
interface FeaturedVideoData {
enabled?: boolean
source?: 'library' | 'embed' | 'upload'
embedUrl?: string
video?: number | string
uploadedVideo?: number | string
autoplay?: boolean
muted?: boolean
replaceImage?: boolean
// Processed fields (added by this hook)
processedEmbedUrl?: string
extractedVideoId?: string
platform?: string
thumbnailUrl?: string
}
interface PostData {
featuredVideo?: FeaturedVideoData
[key: string]: unknown
}
/**
* Hook zum Verarbeiten von featuredVideo Embed-URLs
*
* - Extrahiert Video-ID und Plattform aus der URL
* - Generiert normalisierte Embed-URL mit Privacy-Mode (youtube-nocookie)
* - Speichert Thumbnail-URL für Fallback
*/
export const processFeaturedVideo: CollectionBeforeChangeHook<PostData> = async ({
data,
operation,
}) => {
// Nur wenn featuredVideo existiert und aktiviert ist
if (!data?.featuredVideo?.enabled) {
return data
}
const featuredVideo = data.featuredVideo
// Nur für embed source verarbeiten
if (featuredVideo.source !== 'embed' || !featuredVideo.embedUrl) {
return data
}
const embedUrl = featuredVideo.embedUrl.trim()
// URL parsen
const videoInfo = parseVideoUrl(embedUrl)
if (!videoInfo || videoInfo.platform === 'unknown') {
// URL konnte nicht geparst werden - unverändert lassen
console.warn(`[processFeaturedVideo] Could not parse video URL: ${embedUrl}`)
return data
}
// Video-Metadaten speichern
featuredVideo.extractedVideoId = videoInfo.videoId || undefined
featuredVideo.platform = videoInfo.platform
featuredVideo.thumbnailUrl = videoInfo.thumbnailUrl || undefined
// Embed-URL mit Privacy-Mode und Playback-Optionen generieren
const processedUrl = generateEmbedUrl(videoInfo, {
autoplay: featuredVideo.autoplay ?? false,
muted: featuredVideo.muted ?? true,
privacyMode: true, // Immer Privacy-Mode für DSGVO
showRelated: false, // Keine verwandten Videos
})
if (processedUrl) {
featuredVideo.processedEmbedUrl = processedUrl
}
return {
...data,
featuredVideo,
}
}