refactor(plugins): update GetManager to accept DataStore parameter

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-28 12:13:36 -05:00
parent 3605d5bf08
commit 7c6c49c7a1
12 changed files with 86 additions and 91 deletions

View File

@ -330,12 +330,12 @@ func startPlaybackServer(ctx context.Context) func() error {
// startPluginManager starts the plugin manager, if configured.
func startPluginManager(ctx context.Context) func() error {
return func() error {
manager := GetPluginManager(ctx)
if !conf.Server.Plugins.Enabled {
log.Debug("Plugin system is DISABLED")
return nil
}
log.Info(ctx, "Starting plugin manager")
manager := GetPluginManager(ctx)
return manager.Start(ctx)
}
}

View File

@ -60,7 +60,7 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
insights := metrics.GetInstance(dataStore)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
manager := plugins.GetManager()
manager := plugins.GetManager(dataStore)
agentsAgents := agents.GetAgents(dataStore, manager)
provider := external.NewProvider(dataStore, agentsAgents)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
@ -80,7 +80,7 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
dataStore := persistence.New(sqlDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
manager := plugins.GetManager()
manager := plugins.GetManager(dataStore)
agentsAgents := agents.GetAgents(dataStore, manager)
provider := external.NewProvider(dataStore, agentsAgents)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
@ -105,7 +105,7 @@ func CreatePublicRouter() *public.Router {
dataStore := persistence.New(sqlDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
manager := plugins.GetManager()
manager := plugins.GetManager(dataStore)
agentsAgents := agents.GetAgents(dataStore, manager)
provider := external.NewProvider(dataStore, agentsAgents)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
@ -150,7 +150,7 @@ func CreateScanner(ctx context.Context) model.Scanner {
dataStore := persistence.New(sqlDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
manager := plugins.GetManager()
manager := plugins.GetManager(dataStore)
agentsAgents := agents.GetAgents(dataStore, manager)
provider := external.NewProvider(dataStore, agentsAgents)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
@ -167,7 +167,7 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
dataStore := persistence.New(sqlDB)
fileCache := artwork.GetImageCache()
fFmpeg := ffmpeg.New()
manager := plugins.GetManager()
manager := plugins.GetManager(dataStore)
agentsAgents := agents.GetAgents(dataStore, manager)
provider := external.NewProvider(dataStore, agentsAgents)
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
@ -188,7 +188,9 @@ func GetPlaybackServer() playback.PlaybackServer {
}
func getPluginManager() *plugins.Manager {
manager := plugins.GetManager()
sqlDB := db.Db()
dataStore := persistence.New(sqlDB)
manager := plugins.GetManager(dataStore)
return manager
}
@ -199,6 +201,5 @@ var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New,
func GetPluginManager(ctx context.Context) *plugins.Manager {
manager := getPluginManager()
manager.SetSubsonicRouter(CreateSubsonicAPIRouter(ctx))
manager.SetDataStore(CreateDataStore())
return manager
}

View File

@ -129,6 +129,5 @@ func getPluginManager() *plugins.Manager {
func GetPluginManager(ctx context.Context) *plugins.Manager {
manager := getPluginManager()
manager.SetSubsonicRouter(CreateSubsonicAPIRouter(ctx))
manager.SetDataStore(CreateDataStore())
return manager
}

View File

@ -313,7 +313,7 @@ func (c *insightsCollector) hasSmartPlaylists(ctx context.Context) (bool, error)
// collectPlugins collects information about installed plugins
func (c *insightsCollector) collectPlugins(_ context.Context) map[string]insights.PluginInfo {
manager := plugins.GetManager()
manager := plugins.GetManager(c.ds)
info := manager.GetPluginInfo()
result := make(map[string]insights.PluginInfo, len(info))

View File

@ -71,8 +71,8 @@ var _ = Describe("ArtworkService", Ordered, func() {
// Create and start manager
manager = &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetDataStore(dataStore)
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())

View File

@ -360,8 +360,8 @@ var _ = Describe("CacheService Integration", Ordered, func() {
// Create and start manager
manager = &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetDataStore(dataStore)
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())

View File

@ -76,8 +76,8 @@ var _ = Describe("SchedulerService", Ordered, func() {
// Create and start manager
manager = &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetDataStore(dataStore)
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())

View File

@ -67,9 +67,9 @@ var _ = Describe("SubsonicAPI Host Function", Ordered, func() {
// Create and configure manager
manager = &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetSubsonicRouter(router)
manager.SetDataStore(dataStore)
// Pre-enable the plugin in the mock repo so it loads on startup
// Compute SHA256 of the plugin file to match what SyncPlugins will compute

View File

@ -69,8 +69,8 @@ var _ = Describe("WebSocketService", Ordered, func() {
// Create and start manager
manager = &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetDataStore(dataStore)
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())

View File

@ -109,81 +109,15 @@ func (p *plugin) Close() error {
// GetManager returns a singleton instance of the plugin manager.
// The manager is not started automatically; call Start() to begin loading plugins.
func GetManager() *Manager {
func GetManager(ds model.DataStore) *Manager {
return singleton.GetInstance(func() *Manager {
return &Manager{
ds: ds,
plugins: make(map[string]*plugin),
}
})
}
// adminContext returns a context with admin privileges for DB operations.
func adminContext(ctx context.Context) context.Context {
return request.WithUser(ctx, model.User{IsAdmin: true})
}
// marshalManifest marshals a manifest to JSON string, returning empty string on error.
func marshalManifest(m *Manifest) string {
b, _ := json.Marshal(m)
return string(b)
}
// addPluginToDB adds a new plugin to the database as disabled.
func (m *Manager) addPluginToDB(ctx context.Context, repo model.PluginRepository, name, path string, metadata *PluginMetadata) error {
now := time.Now()
newPlugin := &model.Plugin{
ID: name,
Path: path,
Manifest: marshalManifest(metadata.Manifest),
SHA256: metadata.SHA256,
Enabled: false,
CreatedAt: now,
UpdatedAt: now,
}
if err := repo.Put(newPlugin); err != nil {
return fmt.Errorf("adding plugin to DB: %w", err)
}
log.Info(ctx, "Discovered new plugin", "plugin", name)
return nil
}
// updatePluginInDB updates an existing plugin in the database after a file change.
// If the plugin was enabled, it will be unloaded and disabled.
func (m *Manager) updatePluginInDB(ctx context.Context, repo model.PluginRepository, dbPlugin *model.Plugin, path string, metadata *PluginMetadata) error {
wasEnabled := dbPlugin.Enabled
if wasEnabled {
if err := m.UnloadPlugin(dbPlugin.ID); err != nil {
log.Debug(ctx, "Plugin not loaded during change", "plugin", dbPlugin.ID)
}
}
dbPlugin.Path = path
dbPlugin.Manifest = marshalManifest(metadata.Manifest)
dbPlugin.SHA256 = metadata.SHA256
dbPlugin.Enabled = false
dbPlugin.LastError = ""
dbPlugin.UpdatedAt = time.Now()
if err := repo.Put(dbPlugin); err != nil {
return fmt.Errorf("updating plugin in DB: %w", err)
}
log.Info(ctx, "Plugin file changed", "plugin", dbPlugin.ID, "wasEnabled", wasEnabled)
return nil
}
// removePluginFromDB removes a plugin from the database.
// If the plugin was enabled, it will be unloaded first.
func (m *Manager) removePluginFromDB(ctx context.Context, repo model.PluginRepository, dbPlugin *model.Plugin) error {
if dbPlugin.Enabled {
if err := m.UnloadPlugin(dbPlugin.ID); err != nil {
log.Debug(ctx, "Plugin not loaded during removal", "plugin", dbPlugin.ID)
}
}
if err := repo.Delete(dbPlugin.ID); err != nil {
return fmt.Errorf("deleting plugin from DB: %w", err)
}
log.Info(ctx, "Plugin removed", "plugin", dbPlugin.ID)
return nil
}
// SetSubsonicRouter sets the Subsonic router for SubsonicAPI host functions.
// This should be called after the subsonic router is created but before plugins
// that require SubsonicAPI access are loaded.
@ -191,12 +125,6 @@ func (m *Manager) SetSubsonicRouter(router SubsonicRouter) {
m.subsonicRouter = router
}
// SetDataStore sets the data store for plugins that need database access.
// This should be called before plugins are loaded.
func (m *Manager) SetDataStore(ds model.DataStore) {
m.ds = ds
}
// Start initializes the plugin manager and loads plugins from the configured folder.
// It should be called once during application startup when plugins are enabled.
// The startup flow is:
@ -384,6 +312,73 @@ func (m *Manager) GetPluginInfo() map[string]PluginInfo {
return info
}
// adminContext returns a context with admin privileges for DB operations.
func adminContext(ctx context.Context) context.Context {
return request.WithUser(ctx, model.User{IsAdmin: true})
}
// marshalManifest marshals a manifest to JSON string, returning empty string on error.
func marshalManifest(m *Manifest) string {
b, _ := json.Marshal(m)
return string(b)
}
// addPluginToDB adds a new plugin to the database as disabled.
func (m *Manager) addPluginToDB(ctx context.Context, repo model.PluginRepository, name, path string, metadata *PluginMetadata) error {
now := time.Now()
newPlugin := &model.Plugin{
ID: name,
Path: path,
Manifest: marshalManifest(metadata.Manifest),
SHA256: metadata.SHA256,
Enabled: false,
CreatedAt: now,
UpdatedAt: now,
}
if err := repo.Put(newPlugin); err != nil {
return fmt.Errorf("adding plugin to DB: %w", err)
}
log.Info(ctx, "Discovered new plugin", "plugin", name)
return nil
}
// updatePluginInDB updates an existing plugin in the database after a file change.
// If the plugin was enabled, it will be unloaded and disabled.
func (m *Manager) updatePluginInDB(ctx context.Context, repo model.PluginRepository, dbPlugin *model.Plugin, path string, metadata *PluginMetadata) error {
wasEnabled := dbPlugin.Enabled
if wasEnabled {
if err := m.UnloadPlugin(dbPlugin.ID); err != nil {
log.Debug(ctx, "Plugin not loaded during change", "plugin", dbPlugin.ID)
}
}
dbPlugin.Path = path
dbPlugin.Manifest = marshalManifest(metadata.Manifest)
dbPlugin.SHA256 = metadata.SHA256
dbPlugin.Enabled = false
dbPlugin.LastError = ""
dbPlugin.UpdatedAt = time.Now()
if err := repo.Put(dbPlugin); err != nil {
return fmt.Errorf("updating plugin in DB: %w", err)
}
log.Info(ctx, "Plugin file changed", "plugin", dbPlugin.ID, "wasEnabled", wasEnabled)
return nil
}
// removePluginFromDB removes a plugin from the database.
// If the plugin was enabled, it will be unloaded first.
func (m *Manager) removePluginFromDB(ctx context.Context, repo model.PluginRepository, dbPlugin *model.Plugin) error {
if dbPlugin.Enabled {
if err := m.UnloadPlugin(dbPlugin.ID); err != nil {
log.Debug(ctx, "Plugin not loaded during removal", "plugin", dbPlugin.ID)
}
}
if err := repo.Delete(dbPlugin.ID); err != nil {
return fmt.Errorf("deleting plugin from DB: %w", err)
}
log.Info(ctx, "Plugin removed", "plugin", dbPlugin.ID)
return nil
}
// PluginMetadata holds the extracted information from a plugin file
// without fully initializing the plugin.
type PluginMetadata struct {

View File

@ -114,8 +114,8 @@ func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plu
// Create and start manager
manager := &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
manager.SetDataStore(dataStore)
err = manager.Start(GinkgoT().Context())
Expect(err).ToNot(HaveOccurred())

View File

@ -136,8 +136,8 @@ var _ = Describe("Plugin Watcher", func() {
autoReloadManager := &Manager{
plugins: make(map[string]*plugin),
ds: dataStore,
}
autoReloadManager.SetDataStore(dataStore)
err := autoReloadManager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
DeferCleanup(autoReloadManager.Stop)