mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-27 11:47:30 +01:00
feat: add event broadcasting for library creation, update, and deletion
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
bcf7f14ed9
commit
202af6eab4
6 changed files with 149 additions and 5 deletions
|
|
@ -69,7 +69,7 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
|||
broker := events.GetBroker()
|
||||
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
watcher := scanner.GetWatcher(dataStore, scannerScanner)
|
||||
library := core.NewLibrary(dataStore, scannerScanner, watcher)
|
||||
library := core.NewLibrary(dataStore, scannerScanner, watcher, broker)
|
||||
router := nativeapi.New(dataStore, share, playlists, insights, library)
|
||||
return router
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
)
|
||||
|
||||
// Scanner interface for triggering scans
|
||||
|
|
@ -43,14 +44,16 @@ type libraryService struct {
|
|||
ds model.DataStore
|
||||
scanner Scanner
|
||||
watcher Watcher
|
||||
broker events.Broker
|
||||
}
|
||||
|
||||
// NewLibrary creates a new Library service
|
||||
func NewLibrary(ds model.DataStore, scanner Scanner, watcher Watcher) Library {
|
||||
func NewLibrary(ds model.DataStore, scanner Scanner, watcher Watcher, broker events.Broker) Library {
|
||||
return &libraryService{
|
||||
ds: ds,
|
||||
scanner: scanner,
|
||||
watcher: watcher,
|
||||
broker: broker,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +133,7 @@ func (s *libraryService) NewRepository(ctx context.Context) rest.Repository {
|
|||
ds: s.ds,
|
||||
scanner: s.scanner,
|
||||
watcher: s.watcher,
|
||||
broker: s.broker,
|
||||
}
|
||||
return wrapper
|
||||
}
|
||||
|
|
@ -141,6 +145,7 @@ type libraryRepositoryWrapper struct {
|
|||
ds model.DataStore
|
||||
scanner Scanner
|
||||
watcher Watcher
|
||||
broker events.Broker
|
||||
}
|
||||
|
||||
func (r *libraryRepositoryWrapper) Save(entity interface{}) (string, error) {
|
||||
|
|
@ -165,6 +170,13 @@ func (r *libraryRepositoryWrapper) Save(entity interface{}) (string, error) {
|
|||
go r.triggerScan(lib, "new")
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", strconv.Itoa(lib.ID)))
|
||||
log.Debug(r.ctx, "Library created - sent refresh event", "libraryID", lib.ID, "name", lib.Name)
|
||||
}
|
||||
|
||||
return strconv.Itoa(lib.ID), nil
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +218,13 @@ func (r *libraryRepositoryWrapper) Update(id string, entity interface{}, cols ..
|
|||
}
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", id))
|
||||
log.Debug(r.ctx, "Library updated - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +258,13 @@ func (r *libraryRepositoryWrapper) Delete(id string) error {
|
|||
go r.triggerScan(lib, "deleted")
|
||||
}
|
||||
|
||||
// Send library refresh event to all clients
|
||||
if r.broker != nil {
|
||||
event := &events.RefreshResource{}
|
||||
r.broker.SendBroadcastMessage(r.ctx, event.With("library", id))
|
||||
log.Debug(r.ctx, "Library deleted - sent refresh event", "libraryID", libID, "name", lib.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package core_test
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
_ "github.com/navidrome/navidrome/core/storage/local" // Register local storage
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
|
@ -29,6 +31,7 @@ var _ = Describe("Library Service", func() {
|
|||
var tempDir string
|
||||
var scanner *mockScanner
|
||||
var watcherManager *mockWatcherManager
|
||||
var broker *mockEventBroker
|
||||
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
|
@ -45,7 +48,9 @@ var _ = Describe("Library Service", func() {
|
|||
watcherManager = &mockWatcherManager{
|
||||
libraryStates: make(map[int]model.Library),
|
||||
}
|
||||
service = core.NewLibrary(ds, scanner, watcherManager)
|
||||
// Create a mock event broker
|
||||
broker = &mockEventBroker{}
|
||||
service = core.NewLibrary(ds, scanner, watcherManager, broker)
|
||||
ctx = context.Background()
|
||||
|
||||
// Create a temporary directory for testing valid paths
|
||||
|
|
@ -816,6 +821,51 @@ var _ = Describe("Library Service", func() {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Event Broadcasting", func() {
|
||||
var repo rest.Persistable
|
||||
|
||||
BeforeEach(func() {
|
||||
r := service.NewRepository(ctx)
|
||||
repo = r.(rest.Persistable)
|
||||
// Clear any events from broker
|
||||
broker.Events = []events.Event{}
|
||||
})
|
||||
|
||||
It("sends refresh event when creating a library", func() {
|
||||
library := &model.Library{ID: 1, Name: "New Library", Path: tempDir}
|
||||
|
||||
_, err := repo.Save(library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("sends refresh event when updating a library", func() {
|
||||
// First create a library
|
||||
libraryRepo.SetData(model.Libraries{
|
||||
{ID: 1, Name: "Original Library", Path: tempDir},
|
||||
})
|
||||
|
||||
library := &model.Library{ID: 1, Name: "Updated Library", Path: tempDir}
|
||||
err := repo.Update("1", library)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("sends refresh event when deleting a library", func() {
|
||||
// First create a library
|
||||
libraryRepo.SetData(model.Libraries{
|
||||
{ID: 2, Name: "Library to Delete", Path: tempDir},
|
||||
})
|
||||
|
||||
err := repo.Delete("2")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(broker.Events).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// mockScanner provides a simple mock implementation of core.Scanner for testing
|
||||
|
|
@ -909,3 +959,22 @@ func (m *mockWatcherManager) simulateExistingLibrary(lib model.Library) {
|
|||
defer m.mu.Unlock()
|
||||
m.libraryStates[lib.ID] = lib
|
||||
}
|
||||
|
||||
// mockEventBroker provides a mock implementation of events.Broker for testing
|
||||
type mockEventBroker struct {
|
||||
http.Handler
|
||||
Events []events.Event
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *mockEventBroker) SendMessage(ctx context.Context, event events.Event) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Events = append(m.Events, event)
|
||||
}
|
||||
|
||||
func (m *mockEventBroker) SendBroadcastMessage(ctx context.Context, event events.Event) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Events = append(m.Events, event)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type MockLibraryService struct {
|
|||
Libraries model.Libraries
|
||||
}
|
||||
|
||||
// NewMockLibraryService creates a new mock library service with some default test data
|
||||
// NewMockLibraryService creates a new mock library service for testing
|
||||
func NewMockLibraryService() *MockLibraryService {
|
||||
return &MockLibraryService{
|
||||
Libraries: model.Libraries{
|
||||
|
|
|
|||
|
|
@ -429,6 +429,12 @@ func (f *fakeEventBroker) SendMessage(_ context.Context, event events.Event) {
|
|||
f.events = append(f.events, event)
|
||||
}
|
||||
|
||||
func (f *fakeEventBroker) SendBroadcastMessage(_ context.Context, event events.Event) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.events = append(f.events, event)
|
||||
}
|
||||
|
||||
func (f *fakeEventBroker) getEvents() []events.Event {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -74,12 +74,18 @@ const LibrarySelector = () => {
|
|||
const refresh = useRefresh()
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState(Date.now())
|
||||
|
||||
const { userLibraries, selectedLibraries } = useSelector(
|
||||
(state) => state.library,
|
||||
)
|
||||
|
||||
// Load user's libraries when component mounts
|
||||
// Watch for refresh events to reload libraries
|
||||
const refreshData = useSelector(
|
||||
(state) => state.activity?.refresh || { lastReceived: lastRefreshTime },
|
||||
)
|
||||
|
||||
// Load user's libraries when component mounts or when refresh events occur
|
||||
useEffect(() => {
|
||||
const loadUserLibraries = async () => {
|
||||
// Get userId from localStorage (not from identity which returns username)
|
||||
|
|
@ -102,6 +108,43 @@ const LibrarySelector = () => {
|
|||
loadUserLibraries()
|
||||
}, [dataProvider, dispatch])
|
||||
|
||||
// Watch for refresh events and reload libraries when they occur
|
||||
useEffect(() => {
|
||||
const { resources, lastReceived } = refreshData
|
||||
|
||||
if (lastReceived <= lastRefreshTime) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldReload = resources &&
|
||||
(resources['*'] === '*' ||
|
||||
resources.library ||
|
||||
resources.user)
|
||||
|
||||
if (shouldReload) {
|
||||
setLastRefreshTime(lastReceived)
|
||||
|
||||
const loadUserLibraries = async () => {
|
||||
const userId = localStorage.getItem('userId')
|
||||
if (userId) {
|
||||
try {
|
||||
const { data } = await dataProvider.getOne('user', { id: userId })
|
||||
const libraries = data.libraries || []
|
||||
dispatch(setUserLibraries(libraries))
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Could not reload user libraries after refresh event:',
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadUserLibraries()
|
||||
}
|
||||
}, [dataProvider, dispatch, refreshData, lastRefreshTime])
|
||||
|
||||
// Don't render if user has no libraries or only has one library
|
||||
if (!userLibraries.length || userLibraries.length === 1) {
|
||||
return null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue