From 8daa3d07b5390e00cf880db23fdf12e89aea5ae1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 20 Oct 2026 09:39:17 -0400 Subject: [PATCH] fix: a slot emptied of another song's file drops that file's recording MBID, so missing-pair folding cannot fold it into the copy kept (M498) On 2026-10-20 twelve of the 24 slots the resolver emptied were folded by MBID into the kept copy, so their albums were never wanted back. Co-Authored-By: Claude Opus 5.5 --- internal/db/dbq/tracks.sql.go | 15 +++++++++++++++ internal/db/queries/tracks.sql | 9 +++++++++ internal/library/duplicate_resolve_test.go | 10 ++++++++++ internal/library/duplicate_settle.go | 7 ++++++- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/db/dbq/tracks.sql.go b/internal/db/dbq/tracks.sql.go index 2feaeb21..cec4f1c4 100644 --- a/internal/db/dbq/tracks.sql.go +++ b/internal/db/dbq/tracks.sql.go @@ -56,6 +56,21 @@ func (q *Queries) ClearStaleSuspectSourceTracks(ctx context.Context, pattern str return result.RowsAffected(), nil } +const clearTrackMbids = `-- name: ClearTrackMbids :exec +UPDATE tracks + SET mbid = NULL + WHERE id = ANY($1::uuid[]) +` + +// Forgets the recording MBID of rows whose file was another song's (M498's +// wrong-slot removal). The MBID came from that file, so it names the song kept +// elsewhere on the album, and missing-pair folding would fold the emptied slot +// into it: twelve slots went that way on 2026-10-20 and were never wanted back. +func (q *Queries) ClearTrackMbids(ctx context.Context, ids []pgtype.UUID) error { + _, err := q.db.Exec(ctx, clearTrackMbids, ids) + return err +} + const clearTracksMissing = `-- name: ClearTracksMissing :execrows UPDATE tracks SET missing_since = NULL diff --git a/internal/db/queries/tracks.sql b/internal/db/queries/tracks.sql index 602764df..ca5f492a 100644 --- a/internal/db/queries/tracks.sql +++ b/internal/db/queries/tracks.sql @@ -216,6 +216,15 @@ UPDATE tracks WHERE id = ANY(sqlc.arg(ids)::uuid[]) AND missing_since IS NULL; +-- name: ClearTrackMbids :exec +-- Forgets the recording MBID of rows whose file was another song's (M498's +-- wrong-slot removal). The MBID came from that file, so it names the song kept +-- elsewhere on the album, and missing-pair folding would fold the emptied slot +-- into it: twelve slots went that way on 2026-10-20 and were never wanted back. +UPDATE tracks + SET mbid = NULL + WHERE id = ANY(sqlc.arg(ids)::uuid[]); + -- name: ClearTracksMissing :execrows -- Clears the mark on rows whose file is back. Runs independently of the mtime -- skip check, so a file that reappears unchanged is un-marked even though the diff --git a/internal/library/duplicate_resolve_test.go b/internal/library/duplicate_resolve_test.go index ea707ca4..926b2348 100644 --- a/internal/library/duplicate_resolve_test.go +++ b/internal/library/duplicate_resolve_test.go @@ -381,10 +381,20 @@ func seedTrackList(t *testing.T, pool *pgxpool.Pool, album dbq.Album, songs map[ func TestResolveDuplicates_RemovesACopyFromAnotherSongsSlot_Integration(t *testing.T) { f := newOneAlbumFixture(t, "acoustic", "Void", "Reach") seedTrackList(t, f.pool, f.album, map[string]int{"Void": 220500, "Reach": 281000}) + // The wrong file is Void's recording, so it carries Void's MBID. + if _, err := f.pool.Exec(context.Background(), + `UPDATE tracks SET mbid = 'rec-void' WHERE id = ANY($1)`, []pgtype.UUID{f.a.ID, f.b.ID}); err != nil { + t.Fatal(err) + } res, err := ResolveDuplicates(context.Background(), f.pool, nil, "", true) if err != nil { t.Fatal(err) } + // Twelve slots were lost this way on 2026-10-20: folded by that MBID into + // the copy kept, and so never wanted back. + if pairs, err := PairMissingTracks(context.Background(), f.pool, nil); err != nil || pairs.Folded() != 0 { + t.Fatalf("missing-pair pass = %+v, %v; want the emptied slot left alone", pairs, err) + } if res.WrongFiles != 1 || !exists(f.aPath) || exists(f.bPath) { t.Fatalf("result %+v, void exists %v, reach exists %v; want only Reach's copy removed", res, exists(f.aPath), exists(f.bPath)) diff --git a/internal/library/duplicate_settle.go b/internal/library/duplicate_settle.go index 0d4cb594..5358fbac 100644 --- a/internal/library/duplicate_settle.go +++ b/internal/library/duplicate_settle.go @@ -225,7 +225,9 @@ func neverRead(ctx context.Context, q *dbq.Queries, rg string) bool { // removeWrongSlots deletes each loser's file, then marks its track missing in // the same transaction that closes the group. The row is kept, with its likes -// and plays: it is the slot, and a good copy fetched again takes it over. A +// and plays: it is the slot, and a good copy fetched again takes it over. Its +// recording MBID is dropped, being the wrong file's, so missing-pair folding +// does not fold the slot into the copy that was kept. A // file that cannot be removed stops the group; the ones already gone are still // marked, and the group waits for the next pass. func removeWrongSlots( @@ -253,6 +255,9 @@ func removeWrongSlots( if _, err := tq.MarkTracksMissing(ctx, gone); err != nil { return fmt.Errorf("mark missing: %w", err) } + if err := tq.ClearTrackMbids(ctx, gone); err != nil { + return fmt.Errorf("clear mbids: %w", err) + } if err := logTrackChanges(ctx, tq, gone); err != nil { return fmt.Errorf("log changes: %w", err) }