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