mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins): integrate event broker into plugin manager
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
628e0b58b7
commit
cef1d339a3
@ -60,12 +60,12 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
insights := metrics.GetInstance(dataStore)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
||||
@ -80,7 +80,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
@ -90,7 +91,6 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share)
|
||||
players := core.NewPlayers(dataStore)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
@ -105,7 +105,8 @@ func CreatePublicRouter() *public.Router {
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
@ -150,12 +151,12 @@ func CreateScanner(ctx context.Context) model.Scanner {
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
@ -167,12 +168,12 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
|
||||
dataStore := persistence.New(sqlDB)
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
broker := events.GetBroker()
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
@ -190,7 +191,8 @@ func GetPlaybackServer() playback.PlaybackServer {
|
||||
func getPluginManager() *plugins.Manager {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
manager := plugins.GetManager(dataStore)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
return manager
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/plugins"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/utils/singleton"
|
||||
)
|
||||
|
||||
@ -313,7 +314,8 @@ 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(c.ds)
|
||||
// TODO Fix import/inject cycles
|
||||
manager := plugins.GetManager(c.ds, events.GetBroker())
|
||||
info := manager.GetPluginInfo()
|
||||
|
||||
result := make(map[string]insights.PluginInfo, len(info))
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/utils/singleton"
|
||||
"github.com/rjeczalik/notify"
|
||||
"github.com/tetratelabs/wazero"
|
||||
@ -54,19 +55,31 @@ type Manager struct {
|
||||
// SubsonicAPI host function dependencies (set once before Start, not modified after)
|
||||
subsonicRouter SubsonicRouter
|
||||
ds model.DataStore
|
||||
broker events.Broker
|
||||
}
|
||||
|
||||
// GetManager returns a singleton instance of the plugin manager.
|
||||
// The manager is not started automatically; call Start() to begin loading plugins.
|
||||
func GetManager(ds model.DataStore) *Manager {
|
||||
func GetManager(ds model.DataStore, broker events.Broker) *Manager {
|
||||
return singleton.GetInstance(func() *Manager {
|
||||
return &Manager{
|
||||
ds: ds,
|
||||
broker: broker,
|
||||
plugins: make(map[string]*plugin),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// sendPluginRefreshEvent broadcasts a refresh event for the plugin resource.
|
||||
// This notifies connected UI clients that plugin data has changed.
|
||||
func (m *Manager) sendPluginRefreshEvent(ctx context.Context, pluginIDs ...string) {
|
||||
if m.broker == nil {
|
||||
return
|
||||
}
|
||||
event := (&events.RefreshResource{}).With("plugin", pluginIDs...)
|
||||
m.broker.SendBroadcastMessage(ctx, event)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -304,6 +317,7 @@ func (m *Manager) EnablePlugin(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
log.Info(ctx, "Enabled plugin", "plugin", id)
|
||||
m.sendPluginRefreshEvent(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -339,6 +353,7 @@ func (m *Manager) DisablePlugin(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
log.Info(ctx, "Disabled plugin", "plugin", id)
|
||||
m.sendPluginRefreshEvent(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -380,6 +395,7 @@ func (m *Manager) UpdatePluginConfig(ctx context.Context, id, configJSON string)
|
||||
}
|
||||
|
||||
log.Info(ctx, "Updated plugin config", "plugin", id)
|
||||
m.sendPluginRefreshEvent(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
)
|
||||
|
||||
// PluginMetadata holds the extracted information from a plugin file
|
||||
@ -67,6 +68,7 @@ func (m *Manager) addPluginToDB(ctx context.Context, repo model.PluginRepository
|
||||
return fmt.Errorf("adding plugin to DB: %w", err)
|
||||
}
|
||||
log.Info(ctx, "Discovered new plugin", "plugin", name)
|
||||
m.sendPluginRefreshEvent(ctx, events.Any)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -89,21 +91,24 @@ func (m *Manager) updatePluginInDB(ctx context.Context, repo model.PluginReposit
|
||||
return fmt.Errorf("updating plugin in DB: %w", err)
|
||||
}
|
||||
log.Info(ctx, "Plugin file changed", "plugin", dbPlugin.ID, "wasEnabled", wasEnabled)
|
||||
m.sendPluginRefreshEvent(ctx, dbPlugin.ID)
|
||||
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 {
|
||||
pluginID := dbPlugin.ID
|
||||
if dbPlugin.Enabled {
|
||||
if err := m.unloadPlugin(dbPlugin.ID); err != nil {
|
||||
log.Debug(ctx, "Plugin not loaded during removal", "plugin", dbPlugin.ID, err)
|
||||
if err := m.unloadPlugin(pluginID); err != nil {
|
||||
log.Debug(ctx, "Plugin not loaded during removal", "plugin", pluginID, err)
|
||||
}
|
||||
}
|
||||
if err := repo.Delete(dbPlugin.ID); err != nil {
|
||||
if err := repo.Delete(pluginID); err != nil {
|
||||
return fmt.Errorf("deleting plugin from DB: %w", err)
|
||||
}
|
||||
log.Info(ctx, "Plugin removed", "plugin", dbPlugin.ID)
|
||||
log.Info(ctx, "Plugin removed", "plugin", pluginID)
|
||||
m.sendPluginRefreshEvent(ctx, events.Any)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,11 @@ package plugins
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@ -121,4 +123,74 @@ var _ = Describe("Manager", Ordered, func() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Describe("sendPluginRefreshEvent", func() {
|
||||
var broker *testBroker
|
||||
var manager *Manager
|
||||
|
||||
BeforeEach(func() {
|
||||
broker = &testBroker{}
|
||||
manager = &Manager{
|
||||
broker: broker,
|
||||
}
|
||||
})
|
||||
|
||||
It("sends refresh event with single plugin ID", func() {
|
||||
manager.sendPluginRefreshEvent(ctx, "test-plugin")
|
||||
|
||||
Expect(broker.broadcastCalled).To(BeTrue())
|
||||
Expect(broker.lastEvent).ToNot(BeNil())
|
||||
Expect(broker.lastEventCtx).To(Equal(ctx))
|
||||
|
||||
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
||||
Expect(ok).To(BeTrue(), "event should be a RefreshResource")
|
||||
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["test-plugin"]}`))
|
||||
})
|
||||
|
||||
It("sends refresh event with multiple plugin IDs", func() {
|
||||
manager.sendPluginRefreshEvent(ctx, "plugin-1", "plugin-2", "plugin-3")
|
||||
|
||||
Expect(broker.broadcastCalled).To(BeTrue())
|
||||
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["plugin-1","plugin-2","plugin-3"]}`))
|
||||
})
|
||||
|
||||
It("sends refresh event with wildcard when using events.Any", func() {
|
||||
manager.sendPluginRefreshEvent(ctx, events.Any)
|
||||
|
||||
Expect(broker.broadcastCalled).To(BeTrue())
|
||||
refreshEvent, ok := broker.lastEvent.(*events.RefreshResource)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(refreshEvent.Data(refreshEvent)).To(Equal(`{"plugin":["*"]}`))
|
||||
})
|
||||
|
||||
It("does not panic when broker is nil", func() {
|
||||
manager.broker = nil
|
||||
Expect(func() {
|
||||
manager.sendPluginRefreshEvent(ctx, "test-plugin")
|
||||
}).ToNot(Panic())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// testBroker is a simple mock implementation of events.Broker for testing
|
||||
type testBroker struct {
|
||||
lastEvent events.Event
|
||||
lastEventCtx context.Context
|
||||
broadcastCalled bool
|
||||
}
|
||||
|
||||
func (m *testBroker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Not used in tests
|
||||
}
|
||||
|
||||
func (m *testBroker) SendMessage(ctx context.Context, event events.Event) {
|
||||
// Not used in tests
|
||||
}
|
||||
|
||||
func (m *testBroker) SendBroadcastMessage(ctx context.Context, event events.Event) {
|
||||
m.lastEvent = event
|
||||
m.lastEventCtx = ctx
|
||||
m.broadcastCalled = true
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useMediaQuery, Tooltip, Chip, Typography } from '@material-ui/core'
|
||||
import { MdError } from 'react-icons/md'
|
||||
import { List, DateField, SimpleList } from '../common'
|
||||
import { List, DateField, SimpleList, useResourceRefresh } from '../common'
|
||||
import ToggleEnabledSwitch from './ToggleEnabledSwitch'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
@ -69,6 +69,7 @@ const ManifestField = ({ source }) => {
|
||||
const PluginList = (props) => {
|
||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||
const translate = useTranslate()
|
||||
useResourceRefresh('plugin')
|
||||
|
||||
return (
|
||||
<List {...props} sort={{ field: 'id', order: 'ASC' }} exporter={false}>
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from 'react-admin'
|
||||
import { Box, useMediaQuery } from '@material-ui/core'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { Title } from '../common'
|
||||
import { Title, useResourceRefresh } from '../common'
|
||||
import { usePluginShowStyles } from './styles.js'
|
||||
import { ErrorSection } from './ErrorSection'
|
||||
import { StatusCard } from './StatusCard'
|
||||
@ -28,6 +28,7 @@ const PluginShowLayout = () => {
|
||||
const notify = useNotify()
|
||||
const refresh = useRefresh()
|
||||
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||
useResourceRefresh('plugin')
|
||||
|
||||
const [configPairs, setConfigPairs] = useState([])
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user