🏡 index : quine-global/quintodrome.git

package plugins

import (
	"context"

	"github.com/navidrome/navidrome/model"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("LyricsPlugin", Ordered, func() {
	var (
		lyricsManager *Manager
		provider      *LyricsPlugin
	)

	BeforeAll(func() {
		lyricsManager, _ = createTestManagerWithPlugins(nil,
			"test-lyrics"+PackageExtension,
			"test-metadata-agent"+PackageExtension,
		)

		p, ok := lyricsManager.LoadLyricsProvider("test-lyrics")
		Expect(ok).To(BeTrue())
		provider = p.(*LyricsPlugin)
	})

	Describe("LoadLyricsProvider", func() {
		It("returns a lyrics provider for a plugin with Lyrics capability", func() {
			Expect(provider).ToNot(BeNil())
		})

		It("returns false for a plugin without Lyrics capability", func() {
			_, ok := lyricsManager.LoadLyricsProvider("test-metadata-agent")
			Expect(ok).To(BeFalse())
		})

		It("returns false for non-existent plugin", func() {
			_, ok := lyricsManager.LoadLyricsProvider("non-existent")
			Expect(ok).To(BeFalse())
		})
	})

	Describe("GetLyrics", func() {
		It("successfully returns lyrics from the plugin", func() {
			track := &model.MediaFile{
				ID:     "track-1",
				Title:  "Test Song",
				Artist: "Test Artist",
			}

			result, err := provider.GetLyrics(GinkgoT().Context(), track)
			Expect(err).ToNot(HaveOccurred())
			Expect(result).To(HaveLen(1))
			Expect(result[0].Line).ToNot(BeEmpty())
			Expect(result[0].Line[0].Value).To(ContainSubstring("Test Song"))
		})

		It("defaults language to 'xxx' when plugin does not provide one", func() {
			manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
				"test-lyrics": {"no_lang": "true"},
			}, "test-lyrics"+PackageExtension)

			p, ok := manager.LoadLyricsProvider("test-lyrics")
			Expect(ok).To(BeTrue())

			track := &model.MediaFile{ID: "track-1", Title: "Test Song", Artist: "Test Artist"}
			result, err := p.GetLyrics(GinkgoT().Context(), track)
			Expect(err).ToNot(HaveOccurred())
			Expect(result).To(HaveLen(1))
			Expect(result[0].Lang).To(Equal("xxx"))
		})

		It("blocks new calls while the per-plugin concurrency cap is saturated", func() {
			sem := provider.plugin.lyricsSem
			for range cap(sem) {
				sem <- struct{}{}
			}

			ctx := GinkgoT().Context()
			track := &model.MediaFile{ID: "track-1", Title: "Test Song", Artist: "Test Artist"}
			done := make(chan error, 1)
			go func() {
				_, err := provider.GetLyrics(ctx, track)
				done <- err
			}()

			Consistently(done, "500ms").ShouldNot(Receive())
			<-sem // free one slot; the pending call should now proceed
			Eventually(done).Should(Receive(BeNil()))
			for range cap(sem) - 1 {
				<-sem
			}
		})

		It("gives up waiting for a slot when the context is cancelled", func() {
			sem := provider.plugin.lyricsSem
			for range cap(sem) {
				sem <- struct{}{}
			}
			defer func() {
				for range cap(sem) {
					<-sem
				}
			}()

			ctx, cancel := context.WithCancel(GinkgoT().Context())
			cancel()
			_, err := provider.GetLyrics(ctx, &model.MediaFile{ID: "track-1"})
			Expect(err).To(MatchError(context.Canceled))
		})

		It("returns error when plugin returns error", func() {
			manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
				"test-lyrics": {"error": "service unavailable"},
			}, "test-lyrics"+PackageExtension)

			p, ok := manager.LoadLyricsProvider("test-lyrics")
			Expect(ok).To(BeTrue())

			track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
			_, err := p.GetLyrics(GinkgoT().Context(), track)
			Expect(err).To(HaveOccurred())
		})

		// Each DescribeTable entry proves that the adapter's content-sniffing routes
		// the plugin's rich payload to the right parser rather than mangling it as plain text.
		DescribeTable("content-sniffs plugin responses across all supported formats",
			func(format string, wantSynced bool, wantLine string) {
				manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
					"test-lyrics": {"format": format},
				}, "test-lyrics"+PackageExtension)

				p, ok := manager.LoadLyricsProvider("test-lyrics")
				Expect(ok).To(BeTrue())

				track := &model.MediaFile{ID: "track-1", Title: "Test Song", Artist: "Test Artist"}
				result, err := p.GetLyrics(GinkgoT().Context(), track)
				Expect(err).ToNot(HaveOccurred())
				Expect(result).To(HaveLen(1))
				Expect(result[0].Synced).To(Equal(wantSynced), "unexpected Synced value for format %s", format)
				Expect(result[0].Line).To(HaveLen(1))
				Expect(result[0].Line[0].Value).To(Equal(wantLine))
			},
			Entry("ttml", "ttml", true, "plugin ttml line"),
			Entry("srt", "srt", true, "plugin srt line"),
			Entry("yaml", "yaml", true, "plugin yaml line"),
			Entry("lrc", "lrc", true, "plugin lrc line"),
			Entry("plain", "plain", false, "plugin plain line"),
		)
	})

	Describe("PluginNames", func() {
		It("returns plugin names with Lyrics capability", func() {
			names := lyricsManager.PluginNames("Lyrics")
			Expect(names).To(ContainElement("test-lyrics"))
		})

		It("does not return metadata agent plugins for Lyrics capability", func() {
			names := lyricsManager.PluginNames("Lyrics")
			Expect(names).ToNot(ContainElement("test-metadata-agent"))
		})
	})
})