From 177e7089ee8e8f6b469e93de45b0c87064281f35 Mon Sep 17 00:00:00 2001 From: ranokay Date: Sat, 21 Mar 2026 00:32:01 +0200 Subject: [PATCH] fix(lyrics): avoid derived TTML agent id collisions --- README.md | 2 +- core/lyrics/ttml.go | 7 +++-- core/lyrics/ttml_test.go | 42 +++++++++++++++++++++++-- server/subsonic/media_retrieval_test.go | 8 ++--- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6b9aff799..645f1580d 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/core/lyrics/ttml.go b/core/lyrics/ttml.go index e79dfe846..adbc0c054 100644 --- a/core/lyrics/ttml.go +++ b/core/lyrics/ttml.go @@ -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 { diff --git a/core/lyrics/ttml_test.go b/core/lyrics/ttml_test.go index 5fc484a3b..4e81197d4 100644 --- a/core/lyrics/ttml_test.go +++ b/core/lyrics/ttml_test.go @@ -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(` + + + + Lead + Existing Background Id + + + +
+

+ Lead + Echo +

+

+ Named +

+
+ +
`) + + 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() { diff --git a/server/subsonic/media_retrieval_test.go b/server/subsonic/media_retrieval_test.go index 5489492ce..e4f6a21d4 100644 --- a/server/subsonic/media_retrieval_test.go +++ b/server/subsonic/media_retrieval_test.go @@ -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,