navidrome/server/jellyfin/e2e/lyrics_test.go
Deluan Quintão 29f481cd7b
feat(jellyfin): lyrics endpoint and Lyric stream advertising (#5791)
* feat(jellyfin): add LyricDto and lyrics mapper

* feat(jellyfin): advertise Lyric media stream for embedded lyrics

* feat(jellyfin): implement GET /Audio/{itemId}/Lyrics

* feat(jellyfin): advertise pipeline-resolved lyrics in PlaybackInfo

* feat(jellyfin): advertise server version 10.9.11 for client lyrics gates

* test(jellyfin): e2e coverage for lyrics endpoint and advertising

Seeds "Stairway To Heaven" with an embedded LRC lyric tag (lyrics:eng)
and covers PlaybackInfo's Lyric MediaStream, GET /Audio/{id}/Lyrics,
and the HasLyrics badge end to end.

Fixes a bug the new seed exposed: HasLyrics and the Lyric MediaStream
gate compared mf.Lyrics against "", but the persistence layer never
stores an empty string post-scan (it normalizes to the JSON sentinel
"[]"), so every track was reporting HasLyrics=true. Both call sites
now parse the column via StructuredLyrics()/LyricList.Main() instead.

* fix(jellyfin): cheap sentinel check for embedded lyrics advertising

* chore(jellyfin): trim over-budget comments in lyrics code

* test(jellyfin): cover lyrics pipeline error and nil-start cue skip

* docs(jellyfin): document lyrics support and follow-ups in README

* refactor(jellyfin): promote embedded-lyrics sentinel check to MediaFile

The "[]" no-lyrics sentinel is persistence-layer knowledge; expose it as
model.MediaFile.HasEmbeddedLyrics() instead of a dto-local helper. Also
dedupe the test lyrics-cache construction and pre-size the media stream
slice.

* refactor(jellyfin): consolidate tick conversions around one constant

ticksPerMillis is now the single source of the 100ns-tick unit; the
scrobble handlers' three inline /10_000 divisions become
dto.MillisFromTicks.

* fix(jellyfin): align lyric advertising with the serving predicate

PlaybackInfo advertised on any non-empty LyricList while the endpoint
404s when the main lyric has no lines; both now share servableLyric.
Handler tests also send hex-encoded ids to match real traffic.

* chore(jellyfin): drop unneeded lyrics package alias in e2e suite
2026-07-16 14:39:11 -04:00

72 lines
2.2 KiB
Go

package e2e
import (
"net/http"
"github.com/navidrome/navidrome/server/jellyfin/dto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Lyrics", func() {
BeforeEach(func() { setupTestDB() })
Describe("PlaybackInfo", func() {
It("advertises a Lyric stream for a track with embedded lyrics", func() {
id := songID("Stairway To Heaven")
var info dto.PlaybackInfoResponse
parseInto(get("/Items/"+enc(id)+"/PlaybackInfo"), &info)
var found bool
for _, s := range info.MediaSources[0].MediaStreams {
if s.Type == "Lyric" {
found = true
}
}
Expect(found).To(BeTrue())
})
It("does not advertise a Lyric stream for a track without lyrics", func() {
id := songID("So What")
var info dto.PlaybackInfoResponse
parseInto(get("/Items/"+enc(id)+"/PlaybackInfo"), &info)
for _, s := range info.MediaSources[0].MediaStreams {
Expect(s.Type).ToNot(Equal("Lyric"))
}
})
})
Describe("GET /Audio/{id}/Lyrics", func() {
It("returns the LyricDto for a track with embedded synced lyrics", func() {
id := songID("Stairway To Heaven")
var lyrics dto.LyricDto
parseInto(get("/Audio/"+enc(id)+"/Lyrics"), &lyrics)
Expect(lyrics.Lyrics).To(HaveLen(2))
Expect(lyrics.Lyrics[0].Text).To(Equal("There's a lady who's sure"))
Expect(lyrics.Lyrics[0].Start).ToNot(BeNil())
Expect(*lyrics.Lyrics[0].Start).To(Equal(int64(10000000)))
Expect(lyrics.Metadata.IsSynced).To(BeTrue())
})
It("returns 404 for a track without lyrics", func() {
id := songID("So What")
Expect(get("/Audio/" + enc(id) + "/Lyrics").Code).To(Equal(http.StatusNotFound))
})
It("returns 404 for a fabricated id", func() {
Expect(get("/Audio/" + enc("nope") + "/Lyrics").Code).To(Equal(http.StatusNotFound))
})
})
Describe("HasLyrics badge", func() {
It("is true for a track with embedded lyrics and omitted/false otherwise", func() {
var stairway dto.BaseItemDto
parseInto(get("/Items/"+enc(songID("Stairway To Heaven"))), &stairway)
Expect(stairway.HasLyrics).To(BeTrue())
var soWhat dto.BaseItemDto
parseInto(get("/Items/"+enc(songID("So What"))), &soWhat)
Expect(soWhat.HasLyrics).To(BeFalse())
})
})
})