142 lines
3.3 KiB
Go
142 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// LyricsResponse represents the API response structure.
|
|
type LyricsResponse struct {
|
|
SyncedLyrics string `json:"syncedLyrics"`
|
|
PlainLyrics string `json:"plainLyrics"`
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
var (
|
|
errLyricsNotFound = errors.New("lyrics not found")
|
|
errLyricsTemporary = errors.New("temporary lyrics fetch failure")
|
|
lyricsHTTPClient = &http.Client{Timeout: 15 * time.Second}
|
|
)
|
|
|
|
type temporaryLyricsError struct {
|
|
Reason string
|
|
RetryAfter time.Duration
|
|
}
|
|
|
|
func (e *temporaryLyricsError) Error() string {
|
|
if e.RetryAfter > 0 {
|
|
return fmt.Sprintf("%s: retry after %s", e.Reason, e.RetryAfter)
|
|
}
|
|
|
|
return e.Reason
|
|
}
|
|
|
|
func (e *temporaryLyricsError) Unwrap() error {
|
|
return errLyricsTemporary
|
|
}
|
|
|
|
func fetchLyrics(metadata Metadata) (LyricsResponse, error) {
|
|
for attempt := 1; attempt <= 3; attempt++ {
|
|
lyrics, err := fetchLyricsOnce(metadata)
|
|
if err == nil {
|
|
return lyrics, nil
|
|
}
|
|
|
|
if !errors.Is(err, errLyricsTemporary) || attempt == 3 {
|
|
return LyricsResponse{}, err
|
|
}
|
|
|
|
retryDelay := time.Duration(attempt) * time.Second
|
|
var temporaryErr *temporaryLyricsError
|
|
if errors.As(err, &temporaryErr) && temporaryErr.RetryAfter > 0 {
|
|
retryDelay = temporaryErr.RetryAfter
|
|
}
|
|
|
|
time.Sleep(retryDelay)
|
|
}
|
|
|
|
return LyricsResponse{}, fmt.Errorf("lyrics fetch retries exhausted")
|
|
}
|
|
|
|
func fetchLyricsOnce(metadata Metadata) (LyricsResponse, error) {
|
|
apiURL := fmt.Sprintf("https://lrclib.net/api/get?track_name=%s&artist_name=%s&album_name=%s&duration=%d",
|
|
url.QueryEscape(metadata.TrackName),
|
|
url.QueryEscape(metadata.ArtistName),
|
|
url.QueryEscape(metadata.AlbumName),
|
|
metadata.Duration)
|
|
|
|
resp, err := lyricsHTTPClient.Get(apiURL)
|
|
if err != nil {
|
|
return LyricsResponse{}, &temporaryLyricsError{Reason: fmt.Sprintf("request failed: %v", err)}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return LyricsResponse{}, errLyricsNotFound
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= http.StatusInternalServerError {
|
|
retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
|
|
return LyricsResponse{}, &temporaryLyricsError{Reason: fmt.Sprintf("http status %d", resp.StatusCode), RetryAfter: retryAfter}
|
|
}
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return LyricsResponse{}, fmt.Errorf("lyrics API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return LyricsResponse{}, err
|
|
}
|
|
|
|
var lyricsResp LyricsResponse
|
|
err = json.Unmarshal(body, &lyricsResp)
|
|
if err != nil {
|
|
return LyricsResponse{}, err
|
|
}
|
|
|
|
if lyricsResp.Code == 404 {
|
|
return LyricsResponse{}, errLyricsNotFound
|
|
}
|
|
|
|
if lyricsResp.Code >= 500 {
|
|
return LyricsResponse{}, &temporaryLyricsError{Reason: fmt.Sprintf("api code %d", lyricsResp.Code)}
|
|
}
|
|
|
|
return lyricsResp, nil
|
|
}
|
|
|
|
func parseRetryAfter(value string) time.Duration {
|
|
if value == "" {
|
|
return 0
|
|
}
|
|
|
|
seconds, err := strconv.Atoi(value)
|
|
if err == nil {
|
|
if seconds < 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(seconds) * time.Second
|
|
}
|
|
|
|
retryTime, err := time.Parse(time.RFC1123, value)
|
|
if err != nil {
|
|
retryTime, err = time.Parse(time.RFC1123Z, value)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
if retryTime.Before(time.Now()) {
|
|
return 0
|
|
}
|
|
|
|
return time.Until(retryTime)
|
|
}
|