veza/veza-backend-api/cmd/tools/seed/main.go
senke 7b2f873736 feat: backend, stream server & infra improvements
Backend (Go):
- Config: CORS, RabbitMQ, rate limit, main config updates
- Routes: core, distribution, tracks routing changes
- Middleware: rate limiter, endpoint limiter, response cache hardening
- Handlers: distribution, search handler fixes
- Workers: job worker improvements
- Upload validator and logging config additions
- New migrations: products, orders, performance indexes
- Seed tooling and data

Stream Server (Rust):
- Audio processing, config, routes, simple stream server updates
- Dockerfile improvements

Infrastructure:
- docker-compose.yml updates
- nginx-rtmp config changes
- Makefile improvements (config, dev, high, infra)
- Root package.json and lock file updates
- .env.example updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:36:06 +01:00

685 lines
42 KiB
Go

package main
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()
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
log.Fatal("DATABASE_URL not set")
}
db, err := sql.Open("postgres", dbURL)
must(err, "DB connect")
defer db.Close()
must(db.Ping(), "DB ping")
fmt.Println("╔═══════════════════════════════════════════════╗")
fmt.Println("║ VEZA — Database Seed Script ║")
fmt.Println("╚═══════════════════════════════════════════════╝")
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)
_, 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,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[],$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)
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{
{6, 1}, {6, 2}, {6, 4}, {7, 1}, {7, 3}, {7, 4}, {7, 2},
{8, 1}, {8, 2}, {8, 3}, {8, 4}, {8, 5},
{1, 2}, {2, 1}, {1, 4}, {4, 1}, {3, 1}, {5, 2}, {5, 3},
}
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','published',$6,$7,$8,$9,$10,$11,NOW()-interval '1 day'*$12,NOW())`,
p.id, p.seller, p.title, p.desc, p.price, p.ptype, trackID, p.license, p.bpm, p.key, p.category, randBetween(1, 30))
}
fmt.Printf("%d products\n", len(products))
} else {
fmt.Println("Products already exist — loading IDs...")
rows, _ := db.Query(`SELECT id,seller_id,title FROM products ORDER BY created_at LIMIT 12`)
for rows != nil && rows.Next() {
var p product
_ = rows.Scan(&p.id, &p.seller, &p.title)
products = append(products, p)
}
if rows != nil {
rows.Close()
}
}
// ═════════════════════════════════════════════════════════════════════════
// ORDERS & ORDER ITEMS (4 completed purchases)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "orders") == 0 && len(products) >= 6 {
fmt.Print("Creating orders... ")
orderData := []struct{ buyer int; productIdxs []int; total float64 }{
{6, []int{0, 3}, 44.98}, // music_lover buys lo-fi pack + synth textures
{7, []int{1, 5}, 44.98}, // groove_hunter buys trap essentials + disco edits
{8, []int{6, 9}, 52.98}, // night_owl buys saturday night license + acoustic loops
{6, []int{7}, 34.99}, // music_lover buys foley collection
}
for _, o := range orderData {
oid := uuid.NewString()
tryExec(db, `INSERT INTO orders (id,buyer_id,total_amount,currency,status,created_at,updated_at) VALUES ($1,$2,$3,'EUR','completed',$4,$4)`,
oid, users[o.buyer].id, o.total, daysAgo(randBetween(1, 20)))
for _, pi := range o.productIdxs {
if pi < len(products) {
tryExec(db, `INSERT INTO order_items (order_id,product_id,price) VALUES ($1,$2,$3)`, oid, products[pi].id, products[pi].price)
}
}
}
fmt.Printf("%d orders\n", len(orderData))
}
// ═════════════════════════════════════════════════════════════════════════
// DAILY TRACK STATS (last 30 days for top tracks)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "daily_track_stats") == 0 {
fmt.Print("Creating daily track stats... ")
c := 0
for _, t := range tracks[:10] { // top 10 tracks
for d := 0; d < 30; d++ {
date := daysAgo(d).Format("2006-01-02")
plays := randBetween(1, 25)
uniq := randBetween(1, plays)
complete := randBetween(0, uniq)
totalTime := plays * t.duration * randBetween(60, 100) / 100
avgCompl := float64(randBetween(50, 95)) / 100
tryExec(db, `INSERT INTO daily_track_stats (track_id,date,total_plays,unique_listeners,complete_listens,total_play_time,avg_completion_rate) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT DO NOTHING`,
t.id, date, plays, uniq, complete, totalTime, avgCompl)
c++
}
}
fmt.Printf("%d stat rows\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// COURSES & LESSONS (education)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "courses") == 0 {
fmt.Print("Creating courses & lessons... ")
courseData := []struct{ creator, title, slug, desc, category, level string; price int; lessonCount int }{
{amelieID, "Introduction to Music Production", "intro-music-production", "Learn the basics of music production with Ableton Live. From your first beat to a finished track.", "production", "beginner", 0, 8},
{amelieID, "Melodic Techno Masterclass", "melodic-techno-masterclass", "Deep dive into melodic techno production techniques, sound design, and arrangement.", "production", "intermediate", 4999, 12},
{marcusID, "Hip-Hop Beat Making 101", "hiphop-beatmaking-101", "Learn to make hard-hitting hip-hop beats from scratch. Sampling, drum programming, mixing.", "production", "beginner", 2999, 10},
{sakuraID, "Field Recording & Sound Design", "field-recording-sound-design", "Capture the world around you and turn it into cinematic soundscapes.", "sound-design", "intermediate", 3999, 6},
{claraID, "Songwriting for Beginners", "songwriting-beginners", "Find your voice, write meaningful lyrics, and structure your songs.", "songwriting", "beginner", 0, 5},
}
for _, cd := range courseData {
cid := uuid.NewString()
status := "published"
var publishedAt interface{} = daysAgo(randBetween(5, 40))
tryExec(db, `INSERT INTO courses (id,creator_id,title,slug,description,category,tags,price_cents,currency,pricing_model,status,level,language,lesson_count,published_at,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,ARRAY['music','production'],$7,'EUR','fixed',$8,$9,'fr',$10,$11,$12,$12)`,
cid, cd.creator, cd.title, cd.slug, cd.desc, cd.category, cd.price, status, cd.level, cd.lessonCount, publishedAt, daysAgo(randBetween(10, 50)))
// Create lessons for this course
lessonTitles := []string{
"Getting Started", "Setting Up Your DAW", "Understanding Audio Basics", "Your First Beat",
"Melody and Harmony", "Sound Design Fundamentals", "Arrangement Techniques", "Mixing Basics",
"EQ and Compression", "Effects and Processing", "Mastering Your Track", "Final Project",
}
for li := 0; li < cd.lessonCount && li < len(lessonTitles); li++ {
tryExec(db, `INSERT INTO lessons (course_id,order_index,title,description,duration_seconds,is_preview_free,transcoding_status) VALUES ($1,$2,$3,$4,$5,$6,'completed')`,
cid, li, lessonTitles[li], fmt.Sprintf("Lesson %d of %s", li+1, cd.title), randBetween(300, 1800), li < 2)
}
}
fmt.Printf("%d courses\n", len(courseData))
// Enroll some users
rows, _ := db.Query(`SELECT id FROM courses LIMIT 5`)
var courseIDs []string
for rows != nil && rows.Next() {
var id string
_ = rows.Scan(&id)
courseIDs = append(courseIDs, id)
}
if rows != nil {
rows.Close()
}
for _, cid := range courseIDs {
for _, ui := range []int{6, 7, 8} {
if rand.Intn(3) == 0 {
tryExec(db, `INSERT INTO course_enrollments (user_id,course_id,status,purchased_at) VALUES ($1,$2,'active',NOW()-interval '1 day'*$3) ON CONFLICT DO NOTHING`,
users[ui].id, cid, randBetween(1, 20))
}
}
}
}
// ═════════════════════════════════════════════════════════════════════════
// GEAR ITEMS (creator equipment)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "gear_items") == 0 {
fmt.Print("Creating gear inventory... ")
gearData := []struct{ user int; name, cat, brand, model, status, condition string; price float64 }{
{1, "Ableton Push 3", "controller", "Ableton", "Push 3", "Active", "Excellent", 999},
{1, "Focal Shape 65", "monitors", "Focal", "Shape 65", "Active", "Good", 599},
{1, "RME Babyface Pro FS", "audio-interface", "RME", "Babyface Pro FS", "Active", "Excellent", 849},
{2, "Akai MPC One+", "sampler", "Akai", "MPC One+", "Active", "Good", 699},
{2, "Audio-Technica AT2020", "microphone", "Audio-Technica", "AT2020", "Active", "Good", 99},
{2, "Beyerdynamic DT 770 Pro", "headphones", "Beyerdynamic", "DT 770 Pro", "Active", "Fair", 159},
{3, "Zoom H6", "recorder", "Zoom", "H6", "Active", "Excellent", 349},
{3, "Sennheiser MKH 416", "microphone", "Sennheiser", "MKH 416", "Active", "Good", 999},
{4, "Pioneer DDJ-1000", "dj-controller", "Pioneer", "DDJ-1000", "Active", "Good", 1199},
{4, "Allen & Heath Xone:96", "mixer", "Allen & Heath", "Xone:96", "Active", "Excellent", 1899},
{5, "Martin D-28", "guitar", "Martin", "D-28", "Active", "Good", 2999},
{5, "Neumann U87", "microphone", "Neumann", "U87", "Active", "Excellent", 3199},
}
for _, g := range gearData {
tryExec(db, `INSERT INTO gear_items (user_id,name,category,brand,model,status,condition,purchase_price,currency,purchase_date,is_public,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'EUR',$9,true,NOW(),NOW())`,
users[g.user].id, g.name, g.cat, g.brand, g.model, g.status, g.condition, g.price, daysAgo(randBetween(30, 365)).Format("2006-01-02"))
}
fmt.Printf("%d items\n", len(gearData))
}
// ═════════════════════════════════════════════════════════════════════════
// LIVE STREAMS (scheduled + past)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "live_streams") == 0 {
fmt.Print("Creating live streams... ")
liveData := []struct{ user int; title, desc, cat string; isLive bool; viewers int }{
{4, "Friday Night Disco Set", "Live disco & house set from my studio", "dj-set", false, 0},
{1, "Production Session — New EP Preview", "Working on new melodic techno tracks live", "production", false, 0},
{2, "Beat Making LIVE — Taking Requests", "Making beats on the spot, drop your ideas in chat", "production", false, 0},
{5, "Acoustic Session — Unplugged", "Playing some originals and covers", "performance", false, 0},
}
for _, l := range liveData {
tryExec(db, `INSERT INTO live_streams (user_id,title,description,category,streamer_name,is_live,viewer_count,tags,scheduled_at,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,'[]'::jsonb,$8,NOW(),NOW())`,
users[l.user].id, l.title, l.desc, l.cat, users[l.user].display, l.isLive, l.viewers,
daysAgo(-randBetween(1, 14))) // future scheduled
}
fmt.Printf("%d streams\n", len(liveData))
}
// ═════════════════════════════════════════════════════════════════════════
// ANNOUNCEMENTS
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "announcements") == 0 {
fmt.Print("Creating announcements... ")
annData := []struct{ title, content, atype string }{
{"Welcome to Veza!", "We're thrilled to launch Veza — an ethical music platform built for artists and listeners. Explore, create, and connect.", "info"},
{"Marketplace Now Open", "Buy and sell beats, samples, and presets directly on the platform. Fair pricing, transparent licensing.", "feature"},
{"Scheduled Maintenance", "Brief maintenance window planned for Sunday 3am-5am CET. Streams may be briefly interrupted.", "warning"},
}
for _, a := range annData {
tryExec(db, `INSERT INTO announcements (title,content,type,is_active,starts_at,created_by,created_at) VALUES ($1,$2,$3,true,NOW(),$4,NOW())`,
a.title, a.content, a.atype, users[0].id)
}
fmt.Printf("%d announcements\n", len(annData))
}
// ═════════════════════════════════════════════════════════════════════════
// SUPPORT TICKETS
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "support_tickets") == 0 {
fmt.Print("Creating support tickets... ")
ticketData := []struct{ user int; email, subject, msg, cat, status string }{
{6, "listener1@veza.fr", "Cannot upload profile picture", "I keep getting an error when trying to upload my avatar. File is a 2MB JPEG.", "technical", "open"},
{7, "listener2@veza.fr", "How to create a playlist?", "I'm new here, how do I create a collaborative playlist?", "general", "resolved"},
{2, "marcus@veza.fr", "Payment not received for beat sale", "Sold a beat 5 days ago but haven't received the payout yet.", "billing", "open"},
{8, "listener3@veza.fr", "Feature request: dark mode scheduler", "Would love to have dark mode auto-switch at sunset.", "feature", "open"},
}
for _, t := range ticketData {
tryExec(db, `INSERT INTO support_tickets (user_id,email,subject,message,category,status,created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
users[t.user].id, t.email, t.subject, t.msg, t.cat, t.status, daysAgo(randBetween(0, 10)))
}
fmt.Printf("%d tickets\n", len(ticketData))
}
// ═════════════════════════════════════════════════════════════════════════
// API KEYS (developer portal)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "api_keys") == 0 {
fmt.Print("Creating API keys... ")
apiKeyData := []struct{ user int; name string; scopes string }{
{1, "Amelie Production Bot", "{read,write,tracks}"},
{2, "Marcus Beat Distributor", "{read,tracks,marketplace}"},
}
for _, k := range apiKeyData {
prefix := fmt.Sprintf("veza_%s", uuid.NewString()[:8])
hashedKey, _ := bcrypt.GenerateFromPassword([]byte(uuid.NewString()), 10)
tryExec(db, `INSERT INTO api_keys (user_id,name,prefix,hashed_key,scopes,created_at) VALUES ($1,$2,$3,$4,$5::text[],NOW())`,
users[k.user].id, k.name, prefix, string(hashedKey), k.scopes)
}
fmt.Printf("%d keys\n", len(apiKeyData))
}
// ═════════════════════════════════════════════════════════════════════════
// ANALYTICS EVENTS (general platform events)
// ═════════════════════════════════════════════════════════════════════════
if countRows(db, "analytics_events") == 0 {
fmt.Print("Creating analytics events... ")
c := 0
eventTypes := []string{"page_view", "track_play", "search", "playlist_create", "follow", "signup", "login"}
for d := 0; d < 14; d++ {
numEvents := randBetween(20, 80)
for e := 0; e < numEvents; e++ {
userIdx := rand.Intn(len(users))
evt := eventTypes[rand.Intn(len(eventTypes))]
tryExec(db, `INSERT INTO analytics_events (event_name,user_id,payload,created_at) VALUES ($1,$2,$3,$4)`,
evt, users[userIdx].id, fmt.Sprintf(`{"source":"web","page":"/dashboard","session_id":"%s"}`, uuid.NewString()[:8]),
daysAgo(d).Add(time.Duration(randBetween(0, 86400))*time.Second))
c++
}
}
fmt.Printf("%d events\n", c)
}
// ═════════════════════════════════════════════════════════════════════════
// SUMMARY
// ═════════════════════════════════════════════════════════════════════════
fmt.Println()
fmt.Println("╔═══════════════════════════════════════════════╗")
fmt.Println("║ Seed Complete! ║")
fmt.Println("╚═══════════════════════════════════════════════╝")
fmt.Println()
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))
}
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")
}