diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index 91cdeb05..71b78c62 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -428,6 +428,16 @@ type LRCLibResponse struct { SyncedLyrics string `json:"syncedLyrics"` } +func lrclibTrackName(response *LRCLibResponse) string { + if response == nil { + return "" + } + if trackName := strings.TrimSpace(response.TrackName); trackName != "" { + return trackName + } + return strings.TrimSpace(response.Name) +} + type LyricsLine struct { StartTimeMs int64 `json:"startTimeMs"` Words string `json:"words"` @@ -490,11 +500,19 @@ func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsRes if err := c.lrclibGet("/api/get", params, &lrcResp); err != nil { return nil, err } + if !lyricsSearchTitlesMatch(lrclibTrackName(&lrcResp), track, false) || + !lyricsSearchArtistsMatch(lrcResp.ArtistName, artist) { + return nil, lyricsNotFoundErrorf("LRCLIB returned mismatched track metadata") + } return c.parseLRCLibResponse(&lrcResp), nil } func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec float64) (*LyricsResponse, error) { + return c.fetchLyricsFromLRCLibSearch(query, "", "", durationSec) +} + +func (c *LyricsClient) fetchLyricsFromLRCLibSearch(query, trackName, artistName string, durationSec float64) (*LyricsResponse, error) { params := url.Values{} params.Set("q", query) @@ -507,35 +525,48 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec flo return nil, lyricsNotFoundErrorf("no lyrics found") } - bestMatch := c.findBestMatch(results, durationSec) + bestMatch := c.findBestLRCLibSearchMatch(results, query, trackName, artistName, durationSec) if bestMatch != nil { return c.parseLRCLibResponse(bestMatch), nil } - for _, result := range results { - if result.SyncedLyrics != "" { - return c.parseLRCLibResponse(&result), nil - } - } - - return c.parseLRCLibResponse(&results[0]), nil + return nil, lyricsNotFoundErrorf("no matching lyrics found") } -func (c *LyricsClient) findBestMatch(results []LRCLibResponse, targetDurationSec float64) *LRCLibResponse { +func lrclibSearchResultMatches(result *LRCLibResponse, query, trackName, artistName string, durationSec float64) bool { + if result == nil || !lyricsSearchDurationMatches(result.Duration, durationSec) { + return false + } + + candidateTrack := lrclibTrackName(result) + if strings.TrimSpace(trackName) != "" || strings.TrimSpace(artistName) != "" { + return lyricsSearchTitlesMatch(candidateTrack, trackName, false) && + lyricsSearchArtistsMatch(result.ArtistName, artistName) + } + + normalizedQuery := normalizeLooseArtistName(query) + normalizedTrack := normalizeLooseArtistName(simplifyTrackName(candidateTrack)) + normalizedArtist := normalizeLooseArtistName(normalizeArtistName(result.ArtistName)) + return normalizedQuery != "" && + normalizedTrack != "" && + normalizedArtist != "" && + containsWordSequence(normalizedQuery, normalizedTrack) && + containsWordSequence(normalizedQuery, normalizedArtist) +} + +func (c *LyricsClient) findBestLRCLibSearchMatch(results []LRCLibResponse, query, trackName, artistName string, targetDurationSec float64) *LRCLibResponse { var bestSynced *LRCLibResponse var bestPlain *LRCLibResponse for i := range results { result := &results[i] - - durationMatches := targetDurationSec == 0 || c.durationMatches(result.Duration, targetDurationSec) - - if durationMatches { - if result.SyncedLyrics != "" && bestSynced == nil { - bestSynced = result - } else if result.PlainLyrics != "" && bestPlain == nil { - bestPlain = result - } + if !lrclibSearchResultMatches(result, query, trackName, artistName, targetDurationSec) { + continue + } + if result.SyncedLyrics != "" && bestSynced == nil { + bestSynced = result + } else if result.PlainLyrics != "" && bestPlain == nil { + bestPlain = result } } @@ -1001,7 +1032,7 @@ func (c *LyricsClient) tryLRCLIB(primaryArtist, artistName, trackName, simplifie } query := primaryArtist + " " + trackName - lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec) + lyrics, err = c.fetchLyricsFromLRCLibSearch(query, trackName, primaryArtist, durationSec) if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) { lyrics.Source = "LRCLIB Search" return lyrics, nil @@ -1012,7 +1043,7 @@ func (c *LyricsClient) tryLRCLIB(primaryArtist, artistName, trackName, simplifie if simplifiedTrack != trackName { query = primaryArtist + " " + simplifiedTrack - lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec) + lyrics, err = c.fetchLyricsFromLRCLibSearch(query, simplifiedTrack, primaryArtist, durationSec) if err == nil && lyrics != nil && (len(lyrics.Lines) > 0 || lyrics.Instrumental) { lyrics.Source = "LRCLIB Search (simplified)" return lyrics, nil @@ -1291,8 +1322,8 @@ func convertToLRCWithMetadata(lyrics *LyricsResponse, trackName, artistName stri var simplifyTrackNamePatterns = func() []*regexp.Regexp { patterns := []string{ - `\s*\(feat\..*?\)`, - `\s*\(ft\..*?\)`, + `\s*\(feat\.?.*?\)`, + `\s*\(ft\.?.*?\)`, `\s*\(featuring.*?\)`, `\s*\(with.*?\)`, `\s*-\s*Remaster(ed)?.*$`, @@ -1329,6 +1360,71 @@ func simplifyTrackName(name string) string { return result } +func normalizedLyricsSearchTitle(name string) string { + return strings.ToLower(strings.TrimSpace(simplifyTrackName(name))) +} + +func containsWordSequence(value, sequence string) bool { + valueWords := strings.Fields(value) + sequenceWords := strings.Fields(sequence) + if len(valueWords) == 0 || len(sequenceWords) == 0 || len(sequenceWords) > len(valueWords) { + return false + } + + for start := 0; start <= len(valueWords)-len(sequenceWords); start++ { + matches := true + for offset := range sequenceWords { + if valueWords[start+offset] != sequenceWords[offset] { + matches = false + break + } + } + if matches { + return true + } + } + return false +} + +func lyricsSearchTitlesMatch(candidateTrack, trackName string, allowDecoratedCandidate bool) bool { + expected := normalizedLyricsSearchTitle(trackName) + candidate := normalizedLyricsSearchTitle(candidateTrack) + if expected == "" || candidate == "" { + return false + } + if candidate == expected { + return true + } + return allowDecoratedCandidate && containsWordSequence(candidate, expected) +} + +func lyricsSearchArtistsMatch(candidateArtist, artistName string) bool { + expected := normalizeLooseArtistName(normalizeArtistName(artistName)) + if expected == "" { + return true + } + candidate := normalizeLooseArtistName(normalizeArtistName(candidateArtist)) + if candidate == "" { + return false + } + return candidate == expected || sameWordsUnordered(candidate, expected) +} + +func lyricsSearchDurationMatches(candidateDuration, durationSec float64) bool { + if candidateDuration <= 0 || durationSec <= 0 { + return true + } + return math.Abs(candidateDuration-durationSec) <= durationToleranceSec +} + +func lyricsSearchArtistAppearsInTitle(candidateTrack, artistName string) bool { + expectedArtist := normalizeLooseArtistName(normalizeArtistName(artistName)) + candidateTitle := normalizeLooseArtistName(candidateTrack) + return expectedArtist != "" && + candidateTitle != "" && + containsWordSequence(candidateTitle, expectedArtist) +} + func normalizeArtistName(name string) string { separators := []string{", ", "; ", " & ", " feat. ", " ft. ", " featuring ", " with "} diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go index db300916..46c2e2e3 100644 --- a/go_backend/lyrics_apple.go +++ b/go_backend/lyrics_apple.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "math" "net/http" "net/url" "regexp" @@ -86,53 +85,49 @@ func NewAppleMusicClient() *AppleMusicClient { } } +func appleMusicSearchResultMatches(result appleMusicSearchResult, trackName, artistName string, durationSec float64) bool { + if !lyricsSearchTitlesMatch(result.SongName, trackName, false) { + return false + } + if !lyricsSearchArtistsMatch(result.ArtistName, artistName) { + return false + } + if !lyricsSearchDurationMatches(float64(result.Duration)/1000.0, durationSec) { + return false + } + return true +} + func selectBestAppleMusicSearchResult(results []appleMusicSearchResult, trackName, artistName string, durationSec float64) *appleMusicSearchResult { if len(results) == 0 { return nil } - normalizedTrack := strings.ToLower(strings.TrimSpace(simplifyTrackName(trackName))) - normalizedArtist := strings.ToLower(strings.TrimSpace(normalizeArtistName(artistName))) - if normalizedArtist == "" { - normalizedArtist = strings.ToLower(strings.TrimSpace(artistName)) - } - - bestIndex := 0 + bestIndex := -1 bestScore := -1 for i := range results { result := &results[i] - score := 0 - - candidateTrack := strings.ToLower(strings.TrimSpace(simplifyTrackName(result.SongName))) - candidateArtist := strings.ToLower(strings.TrimSpace(normalizeArtistName(result.ArtistName))) - - switch { - case candidateTrack == normalizedTrack: - score += 50 - case strings.Contains(candidateTrack, normalizedTrack) || strings.Contains(normalizedTrack, candidateTrack): - score += 25 - } - - switch { - case candidateArtist == normalizedArtist: - score += 60 - case strings.Contains(candidateArtist, normalizedArtist) || strings.Contains(normalizedArtist, candidateArtist): - score += 30 - } - - if durationSec > 0 && result.Duration > 0 { - diff := math.Abs(float64(result.Duration)/1000.0 - durationSec) - if diff <= durationToleranceSec { - score += 20 - } + if !appleMusicSearchResultMatches(*result, trackName, artistName, durationSec) { + continue } + score := scoreLyricsSearchCandidate( + result.SongName, + result.ArtistName, + float64(result.Duration)/1000.0, + trackName, + artistName, + durationSec, + ) if score > bestScore { bestScore = score bestIndex = i } } + if bestIndex < 0 { + return nil + } return &results[bestIndex] } diff --git a/go_backend/lyrics_apple_test.go b/go_backend/lyrics_apple_test.go new file mode 100644 index 00000000..b74d208f --- /dev/null +++ b/go_backend/lyrics_apple_test.go @@ -0,0 +1,91 @@ +package gobackend + +import "testing" + +func TestSelectBestAppleMusicSearchResultRejectsWrongSongWithMatchingArtistAndDuration(t *testing.T) { + results := []appleMusicSearchResult{ + { + ID: "azul", + SongName: "Azul", + ArtistName: "Guru Randhawa", + Duration: 186000, + }, + } + + best := selectBestAppleMusicSearchResult( + results, + "SIX", + "Guru Randhawa, Kiran Bajwa, Gurjit Gill & Lavish Dhiman", + 186, + ) + if best != nil { + t.Fatalf("expected the unrelated Azul result to be rejected, got %#v", best) + } +} + +func TestSelectBestAppleMusicSearchResultRequiresAvailableIdentitySignals(t *testing.T) { + tests := []struct { + name string + result appleMusicSearchResult + }{ + { + name: "wrong artist", + result: appleMusicSearchResult{ + ID: "wrong-artist", + SongName: "SIX", + ArtistName: "Different Artist", + Duration: 186000, + }, + }, + { + name: "wrong duration", + result: appleMusicSearchResult{ + ID: "wrong-duration", + SongName: "SIX", + ArtistName: "Guru Randhawa", + Duration: 240000, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + best := selectBestAppleMusicSearchResult( + []appleMusicSearchResult{test.result}, + "SIX", + "Guru Randhawa", + 186, + ) + if best != nil { + t.Fatalf("expected mismatched result to be rejected, got %#v", best) + } + }) + } +} + +func TestSelectBestAppleMusicSearchResultAcceptsCompatibleMetadata(t *testing.T) { + results := []appleMusicSearchResult{ + { + ID: "azul", + SongName: "Azul", + ArtistName: "Guru Randhawa", + Duration: 186000, + }, + { + ID: "six", + SongName: "SIX (feat. Kiran Bajwa)", + ArtistName: "Guru Randhawa & Kiran Bajwa", + Duration: 187000, + }, + } + + best := selectBestAppleMusicSearchResult( + results, + "SIX", + "Guru Randhawa, Kiran Bajwa, Gurjit Gill & Lavish Dhiman", + 186, + ) + if best == nil || best.ID != "six" { + t.Fatalf("expected the compatible SIX result, got %#v", best) + } +} diff --git a/go_backend/lyrics_netease.go b/go_backend/lyrics_netease.go index 20bec370..c7db3e29 100644 --- a/go_backend/lyrics_netease.go +++ b/go_backend/lyrics_netease.go @@ -13,16 +13,18 @@ type NeteaseClient struct { httpClient *http.Client } +type neteaseSearchSong struct { + Name string `json:"name"` + ID int64 `json:"id"` + Artists []struct { + Name string `json:"name"` + } `json:"artists"` +} + type neteaseSearchResponse struct { Result struct { - Songs []struct { - Name string `json:"name"` - ID int64 `json:"id"` - Artists []struct { - Name string `json:"name"` - } `json:"artists"` - } `json:"songs"` - SongCount int `json:"songCount"` + Songs []neteaseSearchSong `json:"songs"` + SongCount int `json:"songCount"` } `json:"result"` Code int `json:"code"` Message string `json:"message"` @@ -104,7 +106,39 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error) return 0, lyricsNotFoundErrorf("no songs found on netease") } - return searchResp.Result.Songs[0].ID, nil + best := selectBestNeteaseSearchResult(searchResp.Result.Songs, trackName, artistName) + if best == nil || best.ID == 0 { + return 0, lyricsNotFoundErrorf("no matching songs found on netease") + } + return best.ID, nil +} + +func selectBestNeteaseSearchResult(results []neteaseSearchSong, trackName, artistName string) *neteaseSearchSong { + bestIndex := -1 + bestScore := -1 + for i := range results { + result := &results[i] + artists := make([]string, 0, len(result.Artists)) + for _, artist := range result.Artists { + if name := strings.TrimSpace(artist.Name); name != "" { + artists = append(artists, name) + } + } + candidateArtist := strings.Join(artists, ", ") + if !lyricsSearchTitlesMatch(result.Name, trackName, false) || + !lyricsSearchArtistsMatch(candidateArtist, artistName) { + continue + } + score := scoreLyricsSearchCandidate(result.Name, candidateArtist, 0, trackName, artistName, 0) + if score > bestScore { + bestIndex = i + bestScore = score + } + } + if bestIndex < 0 { + return nil + } + return &results[bestIndex] } func (c *NeteaseClient) FetchLyricsByID(songID int64, includeTranslation, includeRomanization bool) (string, error) { diff --git a/go_backend/lyrics_paxsenix.go b/go_backend/lyrics_paxsenix.go index c2c911c8..05d39fb7 100644 --- a/go_backend/lyrics_paxsenix.go +++ b/go_backend/lyrics_paxsenix.go @@ -270,16 +270,25 @@ func selectBestSpotifyLyricsSearchResult(results []spotifyLyricsSearchResult, tr return nil } - bestIndex := 0 + bestIndex := -1 bestScore := -1 for i := range results { result := &results[i] - score := scoreLyricsSearchCandidate(result.Name, result.ArtistName, parseClockDuration(result.Duration), trackName, artistName, durationSec) + candidateDuration := parseClockDuration(result.Duration) + if !lyricsSearchTitlesMatch(result.Name, trackName, false) || + !lyricsSearchArtistsMatch(result.ArtistName, artistName) || + !lyricsSearchDurationMatches(candidateDuration, durationSec) { + continue + } + score := scoreLyricsSearchCandidate(result.Name, result.ArtistName, candidateDuration, trackName, artistName, durationSec) if score > bestScore { bestIndex = i bestScore = score } } + if bestIndex < 0 { + return nil + } return &results[bestIndex] } @@ -378,16 +387,27 @@ func selectBestYouTubeLyricsSearchResult(results []youtubeLyricsSearchResult, tr return nil } - bestIndex := 0 + bestIndex := -1 bestScore := -1 for i := range results { result := &results[i] - score := scoreLyricsSearchCandidate(result.Title, result.Author, parseClockDuration(result.Duration), trackName, artistName, durationSec) + candidateDuration := parseClockDuration(result.Duration) + artistMatches := lyricsSearchArtistsMatch(result.Author, artistName) || + lyricsSearchArtistAppearsInTitle(result.Title, artistName) + if !lyricsSearchTitlesMatch(result.Title, trackName, true) || + !artistMatches || + !lyricsSearchDurationMatches(candidateDuration, durationSec) { + continue + } + score := scoreLyricsSearchCandidate(result.Title, result.Author, candidateDuration, trackName, artistName, durationSec) if score > bestScore { bestIndex = i bestScore = score } } + if bestIndex < 0 { + return nil + } return &results[bestIndex] } @@ -435,16 +455,24 @@ func selectBestKugouLyricsSearchResult(results []kugouLyricsSearchResult, trackN return nil } - bestIndex := 0 + bestIndex := -1 bestScore := -1 for i := range results { result := &results[i] + if !lyricsSearchTitlesMatch(result.Title, trackName, false) || + !lyricsSearchArtistsMatch(result.Artist, artistName) || + !lyricsSearchDurationMatches(result.Duration, durationSec) { + continue + } score := scoreLyricsSearchCandidate(result.Title, result.Artist, result.Duration, trackName, artistName, durationSec) if score > bestScore { bestIndex = i bestScore = score } } + if bestIndex < 0 { + return nil + } return &results[bestIndex] } @@ -482,6 +510,14 @@ func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSe return "", fmt.Errorf("failed to decode genius search: %w", err) } + bestURL := selectBestGeniusLyricsSearchResult(results, trackName, artistName, durationSec) + if bestURL == "" { + return "", lyricsNotFoundErrorf("no songs found on genius") + } + return bestURL, nil +} + +func selectBestGeniusLyricsSearchResult(results geniusSearchResponse, trackName, artistName string, durationSec float64) string { bestURL := "" bestScore := -1 for _, section := range results.Response.Sections { @@ -494,6 +530,10 @@ func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSe if strings.TrimSpace(artist) == "" { artist = hit.Result.ArtistNames } + if !lyricsSearchTitlesMatch(hit.Result.Title, trackName, false) || + !lyricsSearchArtistsMatch(artist, artistName) { + continue + } score := scoreLyricsSearchCandidate(hit.Result.Title, artist, 0, trackName, artistName, durationSec) if score > bestScore { bestScore = score @@ -501,11 +541,7 @@ func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSe } } } - - if bestURL == "" { - return "", lyricsNotFoundErrorf("no songs found on genius") - } - return bestURL, nil + return bestURL } func (c *GeniusLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { diff --git a/go_backend/lyrics_search_matching_test.go b/go_backend/lyrics_search_matching_test.go new file mode 100644 index 00000000..f739684f --- /dev/null +++ b/go_backend/lyrics_search_matching_test.go @@ -0,0 +1,155 @@ +package gobackend + +import ( + "encoding/json" + "testing" +) + +func TestLyricsSearchSelectorsRejectUnrelatedSongWithMatchingArtistAndDuration(t *testing.T) { + const ( + trackName = "SIX" + artistName = "Guru Randhawa" + durationSec = 186 + ) + + if best := selectBestSpotifyLyricsSearchResult( + []spotifyLyricsSearchResult{{ + TrackID: "azul", + Name: "Azul", + ArtistName: artistName, + Duration: "3:06", + }}, + trackName, + artistName, + durationSec, + ); best != nil { + t.Fatalf("Spotify accepted unrelated result: %#v", best) + } + + if best := selectBestYouTubeLyricsSearchResult( + []youtubeLyricsSearchResult{{ + VideoID: "azul", + Title: "Azul", + Author: artistName, + Duration: "3:06", + }}, + trackName, + artistName, + durationSec, + ); best != nil { + t.Fatalf("YouTube accepted unrelated result: %#v", best) + } + + if best := selectBestKugouLyricsSearchResult( + []kugouLyricsSearchResult{{ + Hash: "azul", + Title: "Azul", + Artist: artistName, + Duration: durationSec, + }}, + trackName, + artistName, + durationSec, + ); best != nil { + t.Fatalf("Kugou accepted unrelated result: %#v", best) + } + + var geniusResults geniusSearchResponse + if err := json.Unmarshal([]byte(`{ + "response": { + "sections": [{ + "hits": [{ + "type": "song", + "result": { + "title": "Azul", + "primary_artist_names": "Guru Randhawa", + "url": "https://genius.com/guru-randhawa-azul-lyrics" + } + }] + }] + } + }`), &geniusResults); err != nil { + t.Fatalf("decode Genius fixture: %v", err) + } + if bestURL := selectBestGeniusLyricsSearchResult( + geniusResults, + trackName, + artistName, + durationSec, + ); bestURL != "" { + t.Fatalf("Genius accepted unrelated result: %q", bestURL) + } + + var neteaseResults neteaseSearchResponse + if err := json.Unmarshal([]byte(`{ + "result": { + "songCount": 1, + "songs": [{ + "name": "Azul", + "id": 123, + "artists": [{"name": "Guru Randhawa"}] + }] + }, + "code": 200 + }`), &neteaseResults); err != nil { + t.Fatalf("decode Netease fixture: %v", err) + } + if best := selectBestNeteaseSearchResult( + neteaseResults.Result.Songs, + trackName, + artistName, + ); best != nil { + t.Fatalf("Netease accepted unrelated result: %#v", best) + } + + lrclibResult := &LRCLibResponse{ + TrackName: "Azul", + ArtistName: artistName, + Duration: durationSec, + SyncedLyrics: "[00:01.00]Wrong", + } + if lrclibSearchResultMatches( + lrclibResult, + "Guru Randhawa SIX", + trackName, + artistName, + durationSec, + ) { + t.Fatalf("LRCLIB accepted unrelated result: %#v", lrclibResult) + } +} + +func TestYouTubeLyricsSearchAllowsDecoratedTitleWithArtistSignal(t *testing.T) { + results := []youtubeLyricsSearchResult{{ + VideoID: "six", + Title: "Guru Randhawa - SIX (Official Music Video)", + Author: "T-Series", + Duration: "3:06", + }} + + best := selectBestYouTubeLyricsSearchResult( + results, + "SIX", + "Guru Randhawa", + 186, + ) + if best == nil || best.VideoID != "six" { + t.Fatalf("expected decorated YouTube result to match, got %#v", best) + } +} + +func TestDecoratedLyricsTitleMatchingUsesWholeWords(t *testing.T) { + if !lyricsSearchTitlesMatch( + "Guru Randhawa - SIX (Official Music Video)", + "SIX", + true, + ) { + t.Fatal("expected decorated SIX title to match") + } + if lyricsSearchTitlesMatch("SIXTEEN", "SIX", true) { + t.Fatal("SIX must not match SIXTEEN") + } + if lyricsSearchArtistsMatch("Guru Randhawa Tribute", "Guru Randhawa") { + t.Fatal("artist matching must not accept a longer unrelated name") + } +} diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index bcb49437..c57f2877 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -114,7 +114,7 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) { case "/api/get": return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"id":1,"trackName":"Song","artistName":"Artist","duration":180,"syncedLyrics":"[00:01.00]Hello"}`)), Request: req}, nil case "/api/search": - return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"id":2,"duration":180,"plainLyrics":"Plain\nLyric"},{"id":3,"duration":180,"syncedLyrics":"[00:02.00]Synced"}]`)), Request: req}, nil + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"id":2,"trackName":"Song","artistName":"Artist","duration":180,"plainLyrics":"Plain\nLyric"},{"id":3,"trackName":"Song","artistName":"Artist","duration":180,"syncedLyrics":"[00:02.00]Synced"}]`)), Request: req}, nil default: return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil } @@ -127,7 +127,16 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) { if err != nil || len(search.Lines) == 0 { t.Fatalf("FetchLyricsFromLRCLibSearch = %#v/%v", search, err) } - if best := client.findBestMatch([]LRCLibResponse{{Duration: 100, PlainLyrics: "A"}, {Duration: 180, SyncedLyrics: "[00:01.00]B"}}, 180); best == nil || best.SyncedLyrics == "" { + if best := client.findBestLRCLibSearchMatch( + []LRCLibResponse{ + {TrackName: "Other", ArtistName: "Artist", Duration: 180, PlainLyrics: "A"}, + {TrackName: "Song", ArtistName: "Artist", Duration: 180, SyncedLyrics: "[00:01.00]B"}, + }, + "Artist Song", + "Song", + "Artist", + 180, + ); best == nil || best.SyncedLyrics == "" { t.Fatalf("best = %#v", best) } if !client.durationMatches(181, 180) || client.durationMatches(300, 180) {