Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.
Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
today rather than a milestone that would change it.
Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
package sync
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
// LogChange writes a row to library_changes via the supplied DBTX. dbq.DBTX
|
|
// is satisfied by both *pgxpool.Pool and pgx.Tx, so callers can opt into
|
|
// transactional safety where they already hold a tx, or pool-bind for
|
|
// best-effort logging when no tx is available.
|
|
//
|
|
// Callers SHOULD pass a tx that owns the underlying mutation — the change
|
|
// row and the mutation then commit or roll back together. When pool-bound,
|
|
// the scanner-style "log after success" pattern keeps the log consistent
|
|
// with reality at the cost of a tiny race window: if the LogChange call
|
|
// itself fails after the mutation succeeded, that mutation won't appear in
|
|
// the change log until the entity is touched again.
|
|
//
|
|
// entityID is the string form of the row's primary key. For composite-key
|
|
// entities (likes, playlist_tracks), use EncodeLikeID / EncodePlaylistTrackID
|
|
// to produce stable strings. For pgtype.UUID values, use FormatUUID.
|
|
func LogChange(ctx context.Context, dbtx dbq.DBTX, entityType EntityType, entityID string, op Op) error {
|
|
q := dbq.New(dbtx)
|
|
if err := q.InsertLibraryChange(ctx, dbq.InsertLibraryChangeParams{
|
|
EntityType: string(entityType),
|
|
EntityID: entityID,
|
|
Op: string(op),
|
|
}); err != nil {
|
|
return fmt.Errorf("sync.LogChange(%s, %s, %s): %w", entityType, entityID, op, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LogChanges is LogChange for a set of entities of one type sharing one op.
|
|
//
|
|
// Exists because the scan's reconcile pass can mark a large part of a library
|
|
// missing in a single sweep, and a per-row loop there would be thousands of
|
|
// round-trips inside an operation that is already slow. Every
|
|
// other caller mutates one entity and should keep using LogChange.
|
|
//
|
|
// A no-op on an empty set, so callers don't have to guard.
|
|
func LogChanges(ctx context.Context, dbtx dbq.DBTX, entityType EntityType, entityIDs []string, op Op) error {
|
|
if len(entityIDs) == 0 {
|
|
return nil
|
|
}
|
|
q := dbq.New(dbtx)
|
|
if err := q.InsertLibraryChanges(ctx, dbq.InsertLibraryChangesParams{
|
|
EntityType: string(entityType),
|
|
EntityIds: entityIDs,
|
|
Op: string(op),
|
|
}); err != nil {
|
|
return fmt.Errorf("sync.LogChanges(%s, %d ids, %s): %w", entityType, len(entityIDs), op, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FormatUUID renders a pgtype.UUID as the canonical 8-4-4-4-12 hex form.
|
|
// Returns "" if the UUID is not valid.
|
|
func FormatUUID(u pgtype.UUID) string {
|
|
if !u.Valid {
|
|
return ""
|
|
}
|
|
return formatUUIDBytes(u.Bytes)
|
|
}
|
|
|
|
func formatUUIDBytes(b [16]byte) string {
|
|
const hex = "0123456789abcdef"
|
|
out := make([]byte, 36)
|
|
pos := 0
|
|
for i := 0; i < 16; i++ {
|
|
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
out[pos] = '-'
|
|
pos++
|
|
}
|
|
out[pos] = hex[b[i]>>4]
|
|
out[pos+1] = hex[b[i]&0x0f]
|
|
pos += 2
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// EncodeLikeID joins a user UUID and an entity UUID into the stable
|
|
// composite identifier used for like_* rows in library_changes. Both
|
|
// sides are UUID strings so no escaping is needed.
|
|
func EncodeLikeID(userID, entityID string) string {
|
|
return userID + ":" + entityID
|
|
}
|
|
|
|
// EncodePlaylistTrackID joins a playlist UUID and a track UUID into the
|
|
// stable composite identifier used for playlist_track rows.
|
|
func EncodePlaylistTrackID(playlistID, trackID string) string {
|
|
return playlistID + ":" + trackID
|
|
}
|