mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-25 21:40:57 +02:00
refactor(lyrics): classify provider errors by type, not message text
Replace the substring signal lists in isLyricsProviderUnavailableError with typed errors (errLyricsNotFound / errLyricsServiceUnavailable in the new lyrics_errors.go). Providers now classify failures at the point of origin: HTTP statuses via lyricsHTTPStatusError (429/5xx cooldown), netease API codes and paxsenix proxy failures as explicit unavailable, and every "no lyrics/songs found" path as typed not-found. Substring matching survives only for error payloads from third-party proxies (rate limit, missing parameters), whose messages genuinely arrive as free text. Two deliberate behavior changes: LyricsPlus 429/5xx responses now cool the provider down (previously unclassified), and proxy payloads saying "not found" are treated as not-found instead of disabling the provider.
This commit is contained in:
+14
-50
@@ -2,6 +2,7 @@ package gobackend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -230,58 +231,21 @@ func markLyricsProviderUnavailable(providerName string, err error) {
|
||||
GoLog("[Lyrics] Provider %s marked unavailable for %s: %s\n", providerName, lyricsProviderUnavailableCooldown, reason)
|
||||
}
|
||||
|
||||
var lyricsNotFoundSignals = []string{
|
||||
"lyrics not found",
|
||||
"no lyrics found",
|
||||
"no songs found",
|
||||
"not found on",
|
||||
"empty track",
|
||||
"empty search query",
|
||||
"needs a deezer id",
|
||||
}
|
||||
|
||||
// Provider/API-level failures that should temporarily disable a lyrics source.
|
||||
// Transport failures are handled by isConnectivityFailure via typed errors.
|
||||
var lyricsServiceUnavailableSignals = []string{
|
||||
"fetch failed",
|
||||
"missing required parameters",
|
||||
"request failed",
|
||||
"request unsuccessful",
|
||||
"search failed",
|
||||
"search unavailable",
|
||||
"rate limit",
|
||||
"too many requests",
|
||||
"operation too frequent",
|
||||
"操作频繁",
|
||||
"proxy returned http 429",
|
||||
"proxy returned http 5",
|
||||
"unexpected status code: 429",
|
||||
"unexpected status code: 5",
|
||||
"unexpected response code",
|
||||
"returned http 429",
|
||||
"returned http 5",
|
||||
}
|
||||
|
||||
// isLyricsProviderUnavailableError reports whether err is a provider/API-level
|
||||
// failure that should temporarily disable a lyrics source. Providers classify
|
||||
// their failures with the typed errors in lyrics_errors.go at the point of
|
||||
// origin; transport failures are handled by isConnectivityFailure.
|
||||
func isLyricsProviderUnavailableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := strings.ToLower(err.Error())
|
||||
for _, signal := range lyricsNotFoundSignals {
|
||||
if strings.Contains(msg, signal) {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, errLyricsNotFound) {
|
||||
return false
|
||||
}
|
||||
if isConnectivityFailure(err) {
|
||||
if errors.Is(err, errLyricsServiceUnavailable) {
|
||||
return true
|
||||
}
|
||||
for _, signal := range lyricsServiceUnavailableSignals {
|
||||
if strings.Contains(msg, signal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return isConnectivityFailure(err)
|
||||
}
|
||||
|
||||
func GetLyricsProviderOrder() []string {
|
||||
@@ -486,11 +450,11 @@ func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsRes
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fmt.Errorf("lyrics not found")
|
||||
return nil, lyricsNotFoundErrorf("lyrics not found")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var lrcResp LRCLibResponse
|
||||
@@ -521,7 +485,7 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec flo
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var results []LRCLibResponse
|
||||
@@ -530,7 +494,7 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec flo
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no lyrics found")
|
||||
return nil, lyricsNotFoundErrorf("no lyrics found")
|
||||
}
|
||||
|
||||
bestMatch := c.findBestMatch(results, durationSec)
|
||||
@@ -1048,7 +1012,7 @@ func (c *LyricsClient) tryLRCLIB(primaryArtist, artistName, trackName, simplifie
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("LRCLIB: no lyrics found")
|
||||
return nil, lyricsNotFoundErrorf("LRCLIB: no lyrics found")
|
||||
}
|
||||
|
||||
func (c *LyricsClient) parseLRCLibResponse(resp *LRCLibResponse) *LyricsResponse {
|
||||
|
||||
@@ -157,7 +157,7 @@ func (c *AppleMusicClient) getAppleMusicToken() (string, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("apple music page returned HTTP %d", resp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(resp.StatusCode, "apple music page returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
@@ -183,7 +183,7 @@ func (c *AppleMusicClient) getAppleMusicToken() (string, error) {
|
||||
defer jsResp.Body.Close()
|
||||
|
||||
if jsResp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("apple music script returned HTTP %d", jsResp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(jsResp.StatusCode, "apple music script returned HTTP %d", jsResp.StatusCode)
|
||||
}
|
||||
|
||||
jsBody, err := io.ReadAll(jsResp.Body)
|
||||
@@ -241,7 +241,7 @@ func (c *AppleMusicClient) searchSongWithToken(token, query string) ([]appleMusi
|
||||
return nil, errAppleMusicUnauthorized
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("apple music catalog search returned HTTP %d", resp.StatusCode)
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "apple music catalog search returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var searchResp appleMusicCatalogSearchResponse
|
||||
@@ -275,7 +275,7 @@ func (c *AppleMusicClient) searchSongWithToken(token, query string) ([]appleMusi
|
||||
func (c *AppleMusicClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) {
|
||||
query := trackName + " " + artistName
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return "", fmt.Errorf("empty search query")
|
||||
return "", lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
token, err := c.getAppleMusicToken()
|
||||
@@ -298,7 +298,7 @@ func (c *AppleMusicClient) SearchSong(trackName, artistName string, durationSec
|
||||
|
||||
best := selectBestAppleMusicSearchResult(searchResp, trackName, artistName, durationSec)
|
||||
if best == nil || strings.TrimSpace(best.ID) == "" {
|
||||
return "", fmt.Errorf("no songs found on apple music")
|
||||
return "", lyricsNotFoundErrorf("no songs found on apple music")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(best.ID), nil
|
||||
@@ -321,7 +321,7 @@ func (c *AppleMusicClient) FetchLyricsByID(songID string) (string, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("apple music lyrics proxy returned HTTP %d", resp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(resp.StatusCode, "apple music lyrics proxy returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
@@ -454,7 +454,7 @@ func (c *AppleMusicClient) FetchLyrics(
|
||||
return nil, err
|
||||
}
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(rawLyrics); isErrorPayload {
|
||||
return nil, fmt.Errorf("apple music proxy returned non-lyric payload: %s", errMsg)
|
||||
return nil, classifyLyricsPayloadError(0, errMsg, "apple music proxy returned non-lyric payload: %s", errMsg)
|
||||
}
|
||||
|
||||
lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord, preserveWordTiming)
|
||||
@@ -487,5 +487,5 @@ func (c *AppleMusicClient) FetchLyrics(
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no lyrics found on apple music")
|
||||
return nil, lyricsNotFoundErrorf("no lyrics found on apple music")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sentinel classifications for lyrics provider failures. Providers wrap their
|
||||
// errors with the typed constructors below at the point where the failure kind
|
||||
// is known, so isLyricsProviderUnavailableError can classify with errors.Is
|
||||
// instead of parsing error text.
|
||||
var (
|
||||
errLyricsNotFound = errors.New("lyrics not found")
|
||||
errLyricsServiceUnavailable = errors.New("lyrics service unavailable")
|
||||
)
|
||||
|
||||
// lyricsNotFoundError: the provider responded but has nothing for this track
|
||||
// (including "nothing to search for" inputs). Never disables the provider.
|
||||
type lyricsNotFoundError struct{ err error }
|
||||
|
||||
func (e *lyricsNotFoundError) Error() string { return e.err.Error() }
|
||||
func (e *lyricsNotFoundError) Unwrap() error { return e.err }
|
||||
func (e *lyricsNotFoundError) Is(target error) bool { return target == errLyricsNotFound }
|
||||
|
||||
func lyricsNotFoundErrorf(format string, args ...any) error {
|
||||
return &lyricsNotFoundError{err: fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
// lyricsServiceUnavailableError: a provider/API-level failure that should
|
||||
// temporarily disable the lyrics source (see markLyricsProviderUnavailable).
|
||||
type lyricsServiceUnavailableError struct{ err error }
|
||||
|
||||
func (e *lyricsServiceUnavailableError) Error() string { return e.err.Error() }
|
||||
func (e *lyricsServiceUnavailableError) Unwrap() error { return e.err }
|
||||
func (e *lyricsServiceUnavailableError) Is(target error) bool {
|
||||
return target == errLyricsServiceUnavailable
|
||||
}
|
||||
|
||||
func lyricsServiceUnavailableErrorf(format string, args ...any) error {
|
||||
return &lyricsServiceUnavailableError{err: fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
// lyricsHTTPStatusError classifies an upstream HTTP status at the response
|
||||
// site: 429 and 5xx mean the service is temporarily unusable; anything else
|
||||
// stays a plain error.
|
||||
func lyricsHTTPStatusError(statusCode int, format string, args ...any) error {
|
||||
if statusCode == http.StatusTooManyRequests || statusCode >= 500 {
|
||||
return lyricsServiceUnavailableErrorf(format, args...)
|
||||
}
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Error payloads from third-party lyrics proxies arrive as free text, often
|
||||
// with HTTP 200 (see detectLyricsErrorPayload). Their meaning can only be
|
||||
// recognized from the message, so this is the one place lyrics failures are
|
||||
// still classified by substring. Errors generated by our own code must use
|
||||
// the typed constructors above instead.
|
||||
var (
|
||||
lyricsPayloadNotFoundSignals = []string{
|
||||
"lyrics not found",
|
||||
"no lyrics found",
|
||||
"no songs found",
|
||||
"not found",
|
||||
}
|
||||
lyricsPayloadUnavailableSignals = []string{
|
||||
"rate limit",
|
||||
"too many requests",
|
||||
"operation too frequent",
|
||||
"操作频繁",
|
||||
"missing required parameters",
|
||||
}
|
||||
)
|
||||
|
||||
func lyricsPayloadIndicatesNotFound(payloadMsg string) bool {
|
||||
msg := strings.ToLower(payloadMsg)
|
||||
for _, signal := range lyricsPayloadNotFoundSignals {
|
||||
if strings.Contains(msg, signal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// classifyLyricsPayloadError builds a provider error from an upstream error
|
||||
// payload, typed by what the payload message describes. statusCode is the
|
||||
// HTTP status of the response (0 when it arrived with HTTP 200).
|
||||
func classifyLyricsPayloadError(statusCode int, payloadMsg string, format string, args ...any) error {
|
||||
if lyricsPayloadIndicatesNotFound(payloadMsg) {
|
||||
return lyricsNotFoundErrorf(format, args...)
|
||||
}
|
||||
msg := strings.ToLower(payloadMsg)
|
||||
for _, signal := range lyricsPayloadUnavailableSignals {
|
||||
if strings.Contains(msg, signal) {
|
||||
return lyricsServiceUnavailableErrorf(format, args...)
|
||||
}
|
||||
}
|
||||
return lyricsHTTPStatusError(statusCode, format, args...)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (c *LyricsPlusClient) FetchLyrics(
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, fmt.Errorf("lyricsplus: no lyrics found")
|
||||
return nil, lyricsNotFoundErrorf("lyricsplus: no lyrics found")
|
||||
}
|
||||
|
||||
func (c *LyricsPlusClient) fetchFromServer(
|
||||
@@ -132,10 +132,10 @@ func (c *LyricsPlusClient) fetchFromServer(
|
||||
if strings.TrimSpace(isrc) != "" {
|
||||
return c.fetchFromServer(server, trackName, artistName, "", durationSec, multiPersonWordByWord, preserveWordTiming)
|
||||
}
|
||||
return nil, fmt.Errorf("lyrics not found")
|
||||
return nil, lyricsNotFoundErrorf("lyrics not found")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
return nil, lyricsHTTPStatusError(resp.StatusCode, "HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var payload lyricsPlusResponse
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewMusixmatchClient() *MusixmatchClient {
|
||||
|
||||
func (c *MusixmatchClient) fetchLyricsPayload(trackName, artistName string, durationSec float64, lyricsType, language string) (string, error) {
|
||||
if strings.TrimSpace(trackName) == "" || strings.TrimSpace(artistName) == "" {
|
||||
return "", fmt.Errorf("empty track or artist name")
|
||||
return "", lyricsNotFoundErrorf("empty track or artist name")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -62,9 +62,9 @@ func (c *MusixmatchClient) fetchLyricsPayload(trackName, artistName string, dura
|
||||
if resp.StatusCode != 200 {
|
||||
trimmed := strings.TrimSpace(string(body))
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload {
|
||||
return "", fmt.Errorf("musixmatch proxy returned HTTP %d: %s", resp.StatusCode, errMsg)
|
||||
return "", classifyLyricsPayloadError(resp.StatusCode, errMsg, "musixmatch proxy returned HTTP %d: %s", resp.StatusCode, errMsg)
|
||||
}
|
||||
return "", fmt.Errorf("musixmatch proxy returned HTTP %d", resp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(resp.StatusCode, "musixmatch proxy returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var lrcPayload string
|
||||
@@ -78,7 +78,7 @@ func (c *MusixmatchClient) fetchLyricsPayload(trackName, artistName string, dura
|
||||
|
||||
trimmed := strings.TrimSpace(string(body))
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload {
|
||||
return "", fmt.Errorf("%s", errMsg)
|
||||
return "", classifyLyricsPayloadError(0, errMsg, "%s", errMsg)
|
||||
}
|
||||
if trimmed != "" && !strings.HasPrefix(trimmed, "{") {
|
||||
return trimmed, nil
|
||||
@@ -119,7 +119,7 @@ func (c *MusixmatchClient) FetchLyricsInLanguage(trackName, artistName string, d
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no lyrics found on musixmatch for language %s", lang)
|
||||
return nil, lyricsNotFoundErrorf("no lyrics found on musixmatch for language %s", lang)
|
||||
}
|
||||
|
||||
func (c *MusixmatchClient) FetchLyrics(trackName, artistName string, durationSec float64, preferredLanguage string) (*LyricsResponse, error) {
|
||||
@@ -158,5 +158,5 @@ func (c *MusixmatchClient) FetchLyrics(trackName, artistName string, durationSec
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no lyrics found on musixmatch")
|
||||
return nil, lyricsNotFoundErrorf("no lyrics found on musixmatch")
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func NewNeteaseClient() *NeteaseClient {
|
||||
func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error) {
|
||||
query := trackName + " " + artistName
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return 0, fmt.Errorf("empty search query")
|
||||
return 0, lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
searchURL := "https://lyrics.paxsenix.org/netease/search"
|
||||
@@ -81,7 +81,7 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0, fmt.Errorf("netease search returned HTTP %d", resp.StatusCode)
|
||||
return 0, lyricsHTTPStatusError(resp.StatusCode, "netease search returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var searchResp neteaseSearchResponse
|
||||
@@ -97,11 +97,11 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error)
|
||||
if message == "" {
|
||||
message = "unexpected response code"
|
||||
}
|
||||
return 0, fmt.Errorf("netease search unavailable: code %d: %s", searchResp.Code, message)
|
||||
return 0, lyricsServiceUnavailableErrorf("netease search unavailable: code %d: %s", searchResp.Code, message)
|
||||
}
|
||||
|
||||
if searchResp.Result.SongCount == 0 || len(searchResp.Result.Songs) == 0 {
|
||||
return 0, fmt.Errorf("no songs found on netease")
|
||||
return 0, lyricsNotFoundErrorf("no songs found on netease")
|
||||
}
|
||||
|
||||
return searchResp.Result.Songs[0].ID, nil
|
||||
@@ -131,7 +131,7 @@ func (c *NeteaseClient) FetchLyricsByID(songID int64, includeTranslation, includ
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("netease lyrics returned HTTP %d", resp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(resp.StatusCode, "netease lyrics returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var lyricsResp neteaseLyricsResponse
|
||||
@@ -140,7 +140,7 @@ func (c *NeteaseClient) FetchLyricsByID(songID int64, includeTranslation, includ
|
||||
}
|
||||
|
||||
if lyricsResp.LRC == nil || strings.TrimSpace(lyricsResp.LRC.Lyric) == "" {
|
||||
return "", fmt.Errorf("no lyrics available on netease")
|
||||
return "", lyricsNotFoundErrorf("no lyrics available on netease")
|
||||
}
|
||||
|
||||
lyric := lyricsResp.LRC.Lyric
|
||||
|
||||
@@ -122,18 +122,26 @@ func fetchPaxsenixBody(httpClient *http.Client, endpoint string, params url.Valu
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// Any failure of the shared proxy fetch disables the source until the
|
||||
// cooldown expires, except when the payload says the track has no lyrics.
|
||||
trimmed := strings.TrimSpace(string(body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload {
|
||||
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, errMsg)
|
||||
if lyricsPayloadIndicatesNotFound(errMsg) {
|
||||
return "", lyricsNotFoundErrorf("HTTP %d: %s", resp.StatusCode, errMsg)
|
||||
}
|
||||
return "", lyricsServiceUnavailableErrorf("HTTP %d: %s", resp.StatusCode, errMsg)
|
||||
}
|
||||
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
return "", lyricsServiceUnavailableErrorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload {
|
||||
return "", fmt.Errorf("%s", errMsg)
|
||||
if lyricsPayloadIndicatesNotFound(errMsg) {
|
||||
return "", lyricsNotFoundErrorf("%s", errMsg)
|
||||
}
|
||||
return "", lyricsServiceUnavailableErrorf("%s", errMsg)
|
||||
}
|
||||
if trimmed == "" {
|
||||
return "", fmt.Errorf("empty response")
|
||||
return "", lyricsServiceUnavailableErrorf("empty response")
|
||||
}
|
||||
return trimmed, nil
|
||||
}
|
||||
@@ -236,7 +244,7 @@ var regexpSpotifyTrackID = regexp.MustCompile(`^[A-Za-z0-9]{22}$`)
|
||||
func (c *SpotifyLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) {
|
||||
query := strings.TrimSpace(trackName + " " + artistName)
|
||||
if query == "" {
|
||||
return "", fmt.Errorf("empty search query")
|
||||
return "", lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -252,7 +260,7 @@ func (c *SpotifyLyricsClient) SearchSong(trackName, artistName string, durationS
|
||||
}
|
||||
best := selectBestSpotifyLyricsSearchResult(results, trackName, artistName, durationSec)
|
||||
if best == nil || strings.TrimSpace(best.TrackID) == "" {
|
||||
return "", fmt.Errorf("no songs found on spotify")
|
||||
return "", lyricsNotFoundErrorf("no songs found on spotify")
|
||||
}
|
||||
return strings.TrimSpace(best.TrackID), nil
|
||||
}
|
||||
@@ -327,7 +335,7 @@ func (c *DeezerLyricsClient) FetchLyrics(spotifyID, trackName, artistName string
|
||||
if deezerID == "" {
|
||||
spotifyTrackID := normalizeSpotifyLyricsID(spotifyID)
|
||||
if spotifyTrackID == "" {
|
||||
return nil, fmt.Errorf("deezer provider needs a deezer id or spotify id")
|
||||
return nil, lyricsNotFoundErrorf("deezer provider needs a deezer id or spotify id")
|
||||
}
|
||||
resolvedID, err := NewSongLinkClient().GetDeezerIDFromSpotify(spotifyTrackID)
|
||||
if err != nil {
|
||||
@@ -344,7 +352,7 @@ func (c *DeezerLyricsClient) FetchLyrics(spotifyID, trackName, artistName string
|
||||
func (c *YouTubeLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) {
|
||||
query := strings.TrimSpace(trackName + " " + artistName)
|
||||
if query == "" {
|
||||
return "", fmt.Errorf("empty search query")
|
||||
return "", lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -360,7 +368,7 @@ func (c *YouTubeLyricsClient) SearchSong(trackName, artistName string, durationS
|
||||
}
|
||||
best := selectBestYouTubeLyricsSearchResult(results, trackName, artistName, durationSec)
|
||||
if best == nil || strings.TrimSpace(best.VideoID) == "" {
|
||||
return "", fmt.Errorf("no songs found on youtube")
|
||||
return "", lyricsNotFoundErrorf("no songs found on youtube")
|
||||
}
|
||||
return strings.TrimSpace(best.VideoID), nil
|
||||
}
|
||||
@@ -401,7 +409,7 @@ func (c *YouTubeLyricsClient) FetchLyrics(trackName, artistName string, duration
|
||||
func (c *KugouLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) {
|
||||
query := strings.TrimSpace(trackName + " " + artistName)
|
||||
if query == "" {
|
||||
return "", fmt.Errorf("empty search query")
|
||||
return "", lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -417,7 +425,7 @@ func (c *KugouLyricsClient) SearchSong(trackName, artistName string, durationSec
|
||||
}
|
||||
best := selectBestKugouLyricsSearchResult(results, trackName, artistName, durationSec)
|
||||
if best == nil || strings.TrimSpace(best.Hash) == "" {
|
||||
return "", fmt.Errorf("no songs found on kugou")
|
||||
return "", lyricsNotFoundErrorf("no songs found on kugou")
|
||||
}
|
||||
return strings.TrimSpace(best.Hash), nil
|
||||
}
|
||||
@@ -458,7 +466,7 @@ func (c *KugouLyricsClient) FetchLyrics(trackName, artistName string, durationSe
|
||||
func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) {
|
||||
query := strings.TrimSpace(trackName + " " + artistName)
|
||||
if query == "" {
|
||||
return "", fmt.Errorf("empty search query")
|
||||
return "", lyricsNotFoundErrorf("empty search query")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
@@ -495,7 +503,7 @@ func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSe
|
||||
}
|
||||
|
||||
if bestURL == "" {
|
||||
return "", fmt.Errorf("no songs found on genius")
|
||||
return "", lyricsNotFoundErrorf("no songs found on genius")
|
||||
}
|
||||
return bestURL, nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (c *QQMusicClient) fetchLyricsByMetadata(trackName, artistName string, dura
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("qqmusic lyrics proxy returned HTTP %d", resp.StatusCode)
|
||||
return "", lyricsHTTPStatusError(resp.StatusCode, "qqmusic lyrics proxy returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
@@ -101,7 +101,7 @@ func (c *QQMusicClient) FetchLyrics(
|
||||
return nil, err
|
||||
}
|
||||
if errMsg, isErrorPayload := detectLyricsErrorPayload(rawLyrics); isErrorPayload {
|
||||
return nil, fmt.Errorf("qqmusic proxy returned non-lyric payload: %s", errMsg)
|
||||
return nil, classifyLyricsPayloadError(0, errMsg, "qqmusic proxy returned non-lyric payload: %s", errMsg)
|
||||
}
|
||||
|
||||
lrcText, err := formatQQLyricsMetadataToLRC(rawLyrics, multiPersonWordByWord)
|
||||
@@ -134,5 +134,5 @@ func (c *QQMusicClient) FetchLyrics(
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no lyrics found on qqmusic")
|
||||
return nil, lyricsNotFoundErrorf("no lyrics found on qqmusic")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package gobackend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
@@ -61,8 +62,23 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) {
|
||||
if msg, ok := detectLyricsErrorPayload(`{"code":405,"message":"rate limited"}`); !ok || msg != "rate limited" {
|
||||
t.Fatalf("coded error payload = %q/%v", msg, ok)
|
||||
}
|
||||
if !isLyricsProviderUnavailableError(errors.New("rate limit")) {
|
||||
t.Fatal("expected rate-limit errors to mark provider unavailable")
|
||||
if !isLyricsProviderUnavailableError(classifyLyricsPayloadError(0, "rate limit", "proxy error: %s", "rate limit")) {
|
||||
t.Fatal("expected rate-limit payloads to mark provider unavailable")
|
||||
}
|
||||
if !isLyricsProviderUnavailableError(fmt.Errorf("spotify search failed: %w", lyricsServiceUnavailableErrorf("HTTP 503"))) {
|
||||
t.Fatal("expected wrapped unavailable errors to keep their classification")
|
||||
}
|
||||
if !isLyricsProviderUnavailableError(lyricsHTTPStatusError(503, "proxy returned HTTP 503")) {
|
||||
t.Fatal("expected 5xx statuses to mark provider unavailable")
|
||||
}
|
||||
if isLyricsProviderUnavailableError(lyricsHTTPStatusError(403, "proxy returned HTTP 403")) {
|
||||
t.Fatal("4xx statuses other than 429 must not mark provider unavailable")
|
||||
}
|
||||
if isLyricsProviderUnavailableError(classifyLyricsPayloadError(500, "lyrics not found", "HTTP 500: lyrics not found")) {
|
||||
t.Fatal("not-found payloads must never mark provider unavailable")
|
||||
}
|
||||
if isLyricsProviderUnavailableError(errors.New("rate limit")) {
|
||||
t.Fatal("untyped errors must not be classified by message text")
|
||||
}
|
||||
if lrcTimestampToMs("01", "02", "345") != 62345 || msToLRCTimestamp(62340) != "[01:02.34]" {
|
||||
t.Fatal("unexpected LRC timestamp conversion")
|
||||
|
||||
Reference in New Issue
Block a user