feat(radio): publish radio metadata events

Signed-off-by: hiyukoim <11803292+hiyukoim@users.noreply.github.com>
This commit is contained in:
Yuko 2026-06-30 23:32:08 +02:00 committed by hiyukoim
parent 8f70e0e4f3
commit 2109175304
5 changed files with 85 additions and 4 deletions

View File

@ -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()

View File

@ -42,6 +42,7 @@ var allProviders = wire.NewSet(
lastfm.NewRouter,
listenbrainz.NewRouter,
events.GetBroker,
events.NewRadioMetadataPublisher,
scanner.New,
scanner.GetWatcher,
metrics.GetPrometheusInstance,

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}