package playlists import ( "context" "log/slog" "time" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // StartSystemPlaylistCron launches the in-process daily build loop in a // goroutine. Caller passes a context that's cancelled on graceful shutdown. // // Lifecycle: // 1. Clear any stale in_flight=true rows from a previously crashed process // (safe at startup: by definition, nothing is running yet). // 2. Run once at startup so users catch up after a server restart without // waiting up to 24h. // 3. Tick every 24h; runOnce on each tick. // 4. Exit on context cancellation. // // Per-user errors are logged but don't halt the loop. Per-cycle errors (e.g., // "list active users" failed) are logged but the next tick still fires. func StartSystemPlaylistCron(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string) { go func() { q := dbq.New(pool) if err := q.ClearStaleSystemPlaylistInFlight(ctx); err != nil { logger.Warn("system playlist cron: startup recovery failed", "err", err) } runOnce(ctx, pool, logger, dataDir) ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: runOnce(ctx, pool, logger, dataDir) } } }() } func runOnce(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string) { q := dbq.New(pool) users, err := q.ListActiveUsersForSystemPlaylists(ctx) if err != nil { logger.Error("system playlist cron: list active users failed", "err", err) return } for _, userID := range users { if err := BuildSystemPlaylists(ctx, pool, logger, userID, time.Now(), dataDir); err != nil { logger.Warn("system playlist build failed in cron", "user_id", uuidStringPL(userID), "err", err) } } }