diff --git a/plugins/lyrics_adapter.go b/plugins/lyrics_adapter.go index 5ad70be02..20479c333 100644 --- a/plugins/lyrics_adapter.go +++ b/plugins/lyrics_adapter.go @@ -9,7 +9,6 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/plugins/capabilities" - "golang.org/x/sync/singleflight" ) const CapabilityLyrics Capability = "Lyrics" @@ -41,7 +40,6 @@ func newLyricsPlugin(p *plugin) *LyricsPlugin { type LyricsPlugin struct { name string plugin *plugin - calls singleflight.Group } // GetLyrics coalesces concurrent lookups for the same track. The shared call is @@ -60,7 +58,7 @@ func (l *LyricsPlugin) GetLyrics(ctx context.Context, mf *model.MediaFile) (mode return nil, err } - result := l.calls.DoChan(key, func() (any, error) { + result := l.plugin.lyricsCalls.DoChan(key, func() (any, error) { callCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), lyricsPluginCallTimeout) defer cancel() return l.getLyrics(callCtx, mf, req) diff --git a/plugins/lyrics_adapter_test.go b/plugins/lyrics_adapter_test.go index b366a387e..dc273a2a1 100644 --- a/plugins/lyrics_adapter_test.go +++ b/plugins/lyrics_adapter_test.go @@ -66,11 +66,15 @@ var _ = Describe("LyricsPlugin", Ordered, func() { "test-lyrics"+PackageExtension, ) - p, ok := manager.LoadLyricsProvider("test-lyrics") + first, ok := manager.LoadLyricsProvider("test-lyrics") Expect(ok).To(BeTrue()) - coalescingProvider := p.(*LyricsPlugin) + second, ok := manager.LoadLyricsProvider("test-lyrics") + Expect(ok).To(BeTrue()) + firstProvider := first.(*LyricsPlugin) + secondProvider := second.(*LyricsPlugin) + Expect(firstProvider).ToNot(BeIdenticalTo(secondProvider)) - sem := coalescingProvider.plugin.lyricsSem + sem := firstProvider.plugin.lyricsSem for range cap(sem) { sem <- struct{}{} } @@ -87,10 +91,10 @@ var _ = Describe("LyricsPlugin", Ordered, func() { start := make(chan struct{}) results := make(chan callResult, 2) track := &model.MediaFile{ID: "shared-track", Title: "Test Song", Artist: "Test Artist"} - for range 2 { + for _, provider := range []*LyricsPlugin{firstProvider, secondProvider} { go func() { <-start - lyrics, err := coalescingProvider.GetLyrics(GinkgoT().Context(), track) + lyrics, err := provider.GetLyrics(GinkgoT().Context(), track) results <- callResult{lyrics: lyrics, err: err} }() } diff --git a/plugins/manager_plugin.go b/plugins/manager_plugin.go index 13375a70f..394b4c881 100644 --- a/plugins/manager_plugin.go +++ b/plugins/manager_plugin.go @@ -10,6 +10,7 @@ import ( extism "github.com/extism/go-sdk" "github.com/navidrome/navidrome/model" "github.com/tetratelabs/wazero" + "golang.org/x/sync/singleflight" ) // plugin represents a loaded plugin @@ -24,8 +25,9 @@ type plugin struct { allowedUserIDs []string // User IDs this plugin can access (from DB configuration) allUsers bool // If true, plugin can access all users libraries libraryAccess - lyricsSem chan struct{} // Caps concurrent lyrics calls (see LyricsPlugin.GetLyrics) - fsConfig wazero.FSConfig // Sandboxed library mounts, nil if no filesystem permission + lyricsSem chan struct{} // Caps concurrent lyrics calls (see LyricsPlugin.GetLyrics) + lyricsCalls singleflight.Group // Shared by the transient LyricsPlugin adapters + fsConfig wazero.FSConfig // Sandboxed library mounts, nil if no filesystem permission } // instanceConfig is used by every call site, so all instances get the sandboxed mounts.