first commit

This commit is contained in:
2026-04-09 01:42:37 +02:00
commit 75ae131e2a
7 changed files with 1055 additions and 0 deletions

141
lyrics_api.go Normal file
View 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)
}