package db import ( "context" "database/sql" "fmt" "io/fs" "path/filepath" "strings" "time" "golang.org/x/crypto/bcrypt" "github.com/benya/temporserv/internal/auth" "github.com/benya/temporserv/internal/config" ) func Seed(ctx context.Context, database *sql.DB, cfg config.Config) error { if err := seedAdmin(ctx, database, cfg); err != nil { return err } if cfg.AppEnv == "development" && !hasMediaFiles(cfg.MediaRoot) { if err := seedLibrary(ctx, database); err != nil { return err } } return nil } func hasMediaFiles(root string) bool { found := false _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return nil } switch strings.ToLower(filepath.Ext(path)) { case ".aac", ".flac", ".m4a", ".mp3", ".ogg", ".oga", ".opus", ".wav", ".wma": found = true return fs.SkipAll } return nil }) return found } func seedAdmin(ctx context.Context, database *sql.DB, cfg config.Config) error { var count int if err := database.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&count); err != nil { return fmt.Errorf("count users: %w", err) } if count > 0 { return nil } passwordHash, err := bcrypt.GenerateFromPassword([]byte(cfg.DefaultAdminPassword), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("hash admin password: %w", err) } subsonicSecret, err := auth.EncryptSubsonicSecret(cfg.DefaultAdminPassword, cfg.EncryptionKey) if err != nil { return fmt.Errorf("encrypt admin subsonic secret: %w", err) } now := time.Now().UTC().Format(time.RFC3339) _, err = database.ExecContext( ctx, `INSERT INTO users (id, username, password_hash, subsonic_auth_secret, is_admin, created_at, last_login_at) VALUES (?, ?, ?, ?, 1, ?, ?)`, "user-admin", cfg.DefaultAdminUsername, string(passwordHash), subsonicSecret, now, now, ) if err != nil { return fmt.Errorf("insert admin user: %w", err) } return nil } func seedLibrary(ctx context.Context, database *sql.DB) error { var artistCount int if err := database.QueryRowContext(ctx, "SELECT COUNT(*) FROM artists").Scan(&artistCount); err != nil { return fmt.Errorf("count artists: %w", err) } if artistCount > 0 { return nil } now := time.Now().UTC().Format(time.RFC3339) statements := []struct { query string args []any }{ { query: `INSERT INTO artists (id, name, sort_name, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, args: []any{"artist-1", "Tycho", "Tycho", "", now, now}, }, { query: `INSERT INTO artists (id, name, sort_name, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, args: []any{"artist-2", "Bonobo", "Bonobo", "", now, now}, }, { query: `INSERT INTO artists (id, name, sort_name, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, args: []any{"artist-3", "Boards of Canada", "Boards of Canada", "", now, now}, }, { query: `INSERT INTO albums (id, artist_id, title, sort_title, year, genre, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"album-1", "artist-1", "Awake", "Awake", 2014, "Electronic", "", now, now}, }, { query: `INSERT INTO albums (id, artist_id, title, sort_title, year, genre, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"album-2", "artist-2", "Migration", "Migration", 2017, "Electronic", "", now, now}, }, { query: `INSERT INTO albums (id, artist_id, title, sort_title, year, genre, cover_art_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"album-3", "artist-3", "Tomorrow's Harvest", "Tomorrow's Harvest", 2013, "Electronic", "", now, now}, }, { query: `INSERT INTO tracks (id, album_id, artist_id, title, track_number, disc_number, duration_seconds, file_path, content_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"track-1", "album-1", "artist-1", "Awake", 1, 1, 224, "demo/awake.mp3", "audio/mpeg", now, now}, }, { query: `INSERT INTO tracks (id, album_id, artist_id, title, track_number, disc_number, duration_seconds, file_path, content_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"track-2", "album-2", "artist-2", "Migration", 1, 1, 301, "demo/migration.mp3", "audio/mpeg", now, now}, }, { query: `INSERT INTO tracks (id, album_id, artist_id, title, track_number, disc_number, duration_seconds, file_path, content_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: []any{"track-3", "album-3", "artist-3", "Reach for the Dead", 1, 1, 292, "demo/reach-for-the-dead.mp3", "audio/mpeg", now, now}, }, } tx, err := database.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin seed transaction: %w", err) } for _, statement := range statements { if _, err := tx.ExecContext(ctx, statement.query, statement.args...); err != nil { _ = tx.Rollback() return fmt.Errorf("seed statement failed: %w", err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("commit seed transaction: %w", err) } return nil }