import type { OverlayTemplateData, OverlayVariableElementProps, } from '@server/entity/OverlayTemplate'; import { createHash } from 'crypto'; /** * Calculate SHA-256 hash of any input object * Ensures deterministic serialization for consistent hashing * Uses a custom replacer to handle undefined values and ensure deterministic key ordering */ function calculateInputHash(input: unknown): string { // Custom replacer function for deterministic serialization const replacer = (_key: string, value: unknown) => { // Convert undefined to null so it's preserved in the hash if (value === undefined) { return null; } // For objects (but not arrays), sort keys for deterministic ordering if (value !== null && typeof value === 'object' && !Array.isArray(value)) { const sorted: Record = {}; const keys = Object.keys(value).sort(); for (const key of keys) { sorted[key] = (value as Record)[key]; } return sorted; } return value; }; const normalized = JSON.stringify(input, replacer); return createHash('sha256').update(normalized).digest('hex'); } /** * Extract field names from application condition rules recursively */ function extractFieldsFromCondition( condition: | { sections?: { rules?: { field?: string }[]; sectionOperator?: string; }[]; } | null | undefined ): Set { const fields = new Set(); if (!condition || !condition.sections) { return fields; } for (const section of condition.sections) { if (section.rules) { for (const rule of section.rules) { if (rule.field) { fields.add(rule.field); } } } } return fields; } /** * Extract all context field names used by overlay templates * Examines both variable elements AND application conditions * This ensures hash changes when any field affecting overlay rendering changes */ export function extractUsedContextFields( templateDataArray: OverlayTemplateData[], applicationConditions?: ( | { sections?: { rules?: { field?: string }[]; sectionOperator?: string; }[]; } | null | undefined )[] ): Set { const usedFields = new Set(); // Extract fields from variable elements (for rendering) for (const templateData of templateDataArray) { for (const element of templateData.elements) { if (element.type === 'variable') { const props = element.properties as OverlayVariableElementProps; for (const segment of props.segments) { if (segment.type === 'variable' && segment.field) { usedFields.add(segment.field); } } } } } // Extract fields from application conditions (for template matching) // These fields affect WHICH templates apply, so they must be in the hash if (applicationConditions) { for (const condition of applicationConditions) { const conditionFields = extractFieldsFromCondition(condition); for (const field of conditionFields) { usedFields.add(field); } } } // Always include mediaType and isPlaceholder as they're fundamental to overlay rendering usedFields.add('mediaType'); usedFields.add('isPlaceholder'); return usedFields; } /** * Calculate hash for auto-generated poster inputs * Includes item IDs so poster regenerates when collection contents change * Includes template data so poster regenerates when template is modified */ export function calculatePosterInputHash(config: { templateId: number | null; templateData?: unknown; // Include template configuration for change detection itemIds: string[]; collectionName?: string; mediaType?: string; collectionType?: string; collectionSubtype?: string; additionalContext?: Record; personImageUrl?: string; }): string { return calculateInputHash({ templateId: config.templateId, templateData: config.templateData, // Hash the actual template content itemIds: [...config.itemIds].sort(), // Ensure sorted for consistency collectionName: config.collectionName, mediaType: config.mediaType, collectionType: config.collectionType, collectionSubtype: config.collectionSubtype, additionalContext: config.additionalContext, personImageUrl: config.personImageUrl, }); } /** * Calculate hash for wallpaper inputs (filename is sufficient) */ export function calculateWallpaperInputHash(filename: string): string { return createHash('sha256').update(filename).digest('hex'); } /** * Calculate hash for theme inputs (filename is sufficient) */ export function calculateThemeInputHash(filename: string): string { return createHash('sha256').update(filename).digest('hex'); } /** * Calculate hash for overlay inputs * Includes: * - Template IDs (which templates are applied) * - Template data (design: positions, colors, icon/image paths, etc.) * - Context fields actually used by the templates * * This ensures regeneration when: * - Different templates match * - Template design changes (including icon/image path changes) * - Context values change */ export function calculateOverlayInputHash(config: { templateIds: number[]; templateData: OverlayTemplateData[]; usedFields: Set; context: Record; }): string { // Extract only the context fields that are actually used const relevantContext: Record = {}; for (const field of config.usedFields) { relevantContext[field] = config.context[field]; } return calculateInputHash({ templateIds: [...config.templateIds].sort(), // Ensure sorted for consistency templateData: config.templateData, // Include template design (positions, colors, icon paths) context: relevantContext, // Only include fields actually used by templates }); }