mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-12 20:37:12 +02:00
refactor(lyrics): self-skipping parsers collapse the format table to one column
Move the TTML <tt>-document gate into parseTTMLWithDefaultLang itself (after
the encoding fixup, so UTF-16-declared docs are still recognized): non-TTML
content returns (nil, nil) to skip; a malformed <tt> document still errors.
SRT and Lyricsfile YAML already self-skip. With every structured parser
self-skipping, the format table drops to one {suffixes, parse} column named
lyricFormats — no bySuffix/byContent split, no separate sniff-only TTML gate.
Both the suffix and content-sniff paths share the same parser per format.
This commit is contained in:
parent
8c10124b65
commit
630189f95f
2 changed files with 28 additions and 33 deletions
|
|
@ -19,49 +19,36 @@ func parseLyricsfileBytes(_ string, contents []byte) (LyricList, error) {
|
|||
return parseLyricsfile(string(contents))
|
||||
}
|
||||
|
||||
// parseTTMLIfDocument parses TTML only when the content actually looks like a
|
||||
// TTML document, so content-sniffing plain text or LRC does not run the XML
|
||||
// decoder (and never logs a spurious parse warning for non-TTML input).
|
||||
func parseTTMLIfDocument(lang string, contents []byte) (LyricList, error) {
|
||||
if !isTTMLDocument(string(contents)) {
|
||||
return nil, nil
|
||||
}
|
||||
return parseTTMLWithDefaultLang(lang, contents)
|
||||
}
|
||||
|
||||
// registry is the single source of truth for the structured formats. Slice
|
||||
// order is the content-sniff probe order (TTML → SRT → YAML), and each row's
|
||||
// suffixes drive sidecar dispatch. byContent is used when sniffing — it gates a
|
||||
// format so, e.g., TTML only runs the XML decoder on something that looks like a
|
||||
// document; bySuffix parses unconditionally because the file extension already
|
||||
// declares the format. LRC/plain is not listed: it is the fallback floor for
|
||||
// both paths.
|
||||
var registry = []struct {
|
||||
suffixes []string
|
||||
bySuffix lyricParser
|
||||
byContent lyricParser
|
||||
// lyricFormats lists the structured formats. Each parser is self-skipping: it
|
||||
// returns an empty list (not an error) when the content is not its format, so it
|
||||
// is safe to try in order. Slice order is the content-sniff probe order
|
||||
// (TTML → SRT → YAML); each row's suffixes drive sidecar dispatch. LRC/plain is
|
||||
// not listed — it is the fallback floor for both paths.
|
||||
var lyricFormats = []struct {
|
||||
suffixes []string
|
||||
parse lyricParser
|
||||
}{
|
||||
{[]string{".ttml"}, parseTTMLWithDefaultLang, parseTTMLIfDocument},
|
||||
{[]string{".srt"}, parseSRTWithLanguage, parseSRTWithLanguage},
|
||||
{[]string{".yaml", ".yml"}, parseLyricsfileBytes, parseLyricsfileBytes},
|
||||
{[]string{".ttml"}, parseTTMLWithDefaultLang},
|
||||
{[]string{".srt"}, parseSRTWithLanguage},
|
||||
{[]string{".yaml", ".yml"}, parseLyricsfileBytes},
|
||||
}
|
||||
|
||||
// ParseLyrics is the single entry point for parsing lyrics. A known suffix
|
||||
// (.ttml/.srt/.yaml/.yml/.lrc) routes to that format's parser; an empty or
|
||||
// "auto" suffix content-sniffs. In both modes a structured parser that does not
|
||||
// match falls back to the LRC/plain-text floor — never to another structured
|
||||
// format.
|
||||
// "auto" suffix content-sniffs by trying each format in order. In both modes a
|
||||
// structured parser that does not match falls back to the LRC/plain-text floor —
|
||||
// never to another structured format.
|
||||
func ParseLyrics(suffix, lang string, contents []byte) (LyricList, error) {
|
||||
if suffix == "" || strings.EqualFold(suffix, "auto") {
|
||||
candidates := make([]lyricParser, len(registry))
|
||||
for i, r := range registry {
|
||||
candidates[i] = r.byContent
|
||||
candidates := make([]lyricParser, len(lyricFormats))
|
||||
for i, f := range lyricFormats {
|
||||
candidates[i] = f.parse
|
||||
}
|
||||
return parseFirstMatch(lang, stripBOM(contents), candidates...)
|
||||
}
|
||||
for _, r := range registry {
|
||||
if slices.ContainsFunc(r.suffixes, func(s string) bool { return strings.EqualFold(s, suffix) }) {
|
||||
return parseFirstMatch(lang, contents, r.bySuffix)
|
||||
for _, f := range lyricFormats {
|
||||
if slices.ContainsFunc(f.suffixes, func(s string) bool { return strings.EqualFold(s, suffix) }) {
|
||||
return parseFirstMatch(lang, contents, f.parse)
|
||||
}
|
||||
}
|
||||
return plainLRC(lang, contents) // .lrc and any unknown suffix
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ func parseTTML(contents []byte) (LyricList, error) {
|
|||
func parseTTMLWithDefaultLang(defaultLang string, contents []byte) (LyricList, error) {
|
||||
contents = xmlEncodingRegex.ReplaceAll(contents, []byte(`<?xml$1encoding="UTF-8"$2?>`))
|
||||
|
||||
// Self-skip non-TTML content (returns nil, nil) so content-sniffing does not
|
||||
// run the XML decoder on plain text or LRC. Checked after the encoding fixup
|
||||
// above so a UTF-16-declared document is still recognized. Content that is a
|
||||
// <tt> document but malformed still parses below and surfaces its error.
|
||||
if !isTTMLDocument(string(contents)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
p := ttmlParser{
|
||||
decoder: xml.NewDecoder(bytes.NewReader(contents)),
|
||||
params: ttmlTimingParams{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue