feat(server): scanner + DeleteTrackFile write library_changes
Wires sync.LogChange into the library mutation sites so /api/library/sync reflects upserts and deletes. Architectural pivot: LogChange's signature is now (ctx, dbq.DBTX, ...) so it works with both *pgxpool.Pool and pgx.Tx. The scanner doesn't run mutations in explicit transactions, so it pool-binds; delete.go matches. Tx-bound callers (likes/playlists in subsequent commits) keep atomicity. Also: sync.FormatUUID centralizes the pgtype.UUID → canonical string conversion that both the scanner and the sync handler need; library_sync.go now uses it instead of a local copy. Best-effort logging on scanner failures (Warn, don't fail the scan): a LogChange error after a successful upsert is rare and self-healing — the next scan that touches the entity re-emits the change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
|
||||
)
|
||||
|
||||
// ErrTrackNotFound is returned when DeleteTrackFile is called with an id
|
||||
@@ -48,5 +49,12 @@ func DeleteTrackFile(ctx context.Context, pool *pgxpool.Pool, trackID pgtype.UUI
|
||||
if _, err := pool.Exec(ctx, "DELETE FROM tracks WHERE id = $1", trackID); err != nil {
|
||||
return fmt.Errorf("delete row: %w", err)
|
||||
}
|
||||
// Log the change after the delete succeeds. Best-effort: a Warn-level
|
||||
// failure here would leave the cache index orphaned on offline clients
|
||||
// until the next scan touches the surrounding album.
|
||||
if err := syncpkg.LogChange(ctx, pool, syncpkg.EntityTrack,
|
||||
syncpkg.FormatUUID(trackID), syncpkg.OpDelete); err != nil {
|
||||
return fmt.Errorf("log change: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
|
||||
)
|
||||
|
||||
// audioExtensions is the v1 set. Duration extraction is not wired, so all of
|
||||
@@ -195,9 +196,16 @@ func (s *Scanner) scanFile(ctx context.Context, q *dbq.Queries, path string, sta
|
||||
params.Genre = &g
|
||||
}
|
||||
|
||||
if _, err := q.UpsertTrack(ctx, params); err != nil {
|
||||
track, err := q.UpsertTrack(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert track: %w", err)
|
||||
}
|
||||
if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityTrack,
|
||||
syncpkg.FormatUUID(track.ID), syncpkg.OpUpsert); err != nil {
|
||||
// Best-effort: log but don't fail the scan. The next scan that
|
||||
// touches this track will re-emit the change.
|
||||
s.logger.Warn("library scan: LogChange track upsert failed", "track_id", track.ID, "err", err)
|
||||
}
|
||||
|
||||
if knownTrack {
|
||||
stats.Updated++
|
||||
@@ -236,7 +244,15 @@ func (s *Scanner) resolveArtist(ctx context.Context, q *dbq.Queries, name, mbid
|
||||
m := mbid
|
||||
params.Mbid = &m
|
||||
}
|
||||
return q.UpsertArtist(ctx, params)
|
||||
artist, err := q.UpsertArtist(ctx, params)
|
||||
if err != nil {
|
||||
return dbq.Artist{}, err
|
||||
}
|
||||
if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityArtist,
|
||||
syncpkg.FormatUUID(artist.ID), syncpkg.OpUpsert); err != nil {
|
||||
s.logger.Warn("library scan: LogChange artist upsert failed", "artist_id", artist.ID, "err", err)
|
||||
}
|
||||
return artist, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) resolveAlbum(ctx context.Context, q *dbq.Queries, artistID pgtype.UUID, title string, year int, mbid string) (dbq.Album, error) {
|
||||
@@ -285,7 +301,15 @@ func (s *Scanner) resolveAlbum(ctx context.Context, q *dbq.Queries, artistID pgt
|
||||
m := mbid
|
||||
params.Mbid = &m
|
||||
}
|
||||
return q.UpsertAlbum(ctx, params)
|
||||
album, err := q.UpsertAlbum(ctx, params)
|
||||
if err != nil {
|
||||
return dbq.Album{}, err
|
||||
}
|
||||
if err := syncpkg.LogChange(ctx, s.pool, syncpkg.EntityAlbum,
|
||||
syncpkg.FormatUUID(album.ID), syncpkg.OpUpsert); err != nil {
|
||||
s.logger.Warn("library scan: LogChange album upsert failed", "album_id", album.ID, "err", err)
|
||||
}
|
||||
return album, nil
|
||||
}
|
||||
|
||||
// releaseDateFromYear converts a tag-supplied year into a Postgres date,
|
||||
|
||||
Reference in New Issue
Block a user