diff --git a/internal/playlists/service.go b/internal/playlists/service.go index 294311f8..f919abb8 100644 --- a/internal/playlists/service.go +++ b/internal/playlists/service.go @@ -26,6 +26,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" + syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync" ) // Typed errors. The API layer maps each to its wire status code; tests @@ -131,6 +132,10 @@ func (s *Service) Create(ctx context.Context, userID pgtype.UUID, name, descript if err != nil { return nil, fmt.Errorf("create playlist: %w", err) } + if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityPlaylist, + syncpkg.FormatUUID(row.ID), syncpkg.OpUpsert); err != nil { + s.logger.Warn("playlists: LogChange create failed", "playlist_id", row.ID, "err", err) + } full, err := q.GetPlaylist(ctx, row.ID) if err != nil { return nil, fmt.Errorf("get-after-create: %w", err) @@ -265,6 +270,10 @@ func (s *Service) Update(ctx context.Context, callerID, playlistID pgtype.UUID, if err != nil { return nil, fmt.Errorf("update playlist: %w", err) } + if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityPlaylist, + syncpkg.FormatUUID(updated.ID), syncpkg.OpUpsert); err != nil { + s.logger.Warn("playlists: LogChange update failed", "playlist_id", updated.ID, "err", err) + } full, err := q.GetPlaylist(ctx, updated.ID) if err != nil { return nil, fmt.Errorf("get-after-update: %w", err) @@ -297,6 +306,10 @@ func (s *Service) Delete(ctx context.Context, callerID, playlistID pgtype.UUID) if err != nil { return fmt.Errorf("delete playlist: %w", err) } + if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityPlaylist, + syncpkg.FormatUUID(deleted.ID), syncpkg.OpDelete); err != nil { + s.logger.Warn("playlists: LogChange delete failed", "playlist_id", deleted.ID, "err", err) + } if deleted.CoverPath != nil && *deleted.CoverPath != "" { full := filepath.Join(s.dataDir, *deleted.CoverPath) if rerr := os.Remove(full); rerr != nil && !errors.Is(rerr, os.ErrNotExist) { @@ -377,6 +390,12 @@ func (s *Service) AppendTracks(ctx context.Context, callerID, playlistID pgtype. } return fmt.Errorf("append track: %w", ierr) } + // Tx-bound: change row commits with the playlist_track row. + if lerr := syncpkg.LogChange(ctx, tx, syncpkg.EntityPlaylistTrack, + syncpkg.EncodePlaylistTrackID(syncpkg.FormatUUID(playlistID), syncpkg.FormatUUID(tid)), + syncpkg.OpUpsert); lerr != nil { + return fmt.Errorf("log change: %w", lerr) + } } if err := tq.UpdatePlaylistRollups(ctx, playlistID); err != nil { return fmt.Errorf("update rollups: %w", err) @@ -418,12 +437,30 @@ func (s *Service) RemoveTrack(ctx context.Context, callerID, playlistID pgtype.U defer func() { _ = tx.Rollback(ctx) }() tq := dbq.New(tx) + // Capture the track_id before we delete the row — we need it for the + // LogChange composite key after the delete commits. + var deletedTrackID pgtype.UUID + if err := tx.QueryRow(ctx, + `SELECT track_id FROM playlist_tracks WHERE playlist_id = $1 AND position = $2`, + playlistID, position, + ).Scan(&deletedTrackID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNotFound + } + return fmt.Errorf("lookup track at position: %w", err) + } + if err := tq.DeletePlaylistTrack(ctx, dbq.DeletePlaylistTrackParams{ PlaylistID: playlistID, Position: position, }); err != nil { return fmt.Errorf("delete track row: %w", err) } + if lerr := syncpkg.LogChange(ctx, tx, syncpkg.EntityPlaylistTrack, + syncpkg.EncodePlaylistTrackID(syncpkg.FormatUUID(playlistID), syncpkg.FormatUUID(deletedTrackID)), + syncpkg.OpDelete); lerr != nil { + return fmt.Errorf("log change: %w", lerr) + } if err := tq.RenumberPlaylistTracksAfter(ctx, dbq.RenumberPlaylistTracksAfterParams{ PlaylistID: playlistID, Position: position,