From 60377c9485d6213e2126ad90a24d80f5f797912a Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 18 Jun 2026 22:15:02 -0400 Subject: [PATCH] refactor(lyrics): consolidate lyric file parsing into a single function Signed-off-by: Deluan --- core/lyrics/sources.go | 34 ++++------------------------------ model/lyrics.go | 28 ++++++++++++++++++++++++++++ model/lyrics_embedded.go | 8 -------- model/lyrics_srt.go | 2 +- model/lyrics_ttml.go | 25 +++++++++++-------------- model/lyricsfile.go | 10 +--------- 6 files changed, 45 insertions(+), 62 deletions(-) diff --git a/core/lyrics/sources.go b/core/lyrics/sources.go index 18e826cc1..2962c6e5c 100644 --- a/core/lyrics/sources.go +++ b/core/lyrics/sources.go @@ -5,7 +5,6 @@ import ( "errors" "os" "path" - "strings" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -37,35 +36,10 @@ func fromExternalFile(ctx context.Context, mf *model.MediaFile, suffix string) ( return nil, err } - var list model.LyricList - switch { - case strings.EqualFold(suffix, ".ttml"): - list, err = model.ParseTTML(contents) - if err != nil { - log.Error(ctx, "error parsing ttml external file", "path", externalLyric, err) - return nil, err - } - case strings.EqualFold(suffix, ".srt"): - list, err = model.ParseSRT(contents) - if err != nil { - log.Error(ctx, "error parsing srt external file", "path", externalLyric, err) - return nil, err - } - case strings.EqualFold(suffix, ".yaml"), strings.EqualFold(suffix, ".yml"): - list, err = model.ParseLyricsfile(string(contents)) - if err != nil { - log.Error(ctx, "error parsing lyricsfile external file", "path", externalLyric, err) - return nil, err - } - default: - lyrics, err := model.ToLyrics("xxx", string(contents)) - if err != nil { - log.Error(ctx, "error parsing lyric external file", "path", externalLyric, err) - return nil, err - } - if lyrics != nil { - list = model.LyricList{*lyrics} - } + list, err := model.ParseLyricsFile(suffix, contents) + if err != nil { + log.Error(ctx, "error parsing external lyric file", "path", externalLyric, err) + return nil, err } if len(list) == 0 { diff --git a/model/lyrics.go b/model/lyrics.go index 7ba8410c9..bf3c87aa4 100644 --- a/model/lyrics.go +++ b/model/lyrics.go @@ -2,6 +2,7 @@ package model import ( "cmp" + "fmt" "regexp" "slices" "strconv" @@ -227,6 +228,33 @@ func ToLyrics(language, text string) (*Lyrics, error) { return &lyrics, nil } +// ParseLyricsFile parses a sidecar lyrics file, dispatching on its extension to +// the matching format parser. Unknown extensions fall back to the generic +// LRC/plain-text parser. It is the single owner of the suffix→parser mapping, +// mirroring [ParseEmbedded] for tag-embedded lyrics. +func ParseLyricsFile(suffix string, contents []byte) (LyricList, error) { + var list LyricList + var err error + switch { + case strings.EqualFold(suffix, ".ttml"): + list, err = ParseTTML(contents) + case strings.EqualFold(suffix, ".srt"): + list, err = ParseSRT(contents) + case strings.EqualFold(suffix, ".yaml"), strings.EqualFold(suffix, ".yml"): + list, err = ParseLyricsfile(string(contents)) + default: + var lyric *Lyrics + lyric, err = ToLyrics("xxx", string(contents)) + if lyric != nil { + list = LyricList{*lyric} + } + } + if err != nil { + return nil, fmt.Errorf("parsing %s lyrics: %w", strings.TrimPrefix(suffix, "."), err) + } + return list, nil +} + // parseEnhancedLine extracts word-level timing cues from Enhanced LRC inline markers // and computes UTF-8 byte offsets against the final stripped line value. func parseEnhancedLine(text string) (string, []Cue) { diff --git a/model/lyrics_embedded.go b/model/lyrics_embedded.go index b9af377e1..7b412556e 100644 --- a/model/lyrics_embedded.go +++ b/model/lyrics_embedded.go @@ -53,11 +53,3 @@ func isTTMLDocument(text string) bool { } } } - -func normalizeEmbeddedLanguage(language string) string { - language = strings.ToLower(strings.TrimSpace(language)) - if language == "" { - return "xxx" - } - return language -} diff --git a/model/lyrics_srt.go b/model/lyrics_srt.go index db6530ada..928fc45d9 100644 --- a/model/lyrics_srt.go +++ b/model/lyrics_srt.go @@ -40,7 +40,7 @@ func parseSRTWithLanguage(contents []byte, language string) (LyricList, error) { } lyrics := NormalizeLyrics(Lyrics{ - Lang: normalizeEmbeddedLanguage(language), + Lang: normalizeLyricLang(language), Line: lines, Synced: true, }) diff --git a/model/lyrics_ttml.go b/model/lyrics_ttml.go index 8593cce49..8b90f93b1 100644 --- a/model/lyrics_ttml.go +++ b/model/lyrics_ttml.go @@ -122,7 +122,7 @@ func parseTTMLWithDefaultLang(contents []byte, defaultLang string) (LyricList, e definedAgents: make(map[string]ttmlDefinedAgent), } - root := ttmlTimingContext{lang: normalizeTTMLLang(defaultLang)} + root := ttmlTimingContext{lang: normalizeLyricLang(defaultLang)} for { token, err := p.decoder.Token() @@ -183,7 +183,7 @@ func (p *ttmlParser) parseElement(start xml.StartElement, parent ttmlTimingConte if len(tokens) > 0 { parsedLine.Cue = tokens } - parsedLine = hydrateLineTimingFromTokens(parsedLine) + parsedLine = NormalizeLineTiming(parsedLine) lineKey, _ := attrValue(start.Attr, "key") p.addMainLine(ctx.lang, lineKey, parsedLine) @@ -217,7 +217,7 @@ func (p *ttmlParser) parseElement(start xml.StartElement, parent ttmlTimingConte func (p *ttmlParser) parseMetadataTrack(start xml.StartElement, parent ttmlTimingContext, kind string) error { ctx := p.childContext(start.Attr, parent) - lang := normalizeTTMLLang(ctx.lang) + lang := normalizeLyricLang(ctx.lang) for { token, err := p.decoder.Token() @@ -326,7 +326,7 @@ func (p *ttmlParser) parseMetadataText(start xml.StartElement, parent ttmlTiming if len(tokens) > 0 { line.Cue = tokens } - line = hydrateLineTimingFromTokens(line) + line = NormalizeLineTiming(line) if line.Value == "" && len(line.Cue) == 0 { return ttmlMetadataEntry{}, false, nil @@ -623,7 +623,7 @@ func (p *ttmlParser) buildMetadataLyrics(kind string, langOrder []string, entrie endMs := *ref.line.End line.End = &endMs } - line = hydrateLineTimingFromTokens(line) + line = NormalizeLineTiming(line) if line.Value == "" && len(line.Cue) == 0 { continue @@ -804,8 +804,9 @@ func isBackgroundAgentID(agentID string) bool { } func contextHasRole(roles string, role string) bool { + lowerRole := strings.ToLower(role) for _, candidate := range strings.Fields(strings.ToLower(roles)) { - if candidate == strings.ToLower(role) { + if candidate == lowerRole { return true } } @@ -827,7 +828,7 @@ func appendTTMLRoles(existing string, roles string) string { } func (p *ttmlParser) addMainLine(lang string, lineKey string, line Line) { - lang = normalizeTTMLLang(lang) + lang = normalizeLyricLang(lang) if _, ok := p.mainLinesByLang[lang]; !ok { p.mainLangOrder = append(p.mainLangOrder, lang) } @@ -846,7 +847,7 @@ func (p *ttmlParser) addMainLine(lang string, lineKey string, line Line) { } func (p *ttmlParser) addMetadataEntry(kind string, lang string, entry ttmlMetadataEntry) { - lang = normalizeTTMLLang(lang) + lang = normalizeLyricLang(lang) entry.seq = p.metadataSeq p.metadataSeq++ @@ -868,7 +869,7 @@ func (p *ttmlParser) childContext(attrs []xml.Attr, parent ttmlTimingContext) tt ctx := parent if lang, ok := attrValue(attrs, "lang"); ok { - ctx.lang = normalizeTTMLLang(lang) + ctx.lang = normalizeLyricLang(lang) } if agentID, ok := attrValue(attrs, "agent"); ok { ctx.agentID = strings.TrimSpace(agentID) @@ -1228,7 +1229,7 @@ func (p *ttmlParser) skipElement(_ xml.StartElement) error { return nil } -func normalizeTTMLLang(lang string) string { +func normalizeLyricLang(lang string) string { lang = strings.ToLower(strings.TrimSpace(lang)) if lang == "" { return "xxx" @@ -1262,10 +1263,6 @@ func linesAreSynced(lines []Line) bool { return false } -func hydrateLineTimingFromTokens(line Line) Line { - return NormalizeLineTiming(line) -} - func positiveOrDefault(v float64, fallback float64) float64 { if v <= 0 { return fallback diff --git a/model/lyricsfile.go b/model/lyricsfile.go index 1b3787d4a..b2b123256 100644 --- a/model/lyricsfile.go +++ b/model/lyricsfile.go @@ -34,7 +34,7 @@ func ParseLyricsfile(text string) (LyricList, error) { lyrics := Lyrics{ DisplayArtist: str.SanitizeText(doc.Metadata.Artist), DisplayTitle: str.SanitizeText(doc.Metadata.Title), - Lang: normalizeLyricsfileLang(doc.Metadata.Language), + Lang: normalizeLyricLang(doc.Metadata.Language), Kind: LyricKindMain, } if doc.Metadata.OffsetMs != 0 { @@ -274,11 +274,3 @@ func wordsToLineCues(entry lyricsfileLineEntry, agentID string) ([]Cue, string) } return cues, lineValue } - -func normalizeLyricsfileLang(language string) string { - language = strings.ToLower(strings.TrimSpace(language)) - if language == "" { - return "xxx" - } - return language -}