diff --git a/CHANGELOG.md b/CHANGELOG.md index b54e5018..c48744ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ - Now uses `firstArtist` + `otherArtists` instead of deprecated `artists.items` - Logs correctly show "Fetched track: {title} by {artist}" +- **Japanese Artist Name Order**: Fixed artist mismatch for Japanese names with different order + - "Sawano Hiroyuki" vs "Hiroyuki Sawano" now correctly matches + - Added `sameWordsUnordered` check to both Tidal and Qobuz artist matching + - Handles Japanese name order (family name first) vs Western name order (given name first) + --- ## [3.0.0-beta.1] - 2026-01-13 diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 85644886..1d18f632 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -84,6 +84,12 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { return true } + // Check if same words in different order (e.g., "Sawano Hiroyuki" vs "Hiroyuki Sawano") + if qobuzSameWordsUnordered(expectedFirst, foundFirst) { + GoLog("[Qobuz] Artist names have same words in different order, assuming match: '%s' vs '%s'\n", expectedArtist, foundArtist) + return true + } + // If scripts are TRULY different (Latin vs CJK/Arabic/Cyrillic), assume match (transliteration) // Don't treat Latin Extended (Polish, French, etc.) as different script expectedLatin := qobuzIsLatinScript(expectedArtist) @@ -96,6 +102,43 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { return false } +// qobuzSameWordsUnordered checks if two strings have the same words regardless of order +// Useful for Japanese names: "Sawano Hiroyuki" vs "Hiroyuki Sawano" +func qobuzSameWordsUnordered(a, b string) bool { + wordsA := strings.Fields(a) + wordsB := strings.Fields(b) + + // Must have same number of words + if len(wordsA) != len(wordsB) || len(wordsA) == 0 { + return false + } + + // Sort and compare + sortedA := make([]string, len(wordsA)) + sortedB := make([]string, len(wordsB)) + copy(sortedA, wordsA) + copy(sortedB, wordsB) + + // Simple bubble sort (usually just 2-3 words) + for i := 0; i < len(sortedA)-1; i++ { + for j := i + 1; j < len(sortedA); j++ { + if sortedA[i] > sortedA[j] { + sortedA[i], sortedA[j] = sortedA[j], sortedA[i] + } + if sortedB[i] > sortedB[j] { + sortedB[i], sortedB[j] = sortedB[j], sortedB[i] + } + } + } + + for i := range sortedA { + if sortedA[i] != sortedB[i] { + return false + } + } + return true +} + // qobuzTitlesMatch checks if track titles are similar enough func qobuzTitlesMatch(expectedTitle, foundTitle string) bool { normExpected := strings.ToLower(strings.TrimSpace(expectedTitle)) diff --git a/go_backend/tidal.go b/go_backend/tidal.go index df3d2826..30ed3321 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1273,6 +1273,12 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool { return true } + // Check if same words in different order (e.g., "Sawano Hiroyuki" vs "Hiroyuki Sawano") + if sameWordsUnordered(spotifyFirst, tidalFirst) { + GoLog("[Tidal] Artist names have same words in different order, assuming match: '%s' vs '%s'\n", spotifyArtist, tidalArtist) + return true + } + // If scripts are TRULY different (Latin vs CJK/Arabic/Cyrillic), assume match (transliteration) // Don't treat Latin Extended (Polish, French, etc.) as different script // This handles cases like "鈴木雅之" vs "Masayuki Suzuki" @@ -1286,6 +1292,43 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool { return false } +// sameWordsUnordered checks if two strings have the same words regardless of order +// Useful for Japanese names: "Sawano Hiroyuki" vs "Hiroyuki Sawano" +func sameWordsUnordered(a, b string) bool { + wordsA := strings.Fields(a) + wordsB := strings.Fields(b) + + // Must have same number of words + if len(wordsA) != len(wordsB) || len(wordsA) == 0 { + return false + } + + // Sort and compare + sortedA := make([]string, len(wordsA)) + sortedB := make([]string, len(wordsB)) + copy(sortedA, wordsA) + copy(sortedB, wordsB) + + // Simple bubble sort (usually just 2-3 words) + for i := 0; i < len(sortedA)-1; i++ { + for j := i + 1; j < len(sortedA); j++ { + if sortedA[i] > sortedA[j] { + sortedA[i], sortedA[j] = sortedA[j], sortedA[i] + } + if sortedB[i] > sortedB[j] { + sortedB[i], sortedB[j] = sortedB[j], sortedB[i] + } + } + } + + for i := range sortedA { + if sortedA[i] != sortedB[i] { + return false + } + } + return true +} + // titlesMatch checks if track titles are similar enough func titlesMatch(expectedTitle, foundTitle string) bool { normExpected := strings.ToLower(strings.TrimSpace(expectedTitle))