From b34d61c436819ef79897b34fdb4e3e8058ddb095 Mon Sep 17 00:00:00 2001 From: nironics Date: Mon, 9 Nov 2026 17:51:22 -0500 Subject: [PATCH] Handle same track title but different artist scoring better --- src/client/client.go | 25 +++++++++++++++--------- src/client/client_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/client/client_test.go diff --git a/src/client/client.go b/src/client/client.go index c666e90a..bddcdbba 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -260,10 +260,12 @@ func BestMatch(track *models.Track, results []SearchResult, minScore int) (Searc CleanTitle: util.NormalizeTitle(track.CleanTitle), MainArtist: track.MainArtist, Album: track.Album, - File: filepath.Base(track.File), Duration: track.Duration, MBTrackID: track.MusicBrainzTrackID, MBReleaseTrackID: track.MusicBrainzReleaseTrackID} + if track.File != "" { // filepath.Base("") is ".", which would equal a result with no path + nmTrack.File = filepath.Base(track.File) + } for _, r := range results { score := rankResult(nmTrack, r) @@ -296,15 +298,20 @@ func rankResult(track NormalisedTrack, r SearchResult) int { } else if len(track.CleanTitle) > 3 && (strings.Contains(resultTitle, track.CleanTitle) || strings.Contains(track.CleanTitle, resultTitle)) { score += 25 } - if strings.EqualFold(track.Album, r.Album) { - score += 20 - } else if util.ContainsFold(track.Album, r.Album) || util.ContainsFold(r.Album, track.Album) { - score += 15 + // An empty string equals/is contained in anything, so only score fields both sides have + if track.Album != "" && r.Album != "" { + if strings.EqualFold(track.Album, r.Album) { + score += 20 + } else if util.ContainsFold(track.Album, r.Album) || util.ContainsFold(r.Album, track.Album) { + score += 15 + } } - if strings.EqualFold(track.MainArtist, r.Artist) || (len(r.Artists) > 0 && strings.EqualFold(r.Artists[0], track.MainArtist)) { - score += 30 - } else if (util.ContainsFold(track.MainArtist, r.Artist) || util.ContainsFold(r.Artist, track.MainArtist)) || (len(r.Artists) > 0 && util.ContainsFold(r.Artists[0], track.MainArtist)) { - score += 15 + if track.MainArtist != "" { + if strings.EqualFold(track.MainArtist, r.Artist) || (len(r.Artists) > 0 && strings.EqualFold(r.Artists[0], track.MainArtist)) { + score += 30 + } else if (r.Artist != "" && (util.ContainsFold(track.MainArtist, r.Artist) || util.ContainsFold(r.Artist, track.MainArtist))) || (len(r.Artists) > 0 && util.ContainsFold(r.Artists[0], track.MainArtist)) { + score += 15 + } } resultFile := filepath.Base(r.Path) diff --git a/src/client/client_test.go b/src/client/client_test.go new file mode 100644 index 00000000..05b8b79a --- /dev/null +++ b/src/client/client_test.go @@ -0,0 +1,41 @@ +package client + +import ( + "testing" + + "explo/src/models" +) + +func TestBestMatchArtist(t *testing.T) { + // Plex results usually have an empty Artist (originalTitle) and the artist in Artists[0] + sylvan := SearchResult{Title: "Numb", Album: "Free Love", Artists: []string{"Sylvan Esso"}, Duration: 189, Path: "/m/Numb.flac"} + linkin := SearchResult{Title: "Numb", Album: "Meteora", Artists: []string{"Linkin Park"}, Duration: 187, Path: "/m/LP - Numb.flac"} + cake := SearchResult{Title: "I Will Survive", Album: "Fashion Nugget", Artists: []string{"CAKE"}, Duration: 310, Path: "/m/I Will Survive.flac"} + + tests := []struct { + name string + track models.Track + results []SearchResult + want string // expected Artists[0], "" for no match + }{ + {"same title, other artist", models.Track{CleanTitle: "Numb", MainArtist: "Linkin Park", Album: "Meteora", Duration: 187000}, []SearchResult{sylvan}, ""}, + {"other artist, no album", models.Track{CleanTitle: "Numb", MainArtist: "Linkin Park", Duration: 187000}, []SearchResult{sylvan}, ""}, + {"cover, no duration", models.Track{CleanTitle: "I Will Survive", MainArtist: "Gloria Gaynor", Album: "Love Tracks"}, []SearchResult{cake}, ""}, + {"cover, no album", models.Track{CleanTitle: "I Will Survive", MainArtist: "Gloria Gaynor", Duration: 198000}, []SearchResult{cake}, ""}, + {"no path on either side", models.Track{CleanTitle: "Numb", MainArtist: "Linkin Park"}, []SearchResult{{Title: "Numb", Artists: []string{"Sylvan Esso"}}}, ""}, + {"right artist wins", models.Track{CleanTitle: "Numb", MainArtist: "Linkin Park", Album: "Meteora", Duration: 187000}, []SearchResult{sylvan, linkin}, "Linkin Park"}, + {"right artist, no album", models.Track{CleanTitle: "Numb", MainArtist: "Linkin Park"}, []SearchResult{sylvan, linkin}, "Linkin Park"}, + } + for _, tt := range tests { + got, ok := BestMatch(&tt.track, tt.results, 60) + if tt.want == "" { + if ok { + t.Errorf("%s: matched %v with score %d, want no match", tt.name, got.Artists, got.Score) + } + continue + } + if !ok || got.Artists[0] != tt.want { + t.Errorf("%s: got %v (ok=%v), want %s", tt.name, got.Artists, ok, tt.want) + } + } +}