fix(metadata): normalize artist credits and preserve recording versions

This commit is contained in:
zarzet
2026-09-06 19:23:06 +07:00
parent 7d1212f519
commit 56dc9ceb0d
2 changed files with 86 additions and 6 deletions
+27 -6
View File
@@ -120,8 +120,8 @@ func artistsMatch(expectedArtist, foundArtist string) bool {
return true
}
expectedArtists := splitArtists(normExpected)
foundArtists := splitArtists(normFound)
expectedArtists := splitArtists(expectedArtist)
foundArtists := splitArtists(foundArtist)
for _, expected := range expectedArtists {
for _, found := range foundArtists {
@@ -142,20 +142,21 @@ func artistsMatch(expectedArtist, foundArtist string) bool {
}
func splitArtists(artists string) []string {
normalized := artists
normalized := strings.ToLower(artists)
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
normalized = strings.ReplaceAll(normalized, " feat ", "|")
normalized = strings.ReplaceAll(normalized, " ft. ", "|")
normalized = strings.ReplaceAll(normalized, " ft ", "|")
normalized = strings.ReplaceAll(normalized, " & ", "|")
normalized = strings.ReplaceAll(normalized, " and ", "|")
normalized = strings.ReplaceAll(normalized, ", ", "|")
normalized = strings.ReplaceAll(normalized, ",", "|")
normalized = strings.ReplaceAll(normalized, ";", "|")
normalized = strings.ReplaceAll(normalized, " x ", "|")
parts := strings.Split(normalized, "|")
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
trimmed := normalizeLooseArtistName(part)
if trimmed != "" {
result = append(result, trimmed)
}
@@ -253,6 +254,26 @@ func titlesMatch(expectedTitle, foundTitle string) bool {
return false
}
func trackTitlesMatch(expectedTitle, foundTitle string) bool {
expected := normalizeLooseTitle(expectedTitle)
found := normalizeLooseTitle(foundTitle)
if expected != "" && expected == found {
return true
}
// Version words identify recordings; punctuation around them does not.
for _, title := range []string{expected, found} {
for _, word := range strings.Fields(title) {
switch word {
case "mix", "remix", "live", "acoustic", "demo", "instrumental",
"karaoke", "edit", "extended", "slowed", "sped":
return false
}
}
}
return titlesMatch(expectedTitle, foundTitle)
}
func extractCoreTitle(title string) string {
parenIdx := strings.Index(title, "(")
bracketIdx := strings.Index(title, "[")
@@ -425,7 +446,7 @@ func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPre
}
if req.TrackName != "" && resolved.Title != "" &&
!titlesMatch(req.TrackName, resolved.Title) {
!trackTitlesMatch(req.TrackName, resolved.Title) {
GoLog("[%s] Verification failed: title mismatch — expected '%s', got '%s'\n",
logPrefix, req.TrackName, resolved.Title)
return false
+59
View File
@@ -171,6 +171,65 @@ func TestTitlesMatch_SeparatorVariants(t *testing.T) {
}
}
func TestTrackMatchingPreservesArtistCreditBoundaries(t *testing.T) {
req := DownloadRequest{
TrackName: "Signal - Remix", ArtistName: "Composer, Lead Singer & Lyric Writer",
AlbumName: "Original Soundtrack", DurationMS: 234000,
}
for _, artist := range []string{
"Composer, Lead Singer, Guest Writer",
"Lead Singer & Composer",
"GUEST WRITER; LEAD SINGER",
} {
t.Run(artist, func(t *testing.T) {
resolved := resolvedTrackInfo{
Title: "Signal (Remix)", ArtistName: artist,
AlbumName: "Original Soundtrack", Duration: 234,
}
if !trackMatchesRequest(req, resolved, "test") {
t.Fatal("matching recording rejected because contributor credits differ")
}
tracks := []ExtTrackMetadata{{
Name: resolved.Title, Artists: artist, AlbumName: "Collection",
DurationMS: 234000, ProviderID: "provider",
}}
if selectBestMetadataEnrichmentTrack(req, tracks) == nil {
t.Fatal("matching recording rejected during metadata enrichment")
}
})
}
}
func TestTrackMetadataTolerancePreservesRecordingIdentity(t *testing.T) {
req := DownloadRequest{
TrackName: "Signal - Remix", ArtistName: "Composer, Lead Singer & Lyric Writer",
AlbumName: "Original Soundtrack", DurationMS: 234000,
}
for _, tc := range []struct {
name string
title string
artist string
duration int
want bool
}{
{"punctuation", "Signal (Remix)", req.ArtistName, 234, true},
{"original", "Signal", req.ArtistName, 234, false},
{"named mix", "Signal (Club Mix)", req.ArtistName, 234, false},
{"live remix", "Signal (Remix Live)", req.ArtistName, 234, false},
{"other artist", "Signal (Remix)", "Unrelated Singer", 234, false},
{"other duration", "Signal (Remix)", "Composer, Lead Singer, Guest Writer", 305, false},
} {
t.Run(tc.name, func(t *testing.T) {
got := trackMatchesRequest(req, resolvedTrackInfo{
Title: tc.title, ArtistName: tc.artist, AlbumName: req.AlbumName, Duration: tc.duration,
}, "test")
if got != tc.want {
t.Fatalf("trackMatchesRequest = %v, want %v", got, tc.want)
}
})
}
}
func TestTitlesMatch_EmojiStrict(t *testing.T) {
if titlesMatch("🪐", "Higher Power") {
t.Fatal("expected emoji title not to match unrelated textual title")