first commit
This commit is contained in:
141
lyrics_api.go
Normal file
141
lyrics_api.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user