package lyrics
import (
"context"
"strings"
. "github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
)
const maxLegacyLyricsCandidates = 10
type Provider interface {
GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error)
}
type Lyrics interface {
Provider
GetLyricsByArtistTitle(ctx context.Context, artist, title string) (model.LyricList, error)
}
type PluginLoader interface {
LoadLyricsProvider(name string) (Provider, bool)
}
type lyricsService struct {
ds model.DataStore
pluginLoader PluginLoader
}
func NewLyrics(ds model.DataStore, pluginLoader PluginLoader) Lyrics {
return &lyricsService{ds: ds, pluginLoader: pluginLoader}
}
func (l *lyricsService) GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) {
return l.getLyricsForCandidates(ctx, []*model.MediaFile{mf})
}
func (l *lyricsService) GetLyricsByArtistTitle(ctx context.Context, artist, title string) (model.LyricList, error) {
opts := songsByArtistTitleWithLyricsFirst(artist, title)
opts.Max = maxLegacyLyricsCandidates
mediaFiles, err := l.ds.MediaFile().GetAll(ctx, opts)
if err != nil {
return nil, err
}
if len(mediaFiles) == 0 {
return nil, nil
}
candidates := make([]*model.MediaFile, 0, len(mediaFiles))
for i := range mediaFiles {
candidates = append(candidates, &mediaFiles[i])
}
return l.getLyricsForCandidates(ctx, candidates)
}
func songsByArtistTitleWithLyricsFirst(artist, title string) model.QueryOptions {
return model.QueryOptions{
Sort: "lyrics, updated_at",
Order: "desc",
Filters: And{
Eq{"missing": false},
Eq{"title": title},
Or{
persistence.Exists("json_tree(participants, '$.albumartist')", Eq{"value": artist}),
persistence.Exists("json_tree(participants, '$.artist')", Eq{"value": artist}),
},
},
}
}
func (l *lyricsService) getLyricsForCandidates(ctx context.Context, mediaFiles []*model.MediaFile) (model.LyricList, error) {
for pattern := range strings.SplitSeq(conf.Server.LyricsPriority, ",") {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
for _, mf := range mediaFiles {
if mf == nil {
continue
}
lyricsList, err := l.getLyricsFromSource(ctx, mf, pattern)
if err != nil {
log.Error(ctx, "error getting lyrics", "source", pattern, err)
continue
}
if len(lyricsList) > 0 {
return lyricsList, nil
}
}
}
return nil, nil
}
func (l *lyricsService) getLyricsFromSource(ctx context.Context, mf *model.MediaFile, pattern string) (model.LyricList, error) {
switch {
case strings.EqualFold(pattern, "embedded"):
return fromEmbedded(ctx, mf)
case strings.HasPrefix(pattern, "."):
return fromExternalFile(ctx, mf, pattern)
default:
return l.fromPlugin(ctx, mf, pattern)
}
}