refactor(lyrics): move pure format parsers from core/lyrics to model

Relocates the TTML, SRT, and embedded-dispatch parsers (pure text ->
model.LyricList transforms) into package model, alongside the existing
ToLyrics (LRC/ELRC) and ParseLyricsfile parsers.

This removes the model/metadata -> core/lyrics import edge (a lower layer
depending on business logic), which was the only reason model imported
core. core/lyrics now contains only the source-priority + plugin service,
which is consumed solely by server/subsonic.

parseTTML/parseSRT are exported as ParseTTML/ParseSRT so the remaining
core/lyrics service can still call them across the package boundary.
This commit is contained in:
Deluan 2026-06-18 19:18:27 -04:00
parent 85134673a2
commit 917f16a96d
8 changed files with 98 additions and 99 deletions

View File

@ -40,13 +40,13 @@ func fromExternalFile(ctx context.Context, mf *model.MediaFile, suffix string) (
var list model.LyricList
switch {
case strings.EqualFold(suffix, ".ttml"):
list, err = parseTTML(contents)
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 = parseSRT(contents)
list, err = model.ParseSRT(contents)
if err != nil {
log.Error(ctx, "error parsing srt external file", "path", externalLyric, err)
return nil, err

View File

@ -1,17 +1,16 @@
package lyrics
package model
import (
"encoding/xml"
"strings"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
// ParseEmbedded parses lyrics read from media-file metadata tags. It detects rich
// payloads before falling back to the generic LRC/plain-text parser, because
// text sanitization would otherwise strip TTML XML markup.
func ParseEmbedded(language, text string) (model.LyricList, error) {
func ParseEmbedded(language, text string) (LyricList, error) {
text = strings.TrimPrefix(text, "\ufeff")
if isTTMLDocument(text) {
@ -32,14 +31,14 @@ func ParseEmbedded(language, text string) (model.LyricList, error) {
log.Warn("Error parsing embedded SRT lyrics, falling back to plain lyrics", "error", err)
}
lyric, err := model.ToLyrics(language, text)
lyric, err := ToLyrics(language, text)
if err != nil {
return nil, err
}
if lyric == nil || lyric.IsEmpty() {
return nil, nil
}
return model.LyricList{*lyric}, nil
return LyricList{*lyric}, nil
}
func isTTMLDocument(text string) bool {

View File

@ -1,9 +1,8 @@
package lyrics
package model
import (
"strings"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -34,7 +33,7 @@ var _ = Describe("ParseEmbedded", func() {
Expect(list[0].Kind).To(Equal("main"))
Expect(list[0].Lang).To(Equal("eng"))
Expect(list[0].Synced).To(BeTrue())
Expect(list[0].Agents).To(Equal([]model.Agent{{ID: "lead", Role: "main", Name: "Lead Vocal"}}))
Expect(list[0].Agents).To(Equal([]Agent{{ID: "lead", Role: "main", Name: "Lead Vocal"}}))
Expect(list[0].Line).To(HaveLen(1))
Expect(list[0].Line[0].Start).To(Equal(ptr(int64(1000))))
Expect(list[0].Line[0].End).To(Equal(ptr(int64(3000))))
@ -100,10 +99,10 @@ Another subtitle line`
list, err := ParseEmbedded("POR", content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(Equal(model.LyricList{
Expect(list).To(Equal(LyricList{
{
Lang: "por",
Line: []model.Line{
Line: []Line{
{
Start: ptr(int64(18800)),
End: ptr(int64(22800)),
@ -127,7 +126,7 @@ Another subtitle line`
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Line).To(Equal([]model.Line{
Expect(list[0].Line).To(Equal([]Line{
{Start: ptr(int64(1000)), End: ptr(int64(2000)), Value: "First subtitle"},
{Start: ptr(int64(3000)), End: ptr(int64(4000)), Value: "Second subtitle"},
}))

View File

@ -1,4 +1,4 @@
package lyrics
package model
import (
"bytes"
@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/str"
)
@ -15,16 +14,16 @@ var (
srtBlockSeparatorRegex = regexp.MustCompile(`\n\s*\n`)
)
func parseSRT(contents []byte) (model.LyricList, error) {
func ParseSRT(contents []byte) (LyricList, error) {
return parseSRTWithLanguage(contents, "xxx")
}
func parseSRTWithLanguage(contents []byte, language string) (model.LyricList, error) {
func parseSRTWithLanguage(contents []byte, language string) (LyricList, error) {
raw := strings.ReplaceAll(string(contents), "\r\n", "\n")
raw = strings.ReplaceAll(raw, "\r", "\n")
blocks := splitSRTBlocks(raw)
lines := make([]model.Line, 0, len(blocks))
lines := make([]Line, 0, len(blocks))
for _, block := range blocks {
line, ok, err := parseSRTBlock(block)
@ -40,12 +39,12 @@ func parseSRTWithLanguage(contents []byte, language string) (model.LyricList, er
return nil, nil
}
lyrics := model.NormalizeLyrics(model.Lyrics{
lyrics := NormalizeLyrics(Lyrics{
Lang: normalizeEmbeddedLanguage(language),
Line: lines,
Synced: true,
})
return model.LyricList{lyrics}, nil
return LyricList{lyrics}, nil
}
func splitSRTBlocks(raw string) []string {
@ -65,10 +64,10 @@ func splitSRTBlocks(raw string) []string {
return blocks
}
func parseSRTBlock(block string) (model.Line, bool, error) {
func parseSRTBlock(block string) (Line, bool, error) {
scanner := bytes.Split([]byte(block), []byte("\n"))
if len(scanner) == 0 {
return model.Line{}, false, nil
return Line{}, false, nil
}
lines := make([]string, 0, len(scanner))
@ -77,7 +76,7 @@ func parseSRTBlock(block string) (model.Line, bool, error) {
}
if len(lines) == 0 {
return model.Line{}, false, nil
return Line{}, false, nil
}
startIdx := 0
@ -85,21 +84,21 @@ func parseSRTBlock(block string) (model.Line, bool, error) {
startIdx = 1
}
if startIdx >= len(lines) {
return model.Line{}, false, nil
return Line{}, false, nil
}
timing := strings.Split(lines[startIdx], "-->")
if len(timing) != 2 {
return model.Line{}, false, nil
return Line{}, false, nil
}
startMs, err := parseSRTTime(timing[0])
if err != nil {
return model.Line{}, false, err
return Line{}, false, err
}
endMs, err := parseSRTTime(timing[1])
if err != nil {
return model.Line{}, false, err
return Line{}, false, err
}
textLines := make([]string, 0, len(lines)-startIdx-1)
@ -112,10 +111,10 @@ func parseSRTBlock(block string) (model.Line, bool, error) {
value := str.SanitizeText(strings.Join(textLines, "\n"))
if value == "" {
return model.Line{}, false, nil
return Line{}, false, nil
}
return model.Line{
return Line{
Start: &startMs,
End: &endMs,
Value: value,

View File

@ -0,0 +1,5 @@
package model
func ptr[T any](v T) *T {
return &v
}

View File

@ -1,4 +1,4 @@
package lyrics
package model
import (
"bytes"
@ -13,7 +13,6 @@ import (
"unicode"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/str"
)
@ -58,19 +57,19 @@ type ttmlTimingContext struct {
type ttmlLineRef struct {
order int
line model.Line
line Line
}
type ttmlMetadataEntry struct {
key string
line model.Line
line Line
seq int
}
type ttmlResolvedMetadataLine struct {
order int
seq int
line model.Line
line Line
}
type ttmlDefinedAgent struct {
@ -81,7 +80,7 @@ type ttmlDefinedAgent struct {
type ttmlPiece struct {
raw string
cue *model.Cue
cue *Cue
}
type ttmlParser struct {
@ -89,7 +88,7 @@ type ttmlParser struct {
params ttmlTimingParams
mainLangOrder []string
mainLinesByLang map[string][]model.Line
mainLinesByLang map[string][]Line
mainLineRefsByKey map[string]ttmlLineRef
mainLineOrder int
@ -105,11 +104,11 @@ type ttmlParser struct {
metadataSeq int
}
func parseTTML(contents []byte) (model.LyricList, error) {
func ParseTTML(contents []byte) (LyricList, error) {
return parseTTMLWithDefaultLang(contents, "xxx")
}
func parseTTMLWithDefaultLang(contents []byte, defaultLang string) (model.LyricList, error) {
func parseTTMLWithDefaultLang(contents []byte, defaultLang string) (LyricList, error) {
contents = xmlEncodingRegex.ReplaceAll(contents, []byte(`<?xml$1encoding="UTF-8"$2?>`))
p := ttmlParser{
@ -119,7 +118,7 @@ func parseTTMLWithDefaultLang(contents []byte, defaultLang string) (model.LyricL
subFrameRate: defaultTTMLSubFrameRate,
tickRate: defaultTTMLTickRate,
},
mainLinesByLang: make(map[string][]model.Line),
mainLinesByLang: make(map[string][]Line),
mainLineRefsByKey: make(map[string]ttmlLineRef),
translationEntriesByLg: make(map[string][]ttmlMetadataEntry),
pronunciationEntriesByLg: make(map[string][]ttmlMetadataEntry),
@ -175,7 +174,7 @@ func (p *ttmlParser) parseElement(start xml.StartElement, parent ttmlTimingConte
return nil
}
parsedLine := model.Line{Value: lineText}
parsedLine := Line{Value: lineText}
if ctx.hasBegin {
startMs := ctx.begin
parsedLine.Start = &startMs
@ -318,7 +317,7 @@ func (p *ttmlParser) parseMetadataText(start xml.StartElement, parent ttmlTiming
}
value, tokens := buildTTMLLineFromPieces(pieces)
line := model.Line{Value: value}
line := Line{Value: value}
if ctx.hasBegin {
startMs := ctx.begin
line.Start = &startMs
@ -339,7 +338,7 @@ func (p *ttmlParser) parseMetadataText(start xml.StartElement, parent ttmlTiming
return ttmlMetadataEntry{key: forKey, line: line}, true, nil
}
func (p *ttmlParser) parseParagraph(parent ttmlTimingContext) (string, []model.Cue, error) {
func (p *ttmlParser) parseParagraph(parent ttmlTimingContext) (string, []Cue, error) {
var pieces []ttmlPiece
for {
@ -402,7 +401,7 @@ func (p *ttmlParser) parseInlineElement(start xml.StartElement, parent ttmlTimin
rawValue := concatTTMLPieceRaw(pieces)
tokenText := sanitizeTTMLText(rawValue)
if tokenText != "" {
parsedToken := model.Cue{
parsedToken := Cue{
AgentID: p.resolveCueAgentID(ctx),
}
if ctx.hasBegin {
@ -428,7 +427,7 @@ func (p *ttmlParser) parseInlineElement(start xml.StartElement, parent ttmlTimin
}
}
func buildTTMLLineFromPieces(pieces []ttmlPiece) (string, []model.Cue) {
func buildTTMLLineFromPieces(pieces []ttmlPiece) (string, []Cue) {
finalized := finalizeTTMLLines(splitTTMLPiecesByNewline(pieces))
for len(finalized) > 0 && finalized[0].text == "" && len(finalized[0].cues) == 0 {
finalized = finalized[1:]
@ -442,7 +441,7 @@ func buildTTMLLineFromPieces(pieces []ttmlPiece) (string, []model.Cue) {
}
var value strings.Builder
cues := make([]model.Cue, 0, 8)
cues := make([]Cue, 0, 8)
byteOffset := 0
for i, line := range finalized {
if i > 0 {
@ -463,7 +462,7 @@ func buildTTMLLineFromPieces(pieces []ttmlPiece) (string, []model.Cue) {
type ttmlFinalLine struct {
text string
cues []model.Cue
cues []Cue
}
func finalizeTTMLLines(lines [][]ttmlPiece) []ttmlFinalLine {
@ -507,7 +506,7 @@ func splitTTMLPiecesByNewline(pieces []ttmlPiece) [][]ttmlPiece {
return lines
}
func finalizeTTMLLogicalLine(line []ttmlPiece) (string, []model.Cue) {
func finalizeTTMLLogicalLine(line []ttmlPiece) (string, []Cue) {
rawLine := concatTTMLPieceRaw(line)
if rawLine == "" {
return "", nil
@ -521,7 +520,7 @@ func finalizeTTMLLogicalLine(line []ttmlPiece) (string, []model.Cue) {
}
trimmed := strings.TrimSpace(rawLine)
cues := make([]model.Cue, 0, len(line))
cues := make([]Cue, 0, len(line))
cursor := 0
for _, piece := range line {
pieceEnd := cursor + len(piece.raw)
@ -566,7 +565,7 @@ func ttmlPiecesContainCue(pieces []ttmlPiece) bool {
return false
}
func cloneTTMLCue(cue *model.Cue) *model.Cue {
func cloneTTMLCue(cue *Cue) *Cue {
if cue == nil {
return nil
}
@ -575,14 +574,14 @@ func cloneTTMLCue(cue *model.Cue) *model.Cue {
return &cloned
}
func (p *ttmlParser) toLyricList() model.LyricList {
res := make(model.LyricList, 0, len(p.mainLangOrder)+len(p.translationLangOrder)+len(p.pronunciationLangOrder))
func (p *ttmlParser) toLyricList() LyricList {
res := make(LyricList, 0, len(p.mainLangOrder)+len(p.translationLangOrder)+len(p.pronunciationLangOrder))
for _, lang := range p.mainLangOrder {
lines := p.mainLinesByLang[lang]
if len(lines) == 0 {
continue
}
res = append(res, p.finalizeLyrics(model.Lyrics{
res = append(res, p.finalizeLyrics(Lyrics{
Kind: ttmlLyricKindMain,
Lang: lang,
Line: lines,
@ -595,8 +594,8 @@ func (p *ttmlParser) toLyricList() model.LyricList {
return res
}
func (p *ttmlParser) buildMetadataLyrics(kind string, langOrder []string, entriesByLang map[string][]ttmlMetadataEntry) model.LyricList {
res := make(model.LyricList, 0, len(langOrder))
func (p *ttmlParser) buildMetadataLyrics(kind string, langOrder []string, entriesByLang map[string][]ttmlMetadataEntry) LyricList {
res := make(LyricList, 0, len(langOrder))
for _, lang := range langOrder {
entries := entriesByLang[lang]
@ -651,12 +650,12 @@ func (p *ttmlParser) buildMetadataLyrics(kind string, langOrder []string, entrie
return resolved[i].seq < resolved[j].seq
})
lines := make([]model.Line, len(resolved))
lines := make([]Line, len(resolved))
for i := range resolved {
lines[i] = resolved[i].line
}
res = append(res, p.finalizeLyrics(model.Lyrics{
res = append(res, p.finalizeLyrics(Lyrics{
Kind: kind,
Lang: lang,
Line: lines,
@ -667,12 +666,12 @@ func (p *ttmlParser) buildMetadataLyrics(kind string, langOrder []string, entrie
return res
}
func (p *ttmlParser) finalizeLyrics(lyrics model.Lyrics) model.Lyrics {
func (p *ttmlParser) finalizeLyrics(lyrics Lyrics) Lyrics {
lyrics.Line, lyrics.Agents = p.resolveAgents(lyrics.Line)
return model.NormalizeLyrics(lyrics)
return NormalizeLyrics(lyrics)
}
func (p *ttmlParser) resolveAgents(lines []model.Line) ([]model.Line, []model.Agent) {
func (p *ttmlParser) resolveAgents(lines []Line) ([]Line, []Agent) {
if len(lines) == 0 {
return lines, nil
}
@ -735,13 +734,13 @@ func (p *ttmlParser) resolveAgents(lines []model.Line) ([]model.Line, []model.Ag
}
}
agents := make([]model.Agent, 0, len(usedOrder))
agents := make([]Agent, 0, len(usedOrder))
for _, agentID := range usedOrder {
role := p.baseRoleForAgent(agentID)
if agentID == mainID {
role = "main"
}
agent := model.Agent{
agent := Agent{
ID: agentID,
Role: role,
Name: p.agentNameForID(agentID),
@ -830,7 +829,7 @@ func appendTTMLRoles(existing string, roles string) string {
return existing
}
func (p *ttmlParser) addMainLine(lang string, lineKey string, line model.Line) {
func (p *ttmlParser) addMainLine(lang string, lineKey string, line Line) {
lang = normalizeTTMLLang(lang)
if _, ok := p.mainLinesByLang[lang]; !ok {
p.mainLangOrder = append(p.mainLangOrder, lang)
@ -1252,7 +1251,7 @@ func sanitizeTTMLText(raw string) string {
return strings.TrimSpace(strings.Join(lines, "\n"))
}
func linesAreSynced(lines []model.Line) bool {
func linesAreSynced(lines []Line) bool {
for i := range lines {
if lines[i].Start != nil {
return true
@ -1266,8 +1265,8 @@ func linesAreSynced(lines []model.Line) bool {
return false
}
func hydrateLineTimingFromTokens(line model.Line) model.Line {
return model.NormalizeLineTiming(line)
func hydrateLineTimingFromTokens(line Line) Line {
return NormalizeLineTiming(line)
}
func positiveOrDefault(v float64, fallback float64) float64 {

View File

@ -1,12 +1,11 @@
package lyrics
package model
import (
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("parseTTML", func() {
var _ = Describe("ParseTTML", func() {
Describe("Multi-language and timing", func() {
It("should parse multiple language divs with inherited offsets and frame/tick timing", func() {
content := []byte(`<?xml version="1.0" encoding="UTF-8"?>
@ -22,7 +21,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(2))
@ -55,7 +54,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Line).To(HaveLen(1))
@ -76,7 +75,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Lang).To(Equal("eng"))
@ -100,7 +99,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Line).To(HaveLen(2))
@ -125,10 +124,10 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
Expect(list[0].Agents).To(Equal([]Agent{
{ID: "main", Role: "main"},
{ID: "__nd_bg__|main", Role: "bg"},
}))
@ -140,9 +139,9 @@ var _ = Describe("parseTTML", func() {
Expect(line.End).To(Equal(ptr(int64(3000))))
Expect(line.Cue).To(HaveLen(3))
Expect(line.Cue[0]).To(Equal(model.Cue{Start: ptr(int64(1000)), End: ptr(int64(1400)), Value: "He", ByteStart: 0, ByteEnd: 1, AgentID: "main"}))
Expect(line.Cue[1]).To(Equal(model.Cue{Start: ptr(int64(1400)), End: ptr(int64(1800)), Value: "llo", ByteStart: 2, ByteEnd: 4, AgentID: "main"}))
Expect(line.Cue[2]).To(Equal(model.Cue{Start: ptr(int64(2000)), End: ptr(int64(2500)), Value: "echo", ByteStart: 6, ByteEnd: 9, AgentID: "__nd_bg__|main"}))
Expect(line.Cue[0]).To(Equal(Cue{Start: ptr(int64(1000)), End: ptr(int64(1400)), Value: "He", ByteStart: 0, ByteEnd: 1, AgentID: "main"}))
Expect(line.Cue[1]).To(Equal(Cue{Start: ptr(int64(1400)), End: ptr(int64(1800)), Value: "llo", ByteStart: 2, ByteEnd: 4, AgentID: "main"}))
Expect(line.Cue[2]).To(Equal(Cue{Start: ptr(int64(2000)), End: ptr(int64(2500)), Value: "echo", ByteStart: 6, ByteEnd: 9, AgentID: "__nd_bg__|main"}))
})
It("should append role tokens exactly instead of using substring matches", func() {
@ -155,11 +154,11 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
Expect(list[0].Agents).To(Equal([]Agent{
{ID: "main", Role: "main"},
{ID: "__nd_bg__|main", Role: "bg"},
}))
@ -188,10 +187,10 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
Expect(list[0].Agents).To(Equal([]Agent{
{ID: "v1", Role: "main", Name: "Chris Martin"},
{ID: "v2", Role: "voice", Name: "Jin"},
{ID: "v1000", Role: "group", Name: "All"},
@ -223,10 +222,10 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
Expect(list[0].Agents).To(Equal([]Agent{
{ID: "lead", Role: "main", Name: "Lead"},
{ID: "__nd_bg__|lead", Role: "bg", Name: "Lead"},
{ID: "lead__bg", Role: "voice", Name: "Existing Background Id"},
@ -257,10 +256,10 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
Expect(list[0].Agents).To(Equal([]Agent{
{ID: "guest", Role: "main", Name: "Guest Vocal"},
}))
Expect(list[0].Line).To(HaveLen(1))
@ -284,7 +283,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Line).To(HaveLen(1))
@ -294,8 +293,8 @@ var _ = Describe("parseTTML", func() {
Expect(line.Value).To(Equal("go\ngo"))
Expect(line.End).To(Equal(ptr(int64(45570))))
Expect(line.Cue).To(HaveLen(2))
Expect(line.Cue[0]).To(Equal(model.Cue{Start: ptr(int64(43444)), End: ptr(int64(43716)), Value: "go", ByteStart: 0, ByteEnd: 1}))
Expect(line.Cue[1]).To(Equal(model.Cue{Start: ptr(int64(43716)), End: ptr(int64(43887)), Value: "go", ByteStart: 3, ByteEnd: 4}))
Expect(line.Cue[0]).To(Equal(Cue{Start: ptr(int64(43444)), End: ptr(int64(43716)), Value: "go", ByteStart: 0, ByteEnd: 1}))
Expect(line.Cue[1]).To(Equal(Cue{Start: ptr(int64(43716)), End: ptr(int64(43887)), Value: "go", ByteStart: 3, ByteEnd: 4}))
})
})
@ -310,7 +309,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Lang).To(Equal("xxx"))
@ -350,7 +349,7 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(3))
@ -378,8 +377,8 @@ var _ = Describe("parseTTML", func() {
Expect(pronunciation.Line[0].Value).To(Equal("konni"))
Expect(pronunciation.Line[0].End).To(Equal(ptr(int64(2600))))
Expect(pronunciation.Line[0].Cue).To(HaveLen(2))
Expect(pronunciation.Line[0].Cue[0]).To(Equal(model.Cue{Start: ptr(int64(2000)), End: ptr(int64(2300)), Value: "ko", ByteStart: 0, ByteEnd: 1}))
Expect(pronunciation.Line[0].Cue[1]).To(Equal(model.Cue{Start: ptr(int64(2300)), End: ptr(int64(2600)), Value: "nni", ByteStart: 2, ByteEnd: 4}))
Expect(pronunciation.Line[0].Cue[0]).To(Equal(Cue{Start: ptr(int64(2000)), End: ptr(int64(2300)), Value: "ko", ByteStart: 0, ByteEnd: 1}))
Expect(pronunciation.Line[0].Cue[1]).To(Equal(Cue{Start: ptr(int64(2300)), End: ptr(int64(2600)), Value: "nni", ByteStart: 2, ByteEnd: 4}))
})
})
@ -405,10 +404,10 @@ var _ = Describe("parseTTML", func() {
</body>
</tt>`)
list, err := parseTTML(content)
list, err := ParseTTML(content)
Expect(err).ToNot(HaveOccurred())
var pronunciation *model.Lyrics
var pronunciation *Lyrics
for i := range list {
if list[i].Kind == "pronunciation" {
pronunciation = &list[i]
@ -422,9 +421,9 @@ var _ = Describe("parseTTML", func() {
Expect(line.Start).To(Equal(ptr(int64(2747))))
Expect(line.Value).To(Equal("I woke up"))
Expect(line.Cue).To(HaveLen(3))
Expect(line.Cue[0]).To(Equal(model.Cue{Start: ptr(int64(2747)), End: ptr(int64(3018)), Value: "I", ByteStart: 0, ByteEnd: 0}))
Expect(line.Cue[1]).To(Equal(model.Cue{Start: ptr(int64(3018)), End: ptr(int64(3179)), Value: "woke", ByteStart: 2, ByteEnd: 5}))
Expect(line.Cue[2]).To(Equal(model.Cue{Start: ptr(int64(3179)), End: ptr(int64(3582)), Value: "up", ByteStart: 7, ByteEnd: 8}))
Expect(line.Cue[0]).To(Equal(Cue{Start: ptr(int64(2747)), End: ptr(int64(3018)), Value: "I", ByteStart: 0, ByteEnd: 0}))
Expect(line.Cue[1]).To(Equal(Cue{Start: ptr(int64(3018)), End: ptr(int64(3179)), Value: "woke", ByteStart: 2, ByteEnd: 5}))
Expect(line.Cue[2]).To(Equal(Cue{Start: ptr(int64(3179)), End: ptr(int64(3582)), Value: "up", ByteStart: 7, ByteEnd: 8}))
})
})
})

View File

@ -8,7 +8,6 @@ import (
"strconv"
"github.com/navidrome/navidrome/conf"
lyricssvc "github.com/navidrome/navidrome/core/lyrics"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/str"
@ -144,7 +143,7 @@ func (md Metadata) mapLyrics() string {
lang := raw.Key()
text := raw.Value()
lyrics, err := lyricssvc.ParseEmbedded(lang, text)
lyrics, err := model.ParseEmbedded(lang, text)
if err != nil {
log.Warn("Unexpected failure occurred when parsing lyrics", "file", md.filePath, err)
continue