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>
685 lines
42 KiB
Go
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")
|
|
}
|