navidrome/model/lyrics_parse.go
Deluan fd700f8d8a refactor(lyrics): fold parseLyricsfile into lyricParser signature and rename file
- parseLyricsfile now matches the lyricParser signature directly (reads via
  bytes.NewReader), removing the parseLyricsfileBytes adapter and the
  string(contents) copy; the lyricFormats table references it directly.
- StructuredLyrics drops the vestigial LyricList{} init (json.Unmarshal
  overwrites; MarshalJSON owns the empty->[] invariant).
- Rename lyricsfile.go -> lyrics_lyricsfile.go (and its test) to match the
  lyrics_<format>.go convention used by lrc/srt/ttml.
2026-06-19 16:50:46 -04:00

72 lines
2.3 KiB
Go

package model
import (
"bytes"
"fmt"
"slices"
"strings"
"github.com/navidrome/navidrome/log"
)
// lyricParser returns an empty list (not an error) when the input is not its
// format, so parsers can be tried in order. lang is the default for formats that
// do not carry their own.
type lyricParser func(lang string, contents []byte) (LyricList, error)
// lyricFormats is the structured formats in content-sniff probe order; each
// row's suffixes drive sidecar dispatch. LRC/plain is the unlisted fallback floor.
var lyricFormats = []struct {
suffixes []string
parse lyricParser
}{
{[]string{".ttml"}, parseTTMLWithDefaultLang},
{[]string{".srt"}, parseSRTWithLanguage},
{[]string{".yaml", ".yml"}, parseLyricsfile},
}
// ParseLyrics is the single entry point for parsing lyrics. A known suffix routes
// to that format's parser; an empty or "auto" suffix content-sniffs. Either way,
// a structured parser that does not match falls back to the LRC/plain-text floor.
func ParseLyrics(suffix, lang string, contents []byte) (LyricList, error) {
contents = stripBOM(contents)
sniff := suffix == "" || strings.EqualFold(suffix, "auto")
// Sniffing tries every format in order; a known suffix selects just its own.
// Unmatched suffixes leave no candidates, so parseFirstMatch falls to plain.
candidates := make([]lyricParser, 0, len(lyricFormats))
for _, f := range lyricFormats {
if sniff || slices.Contains(f.suffixes, strings.ToLower(suffix)) {
candidates = append(candidates, f.parse)
}
}
return parseFirstMatch(lang, contents, candidates...)
}
func parseFirstMatch(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 {
log.Warn("Error parsing lyrics, falling back to plain text", "error", 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"))
}