From e049f9b868ac96e26ffe7d84e3bc838dffb750ea Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 13 Jan 2026 20:55:46 +0700 Subject: [PATCH] fix: improve artist matching for multi-artist tracks and add cover logging --- CHANGELOG.md | 10 +++++++ go_backend/cover.go | 24 +++++++++++++--- go_backend/qobuz.go | 67 +++++++++++++++++++++++++++++---------------- go_backend/tidal.go | 67 +++++++++++++++++++++++++++++---------------- 4 files changed, 118 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c48744ba..6fb47087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,16 @@ - Added `sameWordsUnordered` check to both Tidal and Qobuz artist matching - Handles Japanese name order (family name first) vs Western name order (given name first) +- **Multi-Artist Matching**: Fixed artist mismatch for collaboration tracks + - "RADWIMPS feat. Toko Miura" now matches when Qobuz/Tidal only shows "Toko Miura" + - Split artists by separators (`, `, ` feat. `, ` ft. `, ` & `, ` and `, ` x `) + - Match if ANY expected artist matches ANY found artist + +- **Cover Download Logging**: Improved cover download logs for debugging + - Shows original URL, upgrade steps, and final URL + - Displays estimated resolution based on file size + - Logs now appear in Settings > Logs via GoLog + --- ## [3.0.0-beta.1] - 2026-01-13 diff --git a/go_backend/cover.go b/go_backend/cover.go index 7e2c5f33..d8e6bcf3 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -30,12 +30,12 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { return nil, fmt.Errorf("no cover URL provided") } - fmt.Printf("[Cover] Downloading cover from: %s\n", coverURL) + GoLog("[Cover] Original URL: %s", coverURL) // First upgrade small (300) to medium (640) - always do this downloadURL := convertSmallToMedium(coverURL) if downloadURL != coverURL { - fmt.Printf("[Cover] Upgraded 300x300 to 640x640: %s\n", downloadURL) + GoLog("[Cover] Upgraded 300x300 → 640x640") } // Then upgrade to max quality if requested @@ -43,10 +43,14 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { maxURL := upgradeToMaxQuality(downloadURL) if maxURL != downloadURL { downloadURL = maxURL - fmt.Printf("[Cover] Upgraded to max quality URL: %s\n", downloadURL) + GoLog("[Cover] Upgraded to max resolution (~2000x2000)") + } else { + GoLog("[Cover] Max resolution not available, using 640x640") } } + GoLog("[Cover] Final URL: %s", downloadURL) + client := NewHTTPClientWithTimeout(DefaultTimeout) // Create request with User-Agent (required by Spotify CDN) @@ -70,7 +74,19 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { return nil, fmt.Errorf("failed to read cover data: %w", err) } - fmt.Printf("[Cover] Downloaded %d bytes\n", len(data)) + // Calculate approximate resolution from file size + // JPEG ~2000x2000 is typically 300-600KB, 640x640 is ~50-100KB + sizeKB := len(data) / 1024 + var resolution string + if sizeKB > 200 { + resolution = "~2000x2000 (hi-res)" + } else if sizeKB > 50 { + resolution = "~640x640" + } else { + resolution = "~300x300" + } + GoLog("[Cover] Downloaded %d KB (%s)", sizeKB, resolution) + return data, nil } diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 1d18f632..198b6262 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -64,30 +64,27 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { return true } - // Check first artist (before comma or feat) - expectedFirst := strings.Split(normExpected, ",")[0] - expectedFirst = strings.Split(expectedFirst, " feat")[0] - expectedFirst = strings.Split(expectedFirst, " ft.")[0] - expectedFirst = strings.TrimSpace(expectedFirst) + // Split expected artists by common separators (comma, feat, ft., &, and) + // e.g., "RADWIMPS, Toko Miura" or "RADWIMPS feat. Toko Miura" + expectedArtists := qobuzSplitArtists(normExpected) + foundArtists := qobuzSplitArtists(normFound) - foundFirst := strings.Split(normFound, ",")[0] - foundFirst = strings.Split(foundFirst, " feat")[0] - foundFirst = strings.Split(foundFirst, " ft.")[0] - foundFirst = strings.TrimSpace(foundFirst) - - if expectedFirst == foundFirst { - return true - } - - // Check if first artist is contained in the other - if strings.Contains(expectedFirst, foundFirst) || strings.Contains(foundFirst, expectedFirst) { - 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 + // Check if ANY expected artist matches ANY found artist + for _, exp := range expectedArtists { + for _, fnd := range foundArtists { + if exp == fnd { + return true + } + // Also check contains for partial matches + if strings.Contains(exp, fnd) || strings.Contains(fnd, exp) { + return true + } + // Check same words different order + if qobuzSameWordsUnordered(exp, fnd) { + GoLog("[Qobuz] Artist names have same words in different order: '%s' vs '%s'\n", exp, fnd) + return true + } + } } // If scripts are TRULY different (Latin vs CJK/Arabic/Cyrillic), assume match (transliteration) @@ -102,6 +99,30 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { return false } +// qobuzSplitArtists splits artist string by common separators +func qobuzSplitArtists(artists string) []string { + // Replace common separators with a standard one + normalized := 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, " x ", "|") + + parts := strings.Split(normalized, "|") + result := make([]string, 0, len(parts)) + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + // 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 { diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 30ed3321..5ed6eb1d 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1253,30 +1253,27 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool { return true } - // Check first artist (before comma or feat) - spotifyFirst := strings.Split(normSpotify, ",")[0] - spotifyFirst = strings.Split(spotifyFirst, " feat")[0] - spotifyFirst = strings.Split(spotifyFirst, " ft.")[0] - spotifyFirst = strings.TrimSpace(spotifyFirst) + // Split artists by common separators (comma, feat, ft., &, and) + // e.g., "RADWIMPS, Toko Miura" or "RADWIMPS feat. Toko Miura" + spotifyArtists := splitArtists(normSpotify) + tidalArtists := splitArtists(normTidal) - tidalFirst := strings.Split(normTidal, ",")[0] - tidalFirst = strings.Split(tidalFirst, " feat")[0] - tidalFirst = strings.Split(tidalFirst, " ft.")[0] - tidalFirst = strings.TrimSpace(tidalFirst) - - if spotifyFirst == tidalFirst { - return true - } - - // Check if first artist is contained in the other - if strings.Contains(spotifyFirst, tidalFirst) || strings.Contains(tidalFirst, spotifyFirst) { - 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 + // Check if ANY expected artist matches ANY found artist + for _, exp := range spotifyArtists { + for _, fnd := range tidalArtists { + if exp == fnd { + return true + } + // Also check contains for partial matches + if strings.Contains(exp, fnd) || strings.Contains(fnd, exp) { + return true + } + // Check same words different order + if sameWordsUnordered(exp, fnd) { + GoLog("[Tidal] Artist names have same words in different order: '%s' vs '%s'\n", exp, fnd) + return true + } + } } // If scripts are TRULY different (Latin vs CJK/Arabic/Cyrillic), assume match (transliteration) @@ -1292,6 +1289,30 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool { return false } +// splitArtists splits artist string by common separators +func splitArtists(artists string) []string { + // Replace common separators with a standard one + normalized := 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, " x ", "|") + + parts := strings.Split(normalized, "|") + result := make([]string, 0, len(parts)) + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + // 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 {