Files
SpotiFLAC-Mobile/go_backend/lyrics_errors.go
T
zarzet 270496b3d5 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.
2026-07-10 15:11:34 +07:00

101 lines
3.5 KiB
Go

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