735383fb1d
System-generated playlists ("For You", "Songs like X") rendered with
empty placeholder boxes in the UI even when their contributing tracks
had album art. The build path inserted playlist rows directly without
ever invoking the existing playlists.GenerateCollage machinery —
that was only wired into user-edited playlist mutations.
After this change, BuildSystemPlaylists generates a 4-cell collage
for every system playlist it creates, post-commit. Same cover
mechanism user playlists use, same on-disk layout
(<DataDir>/playlist_covers/<id>.jpg). Cells whose source album lacks
art fall back to the FabledSword glyph, so a partially-covered
library still produces a sensible visual.
Threading: BuildSystemPlaylists, StartSystemPlaylistCron, and the
api lazy-build path all gain a dataDir string parameter; main.go
passes cfg.Storage.DataDir.
Drops the now-redundant PickTopAlbumCoverForArtistByUser lookup —
the collage is more visually informative than a single album cover
copy, and the seed-artist's top album is one of the contributing
tracks in the mix anyway, so it'll appear as one of the four cells.
Tests pass t.TempDir() for the dataDir parameter.
62 lines
1.8 KiB
Go
62 lines
1.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|