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...) }