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:
parent
2efaa1432b
commit
2eff5a9b10
18 changed files with 3662 additions and 662 deletions
|
|
@ -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
|
||||
|
|
|
|||
174
veza-backend-api/cmd/tools/seed/config.go
Normal file
174
veza-backend-api/cmd/tools/seed/config.go
Normal 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()
|
||||
}
|
||||
636
veza-backend-api/cmd/tools/seed/fake.go
Normal file
636
veza-backend-api/cmd/tools/seed/fake.go
Normal 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",
|
||||
}
|
||||
|
|
@ -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("╔═══════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ Seed Complete! ║")
|
||||
fmt.Println("╚═══════════════════════════════════════════════╝")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
211
veza-backend-api/cmd/tools/seed/seed_analytics.go
Normal file
211
veza-backend-api/cmd/tools/seed/seed_analytics.go
Normal 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
|
||||
}
|
||||
169
veza-backend-api/cmd/tools/seed/seed_chat.go
Normal file
169
veza-backend-api/cmd/tools/seed/seed_chat.go
Normal 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
|
||||
}
|
||||
288
veza-backend-api/cmd/tools/seed/seed_content.go
Normal file
288
veza-backend-api/cmd/tools/seed/seed_content.go
Normal 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
|
||||
}
|
||||
91
veza-backend-api/cmd/tools/seed/seed_live.go
Normal file
91
veza-backend-api/cmd/tools/seed/seed_live.go
Normal 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
|
||||
}
|
||||
225
veza-backend-api/cmd/tools/seed/seed_marketplace.go
Normal file
225
veza-backend-api/cmd/tools/seed/seed_marketplace.go
Normal 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
|
||||
}
|
||||
195
veza-backend-api/cmd/tools/seed/seed_misc.go
Normal file
195
veza-backend-api/cmd/tools/seed/seed_misc.go
Normal 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
|
||||
}
|
||||
158
veza-backend-api/cmd/tools/seed/seed_moderation.go
Normal file
158
veza-backend-api/cmd/tools/seed/seed_moderation.go
Normal 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
|
||||
}
|
||||
94
veza-backend-api/cmd/tools/seed/seed_notifications.go
Normal file
94
veza-backend-api/cmd/tools/seed/seed_notifications.go
Normal 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
|
||||
}
|
||||
133
veza-backend-api/cmd/tools/seed/seed_playlists.go
Normal file
133
veza-backend-api/cmd/tools/seed/seed_playlists.go
Normal 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
|
||||
}
|
||||
248
veza-backend-api/cmd/tools/seed/seed_social.go
Normal file
248
veza-backend-api/cmd/tools/seed/seed_social.go
Normal 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
|
||||
}
|
||||
224
veza-backend-api/cmd/tools/seed/seed_tracks.go
Normal file
224
veza-backend-api/cmd/tools/seed/seed_tracks.go
Normal 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
|
||||
}
|
||||
376
veza-backend-api/cmd/tools/seed/seed_users.go
Normal file
376
veza-backend-api/cmd/tools/seed/seed_users.go
Normal 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
|
||||
}
|
||||
251
veza-backend-api/cmd/tools/seed/utils.go
Normal file
251
veza-backend-api/cmd/tools/seed/utils.go
Normal 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
BIN
veza-backend-api/seed-v2
Executable file
Binary file not shown.
Loading…
Reference in a new issue