veza/fixtures/core/utils/faker-config.ts
2025-12-03 22:56:50 +01:00

484 lines
No EOL
15 KiB
TypeScript

import { faker } from '@faker-js/faker'
import { globalConfig } from '../config'
import type {
AudioGenre,
AudioMetadata,
UserRole,
ConversationType,
MessageType
} from '../schemas/database'
type AudioFormat = 'mp3' | 'wav' | 'flac' | 'aac' | 'ogg'
type AudioQuality = 'low' | 'medium' | 'high' | 'lossless'
/**
* Enhanced Faker configuration for Veza Platform
*/
export class VezaFaker {
private static instance: VezaFaker
private seed: string
private locale: string
constructor(seed?: string, locale?: string) {
this.seed = seed || globalConfig.seed
this.locale = locale || globalConfig.locale
// Configure faker with seed for reproducibility
faker.seed(this.hashSeed(this.seed))
}
static getInstance(seed?: string, locale?: string): VezaFaker {
if (!VezaFaker.instance) {
VezaFaker.instance = new VezaFaker(seed, locale)
}
return VezaFaker.instance
}
/**
* Hash seed string to number for faker
*/
private hashSeed(seed: string): number {
let hash = 0
for (let i = 0; i < seed.length; i++) {
const char = seed.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return Math.abs(hash)
}
/**
* Music-specific generators
*/
music = {
/**
* Generate realistic song titles
*/
songTitle: (): string => {
const patterns = [
() => faker.word.adjective() + ' ' + faker.word.noun(),
() => faker.word.noun() + ' in ' + faker.location.city(),
() => faker.word.verb() + ' ' + faker.word.adverb(),
() => faker.person.firstName() + "'s " + faker.word.noun(),
() => faker.color.human() + ' ' + faker.word.noun(),
() => 'The ' + faker.word.adjective() + ' ' + faker.word.noun(),
]
return faker.helpers.arrayElement(patterns)()
},
/**
* Generate artist names
*/
artistName: (): string => {
const patterns = [
() => faker.person.firstName() + ' ' + faker.person.lastName(),
() => faker.person.firstName(),
() => 'The ' + faker.word.noun() + 's',
() => faker.word.adjective() + ' ' + faker.word.noun(),
() => faker.person.lastName() + ' & ' + faker.person.lastName(),
() => faker.word.noun() + ' ' + faker.word.noun(),
]
return faker.helpers.arrayElement(patterns)()
},
/**
* Generate album titles
*/
albumTitle: (): string => {
const patterns = [
() => faker.word.adjective() + ' ' + faker.word.noun(),
() => faker.location.city() + ' ' + faker.word.noun(),
() => 'The ' + faker.word.adjective() + ' ' + faker.word.noun(),
() => faker.word.noun() + ' of ' + faker.word.noun(),
() => faker.date.recent().getFullYear().toString(),
]
return faker.helpers.arrayElement(patterns)()
},
/**
* Generate playlist names
*/
playlistName: (): string => {
const patterns = [
() => faker.word.adjective() + ' Vibes',
() => faker.word.noun() + ' Mix',
() => 'My ' + faker.word.adjective() + ' Playlist',
() => faker.date.month() + ' ' + faker.date.recent().getFullYear(),
() => faker.word.adjective() + ' ' + faker.word.noun() + 's',
() => 'Best of ' + faker.word.noun(),
]
return faker.helpers.arrayElement(patterns)()
},
/**
* Generate music genre
*/
genre: (): AudioGenre => {
return faker.helpers.arrayElement([
'rock', 'pop', 'jazz', 'classical', 'electronic', 'hip-hop',
'country', 'blues', 'reggae', 'folk', 'metal', 'punk', 'indie',
'ambient', 'techno'
] as AudioGenre[])
},
/**
* Generate realistic audio metadata
*/
audioMetadata: (genre?: AudioGenre): AudioMetadata => {
const baseMetadata = {
duration: faker.number.int({ min: 30, max: 600 }), // 30s to 10min
bitrate: faker.helpers.arrayElement([128, 192, 256, 320]), // kbps
sampleRate: faker.helpers.arrayElement([44100, 48000, 96000]), // Hz
channels: faker.helpers.arrayElement([1, 2]), // Mono or Stereo
format: faker.helpers.arrayElement(['mp3', 'wav', 'flac']) as AudioFormat,
quality: faker.helpers.arrayElement(['medium', 'high', 'lossless']) as AudioQuality,
bpm: faker.number.int({ min: 60, max: 200 }),
loudness: faker.number.float({ min: -30, max: -6, precision: 0.1 }),
}
// Calculate file size based on metadata
const bytesPerSecond = (baseMetadata.bitrate * 1000) / 8
const fileSize = Math.round(bytesPerSecond * baseMetadata.duration)
return { ...baseMetadata, fileSize }
},
/**
* Generate waveform data
*/
waveformData: (duration: number, samples: number = 200): number[] => {
const data: number[] = []
const samplesPerSecond = samples / duration
for (let i = 0; i < samples; i++) {
// Create realistic waveform with peaks and valleys
const time = i / samplesPerSecond
const base = Math.sin(time * 0.5) * 0.3
const noise = (faker.number.float({ min: -0.2, max: 0.2 }))
const peak = Math.random() > 0.95 ? faker.number.float({ min: 0.7, max: 1.0 }) : 0
data.push(Math.max(0, Math.min(1, base + noise + peak)))
}
return data
},
/**
* Generate music tags
*/
tags: (count: number = 3): string[] => {
const musicTags = [
'chill', 'upbeat', 'acoustic', 'electric', 'instrumental', 'vocal',
'experimental', 'mainstream', 'underground', 'vintage', 'modern',
'energetic', 'relaxing', 'emotional', 'party', 'study', 'workout',
'romantic', 'melancholic', 'happy', 'dark', 'bright', 'atmospheric'
]
return faker.helpers.arrayElements(musicTags, count)
}
}
/**
* User-specific generators
*/
user = {
/**
* Generate username variations
*/
username: (firstName?: string, lastName?: string): string => {
const first = firstName || faker.person.firstName()
const last = lastName || faker.person.lastName()
const patterns = [
() => first.toLowerCase() + last.toLowerCase(),
() => first.toLowerCase() + '.' + last.toLowerCase(),
() => first.toLowerCase() + '_' + last.toLowerCase(),
() => first.toLowerCase() + faker.number.int({ min: 1, max: 999 }),
() => first.toLowerCase() + last.toLowerCase() + faker.number.int({ min: 10, max: 99 }),
() => last.toLowerCase() + first.charAt(0).toLowerCase(),
]
return faker.helpers.arrayElement(patterns)()
},
/**
* Generate role-appropriate bio
*/
bio: (role: UserRole): string => {
const bios = {
admin: [
'Administrateur de la plateforme Veza',
'Gardien de la communauté musicale',
'Passionné de musique et de technologie'
],
artist: [
'Artiste indépendant passionné de musique',
'Compositeur et interprète',
'Créateur musical en constante évolution',
'Musicien cherchant à partager sa passion',
'Artiste émergent sur la scène musicale'
],
producer: [
'Producteur musical professionnel',
'Ingénieur du son et producteur',
'Spécialiste en production musicale',
'Créateur de beats et arrangements'
],
user: [
'Mélomane et amateur de musique',
'Passionné de découvertes musicales',
'Collectionneur de playlists',
'Amateur de concerts et festivals'
],
moderator: [
'Modérateur communautaire',
'Gardien de la bonne ambiance',
'Facilitateur des échanges musicaux'
]
}
return faker.helpers.arrayElement(bios[role])
},
/**
* Generate realistic location
*/
location: (): string => {
const frenchCities = [
'Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Nantes',
'Strasbourg', 'Montpellier', 'Bordeaux', 'Lille', 'Rennes',
'Reims', 'Le Havre', 'Toulon', 'Grenoble'
]
return faker.helpers.arrayElement(frenchCities)
}
}
/**
* Chat-specific generators
*/
chat = {
/**
* Generate conversation name
*/
conversationName: (type: ConversationType): string => {
if (type === 'direct') return '' // Direct messages don't have names
const groupNames = [
'Équipe Créative', 'Studio Session', 'Projet Musical', 'Collaboration',
'Mix & Master', 'Songwriting', 'Feedback Loop', 'Artistes Unis',
'Production Team', 'Sound Design', 'Remix Club', 'Beat Makers'
]
const channelNames = [
'général', 'annonces', 'musique', 'collaborations', 'feedback',
'technique', 'événements', 'showcase', 'random', 'aide'
]
return type === 'group'
? faker.helpers.arrayElement(groupNames)
: faker.helpers.arrayElement(channelNames)
},
/**
* Generate realistic message content
*/
messageContent: (type: MessageType = 'text'): string => {
if (type === 'system') {
const systemMessages = [
'a rejoint la conversation',
'a quitté la conversation',
'a changé le nom du groupe',
'a ajouté une photo de profil',
'a partagé un fichier'
]
return faker.helpers.arrayElement(systemMessages)
}
const musicMessages = [
'Salut ! Comment ça va ?',
'Tu as écouté le nouveau son ?',
'On fait une session studio demain ?',
'J\'ai une idée de mélodie géniale !',
'Quel est ton genre musical préféré ?',
'Cette prod est incroyable ! 🔥',
'On pourrait collaborer sur ce projet',
'Le mix sonne vraiment bien',
'Merci pour le feedback !',
'À quelle heure on se retrouve ?',
'Cette mélodie me donne des frissons',
'Le mastering est parfait',
'Bravo pour cette composition !',
'On devrait faire un featuring ensemble',
'Cette basse claque vraiment fort ! 💥'
]
return faker.helpers.arrayElement(musicMessages)
},
/**
* Generate emoji reactions
*/
reactionEmoji: (): string => {
const musicEmojis = ['🎵', '🎶', '🎤', '🎸', '🥁', '🎹', '🎺', '🎷', '🔥', '💥', '👏', '❤️', '😍', '🤩', '💯']
return faker.helpers.arrayElement(musicEmojis)
}
}
/**
* Date generators with realistic patterns
*/
date = {
/**
* Recent activity date
*/
recentActivity: (): Date => {
return faker.date.recent({ days: 7 })
},
/**
* User creation date (platform launch to now)
*/
userCreation: (): Date => {
return faker.date.between({
from: new Date('2024-01-01'),
to: new Date()
})
},
/**
* Content creation date (after user creation)
*/
contentCreation: (userCreatedAt: Date): Date => {
return faker.date.between({
from: userCreatedAt,
to: new Date()
})
},
/**
* Release date for music
*/
releaseDate: (): Date => {
return faker.date.between({
from: new Date('2020-01-01'),
to: new Date()
})
}
}
/**
* Statistics generators
*/
stats = {
/**
* User statistics based on role and creation date
*/
userStats: (role: UserRole, createdAt: Date): any => {
const daysSinceCreation = Math.floor((Date.now() - createdAt.getTime()) / (1000 * 60 * 60 * 24))
const activityMultiplier = Math.min(daysSinceCreation / 30, 12) // Max 12 months of activity
const baseStats = {
user: { tracks: 0, playlists: 2, followers: 5, following: 20, plays: 100 },
artist: { tracks: 15, playlists: 8, followers: 150, following: 50, plays: 2000 },
producer: { tracks: 25, playlists: 12, followers: 300, following: 80, plays: 5000 },
admin: { tracks: 5, playlists: 20, followers: 500, following: 100, plays: 1000 },
moderator: { tracks: 8, playlists: 15, followers: 200, following: 150, plays: 1500 }
}
const base = baseStats[role]
return {
tracksUploaded: Math.floor(base.tracks * activityMultiplier * faker.number.float({ min: 0.5, max: 2 })),
playlistsCreated: Math.floor(base.playlists * activityMultiplier * faker.number.float({ min: 0.8, max: 1.5 })),
followersCount: Math.floor(base.followers * activityMultiplier * faker.number.float({ min: 0.3, max: 3 })),
followingCount: Math.floor(base.following * activityMultiplier * faker.number.float({ min: 0.5, max: 1.8 })),
totalPlays: Math.floor(base.plays * activityMultiplier * faker.number.float({ min: 0.2, max: 5 })),
}
},
/**
* Audio track statistics
*/
audioStats: (createdAt: Date, isPopular: boolean = false): any => {
const daysSinceCreation = Math.floor((Date.now() - createdAt.getTime()) / (1000 * 60 * 60 * 24))
const timeMultiplier = Math.min(daysSinceCreation / 7, 52) // Max 52 weeks
const popularityMultiplier = isPopular ? faker.number.float({ min: 5, max: 20 }) : 1
const basePlays = faker.number.int({ min: 10, max: 500 })
const plays = Math.floor(basePlays * timeMultiplier * popularityMultiplier)
return {
plays,
likes: Math.floor(plays * faker.number.float({ min: 0.05, max: 0.15 })),
shares: Math.floor(plays * faker.number.float({ min: 0.01, max: 0.05 })),
downloads: Math.floor(plays * faker.number.float({ min: 0.02, max: 0.08 })),
comments: Math.floor(plays * faker.number.float({ min: 0.008, max: 0.03 })),
}
}
}
/**
* Utility methods
*/
utils = {
/**
* Generate a slug from text
*/
slug: (text: string): string => {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
},
/**
* Generate a realistic file URL
*/
fileUrl: (type: 'audio' | 'image', filename?: string): string => {
const baseUrl = globalConfig.paths.audioFiles
const actualFilename = filename || faker.system.fileName({ extensionCount: 1 })
return `${baseUrl}/${type}/${actualFilename}`
},
/**
* Weighted random selection
*/
weightedChoice: <T>(choices: Array<{ item: T; weight: number }>): T => {
const totalWeight = choices.reduce((sum, choice) => sum + choice.weight, 0)
let random = faker.number.float({ min: 0, max: totalWeight })
for (const choice of choices) {
random -= choice.weight
if (random <= 0) {
return choice.item
}
}
return choices[choices.length - 1]?.item || choices[0].item
}
}
}
/**
* Global faker instance with direct faker access
*/
export const vezaFaker = {
...VezaFaker.getInstance(),
// Direct faker access for compatibility
datatype: faker.datatype,
number: faker.number,
string: faker.string,
helpers: faker.helpers,
image: faker.image,
location: faker.location,
internet: faker.internet,
person: faker.person,
word: faker.word,
color: faker.color,
date: {
...VezaFaker.getInstance().date,
recent: faker.date.recent,
soon: faker.date.soon,
between: faker.date.between
}
}