harden media
This commit is contained in:
@@ -27,8 +27,22 @@ export interface MediaStoragePort {
|
||||
uploadMedia(buffer: Buffer, options: UploadOptions): Promise<UploadResult>;
|
||||
|
||||
/**
|
||||
* Delete a media file by URL
|
||||
* @param url Media URL to delete
|
||||
* Delete a media file by storage key
|
||||
* @param storageKey Storage key to delete
|
||||
*/
|
||||
deleteMedia(url: string): Promise<void>;
|
||||
deleteMedia(storageKey: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get file bytes as Buffer
|
||||
* @param storageKey Storage key
|
||||
* @returns Buffer or null if not found
|
||||
*/
|
||||
getBytes?(storageKey: string): Promise<Buffer | null>;
|
||||
|
||||
/**
|
||||
* Get file metadata
|
||||
* @param storageKey Storage key
|
||||
* @returns File metadata or null if not found
|
||||
*/
|
||||
getMetadata?(storageKey: string): Promise<{ size: number; contentType: string } | null>;
|
||||
}
|
||||
255
core/media/domain/services/MediaGenerationService.ts
Normal file
255
core/media/domain/services/MediaGenerationService.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
/**
|
||||
* Core Domain Service: MediaGenerationService
|
||||
*
|
||||
* Encapsulates business logic for generating media assets (SVGs) using Faker.
|
||||
* Ensures deterministic results by seeding Faker with entity IDs.
|
||||
*/
|
||||
export class MediaGenerationService {
|
||||
/**
|
||||
* Generates a deterministic SVG avatar for a driver
|
||||
*/
|
||||
generateDriverAvatar(driverId: string): string {
|
||||
faker.seed(this.hashCode(driverId));
|
||||
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
const initials = ((firstName?.[0] || 'D') + (lastName?.[0] || 'R')).toUpperCase();
|
||||
|
||||
const primaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const secondaryColor = faker.color.rgb({ format: 'hex' });
|
||||
|
||||
const patterns = ['gradient', 'stripes', 'circles', 'diamond'];
|
||||
const pattern = faker.helpers.arrayElement(patterns);
|
||||
|
||||
let patternSvg = '';
|
||||
switch (pattern) {
|
||||
case 'gradient':
|
||||
patternSvg = `
|
||||
<defs>
|
||||
<linearGradient id="grad-${driverId}" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${secondaryColor};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100" height="100" rx="50" fill="url(#grad-${driverId})"/>
|
||||
`;
|
||||
break;
|
||||
case 'stripes':
|
||||
patternSvg = `
|
||||
<rect width="100" height="100" rx="50" fill="${primaryColor}"/>
|
||||
<rect x="0" y="0" width="50" height="100" rx="50" fill="${secondaryColor}" opacity="0.3"/>
|
||||
`;
|
||||
break;
|
||||
case 'circles':
|
||||
patternSvg = `
|
||||
<rect width="100" height="100" rx="50" fill="${primaryColor}"/>
|
||||
<circle cx="30" cy="30" r="15" fill="${secondaryColor}" opacity="0.4"/>
|
||||
<circle cx="70" cy="70" r="10" fill="${secondaryColor}" opacity="0.4"/>
|
||||
`;
|
||||
break;
|
||||
case 'diamond':
|
||||
patternSvg = `
|
||||
<rect width="100" height="100" rx="50" fill="${primaryColor}"/>
|
||||
<path d="M50 20 L80 50 L50 80 L20 50 Z" fill="${secondaryColor}" opacity="0.3"/>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
${patternSvg}
|
||||
<text x="50" y="58" font-family="Arial, sans-serif" font-size="36" font-weight="bold" fill="white" text-anchor="middle" letter-spacing="2">${initials}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a deterministic SVG logo for a team
|
||||
* Now includes team name initials for better branding
|
||||
*/
|
||||
generateTeamLogo(teamId: string): string {
|
||||
faker.seed(this.hashCode(teamId));
|
||||
|
||||
const primaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const secondaryColor = faker.color.rgb({ format: 'hex' });
|
||||
|
||||
// Generate deterministic initials from seeded faker data
|
||||
// This creates consistent initials for the same teamId
|
||||
const adjective = faker.company.buzzAdjective();
|
||||
const noun = faker.company.catchPhraseNoun();
|
||||
const initials = ((adjective?.[0] || 'T') + (noun?.[0] || 'M')).toUpperCase();
|
||||
|
||||
const shapes = ['circle', 'square', 'triangle', 'hexagon'];
|
||||
const shape = faker.helpers.arrayElement(shapes);
|
||||
|
||||
let shapeSvg = '';
|
||||
switch (shape) {
|
||||
case 'circle':
|
||||
shapeSvg = `<circle cx="20" cy="16" r="10" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'square':
|
||||
shapeSvg = `<rect x="10" y="6" width="20" height="20" rx="4" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'triangle':
|
||||
shapeSvg = `<path d="M20 6 L30 26 L10 26 Z" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'hexagon':
|
||||
shapeSvg = `<path d="M20 6 L28 10 L28 22 L20 26 L12 22 L12 10 Z" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="120" height="40" viewBox="0 0 120 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad-${teamId}" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${secondaryColor};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="120" height="40" rx="8" fill="#1e293b"/>
|
||||
${shapeSvg}
|
||||
<rect x="40" y="12" width="4" height="16" rx="1" fill="${secondaryColor}" opacity="0.6"/>
|
||||
<rect x="48" y="8" width="4" height="24" rx="1" fill="${primaryColor}" opacity="0.8"/>
|
||||
<rect x="56" y="12" width="4" height="16" rx="1" fill="${secondaryColor}" opacity="0.6"/>
|
||||
<text x="85" y="24" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white" text-anchor="middle">${initials}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a deterministic SVG logo for a league
|
||||
* Updated to use the same faker style as team logos for consistency
|
||||
*/
|
||||
generateLeagueLogo(leagueId: string): string {
|
||||
faker.seed(this.hashCode(leagueId));
|
||||
|
||||
const primaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const secondaryColor = faker.color.rgb({ format: 'hex' });
|
||||
|
||||
// Generate deterministic initials from seeded faker data
|
||||
// This creates consistent initials for the same leagueId
|
||||
const adjective = faker.company.buzzAdjective();
|
||||
const noun = faker.company.catchPhraseNoun();
|
||||
const initials = ((adjective?.[0] || 'L') + (noun?.[0] || 'G')).toUpperCase();
|
||||
|
||||
const shapes = ['circle', 'square', 'triangle', 'hexagon'];
|
||||
const shape = faker.helpers.arrayElement(shapes);
|
||||
|
||||
let shapeSvg = '';
|
||||
switch (shape) {
|
||||
case 'circle':
|
||||
shapeSvg = `<circle cx="20" cy="16" r="10" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'square':
|
||||
shapeSvg = `<rect x="10" y="6" width="20" height="20" rx="4" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'triangle':
|
||||
shapeSvg = `<path d="M20 6 L30 26 L10 26 Z" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
case 'hexagon':
|
||||
shapeSvg = `<path d="M20 6 L28 10 L28 22 L20 26 L12 22 L12 10 Z" fill="${primaryColor}" opacity="0.8"/>`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="120" height="40" viewBox="0 0 120 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad-${leagueId}" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${secondaryColor};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="120" height="40" rx="8" fill="#1e293b"/>
|
||||
${shapeSvg}
|
||||
<rect x="40" y="12" width="4" height="16" rx="1" fill="${secondaryColor}" opacity="0.6"/>
|
||||
<rect x="48" y="8" width="4" height="24" rx="1" fill="${primaryColor}" opacity="0.8"/>
|
||||
<rect x="56" y="12" width="4" height="16" rx="1" fill="${secondaryColor}" opacity="0.6"/>
|
||||
<text x="85" y="24" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white" text-anchor="middle">${initials}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a deterministic SVG cover for a league
|
||||
*/
|
||||
generateLeagueCover(leagueId: string): string {
|
||||
faker.seed(this.hashCode(leagueId));
|
||||
|
||||
const primaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const secondaryColor = faker.color.rgb({ format: 'hex' });
|
||||
|
||||
return `
|
||||
<svg width="800" height="200" viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad-${leagueId}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:${primaryColor};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${secondaryColor};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="800" height="200" fill="url(#grad-${leagueId})"/>
|
||||
<rect width="800" height="200" fill="black" opacity="0.2"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a simple PNG placeholder (base64 encoded)
|
||||
* In production, this would serve actual PNG files from public assets
|
||||
*/
|
||||
generateDefaultPNG(variant: string): Buffer {
|
||||
// For now, generate a simple colored square as PNG placeholder
|
||||
// In production, this would read actual PNG files
|
||||
faker.seed(this.hashCode(variant));
|
||||
|
||||
const color = faker.color.rgb({ format: 'hex' });
|
||||
|
||||
// Parse the hex color
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
// Create a minimal valid PNG (1x1 pixel) with the variant color
|
||||
// This is a very basic PNG - in production you'd serve real files
|
||||
// PNG header and minimal data for a 1x1 RGB pixel
|
||||
const pngHeader = Buffer.from([
|
||||
// PNG signature
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
// IHDR chunk (13 bytes)
|
||||
0x00, 0x00, 0x00, 0x0D, // Length: 13
|
||||
0x49, 0x48, 0x44, 0x52, // Type: IHDR
|
||||
0x00, 0x00, 0x00, 0x01, // Width: 1
|
||||
0x00, 0x00, 0x00, 0x01, // Height: 1
|
||||
0x08, // Bit depth: 8
|
||||
0x02, // Color type: RGB
|
||||
0x00, // Compression method
|
||||
0x00, // Filter method
|
||||
0x00, // Interlace method
|
||||
0x00, 0x00, 0x00, 0x00, // CRC (placeholder, simplified)
|
||||
// IDAT chunk (image data)
|
||||
0x00, 0x00, 0x00, 0x07, // Length: 7
|
||||
0x49, 0x44, 0x41, 0x54, // Type: IDAT
|
||||
0x08, 0x1D, // Zlib header
|
||||
0x01, // Deflate block header
|
||||
r, g, b, // RGB pixel data
|
||||
0x00, 0x00, 0x00, 0x00, // CRC (placeholder)
|
||||
// IEND chunk
|
||||
0x00, 0x00, 0x00, 0x00, // Length: 0
|
||||
0x49, 0x45, 0x4E, 0x44, // Type: IEND
|
||||
0xAE, 0x42, 0x60, 0x82 // CRC (placeholder)
|
||||
]);
|
||||
|
||||
return pngHeader;
|
||||
}
|
||||
|
||||
private hashCode(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user