mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-07 02:38:50 +01:00
Rename the capability from PlaylistGenerator to PlaylistProvider and the internal orchestrator struct from playlistGeneratorOrchestrator to playlistSyncer. The new names better describe what the capability does (provides playlists) rather than how it works internally. All source files, test plugin, PDK packages (Go/Rust), YAML schemas, and exported WASM function names are updated accordingly.
189 lines
6.2 KiB
Go
189 lines
6.2 KiB
Go
//go:build !windows
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/model/id"
|
|
"github.com/navidrome/navidrome/plugins/capabilities"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// findSyncer finds the playlistSyncer in a plugin's closers.
|
|
func findSyncer(m *Manager, pluginName string) *playlistSyncer {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
p, ok := m.plugins[pluginName]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for _, c := range p.closers {
|
|
if syncer, ok := c.(*playlistSyncer); ok {
|
|
return syncer
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ = Describe("PlaylistProvider", Ordered, func() {
|
|
var (
|
|
pgManager *Manager
|
|
mockPlsRepo *tests.MockPlaylistRepo
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
pgManager, _ = createTestManagerWithPlugins(nil,
|
|
"test-playlist-provider"+PackageExtension,
|
|
)
|
|
|
|
mockPlsRepo = pgManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
|
|
})
|
|
|
|
Describe("capability detection", func() {
|
|
It("detects the PlaylistProvider capability", func() {
|
|
names := pgManager.PluginNames(string(CapabilityPlaylistProvider))
|
|
Expect(names).To(ContainElement("test-playlist-provider"))
|
|
})
|
|
})
|
|
|
|
Describe("syncer lifecycle", func() {
|
|
It("creates a syncer for the plugin", func() {
|
|
Expect(findSyncer(pgManager, "test-playlist-provider")).ToNot(BeNil())
|
|
})
|
|
|
|
It("discovers and syncs playlists from the plugin", func() {
|
|
// The syncer runs discoverAndSync in a goroutine on Start().
|
|
// Give it a moment to complete.
|
|
Eventually(func() int {
|
|
return mockPlsRepo.Len()
|
|
}).Should(BeNumerically(">=", 2))
|
|
})
|
|
|
|
It("creates playlists with correct fields", func() {
|
|
Eventually(func() bool {
|
|
return mockPlsRepo.FindByPluginPlaylistID("daily-mix-1") != nil
|
|
}).Should(BeTrue())
|
|
|
|
dailyMix1 := mockPlsRepo.FindByPluginPlaylistID("daily-mix-1")
|
|
Expect(dailyMix1.Name).To(Equal("Daily Mix 1"))
|
|
Expect(dailyMix1.Comment).To(Equal("Your personalized daily mix"))
|
|
Expect(dailyMix1.ExternalImageURL).To(Equal("https://example.com/cover1.jpg"))
|
|
Expect(dailyMix1.OwnerID).To(Equal("user-1"))
|
|
Expect(dailyMix1.PluginID).To(Equal("test-playlist-provider"))
|
|
Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1"))
|
|
Expect(dailyMix1.Public).To(BeFalse())
|
|
})
|
|
|
|
It("generates deterministic playlist IDs", func() {
|
|
expectedID := id.NewHash("test-playlist-provider", "daily-mix-1", "user-1")
|
|
Eventually(func() bool {
|
|
_, exists := mockPlsRepo.GetData(expectedID)
|
|
return exists
|
|
}).Should(BeTrue())
|
|
})
|
|
|
|
It("creates distinct IDs for different playlists", func() {
|
|
id1 := id.NewHash("test-playlist-provider", "daily-mix-1", "user-1")
|
|
id2 := id.NewHash("test-playlist-provider", "daily-mix-2", "user-1")
|
|
Expect(id1).ToNot(Equal(id2))
|
|
|
|
Eventually(func() bool {
|
|
_, exists1 := mockPlsRepo.GetData(id1)
|
|
_, exists2 := mockPlsRepo.GetData(id2)
|
|
return exists1 && exists2
|
|
}).Should(BeTrue())
|
|
})
|
|
})
|
|
|
|
Describe("GetAvailablePlaylists error handling", func() {
|
|
It("handles plugin errors gracefully", func() {
|
|
errManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
|
"test-playlist-provider": {"error": "service unavailable"},
|
|
}, "test-playlist-provider"+PackageExtension)
|
|
|
|
// Should still have the syncer (error is logged, not fatal)
|
|
Expect(findSyncer(errManager, "test-playlist-provider")).ToNot(BeNil())
|
|
|
|
// But no playlists created
|
|
errPlsRepo := errManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
|
|
// The syncer was started but GetAvailablePlaylists returned error,
|
|
// so no playlists should be created
|
|
Consistently(func() int {
|
|
return errPlsRepo.Len()
|
|
}, "500ms").Should(Equal(0))
|
|
})
|
|
})
|
|
|
|
Describe("GetPlaylist NotFound error", func() {
|
|
It("skips playlists when plugin returns NotFound", func() {
|
|
notFoundManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
|
"test-playlist-provider": {
|
|
"get_playlist_error": "playlist temporarily unavailable",
|
|
"get_playlist_error_type": string(capabilities.PlaylistProviderErrorNotFound),
|
|
},
|
|
}, "test-playlist-provider"+PackageExtension)
|
|
|
|
// Should still have the syncer
|
|
Expect(findSyncer(notFoundManager, "test-playlist-provider")).ToNot(BeNil())
|
|
|
|
// No playlists should be created (all returned NotFound)
|
|
notFoundPlsRepo := notFoundManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
|
|
Consistently(func() int {
|
|
return notFoundPlsRepo.Len()
|
|
}, "500ms").Should(Equal(0))
|
|
|
|
// No refresh timers should be scheduled for NotFound playlists
|
|
syncer := findSyncer(notFoundManager, "test-playlist-provider")
|
|
Eventually(func() int32 {
|
|
return syncer.refreshTimerCount.Load()
|
|
}).Should(Equal(int32(0)))
|
|
})
|
|
})
|
|
|
|
Describe("GetPlaylist transient error with RetryInterval", func() {
|
|
It("stores retryInterval and schedules retry on transient errors", func() {
|
|
retryManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
|
"test-playlist-provider": {
|
|
"get_playlist_error": "temporary failure",
|
|
"retry_interval": "60",
|
|
},
|
|
}, "test-playlist-provider"+PackageExtension)
|
|
|
|
syncer := findSyncer(retryManager, "test-playlist-provider")
|
|
Expect(syncer).ToNot(BeNil())
|
|
|
|
// retryInterval should be stored from the response
|
|
Eventually(func() time.Duration {
|
|
return time.Duration(syncer.retryInterval.Load())
|
|
}).Should(Equal(60 * time.Second))
|
|
|
|
// No playlists should be created (GetPlaylist failed)
|
|
retryPlsRepo := retryManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
|
|
Consistently(func() int {
|
|
return retryPlsRepo.Len()
|
|
}, "500ms").Should(Equal(0))
|
|
|
|
// Refresh timers should be scheduled for transient errors
|
|
Eventually(func() int32 {
|
|
return syncer.refreshTimerCount.Load()
|
|
}).Should(BeNumerically(">=", int32(1)))
|
|
})
|
|
})
|
|
|
|
Describe("stop", func() {
|
|
It("stops the syncer when the manager stops", func() {
|
|
stopManager, _ := createTestManagerWithPlugins(nil,
|
|
"test-playlist-provider"+PackageExtension,
|
|
)
|
|
Expect(findSyncer(stopManager, "test-playlist-provider")).ToNot(BeNil())
|
|
|
|
err := stopManager.Stop()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
// After Stop(), the plugin is unloaded so findSyncer returns nil
|
|
Expect(findSyncer(stopManager, "test-playlist-provider")).To(BeNil())
|
|
})
|
|
})
|
|
})
|