refactor(lyrics): consolidate lyric file parsing into a single function

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-06-18 22:15:02 -04:00
parent c8c86b5bfe
commit 60377c9485
6 changed files with 45 additions and 62 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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
}

View File

@ -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,
})

View File

@ -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

View File

@ -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
}