16f76ea707
The daily 03:00 scheduler rebuild (and the manual refresh endpoint) replace a user's system playlists + You-might-like rows but published no event, so a client left open across the rebuild served yesterday's snapshot until a manual reload — the stale-tab case behind #968. Add a user-scoped playlist.system_rebuilt event (envelope {kind,user_id,data:{}}) from both the scheduler (bus threaded into NewScheduler) and handleSystemPlaylistRefresh. Clients consume it to invalidate home / system-playlist views and proactively re-pull a stale active queue. Issue #968. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
// Small helpers for posting live events to the SSE bus (#392). Producers
|
|
// call these from their handler success paths so the wire shape stays in
|
|
// one place. Each helper is a no-op when h.eventbus is nil (older callers
|
|
// or tests that don't construct a bus); production always has one.
|
|
|
|
package api
|
|
|
|
import (
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/eventbus"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
// publishLikeEvent broadcasts a track/album/artist like or unlike. The
|
|
// event is scoped to the user (so only their own clients invalidate); the
|
|
// payload includes the entity_type and entity_id so the dispatcher can
|
|
// invalidate the right query keys.
|
|
func (h *handlers) publishLikeEvent(userID, entityID pgtype.UUID, entityType string, liked bool) {
|
|
if h.eventbus == nil {
|
|
return
|
|
}
|
|
kind := "track.liked"
|
|
if !liked {
|
|
kind = "track.unliked"
|
|
}
|
|
if entityType == "album" {
|
|
if liked {
|
|
kind = "album.liked"
|
|
} else {
|
|
kind = "album.unliked"
|
|
}
|
|
}
|
|
if entityType == "artist" {
|
|
if liked {
|
|
kind = "artist.liked"
|
|
} else {
|
|
kind = "artist.unliked"
|
|
}
|
|
}
|
|
h.eventbus.Publish(eventbus.Event{
|
|
Kind: kind,
|
|
UserID: uuidToString(userID),
|
|
Data: map[string]any{
|
|
"entity_type": entityType,
|
|
"entity_id": uuidToString(entityID),
|
|
},
|
|
})
|
|
}
|
|
|
|
// publishQuarantineEvent broadcasts a quarantine action. User-side
|
|
// flag/unflag are scoped to the user; admin-side resolve / delete-file /
|
|
// delete-via-lidarr are broadcast (empty UserID) because a single admin
|
|
// action can affect every user who'd flagged that track — every client
|
|
// invalidates and the non-affected ones no-op when the query result
|
|
// matches their cache.
|
|
func (h *handlers) publishQuarantineEvent(kind string, userID, trackID pgtype.UUID, broadcast bool) {
|
|
if h.eventbus == nil {
|
|
return
|
|
}
|
|
scope := ""
|
|
if !broadcast {
|
|
scope = uuidToString(userID)
|
|
}
|
|
h.eventbus.Publish(eventbus.Event{
|
|
Kind: kind,
|
|
UserID: scope,
|
|
Data: map[string]any{
|
|
"track_id": uuidToString(trackID),
|
|
},
|
|
})
|
|
}
|
|
|
|
// publishPlaylistEvent broadcasts a playlist mutation to the owner so
|
|
// their other clients invalidate the affected playlist (and playlist list)
|
|
// providers. Public-playlist subscribers are out of scope here — they'd
|
|
// need a separate "playlists.public_updated" broadcast, deferred until
|
|
// the multi-user case is exercised.
|
|
func (h *handlers) publishPlaylistEvent(kind string, ownerID, playlistID pgtype.UUID) {
|
|
if h.eventbus == nil {
|
|
return
|
|
}
|
|
h.eventbus.Publish(eventbus.Event{
|
|
Kind: kind,
|
|
UserID: uuidToString(ownerID),
|
|
Data: map[string]any{
|
|
"playlist_id": uuidToString(playlistID),
|
|
},
|
|
})
|
|
}
|
|
|
|
// 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
|
|
// route to the requester's user_id, not the admin's — the original user
|
|
// is who needs to see the update.
|
|
func (h *handlers) publishRequestStatusChanged(row dbq.LidarrRequest) {
|
|
if h.eventbus == nil {
|
|
return
|
|
}
|
|
h.eventbus.Publish(eventbus.Event{
|
|
Kind: "request.status_changed",
|
|
UserID: uuidToString(row.UserID),
|
|
Data: map[string]any{
|
|
"request_id": uuidToString(row.ID),
|
|
"status": string(row.Status),
|
|
"kind": string(row.Kind),
|
|
},
|
|
})
|
|
}
|