Skip to content

Commit 2879598

Browse files
committed
feat(lyrics): add LRCLib support
closes #166
1 parent 5957b20 commit 2879598

File tree

2 files changed

+79
-3
lines changed

2 files changed

+79
-3
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,11 @@ For example:
517517
518518
## Addon Lyrics
519519
520-
The `lyrics` addon can fetch and embed lyric information from [Genius](https://genius.com/) and [Musixmatch](https://www.musixmatch.com/) in your tracks.
520+
The `lyrics` addon can fetch and embed lyric information from [LRCLib](https://lrclib.net/), [Genius](https://genius.com/), and [Musixmatch](https://www.musixmatch.com/) in your tracks.
521521
522-
The format of the addon config is `lyrics <source>...` where the source is one of `genius` or `musixmatch`. For example, `"lyrics genius musixmatch"`. Note that sources will be tried in the order they are specified.
522+
The format of the addon config is `lyrics <source>...` where the source is one of `lrclib`, `genius`, or `musixmatch`. For example, `"lyrics lrclib genius"`. Note that sources will be tried in the order they are specified.
523+
524+
LRCLib provides synchronized lyrics (LRC format) when available, which includes timestamps for karaoke-style display in compatible players. If synchronized lyrics are not available, plain lyrics will be used instead.
523525
524526
## Addon ReplayGain
525527

lyrics/lyrics.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Package lyrics provides functionality for fetching song lyrics from various sources.
2-
// It supports multiple lyrics providers including Genius and Musixmatch.
2+
// It supports multiple lyrics providers including LRCLib, Genius, and Musixmatch.
33
package lyrics
44

55
import (
66
"context"
7+
"encoding/json"
78
"errors"
89
"fmt"
910
"net/http"
@@ -27,6 +28,8 @@ func NewSource(name string, rateLimit time.Duration) (Source, error) {
2728
return &Genius{RateLimit: rateLimit}, nil
2829
case "musixmatch":
2930
return &Musixmatch{RateLimit: rateLimit}, nil
31+
case "lrclib":
32+
return &LRCLib{RateLimit: rateLimit}, nil
3033
default:
3134
return nil, errors.New("unknown source")
3235
}
@@ -192,3 +195,74 @@ func (ms MultiSource) String() string {
192195
}
193196
return strings.Join(parts, ", ")
194197
}
198+
199+
var lrclibBaseURL = `https://lrclib.net/api/get`
200+
201+
type LRCLib struct {
202+
RateLimit time.Duration
203+
204+
initOnce sync.Once
205+
HTTPClient *http.Client
206+
}
207+
208+
type lrclibResponse struct {
209+
ID int `json:"id"`
210+
Name string `json:"name"`
211+
TrackName string `json:"trackName"`
212+
ArtistName string `json:"artistName"`
213+
AlbumName string `json:"albumName"`
214+
Duration float64 `json:"duration"`
215+
Instrumental bool `json:"instrumental"`
216+
PlainLyrics string `json:"plainLyrics"`
217+
SyncedLyrics string `json:"syncedLyrics"`
218+
}
219+
220+
func (l *LRCLib) Search(ctx context.Context, artist, song string, duration time.Duration) (string, error) {
221+
l.initOnce.Do(func() {
222+
l.HTTPClient = clientutil.Wrap(l.HTTPClient, clientutil.Chain(
223+
clientutil.WithRateLimit(l.RateLimit),
224+
))
225+
})
226+
227+
u, _ := url.Parse(lrclibBaseURL)
228+
q := u.Query()
229+
q.Set("artist_name", artist)
230+
q.Set("track_name", song)
231+
if duration > 0 {
232+
q.Set("duration", fmt.Sprintf("%.0f", duration.Seconds()))
233+
}
234+
u.RawQuery = q.Encode()
235+
236+
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
237+
resp, err := l.HTTPClient.Do(req)
238+
if err != nil {
239+
return "", fmt.Errorf("req page: %w", err)
240+
}
241+
defer resp.Body.Close()
242+
243+
if resp.StatusCode == http.StatusNotFound {
244+
return "", ErrLyricsNotFound
245+
}
246+
if resp.StatusCode/100 != 2 {
247+
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
248+
}
249+
250+
var result lrclibResponse
251+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
252+
return "", fmt.Errorf("decode response: %w", err)
253+
}
254+
255+
// Prefer synced lyrics if available, otherwise fall back to plain lyrics
256+
if result.SyncedLyrics != "" {
257+
return result.SyncedLyrics, nil
258+
}
259+
if result.PlainLyrics != "" {
260+
return result.PlainLyrics, nil
261+
}
262+
263+
return "", ErrLyricsNotFound
264+
}
265+
266+
func (l *LRCLib) String() string {
267+
return "lrclib"
268+
}

0 commit comments

Comments
 (0)