Add a filesystem scanner that ingests supported audio files from MEDIA_ROOT into SQLite using embedded tags with filename fallbacks. Wire startup scanning, manual rescan, and authenticated audio streaming into the backend, then connect the web player to the real stream endpoint.
155 lines
4.8 KiB
Go
155 lines
4.8 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"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)
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
_, err = database.ExecContext(
|
|
ctx,
|
|
`INSERT INTO users (id, username, password_hash, is_admin, created_at, last_login_at)
|
|
VALUES (?, ?, ?, 1, ?, ?)`,
|
|
"user-admin",
|
|
cfg.DefaultAdminUsername,
|
|
string(passwordHash),
|
|
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
|
|
}
|