feat(server): like/unlike handlers write library_changes

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 22:40:56 -04:00
parent 0fa7dc7982
commit e0c5789cee
+19
View File
@@ -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)
}