feat: one recording cut to several songs' lengths is named by AcoustID; the copy in its slot stays, the rest go, and the release is set aside (#6169)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 5m1s
release / integration (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 5m1s
release / integration (push) Successful in 5m1s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
Savant's Mortals held one recording as 16 of its 50 tracks, each trimmed to its own slot's MusicBrainz length, so the lengths could not say which slot was its own and every group waited for the operator. When a one-album mismatch is the same audio (identical, or fingerprints within 0.02) and every copy fits its own slot, the resolver asks what the recording is: the copies' own AcoustID lookups, or a fresh lookup of one copy (five a pass). The track list names each slot's recording; the copy in that slot is kept and the others removed, or every copy when the recording is in none of their slots or AcoustID knows none. AcoustID unreachable or answers in several copies' slots: the group waits. Whenever wrong-slot copies are removed, the download the files were imported from is set aside, so the album fetched again is not the same files. Albums imported before Minstrel downloaded anything have none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -436,6 +436,108 @@ func TestResolveDuplicates_RemovesEveryCopyWhenNoSlotIsTheirs_Integration(t *tes
|
||||
}
|
||||
}
|
||||
|
||||
// seedTrimmedAlbum is newOneAlbumFixture for one recording cut to both its
|
||||
// slots' lengths: identical audio, as Tormentor and as Record Shop, each as
|
||||
// long as its own slot's song, and the track list naming each slot's
|
||||
// recording. The files came from a download Minstrel imported.
|
||||
func seedTrimmedAlbum(t *testing.T) oneAlbumFixture {
|
||||
t.Helper()
|
||||
f := newOneAlbumFixture(t, "exact", "Tormentor", "Record Shop")
|
||||
ctx := context.Background()
|
||||
seedTrackList(t, f.pool, f.album, map[string]int{"Tormentor": 221000, "Record Shop": 219000})
|
||||
for _, stmt := range []struct {
|
||||
sql string
|
||||
args []any
|
||||
}{
|
||||
{`UPDATE mb_release_tracks SET recording_mbid = 'rec-' || lower(replace(title, ' ', '-'))`, nil},
|
||||
{`INSERT INTO grabs (info_hash, name, key, client, save_path, origin, import_state, destination)
|
||||
VALUES ('abc123', 'The Wombats - Void [WEB]', 'k', 'qbit', '/dl', 'schedule', 'imported', $1)`,
|
||||
[]any{filepath.Dir(f.aPath)}},
|
||||
} {
|
||||
if _, err := f.pool.Exec(ctx, stmt.sql, stmt.args...); err != nil {
|
||||
t.Fatalf("exec %q: %v", stmt.sql, err)
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func releaseBlocked(t *testing.T, pool *pgxpool.Pool, hash string) bool {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := pool.QueryRow(context.Background(),
|
||||
`SELECT count(*) FROM release_blocks WHERE info_hash = $1 AND reason = 'files'`, hash).Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return n == 1
|
||||
}
|
||||
|
||||
// AcoustID matched one copy to Tormentor's recording: that copy stays, Record
|
||||
// Shop's goes, and the download they came from is set aside (#6169).
|
||||
func TestResolveDuplicates_KeepsTheCopyAcoustIDNames_Integration(t *testing.T) {
|
||||
f := seedTrimmedAlbum(t)
|
||||
if _, err := f.pool.Exec(context.Background(),
|
||||
`INSERT INTO track_acoustid_lookups (track_id, state, candidates, recording_mbid) VALUES ($1, 'matched', 1, 'rec-tormentor')`,
|
||||
f.a.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asked := 0
|
||||
res, err := ResolveDuplicatesWith(context.Background(), f.pool, nil, "", true, ResolveSources{
|
||||
Identify: func(context.Context, string) ([]string, error) { asked++; return nil, nil },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if asked != 0 {
|
||||
t.Errorf("AcoustID asked %d times; the copy's own lookup already answered", asked)
|
||||
}
|
||||
if res.WrongFiles != 1 || !exists(f.aPath) || exists(f.bPath) {
|
||||
t.Fatalf("result %+v, tormentor exists %v, record shop exists %v; want only Record Shop's copy removed",
|
||||
res, exists(f.aPath), exists(f.bPath))
|
||||
}
|
||||
if !releaseBlocked(t, f.pool, "abc123") {
|
||||
t.Error("the download the files came from was not set aside, so the album would be fetched from it again")
|
||||
}
|
||||
}
|
||||
|
||||
// AcoustID knows the recording as no song of the album: every copy goes. With
|
||||
// AcoustID unreachable, nothing is decided and the group waits.
|
||||
func TestResolveDuplicates_TrimmedCopiesAcoustIDCannotName_Integration(t *testing.T) {
|
||||
t.Run("no match", func(t *testing.T) {
|
||||
f := seedTrimmedAlbum(t)
|
||||
asked := 0
|
||||
res, err := ResolveDuplicatesWith(context.Background(), f.pool, nil, "", true, ResolveSources{
|
||||
Identify: func(context.Context, string) ([]string, error) {
|
||||
asked++
|
||||
return []string{"rec-elsewhere"}, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if asked != 1 || res.WrongFiles != 2 || exists(f.aPath) || exists(f.bPath) {
|
||||
t.Fatalf("asked %d, result %+v; want one lookup and both copies removed", asked, res)
|
||||
}
|
||||
if status, _, _, _ := (resolveFixture{pool: f.pool, groupID: f.groupID}).group(t); status != "merged" {
|
||||
t.Errorf("group = %s, want settled", status)
|
||||
}
|
||||
})
|
||||
t.Run("unreachable", func(t *testing.T) {
|
||||
f := seedTrimmedAlbum(t)
|
||||
res, err := ResolveDuplicatesWith(context.Background(), f.pool, nil, "", true, ResolveSources{
|
||||
Identify: func(context.Context, string) ([]string, error) { return nil, ErrIdentifyUnavailable },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.WrongFiles != 0 || !exists(f.aPath) || !exists(f.bPath) || releaseBlocked(t, f.pool, "abc123") {
|
||||
t.Fatalf("result %+v; want nothing removed or set aside while AcoustID cannot answer", res)
|
||||
}
|
||||
if status, _, _, _ := (resolveFixture{pool: f.pool, groupID: f.groupID}).group(t); status != "pending" {
|
||||
t.Errorf("group = %s, want pending", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// An album whose track list the catalogue never read: the resolver asks for
|
||||
// it, once, and decides with what comes back.
|
||||
func TestResolveDuplicates_FetchesAMissingTrackList_Integration(t *testing.T) {
|
||||
@@ -454,7 +556,7 @@ func TestResolveDuplicates_FetchesAMissingTrackList_Integration(t *testing.T) {
|
||||
seedTrackList(t, f.pool, f.album, map[string]int{"Void": 220500, "Reach": 281000})
|
||||
return nil
|
||||
}
|
||||
res, err := ResolveDuplicatesFetching(context.Background(), f.pool, nil, "", true, fetch)
|
||||
res, err := ResolveDuplicatesWith(context.Background(), f.pool, nil, "", true, ResolveSources{TrackLists: fetch})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user