refactor(backend): split seed tool into domain-specific modules

Extract monolithic seed main.go into separate files per domain:
users, tracks, playlists, chat, analytics, marketplace, social,
content, live, moderation, notifications, and misc. Add config,
fake data helpers, and utility modules. Update Makefile targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-03-25 23:35:07 +01:00
parent 2efaa1432b
commit 2eff5a9b10
18 changed files with 3662 additions and 662 deletions

View file

@ -274,9 +274,13 @@ migrate: ## Exécute les migrations de base de données
db-migrate: migrate ## Alias pour migrate
db-seed: ## Peuple la base de données avec des données de test
db-seed: ## Peuple la base de données avec des données réalistes (1200 users, 5000 tracks)
@echo "$(GREEN)🌱 Peuplement de la base de données...$(NC)"
@echo "$(YELLOW)⚠️ Seeding non implémenté$(NC)"
@go run ./cmd/tools/seed/
db-seed-minimal: ## Peuple la base avec un jeu réduit (50 users, 200 tracks)
@echo "$(GREEN)🌱 Peuplement réduit...$(NC)"
@go run ./cmd/tools/seed/ --minimal
# Lab Environment
migrate-lab: ## Applique les migrations en environnement Lab

View file

@ -0,0 +1,174 @@
package main
import "flag"
// Config holds all seed volume parameters.
type Config struct {
// Users
TotalUsers int
NormalUsers int
Artists int
Labels int
Moderators int
Admins int
// Content
Tracks int
Albums int
Playlists int
PlaylistMinTracks int
PlaylistMaxTracks int
// Social
Follows int
TrackLikes int
TrackReposts int
Comments int
CommentLikes int
// Chat
Conversations int
Messages int
// Live
PastLiveStreams int
LiveStreams int
// Marketplace
Products int
Orders int
ProductReviews int
// Analytics
PlayEvents int
ProfileViews int
AnalyticsMonths int
// Notifications
Notifications int
// Files
FileEntries int
// Misc
Groups int
SupportTickets int
APIKeys int
Announcements int
Reports int
DataExports int
GearItems int
Courses int
}
// FullConfig returns the full-scale configuration (~1200 users, ~5000 tracks).
func FullConfig() Config {
return Config{
TotalUsers: 1200,
NormalUsers: 1000,
Artists: 150,
Labels: 30,
Moderators: 15,
Admins: 5,
Tracks: 5000,
Albums: 300,
Playlists: 800,
PlaylistMinTracks: 5,
PlaylistMaxTracks: 50,
Follows: 15000,
TrackLikes: 30000,
TrackReposts: 5000,
Comments: 8000,
CommentLikes: 2000,
Conversations: 500,
Messages: 10000,
PastLiveStreams: 200,
LiveStreams: 5,
Products: 400,
Orders: 600,
ProductReviews: 300,
PlayEvents: 100000,
ProfileViews: 20000,
AnalyticsMonths: 6,
Notifications: 15000,
FileEntries: 6000,
Groups: 40,
SupportTickets: 80,
APIKeys: 30,
Announcements: 10,
Reports: 60,
DataExports: 15,
GearItems: 200,
Courses: 25,
}
}
// MinimalConfig returns a reduced configuration for fast dev iteration.
func MinimalConfig() Config {
return Config{
TotalUsers: 50,
NormalUsers: 30,
Artists: 12,
Labels: 3,
Moderators: 2,
Admins: 3,
Tracks: 200,
Albums: 25,
Playlists: 40,
PlaylistMinTracks: 3,
PlaylistMaxTracks: 15,
Follows: 200,
TrackLikes: 500,
TrackReposts: 100,
Comments: 150,
CommentLikes: 50,
Conversations: 20,
Messages: 200,
PastLiveStreams: 10,
LiveStreams: 2,
Products: 30,
Orders: 40,
ProductReviews: 20,
PlayEvents: 2000,
ProfileViews: 400,
AnalyticsMonths: 2,
Notifications: 300,
FileEntries: 250,
Groups: 5,
SupportTickets: 10,
APIKeys: 5,
Announcements: 3,
Reports: 8,
DataExports: 3,
GearItems: 20,
Courses: 5,
}
}
// ParseFlags parses CLI flags and returns the appropriate config.
func ParseFlags() Config {
minimal := flag.Bool("minimal", false, "Use reduced volumes (50 users, 200 tracks) for fast dev")
flag.Parse()
if *minimal {
return MinimalConfig()
}
return FullConfig()
}

View file

@ -0,0 +1,636 @@
package main
import (
"fmt"
"math"
"math/rand"
"strings"
"time"
"github.com/google/uuid"
)
// Deterministic RNG — set once in main.go
var rng *rand.Rand
// InitRNG initializes the deterministic RNG.
func InitRNG(seed int64) {
rng = rand.New(rand.NewSource(seed))
}
// ── UUID helpers ─────────────────────────────────────────────────────────────
func newUUID() string {
// Use rng bytes to make UUIDs deterministic
var b [16]byte
for i := range b {
b[i] = byte(rng.Intn(256))
}
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant 1
u, _ := uuid.FromBytes(b[:])
return u.String()
}
// ── Random primitives ────────────────────────────────────────────────────────
func randInt(min, max int) int {
if min >= max {
return min
}
return min + rng.Intn(max-min+1)
}
func randFloat(min, max float64) float64 {
return min + rng.Float64()*(max-min)
}
func randBool() bool {
return rng.Intn(2) == 0
}
func randChance(pct int) bool {
return rng.Intn(100) < pct
}
func pick[T any](slice []T) T {
return slice[rng.Intn(len(slice))]
}
func pickN[T any](slice []T, n int) []T {
if n >= len(slice) {
cp := make([]T, len(slice))
copy(cp, slice)
return cp
}
perm := rng.Perm(len(slice))
result := make([]T, n)
for i := 0; i < n; i++ {
result[i] = slice[perm[i]]
}
return result
}
func pickWeighted(weights []float64) int {
total := 0.0
for _, w := range weights {
total += w
}
r := rng.Float64() * total
cum := 0.0
for i, w := range weights {
cum += w
if r <= cum {
return i
}
}
return len(weights) - 1
}
// ── Power-law distribution ───────────────────────────────────────────────────
// PowerLaw returns a value following a power-law distribution.
// Used for follower counts, play counts, etc.
func PowerLaw(min, max int, alpha float64) int {
u := rng.Float64()
x := math.Pow(
math.Pow(float64(max), alpha+1)-math.Pow(float64(min), alpha+1)*u+math.Pow(float64(min), alpha+1),
1.0/(alpha+1),
)
v := int(x)
if v < min {
v = min
}
if v > max {
v = max
}
return v
}
// ── Temporal helpers ─────────────────────────────────────────────────────────
var baseTime = time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC)
func seedTimeRange() (time.Time, time.Time) {
return baseTime, time.Now()
}
// RandomTimeBetween returns a random time between start and end.
func RandomTimeBetween(start, end time.Time) time.Time {
delta := end.Sub(start)
if delta <= 0 {
return start
}
offset := time.Duration(rng.Int63n(int64(delta)))
return start.Add(offset)
}
// RandomTimeAfter returns a random time between after and now.
func RandomTimeAfter(after time.Time) time.Time {
return RandomTimeBetween(after, time.Now())
}
// RandomPastTime returns a random time within the seed period.
func RandomPastTime() time.Time {
start, end := seedTimeRange()
return RandomTimeBetween(start, end)
}
// RandomRegistrationTime returns a time spread over 18 months with growth curve.
// Earlier months have fewer registrations, later months have more.
func RandomRegistrationTime(index, total int) time.Time {
start, end := seedTimeRange()
// Use a quadratic growth curve: more users registered recently
t := float64(index) / float64(total)
// Quadratic: t^1.5 gives accelerating growth
adjusted := math.Pow(t, 1.5)
offset := time.Duration(adjusted * float64(end.Sub(start)))
return start.Add(offset)
}
// RealisticHour adjusts a time to a realistic hour (peak 18-23h, low 3-7h).
func RealisticHour(t time.Time) time.Time {
// Distribution weights for each hour (0-23)
hourWeights := []float64{
3, 2, 1.5, 1, 0.5, 0.5, 1, 2, // 00-07
4, 5, 6, 7, 7, 6, 6, 7, // 08-15
8, 10, 12, 14, 15, 13, 10, 6, // 16-23
}
hour := pickWeighted(hourWeights)
minute := rng.Intn(60)
second := rng.Intn(60)
return time.Date(t.Year(), t.Month(), t.Day(), hour, minute, second, 0, t.Location())
}
// DaysAgo returns a time N days ago.
func DaysAgo(d int) time.Time {
return time.Now().Add(-time.Duration(d) * 24 * time.Hour)
}
// ── Artist names ─────────────────────────────────────────────────────────────
var artistPrefixes = []string{
"DJ ", "MC ", "", "", "", "", "", "", "", "", // 80% no prefix
}
var artistFirstNames = []string{
"Luna", "Nova", "Ash", "Kai", "Rio", "Zara", "Milo", "Jade", "Axel", "Nina",
"Felix", "Aria", "Leo", "Maya", "Theo", "Isla", "Remy", "Cleo", "Hugo", "Vera",
"Soren", "Lyra", "Nico", "Freya", "Dante", "Suki", "Orion", "Yuna", "Blaze", "Kira",
"Samir", "Elara", "Raven", "Zion", "Atlas", "Sage", "Phoenix", "Echo", "Jasper", "Aurora",
}
var artistLastParts = []string{
"beats", "sound", "wave", "bass", "pulse", "tone", "flux", "vibe", "sonic", "audio",
"rhythm", "synth", "groove", "noise", "echo", "drift", "loop", "fade", "glitch", "static",
}
var stylizedNames = []string{
"KNTRL", "Nø Signal", "×void×", "fm.static", "lowkey", "BVSS", "h3lix", "wav.form",
"neo.soul", "ctrl+alt", "404.wav", "null.set", "bit.crush", "sub.zero", "hi.pass",
"vrtx", "DRMZ", "pxlgrid", "snthwv", "bassface", "deepstate", "skyline",
"midnight", "goldchain", "velvet", "crimson", "phantom", "spectra", "zenith",
"vertex", "cascade", "prism", "solstice", "tempest", "horizon", "meridian",
}
var frenchNames = []string{
"Amélie", "Baptiste", "Camille", "Dimitri", "Éloïse", "Florian", "Gaëlle", "Hugo",
"Inès", "Jules", "Katia", "Léo", "Manon", "Nathan", "Océane", "Pierre",
"Quentin", "Rose", "Sébastien", "Théo", "Ulysse", "Victoire", "William", "Xavier",
"Yasmine", "Zoé", "Adrien", "Bérénice", "Clément", "Diane", "Émile", "Fantine",
}
var frenchLastNames = []string{
"Dubois", "Martin", "Lefèvre", "Moreau", "Laurent", "Garcia", "Petit", "Roux",
"Bernard", "Robert", "Durand", "Simon", "Michel", "Richard", "Thomas", "Leroux",
"David", "Bertrand", "Fournier", "Girard", "Mercier", "Dupont", "Lambert", "Bonnet",
}
// GenArtistName generates a realistic artist/stage name.
func GenArtistName(index int) string {
switch {
case index%5 == 0:
// Stylized name
return pick(stylizedNames)
case index%3 == 0:
// French full name
return pick(frenchNames) + " " + pick(frenchLastNames)
default:
// Prefix + first name + maybe suffix
prefix := pick(artistPrefixes)
name := pick(artistFirstNames)
if randChance(30) {
return prefix + name + " " + pick(artistLastParts)
}
return prefix + name
}
}
// GenUsername generates a valid username (3-30 chars, [a-zA-Z0-9_]).
func GenUsername(displayName string, index int) string {
// Sanitize display name
clean := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
return r
}
if r == ' ' || r == '.' || r == '-' {
return '_'
}
// Strip accents by dropping non-ASCII
return -1
}, displayName)
clean = strings.ToLower(clean)
// Remove consecutive underscores
for strings.Contains(clean, "__") {
clean = strings.ReplaceAll(clean, "__", "_")
}
clean = strings.Trim(clean, "_")
if len(clean) < 3 {
clean = fmt.Sprintf("user_%d", index)
}
// Ensure uniqueness with index suffix
if len(clean) > 20 {
clean = clean[:20]
}
return fmt.Sprintf("%s_%04d", clean, index)
}
// ── Track titles ─────────────────────────────────────────────────────────────
var trackAdjectives = []string{
"Neon", "Midnight", "Golden", "Velvet", "Crystal", "Electric", "Digital", "Cosmic",
"Lunar", "Solar", "Deep", "Lost", "Broken", "Silent", "Frozen", "Burning",
"Faded", "Hollow", "Phantom", "Sacred", "Vivid", "Crimson", "Amber", "Sapphire",
"Endless", "Haunted", "Infinite", "Liquid", "Mystic", "Urban", "Ethereal", "Raw",
}
var trackNouns = []string{
"Dreams", "Waves", "Lights", "Rain", "Echo", "Pulse", "Horizon", "Shadow",
"Storm", "Flame", "Drift", "Spiral", "Signal", "Memory", "Voyage", "Whisper",
"Thunder", "Mirage", "Paradise", "Frequency", "Cascade", "Odyssey", "Nebula", "Prism",
"Skyline", "Orbit", "Labyrinth", "Phoenix", "Tempest", "Solitude", "Reverie", "Zenith",
}
var trackTemplates = []string{
"%s %s", // "Neon Dreams"
"%s %s", // "Midnight Waves"
"The %s", // "The Storm"
"%s", // "Pulse"
"%s & %s", // "Lights & Shadows"
"After %s", // "After Rain"
"Last %s", // "Last Signal"
"%s at Dawn", // "Waves at Dawn"
"%s Protocol", // "Midnight Protocol"
"%s Sessions", // "Deep Sessions"
"Into the %s", // "Into the Storm"
"%s Mode", // "Neon Mode"
}
// GenTrackTitle generates a realistic track title.
func GenTrackTitle() string {
tmpl := pick(trackTemplates)
count := strings.Count(tmpl, "%s")
switch count {
case 0:
return tmpl
case 1:
if randBool() {
return fmt.Sprintf(tmpl, pick(trackAdjectives))
}
return fmt.Sprintf(tmpl, pick(trackNouns))
case 2:
return fmt.Sprintf(tmpl, pick(trackAdjectives), pick(trackNouns))
}
return pick(trackAdjectives) + " " + pick(trackNouns)
}
// ── Albums ───────────────────────────────────────────────────────────────────
var albumTemplates = []string{
"%s EP", "%s Vol. %d", "%s Sessions", "The %s Collection",
"%s", "%s Tapes", "%s Archives", "Side %s",
}
func GenAlbumTitle() string {
tmpl := pick(albumTemplates)
if strings.Contains(tmpl, "%d") {
return fmt.Sprintf(tmpl, pick(trackNouns), randInt(1, 3))
}
if strings.Contains(tmpl, "%s") {
return fmt.Sprintf(tmpl, pick(trackAdjectives))
}
return tmpl
}
// ── Playlist names ───────────────────────────────────────────────────────────
var playlistNames = []string{
"Chill Vibes 2025", "Workout Mix", "Late Night Sessions", "Morning Coffee",
"Deep Focus", "Road Trip Playlist", "Party Starters", "Rainy Day Beats",
"Sunset Grooves", "Study Sessions", "Pre-Game Hype", "Acoustic Chill",
"Underground Gems", "Throwback Jams", "Midnight Drive", "Sunday Morning",
"Bass Heavy", "Melodic Journey", "Lo-Fi & Chill", "Festival Favorites",
"New Discoveries", "Indie Gems", "Electronic Essentials", "Hip-Hop Rotation",
"Jazz Lounge", "Ambient Textures", "Funk & Soul", "Classical Focus",
"Drum & Bass Energy", "Techno Temple", "House Classics", "Reggae Vibes",
"Synthwave Dreams", "Post-Rock Journey", "Metal Mondays", "Blues Collection",
"Downtempo Drift", "Peak Time Bangers", "Warm Up Set", "After Hours",
"Bedroom Sessions", "Creative Flow", "Gym Power", "Cooking Tunes",
"Cleaning Motivation", "Meditation Sounds", "Nature Ambiance", "City Sounds",
"Vinyl Selections", "Hidden Treasures", "Fresh Finds Weekly", "Nostalgia Trip",
}
func GenPlaylistName(index int) string {
if index < len(playlistNames) {
return playlistNames[index]
}
adj := pick(trackAdjectives)
noun := pick([]string{"Mix", "Playlist", "Collection", "Selection", "Vibes", "Beats", "Tunes", "Session"})
return fmt.Sprintf("%s %s #%d", adj, noun, index)
}
// ── Genres ────────────────────────────────────────────────────────────────────
type GenreInfo struct {
Name string
Slug string
BPMRange [2]int
Keys []string
}
var Genres = []GenreInfo{
{"Electronic", "electronic", [2]int{120, 140}, []string{"Am", "Cm", "Dm", "Em", "Fm"}},
{"House", "house", [2]int{118, 130}, []string{"Am", "Cm", "Dm", "G", "F"}},
{"Techno", "techno", [2]int{125, 150}, []string{"Am", "Dm", "Em", "Bm", "Cm"}},
{"Ambient", "ambient", [2]int{60, 100}, []string{"C", "D", "F", "G", "Am"}},
{"Drum & Bass", "drum-and-bass", [2]int{160, 180}, []string{"Am", "Dm", "Em", "Fm", "Gm"}},
{"Dubstep", "dubstep", [2]int{135, 145}, []string{"Dm", "Em", "Fm", "Gm", "Am"}},
{"Trance", "trance", [2]int{130, 150}, []string{"Am", "Dm", "Em", "Cm", "Fm"}},
{"Jazz", "jazz", [2]int{80, 160}, []string{"Dm", "Gm", "Cm", "Fm", "Bb"}},
{"Rock", "rock", [2]int{100, 160}, []string{"E", "A", "D", "G", "Em"}},
{"Pop", "pop", [2]int{90, 130}, []string{"C", "G", "Am", "F", "D"}},
{"Hip-Hop", "hip-hop", [2]int{70, 100}, []string{"Cm", "Fm", "Gm", "Dm", "Am"}},
{"Classical", "classical", [2]int{40, 180}, []string{"C", "D", "G", "Am", "Em"}},
{"Folk", "folk", [2]int{80, 130}, []string{"G", "C", "D", "Am", "Em"}},
{"Reggae", "reggae", [2]int{60, 90}, []string{"Am", "Dm", "G", "C", "Em"}},
{"Soul", "soul", [2]int{70, 110}, []string{"Dm", "Am", "Gm", "Cm", "Fm"}},
{"Funk", "funk", [2]int{90, 120}, []string{"Em", "Am", "Dm", "G", "A"}},
{"Blues", "blues", [2]int{60, 120}, []string{"E", "A", "B", "Am", "Em"}},
{"Metal", "metal", [2]int{100, 200}, []string{"Em", "Am", "Dm", "E", "D"}},
{"Indie", "indie", [2]int{90, 140}, []string{"C", "G", "Am", "F", "Em"}},
{"Experimental", "experimental", [2]int{40, 200}, []string{"", "Am", "C", "Dm", ""}},
{"World", "world", [2]int{70, 140}, []string{"Dm", "Am", "Em", "G", "C"}},
{"Latin", "latin", [2]int{80, 140}, []string{"Am", "Dm", "G", "C", "Em"}},
{"Lo-Fi", "lo-fi", [2]int{60, 90}, []string{"Am", "Dm", "Em", "C", "Fm"}},
}
// GenreForArtist returns 1-3 consistent genres for an artist.
func GenreForArtist(artistIndex int) []GenreInfo {
// Use artist index as seed for consistency
primary := artistIndex % len(Genres)
count := 1 + rng.Intn(3)
result := []GenreInfo{Genres[primary]}
// Add nearby genres
for i := 1; i < count && i < len(Genres); i++ {
next := (primary + i) % len(Genres)
result = append(result, Genres[next])
}
return result
}
// ── Bio generator ────────────────────────────────────────────────────────────
var bioTemplates = []string{
"Producteur %s basé à %s. %s",
"%s artist exploring the boundaries of %s and %s.",
"Making %s since %d. %s",
"Born in %s, raised on %s. Currently working on new material.",
"%s producer & DJ. Resident at %s. %s",
"Independent %s artist. Available for collaborations.",
"Passionate about %s and sound design. %s",
"Multi-instrumentalist blending %s with %s. Based in %s.",
"Creating sonic landscapes between %s and %s.",
"🎵 %s | %s | Based in %s",
}
var cities = []string{
"Paris", "Lyon", "Marseille", "Toulouse", "Bordeaux", "Nantes", "Lille", "Strasbourg",
"Berlin", "London", "Amsterdam", "Barcelona", "Brussels", "Montreal", "New York",
"Los Angeles", "Tokyo", "Lagos", "São Paulo", "Melbourne", "Stockholm", "Vienna",
}
var bioSuffixes = []string{
"Open for collabs.", "New EP coming soon.", "Bookings: see website.",
"Label founder.", "Self-taught producer.", "Vinyl collector.",
"Always looking for new sounds.", "Music is a journey, not a destination.",
"", "", "", // Sometimes no suffix
}
func GenBio(genre string) string {
tmpl := pick(bioTemplates)
city := pick(cities)
suffix := pick(bioSuffixes)
year := randInt(2008, 2023)
genre2 := pick(Genres).Name
s := tmpl
s = strings.Replace(s, "%s", genre, 1)
s = strings.Replace(s, "%s", city, 1)
s = strings.Replace(s, "%s", suffix, 1)
s = strings.Replace(s, "%s", genre2, 1)
s = strings.Replace(s, "%s", city, 1)
s = strings.Replace(s, "%d", fmt.Sprintf("%d", year), 1)
// Clean up leftover %s
for strings.Contains(s, "%s") {
s = strings.Replace(s, "%s", pick([]string{genre, city, suffix}), 1)
}
return s
}
// GenShortBio generates a 20-80 char bio for regular users.
func GenShortBio() string {
bios := []string{
"Music lover.", "Just here for the vibes.", "Discovering new sounds daily.",
"Vinyl junkie.", "Festival goer. Playlist maker.", "Underground music enthusiast.",
"Bass head.", "Chillwave addict.", "Lo-fi and coffee.", "Night owl.",
"Always on repeat.", "Headphones on, world off.", "Eclectic listener.",
"Beatmaker wannabe.", "Melody seeker.", "Groove collector.",
"Sound explorer.", "Audio nerd.", "Rhythm is life.",
"Music is the answer.", "Curator of good vibes.",
}
return pick(bios)
}
// ── Comment generator ────────────────────────────────────────────────────────
var commentTemplates = []string{
"This track is incredible! 🔥", "Love the vibe on this one.",
"The production quality is amazing.", "Can't stop listening to this.",
"This hits different at night.", "Perfect track for my playlist.",
"The bass on this is insane.", "Beautiful melody, well done!",
"Discovered this today, instant favorite.", "The mix is so clean.",
"This deserves way more plays.", "Reminds me of early %s.",
"Your best work yet!", "How did you get that synth sound?",
"The drop at 1:30 is everything.", "Smooth production, respect.",
"This needs to be in a movie soundtrack.", "Played this 10 times today already.",
"The atmosphere on this track is unreal.", "Fire 🔥🔥🔥",
"Adding this to every playlist.", "The outro is hauntingly beautiful.",
"Chef's kiss on the mastering.", "This is what I needed today.",
"Absolute banger.", "So underrated.", "The groove never stops.",
"Your sound design skills are next level.", "Pure vibes.",
"This brought me back to my first rave.", "Incredible arrangement.",
"The percussion work is stellar.", "Love the vocal chops.",
"This gives me goosebumps every time.", "What DAW did you use?",
"Certified classic.", "Sharing this with everyone I know.",
"The transition at 2:45 is genius.", "Perfect driving music.",
"I could listen to this forever.", "The ambiance is just right.",
}
func GenComment() string {
tmpl := pick(commentTemplates)
if strings.Contains(tmpl, "%s") {
return fmt.Sprintf(tmpl, pick(Genres).Name)
}
return tmpl
}
// ── Message generator ────────────────────────────────────────────────────────
var chatMessages = []string{
"Hey, what's up?", "Anyone heard the new release?", "Great set last night!",
"Looking for collab partners.", "What plugins do you recommend?",
"Just finished a new track, feedback welcome!", "Thanks for the follow!",
"Love your latest upload.", "When's your next live stream?",
"Check out my new playlist.", "The mix sounds great.",
"What DAW are you using?", "Ableton or FL Studio?",
"Anyone going to the festival this summer?", "Great community here.",
"Need help with mixing, any tips?", "The platform is awesome.",
"Just uploaded my first track!", "Who wants to do a remix swap?",
"Your sound design is incredible.", "How do you get that bass sound?",
"Thanks for sharing!", "Welcome to the group!",
"Let's set up a listening session.", "Perfect track for the weekend.",
"Anyone know good sample packs?", "Mastering tips?",
"Studio session later?", "New beat ready for vocals.",
"This community is fire.", "Appreciate the support everyone!",
}
func GenMessage() string {
return pick(chatMessages)
}
// ── Product names ────────────────────────────────────────────────────────────
var productTypes = []string{"beat", "sample-pack", "beat", "sample-pack", "beat"}
var productCategories = []string{
"beats", "samples", "electronic", "house", "acoustic", "stems",
"hip-hop", "trap", "lo-fi", "ambient", "dj-tools", "sfx", "sync",
}
func GenProductTitle(genre string) string {
templates := []string{
"%s Beat Pack Vol. %d", "%s Essentials", "%s Sample Collection",
"%s Stems Bundle", "%s Loop Kit", "Premium %s Beats",
"%s Sound Design Pack", "%s Producer Kit", "%s One-Shots",
"%s Drum Kit", "%s Texture Pack", "%s Presets Collection",
}
tmpl := pick(templates)
if strings.Contains(tmpl, "%d") {
return fmt.Sprintf(tmpl, genre, randInt(1, 5))
}
return fmt.Sprintf(tmpl, genre)
}
// ── IP and User-Agent generators ─────────────────────────────────────────────
func GenIP() string {
return fmt.Sprintf("%d.%d.%d.%d", randInt(1, 223), randInt(0, 255), randInt(0, 255), randInt(1, 254))
}
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 Chrome/119.0.0.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15",
"Mozilla/5.0 (Android 14; Mobile) AppleWebKit/537.36 Chrome/120.0.0.0",
"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15",
}
func GenUserAgent() string {
return pick(userAgents)
}
var countryCodes = []string{
"FR", "FR", "FR", "FR", "FR", // 50% French
"US", "US", "DE", "GB", "BE", "CA", "CH", "ES", "IT", "NL",
"JP", "BR", "AU", "SE", "PT", "MA", "SN", "CI", "TN", "DZ",
}
func GenCountry() string {
return pick(countryCodes)
}
var sources = []string{"web", "web", "web", "mobile", "mobile", "api"}
func GenSource() string {
return pick(sources)
}
// ── Room names ───────────────────────────────────────────────────────────────
var roomNames = []string{
"General", "Production Tips", "Beat Marketplace", "Feedback Corner",
"Mixing & Mastering", "Vinyl Talk", "Festival Chat", "Sample Swap",
"Sound Design Lab", "Artist Lounge", "New Releases", "Collab Hub",
"Genre Discussion", "Gear Talk", "Music Theory", "Business & Promo",
"Open Mic", "Challenge of the Week", "Studio Setup", "Intro Thread",
}
// ── Gear ─────────────────────────────────────────────────────────────────────
type GearTemplate struct {
Name, Category, Brand, Model string
Price float64
}
var gearTemplates = []GearTemplate{
{"Ableton Push 3", "controller", "Ableton", "Push 3", 999},
{"Focal Shape 65", "monitors", "Focal", "Shape 65", 599},
{"RME Babyface Pro FS", "audio-interface", "RME", "Babyface Pro FS", 849},
{"Akai MPC One+", "sampler", "Akai", "MPC One+", 699},
{"Audio-Technica AT2020", "microphone", "Audio-Technica", "AT2020", 99},
{"Beyerdynamic DT 770 Pro", "headphones", "Beyerdynamic", "DT 770 Pro", 159},
{"Zoom H6", "recorder", "Zoom", "H6", 349},
{"Sennheiser MKH 416", "microphone", "Sennheiser", "MKH 416", 999},
{"Pioneer DDJ-1000", "dj-controller", "Pioneer", "DDJ-1000", 1199},
{"Allen & Heath Xone:96", "mixer", "Allen & Heath", "Xone:96", 1899},
{"Martin D-28", "guitar", "Martin", "D-28", 2999},
{"Neumann U87", "microphone", "Neumann", "U87", 3199},
{"Korg Minilogue XD", "synthesizer", "Korg", "Minilogue XD", 649},
{"Arturia KeyLab 61 MkII", "controller", "Arturia", "KeyLab 61 MkII", 449},
{"Universal Audio Apollo Twin", "audio-interface", "Universal Audio", "Apollo Twin", 899},
{"Yamaha HS8", "monitors", "Yamaha", "HS8", 349},
{"Shure SM7B", "microphone", "Shure", "SM7B", 399},
{"Roland TR-8S", "drum-machine", "Roland", "TR-8S", 599},
{"Moog Subsequent 37", "synthesizer", "Moog", "Subsequent 37", 1499},
{"Native Instruments Maschine+", "sampler", "Native Instruments", "Maschine+", 1399},
{"Audient iD14 MkII", "audio-interface", "Audient", "iD14 MkII", 249},
{"KRK Rokit 5 G4", "monitors", "KRK", "Rokit 5 G4", 179},
{"Rode NT1-A", "microphone", "Rode", "NT1-A", 229},
{"Elektron Digitakt II", "sampler", "Elektron", "Digitakt II", 899},
{"Teenage Engineering OP-1 Field", "synthesizer", "Teenage Engineering", "OP-1 Field", 1999},
}
// ── Live stream titles ───────────────────────────────────────────────────────
var liveStreamTitles = []string{
"Friday Night Set", "Production Session — New EP Preview", "Beat Making LIVE",
"Acoustic Session — Unplugged", "Late Night Vinyl Mix", "Sunday Jazz Session",
"Studio Tour & Q&A", "Making a Track From Scratch", "Mixing Masterclass",
"Sample Flipping Challenge", "Ambient Soundscape Session", "Open Decks Night",
"Synth Jam Session", "Drum Programming Workshop", "Vocal Recording Session",
"Lo-Fi Beats to Study To — LIVE", "Deep House Sunday", "Techno Warehouse Set",
"Indie Acoustic Session", "Sound Design Exploration",
}

