mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-13 13:29:09 +02:00
fix(metadata): normalize artist credits and preserve recording versions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user