mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(lyrics): consolidate lyric file parsing into a single function
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
c8c86b5bfe
commit
60377c9485
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user