484 lines
No EOL
15 KiB
TypeScript
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
|
|
}
|
|
} |