package model
import (
"bytes"
"context"
"fmt"
"slices"
"strings"
"github.com/navidrome/navidrome/log"
)
type lyricParser func(lang string, contents []byte) (LyricList, error)
var lyricFormats = []struct {
suffixes []string
parse lyricParser
}{
{[]string{".ttml"}, parseTTML},
{[]string{".srt"}, parseSRT},
{[]string{".yaml", ".yml"}, parseLyricsfile},
}
func ParseLyrics(ctx context.Context, suffix, lang string, contents []byte) (LyricList, error) {
contents = stripBOM(contents)
suffix = strings.ToLower(suffix)
sniff := suffix == "" || suffix == "auto"
candidates := make([]lyricParser, 0, len(lyricFormats))
for _, f := range lyricFormats {
if sniff || slices.Contains(f.suffixes, suffix) {
candidates = append(candidates, f.parse)
}
}
return parseFirstMatch(ctx, sniff, lang, contents, candidates...)
}
func parseFirstMatch(ctx context.Context, sniff bool, lang string, contents []byte, candidates ...lyricParser) (LyricList, error) {
for _, parse := range candidates {
list, err := parse(lang, contents)
if err == nil && len(list) > 0 {
return list, nil
}
if err != nil {
if sniff {
log.Trace(ctx, "Lyrics probe did not match, trying next format", err)
} else {
log.Warn(ctx, "Error parsing lyrics, falling back to plain text", err)
}
}
}
return plainLRC(lang, contents)
}
func plainLRC(lang string, contents []byte) (LyricList, error) {
lyric, err := parseLRC(lang, string(contents))
if err != nil {
return nil, fmt.Errorf("parsing lyrics: %w", err)
}
if lyric == nil || lyric.IsEmpty() {
return nil, nil
}
return LyricList{*lyric}, nil
}
func stripBOM(contents []byte) []byte {
return bytes.TrimPrefix(contents, []byte("\ufeff"))
}