diff --git a/go_backend/lyrics_paxsenix.go b/go_backend/lyrics_paxsenix.go index 0d27905d..04edf6fe 100644 --- a/go_backend/lyrics_paxsenix.go +++ b/go_backend/lyrics_paxsenix.go @@ -151,7 +151,7 @@ func parsePaxsenixLyricsPayload(raw, provider string, multiPersonWordByWord bool if err := json.Unmarshal([]byte(raw), &lrcPayload); err == nil { lrcPayload = strings.TrimSpace(lrcPayload) if lrcPayload == "" { - return nil, fmt.Errorf("%s returned empty lyrics", provider) + return nil, lyricsServiceUnavailableErrorf("%s returned empty lyrics", provider) } return lyricsResponseFromText(lrcPayload, provider), nil } @@ -191,7 +191,13 @@ func parsePaxsenixLyricsPayload(raw, provider string, multiPersonWordByWord bool if trimmed != "" && !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") { return lyricsResponseFromText(trimmed, provider), nil } - return nil, fmt.Errorf("failed to decode %s lyrics response", provider) + if json.Valid([]byte(trimmed)) { + return nil, lyricsServiceUnavailableErrorf( + "%s returned a response without usable lyrics", + provider, + ) + } + return nil, lyricsServiceUnavailableErrorf("failed to decode %s lyrics response", provider) } // lyricsResponseFromLRCText parses LRC-or-plain text into a response, or nil @@ -541,6 +547,10 @@ func (c *GeniusLyricsClient) FetchLyrics(trackName, artistName string, durationS params := url.Values{} params.Set("url", geniusURL) + // The legacy v1 contract can report success with an empty lyrics string. + // v2 keeps the same string payload shape while using the maintained + // normalized Genius extractor. + params.Set("v", "2") raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/genius/lyrics", params) if err != nil { return nil, fmt.Errorf("genius lyrics fetch failed: %w", err) diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index 27b0a400..5139751c 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -165,6 +165,23 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) { } } +func TestPaxsenixEmptyLyricsPayloadMarksProviderUnavailable(t *testing.T) { + lyrics, err := parsePaxsenixLyricsPayload( + `{"error":false,"lyrics":""}`, + "Genius", + false, + ) + if lyrics != nil || err == nil { + t.Fatalf("empty PAX Senix payload = %#v/%v", lyrics, err) + } + if !isLyricsProviderUnavailableError(err) { + t.Fatalf("empty PAX Senix payload was not marked unavailable: %v", err) + } + if strings.Contains(strings.ToLower(err.Error()), "decode") { + t.Fatalf("empty JSON payload was misreported as a decode failure: %v", err) + } +} + func TestLyricsProviderHealthSkipsUnavailableProvider(t *testing.T) { SetLyricsProviderOrder([]string{LyricsProviderLRCLIB}) defer SetLyricsProviderOrder(nil) @@ -493,6 +510,9 @@ func TestExternalLyricsProvidersWithFakeHTTP(t *testing.T) { } return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"response":{"sections":[{"hits":[{"type":"song","result":{"title":"Song","primary_artist_names":"Artist","url":"https://genius.com/artist-song-lyrics"}}]}]}}`)), Request: req}, nil case strings.Contains(req.URL.Path, "/genius/lyrics"): + if got := req.URL.Query().Get("v"); got != "2" { + t.Fatalf("genius API version = %q", got) + } return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"error":false,"lyrics":"Genius line"}`)), Request: req}, nil default: return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil