f03ca44a8e
* feat(plugins): add lyrics provider plugin capability Refactor the lyrics system from a static function to an interface-based service that supports WASM plugin providers. Plugins listed in the LyricsPriority config (alongside "embedded" and file extensions) are now resolved through the plugin system. Includes capability definition, Go/Rust PDK, adapter, Wire integration, and tests for plugin fallback behavior. * test(plugins): add lyrics capability integration test with test plugin * fix(plugins): default lyrics language to 'xxx' when plugin omits it Per the OpenSubsonic spec, the server must return 'und' or 'xxx' when the lyrics language is unknown. The lyrics plugin adapter was passing an empty string through when a plugin didn't provide a language value. This defaults the language to 'xxx', consistent with all other callers of model.ToLyrics() in the codebase. * refactor(plugins): rename lyrics import to improve clarity Signed-off-by: Deluan <deluan@navidrome.org> * refactor(lyrics): update TrackInfo description for clarity Signed-off-by: Deluan <deluan@navidrome.org> * fix(lyrics): enhance lyrics plugin handling and case sensitivity Signed-off-by: Deluan <deluan@navidrome.org> * fix(plugins): update payload type to string with byte format for task data Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
293 lines
8.9 KiB
Go
293 lines
8.9 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/criteria"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ playlists.Playlists = (*fakePlaylists)(nil)
|
|
|
|
var _ = Describe("buildPlaylist", func() {
|
|
var router *Router
|
|
var ds model.DataStore
|
|
var ctx context.Context
|
|
var playlist model.Playlist
|
|
|
|
BeforeEach(func() {
|
|
ds = &tests.MockDataStore{}
|
|
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
|
ctx = context.Background()
|
|
})
|
|
|
|
Describe("normal playlist", func() {
|
|
BeforeEach(func() {
|
|
createdAt := time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
updatedAt := time.Date(2023, 2, 20, 14, 45, 0, 0, time.UTC)
|
|
|
|
playlist = model.Playlist{
|
|
ID: "pls-1",
|
|
Name: "My Playlist",
|
|
Comment: "Test comment",
|
|
OwnerName: "admin",
|
|
OwnerID: "1234",
|
|
Public: true,
|
|
SongCount: 10,
|
|
Duration: 600,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
})
|
|
|
|
Context("with minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "minimal-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns only basic fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
|
|
// These should not be set
|
|
Expect(result.Comment).To(BeEmpty())
|
|
Expect(result.Owner).To(BeEmpty())
|
|
Expect(result.Public).To(BeFalse())
|
|
Expect(result.CoverArt).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Context("with non-minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "regular-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeTrue())
|
|
})
|
|
|
|
It("returns all fields when as owner", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "1234", UserName: "admin"})
|
|
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("when minimal clients list is empty", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = ""
|
|
player := model.Player{Client: "any-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("when no player in context", func() {
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("smart playlist", func() {
|
|
evaluatedAt := time.Date(2023, 2, 20, 15, 45, 0, 0, time.UTC)
|
|
validUntil := evaluatedAt.Add(5 * time.Second)
|
|
|
|
BeforeEach(func() {
|
|
createdAt := time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
updatedAt := time.Date(2023, 2, 20, 14, 45, 0, 0, time.UTC)
|
|
|
|
playlist = model.Playlist{
|
|
ID: "pls-1",
|
|
Name: "My Playlist",
|
|
Comment: "Test comment",
|
|
OwnerName: "admin",
|
|
OwnerID: "1234",
|
|
Public: true,
|
|
SongCount: 10,
|
|
Duration: 600,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
EvaluatedAt: &evaluatedAt,
|
|
Rules: &criteria.Criteria{
|
|
Expression: criteria.All{criteria.Contains{"title": "title"}},
|
|
},
|
|
}
|
|
})
|
|
|
|
Context("with minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "minimal-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns only basic fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(evaluatedAt))
|
|
|
|
// These should not be set
|
|
Expect(result.Comment).To(BeEmpty())
|
|
Expect(result.Owner).To(BeEmpty())
|
|
Expect(result.Public).To(BeFalse())
|
|
Expect(result.CoverArt).To(BeEmpty())
|
|
Expect(result.OpenSubsonicPlaylist).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("with non-minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "regular-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(*playlist.EvaluatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeTrue())
|
|
Expect(result.ValidUntil).To(Equal(&validUntil))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("UpdatePlaylist", func() {
|
|
var router *Router
|
|
var ds model.DataStore
|
|
var playlists *fakePlaylists
|
|
|
|
BeforeEach(func() {
|
|
ds = &tests.MockDataStore{}
|
|
playlists = &fakePlaylists{}
|
|
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() {
|
|
r := newGetRequest("playlistId=123", "comment=")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastComment).ToNot(BeNil())
|
|
Expect(*playlists.lastComment).To(Equal(""))
|
|
})
|
|
|
|
It("leaves comment unchanged when parameter is missing", func() {
|
|
r := newGetRequest("playlistId=123")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastComment).To(BeNil())
|
|
})
|
|
|
|
It("sets public to true when parameter is 'true'", func() {
|
|
r := newGetRequest("playlistId=123", "public=true")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).ToNot(BeNil())
|
|
Expect(*playlists.lastPublic).To(BeTrue())
|
|
})
|
|
|
|
It("sets public to false when parameter is 'false'", func() {
|
|
r := newGetRequest("playlistId=123", "public=false")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).ToNot(BeNil())
|
|
Expect(*playlists.lastPublic).To(BeFalse())
|
|
})
|
|
|
|
It("leaves public unchanged when parameter is missing", func() {
|
|
r := newGetRequest("playlistId=123")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).To(BeNil())
|
|
})
|
|
})
|
|
|
|
type fakePlaylists struct {
|
|
playlists.Playlists
|
|
lastPlaylistID string
|
|
lastName *string
|
|
lastComment *string
|
|
lastPublic *bool
|
|
lastAdd []string
|
|
lastRemove []int
|
|
}
|
|
|
|
func (f *fakePlaylists) Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error {
|
|
f.lastPlaylistID = playlistID
|
|
f.lastName = name
|
|
f.lastComment = comment
|
|
f.lastPublic = public
|
|
f.lastAdd = idsToAdd
|
|
f.lastRemove = idxToRemove
|
|
return nil
|
|
}
|