fix(lyrics): avoid derived TTML agent id collisions

This commit is contained in:
ranokay 2026-03-21 00:32:01 +02:00
parent d6a684e60e
commit 177e7089ee
No known key found for this signature in database
4 changed files with 49 additions and 10 deletions

View File

@ -52,7 +52,7 @@ A share of the revenue helps fund the development of Navidrome at no additional
- **Multi-platform**, runs on macOS, Linux and Windows. **Docker** images are also provided
- Ready to use binaries for all major platforms, including **Raspberry Pi**
- Automatically **monitors your library** for changes, importing new files and reloading new metadata
- Supports synchronized lyrics from sidecar **.lrc** and **.ttml** files (via `lyricspriority`)
- Supports lyrics from sidecar **.ttml**, **.elrc**, **.lrc**, **.srt**, **.txt** files and embedded tags (via `lyricspriority`)
- **Themeable**, modern and responsive **Web interface** based on [Material UI](https://material-ui.com)
- **Compatible** with all Subsonic/Madsonic/Airsonic [clients](https://www.navidrome.org/docs/overview/#apps)
- **Transcoding** on the fly. Can be set per user/player. **Opus encoding is supported**

View File

@ -24,6 +24,7 @@ const (
ttmlLyricKindMain = "main"
ttmlLyricKindTranslation = "translation"
ttmlLyricKindPronunciation = "pronunciation"
ttmlBackgroundAgentPrefix = "__nd_bg__|"
)
var offsetTimeRegex = regexp.MustCompile(`^([0-9]+(?:\.[0-9]+)?)(h|m|s|ms|f|t)$`)
@ -623,7 +624,7 @@ func (p *ttmlParser) baseRoleForAgent(agentID string) string {
func (p *ttmlParser) agentNameForID(agentID string) string {
if isBackgroundAgentID(agentID) {
baseID := strings.TrimSuffix(agentID, "__bg")
baseID := strings.TrimPrefix(agentID, ttmlBackgroundAgentPrefix)
if baseID == "main" {
return ""
}
@ -641,11 +642,11 @@ func (p *ttmlParser) agentNameForID(agentID string) string {
}
func backgroundAgentID(agentID string) string {
return agentID + "__bg"
return ttmlBackgroundAgentPrefix + agentID
}
func isBackgroundAgentID(agentID string) bool {
return strings.HasSuffix(agentID, "__bg")
return strings.HasPrefix(agentID, ttmlBackgroundAgentPrefix)
}
func contextHasRole(roles string, role string) bool {

View File

@ -131,7 +131,7 @@ var _ = Describe("parseTTML", func() {
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
{ID: "main", Role: "main"},
{ID: "main__bg", Role: "bg"},
{ID: "__nd_bg__|main", Role: "bg"},
}))
Expect(list[0].Line).To(HaveLen(1))
@ -143,7 +143,7 @@ var _ = Describe("parseTTML", func() {
Expect(line.Cue[0]).To(Equal(model.Cue{Start: gg.P(int64(1000)), End: gg.P(int64(1400)), Value: "He", AgentID: "main"}))
Expect(line.Cue[1]).To(Equal(model.Cue{Start: gg.P(int64(1400)), End: gg.P(int64(1800)), Value: "llo", AgentID: "main"}))
Expect(line.Cue[2]).To(Equal(model.Cue{Start: gg.P(int64(2000)), End: gg.P(int64(2500)), Value: "echo", AgentID: "main__bg"}))
Expect(line.Cue[2]).To(Equal(model.Cue{Start: gg.P(int64(2000)), End: gg.P(int64(2500)), Value: "echo", AgentID: "__nd_bg__|main"}))
})
It("should parse named TTML agents into main, voice, and group roles", func() {
@ -177,6 +177,44 @@ var _ = Describe("parseTTML", func() {
Expect(list[0].Line[1].Cue[0].AgentID).To(Equal("v2"))
Expect(list[0].Line[2].Cue[0].AgentID).To(Equal("v1000"))
})
It("should avoid collisions between derived background agents and explicit TTML agent ids", func() {
content := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata">
<head>
<metadata>
<ttm:agent xml:id="lead" type="person"><ttm:name>Lead</ttm:name></ttm:agent>
<ttm:agent xml:id="lead__bg" type="person"><ttm:name>Existing Background Id</ttm:name></ttm:agent>
</metadata>
</head>
<body xml:lang="eng">
<div>
<p begin="1s" end="2s" ttm:agent="lead">
<span begin="1s" end="1.4s">Lead</span>
<span ttm:role="x-bg"><span begin="1.5s" end="1.8s">Echo</span></span>
</p>
<p begin="2s" end="3s" ttm:agent="lead__bg">
<span begin="2s" end="2.5s">Named</span>
</p>
</div>
</body>
</tt>`)
list, err := parseTTML(content)
Expect(err).ToNot(HaveOccurred())
Expect(list).To(HaveLen(1))
Expect(list[0].Agents).To(Equal([]model.Agent{
{ID: "lead", Role: "main", Name: "Lead"},
{ID: "__nd_bg__|lead", Role: "bg", Name: "Lead"},
{ID: "lead__bg", Role: "voice", Name: "Existing Background Id"},
}))
Expect(list[0].Line).To(HaveLen(2))
Expect(list[0].Line[0].Cue).To(HaveLen(2))
Expect(list[0].Line[0].Cue[0].AgentID).To(Equal("lead"))
Expect(list[0].Line[0].Cue[1].AgentID).To(Equal("__nd_bg__|lead"))
Expect(list[0].Line[1].Cue).To(HaveLen(1))
Expect(list[0].Line[1].Cue[0].AgentID).To(Equal("lead__bg"))
})
})
Describe("Ambiguous decimal timing", func() {

View File

@ -543,7 +543,7 @@ var _ = Describe("MediaRetrievalController", func() {
lyricsJson, err := json.Marshal(model.LyricList{
{
Lang: "eng",
Agents: []model.Agent{{ID: "lead", Role: "main"}, {ID: "lead__bg", Role: "bg"}},
Agents: []model.Agent{{ID: "lead", Role: "main"}, {ID: "__nd_bg__|lead", Role: "bg"}},
Synced: true,
Line: []model.Line{
{
@ -561,7 +561,7 @@ var _ = Describe("MediaRetrievalController", func() {
Start: &tokenStartB,
End: &tokenEndB,
Value: "echo",
AgentID: "lead__bg",
AgentID: "__nd_bg__|lead",
},
},
},
@ -591,7 +591,7 @@ var _ = Describe("MediaRetrievalController", func() {
Synced: true,
Agents: []responses.Agent{
{ID: "lead", Role: "main"},
{ID: "lead__bg", Role: "bg"},
{ID: "__nd_bg__|lead", Role: "bg"},
},
Line: []responses.Line{
{
@ -619,7 +619,7 @@ var _ = Describe("MediaRetrievalController", func() {
Start: &lineStart,
End: &lineEnd,
Value: "Hello echo",
AgentID: "lead__bg",
AgentID: "__nd_bg__|lead",
Cue: []responses.LyricCue{
{
Start: tokenStartB,