From 210917530425ac590611596c0aea3be487cb85c8 Mon Sep 17 00:00:00 2001 From: Yuko <11803292+hiyukoim@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:32:08 +0200 Subject: [PATCH] feat(radio): publish radio metadata events Signed-off-by: hiyukoim <11803292+hiyukoim@users.noreply.github.com> --- cmd/wire_gen.go | 5 ++-- cmd/wire_injectors.go | 1 + core/radio/provider.go | 4 +-- server/events/events.go | 22 ++++++++++++++ server/events/events_test.go | 57 ++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4588d5810..999ccbe49 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -84,7 +84,8 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager) user := core.NewUser(dataStore, manager) maintenance := core.NewMaintenance(dataStore) - metadataManager := radio.NewMetadataManagerService() + titlePublisher := events.NewRadioMetadataPublisher(broker) + metadataManager := radio.NewMetadataManagerService(titlePublisher) router := nativeapi.New(dataStore, share, playlistsPlaylists, insights, library, user, maintenance, manager, imageUploadService, metadataManager) return router } @@ -249,7 +250,7 @@ func getPluginManager() *plugins.Manager { // wire_injectors.go: -var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, jellyfin.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, sonic.New, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(lyrics.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.Engine), new(*sonic.Sonic)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(nativeapi.RadioMetadataManager), new(*radio.MetadataManager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) +var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, jellyfin.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, events.NewRadioMetadataPublisher, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, sonic.New, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(lyrics.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.PluginLoader), new(*plugins.Manager)), wire.Bind(new(sonic.Engine), new(*sonic.Sonic)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(nativeapi.RadioMetadataManager), new(*radio.MetadataManager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) func GetPluginManager(ctx context.Context) *plugins.Manager { manager := getPluginManager() diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index 4f3ff2a29..f639e69da 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -42,6 +42,7 @@ var allProviders = wire.NewSet( lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, + events.NewRadioMetadataPublisher, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, diff --git a/core/radio/provider.go b/core/radio/provider.go index 5333bc9ac..7fed4095a 100644 --- a/core/radio/provider.go +++ b/core/radio/provider.go @@ -7,8 +7,8 @@ import ( "github.com/navidrome/navidrome/core/radio/icy" ) -func NewMetadataManagerService() *MetadataManager { +func NewMetadataManagerService(publisher TitlePublisher) *MetadataManager { return NewMetadataManager(func(ctx context.Context, streamURL string, handleTitle func(string)) error { return icy.ReadHTTPStreamTitles(ctx, http.DefaultClient, streamURL, handleTitle) - }, nil) + }, publisher) } diff --git a/server/events/events.go b/server/events/events.go index ff0a8a40a..2a6edf103 100644 --- a/server/events/events.go +++ b/server/events/events.go @@ -7,6 +7,8 @@ import ( "strings" "time" "unicode" + + coreradio "github.com/navidrome/navidrome/core/radio" ) type eventCtxKey string @@ -68,6 +70,26 @@ type NowPlayingCount struct { Count int `json:"count"` } +type RadioNowPlaying struct { + baseEvent + RadioID string `json:"radioId"` + Title string `json:"title"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func NewRadioMetadataPublisher(broker Broker) coreradio.TitlePublisher { + return func(ctx context.Context, update coreradio.TitleUpdate) { + if broker == nil { + return + } + broker.SendMessage(ctx, &RadioNowPlaying{ + RadioID: update.RadioID, + Title: update.Title, + UpdatedAt: update.UpdatedAt, + }) + } +} + func (rr *RefreshResource) With(resource string, ids ...string) *RefreshResource { if rr.resources == nil { rr.resources = make(map[string][]string) diff --git a/server/events/events_test.go b/server/events/events_test.go index abfab9b6e..0809a57ad 100644 --- a/server/events/events_test.go +++ b/server/events/events_test.go @@ -1,6 +1,11 @@ package events import ( + "context" + "net/http" + "time" + + coreradio "github.com/navidrome/navidrome/core/radio" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -21,6 +26,41 @@ var _ = Describe("Events", func() { }) }) + Describe("RadioNowPlaying", func() { + It("serialises with a stable event name and payload", func() { + updatedAt := time.Date(2026, time.June, 30, 12, 34, 56, 0, time.UTC) + event := &RadioNowPlaying{ + RadioID: "rd-1", + Title: "Artist - Title", + UpdatedAt: updatedAt, + } + + Expect(event.Name(event)).To(Equal("radioNowPlaying")) + Expect(event.Data(event)).To(Equal(`{"radioId":"rd-1","title":"Artist - Title","updatedAt":"2026-06-30T12:34:56Z"}`)) + }) + + It("publishes metadata updates through the broker", func() { + ctx := context.Background() + updatedAt := time.Date(2026, time.June, 30, 12, 34, 56, 0, time.UTC) + broker := &capturingBroker{} + + publish := NewRadioMetadataPublisher(broker) + publish(ctx, coreradio.TitleUpdate{ + RadioID: "rd-1", + Title: "Artist - Title", + UpdatedAt: updatedAt, + }) + + Expect(broker.ctx).To(Equal(ctx)) + Expect(broker.broadcast).To(BeNil()) + Expect(broker.event).To(Equal(&RadioNowPlaying{ + RadioID: "rd-1", + Title: "Artist - Title", + UpdatedAt: updatedAt, + })) + }) + }) + Describe("RefreshResource", func() { var rr *RefreshResource BeforeEach(func() { @@ -44,3 +84,20 @@ var _ = Describe("Events", func() { }) }) }) + +type capturingBroker struct { + ctx context.Context + event Event + broadcast Event +} + +func (b *capturingBroker) ServeHTTP(http.ResponseWriter, *http.Request) {} + +func (b *capturingBroker) SendMessage(ctx context.Context, event Event) { + b.ctx = ctx + b.event = event +} + +func (b *capturingBroker) SendBroadcastMessage(_ context.Context, event Event) { + b.broadcast = event +}