diff --git a/model/lyrics.go b/model/lyrics.go index 46612d1e9..bf3936f46 100644 --- a/model/lyrics.go +++ b/model/lyrics.go @@ -55,15 +55,6 @@ const ( LyricKindPronunciation = "pronunciation" ) -// LyricKindOrMain returns kind, defaulting to LyricKindMain when empty. A blank -// kind means an untyped (single-track) lyric, which the contract treats as main. -func LyricKindOrMain(kind string) string { - if strings.TrimSpace(kind) == "" { - return LyricKindMain - } - return kind -} - // support the standard [mm:ss.mm], as well as [hh:*] and [*.mmm] const timeRegexString = `\[([0-9]{1,2}:)?([0-9]{1,2}):([0-9]{1,2})(\.[0-9]{1,3})?\]` @@ -82,6 +73,22 @@ func (l Lyrics) IsEmpty() bool { return len(l.Line) == 0 } +// IsMainKind reports whether the lyric is the main track. A blank kind is an +// untyped (single-track) lyric, which the contract treats as main. +func (l Lyrics) IsMainKind() bool { + return l.EffectiveKind() == LyricKindMain +} + +// EffectiveKind returns the lyric kind, defaulting to LyricKindMain when blank. +// A blank kind means an untyped (single-track) lyric, which the contract treats +// as main. +func (l Lyrics) EffectiveKind() string { + if strings.TrimSpace(l.Kind) == "" { + return LyricKindMain + } + return l.Kind +} + func ToLyrics(language, text string) (*Lyrics, error) { text = str.SanitizeText(text) @@ -434,6 +441,22 @@ func parseTime(line string, match []int) (int64, error) { type LyricList []Lyrics +// Main returns the main-kind lyric, falling back to the first entry so untyped +// lyrics still resolve. The bool is false only when the list is empty. It is +// used to surface a single lyric through the plain-text legacy getLyrics +// endpoint, which has no notion of translation/pronunciation tracks. +func (ll LyricList) Main() (Lyrics, bool) { + if len(ll) == 0 { + return Lyrics{}, false + } + for _, l := range ll { + if l.IsMainKind() { + return l, true + } + } + return ll[0], true +} + func NormalizeLyrics(lyrics Lyrics) Lyrics { lyrics.Line = NormalizeCueLines(lyrics.Line) if len(lyrics.Agents) == 0 { diff --git a/model/lyrics_test.go b/model/lyrics_test.go index abeaf4da2..cf7923e89 100644 --- a/model/lyrics_test.go +++ b/model/lyrics_test.go @@ -255,3 +255,66 @@ var _ = Describe("NormalizeCueLines", func() { Expect(lines[0].Cue[1].End).To(BeNil()) }) }) + +var _ = Describe("Lyrics.EffectiveKind", func() { + It("defaults a blank kind to main", func() { + Expect(Lyrics{}.EffectiveKind()).To(Equal(LyricKindMain)) + Expect(Lyrics{Kind: " "}.EffectiveKind()).To(Equal(LyricKindMain)) + }) + + It("returns the kind as-is when set", func() { + Expect(Lyrics{Kind: LyricKindTranslation}.EffectiveKind()).To(Equal(LyricKindTranslation)) + }) +}) + +var _ = Describe("Lyrics.IsMainKind", func() { + It("is true for a blank (untyped) kind", func() { + Expect(Lyrics{}.IsMainKind()).To(BeTrue()) + }) + + It("is true for the main kind", func() { + Expect(Lyrics{Kind: LyricKindMain}.IsMainKind()).To(BeTrue()) + }) + + It("is false for translation and pronunciation kinds", func() { + Expect(Lyrics{Kind: LyricKindTranslation}.IsMainKind()).To(BeFalse()) + Expect(Lyrics{Kind: LyricKindPronunciation}.IsMainKind()).To(BeFalse()) + }) +}) + +var _ = Describe("LyricList.Main", func() { + It("returns false when the list is empty", func() { + _, ok := LyricList{}.Main() + Expect(ok).To(BeFalse()) + }) + + It("returns the main-kind entry when present", func() { + list := LyricList{ + {Kind: LyricKindTranslation, Lang: "en"}, + {Kind: LyricKindMain, Lang: "xxx"}, + } + main, ok := list.Main() + Expect(ok).To(BeTrue()) + Expect(main.Kind).To(Equal(LyricKindMain)) + }) + + It("falls back to the first entry when no main kind exists", func() { + list := LyricList{ + {Kind: LyricKindTranslation, Lang: "en"}, + {Kind: LyricKindPronunciation, Lang: "ja"}, + } + main, ok := list.Main() + Expect(ok).To(BeTrue()) + Expect(main.Lang).To(Equal("en")) + }) + + It("treats a blank kind as main", func() { + list := LyricList{ + {Kind: LyricKindTranslation, Lang: "en"}, + {Lang: "xxx"}, + } + main, ok := list.Main() + Expect(ok).To(BeTrue()) + Expect(main.Lang).To(Equal("xxx")) + }) +}) diff --git a/server/subsonic/lyrics.go b/server/subsonic/lyrics.go index cd26908c3..ce3c3fae4 100644 --- a/server/subsonic/lyrics.go +++ b/server/subsonic/lyrics.go @@ -19,7 +19,7 @@ func buildLyricsList(mf *model.MediaFile, lyricsList model.LyricList, enhanced b // Without enhanced, only return main-kind entries (a blank kind is main). filtered = nil for _, l := range lyricsList { - if model.LyricKindOrMain(l.Kind) == model.LyricKindMain { + if l.IsMainKind() { filtered = append(filtered, l) } } @@ -32,21 +32,6 @@ func buildLyricsList(mf *model.MediaFile, lyricsList model.LyricList, enhanced b return &responses.LyricsList{StructuredLyrics: lyricList} } -// mainKindLyric returns the main-kind lyric to surface through the plain-text -// legacy getLyrics endpoint, which has no notion of translation/pronunciation -// tracks. It falls back to the first entry so untyped lyrics still resolve. -func mainKindLyric(lyricsList model.LyricList) (model.Lyrics, bool) { - if len(lyricsList) == 0 { - return model.Lyrics{}, false - } - for _, l := range lyricsList { - if model.LyricKindOrMain(l.Kind) == model.LyricKindMain { - return l, true - } - } - return lyricsList[0], true -} - func buildStructuredLyric(mf *model.MediaFile, lyrics model.Lyrics, enhanced bool) responses.StructuredLyric { agents := newLyricAgents(lyrics.Agents) @@ -70,7 +55,7 @@ func buildStructuredLyric(mf *model.MediaFile, lyrics model.Lyrics, enhanced boo } if enhanced { - structured.Kind = model.LyricKindOrMain(lyrics.Kind) + structured.Kind = lyrics.EffectiveKind() if len(cueLines) > 0 && len(agents.response) > 0 { structured.Agents = agents.response } diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index 3b029e672..9acda1929 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -119,7 +119,7 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) { return nil, err } - mainLyric, ok := mainKindLyric(structuredLyrics) + mainLyric, ok := structuredLyrics.Main() if !ok { return response, nil }