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>
248 lines
7.1 KiB
Go
248 lines
7.1 KiB
Go
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
|
|
}
|