diff --git a/internal/api/admin_requests.go b/internal/api/admin_requests.go index 04c6a118..69a61bdb 100644 --- a/internal/api/admin_requests.go +++ b/internal/api/admin_requests.go @@ -125,6 +125,7 @@ func (h *handlers) handleApproveRequest(w http.ResponseWriter, r *http.Request) return } + h.publishRequestStatusChanged(row) writeJSON(w, http.StatusOK, requestViewFrom(row)) } @@ -164,5 +165,6 @@ func (h *handlers) handleRejectRequest(w http.ResponseWriter, r *http.Request) { return } + h.publishRequestStatusChanged(row) writeJSON(w, http.StatusOK, requestViewFrom(row)) } diff --git a/internal/api/events_publish.go b/internal/api/events_publish.go new file mode 100644 index 00000000..80b99cb0 --- /dev/null +++ b/internal/api/events_publish.go @@ -0,0 +1,68 @@ +// 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), + }, + }) +} + +// 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), + }, + }) +} diff --git a/internal/api/likes.go b/internal/api/likes.go index 83621e7f..87ad477a 100644 --- a/internal/api/likes.go +++ b/internal/api/likes.go @@ -60,6 +60,7 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) { _ = playevents.CaptureContextualLikeIfPlaying(r.Context(), q, user.ID, id, h.logger) } h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpUpsert) + h.publishLikeEvent(user.ID, id, "track", true) w.WriteHeader(http.StatusNoContent) } @@ -83,6 +84,7 @@ func (h *handlers) handleUnlikeTrack(w http.ResponseWriter, r *http.Request) { // Don't fail the response — soft-delete is best-effort. } h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpDelete) + h.publishLikeEvent(user.ID, id, "track", false) w.WriteHeader(http.StatusNoContent) } diff --git a/internal/api/requests.go b/internal/api/requests.go index 787ae252..fcd84b95 100644 --- a/internal/api/requests.go +++ b/internal/api/requests.go @@ -175,6 +175,7 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) { } } + h.publishRequestStatusChanged(row) writeJSON(w, http.StatusCreated, requestViewFrom(row)) } @@ -254,5 +255,6 @@ func (h *handlers) handleCancelRequest(w http.ResponseWriter, r *http.Request) { return } + h.publishRequestStatusChanged(row) writeJSON(w, http.StatusOK, requestViewFrom(row)) }