diff --git a/cmd/minstrel/main.go b/cmd/minstrel/main.go index 4657f232..4e887257 100644 --- a/cmd/minstrel/main.go +++ b/cmd/minstrel/main.go @@ -242,7 +242,12 @@ func run() error { // active user's daily build at 03:00 in their stored timezone. // Replaces the 24h-anchored cron loop (removed in the next commit // of this arc). - playlistScheduler, err := playlists.NewScheduler(pool, logger.With("component", "playlist_scheduler"), cfg.Storage.DataDir) + playlistScheduler, err := playlists.NewScheduler( + pool, + logger.With("component", "playlist_scheduler"), + cfg.Storage.DataDir, + bus, + ) if err != nil { return fmt.Errorf("init playlist scheduler: %w", err) } diff --git a/internal/api/events_publish.go b/internal/api/events_publish.go index cdf3289a..7b77edb4 100644 --- a/internal/api/events_publish.go +++ b/internal/api/events_publish.go @@ -88,6 +88,21 @@ func (h *handlers) publishPlaylistEvent(kind string, ownerID, playlistID pgtype. }) } +// publishSystemRebuilt notifies the owner's clients that their system +// playlists (and You-might-like rows) were regenerated, so they invalidate +// the home / system-playlist providers and a stale active queue can re-pull. +// Mirrors the daily scheduler's event; fired here from the manual refresh. +func (h *handlers) publishSystemRebuilt(userID pgtype.UUID) { + if h.eventbus == nil { + return + } + h.eventbus.Publish(eventbus.Event{ + Kind: "playlist.system_rebuilt", + UserID: uuidToString(userID), + Data: map[string]any{}, + }) +} + // publishRequestStatusChanged broadcasts a Lidarr request status flip to // the request's original requester so their /requests page reflects the // new state without manual refresh. Admin actors (approve / reject) still diff --git a/internal/api/playlists_system_shuffle.go b/internal/api/playlists_system_shuffle.go index bca19717..84be5c1a 100644 --- a/internal/api/playlists_system_shuffle.go +++ b/internal/api/playlists_system_shuffle.go @@ -73,6 +73,8 @@ func (h *handlers) handleSystemPlaylistRefresh(w http.ResponseWriter, r *http.Re writeErr(w, apierror.InternalMsg("build failed", err)) return } + // #968: announce the rebuild so the user's other clients refresh. + h.publishSystemRebuilt(user.ID) q := dbq.New(h.pool) v := kind pl, err := q.GetSystemPlaylistByVariantForUser(r.Context(), diff --git a/internal/playlists/scheduler.go b/internal/playlists/scheduler.go index 6d325378..97ab098f 100644 --- a/internal/playlists/scheduler.go +++ b/internal/playlists/scheduler.go @@ -28,6 +28,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" + "git.fabledsword.com/bvandeusen/minstrel/internal/eventbus" "git.fabledsword.com/bvandeusen/minstrel/internal/taste" ) @@ -47,14 +48,21 @@ type Scheduler struct { pool *pgxpool.Pool logger *slog.Logger dataDir string + bus *eventbus.Bus // #968: announce rebuilds so clients self-refresh mu sync.Mutex jobs map[pgtype.UUID]uuid.UUID // user_id → gocron job id } // NewScheduler builds an idle scheduler. Caller must invoke Start -// before any builds fire. -func NewScheduler(pool *pgxpool.Pool, logger *slog.Logger, dataDir string) (*Scheduler, error) { +// before any builds fire. bus may be nil (tests); rebuild events are +// then skipped. +func NewScheduler( + pool *pgxpool.Pool, + logger *slog.Logger, + dataDir string, + bus *eventbus.Bus, +) (*Scheduler, error) { g, err := gocron.NewScheduler() if err != nil { return nil, fmt.Errorf("init gocron: %w", err) @@ -64,6 +72,7 @@ func NewScheduler(pool *pgxpool.Pool, logger *slog.Logger, dataDir string) (*Sch pool: pool, logger: logger, dataDir: dataDir, + bus: bus, jobs: map[pgtype.UUID]uuid.UUID{}, }, nil } @@ -234,7 +243,26 @@ func (s *Scheduler) rebuildUserDaily(ctx context.Context, userID pgtype.UUID, no if err := BuildSystemPlaylists(ctx, s.pool, s.logger, userID, now, s.dataDir); err != nil { s.logger.Warn("scheduler: build failed", "user_id", uuidStringPL(userID), "err", err) + return } + // #968: tell the user's connected clients their home content was + // regenerated, so a tab/app left open across the rebuild refreshes its + // system-playlist + You-might-like views (and a stale active queue can + // re-pull) instead of serving yesterday's snapshot until a manual reload. + s.publishRebuilt(userID) +} + +// publishRebuilt broadcasts a user-scoped "system playlists rebuilt" event. +// No-op when the bus is nil (test construction). +func (s *Scheduler) publishRebuilt(userID pgtype.UUID) { + if s.bus == nil { + return + } + s.bus.Publish(eventbus.Event{ + Kind: "playlist.system_rebuilt", + UserID: uuidStringPL(userID), + Data: map[string]any{}, + }) } // Stop drains gocron and stops the scheduler loop.