500 lines
No EOL
17 KiB
TypeScript
500 lines
No EOL
17 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid'
|
|
import { vezaFaker } from '../utils/faker-config'
|
|
import { globalConfig } from '../config'
|
|
import type {
|
|
Audio,
|
|
AudioGenre,
|
|
AudioMetadata,
|
|
Playlist,
|
|
PlaylistVisibility
|
|
} from '../schemas/database'
|
|
|
|
/**
|
|
* Audio generation options
|
|
*/
|
|
export interface AudioGenerationOptions {
|
|
genre?: AudioGenre
|
|
uploadedById?: string
|
|
albumId?: string
|
|
albumTitle?: string
|
|
isPublic?: boolean
|
|
isExplicit?: boolean
|
|
downloadEnabled?: boolean
|
|
withWaveform?: boolean
|
|
withCover?: boolean
|
|
withLyrics?: boolean
|
|
isPopular?: boolean
|
|
releaseDate?: Date
|
|
}
|
|
|
|
/**
|
|
* Album data structure
|
|
*/
|
|
export interface Album {
|
|
id: string
|
|
title: string
|
|
artist: string
|
|
genre: AudioGenre
|
|
releaseDate: Date
|
|
coverImageUrl?: string
|
|
tracks: Audio[]
|
|
totalDuration: number
|
|
isExplicit: boolean
|
|
}
|
|
|
|
/**
|
|
* Playlist generation options
|
|
*/
|
|
export interface PlaylistGenerationOptions {
|
|
createdById?: string
|
|
trackCount?: number
|
|
visibility?: PlaylistVisibility
|
|
isCollaborative?: boolean
|
|
withCover?: boolean
|
|
genre?: AudioGenre
|
|
theme?: 'workout' | 'chill' | 'party' | 'focus' | 'random'
|
|
}
|
|
|
|
/**
|
|
* Advanced audio content generator for Veza Platform
|
|
*/
|
|
export class AudioGenerator {
|
|
private static generatedTracks: Map<string, Audio> = new Map()
|
|
private static generatedAlbums: Map<string, Album> = new Map()
|
|
private static generatedPlaylists: Map<string, Playlist> = new Map()
|
|
private static usedSlugs: Set<string> = new Set()
|
|
|
|
/**
|
|
* Generate a single audio track
|
|
*/
|
|
static generate(options: AudioGenerationOptions = {}): Audio {
|
|
const genre = options.genre || vezaFaker.music.genre()
|
|
const title = vezaFaker.music.songTitle()
|
|
const artist = vezaFaker.music.artistName()
|
|
const slug = this.generateUniqueSlug(title, artist)
|
|
const metadata = vezaFaker.music.audioMetadata(genre)
|
|
const createdAt = vezaFaker.date.contentCreation(new Date('2024-01-01'))
|
|
const releaseDate = options.releaseDate || vezaFaker.date.releaseDate()
|
|
|
|
const track: Audio = {
|
|
id: uuidv4(),
|
|
title,
|
|
slug,
|
|
artist,
|
|
albumId: options.albumId,
|
|
albumTitle: options.albumTitle,
|
|
genre,
|
|
subgenre: this.generateSubgenre(genre),
|
|
tags: vezaFaker.music.tags(),
|
|
description: this.generateTrackDescription(title, artist, genre),
|
|
lyrics: options.withLyrics !== false ? this.generateLyrics() : undefined,
|
|
metadata,
|
|
waveformData: options.withWaveform !== false ? vezaFaker.music.waveformData(metadata.duration) : undefined,
|
|
fileUrl: vezaFaker.utils.fileUrl('audio', `${slug}.${metadata.format}`),
|
|
coverImageUrl: options.withCover !== false ? vezaFaker.utils.fileUrl('image', `${slug}-cover.jpg`) : undefined,
|
|
uploadedById: options.uploadedById || uuidv4(), // Will be replaced with real user ID
|
|
isPublic: options.isPublic ?? vezaFaker.datatype.boolean({ probability: 0.8 }),
|
|
isExplicit: options.isExplicit ?? vezaFaker.datatype.boolean({ probability: 0.1 }),
|
|
downloadEnabled: options.downloadEnabled ?? vezaFaker.datatype.boolean({ probability: 0.3 }),
|
|
stats: vezaFaker.stats.audioStats(createdAt, options.isPopular),
|
|
releaseDate,
|
|
createdAt,
|
|
updatedAt: vezaFaker.date.between({ from: createdAt, to: new Date() }),
|
|
}
|
|
|
|
this.generatedTracks.set(track.id, track)
|
|
return track
|
|
}
|
|
|
|
/**
|
|
* Generate multiple tracks
|
|
*/
|
|
static generateBatch(count: number, options: AudioGenerationOptions = {}): Audio[] {
|
|
const tracks: Audio[] = []
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
tracks.push(this.generate(options))
|
|
}
|
|
|
|
return tracks
|
|
}
|
|
|
|
/**
|
|
* Generate a complete album with tracks
|
|
*/
|
|
static generateAlbum(trackCount: number = 10, options: Partial<AudioGenerationOptions> = {}): Album {
|
|
const albumId = uuidv4()
|
|
const albumTitle = vezaFaker.music.albumTitle()
|
|
const artist = vezaFaker.music.artistName()
|
|
const genre = options.genre || vezaFaker.music.genre()
|
|
const releaseDate = options.releaseDate || vezaFaker.date.releaseDate()
|
|
const isExplicit = options.isExplicit ?? vezaFaker.datatype.boolean({ probability: 0.15 })
|
|
|
|
const tracks: Audio[] = []
|
|
let totalDuration = 0
|
|
|
|
for (let i = 0; i < trackCount; i++) {
|
|
const track = this.generate({
|
|
...options,
|
|
albumId,
|
|
albumTitle,
|
|
genre: i === 0 ? genre : (vezaFaker.datatype.boolean({ probability: 0.7 }) ? genre : vezaFaker.music.genre()),
|
|
uploadedById: options.uploadedById || uuidv4(),
|
|
isExplicit: isExplicit || vezaFaker.datatype.boolean({ probability: 0.05 }),
|
|
releaseDate,
|
|
})
|
|
|
|
// Override artist for consistency (80% same artist, 20% featuring)
|
|
if (vezaFaker.datatype.boolean({ probability: 0.8 })) {
|
|
track.artist = artist
|
|
} else {
|
|
track.artist = `${artist} feat. ${vezaFaker.music.artistName()}`
|
|
}
|
|
|
|
tracks.push(track)
|
|
totalDuration += track.metadata.duration
|
|
}
|
|
|
|
const album: Album = {
|
|
id: albumId,
|
|
title: albumTitle,
|
|
artist,
|
|
genre,
|
|
releaseDate,
|
|
coverImageUrl: vezaFaker.utils.fileUrl('image', `${albumId}-album-cover.jpg`),
|
|
tracks,
|
|
totalDuration,
|
|
isExplicit
|
|
}
|
|
|
|
this.generatedAlbums.set(albumId, album)
|
|
return album
|
|
}
|
|
|
|
/**
|
|
* Generate multiple albums
|
|
*/
|
|
static generateAlbums(count: number, options: Partial<AudioGenerationOptions> = {}): Album[] {
|
|
const albums: Album[] = []
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const trackCount = vezaFaker.number.int({ min: 3, max: 15 })
|
|
albums.push(this.generateAlbum(trackCount, options))
|
|
}
|
|
|
|
return albums
|
|
}
|
|
|
|
/**
|
|
* Generate a playlist
|
|
*/
|
|
static generatePlaylist(options: PlaylistGenerationOptions = {}): Playlist {
|
|
const trackCount = options.trackCount || vezaFaker.number.int({ min: 5, max: 50 })
|
|
const theme = options.theme || vezaFaker.helpers.arrayElement(['workout', 'chill', 'party', 'focus', 'random'])
|
|
const title = this.generatePlaylistTitle(theme)
|
|
const slug = this.generateUniqueSlug(title)
|
|
const createdAt = vezaFaker.date.contentCreation(new Date('2024-01-01'))
|
|
|
|
// Generate track IDs (will be replaced with real track IDs)
|
|
const trackIds = Array.from({ length: trackCount }, () => uuidv4())
|
|
|
|
const playlist: Playlist = {
|
|
id: uuidv4(),
|
|
title,
|
|
slug,
|
|
description: this.generatePlaylistDescription(title, theme),
|
|
coverImageUrl: options.withCover !== false ? vezaFaker.utils.fileUrl('image', `${slug}-playlist.jpg`) : undefined,
|
|
createdById: options.createdById || uuidv4(),
|
|
visibility: options.visibility || vezaFaker.helpers.arrayElement(['public', 'unlisted', 'private']),
|
|
isCollaborative: options.isCollaborative ?? vezaFaker.datatype.boolean({ probability: 0.1 }),
|
|
tags: this.generatePlaylistTags(theme),
|
|
trackIds,
|
|
stats: {
|
|
followers: vezaFaker.number.int({ min: 0, max: 500 }),
|
|
plays: vezaFaker.number.int({ min: 0, max: 10000 }),
|
|
shares: vezaFaker.number.int({ min: 0, max: 100 }),
|
|
},
|
|
createdAt,
|
|
updatedAt: vezaFaker.date.between({ from: createdAt, to: new Date() }),
|
|
}
|
|
|
|
this.generatedPlaylists.set(playlist.id, playlist)
|
|
return playlist
|
|
}
|
|
|
|
/**
|
|
* Generate multiple playlists
|
|
*/
|
|
static generatePlaylists(count: number, options: PlaylistGenerationOptions = {}): Playlist[] {
|
|
const playlists: Playlist[] = []
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
playlists.push(this.generatePlaylist(options))
|
|
}
|
|
|
|
return playlists
|
|
}
|
|
|
|
/**
|
|
* Generate genre-specific content
|
|
*/
|
|
static generateGenreCollection(genre: AudioGenre, trackCount: number = 20): {
|
|
tracks: Audio[]
|
|
albums: Album[]
|
|
playlists: Playlist[]
|
|
} {
|
|
// Generate individual tracks
|
|
const tracks = this.generateBatch(Math.floor(trackCount * 0.6), { genre })
|
|
|
|
// Generate albums
|
|
const albumCount = Math.floor(trackCount * 0.3 / 8) // Average 8 tracks per album
|
|
const albums = this.generateAlbums(albumCount, { genre })
|
|
|
|
// Add album tracks to tracks collection
|
|
albums.forEach(album => {
|
|
tracks.push(...album.tracks)
|
|
})
|
|
|
|
// Generate playlists
|
|
const playlistCount = Math.max(1, Math.floor(trackCount / 10))
|
|
const playlists = this.generatePlaylists(playlistCount, { genre })
|
|
|
|
return { tracks, albums, playlists }
|
|
}
|
|
|
|
/**
|
|
* Generate content for specific user roles
|
|
*/
|
|
static generateUserContent(userId: string, role: 'artist' | 'producer' | 'user'): {
|
|
tracks: Audio[]
|
|
albums: Album[]
|
|
playlists: Playlist[]
|
|
} {
|
|
const content = {
|
|
artist: () => {
|
|
const trackCount = vezaFaker.number.int({ min: 5, max: 30 })
|
|
const albumCount = vezaFaker.number.int({ min: 1, max: 4 })
|
|
const playlistCount = vezaFaker.number.int({ min: 2, max: 8 })
|
|
|
|
return {
|
|
tracks: this.generateBatch(trackCount, {
|
|
uploadedById: userId,
|
|
isPopular: vezaFaker.datatype.boolean({ probability: 0.3 })
|
|
}),
|
|
albums: this.generateAlbums(albumCount, { uploadedById: userId }),
|
|
playlists: this.generatePlaylists(playlistCount, { createdById: userId })
|
|
}
|
|
},
|
|
producer: () => {
|
|
const trackCount = vezaFaker.number.int({ min: 10, max: 50 })
|
|
const albumCount = vezaFaker.number.int({ min: 2, max: 6 })
|
|
const playlistCount = vezaFaker.number.int({ min: 5, max: 15 })
|
|
|
|
return {
|
|
tracks: this.generateBatch(trackCount, {
|
|
uploadedById: userId,
|
|
genre: vezaFaker.helpers.arrayElement(['electronic', 'hip-hop', 'pop']),
|
|
downloadEnabled: true
|
|
}),
|
|
albums: this.generateAlbums(albumCount, { uploadedById: userId }),
|
|
playlists: this.generatePlaylists(playlistCount, {
|
|
createdById: userId,
|
|
isCollaborative: vezaFaker.datatype.boolean({ probability: 0.3 })
|
|
})
|
|
}
|
|
},
|
|
user: () => ({
|
|
tracks: [],
|
|
albums: [],
|
|
playlists: this.generatePlaylists(vezaFaker.number.int({ min: 1, max: 5 }), {
|
|
createdById: userId,
|
|
visibility: vezaFaker.helpers.arrayElement(['private', 'unlisted', 'public'])
|
|
})
|
|
})
|
|
}
|
|
|
|
return content[role]()
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
private static generateUniqueSlug(title: string, artist?: string): string {
|
|
let baseSlug = vezaFaker.utils.slug(artist ? `${artist} ${title}` : title)
|
|
let slug = baseSlug
|
|
let counter = 1
|
|
|
|
while (this.usedSlugs.has(slug)) {
|
|
slug = `${baseSlug}-${counter}`
|
|
counter++
|
|
}
|
|
|
|
this.usedSlugs.add(slug)
|
|
return slug
|
|
}
|
|
|
|
private static generateSubgenre(genre: AudioGenre): string | undefined {
|
|
const subgenres: Record<AudioGenre, string[]> = {
|
|
rock: ['alternative', 'classic', 'indie', 'progressive', 'punk'],
|
|
pop: ['synth-pop', 'indie-pop', 'electro-pop', 'dance-pop'],
|
|
jazz: ['smooth', 'fusion', 'bebop', 'swing', 'contemporary'],
|
|
electronic: ['house', 'techno', 'ambient', 'dubstep', 'trance'],
|
|
'hip-hop': ['trap', 'boom-bap', 'conscious', 'mumble', 'old-school'],
|
|
classical: ['baroque', 'romantic', 'contemporary', 'chamber'],
|
|
country: ['modern', 'traditional', 'bluegrass', 'folk'],
|
|
blues: ['electric', 'acoustic', 'chicago', 'delta'],
|
|
reggae: ['roots', 'dancehall', 'dub', 'ska'],
|
|
folk: ['indie', 'traditional', 'contemporary', 'acoustic'],
|
|
metal: ['heavy', 'death', 'black', 'progressive', 'thrash'],
|
|
punk: ['hardcore', 'pop-punk', 'post-punk', 'ska-punk'],
|
|
indie: ['indie-rock', 'indie-folk', 'indie-electronic'],
|
|
ambient: ['dark', 'drone', 'new-age', 'cinematic'],
|
|
techno: ['minimal', 'acid', 'detroit', 'hard']
|
|
}
|
|
|
|
const options = subgenres[genre]
|
|
return options ? vezaFaker.helpers.arrayElement(options) : undefined
|
|
}
|
|
|
|
private static generateTrackDescription(title: string, artist: string, genre: AudioGenre): string {
|
|
const templates = [
|
|
`Nouveau single de ${artist} - ${title}. Un morceau ${genre} qui ne vous laissera pas indifférent.`,
|
|
`${title} par ${artist}. Une composition ${genre} originale et captivante.`,
|
|
`Découvrez ${title}, le dernier titre de ${artist} dans un style ${genre} moderne.`,
|
|
`${artist} nous présente ${title}, une pièce ${genre} pleine d'émotion.`,
|
|
`${title} - Une création ${genre} signée ${artist}. À écouter absolument !`
|
|
]
|
|
|
|
return vezaFaker.helpers.arrayElement(templates)
|
|
}
|
|
|
|
private static generateLyrics(): string {
|
|
const verses = [
|
|
"Dans la nuit étoilée, je marche seul\nMes pensées s'envolent vers l'horizon\nLa musique résonne dans mon cœur\nComme un écho de ma passion",
|
|
|
|
"Les mélodies dansent dans l'air\nPortées par le vent du soir\nChaque note raconte une histoire\nD'amour, de joie et d'espoir",
|
|
|
|
"Sur cette scène illuminée\nJe chante ma vérité\nLes mots s'échappent de mon âme\nPour toucher l'humanité"
|
|
]
|
|
|
|
return vezaFaker.helpers.arrayElements(verses, vezaFaker.number.int({ min: 1, max: 3 })).join('\n\n')
|
|
}
|
|
|
|
private static generatePlaylistTitle(theme: string): string {
|
|
const titles: Record<string, string[]> = {
|
|
workout: ['Motivation Max', 'Energie Workout', 'Cardio Beats', 'Pump It Up', 'Training Zone'],
|
|
chill: ['Détente Totale', 'Ambiance Chill', 'Relax Time', 'Zen Vibes', 'Sunday Morning'],
|
|
party: ['Party Mix', 'Dancefloor Hits', 'Soirée Endiablée', 'Club Anthems', 'Weekend Vibes'],
|
|
focus: ['Focus Mode', 'Concentration', 'Study Beats', 'Deep Work', 'Productivité'],
|
|
random: ['Mix du Jour', 'Découvertes', 'Playlist Perso', 'Mes Favoris', 'Random Play']
|
|
}
|
|
|
|
const themeOptions = titles[theme] || titles.random
|
|
return vezaFaker.helpers.arrayElement(themeOptions)
|
|
}
|
|
|
|
private static generatePlaylistDescription(title: string, theme: string): string {
|
|
const descriptions: Record<string, string[]> = {
|
|
workout: [
|
|
'Playlist parfaite pour vos séances de sport et d\'entraînement.',
|
|
'Des beats énergiques pour vous motiver pendant l\'effort.',
|
|
'La bande sonore idéale pour vos sessions fitness.'
|
|
],
|
|
chill: [
|
|
'Musiques relaxantes pour se détendre et décompresser.',
|
|
'Ambiance apaisante pour vos moments de repos.',
|
|
'Sons doux pour accompagner vos instants de tranquillité.'
|
|
],
|
|
party: [
|
|
'Hits incontournables pour faire la fête toute la nuit.',
|
|
'Morceaux dansants pour mettre l\'ambiance.',
|
|
'La playlist ultime pour vos soirées entre amis.'
|
|
],
|
|
focus: [
|
|
'Musiques instrumentales pour améliorer votre concentration.',
|
|
'Sons apaisants pour favoriser la productivité.',
|
|
'Ambiance sonore idéale pour le travail et les études.'
|
|
],
|
|
random: [
|
|
'Sélection éclectique de mes morceaux préférés.',
|
|
'Mix varié pour toutes les humeurs.',
|
|
'Découvertes musicales et coups de cœur.'
|
|
]
|
|
}
|
|
|
|
const themeDescriptions = descriptions[theme] || descriptions.random
|
|
return vezaFaker.helpers.arrayElement(themeDescriptions)
|
|
}
|
|
|
|
private static generatePlaylistTags(theme: string): string[] {
|
|
const tags: Record<string, string[]> = {
|
|
workout: ['sport', 'motivation', 'énergie', 'cardio', 'fitness'],
|
|
chill: ['détente', 'relaxation', 'calme', 'zen', 'ambient'],
|
|
party: ['fête', 'danse', 'club', 'soirée', 'hits'],
|
|
focus: ['concentration', 'étude', 'travail', 'instrumental', 'productivité'],
|
|
random: ['mix', 'varié', 'découverte', 'éclectique', 'personnel']
|
|
}
|
|
|
|
const themeTags = tags[theme] || tags.random
|
|
return vezaFaker.helpers.arrayElements(themeTags, vezaFaker.number.int({ min: 2, max: 4 }))
|
|
}
|
|
|
|
/**
|
|
* Utility methods
|
|
*/
|
|
static getGeneratedTrack(id: string): Audio | undefined {
|
|
return this.generatedTracks.get(id)
|
|
}
|
|
|
|
static getGeneratedAlbum(id: string): Album | undefined {
|
|
return this.generatedAlbums.get(id)
|
|
}
|
|
|
|
static getGeneratedPlaylist(id: string): Playlist | undefined {
|
|
return this.generatedPlaylists.get(id)
|
|
}
|
|
|
|
static getAllGeneratedTracks(): Audio[] {
|
|
return Array.from(this.generatedTracks.values())
|
|
}
|
|
|
|
static getAllGeneratedAlbums(): Album[] {
|
|
return Array.from(this.generatedAlbums.values())
|
|
}
|
|
|
|
static getAllGeneratedPlaylists(): Playlist[] {
|
|
return Array.from(this.generatedPlaylists.values())
|
|
}
|
|
|
|
static clearCache(): void {
|
|
this.generatedTracks.clear()
|
|
this.generatedAlbums.clear()
|
|
this.generatedPlaylists.clear()
|
|
this.usedSlugs.clear()
|
|
}
|
|
|
|
static getStats(): {
|
|
totalTracks: number
|
|
totalAlbums: number
|
|
totalPlaylists: number
|
|
byGenre: Record<AudioGenre, number>
|
|
} {
|
|
const tracks = this.getAllGeneratedTracks()
|
|
const byGenre: Record<AudioGenre, number> = {
|
|
rock: 0, pop: 0, jazz: 0, classical: 0, electronic: 0, 'hip-hop': 0,
|
|
country: 0, blues: 0, reggae: 0, folk: 0, metal: 0, punk: 0,
|
|
indie: 0, ambient: 0, techno: 0
|
|
}
|
|
|
|
tracks.forEach(track => {
|
|
byGenre[track.genre]++
|
|
})
|
|
|
|
return {
|
|
totalTracks: tracks.length,
|
|
totalAlbums: this.getAllGeneratedAlbums().length,
|
|
totalPlaylists: this.getAllGeneratedPlaylists().length,
|
|
byGenre
|
|
}
|
|
}
|
|
} |