diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 204d90ba..e8df9a38 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run -mod=mod github.com/google/wire/cmd/wire gen -tags "netgo" +//go:generate go run -mod=mod github.com/google/wire/cmd/wire gen -tags "netgo sqlite_fts5" //go:build !wireinject // +build !wireinject @@ -16,6 +16,7 @@ import ( "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/external" "github.com/navidrome/navidrome/core/ffmpeg" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playlists" @@ -103,7 +104,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router { modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics) playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager) playbackServer := playback.GetInstance(dataStore) - router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics) + lyricsLyrics := lyrics.NewLyrics(manager) + router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics) return router } @@ -207,7 +209,7 @@ func getPluginManager() *plugins.Manager { // wire_injectors.go: -var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), 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, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, 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(nativeapi.PluginManager), new(*plugins.Manager)), 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 56206feb..d87a8d6d 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -11,6 +11,7 @@ import ( "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/agents" "github.com/navidrome/navidrome/core/artwork" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/scrobbler" @@ -44,6 +45,7 @@ var allProviders = wire.NewSet( plugins.GetManager, 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(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), diff --git a/core/lyrics/lyrics.go b/core/lyrics/lyrics.go index 858a3ffd..75805304 100644 --- a/core/lyrics/lyrics.go +++ b/core/lyrics/lyrics.go @@ -9,23 +9,45 @@ import ( "github.com/navidrome/navidrome/model" ) -func GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) { +// Lyrics can fetch lyrics for a media file. +type Lyrics interface { + GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) +} + +// PluginLoader discovers and loads lyrics provider plugins. +type PluginLoader interface { + LoadLyricsProvider(name string) (Lyrics, bool) +} + +type lyricsService struct { + pluginLoader PluginLoader +} + +// NewLyrics creates a new lyrics service. pluginLoader may be nil if no plugin +// system is available. +func NewLyrics(pluginLoader PluginLoader) Lyrics { + return &lyricsService{pluginLoader: pluginLoader} +} + +// GetLyrics returns lyrics for the given media file, trying sources in the +// order specified by conf.Server.LyricsPriority. +func (l *lyricsService) GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) { var lyricsList model.LyricList var err error - for pattern := range strings.SplitSeq(strings.ToLower(conf.Server.LyricsPriority), ",") { + for pattern := range strings.SplitSeq(conf.Server.LyricsPriority, ",") { pattern = strings.TrimSpace(pattern) switch { - case pattern == "embedded": + case strings.EqualFold(pattern, "embedded"): lyricsList, err = fromEmbedded(ctx, mf) case strings.HasPrefix(pattern, "."): - lyricsList, err = fromExternalFile(ctx, mf, pattern) + lyricsList, err = fromExternalFile(ctx, mf, strings.ToLower(pattern)) default: - log.Error(ctx, "Invalid lyric pattern", "pattern", pattern) + lyricsList, err = l.fromPlugin(ctx, mf, pattern) } if err != nil { - log.Error(ctx, "error parsing lyrics", "source", pattern, err) + log.Error(ctx, "error getting lyrics", "source", pattern, err) } if len(lyricsList) > 0 { diff --git a/core/lyrics/lyrics_test.go b/core/lyrics/lyrics_test.go index f4197ccf..2e495a71 100644 --- a/core/lyrics/lyrics_test.go +++ b/core/lyrics/lyrics_test.go @@ -3,6 +3,7 @@ package lyrics_test import ( "context" "encoding/json" + "fmt" "os" "github.com/navidrome/navidrome/conf" @@ -72,7 +73,8 @@ var _ = Describe("sources", func() { DescribeTable("Lyrics Priority", func(priority string, expected model.LyricList) { conf.Server.LyricsPriority = priority - list, err := lyrics.GetLyrics(ctx, &mf) + svc := lyrics.NewLyrics(nil) + list, err := svc.GetLyrics(ctx, &mf) Expect(err).To(BeNil()) Expect(list).To(Equal(expected)) }, @@ -107,7 +109,8 @@ var _ = Describe("sources", func() { It("should fallback to embedded if an error happens when parsing file", func() { conf.Server.LyricsPriority = ".mp3,embedded" - list, err := lyrics.GetLyrics(ctx, &mf) + svc := lyrics.NewLyrics(nil) + list, err := svc.GetLyrics(ctx, &mf) Expect(err).To(BeNil()) Expect(list).To(Equal(embeddedLyrics)) }) @@ -115,10 +118,109 @@ var _ = Describe("sources", func() { It("should return nothing if error happens when trying to parse file", func() { conf.Server.LyricsPriority = ".mp3" - list, err := lyrics.GetLyrics(ctx, &mf) + svc := lyrics.NewLyrics(nil) + list, err := svc.GetLyrics(ctx, &mf) Expect(err).To(BeNil()) Expect(list).To(BeEmpty()) }) }) }) + + Context("plugin sources", func() { + var mockLoader *mockPluginLoader + + BeforeEach(func() { + mockLoader = &mockPluginLoader{} + }) + + It("should return lyrics from a plugin", func() { + conf.Server.LyricsPriority = "test-lyrics-plugin" + mockLoader.lyrics = unsyncedLyrics + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(unsyncedLyrics)) + }) + + It("should try plugin after embedded returns nothing", func() { + conf.Server.LyricsPriority = "embedded,test-lyrics-plugin" + mf.Lyrics = "" // No embedded lyrics + mockLoader.lyrics = unsyncedLyrics + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(unsyncedLyrics)) + }) + + It("should skip plugin if embedded has lyrics", func() { + conf.Server.LyricsPriority = "embedded,test-lyrics-plugin" + mockLoader.lyrics = unsyncedLyrics + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(embeddedLyrics)) // embedded wins + }) + + It("should skip unknown plugin names gracefully", func() { + conf.Server.LyricsPriority = "nonexistent-plugin,embedded" + mockLoader.notFound = true + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(embeddedLyrics)) // falls through to embedded + }) + + It("should preserve plugin name case from config", func() { + conf.Server.LyricsPriority = "MyLyricsPlugin" + mockLoader.pluginName = "MyLyricsPlugin" + mockLoader.lyrics = unsyncedLyrics + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(unsyncedLyrics)) + }) + + It("should handle plugin error gracefully", func() { + conf.Server.LyricsPriority = "test-lyrics-plugin,embedded" + mockLoader.err = fmt.Errorf("plugin error") + svc := lyrics.NewLyrics(mockLoader) + list, err := svc.GetLyrics(ctx, &mf) + Expect(err).To(BeNil()) + Expect(list).To(Equal(embeddedLyrics)) // falls through to embedded + }) + }) }) + +type mockPluginLoader struct { + lyrics model.LyricList + err error + notFound bool + pluginName string // expected plugin name (exact match, like real manager) +} + +func (m *mockPluginLoader) PluginNames(_ string) []string { + if m.notFound { + return nil + } + return []string{"test-lyrics-plugin"} +} + +func (m *mockPluginLoader) LoadLyricsProvider(name string) (lyrics.Lyrics, bool) { + if m.notFound { + return nil, false + } + // If pluginName is set, require exact match (like the real plugin manager) + if m.pluginName != "" && name != m.pluginName { + return nil, false + } + return &mockLyricsProvider{lyrics: m.lyrics, err: m.err}, true +} + +type mockLyricsProvider struct { + lyrics model.LyricList + err error +} + +func (m *mockLyricsProvider) GetLyrics(_ context.Context, _ *model.MediaFile) (model.LyricList, error) { + return m.lyrics, m.err +} diff --git a/core/lyrics/sources.go b/core/lyrics/sources.go index 857dc2ee..82a10ca4 100644 --- a/core/lyrics/sources.go +++ b/core/lyrics/sources.go @@ -49,3 +49,27 @@ func fromExternalFile(ctx context.Context, mf *model.MediaFile, suffix string) ( return model.LyricList{*lyrics}, nil } + +// fromPlugin attempts to load lyrics from a plugin with the given name. +func (l *lyricsService) fromPlugin(ctx context.Context, mf *model.MediaFile, pluginName string) (model.LyricList, error) { + if l.pluginLoader == nil { + log.Debug(ctx, "Invalid lyric source", "source", pluginName) + return nil, nil + } + + provider, ok := l.pluginLoader.LoadLyricsProvider(pluginName) + if !ok { + log.Warn(ctx, "Lyrics plugin not found", "plugin", pluginName) + return nil, nil + } + + lyricsList, err := provider.GetLyrics(ctx, mf) + if err != nil { + return nil, err + } + + if len(lyricsList) > 0 { + log.Trace(ctx, "Retrieved lyrics from plugin", "plugin", pluginName, "count", len(lyricsList)) + } + return lyricsList, nil +} diff --git a/core/wire_providers.go b/core/wire_providers.go index 503feb78..f9b47201 100644 --- a/core/wire_providers.go +++ b/core/wire_providers.go @@ -5,6 +5,7 @@ import ( "github.com/navidrome/navidrome/core/agents" "github.com/navidrome/navidrome/core/external" "github.com/navidrome/navidrome/core/ffmpeg" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playlists" @@ -28,4 +29,5 @@ var Set = wire.NewSet( scrobbler.GetPlayTracker, playback.GetInstance, metrics.GetInstance, + lyrics.NewLyrics, ) diff --git a/plugins/capabilities/lyrics.go b/plugins/capabilities/lyrics.go new file mode 100644 index 00000000..6f6d1917 --- /dev/null +++ b/plugins/capabilities/lyrics.go @@ -0,0 +1,26 @@ +package capabilities + +// Lyrics provides lyrics for a given track from external sources. +// +//nd:capability name=lyrics required=true +type Lyrics interface { + //nd:export name=nd_lyrics_get_lyrics + GetLyrics(GetLyricsRequest) (GetLyricsResponse, error) +} + +// GetLyricsRequest contains the track information for lyrics lookup. +type GetLyricsRequest struct { + Track TrackInfo `json:"track"` +} + +// GetLyricsResponse contains the lyrics returned by the plugin. +type GetLyricsResponse struct { + Lyrics []LyricsText `json:"lyrics"` +} + +// LyricsText represents a single set of lyrics in raw text format. +// Text can be plain text or LRC format — Navidrome will parse it. +type LyricsText struct { + Lang string `json:"lang,omitempty"` + Text string `json:"text"` +} diff --git a/plugins/capabilities/lyrics.yaml b/plugins/capabilities/lyrics.yaml new file mode 100644 index 00000000..e4f88476 --- /dev/null +++ b/plugins/capabilities/lyrics.yaml @@ -0,0 +1,115 @@ +version: v1-draft +exports: + nd_lyrics_get_lyrics: + input: + $ref: '#/components/schemas/GetLyricsRequest' + contentType: application/json + output: + $ref: '#/components/schemas/GetLyricsResponse' + contentType: application/json +components: + schemas: + ArtistRef: + description: ArtistRef is a reference to an artist with name and optional MBID. + properties: + id: + type: string + description: ID is the internal Navidrome artist ID (if known). + name: + type: string + description: Name is the artist name. + mbid: + type: string + description: MBID is the MusicBrainz ID for the artist. + required: + - name + GetLyricsRequest: + description: GetLyricsRequest contains the track information for lyrics lookup. + properties: + track: + $ref: '#/components/schemas/TrackInfo' + required: + - track + GetLyricsResponse: + description: GetLyricsResponse contains the lyrics returned by the plugin. + properties: + lyrics: + type: array + items: + $ref: '#/components/schemas/LyricsText' + required: + - lyrics + LyricsText: + description: |- + LyricsText represents a single set of lyrics in raw text format. + Text can be plain text or LRC format — Navidrome will parse it. + properties: + lang: + type: string + text: + type: string + required: + - text + TrackInfo: + description: TrackInfo contains track metadata. + properties: + id: + type: string + description: ID is the internal Navidrome track ID. + title: + type: string + description: Title is the track title. + album: + type: string + description: Album is the album name. + artist: + type: string + description: Artist is the formatted artist name for display (e.g., "Artist1 • Artist2"). + albumArtist: + type: string + description: AlbumArtist is the formatted album artist name for display. + artists: + type: array + description: Artists is the list of track artists. + items: + $ref: '#/components/schemas/ArtistRef' + albumArtists: + type: array + description: AlbumArtists is the list of album artists. + items: + $ref: '#/components/schemas/ArtistRef' + duration: + type: number + format: float + description: Duration is the track duration in seconds. + trackNumber: + type: integer + format: int32 + description: TrackNumber is the track number on the album. + discNumber: + type: integer + format: int32 + description: DiscNumber is the disc number. + mbzRecordingId: + type: string + description: MBZRecordingID is the MusicBrainz recording ID. + mbzAlbumId: + type: string + description: MBZAlbumID is the MusicBrainz album/release ID. + mbzReleaseGroupId: + type: string + description: MBZReleaseGroupID is the MusicBrainz release group ID. + mbzReleaseTrackId: + type: string + description: MBZReleaseTrackID is the MusicBrainz release track ID. + required: + - id + - title + - album + - artist + - albumArtist + - artists + - albumArtists + - duration + - trackNumber + - discNumber diff --git a/plugins/capabilities/scrobbler.go b/plugins/capabilities/scrobbler.go index 300652cb..8091efe5 100644 --- a/plugins/capabilities/scrobbler.go +++ b/plugins/capabilities/scrobbler.go @@ -38,7 +38,7 @@ type ArtistRef struct { MBID string `json:"mbid,omitempty"` } -// TrackInfo contains track metadata for scrobbling. +// TrackInfo contains track metadata. type TrackInfo struct { // ID is the internal Navidrome track ID. ID string `json:"id"` diff --git a/plugins/capabilities/scrobbler.yaml b/plugins/capabilities/scrobbler.yaml index d8f47c95..5de351a5 100644 --- a/plugins/capabilities/scrobbler.yaml +++ b/plugins/capabilities/scrobbler.yaml @@ -77,7 +77,7 @@ components: - track - timestamp TrackInfo: - description: TrackInfo contains track metadata for scrobbling. + description: TrackInfo contains track metadata. properties: id: type: string diff --git a/plugins/lyrics_adapter.go b/plugins/lyrics_adapter.go new file mode 100644 index 00000000..aa993066 --- /dev/null +++ b/plugins/lyrics_adapter.go @@ -0,0 +1,59 @@ +package plugins + +import ( + "context" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/plugins/capabilities" +) + +const CapabilityLyrics Capability = "Lyrics" + +const ( + FuncLyricsGetLyrics = "nd_lyrics_get_lyrics" +) + +func init() { + registerCapability( + CapabilityLyrics, + FuncLyricsGetLyrics, + ) +} + +// LyricsPlugin adapts a WASM plugin with the Lyrics capability. +type LyricsPlugin struct { + name string + plugin *plugin +} + +// GetLyrics calls the plugin to fetch lyrics, then parses the raw text responses +// using model.ToLyrics. +func (l *LyricsPlugin) GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) { + req := capabilities.GetLyricsRequest{ + Track: mediaFileToTrackInfo(mf), + } + resp, err := callPluginFunction[capabilities.GetLyricsRequest, capabilities.GetLyricsResponse]( + ctx, l.plugin, FuncLyricsGetLyrics, req, + ) + if err != nil { + return nil, err + } + + var result model.LyricList + for _, lt := range resp.Lyrics { + lang := lt.Lang + if lang == "" { + lang = "xxx" + } + parsed, err := model.ToLyrics(lang, lt.Text) + if err != nil { + log.Warn(ctx, "Error parsing plugin lyrics", "plugin", l.name, err) + continue + } + if parsed != nil && !parsed.IsEmpty() { + result = append(result, *parsed) + } + } + return result, nil +} diff --git a/plugins/lyrics_adapter_test.go b/plugins/lyrics_adapter_test.go new file mode 100644 index 00000000..a1a6c180 --- /dev/null +++ b/plugins/lyrics_adapter_test.go @@ -0,0 +1,99 @@ +//go:build !windows + +package plugins + +import ( + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LyricsPlugin", Ordered, func() { + var ( + lyricsManager *Manager + provider *LyricsPlugin + ) + + BeforeAll(func() { + lyricsManager, _ = createTestManagerWithPlugins(nil, + "test-lyrics"+PackageExtension, + "test-metadata-agent"+PackageExtension, + ) + + p, ok := lyricsManager.LoadLyricsProvider("test-lyrics") + Expect(ok).To(BeTrue()) + provider = p.(*LyricsPlugin) + }) + + Describe("LoadLyricsProvider", func() { + It("returns a lyrics provider for a plugin with Lyrics capability", func() { + Expect(provider).ToNot(BeNil()) + }) + + It("returns false for a plugin without Lyrics capability", func() { + _, ok := lyricsManager.LoadLyricsProvider("test-metadata-agent") + Expect(ok).To(BeFalse()) + }) + + It("returns false for non-existent plugin", func() { + _, ok := lyricsManager.LoadLyricsProvider("non-existent") + Expect(ok).To(BeFalse()) + }) + }) + + Describe("GetLyrics", func() { + It("successfully returns lyrics from the plugin", func() { + track := &model.MediaFile{ + ID: "track-1", + Title: "Test Song", + Artist: "Test Artist", + } + + result, err := provider.GetLyrics(GinkgoT().Context(), track) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Line).ToNot(BeEmpty()) + Expect(result[0].Line[0].Value).To(ContainSubstring("Test Song")) + }) + + It("defaults language to 'xxx' when plugin does not provide one", func() { + manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ + "test-lyrics": {"no_lang": "true"}, + }, "test-lyrics"+PackageExtension) + + p, ok := manager.LoadLyricsProvider("test-lyrics") + Expect(ok).To(BeTrue()) + + track := &model.MediaFile{ID: "track-1", Title: "Test Song", Artist: "Test Artist"} + result, err := p.GetLyrics(GinkgoT().Context(), track) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Lang).To(Equal("xxx")) + }) + + It("returns error when plugin returns error", func() { + manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ + "test-lyrics": {"error": "service unavailable"}, + }, "test-lyrics"+PackageExtension) + + p, ok := manager.LoadLyricsProvider("test-lyrics") + Expect(ok).To(BeTrue()) + + track := &model.MediaFile{ID: "track-1", Title: "Test Song"} + _, err := p.GetLyrics(GinkgoT().Context(), track) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("PluginNames", func() { + It("returns plugin names with Lyrics capability", func() { + names := lyricsManager.PluginNames("Lyrics") + Expect(names).To(ContainElement("test-lyrics")) + }) + + It("does not return metadata agent plugins for Lyrics capability", func() { + names := lyricsManager.PluginNames("Lyrics") + Expect(names).ToNot(ContainElement("test-metadata-agent")) + }) + }) +}) diff --git a/plugins/manager.go b/plugins/manager.go index bf6bab6e..0c7c91ed 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -16,6 +16,7 @@ import ( extism "github.com/extism/go-sdk" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/core/agents" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -282,6 +283,22 @@ func (m *Manager) LoadScrobbler(name string) (scrobbler.Scrobbler, bool) { }, true } +// LoadLyricsProvider loads and returns a lyrics provider plugin by name. +func (m *Manager) LoadLyricsProvider(name string) (lyrics.Lyrics, bool) { + m.mu.RLock() + plugin, ok := m.plugins[name] + m.mu.RUnlock() + + if !ok || !hasCapability(plugin.capabilities, CapabilityLyrics) { + return nil, false + } + + return &LyricsPlugin{ + name: plugin.name, + plugin: plugin, + }, true +} + // PluginInfo contains basic information about a plugin for metrics/insights. type PluginInfo struct { Name string diff --git a/plugins/pdk/go/lyrics/lyrics.go b/plugins/pdk/go/lyrics/lyrics.go new file mode 100644 index 00000000..4f5aa630 --- /dev/null +++ b/plugins/pdk/go/lyrics/lyrics.go @@ -0,0 +1,118 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the Lyrics capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package lyrics + +import ( + "github.com/navidrome/navidrome/plugins/pdk/go/pdk" +) + +// ArtistRef is a reference to an artist with name and optional MBID. +type ArtistRef struct { + // ID is the internal Navidrome artist ID (if known). + ID string `json:"id,omitempty"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid,omitempty"` +} + +// GetLyricsRequest contains the track information for lyrics lookup. +type GetLyricsRequest struct { + Track TrackInfo `json:"track"` +} + +// GetLyricsResponse contains the lyrics returned by the plugin. +type GetLyricsResponse struct { + Lyrics []LyricsText `json:"lyrics"` +} + +// LyricsText represents a single set of lyrics in raw text format. +// Text can be plain text or LRC format — Navidrome will parse it. +type LyricsText struct { + Lang string `json:"lang,omitempty"` + Text string `json:"text"` +} + +// TrackInfo contains track metadata. +type TrackInfo struct { + // ID is the internal Navidrome track ID. + ID string `json:"id"` + // Title is the track title. + Title string `json:"title"` + // Album is the album name. + Album string `json:"album"` + // Artist is the formatted artist name for display (e.g., "Artist1 • Artist2"). + Artist string `json:"artist"` + // AlbumArtist is the formatted album artist name for display. + AlbumArtist string `json:"albumArtist"` + // Artists is the list of track artists. + Artists []ArtistRef `json:"artists"` + // AlbumArtists is the list of album artists. + AlbumArtists []ArtistRef `json:"albumArtists"` + // Duration is the track duration in seconds. + Duration float32 `json:"duration"` + // TrackNumber is the track number on the album. + TrackNumber int32 `json:"trackNumber"` + // DiscNumber is the disc number. + DiscNumber int32 `json:"discNumber"` + // MBZRecordingID is the MusicBrainz recording ID. + MBZRecordingID string `json:"mbzRecordingId,omitempty"` + // MBZAlbumID is the MusicBrainz album/release ID. + MBZAlbumID string `json:"mbzAlbumId,omitempty"` + // MBZReleaseGroupID is the MusicBrainz release group ID. + MBZReleaseGroupID string `json:"mbzReleaseGroupId,omitempty"` + // MBZReleaseTrackID is the MusicBrainz release track ID. + MBZReleaseTrackID string `json:"mbzReleaseTrackId,omitempty"` +} + +// Lyrics requires all methods to be implemented. +// Lyrics provides lyrics for a given track from external sources. +type Lyrics interface { + // GetLyrics + GetLyrics(GetLyricsRequest) (GetLyricsResponse, error) +} // Internal implementation holders +var ( + lyricsImpl func(GetLyricsRequest) (GetLyricsResponse, error) +) + +// Register registers a lyrics implementation. +// All methods are required. +func Register(impl Lyrics) { + lyricsImpl = impl.GetLyrics +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//go:wasmexport nd_lyrics_get_lyrics +func _NdLyricsGetLyrics() int32 { + if lyricsImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input GetLyricsRequest + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := lyricsImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/lyrics/lyrics_stub.go b/plugins/pdk/go/lyrics/lyrics_stub.go new file mode 100644 index 00000000..1fdf184e --- /dev/null +++ b/plugins/pdk/go/lyrics/lyrics_stub.go @@ -0,0 +1,82 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package lyrics + +// ArtistRef is a reference to an artist with name and optional MBID. +type ArtistRef struct { + // ID is the internal Navidrome artist ID (if known). + ID string `json:"id,omitempty"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid,omitempty"` +} + +// GetLyricsRequest contains the track information for lyrics lookup. +type GetLyricsRequest struct { + Track TrackInfo `json:"track"` +} + +// GetLyricsResponse contains the lyrics returned by the plugin. +type GetLyricsResponse struct { + Lyrics []LyricsText `json:"lyrics"` +} + +// LyricsText represents a single set of lyrics in raw text format. +// Text can be plain text or LRC format — Navidrome will parse it. +type LyricsText struct { + Lang string `json:"lang,omitempty"` + Text string `json:"text"` +} + +// TrackInfo contains track metadata. +type TrackInfo struct { + // ID is the internal Navidrome track ID. + ID string `json:"id"` + // Title is the track title. + Title string `json:"title"` + // Album is the album name. + Album string `json:"album"` + // Artist is the formatted artist name for display (e.g., "Artist1 • Artist2"). + Artist string `json:"artist"` + // AlbumArtist is the formatted album artist name for display. + AlbumArtist string `json:"albumArtist"` + // Artists is the list of track artists. + Artists []ArtistRef `json:"artists"` + // AlbumArtists is the list of album artists. + AlbumArtists []ArtistRef `json:"albumArtists"` + // Duration is the track duration in seconds. + Duration float32 `json:"duration"` + // TrackNumber is the track number on the album. + TrackNumber int32 `json:"trackNumber"` + // DiscNumber is the disc number. + DiscNumber int32 `json:"discNumber"` + // MBZRecordingID is the MusicBrainz recording ID. + MBZRecordingID string `json:"mbzRecordingId,omitempty"` + // MBZAlbumID is the MusicBrainz album/release ID. + MBZAlbumID string `json:"mbzAlbumId,omitempty"` + // MBZReleaseGroupID is the MusicBrainz release group ID. + MBZReleaseGroupID string `json:"mbzReleaseGroupId,omitempty"` + // MBZReleaseTrackID is the MusicBrainz release track ID. + MBZReleaseTrackID string `json:"mbzReleaseTrackId,omitempty"` +} + +// Lyrics requires all methods to be implemented. +// Lyrics provides lyrics for a given track from external sources. +type Lyrics interface { + // GetLyrics + GetLyrics(GetLyricsRequest) (GetLyricsResponse, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ Lyrics) {} diff --git a/plugins/pdk/go/scrobbler/scrobbler.go b/plugins/pdk/go/scrobbler/scrobbler.go index 258b1b4c..c694f59d 100644 --- a/plugins/pdk/go/scrobbler/scrobbler.go +++ b/plugins/pdk/go/scrobbler/scrobbler.go @@ -62,7 +62,7 @@ type ScrobbleRequest struct { Timestamp int64 `json:"timestamp"` } -// TrackInfo contains track metadata for scrobbling. +// TrackInfo contains track metadata. type TrackInfo struct { // ID is the internal Navidrome track ID. ID string `json:"id"` diff --git a/plugins/pdk/go/scrobbler/scrobbler_stub.go b/plugins/pdk/go/scrobbler/scrobbler_stub.go index f2fc584a..6d4afd81 100644 --- a/plugins/pdk/go/scrobbler/scrobbler_stub.go +++ b/plugins/pdk/go/scrobbler/scrobbler_stub.go @@ -59,7 +59,7 @@ type ScrobbleRequest struct { Timestamp int64 `json:"timestamp"` } -// TrackInfo contains track metadata for scrobbling. +// TrackInfo contains track metadata. type TrackInfo struct { // ID is the internal Navidrome track ID. ID string `json:"id"` diff --git a/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs b/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs index 06c2c5c0..85375b52 100644 --- a/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs +++ b/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs @@ -6,6 +6,7 @@ //! for implementing Navidrome plugin capabilities in Rust. pub mod lifecycle; +pub mod lyrics; pub mod metadata; pub mod scheduler; pub mod scrobbler; diff --git a/plugins/pdk/rust/nd-pdk-capabilities/src/lyrics.rs b/plugins/pdk/rust/nd-pdk-capabilities/src/lyrics.rs new file mode 100644 index 00000000..16882aba --- /dev/null +++ b/plugins/pdk/rust/nd-pdk-capabilities/src/lyrics.rs @@ -0,0 +1,148 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the Lyrics capability. +// It is intended for use in Navidrome plugins built with extism-pdk. + +use serde::{Deserialize, Serialize}; + +// Helper functions for skip_serializing_if with numeric types +#[allow(dead_code)] +fn is_zero_i32(value: &i32) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_u32(value: &u32) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_i64(value: &i64) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_u64(value: &u64) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_f32(value: &f32) -> bool { *value == 0.0 } +#[allow(dead_code)] +fn is_zero_f64(value: &f64) -> bool { *value == 0.0 } +/// ArtistRef is a reference to an artist with name and optional MBID. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ArtistRef { + /// ID is the internal Navidrome artist ID (if known). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub id: String, + /// Name is the artist name. + #[serde(default)] + pub name: String, + /// MBID is the MusicBrainz ID for the artist. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbid: String, +} +/// GetLyricsRequest contains the track information for lyrics lookup. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetLyricsRequest { + #[serde(default)] + pub track: TrackInfo, +} +/// GetLyricsResponse contains the lyrics returned by the plugin. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetLyricsResponse { + #[serde(default)] + pub lyrics: Vec, +} +/// LyricsText represents a single set of lyrics in raw text format. +/// Text can be plain text or LRC format — Navidrome will parse it. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricsText { + #[serde(default, skip_serializing_if = "String::is_empty")] + pub lang: String, + #[serde(default)] + pub text: String, +} +/// TrackInfo contains track metadata. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackInfo { + /// ID is the internal Navidrome track ID. + #[serde(default)] + pub id: String, + /// Title is the track title. + #[serde(default)] + pub title: String, + /// Album is the album name. + #[serde(default)] + pub album: String, + /// Artist is the formatted artist name for display (e.g., "Artist1 • Artist2"). + #[serde(default)] + pub artist: String, + /// AlbumArtist is the formatted album artist name for display. + #[serde(default)] + pub album_artist: String, + /// Artists is the list of track artists. + #[serde(default)] + pub artists: Vec, + /// AlbumArtists is the list of album artists. + #[serde(default)] + pub album_artists: Vec, + /// Duration is the track duration in seconds. + #[serde(default)] + pub duration: f32, + /// TrackNumber is the track number on the album. + #[serde(default)] + pub track_number: i32, + /// DiscNumber is the disc number. + #[serde(default)] + pub disc_number: i32, + /// MBZRecordingID is the MusicBrainz recording ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbz_recording_id: String, + /// MBZAlbumID is the MusicBrainz album/release ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbz_album_id: String, + /// MBZReleaseGroupID is the MusicBrainz release group ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbz_release_group_id: String, + /// MBZReleaseTrackID is the MusicBrainz release track ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbz_release_track_id: String, +} + +/// Error represents an error from a capability method. +#[derive(Debug)] +pub struct Error { + pub message: String, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for Error {} + +impl Error { + pub fn new(message: impl Into) -> Self { + Self { message: message.into() } + } +} + +/// Lyrics requires all methods to be implemented. +/// Lyrics provides lyrics for a given track from external sources. +pub trait Lyrics { + /// GetLyrics + fn get_lyrics(&self, req: GetLyricsRequest) -> Result; +} + +/// Register all exports for the Lyrics capability. +/// This macro generates the WASM export functions for all trait methods. +#[macro_export] +macro_rules! register_lyrics { + ($plugin_type:ty) => { + #[extism_pdk::plugin_fn] + pub fn nd_lyrics_get_lyrics( + req: extism_pdk::Json<$crate::lyrics::GetLyricsRequest> + ) -> extism_pdk::FnResult> { + let plugin = <$plugin_type>::default(); + let result = $crate::lyrics::Lyrics::get_lyrics(&plugin, req.into_inner())?; + Ok(extism_pdk::Json(result)) + } + }; +} diff --git a/plugins/pdk/rust/nd-pdk-capabilities/src/scrobbler.rs b/plugins/pdk/rust/nd-pdk-capabilities/src/scrobbler.rs index 9dbedd04..2572712d 100644 --- a/plugins/pdk/rust/nd-pdk-capabilities/src/scrobbler.rs +++ b/plugins/pdk/rust/nd-pdk-capabilities/src/scrobbler.rs @@ -76,7 +76,7 @@ pub struct ScrobbleRequest { #[serde(default)] pub timestamp: i64, } -/// TrackInfo contains track metadata for scrobbling. +/// TrackInfo contains track metadata. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TrackInfo { diff --git a/plugins/testdata/test-lyrics/go.mod b/plugins/testdata/test-lyrics/go.mod new file mode 100644 index 00000000..fbbb23fc --- /dev/null +++ b/plugins/testdata/test-lyrics/go.mod @@ -0,0 +1,16 @@ +module test-lyrics + +go 1.25 + +require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/extism/go-pdk v1.1.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go diff --git a/plugins/testdata/test-lyrics/go.sum b/plugins/testdata/test-lyrics/go.sum new file mode 100644 index 00000000..af880eb5 --- /dev/null +++ b/plugins/testdata/test-lyrics/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ= +github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/testdata/test-lyrics/main.go b/plugins/testdata/test-lyrics/main.go new file mode 100644 index 00000000..0e485ceb --- /dev/null +++ b/plugins/testdata/test-lyrics/main.go @@ -0,0 +1,42 @@ +// Test lyrics plugin for Navidrome plugin system integration tests. +package main + +import ( + "fmt" + + "github.com/navidrome/navidrome/plugins/pdk/go/lyrics" + "github.com/navidrome/navidrome/plugins/pdk/go/pdk" +) + +func init() { + lyrics.Register(&testLyrics{}) +} + +type testLyrics struct{} + +func (t *testLyrics) GetLyrics(input lyrics.GetLyricsRequest) (lyrics.GetLyricsResponse, error) { + // Check for configured error + errMsg, hasErr := pdk.GetConfig("error") + if hasErr && errMsg != "" { + return lyrics.GetLyricsResponse{}, fmt.Errorf("%s", errMsg) + } + + // Check if we should omit language (to test default language handling) + noLang, hasNoLang := pdk.GetConfig("no_lang") + lang := "eng" + if hasNoLang && noLang == "true" { + lang = "" + } + + // Return test lyrics based on track info + return lyrics.GetLyricsResponse{ + Lyrics: []lyrics.LyricsText{ + { + Lang: lang, + Text: "Test lyrics for " + input.Track.Title + "\nBy " + input.Track.Artist, + }, + }, + }, nil +} + +func main() {} diff --git a/plugins/testdata/test-lyrics/manifest.json b/plugins/testdata/test-lyrics/manifest.json new file mode 100644 index 00000000..a61299e9 --- /dev/null +++ b/plugins/testdata/test-lyrics/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Test Lyrics", + "author": "Navidrome Test", + "version": "1.0.0", + "description": "A test lyrics plugin for integration testing" +} diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index 0e0ca606..484a91cc 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -19,6 +19,7 @@ import ( "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/external" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playlists" @@ -396,6 +397,7 @@ func setupTestDB() { core.NewShare(ds), playback.PlaybackServer(nil), metrics.NewNoopInstance(), + lyrics.NewLyrics(nil), ) } diff --git a/server/subsonic/album_lists_test.go b/server/subsonic/album_lists_test.go index 63c2614c..aac2d63d 100644 --- a/server/subsonic/album_lists_test.go +++ b/server/subsonic/album_lists_test.go @@ -27,7 +27,7 @@ var _ = Describe("Album Lists", func() { ds = &tests.MockDataStore{} auth.Init(ds) mockRepo = ds.Album(ctx).(*tests.MockAlbumRepo) - router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) w = httptest.NewRecorder() }) diff --git a/server/subsonic/api.go b/server/subsonic/api.go index d0d9bb16..8674a294 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -14,6 +14,7 @@ import ( "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/external" + lyricssvc "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" playlistsvc "github.com/navidrome/navidrome/core/playlists" @@ -48,12 +49,13 @@ type Router struct { share core.Share playback playback.PlaybackServer metrics metrics.Metrics + lyrics lyricssvc.Lyrics } func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver, players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker, playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer, - metrics metrics.Metrics, + metrics metrics.Metrics, lyrics lyricssvc.Lyrics, ) *Router { r := &Router{ ds: ds, @@ -69,6 +71,7 @@ func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreame share: share, playback: playback, metrics: metrics, + lyrics: lyrics, } r.Handler = r.routes() return r diff --git a/server/subsonic/media_annotation_test.go b/server/subsonic/media_annotation_test.go index 6f09f534..57809fbb 100644 --- a/server/subsonic/media_annotation_test.go +++ b/server/subsonic/media_annotation_test.go @@ -27,7 +27,7 @@ var _ = Describe("MediaAnnotationController", func() { ds = &tests.MockDataStore{} playTracker = &fakePlayTracker{} eventBroker = &fakeEventBroker{} - router = New(ds, nil, nil, nil, nil, nil, nil, eventBroker, nil, playTracker, nil, nil, nil) + router = New(ds, nil, nil, nil, nil, nil, nil, eventBroker, nil, playTracker, nil, nil, nil, nil) }) Describe("Scrobble", func() { diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index c16779e3..54fcb5e3 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -10,7 +10,6 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/resources" @@ -109,7 +108,7 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) { return response, nil } - structuredLyrics, err := lyrics.GetLyrics(r.Context(), &mediaFiles[0]) + structuredLyrics, err := api.lyrics.GetLyrics(r.Context(), &mediaFiles[0]) if err != nil { return nil, err } @@ -142,7 +141,7 @@ func (api *Router) GetLyricsBySongId(r *http.Request) (*responses.Subsonic, erro return nil, err } - structuredLyrics, err := lyrics.GetLyrics(r.Context(), mediaFile) + structuredLyrics, err := api.lyrics.GetLyrics(r.Context(), mediaFile) if err != nil { return nil, err } diff --git a/server/subsonic/media_retrieval_test.go b/server/subsonic/media_retrieval_test.go index 351b4e59..1a638f06 100644 --- a/server/subsonic/media_retrieval_test.go +++ b/server/subsonic/media_retrieval_test.go @@ -14,6 +14,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/core/artwork" + "github.com/navidrome/navidrome/core/lyrics" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/tests" @@ -33,7 +34,7 @@ var _ = Describe("MediaRetrievalController", func() { MockedMediaFile: mockRepo, } artwork = &fakeArtwork{data: "image data"} - router = New(ds, artwork, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + router = New(ds, artwork, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, lyrics.NewLyrics(nil)) w = httptest.NewRecorder() DeferCleanup(configtest.SetupConfig()) conf.Server.LyricsPriority = "embedded,.lrc" diff --git a/server/subsonic/opensubsonic_test.go b/server/subsonic/opensubsonic_test.go index 58dca682..c02b262b 100644 --- a/server/subsonic/opensubsonic_test.go +++ b/server/subsonic/opensubsonic_test.go @@ -19,7 +19,7 @@ var _ = Describe("GetOpenSubsonicExtensions", func() { ) BeforeEach(func() { - router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) w = httptest.NewRecorder() r = httptest.NewRequest("GET", "/getOpenSubsonicExtensions?f=json", nil) }) diff --git a/server/subsonic/playlists_test.go b/server/subsonic/playlists_test.go index d99e244b..86c17b39 100644 --- a/server/subsonic/playlists_test.go +++ b/server/subsonic/playlists_test.go @@ -24,7 +24,7 @@ var _ = Describe("buildPlaylist", func() { BeforeEach(func() { ds = &tests.MockDataStore{} - router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) ctx = context.Background() }) @@ -224,7 +224,7 @@ var _ = Describe("UpdatePlaylist", func() { BeforeEach(func() { ds = &tests.MockDataStore{} playlists = &fakePlaylists{} - router = New(ds, nil, nil, nil, nil, nil, nil, nil, playlists, nil, nil, nil, nil) + router = New(ds, nil, nil, nil, nil, nil, nil, nil, playlists, nil, nil, nil, nil, nil) }) It("clears the comment when parameter is empty", func() { diff --git a/server/subsonic/searching_test.go b/server/subsonic/searching_test.go index 7f7de381..d4b7e970 100644 --- a/server/subsonic/searching_test.go +++ b/server/subsonic/searching_test.go @@ -21,7 +21,7 @@ var _ = Describe("Search", func() { ds = &tests.MockDataStore{} auth.Init(ds) - router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) // Get references to the mock repositories so we can inspect their Options mockAlbumRepo = ds.Album(nil).(*tests.MockAlbumRepo)