From e0c5789ceed2bbc07dbcaa25115921e5fa4250f3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 9 May 2026 22:40:56 -0400 Subject: [PATCH] feat(server): like/unlike handlers write library_changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 6 like/unlike sites (track/album/artist × like/unlike) now emit a library_changes row via the shared logLikeChange helper. Best-effort: LogChange failures Warn but don't fail the HTTP response — a missed log row is recovered at the next mutation on the same entity. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/api/likes.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/api/likes.go b/internal/api/likes.go index 7e877fbc..83621e7f 100644 --- a/internal/api/likes.go +++ b/internal/api/likes.go @@ -10,8 +10,21 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/playevents" + syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync" ) +// logLikeChange writes a library_changes row for a like / unlike. Best- +// effort: a failure here means the next sync may miss the row, but the +// HTTP response still succeeds. Cheap consequence — operator's other +// device will see the like once it manually pulls or once a future +// like/unlike on the same entity emits a fresh log row. +func (h *handlers) logLikeChange(r *http.Request, et syncpkg.EntityType, userID, entityID pgtype.UUID, op syncpkg.Op) { + if err := syncpkg.LogChange(r.Context(), h.pool, et, + syncpkg.EncodeLikeID(syncpkg.FormatUUID(userID), syncpkg.FormatUUID(entityID)), op); err != nil { + h.logger.Warn("api: LogChange like", "kind", et, "user", userID, "entity", entityID, "err", err) + } +} + type likedIDsResponse struct { TrackIDs []string `json:"track_ids"` AlbumIDs []string `json:"album_ids"` @@ -46,6 +59,7 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) { if rows == 1 { _ = playevents.CaptureContextualLikeIfPlaying(r.Context(), q, user.ID, id, h.logger) } + h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpUpsert) w.WriteHeader(http.StatusNoContent) } @@ -68,6 +82,7 @@ func (h *handlers) handleUnlikeTrack(w http.ResponseWriter, r *http.Request) { h.logger.Error("api: soft-delete contextual_likes", "err", err) // Don't fail the response — soft-delete is best-effort. } + h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpDelete) w.WriteHeader(http.StatusNoContent) } @@ -95,6 +110,7 @@ func (h *handlers) handleLikeAlbum(w http.ResponseWriter, r *http.Request) { writeErr(w, apierror.InternalMsg("insert failed", err)) return } + h.logLikeChange(r, syncpkg.EntityLikeAlbum, user.ID, id, syncpkg.OpUpsert) w.WriteHeader(http.StatusNoContent) } @@ -112,6 +128,7 @@ func (h *handlers) handleUnlikeAlbum(w http.ResponseWriter, r *http.Request) { writeErr(w, apierror.InternalMsg("delete failed", err)) return } + h.logLikeChange(r, syncpkg.EntityLikeAlbum, user.ID, id, syncpkg.OpDelete) w.WriteHeader(http.StatusNoContent) } @@ -139,6 +156,7 @@ func (h *handlers) handleLikeArtist(w http.ResponseWriter, r *http.Request) { writeErr(w, apierror.InternalMsg("insert failed", err)) return } + h.logLikeChange(r, syncpkg.EntityLikeArtist, user.ID, id, syncpkg.OpUpsert) w.WriteHeader(http.StatusNoContent) } @@ -156,6 +174,7 @@ func (h *handlers) handleUnlikeArtist(w http.ResponseWriter, r *http.Request) { writeErr(w, apierror.InternalMsg("delete failed", err)) return } + h.logLikeChange(r, syncpkg.EntityLikeArtist, user.ID, id, syncpkg.OpDelete) w.WriteHeader(http.StatusNoContent) }