View file

@ -4,688 +4,211 @@ import (
"database/sql"
"fmt"
"log"
"math/rand"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt"
)
// ─── helpers ────────────────────────────────────────────────────────────────
func must(err error, msg string) {
if err != nil {
log.Fatalf("%s: %v", msg, err)
}
}
func tryExec(db *sql.DB, query string, args ...interface{}) {
_, _ = db.Exec(query, args...)
}
func execOrWarn(db *sql.DB, label string, query string, args ...interface{}) {
if _, err := db.Exec(query, args...); err != nil {
log.Printf(" ⚠ %s: %v", label, err)
}
}
func countRows(db *sql.DB, table string) int {
var n int
_ = db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&n)
return n
}
func randBetween(min, max int) int { return min + rand.Intn(max-min+1) }
func daysAgo(d int) time.Time { return time.Now().Add(-time.Duration(d) * 24 * time.Hour) }
func hoursAgo(h int) time.Time { return time.Now().Add(-time.Duration(h) * time.Hour) }
// ─── main ───────────────────────────────────────────────────────────────────
func main() {
_ = godotenv.Load()
cfg := ParseFlags()
// Deterministic seed for reproducibility
InitRNG(42)
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
log.Fatal("DATABASE_URL not set")
}
db, err := sql.Open("postgres", dbURL)
must(err, "DB connect")
if err != nil {
log.Fatalf("DB connect: %v", err)
}
defer db.Close()
must(db.Ping(), "DB ping")
if err := db.Ping(); err != nil {
log.Fatalf("DB ping: %v", err)
}
fmt.Println("╔═══════════════════════════════════════════════╗")
fmt.Println("║ VEZA — Database Seed Script ║")
fmt.Println("╚═══════════════════════════════════════════════╝")
// Increase connection pool for bulk operations
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
startTime := time.Now()
fmt.Println("╔═══════════════════════════════════════════════════════════╗")
fmt.Println("║ VEZA — Realistic Database Seeder v2 ║")
fmt.Println("╚═══════════════════════════════════════════════════════════╝")
fmt.Printf("\n Mode: %s\n", modeName(cfg))
fmt.Printf(" Users: %d | Tracks: %d | Plays: %d\n\n", cfg.TotalUsers, cfg.Tracks, cfg.PlayEvents)
// ── TRUNCATE all tables ──────────────────────────────────────────────────
fmt.Print("Truncating all tables... ")
if err := TruncateAll(db); err != nil {
log.Fatalf("truncate: %v", err)
}
fmt.Println("done")
// ── SEED in dependency order ─────────────────────────────────────────────
// Level 0: Users (no FK dependencies)
users, err := SeedUsers(db, cfg)
if err != nil {
log.Fatalf("seed users: %v", err)
}
// Level 1: Tracks (depends on users)
tracks, err := SeedTracks(db, cfg, users)
if err != nil {
log.Fatalf("seed tracks: %v", err)
}
// Level 2: Playlists (depends on users, tracks)
_, err = SeedPlaylists(db, cfg, users, tracks)
if err != nil {
log.Fatalf("seed playlists: %v", err)
}
// Level 2: Social (depends on users, tracks)
if err := SeedSocial(db, cfg, users, tracks); err != nil {
log.Fatalf("seed social: %v", err)
}
// Level 2: Chat (depends on users)
_, err = SeedChat(db, cfg, users)
if err != nil {
log.Fatalf("seed chat: %v", err)
}
// Level 2: Live streams (depends on users)
if err := SeedLive(db, cfg, users, tracks); err != nil {
log.Fatalf("seed live: %v", err)
}
// Level 2: Marketplace (depends on users, tracks)
_, err = SeedMarketplace(db, cfg, users, tracks)
if err != nil {
log.Fatalf("seed marketplace: %v", err)
}
// Level 3: Analytics (depends on users, tracks — heaviest step)
if err := SeedAnalytics(db, cfg, users, tracks); err != nil {
log.Fatalf("seed analytics: %v", err)
}
// Level 2: Content (depends on users, tracks)
if err := SeedContent(db, cfg, users, tracks); err != nil {
log.Fatalf("seed content: %v", err)
}
// Level 2: Moderation (depends on users, tracks)
if err := SeedModeration(db, cfg, users, tracks); err != nil {
log.Fatalf("seed moderation: %v", err)
}
// Level 2: Notifications (depends on users)
if err := SeedNotifications(db, cfg, users); err != nil {
log.Fatalf("seed notifications: %v", err)
}
// Level 2: Misc (depends on users)
if err := SeedMisc(db, cfg, users); err != nil {
log.Fatalf("seed misc: %v", err)
}
// ── VALIDATION ───────────────────────────────────────────────────────────
fmt.Println("\n═══ VALIDATION ═══")
validate(db)
// ── SUMMARY ──────────────────────────────────────────────────────────────
elapsed := time.Since(startTime)
fmt.Println()
// Hash a shared password (bcrypt cost 12)
hash, err := bcrypt.GenerateFromPassword([]byte("Password123!"), 12)
must(err, "bcrypt")
pw := string(hash)
// ═════════════════════════════════════════════════════════════════════════
// USERS (10)
// ═════════════════════════════════════════════════════════════════════════
type user struct {
id, email, username, display, role, bio string
isAdmin bool
}
var users []user
if countRows(db, "users") == 0 {
fmt.Print("Creating users... ")
users = []user{
{uuid.NewString(), "admin@veza.fr", "admin_veza", "Admin Veza", "admin", "Platform administrator", true},
{uuid.NewString(), "amelie@veza.fr", "amelie_dubois", "Amelie Dubois", "creator", "Productrice electro basee a Paris. Melodic techno & ambient.", false},
{uuid.NewString(), "marcus@veza.fr", "marcus_beats", "Marcus Beats", "creator", "Beatmaker from Lyon. Hip-hop, trap, lo-fi.", false},
{uuid.NewString(), "sakura@veza.fr", "sakura_sound", "Sakura Sound", "creator", "Sound designer & foley artist. Cinematic textures.", false},
{uuid.NewString(), "djrenzo@veza.fr", "dj_renzo", "DJ Renzo", "creator", "House & disco edits. Paris nightlife.", false},
{uuid.NewString(), "clara@veza.fr", "clara_voice", "Clara Voix", "creator", "Singer-songwriter. Indie folk & acoustic.", false},
{uuid.NewString(), "listener1@veza.fr", "music_lover", "Music Lover", "user", "Just here for the vibes.", false},
{uuid.NewString(), "listener2@veza.fr", "groove_hunter", "Groove Hunter", "user", "Always looking for fresh beats.", false},
{uuid.NewString(), "listener3@veza.fr", "night_owl", "Night Owl", "premium", "Late night music sessions.", false},
{uuid.NewString(), "mod@veza.fr", "moderator_veza", "Moderator", "moderator", "Community moderator.", false},
}
for _, u := range users {
_, err := db.Exec(`INSERT INTO users (id, email, email_verified_at, password_hash, username, slug, display_name,
role, is_active, is_verified, is_admin, bio, created_at, updated_at)
VALUES ($1,$2,NOW(),$3,$4,$5,$6,$7,true,true,$8,$9,NOW()-interval '1 day'*$10,NOW())`,
u.id, u.email, pw, u.username, u.username, u.display, u.role, u.isAdmin, u.bio, randBetween(1, 60))
must(err, "user "+u.username)
}
fmt.Printf("%d created\n", len(users))
// Profiles & settings
for _, u := range users {
tryExec(db, `INSERT INTO user_profiles (user_id,bio,tagline,language,theme,profile_visibility) VALUES ($1,$2,$3,'fr','auto','public')`,
u.id, u.bio, strings.Split(u.bio, ".")[0])
tryExec(db, `INSERT INTO user_settings (user_id) VALUES ($1) ON CONFLICT DO NOTHING`, u.id)
}
// Roles
tryExec(db, `INSERT INTO user_roles (user_id,role_id) SELECT $1,id FROM roles WHERE name='admin' ON CONFLICT DO NOTHING`, users[0].id)
tryExec(db, `INSERT INTO user_roles (user_id,role_id) SELECT $1,id FROM roles WHERE name='moderator' ON CONFLICT DO NOTHING`, users[9].id)
} else {
fmt.Println("Users already exist — loading IDs...")
rows, _ := db.Query(`SELECT id,email,username,display_name,role,COALESCE(bio,''),is_admin FROM users ORDER BY created_at LIMIT 10`)
for rows != nil && rows.Next() {
var u user
_ = rows.Scan(&u.id, &u.email, &u.username, &u.display, &u.role, &u.bio, &u.isAdmin)
users = append(users, u)
}
if rows != nil {
rows.Close()
}
}
if len(users) < 10 {
fmt.Println("⚠ Need at least 10 users for full seed. Exiting.")
os.Exit(0)
}
amelieID := users[1].id
marcusID := users[2].id
sakuraID := users[3].id
renzoID := users[4].id
claraID := users[5].id
// ═════════════════════════════════════════════════════════════════════════
// TRACKS (22)
// ═════════════════════════════════════════════════════════════════════════
type track struct {
id, creator, title, artist, album, genre, key, tags string
duration, bpm int
}
var tracks []track
if countRows(db, "tracks") == 0 {
fmt.Print("Creating tracks... ")
tracks = []track{
{uuid.NewString(), amelieID, "Neon Dreams", "Amelie Dubois", "Neon EP", "electronic", "Am", "{electronic,ambient,melodic}", 342, 128},
{uuid.NewString(), amelieID, "Midnight Protocol", "Amelie Dubois", "Neon EP", "techno", "Dm", "{techno,dark,melodic}", 410, 132},
{uuid.NewString(), amelieID, "Aurora Borealis", "Amelie Dubois", "Neon EP", "ambient", "C", "{ambient,atmospheric,chill}", 520, 90},
{uuid.NewString(), amelieID, "Digital Rain", "Amelie Dubois", "Singles", "electronic", "Em", "{electronic,synth,progressive}", 285, 126},
{uuid.NewString(), amelieID, "Pulse", "Amelie Dubois", "Singles", "techno", "Bm", "{techno,driving,peak}", 378, 134},
{uuid.NewString(), marcusID, "Late Night Loops", "Marcus Beats", "Bedroom Sessions", "hip-hop", "Cm", "{lofi,chill,beats}", 198, 85},
{uuid.NewString(), marcusID, "Concrete Jungle", "Marcus Beats", "Bedroom Sessions", "hip-hop", "Fm", "{hiphop,boom-bap,gritty}", 225, 90},
{uuid.NewString(), marcusID, "Velvet Touch", "Marcus Beats", "Bedroom Sessions", "r&b", "Ab", "{rnb,smooth,lofi}", 240, 78},
{uuid.NewString(), marcusID, "City Lights", "Marcus Beats", "Singles", "trap", "Gm", "{trap,melodic,urban}", 210, 140},
{uuid.NewString(), marcusID, "Rainy Days", "Marcus Beats", "Singles", "lo-fi", "D", "{lofi,rain,relax}", 180, 72},
{uuid.NewString(), sakuraID, "Forest Whispers", "Sakura Sound", "Nature Vol.1", "ambient", "F", "{nature,foley,cinematic}", 480, 60},
{uuid.NewString(), sakuraID, "Ocean Depths", "Sakura Sound", "Nature Vol.1", "ambient", "Eb", "{water,deep,ambient}", 540, 55},
{uuid.NewString(), sakuraID, "Thunder Plains", "Sakura Sound", "Nature Vol.1", "cinematic", "Bb", "{storm,epic,cinematic}", 360, 80},
{uuid.NewString(), sakuraID, "Urban Field Recording", "Sakura Sound", "Singles", "experimental", "", "{field-recording,urban,experimental}", 300, 0},
{uuid.NewString(), renzoID, "Saturday Night Edit", "DJ Renzo", "Club Cuts", "house", "G", "{house,disco,funky}", 420, 122},
{uuid.NewString(), renzoID, "Funky Elevator", "DJ Renzo", "Club Cuts", "disco", "A", "{disco,funk,groovy}", 355, 118},
{uuid.NewString(), renzoID, "Deep in the Club", "DJ Renzo", "Club Cuts", "deep house", "Dm", "{deephouse,minimal,late-night}", 480, 124},
{uuid.NewString(), renzoID, "Sunrise Set", "DJ Renzo", "Singles", "house", "C", "{house,progressive,sunrise}", 600, 120},
{uuid.NewString(), claraID, "Paper Boats", "Clara Voix", "Whisper", "folk", "G", "{folk,acoustic,indie}", 220, 95},
{uuid.NewString(), claraID, "Morning Light", "Clara Voix", "Whisper", "indie", "D", "{indie,dreamy,morning}", 198, 100},
{uuid.NewString(), claraID, "Letters Never Sent", "Clara Voix", "Whisper", "folk", "Em", "{folk,emotional,singer-songwriter}", 265, 88},
{uuid.NewString(), claraID, "Wildflowers", "Clara Voix", "Singles", "acoustic", "C", "{acoustic,nature,gentle}", 185, 92},
}
for i, t := range tracks {
createdAt := daysAgo(60 - i*2)
// file_path uses "audio/{Title_With_Underscores}.mp3" matching generated audio files
filePath := "audio/" + strings.ReplaceAll(t.title, " ", "_") + ".mp3"
_, err := db.Exec(`INSERT INTO tracks (id,creator_id,user_id,title,artist,album,genre,duration,bpm,musical_key,
visibility,is_public,is_downloadable,status,stream_status,play_count,like_count,tags,file_path,format,published_at,created_at,updated_at)
VALUES ($1,$2,$2,$3,$4,$5,$6,$7,$8,$9,'public',true,false,'completed','ready',$10,$11,$12::text[],$14,'mp3',$13,$13,$13)`,
t.id, t.creator, t.title, t.artist, t.album, t.genre, t.duration, t.bpm, t.key,
randBetween(10, 500), randBetween(2, 50), t.tags, createdAt, filePath)
must(err, "track "+t.title)
}
fmt.Printf("%d created\n", len(tracks))
} else {
fmt.Println("Tracks already exist — loading IDs...")
rows, _ := db.Query(`SELECT id,creator_id,title,artist,COALESCE(album,''),COALESCE(genre,''),COALESCE(musical_key,''),'{}',duration,COALESCE(bpm,0) FROM tracks ORDER BY created_at LIMIT 22`)
for rows != nil && rows.Next() {
var t track
_ = rows.Scan(&t.id, &t.creator, &t.title, &t.artist, &t.album, &t.genre, &t.key, &t.tags, &t.duration, &t.bpm)
tracks = append(tracks, t)
}
if rows != nil {
rows.Close()
}
}
// ═════════════════════════════════════════════════════════════════════════
// PLAYLISTS (6)
// ═════════════════════════════════════════════════════════════════════════
type playlist struct{ id, user, name, desc string }
var playlists []playlist
if countRows(db, "playlists") < 6 {
fmt.Print("Creating playlists... ")
playlists = []playlist{
{uuid.NewString(), amelieID, "Late Night Techno", "My favorite tracks for late sessions"},
{uuid.NewString(), marcusID, "Chill Beats Study", "Perfect background music for focus"},
{uuid.NewString(), renzoID, "Weekend Warm-Up", "Pre-party essentials"},
{uuid.NewString(), claraID, "Acoustic Mornings", "Gentle wake-up tracks"},
{uuid.NewString(), users[6].id, "Discovery Mix", "New finds from this month"},
{uuid.NewString(), users[7].id, "Workout Energy", "High-BPM motivation"},
}
for _, p := range playlists {
tryExec(db, `INSERT INTO playlists (id,user_id,name,title,description,visibility,is_public,is_collaborative) VALUES ($1,$2,$3,$3,$4,'public',true,false)`,
p.id, p.user, p.name, p.desc)
}
fmt.Printf("%d created\n", len(playlists))
// Playlist tracks
ptMap := []struct{ pi int; ti []int }{
{0, []int{0, 1, 4, 14, 16, 17}}, {1, []int{5, 7, 9, 18, 19}},
{2, []int{14, 15, 16, 3, 4}}, {3, []int{18, 19, 20, 21, 10}},
{4, []int{0, 5, 10, 14, 18, 8}}, {5, []int{1, 4, 8, 14, 15, 16}},
}
for _, pt := range ptMap {
for pos, ti := range pt.ti {
if ti < len(tracks) {
tryExec(db, `INSERT INTO playlist_tracks (playlist_id,track_id,position,added_by) VALUES ($1,$2,$3,$4)`,
playlists[pt.pi].id, tracks[ti].id, pos, playlists[pt.pi].user)
}
}
tryExec(db, `UPDATE playlists SET track_count=(SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id=$1) WHERE id=$1`, playlists[pt.pi].id)
}
} else {
fmt.Println("Playlists already exist — skipping")
}
// ═════════════════════════════════════════════════════════════════════════
// FOLLOWS (18)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "follows") < 10 {
fmt.Print("Creating follows... ")
follows := [][2]int{
// Listeners follow creators
{6, 1}, {6, 2}, {6, 4}, {7, 1}, {7, 3}, {7, 4}, {7, 2},
{8, 1}, {8, 2}, {8, 3}, {8, 4}, {8, 5},
// Creators follow each other
{1, 2}, {2, 1}, {1, 4}, {4, 1}, {3, 1}, {5, 2}, {5, 3},
// Admin follows all creators (so feed has content)
{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5},
}
c := 0
for _, f := range follows {
if _, err := db.Exec(`INSERT INTO follows (follower_id,followed_id) VALUES ($1,$2) ON CONFLICT DO NOTHING`, users[f[0]].id, users[f[1]].id); err == nil {
c++
}
}
fmt.Printf("%d created\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// CHAT ROOMS & MESSAGES
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "rooms") == 0 {
fmt.Print("Creating chat rooms & messages... ")
roomIDs := [3]string{uuid.NewString(), uuid.NewString(), uuid.NewString()}
roomData := []struct{ name, owner string }{
{"General", users[0].id}, {"Production Tips", amelieID}, {"Beat Marketplace", marcusID},
}
for i, r := range roomData {
tryExec(db, `INSERT INTO rooms (id,name,owner_id,creator_id,room_type,is_private,created_at,updated_at) VALUES ($1,$2,$3,$3,'group',false,NOW(),NOW())`, roomIDs[i], r.name, r.owner)
}
for _, rid := range roomIDs {
for _, u := range users[:8] {
tryExec(db, `INSERT INTO room_members (room_id,user_id,role) VALUES ($1,$2,'member') ON CONFLICT DO NOTHING`, rid, u.id)
}
}
msgs := []struct{ r, s int; c string }{
{0, 1, "Hey everyone! Welcome to Veza."}, {0, 2, "Glad to be here. Just uploaded some new beats!"},
{0, 6, "Love the vibes on this platform."}, {0, 3, "Anyone interested in some cinematic samples?"},
{0, 4, "Weekend set coming soon, stay tuned!"}, {1, 1, "What DAW is everyone using?"},
{1, 2, "Ableton all the way. FL Studio for quick ideas."}, {1, 3, "Pro Tools for recording, Reaper for mixing."},
{1, 5, "Logic Pro X here. Love the stock plugins."}, {2, 2, "New beat pack dropping this weekend. 10 beats, all original."},
{2, 7, "How much for exclusive rights?"}, {2, 2, "DM me for pricing on exclusives!"},
}
for i, m := range msgs {
ts := hoursAgo(len(msgs) - i)
tryExec(db, `INSERT INTO messages (room_id,sender_id,user_id,content,message_type,created_at,updated_at) VALUES ($1,$2,$2,$3,'text',$4,$4)`,
roomIDs[m.r], users[m.s].id, m.c, ts)
}
fmt.Printf("3 rooms, %d messages\n", len(msgs))
}
// ═════════════════════════════════════════════════════════════════════════
// TRACK PLAYS (analytics — fixed column names)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "track_plays") == 0 {
fmt.Print("Creating play history... ")
c := 0
listeners := []int{6, 7, 8}
sources := []string{"web", "mobile", "api"}
countries := []string{"FR", "US", "DE", "GB", "JP", "BR", "CA"}
for _, li := range listeners {
for _, t := range tracks {
// Each listener plays ~70% of tracks, some multiple times
plays := 0
if rand.Intn(10) < 7 {
plays = 1
}
if rand.Intn(10) < 3 {
plays = randBetween(2, 5) // replay
}
for p := 0; p < plays; p++ {
ts := daysAgo(randBetween(0, 45))
dur := t.duration * randBetween(60, 100) / 100 // 60-100% of track
tryExec(db, `INSERT INTO track_plays (track_id,user_id,duration,played_at,source,country_code,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$4,$4)`,
t.id, users[li].id, dur, ts, sources[rand.Intn(len(sources))], countries[rand.Intn(len(countries))])
c++
}
}
}
fmt.Printf("%d plays recorded\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// TRACK LIKES
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "track_likes") < 20 {
fmt.Print("Creating likes... ")
c := 0
for _, li := range []int{6, 7, 8} {
for i, t := range tracks {
if i%3 == 0 || i%5 == 0 || rand.Intn(4) == 0 {
if _, err := db.Exec(`INSERT INTO track_likes (track_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING`, t.id, users[li].id); err == nil {
c++
}
}
}
}
fmt.Printf("%d likes\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// COMMENTS
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "comments") == 0 {
fmt.Print("Creating comments... ")
commentData := []struct{ track, user int; content string }{
{0, 6, "This track is incredible, the synth work is amazing!"}, {0, 7, "Perfect for late night coding sessions."},
{0, 8, "Amelie never disappoints. 🔥"}, {1, 7, "Dark and moody, love it."},
{5, 6, "These loops are so clean."}, {5, 8, "Could listen to this on repeat all day."},
{7, 6, "Smooth R&B vibes, exactly what I needed."}, {10, 8, "Beautiful nature sounds, so calming."},
{14, 7, "DJ Renzo always brings the groove!"}, {14, 6, "This one gets the party started!"},
{18, 6, "Clara your voice is so beautiful."}, {18, 8, "Acoustic perfection."},
{20, 7, "This made me emotional, beautiful songwriting."}, {9, 6, "Lo-fi perfection for rainy days."},
{3, 8, "The production quality is top notch."}, {16, 7, "Deep house at its finest."},
{11, 6, "I can hear the ocean in my headphones."}, {19, 8, "Morning Light is my alarm song now."},
{4, 7, "Peak time techno! Need this in a set."}, {15, 6, "Funky Elevator is an instant classic."},
}
for _, cm := range commentData {
if cm.track < len(tracks) {
tryExec(db, `INSERT INTO comments (user_id,target_id,target_type,content,created_at,updated_at) VALUES ($1,$2,'track',$3,$4,$4)`,
users[cm.user].id, tracks[cm.track].id, cm.content, daysAgo(randBetween(0, 30)))
}
}
fmt.Printf("%d comments\n", len(commentData))
}
// ═════════════════════════════════════════════════════════════════════════
// NOTIFICATIONS (fixed: column "read" not "is_read", "content" not "message")
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "notifications") == 0 {
fmt.Print("Creating notifications... ")
notifs := []struct{ user int; ntype, title, content string }{
{1, "follow", "New follower", "music_lover started following you"},
{1, "follow", "New follower", "groove_hunter started following you"},
{2, "follow", "New follower", "night_owl started following you"},
{1, "like", "Track liked", "Someone liked your track Neon Dreams"},
{2, "like", "Track liked", "Someone liked your track Late Night Loops"},
{3, "like", "Track liked", "Someone liked your track Forest Whispers"},
{4, "comment", "New comment", "music_lover commented on Saturday Night Edit"},
{5, "comment", "New comment", "night_owl commented on Paper Boats"},
{1, "system", "Welcome", "Welcome to Veza! Start by uploading your first track."},
{6, "system", "Welcome", "Welcome to Veza! Discover amazing music from independent artists."},
{7, "system", "Welcome", "Welcome to Veza! Follow your favorite artists to see their new releases."},
{0, "system", "Admin alert", "New user registrations this week: 5"},
{2, "milestone", "Milestone reached", "Your track Late Night Loops just hit 100 plays!"},
{1, "milestone", "Milestone reached", "You now have 5 followers!"},
}
for _, n := range notifs {
tryExec(db, `INSERT INTO notifications (user_id,type,title,content,read,created_at,updated_at) VALUES ($1,$2,$3,$4,false,$5,$5)`,
users[n.user].id, n.ntype, n.title, n.content, daysAgo(randBetween(0, 14)))
}
fmt.Printf("%d created\n", len(notifs))
}
// ═════════════════════════════════════════════════════════════════════════
// PRODUCTS (marketplace — 12 products from creators)
// ═════════════════════════════════════════════════════════════════════════
type product struct{ id, seller, title, desc, ptype, license, category string; price float64; trackIdx int; bpm int; key string }
var products []product
if countRows(db, "products") == 0 {
fmt.Print("Creating marketplace products... ")
products = []product{
{uuid.NewString(), marcusID, "Lo-Fi Beats Pack Vol.1", "10 royalty-free lo-fi beats for content creators", "sample-pack", "non-exclusive", "beats", 29.99, -1, 80, "Cm"},
{uuid.NewString(), marcusID, "Trap Essentials", "5 hard-hitting trap beats ready to use", "sample-pack", "non-exclusive", "beats", 19.99, -1, 140, "Gm"},
{uuid.NewString(), amelieID, "Neon Dreams — Exclusive License", "Full exclusive rights to Neon Dreams", "beat", "exclusive", "electronic", 299.99, 0, 128, "Am"},
{uuid.NewString(), amelieID, "Synth Textures Pack", "50 custom synth one-shots and loops", "sample-pack", "non-exclusive", "samples", 14.99, -1, 0, ""},
{uuid.NewString(), amelieID, "Techno Stems — Midnight Protocol", "Individual stems for remix", "beat", "non-exclusive", "stems", 49.99, 1, 132, "Dm"},
{uuid.NewString(), renzoID, "Disco Edits Bundle", "3 disco edits ready for DJ sets", "sample-pack", "non-exclusive", "dj-tools", 24.99, -1, 120, "G"},
{uuid.NewString(), renzoID, "Saturday Night Edit — License", "Non-exclusive license for streaming", "beat", "non-exclusive", "house", 39.99, 14, 122, "G"},
{uuid.NewString(), sakuraID, "Cinematic Foley Collection", "200+ foley sounds from nature recordings", "sample-pack", "non-exclusive", "sfx", 34.99, -1, 0, ""},
{uuid.NewString(), sakuraID, "Ambient Textures Vol.1", "Layered ambient textures for film scoring", "sample-pack", "non-exclusive", "ambient", 19.99, -1, 0, ""},
{uuid.NewString(), claraID, "Acoustic Guitar Loops", "15 acoustic guitar loops in various keys", "sample-pack", "non-exclusive", "acoustic", 12.99, -1, 95, "G"},
{uuid.NewString(), claraID, "Paper Boats — Sync License", "Sync license for film/TV/ads", "beat", "non-exclusive", "sync", 149.99, 18, 95, "G"},
{uuid.NewString(), marcusID, "City Lights — Lease", "Standard lease for City Lights beat", "beat", "non-exclusive", "trap", 49.99, 8, 140, "Gm"},
}
for _, p := range products {
var trackID interface{}
if p.trackIdx >= 0 && p.trackIdx < len(tracks) {
trackID = tracks[p.trackIdx].id
}
tryExec(db, `INSERT INTO products (id,seller_id,title,description,price,currency,status,product_type,track_id,license_type,bpm,musical_key,category,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,'EUR','active',$6,$7,$8,$9,$10,$11,NOW()-interval '1 day'*$12,NOW())`,
p.id, p.seller, p.title, p.desc, p.price, p.ptype, trackID, p.license, p.bpm, p.key, p.category, randBetween(1, 30))
}
fmt.Printf("%d products\n", len(products))
} else {
fmt.Println("Products already exist — loading IDs...")
rows, _ := db.Query(`SELECT id,seller_id,title FROM products ORDER BY created_at LIMIT 12`)
for rows != nil && rows.Next() {
var p product
_ = rows.Scan(&p.id, &p.seller, &p.title)
products = append(products, p)
}
if rows != nil {
rows.Close()
}
}
// ═════════════════════════════════════════════════════════════════════════
// ORDERS & ORDER ITEMS (4 completed purchases)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "orders") == 0 && len(products) >= 6 {
fmt.Print("Creating orders... ")
orderData := []struct{ buyer int; productIdxs []int; total float64 }{
{6, []int{0, 3}, 44.98}, // music_lover buys lo-fi pack + synth textures
{7, []int{1, 5}, 44.98}, // groove_hunter buys trap essentials + disco edits
{8, []int{6, 9}, 52.98}, // night_owl buys saturday night license + acoustic loops
{6, []int{7}, 34.99}, // music_lover buys foley collection
}
for _, o := range orderData {
oid := uuid.NewString()
tryExec(db, `INSERT INTO orders (id,buyer_id,total_amount,currency,status,created_at,updated_at) VALUES ($1,$2,$3,'EUR','completed',$4,$4)`,
oid, users[o.buyer].id, o.total, daysAgo(randBetween(1, 20)))
for _, pi := range o.productIdxs {
if pi < len(products) {
tryExec(db, `INSERT INTO order_items (order_id,product_id,price) VALUES ($1,$2,$3)`, oid, products[pi].id, products[pi].price)
}
}
}
fmt.Printf("%d orders\n", len(orderData))
}
// ═════════════════════════════════════════════════════════════════════════
// DAILY TRACK STATS (last 30 days for top tracks)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "daily_track_stats") == 0 {
fmt.Print("Creating daily track stats... ")
c := 0
for _, t := range tracks[:10] { // top 10 tracks
for d := 0; d < 30; d++ {
date := daysAgo(d).Format("2006-01-02")
plays := randBetween(1, 25)
uniq := randBetween(1, plays)
complete := randBetween(0, uniq)
totalTime := plays * t.duration * randBetween(60, 100) / 100
avgCompl := float64(randBetween(50, 95)) / 100
tryExec(db, `INSERT INTO daily_track_stats (track_id,date,total_plays,unique_listeners,complete_listens,total_play_time,avg_completion_rate) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT DO NOTHING`,
t.id, date, plays, uniq, complete, totalTime, avgCompl)
c++
}
}
fmt.Printf("%d stat rows\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// COURSES & LESSONS (education)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "courses") == 0 {
fmt.Print("Creating courses & lessons... ")
courseData := []struct{ creator, title, slug, desc, category, level string; price int; lessonCount int }{
{amelieID, "Introduction to Music Production", "intro-music-production", "Learn the basics of music production with Ableton Live. From your first beat to a finished track.", "production", "beginner", 0, 8},
{amelieID, "Melodic Techno Masterclass", "melodic-techno-masterclass", "Deep dive into melodic techno production techniques, sound design, and arrangement.", "production", "intermediate", 4999, 12},
{marcusID, "Hip-Hop Beat Making 101", "hiphop-beatmaking-101", "Learn to make hard-hitting hip-hop beats from scratch. Sampling, drum programming, mixing.", "production", "beginner", 2999, 10},
{sakuraID, "Field Recording & Sound Design", "field-recording-sound-design", "Capture the world around you and turn it into cinematic soundscapes.", "sound-design", "intermediate", 3999, 6},
{claraID, "Songwriting for Beginners", "songwriting-beginners", "Find your voice, write meaningful lyrics, and structure your songs.", "songwriting", "beginner", 0, 5},
}
for _, cd := range courseData {
cid := uuid.NewString()
status := "published"
var publishedAt interface{} = daysAgo(randBetween(5, 40))
tryExec(db, `INSERT INTO courses (id,creator_id,title,slug,description,category,tags,price_cents,currency,pricing_model,status,level,language,lesson_count,published_at,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,ARRAY['music','production'],$7,'EUR','fixed',$8,$9,'fr',$10,$11,$12,$12)`,
cid, cd.creator, cd.title, cd.slug, cd.desc, cd.category, cd.price, status, cd.level, cd.lessonCount, publishedAt, daysAgo(randBetween(10, 50)))
// Create lessons for this course
lessonTitles := []string{
"Getting Started", "Setting Up Your DAW", "Understanding Audio Basics", "Your First Beat",
"Melody and Harmony", "Sound Design Fundamentals", "Arrangement Techniques", "Mixing Basics",
"EQ and Compression", "Effects and Processing", "Mastering Your Track", "Final Project",
}
for li := 0; li < cd.lessonCount && li < len(lessonTitles); li++ {
tryExec(db, `INSERT INTO lessons (course_id,order_index,title,description,duration_seconds,is_preview_free,transcoding_status) VALUES ($1,$2,$3,$4,$5,$6,'completed')`,
cid, li, lessonTitles[li], fmt.Sprintf("Lesson %d of %s", li+1, cd.title), randBetween(300, 1800), li < 2)
}
}
fmt.Printf("%d courses\n", len(courseData))
// Enroll some users
rows, _ := db.Query(`SELECT id FROM courses LIMIT 5`)
var courseIDs []string
for rows != nil && rows.Next() {
var id string
_ = rows.Scan(&id)
courseIDs = append(courseIDs, id)
}
if rows != nil {
rows.Close()
}
for _, cid := range courseIDs {
for _, ui := range []int{6, 7, 8} {
if rand.Intn(3) == 0 {
tryExec(db, `INSERT INTO course_enrollments (user_id,course_id,status,purchased_at) VALUES ($1,$2,'active',NOW()-interval '1 day'*$3) ON CONFLICT DO NOTHING`,
users[ui].id, cid, randBetween(1, 20))
}
}
}
}
// ═════════════════════════════════════════════════════════════════════════
// GEAR ITEMS (creator equipment)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "gear_items") == 0 {
fmt.Print("Creating gear inventory... ")
gearData := []struct{ user int; name, cat, brand, model, status, condition string; price float64 }{
{1, "Ableton Push 3", "controller", "Ableton", "Push 3", "Active", "Excellent", 999},
{1, "Focal Shape 65", "monitors", "Focal", "Shape 65", "Active", "Good", 599},
{1, "RME Babyface Pro FS", "audio-interface", "RME", "Babyface Pro FS", "Active", "Excellent", 849},
{2, "Akai MPC One+", "sampler", "Akai", "MPC One+", "Active", "Good", 699},
{2, "Audio-Technica AT2020", "microphone", "Audio-Technica", "AT2020", "Active", "Good", 99},
{2, "Beyerdynamic DT 770 Pro", "headphones", "Beyerdynamic", "DT 770 Pro", "Active", "Fair", 159},
{3, "Zoom H6", "recorder", "Zoom", "H6", "Active", "Excellent", 349},
{3, "Sennheiser MKH 416", "microphone", "Sennheiser", "MKH 416", "Active", "Good", 999},
{4, "Pioneer DDJ-1000", "dj-controller", "Pioneer", "DDJ-1000", "Active", "Good", 1199},
{4, "Allen & Heath Xone:96", "mixer", "Allen & Heath", "Xone:96", "Active", "Excellent", 1899},
{5, "Martin D-28", "guitar", "Martin", "D-28", "Active", "Good", 2999},
{5, "Neumann U87", "microphone", "Neumann", "U87", "Active", "Excellent", 3199},
}
for _, g := range gearData {
tryExec(db, `INSERT INTO gear_items (user_id,name,category,brand,model,status,condition,purchase_price,currency,purchase_date,is_public,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'EUR',$9,true,NOW(),NOW())`,
users[g.user].id, g.name, g.cat, g.brand, g.model, g.status, g.condition, g.price, daysAgo(randBetween(30, 365)).Format("2006-01-02"))
}
fmt.Printf("%d items\n", len(gearData))
}
// ═════════════════════════════════════════════════════════════════════════
// LIVE STREAMS (scheduled + past)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "live_streams") == 0 {
fmt.Print("Creating live streams... ")
liveData := []struct{ user int; title, desc, cat string; isLive bool; viewers int }{
{4, "Friday Night Disco Set", "Live disco & house set from my studio", "dj-set", false, 0},
{1, "Production Session — New EP Preview", "Working on new melodic techno tracks live", "production", false, 0},
{2, "Beat Making LIVE — Taking Requests", "Making beats on the spot, drop your ideas in chat", "production", false, 0},
{5, "Acoustic Session — Unplugged", "Playing some originals and covers", "performance", false, 0},
}
for _, l := range liveData {
tryExec(db, `INSERT INTO live_streams (user_id,title,description,category,streamer_name,is_live,viewer_count,tags,scheduled_at,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,'[]'::jsonb,$8,NOW(),NOW())`,
users[l.user].id, l.title, l.desc, l.cat, users[l.user].display, l.isLive, l.viewers,
daysAgo(-randBetween(1, 14))) // future scheduled
}
fmt.Printf("%d streams\n", len(liveData))
}
// ═════════════════════════════════════════════════════════════════════════
// ANNOUNCEMENTS
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "announcements") == 0 {
fmt.Print("Creating announcements... ")
annData := []struct{ title, content, atype string }{
{"Welcome to Veza!", "We're thrilled to launch Veza — an ethical music platform built for artists and listeners. Explore, create, and connect.", "info"},
{"Marketplace Now Open", "Buy and sell beats, samples, and presets directly on the platform. Fair pricing, transparent licensing.", "feature"},
{"Scheduled Maintenance", "Brief maintenance window planned for Sunday 3am-5am CET. Streams may be briefly interrupted.", "warning"},
}
for _, a := range annData {
tryExec(db, `INSERT INTO announcements (title,content,type,is_active,starts_at,created_by,created_at) VALUES ($1,$2,$3,true,NOW(),$4,NOW())`,
a.title, a.content, a.atype, users[0].id)
}
fmt.Printf("%d announcements\n", len(annData))
}
// ═════════════════════════════════════════════════════════════════════════
// SUPPORT TICKETS
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "support_tickets") == 0 {
fmt.Print("Creating support tickets... ")
ticketData := []struct{ user int; email, subject, msg, cat, status string }{
{6, "listener1@veza.fr", "Cannot upload profile picture", "I keep getting an error when trying to upload my avatar. File is a 2MB JPEG.", "technical", "open"},
{7, "listener2@veza.fr", "How to create a playlist?", "I'm new here, how do I create a collaborative playlist?", "general", "resolved"},
{2, "marcus@veza.fr", "Payment not received for beat sale", "Sold a beat 5 days ago but haven't received the payout yet.", "billing", "open"},
{8, "listener3@veza.fr", "Feature request: dark mode scheduler", "Would love to have dark mode auto-switch at sunset.", "feature", "open"},
}
for _, t := range ticketData {
tryExec(db, `INSERT INTO support_tickets (user_id,email,subject,message,category,status,created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
users[t.user].id, t.email, t.subject, t.msg, t.cat, t.status, daysAgo(randBetween(0, 10)))
}
fmt.Printf("%d tickets\n", len(ticketData))
}
// ═════════════════════════════════════════════════════════════════════════
// API KEYS (developer portal)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "api_keys") == 0 {
fmt.Print("Creating API keys... ")
apiKeyData := []struct{ user int; name string; scopes string }{
{1, "Amelie Production Bot", "{read,write,tracks}"},
{2, "Marcus Beat Distributor", "{read,tracks,marketplace}"},
}
for _, k := range apiKeyData {
prefix := fmt.Sprintf("veza_%s", uuid.NewString()[:8])
hashedKey, _ := bcrypt.GenerateFromPassword([]byte(uuid.NewString()), 10)
tryExec(db, `INSERT INTO api_keys (user_id,name,prefix,hashed_key,scopes,created_at) VALUES ($1,$2,$3,$4,$5::text[],NOW())`,
users[k.user].id, k.name, prefix, string(hashedKey), k.scopes)
}
fmt.Printf("%d keys\n", len(apiKeyData))
}
// ═════════════════════════════════════════════════════════════════════════
// ANALYTICS EVENTS (general platform events)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "analytics_events") == 0 {
fmt.Print("Creating analytics events... ")
c := 0
eventTypes := []string{"page_view", "track_play", "search", "playlist_create", "follow", "signup", "login"}
for d := 0; d < 14; d++ {
numEvents := randBetween(20, 80)
for e := 0; e < numEvents; e++ {
userIdx := rand.Intn(len(users))
evt := eventTypes[rand.Intn(len(eventTypes))]
tryExec(db, `INSERT INTO analytics_events (event_name,user_id,payload,created_at) VALUES ($1,$2,$3,$4)`,
evt, users[userIdx].id, fmt.Sprintf(`{"source":"web","page":"/dashboard","session_id":"%s"}`, uuid.NewString()[:8]),
daysAgo(d).Add(time.Duration(randBetween(0, 86400))*time.Second))
c++
}
}
fmt.Printf("%d events\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// SUMMARY
// ═════════════════════════════════════════════════════════════════════════
fmt.Println()
fmt.Println("╔═══════════════════════════════════════════════╗")
fmt.Println("║ Seed Complete! ║")
fmt.Println("╚═══════════════════════════════════════════════╝")
fmt.Println("╔═══════════════════════════════════════════════════════════╗")
fmt.Println("║ Seed Complete! ║")
fmt.Println("╚═══════════════════════════════════════════════════════════╝")
fmt.Println()
tables := []string{
"users", "tracks", "playlists", "follows", "rooms", "messages",
"track_plays", "track_likes", "comments", "notifications",
"products", "orders", "order_items", "daily_track_stats",
"courses", "lessons", "course_enrollments", "gear_items",
"live_streams", "announcements", "support_tickets", "api_keys", "analytics_events",
}
for _, t := range tables {
fmt.Printf(" %-24s %d rows\n", t, countRows(db, t))
"users", "user_profiles", "user_settings", "user_roles",
"tracks", "track_genres", "track_tags",
"playlists", "playlist_tracks", "playlist_follows",
"follows", "track_likes", "track_reposts", "track_comments", "comments", "user_blocks",
"rooms", "room_members", "messages",
"live_streams", "co_listening_sessions",
"products", "orders", "order_items", "product_reviews",
"seller_stripe_accounts", "seller_balances",
"track_plays", "playback_history", "daily_track_stats",
"geographic_play_stats", "analytics_events",
"files", "user_storage_quotas",
"courses", "lessons", "course_enrollments",
"gear_items",
"groups", "group_members",
"reports", "moderation_actions", "user_strikes", "user_suspensions",
"notifications", "notification_preferences",
"support_tickets", "api_keys", "announcements",
"data_exports", "login_history", "user_preferences",
}
totalRows := 0
for _, t := range tables {
n := CountRows(db, t)
totalRows += n
fmt.Printf(" %-30s %7d rows\n", t, n)
}
fmt.Printf("\n %-30s %7d rows\n", "TOTAL", totalRows)
fmt.Printf(" %-30s %s\n", "Duration", elapsed.Round(time.Millisecond))
fmt.Println("\n--- Test Accounts ---")
for _, ta := range testAccounts {
fmt.Printf(" %-12s %-25s password: %s\n", ta.Role, ta.Email, ta.Password)
}
fmt.Println()
fmt.Println("--- Login Credentials (Password123! for all) ---")
fmt.Println(" Admin: admin@veza.fr")
fmt.Println(" Creator: amelie@veza.fr / marcus@veza.fr / sakura@veza.fr")
fmt.Println(" Creator: djrenzo@veza.fr / clara@veza.fr")
fmt.Println(" Listener: listener1@veza.fr / listener2@veza.fr / listener3@veza.fr")
fmt.Println(" Moderator: mod@veza.fr")
fmt.Println()
fmt.Println(" Dashboard: http://veza.fr:5173/dashboard")
}
func modeName(cfg Config) string {
if cfg.TotalUsers <= 100 {
return "MINIMAL"
}
return "FULL"
}
// validate checks data integrity.
func validate(db *sql.DB) {
checks := []struct {
name string
query string
}{
{"test accounts exist", "SELECT COUNT(*) FROM users WHERE email IN ('admin@veza.music','artist@veza.music','user@veza.music','mod@veza.music','new@veza.music')"},
{"no orphan track_plays", "SELECT COUNT(*) FROM track_plays tp LEFT JOIN tracks t ON tp.track_id = t.id WHERE t.id IS NULL"},
{"no orphan follows", "SELECT COUNT(*) FROM follows f LEFT JOIN users u ON f.follower_id = u.id WHERE u.id IS NULL"},
{"no orphan playlist_tracks", "SELECT COUNT(*) FROM playlist_tracks pt LEFT JOIN playlists p ON pt.playlist_id = p.id WHERE p.id IS NULL"},
{"no orphan messages", "SELECT COUNT(*) FROM messages m LEFT JOIN rooms r ON m.room_id = r.id WHERE r.id IS NULL"},
{"no orphan order_items", "SELECT COUNT(*) FROM order_items oi LEFT JOIN orders o ON oi.order_id = o.id WHERE o.id IS NULL"},
}
for _, c := range checks {
var n int
err := db.QueryRow(c.query).Scan(&n)
if err != nil {
fmt.Printf(" ⚠ %s: query error: %v\n", c.name, err)
continue
}
if c.name == "test accounts exist" {
if n == 5 {
fmt.Printf(" ✓ %s: %d/5\n", c.name, n)
} else {
fmt.Printf(" ✗ %s: only %d/5 found!\n", c.name, n)
}
} else {
if n == 0 {
fmt.Printf(" ✓ %s: OK\n", c.name)
} else {
fmt.Printf(" ✗ %s: %d orphans found!\n", c.name, n)
}
}
}
}

View file

@ -0,0 +1,211 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedAnalytics creates track_plays, playback_history, daily_track_stats,
// geographic_play_stats, and analytics_events.
func SeedAnalytics(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ ANALYTICS ═══")
if len(tracks) == 0 || len(users) == 0 {
return nil
}
// ── 1. Track plays (the big one) ─────────────────────────────────────────
p := NewProgress("track_plays", cfg.PlayEvents)
playRows := make([][]interface{}, 0, cfg.PlayEvents)
monthsBack := cfg.AnalyticsMonths
startDate := time.Now().AddDate(0, -monthsBack, 0)
for i := 0; i < cfg.PlayEvents; i++ {
user := users[rng.Intn(len(users))]
// Power-law: popular tracks get way more plays
track := tracks[PowerLaw(0, len(tracks)-1, 1.5)]
playedAt := RandomTimeBetween(startDate, time.Now())
playedAt = RealisticHour(playedAt)
// Duration: 30-100% of track (most listen to 60%+)
pctListened := randInt(30, 100)
duration := track.Duration * pctListened / 100
if duration < 1 {
duration = 1
}
playRows = append(playRows, []interface{}{
newUUID(), track.ID, user.ID, duration,
playedAt, nil, nil, GenUserAgent(),
GenSource(), GenCountry(),
playedAt, playedAt, nil,
})
}
_, err := BulkInsert(db, "track_plays",
"id, track_id, user_id, duration, played_at, device, ip_address, user_agent, source, country_code, created_at, updated_at, deleted_at",
playRows)
if err != nil {
return fmt.Errorf("insert track_plays: %w", err)
}
p.Update(cfg.PlayEvents)
p.Done()
// ── 2. Playback history ──────────────────────────────────────────────────
historyCount := cfg.PlayEvents / 5 // 20% of plays recorded in history
p = NewProgress("playback_history", historyCount)
historyRows := make([][]interface{}, 0, historyCount)
for i := 0; i < historyCount; i++ {
user := users[rng.Intn(len(users))]
track := tracks[rng.Intn(len(tracks))]
playedAt := RandomTimeBetween(startDate, time.Now())
duration := randInt(30, track.Duration)
completion := 0
if track.Duration > 0 {
completion = duration * 100 / track.Duration
}
if completion > 100 {
completion = 100
}
source := GenSource()
device := pick([]string{"desktop", "mobile", "tablet"})
historyRows = append(historyRows, []interface{}{
newUUID(), user.ID, track.ID,
duration, completion, source, nil, device, playedAt,
})
}
_, err = BulkInsert(db, "playback_history",
"id, user_id, track_id, played_duration, completion_percentage, source, source_id, device_type, played_at",
historyRows)
if err != nil {
return fmt.Errorf("insert playback_history: %w", err)
}
p.Update(historyCount)
p.Done()
// ── 3. Daily track stats ─────────────────────────────────────────────────
// Top 200 tracks, 180 days of stats
topTrackCount := len(tracks) / 5
if topTrackCount > 200 {
topTrackCount = 200
}
days := monthsBack * 30
statsCount := topTrackCount * days
p = NewProgress("daily_track_stats", statsCount)
statsRows := make([][]interface{}, 0, statsCount)
for ti := 0; ti < topTrackCount; ti++ {
track := tracks[ti]
for d := 0; d < days; d++ {
date := time.Now().AddDate(0, 0, -d).Format("2006-01-02")
// Growth curve: more plays for newer days
dayMultiplier := float64(days-d) / float64(days)
basePlays := PowerLaw(0, 50, 1.0)
totalPlays := int(float64(basePlays) * (0.5 + dayMultiplier))
if totalPlays < 0 {
totalPlays = 0
}
uniqueListeners := totalPlays * randInt(50, 90) / 100
if uniqueListeners < 0 {
uniqueListeners = 0
}
completeListens := uniqueListeners * randInt(40, 80) / 100
totalPlayTime := totalPlays * track.Duration * randInt(60, 100) / 100
avgCompletion := randFloat(0.4, 0.95)
statsRows = append(statsRows, []interface{}{
track.ID, date, totalPlays, uniqueListeners,
completeListens, totalPlayTime, avgCompletion,
})
}
}
_, err = BulkInsertRaw(db, "daily_track_stats",
"track_id, date, total_plays, unique_listeners, complete_listens, total_play_time, avg_completion_rate",
statsRows, "ON CONFLICT (track_id, date) DO NOTHING")
if err != nil {
return fmt.Errorf("insert daily_track_stats: %w", err)
}
p.Update(len(statsRows))
p.Done()
// ── 4. Geographic play stats ─────────────────────────────────────────────
geoCount := topTrackCount * 10 // ~10 countries per top track
p = NewProgress("geographic_play_stats", geoCount)
geoRows := make([][]interface{}, 0, geoCount)
for ti := 0; ti < topTrackCount; ti++ {
track := tracks[ti]
countries := pickN(countryCodes, randInt(3, 10))
for _, cc := range countries {
date := time.Now().AddDate(0, 0, -randInt(0, days)).Format("2006-01-02")
geoRows = append(geoRows, []interface{}{
newUUID(), track.ID, cc, "", date,
int64(randInt(1, 5000)), int64(randInt(1, 3000)),
time.Now(), time.Now(),
})
}
}
_, _ = BulkInsert(db, "geographic_play_stats",
"id, track_id, country_code, region, date, play_count, unique_listeners, created_at, updated_at",
geoRows)
p.Update(len(geoRows))
p.Done()
// ── 5. Analytics events ──────────────────────────────────────────────────
eventCount := cfg.PlayEvents / 4
p = NewProgress("analytics_events", eventCount)
eventRows := make([][]interface{}, 0, eventCount)
eventTypes := []string{
"page_view", "track_play", "search", "playlist_create",
"follow", "signup", "login", "track_upload", "profile_view",
"playlist_view", "marketplace_view", "product_view",
}
for i := 0; i < eventCount; i++ {
user := users[rng.Intn(len(users))]
evt := pick(eventTypes)
createdAt := RandomTimeBetween(startDate, time.Now())
createdAt = RealisticHour(createdAt)
payload := fmt.Sprintf(`{"source":"%s","page":"/dashboard","session_id":"%s"}`, GenSource(), newUUID()[:8])
eventRows = append(eventRows, []interface{}{
newUUID(), evt, user.ID, payload, createdAt,
})
}
_, err = BulkInsert(db, "analytics_events",
"id, event_name, user_id, payload, created_at",
eventRows)
if err != nil {
return fmt.Errorf("insert analytics_events: %w", err)
}
p.Update(eventCount)
p.Done()
// ── 6. Update track play/like counts ─────────────────────────────────────
p = NewProgress("update track counts", 1)
_, _ = db.Exec("UPDATE tracks SET play_count = (SELECT COUNT(*) FROM track_plays WHERE track_plays.track_id = tracks.id)")
_, _ = db.Exec("UPDATE tracks SET like_count = (SELECT COUNT(*) FROM track_likes WHERE track_likes.track_id = tracks.id)")
_, _ = db.Exec("UPDATE tracks SET comment_count = (SELECT COUNT(*) FROM track_comments WHERE track_comments.track_id = tracks.id)")
p.Update(1)
p.Done()
// ── 7. Update user profile counts ────────────────────────────────────────
p = NewProgress("update profile counts", 1)
_, _ = db.Exec("UPDATE user_profiles SET follower_count = (SELECT COUNT(*) FROM follows WHERE follows.followed_id = user_profiles.user_id)")
_, _ = db.Exec("UPDATE user_profiles SET following_count = (SELECT COUNT(*) FROM follows WHERE follows.follower_id = user_profiles.user_id)")
_, _ = db.Exec("UPDATE user_profiles SET track_count = (SELECT COUNT(*) FROM tracks WHERE tracks.creator_id = user_profiles.user_id AND tracks.deleted_at IS NULL)")
_, _ = db.Exec("UPDATE user_profiles SET playlist_count = (SELECT COUNT(*) FROM playlists WHERE playlists.user_id = user_profiles.user_id AND playlists.deleted_at IS NULL)")
p.Update(1)
p.Done()
return nil
}

View file

@ -0,0 +1,169 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeededRoom holds room data.
type SeededRoom struct {
ID string
CreatorID string
Name string
}
// SeedChat creates rooms, room_members, messages, read_receipts, and message_reactions.
func SeedChat(db *sql.DB, cfg Config, users []SeededUser) ([]SeededRoom, error) {
fmt.Println("\n═══ CHAT ═══")
rooms := make([]SeededRoom, 0, cfg.Conversations)
allUsers := users
// ── 1. Create rooms ──────────────────────────────────────────────────────
// Mix of group rooms and DM rooms
groupCount := cfg.Conversations / 5 // 20% group rooms
dmCount := cfg.Conversations - groupCount // 80% DMs
p := NewProgress("rooms", cfg.Conversations)
roomRows := make([][]interface{}, 0, cfg.Conversations)
// Group rooms
for i := 0; i < groupCount; i++ {
id := newUUID()
creator := allUsers[rng.Intn(len(allUsers))]
name := pick(roomNames)
if i >= len(roomNames) {
name = fmt.Sprintf("%s #%d", pick(roomNames), i)
}
slug := fmt.Sprintf("room-%d", i)
createdAt := RandomTimeAfter(creator.CreatedAt)
r := SeededRoom{ID: id, CreatorID: creator.ID, Name: name}
rooms = append(rooms, r)
roomRows = append(roomRows, []interface{}{
id, name, slug, fmt.Sprintf("Group chat: %s", name),
"group", false, nil, 50, // room_type, is_private, password_hash, max_members
creator.ID, 0, 0, creator.ID, true,
createdAt, createdAt, nil,
})
}
// DM rooms
for i := 0; i < dmCount; i++ {
id := newUUID()
user1 := allUsers[rng.Intn(len(allUsers))]
user2 := allUsers[rng.Intn(len(allUsers))]
if user1.ID == user2.ID {
continue
}
slug := fmt.Sprintf("dm-%d", i)
createdAt := RandomTimeAfter(user1.CreatedAt)
r := SeededRoom{ID: id, CreatorID: user1.ID, Name: "DM"}
rooms = append(rooms, r)
roomRows = append(roomRows, []interface{}{
id, nil, slug, nil,
"direct", true, nil, 2,
user1.ID, 0, 0, user1.ID, true,
createdAt, createdAt, nil,
})
}
_, err := BulkInsert(db, "rooms",
"id, name, slug, description, room_type, is_private, password_hash, max_members, creator_id, member_count, message_count, owner_id, is_active, created_at, updated_at, deleted_at",
roomRows)
if err != nil {
return nil, fmt.Errorf("insert rooms: %w", err)
}
p.Update(len(roomRows))
p.Done()
// ── 2. Room members ──────────────────────────────────────────────────────
p = NewProgress("room_members", len(rooms)*5)
memberRows := make([][]interface{}, 0, len(rooms)*5)
memberSeen := make(map[string]bool)
for ri, room := range rooms {
// Always add the creator as owner
key := room.ID + ":" + room.CreatorID
if !memberSeen[key] {
memberSeen[key] = true
memberRows = append(memberRows, []interface{}{
newUUID(), room.ID, room.CreatorID, "owner", false, false, nil,
time.Now(), time.Now(), time.Now(), nil,
})
}
// Add members
var memberCount int
if ri < groupCount {
memberCount = randInt(3, 20) // group rooms
} else {
memberCount = 1 // DM: just the other person
}
for j := 0; j < memberCount; j++ {
member := allUsers[rng.Intn(len(allUsers))]
key = room.ID + ":" + member.ID
if memberSeen[key] {
continue
}
memberSeen[key] = true
memberRows = append(memberRows, []interface{}{
newUUID(), room.ID, member.ID, "member", false, false, nil,
time.Now(), time.Now(), time.Now(), nil,
})
}
}
_, err = BulkInsert(db, "room_members",
"id, room_id, user_id, role, is_banned, is_muted, last_read_at, joined_at, created_at, updated_at, deleted_at",
memberRows)
if err != nil {
return nil, fmt.Errorf("insert room_members: %w", err)
}
p.Update(len(memberRows))
p.Done()
// ── 3. Messages ──────────────────────────────────────────────────────────
p = NewProgress("messages", cfg.Messages)
msgRows := make([][]interface{}, 0, cfg.Messages)
// Distribute messages across rooms (more active rooms get more messages)
for i := 0; i < cfg.Messages; i++ {
room := rooms[PowerLaw(0, len(rooms)-1, 1.0)]
sender := allUsers[rng.Intn(len(allUsers))]
content := GenMessage()
// Space messages over time
baseTime := DaysAgo(180)
msgTime := RandomTimeBetween(baseTime, time.Now())
msgTime = RealisticHour(msgTime)
msgRows = append(msgRows, []interface{}{
newUUID(), room.ID, sender.ID, content,
"text", nil, nil, // message_type, attachment_file_id, reply_to_id
false, nil, false, false, nil, // is_edited, edited_at, is_deleted, is_pinned, metadata
sender.ID, nil, // user_id, parent_id
msgTime, nil, msgTime,
})
}
_, err = BulkInsert(db, "messages",
"id, room_id, sender_id, content, message_type, attachment_file_id, reply_to_id, is_edited, edited_at, is_deleted, is_pinned, metadata, user_id, parent_id, created_at, deleted_at, updated_at",
msgRows)
if err != nil {
return nil, fmt.Errorf("insert messages: %w", err)
}
p.Update(len(msgRows))
p.Done()
// Update room message_count and member_count
_, _ = db.Exec("UPDATE rooms SET message_count = (SELECT COUNT(*) FROM messages WHERE messages.room_id = rooms.id)")
_, _ = db.Exec("UPDATE rooms SET member_count = (SELECT COUNT(*) FROM room_members WHERE room_members.room_id = rooms.id)")
return rooms, nil
}

View file

@ -0,0 +1,288 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedContent creates files, user_storage_quotas, courses, lessons, gear_items, groups.
func SeedContent(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ CONTENT ═══")
artists := GetArtists(users)
// ── 1. Files (simulated entries for tracks and avatars) ──────────────────
p := NewProgress("files", cfg.FileEntries)
fileRows := make([][]interface{}, 0, cfg.FileEntries)
// Audio files for tracks
for i, t := range tracks {
if i >= cfg.FileEntries*2/3 {
break
}
fileSize := int64(t.Duration) * int64(randInt(16000, 32000))
storagePath := fmt.Sprintf("audio/%s.mp3", t.ID)
url := fmt.Sprintf("https://cdn.veza.music/%s", storagePath)
fileRows = append(fileRows, []interface{}{
newUUID(), t.CreatorID,
fmt.Sprintf("%s.mp3", t.ID), fmt.Sprintf("%s.mp3", t.Title),
"audio/mpeg", fileSize, storagePath, "s3",
"veza-audio", url, nil, // thumbnail_url
fmt.Sprintf("%x", rng.Int63()), nil, // file_hash, metadata
true, time.Now(), nil, // is_processed, processed_at, processing_error
true, "clean", time.Now(), // virus_scanned, virus_scan_result, virus_scanned_at
false, // is_public
time.Now(), time.Now(), nil,
})
}
// Avatar files for users
avatarCount := cfg.FileEntries / 3
for i := 0; i < avatarCount && i < len(users); i++ {
u := users[i]
fileSize := int64(randInt(50000, 500000)) // 50KB - 500KB
storagePath := fmt.Sprintf("avatars/%s.jpg", u.ID)
url := fmt.Sprintf("https://cdn.veza.music/%s", storagePath)
fileRows = append(fileRows, []interface{}{
newUUID(), u.ID,
fmt.Sprintf("%s.jpg", u.ID), "avatar.jpg",
"image/jpeg", fileSize, storagePath, "s3",
"veza-avatars", url, nil,
fmt.Sprintf("%x", rng.Int63()), nil,
true, time.Now(), nil,
true, "clean", time.Now(),
true,
u.CreatedAt, u.CreatedAt, nil,
})
}
_, err := BulkInsert(db, "files",
"id, user_id, filename, original_filename, mime_type, file_size, storage_path, storage_provider, bucket_name, url, thumbnail_url, file_hash, metadata, is_processed, processed_at, processing_error, virus_scanned, virus_scan_result, virus_scanned_at, is_public, created_at, updated_at, deleted_at",
fileRows)
if err != nil {
return fmt.Errorf("insert files: %w", err)
}
p.Update(len(fileRows))
p.Done()
// ── 2. User storage quotas ───────────────────────────────────────────────
p = NewProgress("user_storage_quotas", len(artists))
quotaRows := make([][]interface{}, 0, len(artists))
for _, a := range artists {
usedBytes := int64(randInt(100, 5000)) * 1024 * 1024 // 100MB - 5GB
maxBytes := int64(10) * 1024 * 1024 * 1024 // 10GB
quotaRows = append(quotaRows, []interface{}{
newUUID(), a.ID, usedBytes, maxBytes, time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "user_storage_quotas",
"id, user_id, used_bytes, max_bytes, created_at, updated_at",
quotaRows)
p.Update(len(quotaRows))
p.Done()
// ── 3. Courses & Lessons ─────────────────────────────────────────────────
p = NewProgress("courses", cfg.Courses)
courseRows := make([][]interface{}, 0, cfg.Courses)
type courseInfo struct {
id string
lessonCount int
}
courses := make([]courseInfo, 0, cfg.Courses)
courseCategories := []string{"production", "mixing", "sound-design", "songwriting", "djing", "mastering"}
levels := []string{"beginner", "intermediate", "advanced"}
lessonTitles := []string{
"Getting Started", "Setting Up Your DAW", "Understanding Audio Basics",
"Your First Beat", "Melody and Harmony", "Sound Design Fundamentals",
"Arrangement Techniques", "Mixing Basics", "EQ and Compression",
"Effects and Processing", "Mastering Your Track", "Final Project",
"Advanced Techniques", "Creative Workflows", "Industry Standards",
}
for i := 0; i < cfg.Courses && i < len(artists); i++ {
id := newUUID()
artist := artists[i%len(artists)]
title := fmt.Sprintf("%s Masterclass — %s", GenreForArtist(i)[0].Name, pick([]string{"From Zero to Pro", "Complete Guide", "Deep Dive", "Essentials", "Workshop"}))
slug := fmt.Sprintf("course-%d", i)
category := pick(courseCategories)
level := pick(levels)
price := randInt(0, 9999) // 0 = free
lessonCount := randInt(5, 15)
createdAt := RandomTimeAfter(artist.CreatedAt)
var publishedAt interface{} = createdAt
status := "published"
if randChance(10) {
status = "draft"
publishedAt = nil
}
c := courseInfo{id: id, lessonCount: lessonCount}
courses = append(courses, c)
courseRows = append(courseRows, []interface{}{
id, artist.ID, title, slug,
fmt.Sprintf("Learn %s with %s. Comprehensive course covering everything from basics to advanced techniques.", category, artist.DisplayName),
category, fmt.Sprintf("{music,production,%s}", category),
price, "EUR", "fixed", status, level, "fr",
lessonCount, publishedAt, createdAt, createdAt,
})
}
_, err = BulkInsert(db, "courses",
"id, creator_id, title, slug, description, category, tags, price_cents, currency, pricing_model, status, level, language, lesson_count, published_at, created_at, updated_at",
courseRows)
if err != nil {
return fmt.Errorf("insert courses: %w", err)
}
p.Update(len(courseRows))
p.Done()
// Lessons
totalLessons := 0
for _, c := range courses {
totalLessons += c.lessonCount
}
p = NewProgress("lessons", totalLessons)
lessonRows := make([][]interface{}, 0, totalLessons)
for _, c := range courses {
for li := 0; li < c.lessonCount; li++ {
title := lessonTitles[li%len(lessonTitles)]
lessonRows = append(lessonRows, []interface{}{
newUUID(), c.id, li, title,
fmt.Sprintf("Lesson %d: %s", li+1, title),
randInt(300, 1800), // duration 5-30min
li < 2, // first 2 lessons free preview
"completed",
})
}
}
_, _ = BulkInsert(db, "lessons",
"id, course_id, order_index, title, description, duration_seconds, is_preview_free, transcoding_status",
lessonRows)
p.Update(totalLessons)
p.Done()
// Course enrollments
enrollCount := cfg.Courses * 5
p = NewProgress("course_enrollments", enrollCount)
enrollRows := make([][]interface{}, 0, enrollCount)
enrollSeen := make(map[string]bool)
for i := 0; i < enrollCount*2 && len(enrollRows) < enrollCount; i++ {
c := courses[rng.Intn(len(courses))]
u := users[rng.Intn(len(users))]
key := c.id + ":" + u.ID
if enrollSeen[key] {
continue
}
enrollSeen[key] = true
enrollRows = append(enrollRows, []interface{}{
newUUID(), u.ID, c.id, "active", time.Now(),
})
}
_, _ = BulkInsert(db, "course_enrollments",
"id, user_id, course_id, status, purchased_at",
enrollRows)
p.Update(len(enrollRows))
p.Done()
// ── 4. Gear items ────────────────────────────────────────────────────────
p = NewProgress("gear_items", cfg.GearItems)
gearRows := make([][]interface{}, 0, cfg.GearItems)
conditions := []string{"Excellent", "Good", "Fair"}
for i := 0; i < cfg.GearItems; i++ {
artist := artists[rng.Intn(len(artists))]
gear := pick(gearTemplates)
purchaseDate := RandomTimeAfter(artist.CreatedAt).Format("2006-01-02")
gearRows = append(gearRows, []interface{}{
newUUID(), artist.ID, gear.Name, gear.Category, gear.Brand, gear.Model,
"Active", pick(conditions), gear.Price, "EUR", purchaseDate,
true, time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "gear_items",
"id, user_id, name, category, brand, model, status, condition, purchase_price, currency, purchase_date, is_public, created_at, updated_at",
gearRows)
p.Update(cfg.GearItems)
p.Done()
// ── 5. Groups ────────────────────────────────────────────────────────────
p = NewProgress("groups", cfg.Groups)
groupRows := make([][]interface{}, 0, cfg.Groups)
type groupInfo struct {
id string
creatorID string
}
groups := make([]groupInfo, 0, cfg.Groups)
groupNames := []string{
"Electronic Producers", "Hip-Hop Collective", "Ambient Sound Lab",
"Jazz Fusion Circle", "Indie Artists Network", "Vinyl Enthusiasts",
"Sound Design Guild", "Remix Community", "Songwriters Hub",
"Studio Gear Talk", "Music Theory Nerds", "Live Performance Group",
"Lo-Fi Producers", "Techno Underground", "Acoustic Sessions",
}
for i := 0; i < cfg.Groups; i++ {
id := newUUID()
creator := users[rng.Intn(len(users))]
name := groupNames[i%len(groupNames)]
if i >= len(groupNames) {
name = fmt.Sprintf("%s #%d", pick(groupNames), i)
}
createdAt := RandomTimeAfter(creator.CreatedAt)
g := groupInfo{id: id, creatorID: creator.ID}
groups = append(groups, g)
groupRows = append(groupRows, []interface{}{
id, name, fmt.Sprintf("A community for %s enthusiasts", name),
creator.ID, nil, true, 1, createdAt, createdAt, nil,
})
}
_, _ = BulkInsert(db, "groups",
"id, name, description, creator_id, avatar_url, is_public, member_count, created_at, updated_at, deleted_at",
groupRows)
p.Update(cfg.Groups)
p.Done()
// Group members
memberCount := cfg.Groups * 15
p = NewProgress("group_members", memberCount)
gmRows := make([][]interface{}, 0, memberCount)
gmSeen := make(map[string]bool)
for _, g := range groups {
// Add creator
key := g.id + ":" + g.creatorID
gmSeen[key] = true
gmRows = append(gmRows, []interface{}{
newUUID(), g.id, g.creatorID, "admin", time.Now(), time.Now(),
})
// Add random members
count := randInt(5, 30)
for j := 0; j < count; j++ {
u := users[rng.Intn(len(users))]
key = g.id + ":" + u.ID
if gmSeen[key] {
continue
}
gmSeen[key] = true
gmRows = append(gmRows, []interface{}{
newUUID(), g.id, u.ID, "member", time.Now(), time.Now(),
})
}
}
_, _ = BulkInsert(db, "group_members",
"id, group_id, user_id, role, joined_at, created_at",
gmRows)
// Update member counts
_, _ = db.Exec("UPDATE groups SET member_count = (SELECT COUNT(*) FROM group_members WHERE group_members.group_id = groups.id)")
p.Update(len(gmRows))
p.Done()
return nil
}

View file

@ -0,0 +1,91 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedLive creates live_streams and co_listening_sessions.
func SeedLive(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ LIVE STREAMS ═══")
artists := GetArtists(users)
if len(artists) == 0 {
return nil
}
// ── 1. Past live streams ─────────────────────────────────────────────────
p := NewProgress("live_streams (past)", cfg.PastLiveStreams)
liveRows := make([][]interface{}, 0, cfg.PastLiveStreams+cfg.LiveStreams)
for i := 0; i < cfg.PastLiveStreams; i++ {
artist := artists[rng.Intn(len(artists))]
title := pick(liveStreamTitles)
desc := fmt.Sprintf("Live session by %s", artist.DisplayName)
category := pick([]string{"dj-set", "production", "performance", "tutorial", "q&a"})
startTime := RandomTimeAfter(artist.CreatedAt)
viewerCount := PowerLaw(1, 500, 1.5)
liveRows = append(liveRows, []interface{}{
newUUID(), artist.ID, title, desc, category,
artist.DisplayName, false, viewerCount,
"[]", // tags (jsonb)
startTime, startTime, startTime,
})
}
p.Update(cfg.PastLiveStreams)
p.Done()
// ── 2. Current live streams ──────────────────────────────────────────────
p = NewProgress("live_streams (active)", cfg.LiveStreams)
for i := 0; i < cfg.LiveStreams && i < len(artists); i++ {
artist := artists[i]
title := pick(liveStreamTitles)
desc := fmt.Sprintf("LIVE NOW: %s", artist.DisplayName)
category := pick([]string{"dj-set", "production", "performance"})
startTime := time.Now().Add(-time.Duration(randInt(5, 90)) * time.Minute)
viewerCount := randInt(5, 150)
liveRows = append(liveRows, []interface{}{
newUUID(), artist.ID, title, desc, category,
artist.DisplayName, true, viewerCount,
"[]",
startTime, startTime, startTime,
})
}
p.Update(cfg.LiveStreams)
p.Done()
_, err := BulkInsert(db, "live_streams",
"id, user_id, title, description, category, streamer_name, is_live, viewer_count, tags, scheduled_at, created_at, updated_at",
liveRows)
if err != nil {
return fmt.Errorf("insert live_streams: %w", err)
}
// ── 3. Co-listening sessions ─────────────────────────────────────────────
coCount := cfg.PastLiveStreams / 10
if coCount < 5 {
coCount = 5
}
p = NewProgress("co_listening_sessions", coCount)
coRows := make([][]interface{}, 0, coCount)
for i := 0; i < coCount && len(tracks) > 0; i++ {
host := users[rng.Intn(len(users))]
track := tracks[rng.Intn(len(tracks))]
createdAt := RandomTimeAfter(host.CreatedAt)
expiresAt := createdAt.Add(4 * time.Hour)
coRows = append(coRows, []interface{}{
newUUID(), host.ID, track.ID, createdAt, expiresAt,
})
}
_, _ = BulkInsert(db, "co_listening_sessions",
"id, host_id, track_id, created_at, expires_at",
coRows)
p.Update(coCount)
p.Done()
return nil
}

View file

@ -0,0 +1,225 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeededProduct holds product data.
type SeededProduct struct {
ID string
SellerID string
Title string
Price float64
}
// SeedMarketplace creates products, orders, order_items, product_reviews,
// seller_stripe_accounts, and seller_balances.
func SeedMarketplace(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) ([]SeededProduct, error) {
fmt.Println("\n═══ MARKETPLACE ═══")
artists := GetArtists(users)
listeners := GetListeners(users)
if len(artists) == 0 {
return nil, nil
}
products := make([]SeededProduct, 0, cfg.Products)
// ── 1. Products ──────────────────────────────────────────────────────────
p := NewProgress("products", cfg.Products)
productRows := make([][]interface{}, 0, cfg.Products)
for i := 0; i < cfg.Products; i++ {
id := newUUID()
seller := artists[rng.Intn(len(artists))]
genres := GenreForArtist(rng.Intn(len(artists)))
genre := genres[0]
title := GenProductTitle(genre.Name)
desc := fmt.Sprintf("Professional %s content by %s", genre.Slug, seller.DisplayName)
price := float64(randInt(999, 29999)) / 100.0 // €9.99 - €299.99
ptype := pick(productTypes)
license := pick([]string{"non-exclusive", "exclusive", "non-exclusive"})
category := pick(productCategories)
bpm := randInt(genre.BPMRange[0], genre.BPMRange[1])
key := ""
if len(genre.Keys) > 0 {
key = pick(genre.Keys)
}
// Optionally link to a track
var trackID interface{}
if ptype == "beat" && len(tracks) > 0 && randChance(50) {
// Find a track by this seller
for _, t := range tracks {
if t.CreatorID == seller.ID {
trackID = t.ID
break
}
}
}
createdAt := RandomTimeAfter(seller.CreatedAt)
prod := SeededProduct{ID: id, SellerID: seller.ID, Title: title, Price: price}
products = append(products, prod)
productRows = append(productRows, []interface{}{
id, seller.ID, title, desc, price, "EUR", "active",
ptype, trackID, license, bpm, key, category,
createdAt, createdAt,
})
}
_, err := BulkInsert(db, "products",
"id, seller_id, title, description, price, currency, status, product_type, track_id, license_type, bpm, musical_key, category, created_at, updated_at",
productRows)
if err != nil {
return nil, fmt.Errorf("insert products: %w", err)
}
p.Update(len(productRows))
p.Done()
// ── 2. Orders ────────────────────────────────────────────────────────────
p = NewProgress("orders + order_items", cfg.Orders)
orderRows := make([][]interface{}, 0, cfg.Orders)
itemRows := make([][]interface{}, 0, cfg.Orders*2)
allBuyers := append(listeners, users...) // Mix of listeners and all users
statuses := []string{"completed", "completed", "completed", "completed", "pending", "cancelled"}
for i := 0; i < cfg.Orders; i++ {
orderID := newUUID()
buyer := allBuyers[rng.Intn(len(allBuyers))]
status := pick(statuses)
createdAt := RandomTimeAfter(buyer.CreatedAt)
// 1-3 items per order
itemCount := randInt(1, 3)
totalAmount := 0.0
selectedProducts := pickN(products, itemCount)
for _, prod := range selectedProducts {
totalAmount += prod.Price
itemRows = append(itemRows, []interface{}{
newUUID(), orderID, prod.ID, prod.Price,
})
}
orderRows = append(orderRows, []interface{}{
orderID, buyer.ID, totalAmount, "EUR", status,
createdAt, createdAt,
})
}
_, err = BulkInsert(db, "orders",
"id, buyer_id, total_amount, currency, status, created_at, updated_at",
orderRows)
if err != nil {
return nil, fmt.Errorf("insert orders: %w", err)
}
_, err = BulkInsert(db, "order_items",
"id, order_id, product_id, price",
itemRows)
if err != nil {
return nil, fmt.Errorf("insert order_items: %w", err)
}
p.Update(cfg.Orders)
p.Done()
// ── 3. Product reviews ───────────────────────────────────────────────────
p = NewProgress("product_reviews", cfg.ProductReviews)
reviewRows := make([][]interface{}, 0, cfg.ProductReviews)
reviewSeen := make(map[string]bool)
reviewContents := []string{
"Excellent quality, exactly what I needed!", "Great sounds, well organized.",
"Good value for the price.", "Professional quality samples.",
"Perfect for my latest project.", "Would buy again!",
"Decent pack, some gems in there.", "Not what I expected but still useful.",
"Top-notch production quality.", "These beats are fire!",
"Clean mix, ready to use.", "Amazing variety in this pack.",
}
for i := 0; i < cfg.ProductReviews*2 && len(reviewRows) < cfg.ProductReviews; i++ {
prod := products[rng.Intn(len(products))]
reviewer := allBuyers[rng.Intn(len(allBuyers))]
if reviewer.ID == prod.SellerID {
continue
}
key := prod.ID + ":" + reviewer.ID
if reviewSeen[key] {
continue
}
reviewSeen[key] = true
reviewRows = append(reviewRows, []interface{}{
newUUID(), prod.ID, reviewer.ID,
randInt(3, 5), // rating 3-5 (realistic positive bias)
pick(reviewContents),
time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "product_reviews",
"id, product_id, user_id, rating, content, created_at, updated_at",
reviewRows)
p.Update(len(reviewRows))
p.Done()
// ── 4. Seller stripe accounts ────────────────────────────────────────────
p = NewProgress("seller_stripe_accounts", len(artists))
sellerRows := make([][]interface{}, 0, len(artists))
for _, artist := range artists {
if !randChance(60) {
continue
}
sellerRows = append(sellerRows, []interface{}{
newUUID(), artist.ID,
fmt.Sprintf("acct_%s", newUUID()[:16]),
true, // is_onboarded
true, // payouts_enabled
true, // charges_enabled
"verified", // kyc_status
nil, nil, // kyc_verification_session_id, kyc_verified_at
nil, // kyc_last_error
time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "seller_stripe_accounts",
"id, user_id, stripe_account_id, is_onboarded, payouts_enabled, charges_enabled, kyc_status, kyc_verification_session_id, kyc_verified_at, kyc_last_error, created_at, updated_at",
sellerRows)
p.Update(len(sellerRows))
p.Done()
// ── 5. Seller balances ───────────────────────────────────────────────────
p = NewProgress("seller_balances", len(artists))
balanceRows := make([][]interface{}, 0, len(artists))
for _, artist := range artists {
if !randChance(50) {
continue
}
totalEarned := int64(randInt(0, 100000))
totalPaid := totalEarned * int64(randInt(30, 80)) / 100
available := totalEarned - totalPaid - int64(randInt(0, 10000))
if available < 0 {
available = 0
}
pending := int64(randInt(0, 10000))
balanceRows = append(balanceRows, []interface{}{
newUUID(), artist.ID,
available, pending, totalEarned, totalPaid,
"EUR", time.Now(),
})
}
_, _ = BulkInsert(db, "seller_balances",
"id, seller_id, available_cents, pending_cents, total_earned_cents, total_paid_out_cents, currency, updated_at",
balanceRows)
p.Update(len(balanceRows))
p.Done()
return products, nil
}

View file

@ -0,0 +1,195 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedMisc creates support_tickets, api_keys, announcements, data_exports,
// login_history, audit_logs, and user_preferences.
func SeedMisc(db *sql.DB, cfg Config, users []SeededUser) error {
fmt.Println("\n═══ MISC ═══")
artists := GetArtists(users)
admins := GetAdmins(users)
// ── 1. Support tickets ───────────────────────────────────────────────────
p := NewProgress("support_tickets", cfg.SupportTickets)
ticketRows := make([][]interface{}, 0, cfg.SupportTickets)
ticketCategories := []string{"technical", "billing", "general", "feature", "account"}
ticketStatuses := []string{"open", "open", "in_progress", "resolved", "closed"}
ticketSubjects := []string{
"Cannot upload audio file", "Payment not received", "Account access issue",
"Feature request: dark mode", "Playlist sync problem", "Mobile app crash",
"Stream quality issues", "Profile picture upload failed", "How to sell beats?",
"Collaboration feature not working", "Missing analytics data", "Copyright claim dispute",
"Email notifications not arriving", "Two-factor auth setup", "Delete my account",
}
for i := 0; i < cfg.SupportTickets; i++ {
user := users[rng.Intn(len(users))]
subject := pick(ticketSubjects)
ticketRows = append(ticketRows, []interface{}{
newUUID(), user.ID, user.Email,
subject, fmt.Sprintf("Details about: %s. Please help!", subject),
pick(ticketCategories), pick(ticketStatuses),
RandomTimeAfter(user.CreatedAt),
})
}
_, _ = BulkInsert(db, "support_tickets",
"id, user_id, email, subject, message, category, status, created_at",
ticketRows)
p.Update(cfg.SupportTickets)
p.Done()
// ── 2. API keys ──────────────────────────────────────────────────────────
p = NewProgress("api_keys", cfg.APIKeys)
apiRows := make([][]interface{}, 0, cfg.APIKeys)
apiNames := []string{
"Production Bot", "Beat Distributor", "Analytics Dashboard",
"Auto-Upload Script", "Playlist Sync", "Stream Monitor",
}
for i := 0; i < cfg.APIKeys && i < len(artists); i++ {
artist := artists[i%len(artists)]
prefix := fmt.Sprintf("veza_%s", newUUID()[:8])
hashedKey := fmt.Sprintf("$2a$10$%s", newUUID()) // Placeholder hash
name := apiNames[i%len(apiNames)]
scopes := pick([]string{"{read,write,tracks}", "{read,tracks,marketplace}", "{read}", "{read,write}"})
apiRows = append(apiRows, []interface{}{
newUUID(), artist.ID, name, prefix, hashedKey, scopes, time.Now(),
})
}
_, _ = BulkInsert(db, "api_keys",
"id, user_id, name, prefix, hashed_key, scopes, created_at",
apiRows)
p.Update(len(apiRows))
p.Done()
// ── 3. Announcements ─────────────────────────────────────────────────────
p = NewProgress("announcements", cfg.Announcements)
annRows := make([][]interface{}, 0, cfg.Announcements)
annData := []struct{ title, content, atype string }{
{"Welcome to Veza!", "We're thrilled to launch Veza — an ethical music platform built for artists and listeners.", "info"},
{"Marketplace Now Open", "Buy and sell beats, samples, and presets directly on the platform.", "feature"},
{"New: Live Streaming", "Stream your sessions live to your followers. Available now for all creators.", "feature"},
{"Scheduled Maintenance", "Brief maintenance window planned for this weekend. Streams may be briefly interrupted.", "warning"},
{"Community Guidelines Updated", "We've updated our community guidelines. Please review them.", "info"},
{"Mobile App Beta", "The mobile app is now in beta. Sign up to be a tester!", "feature"},
{"Holiday Sale", "Special discounts on marketplace products this week.", "info"},
{"New Audio Quality Options", "Hi-Res audio streaming is now available for premium users.", "feature"},
{"Creator Fund Launched", "Earn more from your music with our new creator support fund.", "info"},
{"Platform Update v2.0", "Major platform update with improved UI, faster streaming, and new features.", "feature"},
}
adminID := ""
if len(admins) > 0 {
adminID = admins[0].ID
} else {
adminID = users[0].ID
}
for i := 0; i < cfg.Announcements && i < len(annData); i++ {
a := annData[i]
annRows = append(annRows, []interface{}{
newUUID(), a.title, a.content, a.atype,
true, time.Now(), adminID, DaysAgo(cfg.Announcements - i),
})
}
_, _ = BulkInsert(db, "announcements",
"id, title, content, type, is_active, starts_at, created_by, created_at",
annRows)
p.Update(len(annRows))
p.Done()
// ── 4. Data exports (RGPD) ───────────────────────────────────────────────
p = NewProgress("data_exports", cfg.DataExports)
exportRows := make([][]interface{}, 0, cfg.DataExports)
exportStatuses := []string{"completed", "completed", "pending", "processing"}
for i := 0; i < cfg.DataExports; i++ {
user := users[rng.Intn(len(users))]
status := pick(exportStatuses)
createdAt := RandomTimeAfter(user.CreatedAt)
expiresAt := createdAt.Add(7 * 24 * time.Hour) // 7 day expiry
var completedAt interface{}
var s3Key interface{}
var fileSize interface{}
if status == "completed" {
completedAt = RandomTimeAfter(createdAt)
s3Key = fmt.Sprintf("exports/%s/%s.zip", user.ID, newUUID()[:8])
fs := int64(randInt(1000000, 50000000)) // 1MB - 50MB
fileSize = fs
}
exportRows = append(exportRows, []interface{}{
newUUID(), user.ID, status, s3Key, fileSize,
expiresAt, createdAt, completedAt, nil,
})
}
_, _ = BulkInsert(db, "data_exports",
"id, user_id, status, s3_key, file_size_bytes, expires_at, created_at, completed_at, error_message",
exportRows)
p.Update(cfg.DataExports)
p.Done()
// ── 5. Login history ─────────────────────────────────────────────────────
loginCount := len(users) * 3 // ~3 login entries per user on average
p = NewProgress("login_history", loginCount)
loginRows := make([][]interface{}, 0, loginCount)
for i := 0; i < loginCount; i++ {
user := users[rng.Intn(len(users))]
loginAt := RandomTimeAfter(user.CreatedAt)
loginAt = RealisticHour(loginAt)
success := randChance(95) // 95% success rate
reason := ""
if !success {
reason = pick([]string{"invalid_password", "account_locked", "expired_session"})
}
loginRows = append(loginRows, []interface{}{
newUUID(), user.ID, GenIP(), GenUserAgent(),
success, reason, loginAt,
})
}
_, _ = BulkInsert(db, "login_history",
"id, user_id, ip_address, user_agent, success, reason, created_at",
loginRows)
p.Update(loginCount)
p.Done()
// ── 6. User preferences ──────────────────────────────────────────────────
prefCount := len(users) / 3 // ~33% have explicit preferences
p = NewProgress("user_preferences", prefCount)
prefRows := make([][]interface{}, 0, prefCount)
themes := []string{"light", "dark", "auto"}
langs := []string{"fr", "en", "de", "es"}
contrasts := []string{"normal", "high"}
densities := []string{"comfortable", "compact", "spacious"}
prefSeen := make(map[string]bool)
for i := 0; i < prefCount; i++ {
user := users[rng.Intn(len(users))]
if prefSeen[user.ID] {
continue
}
prefSeen[user.ID] = true
prefRows = append(prefRows, []interface{}{
user.ID, pick(themes), pick(langs), "UTC",
"{}", "{}", `{"quality":"high","autoplay":true}`,
pick(contrasts), pick(densities),
randInt(180, 280), randInt(14, 20),
time.Now(),
})
}
_, _ = BulkInsert(db, "user_preferences",
"user_id, theme, language, timezone, notifications, privacy, audio, contrast, density, accent_hue, font_size, updated_at",
prefRows)
p.Update(len(prefRows))
p.Done()
return nil
}

View file

@ -0,0 +1,158 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedModeration creates reports, moderation_actions, user_strikes, user_suspensions.
func SeedModeration(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ MODERATION ═══")
mods := GetModerators(users)
admins := GetAdmins(users)
moderators := append(mods, admins...)
if len(moderators) == 0 {
return nil
}
// ── 1. Reports ───────────────────────────────────────────────────────────
p := NewProgress("reports", cfg.Reports)
reportRows := make([][]interface{}, 0, cfg.Reports)
reportReasons := []string{
"spam", "harassment", "copyright", "inappropriate_content",
"misleading", "hate_speech", "violence", "impersonation",
}
reportStatuses := []string{"pending", "pending", "reviewed", "resolved", "dismissed"}
type reportInfo struct {
id string
}
reports := make([]reportInfo, 0, cfg.Reports)
for i := 0; i < cfg.Reports; i++ {
id := newUUID()
reporter := users[rng.Intn(len(users))]
reason := pick(reportReasons)
status := pick(reportStatuses)
createdAt := RandomTimeAfter(reporter.CreatedAt)
// Report target: user or track
var reportedUserID interface{}
var contentType string
var contentID interface{}
if randChance(60) && len(tracks) > 0 {
t := tracks[rng.Intn(len(tracks))]
contentType = "track"
contentID = t.ID
} else {
u := users[rng.Intn(len(users))]
reportedUserID = u.ID
contentType = "user"
}
reports = append(reports, reportInfo{id: id})
reportRows = append(reportRows, []interface{}{
id, reporter.ID, reportedUserID, contentType, contentID,
fmt.Sprintf("Reported for %s", reason),
status, nil, nil, // resolved_by, resolved_at
createdAt,
pick(reportReasons), "normal", "", "", nil, // category, priority, resolution_note, resolution_action, assigned_to
createdAt,
})
}
_, err := BulkInsert(db, "reports",
"id, reporter_id, reported_user_id, content_type, content_id, reason, status, resolved_by, resolved_at, created_at, category, priority, resolution_note, resolution_action, assigned_to, updated_at",
reportRows)
if err != nil {
return fmt.Errorf("insert reports: %w", err)
}
p.Update(cfg.Reports)
p.Done()
// ── 2. Moderation actions ────────────────────────────────────────────────
actionCount := cfg.Reports / 2 // ~50% of reports get an action
p = NewProgress("moderation_actions", actionCount)
actionRows := make([][]interface{}, 0, actionCount)
actionTypes := []string{"warn", "mute", "suspend", "ban", "content_removal", "dismiss"}
for i := 0; i < actionCount && i < len(reports); i++ {
mod := moderators[rng.Intn(len(moderators))]
action := pick(actionTypes)
targetUser := users[rng.Intn(len(users))]
actionRows = append(actionRows, []interface{}{
newUUID(), mod.ID, targetUser.ID,
nil, nil, // target_content_type, target_content_id
action, fmt.Sprintf("Action: %s — reviewed by moderator", action),
"{}", time.Now(),
})
}
_, _ = BulkInsert(db, "moderation_actions",
"id, moderator_id, target_user_id, target_content_type, target_content_id, action, reason, metadata, created_at",
actionRows)
p.Update(len(actionRows))
p.Done()
// ── 3. User strikes ──────────────────────────────────────────────────────
strikeCount := cfg.Reports / 5
p = NewProgress("user_strikes", strikeCount)
strikeRows := make([][]interface{}, 0, strikeCount)
strikeReasons := []string{"spam", "harassment", "copyright_violation", "inappropriate_content"}
for i := 0; i < strikeCount; i++ {
targetUser := users[rng.Intn(len(users))]
mod := moderators[rng.Intn(len(moderators))]
reason := pick(strikeReasons)
severity := pick([]string{"warning", "minor", "major", "critical"})
expiresAt := time.Now().AddDate(0, 3, 0) // 3 months
strikeRows = append(strikeRows, []interface{}{
newUUID(), targetUser.ID, nil, // report_id
reason, severity, mod.ID,
true, false, nil, false, nil, nil, nil, // is_active, appealed, appeal_text, appeal_resolved, appeal_result, appeal_resolved_by, appeal_resolved_at
expiresAt, time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "user_strikes",
"id, user_id, report_id, reason, severity, issued_by, is_active, appealed, appeal_text, appeal_resolved, appeal_result, appeal_resolved_by, appeal_resolved_at, expires_at, created_at, updated_at",
strikeRows)
p.Update(len(strikeRows))
p.Done()
// ── 4. User suspensions (very few) ───────────────────────────────────────
suspCount := strikeCount / 3
if suspCount < 2 {
suspCount = 2
}
p = NewProgress("user_suspensions", suspCount)
suspRows := make([][]interface{}, 0, suspCount)
for i := 0; i < suspCount; i++ {
targetUser := users[randInt(len(users)/2, len(users)-1)] // Pick from second half (less important)
mod := moderators[rng.Intn(len(moderators))]
reason := pick(strikeReasons)
suspendedUntil := time.Now().Add(time.Duration(randInt(1, 30)) * 24 * time.Hour)
suspRows = append(suspRows, []interface{}{
newUUID(), targetUser.ID,
fmt.Sprintf("Suspended for %s", reason),
mod.ID, suspendedUntil,
true, nil, nil, // is_active, lifted_by, lifted_at
time.Now(),
})
}
_, _ = BulkInsert(db, "user_suspensions",
"id, user_id, reason, suspended_by, suspended_until, is_active, lifted_by, lifted_at, created_at",
suspRows)
p.Update(len(suspRows))
p.Done()
return nil
}

View file

@ -0,0 +1,94 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedNotifications creates notifications and notification_preferences.
func SeedNotifications(db *sql.DB, cfg Config, users []SeededUser) error {
fmt.Println("\n═══ NOTIFICATIONS ═══")
// ── 1. Notifications ─────────────────────────────────────────────────────
p := NewProgress("notifications", cfg.Notifications)
notifRows := make([][]interface{}, 0, cfg.Notifications)
notifTypes := []string{
"follow", "like", "comment", "message", "system",
"milestone", "track_upload", "mention", "playlist_follow",
}
notifTemplates := map[string][2]string{
"follow": {"New follower", "Someone started following you"},
"like": {"Track liked", "Someone liked your track"},
"comment": {"New comment", "New comment on your track"},
"message": {"New message", "You have a new message"},
"system": {"System", "Welcome to Veza!"},
"milestone": {"Milestone", "Your track reached 100 plays!"},
"track_upload": {"New release", "An artist you follow uploaded a new track"},
"mention": {"Mentioned", "You were mentioned in a comment"},
"playlist_follow": {"Playlist followed", "Someone followed your playlist"},
}
for i := 0; i < cfg.Notifications; i++ {
user := users[rng.Intn(len(users))]
ntype := pick(notifTypes)
tmpl := notifTemplates[ntype]
createdAt := RandomTimeAfter(user.CreatedAt)
createdAt = RealisticHour(createdAt)
// 70% read, 30% unread
isRead := randChance(70)
var readAt interface{}
if isRead {
readAt = RandomTimeAfter(createdAt)
}
notifRows = append(notifRows, []interface{}{
newUUID(), user.ID, ntype, tmpl[0], tmpl[1],
nil, isRead, createdAt, createdAt, readAt,
})
}
_, err := BulkInsert(db, "notifications",
"id, user_id, type, title, content, link, read, created_at, updated_at, read_at",
notifRows)
if err != nil {
return fmt.Errorf("insert notifications: %w", err)
}
p.Update(cfg.Notifications)
p.Done()
// ── 2. Notification preferences ──────────────────────────────────────────
// Give ~30% of users custom notification preferences
prefCount := len(users) * 30 / 100
p = NewProgress("notification_preferences", prefCount)
prefRows := make([][]interface{}, 0, prefCount)
npSeen := make(map[string]bool)
for i := 0; i < prefCount && i < len(users); i++ {
user := users[rng.Intn(len(users))]
if npSeen[user.ID] {
continue
}
npSeen[user.ID] = true
prefRows = append(prefRows, []interface{}{
user.ID,
randChance(80), // push_follow
randChance(80), // push_like
randChance(80), // push_comment
randChance(90), // push_message
randChance(70), // push_mention
time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "notification_preferences",
"user_id, push_follow, push_like, push_comment, push_message, push_mention, created_at, updated_at",
prefRows)
p.Update(len(prefRows))
p.Done()
return nil
}

View file

@ -0,0 +1,133 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeededPlaylist holds playlist data.
type SeededPlaylist struct {
ID string
UserID string
Name string
}
// SeedPlaylists creates playlists and populates playlist_tracks, playlist_follows.
func SeedPlaylists(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) ([]SeededPlaylist, error) {
fmt.Println("\n═══ PLAYLISTS ═══")
allUsers := users
playlists := make([]SeededPlaylist, 0, cfg.Playlists)
// ── 1. Create playlists ──────────────────────────────────────────────────
p := NewProgress("playlists", cfg.Playlists)
playlistRows := make([][]interface{}, 0, cfg.Playlists)
for i := 0; i < cfg.Playlists; i++ {
id := newUUID()
user := allUsers[rng.Intn(len(allUsers))]
name := GenPlaylistName(i)
desc := fmt.Sprintf("Curated by %s", user.DisplayName)
createdAt := RandomTimeAfter(user.CreatedAt)
visibility := "public"
isPublic := true
if randChance(15) {
visibility = "private"
isPublic = false
}
isCollab := randChance(10)
pl := SeededPlaylist{ID: id, UserID: user.ID, Name: name}
playlists = append(playlists, pl)
playlistRows = append(playlistRows, []interface{}{
id, user.ID, name, desc, nil, // cover_url
visibility, isCollab, 0, 0, 0, // track_count, duration_seconds, follower_count
name, isPublic,
createdAt, createdAt, nil,
})
}
_, err := BulkInsert(db, "playlists",
"id, user_id, name, description, cover_url, visibility, is_collaborative, track_count, duration_seconds, follower_count, title, is_public, created_at, updated_at, deleted_at",
playlistRows)
if err != nil {
return nil, fmt.Errorf("insert playlists: %w", err)
}
p.Update(cfg.Playlists)
p.Done()
// ── 2. Populate playlist_tracks ──────────────────────────────────────────
if len(tracks) == 0 {
return playlists, nil
}
p = NewProgress("playlist_tracks", cfg.Playlists)
ptRows := make([][]interface{}, 0, cfg.Playlists*15)
seen := make(map[string]bool) // prevent duplicate (playlist_id, track_id)
for _, pl := range playlists {
trackCount := randInt(cfg.PlaylistMinTracks, cfg.PlaylistMaxTracks)
if trackCount > len(tracks) {
trackCount = len(tracks)
}
selectedTracks := pickN(tracks, trackCount)
for pos, t := range selectedTracks {
key := pl.ID + ":" + t.ID
if seen[key] {
continue
}
seen[key] = true
ptRows = append(ptRows, []interface{}{
newUUID(), pl.ID, t.ID, pos, pl.UserID, time.Now(),
})
}
}
_, err = BulkInsert(db, "playlist_tracks",
"id, playlist_id, track_id, position, added_by, added_at",
ptRows)
if err != nil {
return nil, fmt.Errorf("insert playlist_tracks: %w", err)
}
p.Update(len(ptRows))
p.Done()
// ── 3. Update track_count on playlists ───────────────────────────────────
_, _ = db.Exec("UPDATE playlists SET track_count = (SELECT COUNT(*) FROM playlist_tracks WHERE playlist_tracks.playlist_id = playlists.id)")
// ── 4. Playlist follows ──────────────────────────────────────────────────
followCount := len(playlists) * 3 // ~3 follows per playlist on average
p = NewProgress("playlist_follows", followCount)
pfRows := make([][]interface{}, 0, followCount)
pfSeen := make(map[string]bool)
for i := 0; i < followCount; i++ {
pl := playlists[rng.Intn(len(playlists))]
user := allUsers[rng.Intn(len(allUsers))]
if user.ID == pl.UserID {
continue // Don't follow own playlist
}
key := pl.ID + ":" + user.ID
if pfSeen[key] {
continue
}
pfSeen[key] = true
pfRows = append(pfRows, []interface{}{
newUUID(), pl.ID, user.ID, time.Now(), time.Now(), nil,
})
}
_, err = BulkInsert(db, "playlist_follows",
"id, playlist_id, user_id, created_at, updated_at, deleted_at",
pfRows)
if err != nil {
return nil, fmt.Errorf("insert playlist_follows: %w", err)
}
p.Update(len(pfRows))
p.Done()
return playlists, nil
}

View file

@ -0,0 +1,248 @@
package main
import (
"database/sql"
"fmt"
"time"
)
// SeedSocial creates follows, track_likes, track_reposts, comments, and user_blocks.
func SeedSocial(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ SOCIAL ═══")
artists := GetArtists(users)
allUsers := users
// ── 1. Follows (scale-free distribution) ─────────────────────────────────
p := NewProgress("follows", cfg.Follows)
followRows := make([][]interface{}, 0, cfg.Follows)
followSeen := make(map[string]bool)
// Build popularity weights: artists are much more likely to be followed
artistSet := make(map[string]bool)
for _, a := range artists {
artistSet[a.ID] = true
}
for i := 0; i < cfg.Follows*2 && len(followRows) < cfg.Follows; i++ {
follower := allUsers[rng.Intn(len(allUsers))]
// 70% chance to follow an artist, 30% random user
var followed SeededUser
if randChance(70) && len(artists) > 0 {
// Power-law: top artists get followed more
idx := PowerLaw(0, len(artists)-1, 1.5)
followed = artists[idx]
} else {
followed = allUsers[rng.Intn(len(allUsers))]
}
if follower.ID == followed.ID {
continue
}
key := follower.ID + ":" + followed.ID
if followSeen[key] {
continue
}
followSeen[key] = true
createdAt := RandomTimeAfter(follower.CreatedAt)
if followed.CreatedAt.After(follower.CreatedAt) {
createdAt = RandomTimeAfter(followed.CreatedAt)
}
followRows = append(followRows, []interface{}{
newUUID(), follower.ID, followed.ID, createdAt, createdAt,
})
}
// Ensure test artist@veza.music has many followers
testArtist := findUser(users, "artist@veza.music")
if testArtist != nil {
for _, u := range users[:min(200, len(users))] {
if u.ID == testArtist.ID {
continue
}
key := u.ID + ":" + testArtist.ID
if followSeen[key] {
continue
}
followSeen[key] = true
followRows = append(followRows, []interface{}{
newUUID(), u.ID, testArtist.ID, RandomTimeAfter(u.CreatedAt), time.Now(),
})
}
}
_, err := BulkInsert(db, "follows",
"id, follower_id, followed_id, created_at, updated_at",
followRows)
if err != nil {
return fmt.Errorf("insert follows: %w", err)
}
p.Update(len(followRows))
p.Done()
// ── 2. Track likes ───────────────────────────────────────────────────────
if len(tracks) == 0 {
return nil
}
p = NewProgress("track_likes", cfg.TrackLikes)
likeRows := make([][]interface{}, 0, cfg.TrackLikes)
likeSeen := make(map[string]bool)
for i := 0; i < cfg.TrackLikes*2 && len(likeRows) < cfg.TrackLikes; i++ {
user := allUsers[rng.Intn(len(allUsers))]
// Power-law: popular tracks get more likes
track := tracks[PowerLaw(0, len(tracks)-1, 1.2)]
key := user.ID + ":" + track.ID
if likeSeen[key] {
continue
}
likeSeen[key] = true
likeRows = append(likeRows, []interface{}{
newUUID(), user.ID, track.ID, time.Now(),
})
}
_, err = BulkInsert(db, "track_likes",
"id, user_id, track_id, created_at",
likeRows)
if err != nil {
return fmt.Errorf("insert track_likes: %w", err)
}
p.Update(len(likeRows))
p.Done()
// ── 3. Track reposts ─────────────────────────────────────────────────────
p = NewProgress("track_reposts", cfg.TrackReposts)
repostRows := make([][]interface{}, 0, cfg.TrackReposts)
repostSeen := make(map[string]bool)
for i := 0; i < cfg.TrackReposts*2 && len(repostRows) < cfg.TrackReposts; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[PowerLaw(0, len(tracks)-1, 1.2)]
if user.ID == track.CreatorID {
continue // Don't repost own tracks
}
key := user.ID + ":" + track.ID
if repostSeen[key] {
continue
}
repostSeen[key] = true
repostRows = append(repostRows, []interface{}{
newUUID(), user.ID, track.ID, time.Now(),
})
}
_, err = BulkInsert(db, "track_reposts",
"id, user_id, track_id, created_at",
repostRows)
if err != nil {
return fmt.Errorf("insert track_reposts: %w", err)
}
p.Update(len(repostRows))
p.Done()
// ── 4. Comments (track_comments + generic comments) ──────────────────────
p = NewProgress("track_comments", cfg.Comments)
commentRows := make([][]interface{}, 0, cfg.Comments)
for i := 0; i < cfg.Comments; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[PowerLaw(0, len(tracks)-1, 1.0)]
content := GenComment()
createdAt := RandomTimeAfter(user.CreatedAt)
// Timed comment: position within the track
var timestampSec *int
if randChance(40) {
ts := randInt(0, track.Duration)
timestampSec = &ts
}
commentRows = append(commentRows, []interface{}{
newUUID(), track.ID, user.ID, content,
nil, // parent_comment_id
timestampSec, false, false, nil,
createdAt, createdAt, nil,
})
}
_, err = BulkInsert(db, "track_comments",
"id, track_id, user_id, content, parent_comment_id, timestamp_seconds, is_edited, is_deleted, parent_id, created_at, updated_at, deleted_at",
commentRows)
if err != nil {
return fmt.Errorf("insert track_comments: %w", err)
}
p.Update(len(commentRows))
p.Done()
// Also populate the generic 'comments' table (used for track comments via target_type)
p = NewProgress("comments (generic)", cfg.Comments/2)
genCommentRows := make([][]interface{}, 0, cfg.Comments/2)
for i := 0; i < cfg.Comments/2; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[rng.Intn(len(tracks))]
content := GenComment()
createdAt := RandomTimeAfter(user.CreatedAt)
genCommentRows = append(genCommentRows, []interface{}{
newUUID(), user.ID, track.ID, "track", content, createdAt, createdAt,
})
}
_, _ = BulkInsert(db, "comments",
"id, user_id, target_id, target_type, content, created_at, updated_at",
genCommentRows)
p.Update(len(genCommentRows))
p.Done()
// ── 5. User blocks (small number) ────────────────────────────────────────
blockCount := len(users) / 50 // ~2% of users have blocked someone
if blockCount < 5 {
blockCount = 5
}
p = NewProgress("user_blocks", blockCount)
blockRows := make([][]interface{}, 0, blockCount)
blockSeen := make(map[string]bool)
for i := 0; i < blockCount; i++ {
blocker := allUsers[rng.Intn(len(allUsers))]
blocked := allUsers[rng.Intn(len(allUsers))]
if blocker.ID == blocked.ID {
continue
}
key := blocker.ID + ":" + blocked.ID
if blockSeen[key] {
continue
}
blockSeen[key] = true
blockRows = append(blockRows, []interface{}{
newUUID(), blocker.ID, blocked.ID, nil, time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "user_blocks",
"id, blocker_id, blocked_id, reason, created_at, updated_at",
blockRows)
p.Update(len(blockRows))
p.Done()
return nil
}
func findUser(users []SeededUser, email string) *SeededUser {
for i := range users {
if users[i].Email == email {
return &users[i]
}
}
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View file

@ -0,0 +1,224 @@
package main
import (
"database/sql"
"fmt"
"math"
"strings"
)
// SeededTrack holds track data for cross-referencing.
type SeededTrack struct {
ID string
CreatorID string
Title string
Artist string
Genre string
Duration int
BPM int
Key string
AlbumID string
CreatedAt string // RFC3339
}
// SeededAlbum holds album data.
type SeededAlbum struct {
ID string
CreatorID string
Title string
}
// SeedTracks creates tracks with power-law distribution across artists.
func SeedTracks(db *sql.DB, cfg Config, users []SeededUser) ([]SeededTrack, error) {
fmt.Println("\n═══ TRACKS ═══")
artists := GetArtists(users)
if len(artists) == 0 {
return nil, fmt.Errorf("no artists found")
}
// ── 1. Distribute tracks across artists using power law ──────────────────
// Top artists get many more tracks than bottom ones
trackCounts := distributeTracksToArtists(cfg.Tracks, len(artists))
// ── 2. Generate tracks ───────────────────────────────────────────────────
tracks := make([]SeededTrack, 0, cfg.Tracks)
trackRows := make([][]interface{}, 0, cfg.Tracks)
p := NewProgress("tracks", cfg.Tracks)
for ai, artist := range artists {
count := trackCounts[ai]
genres := GenreForArtist(ai)
primaryGenre := genres[0]
for ti := 0; ti < count; ti++ {
id := newUUID()
title := GenTrackTitle()
genre := primaryGenre
if len(genres) > 1 && randChance(30) {
genre = genres[rng.Intn(len(genres))]
}
duration := randInt(30, 720) // 30s to 12min
// Majority 2-5min
if randChance(60) {
duration = randInt(120, 300)
}
bpm := randInt(genre.BPMRange[0], genre.BPMRange[1])
key := ""
if len(genre.Keys) > 0 {
key = pick(genre.Keys)
}
createdAt := RandomTimeAfter(artist.CreatedAt)
createdAt = RealisticHour(createdAt)
filePath := fmt.Sprintf("audio/%s/%s.mp3", artist.Username, strings.ReplaceAll(strings.ToLower(title), " ", "_"))
fileSize := int64(duration) * int64(randInt(16000, 32000)) // ~128-256 kbps
tags := fmt.Sprintf("{%s,%s}", genre.Slug, pick([]string{"chill", "energetic", "dark", "melodic", "atmospheric", "groovy", "deep", "raw", "smooth", "heavy"}))
t := SeededTrack{
ID: id,
CreatorID: artist.ID,
Title: title,
Artist: artist.DisplayName,
Genre: genre.Slug,
Duration: duration,
BPM: bpm,
Key: key,
CreatedAt: createdAt.Format("2006-01-02T15:04:05Z"),
}
tracks = append(tracks, t)
trackRows = append(trackRows, []interface{}{
id, artist.ID, artist.ID, // creator_id, user_id
title, nil, // description
artist.DisplayName, nil, genre.Slug, // artist, album, genre
0, duration, bpm, key,
"public", false, // visibility, is_downloadable
nil, nil, // cover_art_file_id, waveform_data
0, 0, 0, 0, // counts (will be updated)
filePath, fileSize, "mp3", 320, 44100, // file details
nil, nil, // waveform_path, cover_art_path
"completed", nil, "ready", nil, // status, stream_status
true, tags, createdAt, createdAt, createdAt, nil,
})
}
}
_, err := BulkInsert(db, "tracks",
"id, creator_id, user_id, title, description, artist, album, genre, year, duration, bpm, musical_key, visibility, is_downloadable, cover_art_file_id, waveform_data, play_count, like_count, comment_count, download_count, file_path, file_size, format, bitrate, sample_rate, waveform_path, cover_art_path, status, status_message, stream_status, stream_manifest_url, is_public, tags, published_at, created_at, updated_at, deleted_at",
trackRows)
if err != nil {
return nil, fmt.Errorf("insert tracks: %w", err)
}
p.Update(len(trackRows))
p.Done()
// ── 3. Link tracks to genres (track_genres table) ────────────────────────
p = NewProgress("track_genres", len(tracks))
// First, fetch genre IDs
genreMap := make(map[string]string)
rows, err := db.Query("SELECT id, slug FROM genres")
if err == nil {
for rows.Next() {
var id, slug string
_ = rows.Scan(&id, &slug)
genreMap[slug] = id
}
rows.Close()
}
if len(genreMap) > 0 {
tgRows := make([][]interface{}, 0, len(tracks))
for _, t := range tracks {
if gid, ok := genreMap[t.Genre]; ok {
tgRows = append(tgRows, []interface{}{t.ID, gid})
}
}
_, _ = BulkInsert(db, "track_genres", "track_id, genre_id", tgRows)
}
p.Update(len(tracks))
p.Done()
// ── 4. Link tracks to tags (track_tags table) ────────────────────────────
p = NewProgress("track_tags", len(tracks))
// Fetch tag IDs
tagMap := make(map[string]string)
rows, err = db.Query("SELECT id, name FROM tags")
if err == nil {
for rows.Next() {
var id, name string
_ = rows.Scan(&id, &name)
tagMap[strings.ToLower(name)] = id
}
rows.Close()
}
if len(tagMap) > 0 {
ttRows := make([][]interface{}, 0, len(tracks)*2)
for _, t := range tracks {
if tid, ok := tagMap[t.Genre]; ok {
ttRows = append(ttRows, []interface{}{t.ID, tid})
}
}
if len(ttRows) > 0 {
_, _ = BulkInsert(db, "track_tags", "track_id, tag_id", ttRows)
}
}
p.Update(len(tracks))
p.Done()
return tracks, nil
}
// distributeTracksToArtists distributes totalTracks across numArtists
// using a power-law distribution. Top artists get many more tracks.
func distributeTracksToArtists(totalTracks, numArtists int) []int {
counts := make([]int, numArtists)
// Generate power-law weights
weights := make([]float64, numArtists)
for i := range weights {
// Zipf-like: weight = 1/(rank^0.8)
weights[i] = 1.0 / math.Pow(float64(i+1), 0.8)
}
// Normalize to sum to totalTracks
totalWeight := 0.0
for _, w := range weights {
totalWeight += w
}
assigned := 0
for i := range counts {
counts[i] = int(float64(totalTracks) * weights[i] / totalWeight)
if counts[i] < 1 {
counts[i] = 1
}
assigned += counts[i]
}
// Adjust to hit exact total
diff := totalTracks - assigned
for i := 0; diff > 0; i = (i + 1) % numArtists {
counts[i]++
diff--
}
for i := 0; diff < 0; i = (i + 1) % numArtists {
if counts[i] > 1 {
counts[i]--
diff++
}
}
// Cap at 80 tracks per artist
for i := range counts {
if counts[i] > 80 {
counts[i] = 80
}
}
return counts
}

View file

@ -0,0 +1,376 @@
package main
import (
"database/sql"
"fmt"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
)
// TestAccount represents a predefined test account.
type TestAccount struct {
Email string
Password string
Username string
DisplayName string
Role string
IsAdmin bool
Bio string
}
var testAccounts = []TestAccount{
{"admin@veza.music", "Admin123!", "admin_veza", "Admin Veza", "admin", true, "Platform administrator. Managing all the things."},
{"artist@veza.music", "Artist123!", "top_artist", "Luna Dubois", "creator", false, "Productrice electro basée à Paris. Melodic techno & ambient. Label founder."},
{"user@veza.music", "User123!", "music_fan", "Music Fan", "user", false, "Music lover. Always discovering new sounds. Playlist curator."},
{"mod@veza.music", "Mod123!", "mod_veza", "Moderator Veza", "moderator", false, "Community moderator. Keeping the vibes positive."},
{"new@veza.music", "New123!", "new_user", "New User", "user", false, "Just joined!"},
}
// SeededUser holds user data for cross-referencing in other seeders.
type SeededUser struct {
ID string
Email string
Username string
DisplayName string
Role string
IsAdmin bool
CreatedAt time.Time
}
// SeedUsers creates all users, profiles, settings, and role assignments.
// Returns the full list of seeded users for use by other seeders.
func SeedUsers(db *sql.DB, cfg Config) ([]SeededUser, error) {
fmt.Println("\n═══ USERS ═══")
// Pre-hash passwords (bcrypt cost 12)
defaultHash, err := bcrypt.GenerateFromPassword([]byte("Password123!"), 12)
if err != nil {
return nil, fmt.Errorf("bcrypt default: %w", err)
}
// Hash each test account password
testHashes := make([]string, len(testAccounts))
for i, ta := range testAccounts {
h, err := bcrypt.GenerateFromPassword([]byte(ta.Password), 12)
if err != nil {
return nil, fmt.Errorf("bcrypt test account %s: %w", ta.Email, err)
}
testHashes[i] = string(h)
}
users := make([]SeededUser, 0, cfg.TotalUsers)
// ── 1. Create test accounts first ────────────────────────────────────────
p := NewProgress("users (test accounts)", len(testAccounts))
testRows := make([][]interface{}, 0, len(testAccounts))
for i, ta := range testAccounts {
id := newUUID()
createdAt := DaysAgo(365 + (len(testAccounts)-i)*30) // Staggered creation
u := SeededUser{
ID: id,
Email: ta.Email,
Username: ta.Username,
DisplayName: ta.DisplayName,
Role: ta.Role,
IsAdmin: ta.IsAdmin,
CreatedAt: createdAt,
}
users = append(users, u)
testRows = append(testRows, []interface{}{
id, ta.Email, createdAt, // email_verified_at = created_at
testHashes[i], ta.Username, ta.Username,
ta.DisplayName, nil, nil, // first_name, last_name
ta.Bio, nil, // location
ta.Role, true, true, false, ta.IsAdmin, true,
0, nil, createdAt, 0, nil, nil,
createdAt, createdAt, nil, "{}",
})
}
_, err = BulkInsert(db, "users",
"id, email, email_verified_at, password_hash, username, slug, display_name, first_name, last_name, bio, location, role, is_active, is_verified, is_banned, is_admin, is_public, token_version, last_password_change_at, last_login_at, login_count, last_login_ip, username_changed_at, created_at, updated_at, deleted_at, social_links",
testRows)
if err != nil {
return nil, fmt.Errorf("insert test users: %w", err)
}
p.Update(len(testAccounts))
p.Done()
// ── 2. Generate remaining users ──────────────────────────────────────────
remaining := cfg.TotalUsers - len(testAccounts)
if remaining <= 0 {
return users, nil
}
// Distribute roles
artistCount := cfg.Artists - 1 // -1 for test artist
labelCount := cfg.Labels
modCount := cfg.Moderators - 1 // -1 for test mod
adminCount := cfg.Admins - 1 // -1 for test admin
normalCount := remaining - artistCount - labelCount - modCount - adminCount
type roleAssignment struct {
role string
isAdmin bool
count int
}
assignments := []roleAssignment{
{"admin", true, adminCount},
{"moderator", false, modCount},
{"creator", false, artistCount},
{"creator", false, labelCount}, // Labels are creators too
{"user", false, normalCount},
}
pwHash := string(defaultHash)
userIdx := len(testAccounts)
allRows := make([][]interface{}, 0, remaining)
for _, ra := range assignments {
p = NewProgress(fmt.Sprintf("users (%s)", ra.role), ra.count)
for i := 0; i < ra.count; i++ {
id := newUUID()
createdAt := RandomRegistrationTime(userIdx, cfg.TotalUsers)
createdAt = RealisticHour(createdAt)
var displayName, bio, location string
var firstName, lastName *string
if ra.role == "creator" {
displayName = GenArtistName(userIdx)
genres := GenreForArtist(userIdx)
bio = GenBio(genres[0].Name)
location = pick(cities)
fn := pick(frenchNames)
ln := pick(frenchLastNames)
firstName = &fn
lastName = &ln
} else {
fn := pick(frenchNames)
ln := pick(frenchLastNames)
displayName = fn + " " + ln
firstName = &fn
lastName = &ln
if randChance(60) {
bio = GenShortBio()
}
if randChance(40) {
location = pick(cities)
}
}
username := GenUsername(displayName, userIdx)
email := fmt.Sprintf("%s@veza-user.local", username)
// Email verification: 90% verified
var emailVerifiedAt interface{}
isVerified := randChance(90)
if isVerified {
emailVerifiedAt = RandomTimeAfter(createdAt)
}
var socialLinks string
if ra.role == "creator" && randChance(70) {
socialLinks = fmt.Sprintf(`{"website":"https://%s.com","instagram":"@%s"}`, username, username)
} else {
socialLinks = "{}"
}
var lastLoginAt interface{}
loginCount := 0
if randChance(80) {
lastLoginAt = RandomTimeAfter(createdAt)
loginCount = randInt(1, 200)
}
u := SeededUser{
ID: id,
Email: email,
Username: username,
DisplayName: displayName,
Role: ra.role,
IsAdmin: ra.isAdmin,
CreatedAt: createdAt,
}
users = append(users, u)
allRows = append(allRows, []interface{}{
id, email, emailVerifiedAt,
pwHash, username, username,
displayName, firstName, lastName,
bio, location,
ra.role, true, isVerified, false, ra.isAdmin, true,
0, nil, lastLoginAt, loginCount, nil, nil,
createdAt, createdAt, nil, socialLinks,
})
userIdx++
}
p.Update(ra.count)
p.Done()
}
p = NewProgress("users (bulk insert)", len(allRows))
_, err = BulkInsert(db, "users",
"id, email, email_verified_at, password_hash, username, slug, display_name, first_name, last_name, bio, location, role, is_active, is_verified, is_banned, is_admin, is_public, token_version, last_password_change_at, last_login_at, login_count, last_login_ip, username_changed_at, created_at, updated_at, deleted_at, social_links",
allRows)
if err != nil {
return nil, fmt.Errorf("insert generated users: %w", err)
}
p.Update(len(allRows))
p.Done()
// ── 3. User profiles ─────────────────────────────────────────────────────
p = NewProgress("user_profiles", len(users))
profileRows := make([][]interface{}, 0, len(users))
for _, u := range users {
var bio, tagline, website, bannerURL, avatarURL *string
language := "fr"
if randChance(30) {
language = "en"
}
theme := pick([]string{"auto", "light", "dark"})
visibility := "public"
if u.Role == "creator" {
b := GenBio(GenreForArtist(0)[0].Name)
bio = &b
t := strings.Split(b, ".")[0]
tagline = &t
w := fmt.Sprintf("https://%s.com", u.Username)
website = &w
bn := fmt.Sprintf("https://cdn.veza.music/banners/%s.jpg", u.ID)
bannerURL = &bn
}
av := fmt.Sprintf("https://cdn.veza.music/avatars/%s.jpg", u.ID)
avatarURL = &av
profileRows = append(profileRows, []interface{}{
newUUID(), u.ID, bio, tagline, nil, website, nil, nil,
avatarURL, bannerURL,
language, "UTC", theme, visibility,
false, true,
0, 0, 0, 0, // counts will be updated
u.CreatedAt, u.CreatedAt,
})
}
_, err = BulkInsert(db, "user_profiles",
"id, user_id, bio, tagline, location, website_url, birthdate, gender, avatar_url, banner_url, language, timezone, theme, profile_visibility, show_email, show_location, follower_count, following_count, track_count, playlist_count, created_at, updated_at",
profileRows)
if err != nil {
return nil, fmt.Errorf("insert profiles: %w", err)
}
p.Update(len(users))
p.Done()
// ── 4. User settings ─────────────────────────────────────────────────────
p = NewProgress("user_settings", len(users))
settingsRows := make([][]interface{}, 0, len(users))
for _, u := range users {
settingsRows = append(settingsRows, []interface{}{
newUUID(), u.ID,
true, true, true, true, true, true, true, true,
false, true, true, false, true,
u.CreatedAt, u.CreatedAt,
})
}
_, err = BulkInsert(db, "user_settings",
"id, user_id, email_notifications, push_notifications, browser_notifications, email_on_follow, email_on_like, email_on_comment, email_on_message, email_on_mention, email_marketing, allow_search_indexing, show_activity, explicit_content, autoplay, created_at, updated_at",
settingsRows)
if err != nil {
return nil, fmt.Errorf("insert settings: %w", err)
}
p.Update(len(users))
p.Done()
// ── 5. User roles (via roles table) ──────────────────────────────────────
p = NewProgress("user_roles", len(users))
// First, fetch role IDs from the roles table
roleMap := make(map[string]string)
rows, err := db.Query("SELECT id, name FROM roles")
if err != nil {
return nil, fmt.Errorf("fetch roles: %w", err)
}
for rows.Next() {
var id, name string
_ = rows.Scan(&id, &name)
roleMap[name] = id
}
rows.Close()
roleRows := make([][]interface{}, 0, len(users))
for _, u := range users {
roleName := u.Role
roleID, ok := roleMap[roleName]
if !ok {
// Try mapping 'creator' to a role in the table
if roleName == "creator" {
if rid, ok2 := roleMap["creator"]; ok2 {
roleID = rid
} else {
continue
}
} else {
continue
}
}
roleRows = append(roleRows, []interface{}{
newUUID(), u.ID, roleID, nil, roleName, true, nil, nil,
u.CreatedAt, nil, true, u.CreatedAt,
})
}
_, err = BulkInsert(db, "user_roles",
"id, user_id, role_id, assigned_by, role, verified, verified_at, verified_by, assigned_at, expires_at, is_active, created_at",
roleRows)
if err != nil {
return nil, fmt.Errorf("insert user_roles: %w", err)
}
p.Update(len(users))
p.Done()
return users, nil
}
// GetArtists returns only users with role "creator".
func GetArtists(users []SeededUser) []SeededUser {
var artists []SeededUser
for _, u := range users {
if u.Role == "creator" {
artists = append(artists, u)
}
}
return artists
}
// GetListeners returns non-creator, non-admin, non-moderator users.
func GetListeners(users []SeededUser) []SeededUser {
var listeners []SeededUser
for _, u := range users {
if u.Role == "user" || u.Role == "premium" {
listeners = append(listeners, u)
}
}
return listeners
}
// GetModerators returns users with moderator role.
func GetModerators(users []SeededUser) []SeededUser {
var mods []SeededUser
for _, u := range users {
if u.Role == "moderator" {
mods = append(mods, u)
}
}
return mods
}
// GetAdmins returns users with admin role.
func GetAdmins(users []SeededUser) []SeededUser {
var admins []SeededUser
for _, u := range users {
if u.IsAdmin {
admins = append(admins, u)
}
}
return admins
}

View file

@ -0,0 +1,251 @@
package main
import (
"database/sql"
"fmt"
"strings"
"time"
)
// BulkInsert performs a multi-row INSERT for maximum performance.
// columns: comma-separated column list. rows: slice of value slices.
// Returns number of rows inserted.
func BulkInsert(db *sql.DB, table string, columns string, rows [][]interface{}) (int, error) {
if len(rows) == 0 {
return 0, nil
}
cols := strings.Split(columns, ",")
for i := range cols {
cols[i] = strings.TrimSpace(cols[i])
}
numCols := len(cols)
// Batch in chunks of 500 rows to stay within PG parameter limit (65535)
batchSize := 500
if numCols > 20 {
batchSize = 200
}
if numCols > 40 {
batchSize = 100
}
total := 0
for start := 0; start < len(rows); start += batchSize {
end := start + batchSize
if end > len(rows) {
end = len(rows)
}
batch := rows[start:end]
var sb strings.Builder
sb.WriteString("INSERT INTO ")
sb.WriteString(table)
sb.WriteString(" (")
sb.WriteString(strings.Join(cols, ","))
sb.WriteString(") VALUES ")
args := make([]interface{}, 0, len(batch)*numCols)
paramIdx := 1
for i, row := range batch {
if i > 0 {
sb.WriteByte(',')
}
sb.WriteByte('(')
for j := 0; j < numCols; j++ {
if j > 0 {
sb.WriteByte(',')
}
fmt.Fprintf(&sb, "$%d", paramIdx)
paramIdx++
if j < len(row) {
args = append(args, row[j])
} else {
args = append(args, nil)
}
}
sb.WriteByte(')')
}
sb.WriteString(" ON CONFLICT DO NOTHING")
_, err := db.Exec(sb.String(), args...)
if err != nil {
return total, fmt.Errorf("bulk insert into %s (batch at offset %d): %w", table, start, err)
}
total += len(batch)
}
return total, nil
}
// BulkInsertRaw is like BulkInsert but with a raw suffix instead of ON CONFLICT DO NOTHING.
func BulkInsertRaw(db *sql.DB, table string, columns string, rows [][]interface{}, suffix string) (int, error) {
if len(rows) == 0 {
return 0, nil
}
cols := strings.Split(columns, ",")
for i := range cols {
cols[i] = strings.TrimSpace(cols[i])
}
numCols := len(cols)
batchSize := 500
if numCols > 20 {
batchSize = 200
}
if numCols > 40 {
batchSize = 100
}
total := 0
for start := 0; start < len(rows); start += batchSize {
end := start + batchSize
if end > len(rows) {
end = len(rows)
}
batch := rows[start:end]
var sb strings.Builder
sb.WriteString("INSERT INTO ")
sb.WriteString(table)
sb.WriteString(" (")
sb.WriteString(strings.Join(cols, ","))
sb.WriteString(") VALUES ")
args := make([]interface{}, 0, len(batch)*numCols)
paramIdx := 1
for i, row := range batch {
if i > 0 {
sb.WriteByte(',')
}
sb.WriteByte('(')
for j := 0; j < numCols; j++ {
if j > 0 {
sb.WriteByte(',')
}
fmt.Fprintf(&sb, "$%d", paramIdx)
paramIdx++
if j < len(row) {
args = append(args, row[j])
} else {
args = append(args, nil)
}
}
sb.WriteByte(')')
}
if suffix != "" {
sb.WriteByte(' ')
sb.WriteString(suffix)
}
_, err := db.Exec(sb.String(), args...)
if err != nil {
return total, fmt.Errorf("bulk insert into %s (batch at offset %d): %w", table, start, err)
}
total += len(batch)
}
return total, nil
}
// TruncateAll truncates all seedable tables in correct FK order.
func TruncateAll(db *sql.DB) error {
// TRUNCATE CASCADE handles FK ordering for us
tables := []string{
// Level 3 (deepest dependents first)
"order_items", "product_previews", "product_images", "product_licenses",
"product_reviews", "product_views", "licenses",
"read_receipts", "delivered_status", "message_reactions",
"cloud_file_versions", "cloud_file_shares",
"shared_queue_items", "queue_sessions",
"lesson_progress", "course_enrollments", "certificates", "course_reviews",
"track_distribution_status_history", "external_streaming_royalties",
// Level 2
"playlist_tracks", "playlist_collaborators", "playlist_follows", "playlist_share_links",
"track_versions", "track_plays", "track_likes", "track_comments", "track_shares",
"track_history", "track_lyrics", "track_stems", "track_tags", "track_genres",
"track_reposts", "track_distributions", "daily_track_stats", "geographic_play_stats",
"track_discovery_sources", "track_segment_stats", "audio_fingerprints",
"playback_history", "playback_analytics", "hls_streams", "hls_transcode_queue",
"file_metadata", "file_conversions", "user_files",
"room_members", "messages", "room_invitations",
"group_members", "group_join_requests", "group_invitations",
"comments", "likes", "webhook_failures",
"gear_images", "gear_documents", "gear_repairs",
"products", "orders",
"queue_items", "analytics_events", "moderation_actions",
"subscription_invoices", "lessons",
"seller_transfers", "seller_payouts",
// Level 1
"user_profiles", "user_settings", "user_roles", "user_preferences", "user_presence",
"user_blocks", "user_storage_quotas", "user_folders", "user_genre_follows",
"user_tag_follows", "notification_preferences", "push_subscriptions",
"federated_identities", "refresh_tokens", "password_reset_tokens",
"email_verification_tokens", "user_sessions",
"follows", "tracks", "playlists", "groups", "posts", "rooms",
"files", "file_uploads", "live_streams", "gear_items",
"api_keys", "webhooks", "reports", "announcements", "support_tickets",
"data_exports", "seller_stripe_accounts", "seller_balances",
"co_listening_sessions", "user_subscriptions", "notifications",
"password_history", "login_history", "sms_verification_codes",
"webauthn_credentials", "metric_alerts", "metric_alert_preferences",
"user_strikes", "user_suspensions", "spam_detections", "queues",
"courses",
// Level 0
"users", "audit_logs",
}
for _, t := range tables {
_, err := db.Exec(fmt.Sprintf("TRUNCATE TABLE %s CASCADE", t))
if err != nil {
// Table might not exist yet, skip silently
continue
}
}
return nil
}
// CountRows returns the row count for a table.
func CountRows(db *sql.DB, table string) int {
var n int
_ = db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&n)
return n
}
// Progress tracks and displays progress for a seeding step.
type Progress struct {
label string
total int
current int
startTime time.Time
}
// NewProgress creates a new progress tracker.
func NewProgress(label string, total int) *Progress {
return &Progress{
label: label,
total: total,
startTime: time.Now(),
}
}
// Update advances the progress counter.
func (p *Progress) Update(n int) {
p.current += n
}
// Done prints the completion message with timing.
func (p *Progress) Done() {
elapsed := time.Since(p.startTime)
fmt.Printf(" %-30s %6d rows (%s)\n", p.label, p.current, elapsed.Round(time.Millisecond))
}
// SeedResult stores the result of a seeding operation.
type SeedResult struct {
Table string
Count int
Duration time.Duration
}

BIN
veza-backend-api/seed-v2 Executable file

Binary file not shown.