feat: add support for public/private playlists in NSP import

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan
2026-01-16 19:10:19 -05:00
parent b9247ba34e
commit c5447a637a
4 changed files with 56 additions and 1 deletions
+13 -1
View File
@@ -168,6 +168,11 @@ func (s *playlists) parseNSP(_ context.Context, pls *model.Playlist, reader io.R
if nsp.Comment != "" { if nsp.Comment != "" {
pls.Comment = nsp.Comment pls.Comment = nsp.Comment
} }
if nsp.Public != nil {
pls.Public = *nsp.Public
} else {
pls.Public = conf.Server.DefaultPlaylistPublicVisibility
}
return nil return nil
} }
@@ -409,7 +414,10 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist)
} else { } else {
log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName) log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName)
newPls.OwnerID = owner.ID newPls.OwnerID = owner.ID
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility // For NSP files, Public may already be set from the file; for M3U, use server default
if !newPls.IsSmartPlaylist() {
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility
}
} }
return s.ds.Playlist(ctx).Put(newPls) return s.ds.Playlist(ctx).Put(newPls)
} }
@@ -473,6 +481,7 @@ type nspFile struct {
criteria.Criteria criteria.Criteria
Name string `json:"name"` Name string `json:"name"`
Comment string `json:"comment"` Comment string `json:"comment"`
Public *bool `json:"public"`
} }
func (i *nspFile) UnmarshalJSON(data []byte) error { func (i *nspFile) UnmarshalJSON(data []byte) error {
@@ -483,5 +492,8 @@ func (i *nspFile) UnmarshalJSON(data []byte) error {
} }
i.Name, _ = m["name"].(string) i.Name, _ = m["name"].(string)
i.Comment, _ = m["comment"].(string) i.Comment, _ = m["comment"].(string)
if public, ok := m["public"].(bool); ok {
i.Public = &public
}
return json.Unmarshal(data, &i.Criteria) return json.Unmarshal(data, &i.Criteria)
} }
+21
View File
@@ -112,6 +112,27 @@ var _ = Describe("Playlists", func() {
_, err := ps.ImportFile(ctx, folder, "invalid_json.nsp") _, err := ps.ImportFile(ctx, folder, "invalid_json.nsp")
Expect(err.Error()).To(ContainSubstring("line 19, column 1: invalid character '\\n'")) Expect(err.Error()).To(ContainSubstring("line 19, column 1: invalid character '\\n'"))
}) })
It("parses NSP with public: true and creates public playlist", func() {
pls, err := ps.ImportFile(ctx, folder, "public_playlist.nsp")
Expect(err).ToNot(HaveOccurred())
Expect(pls.Name).To(Equal("Public Playlist"))
Expect(pls.Public).To(BeTrue())
})
It("parses NSP with public: false and creates private playlist", func() {
pls, err := ps.ImportFile(ctx, folder, "private_playlist.nsp")
Expect(err).ToNot(HaveOccurred())
Expect(pls.Name).To(Equal("Private Playlist"))
Expect(pls.Public).To(BeFalse())
})
It("uses server default when public field is absent", func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DefaultPlaylistPublicVisibility = true
pls, err := ps.ImportFile(ctx, folder, "recently_played.nsp")
Expect(err).ToNot(HaveOccurred())
Expect(pls.Name).To(Equal("Recently Played"))
Expect(pls.Public).To(BeTrue()) // Should be true since server default is true
})
}) })
Describe("Cross-library relative paths", func() { Describe("Cross-library relative paths", func() {
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Private Playlist",
"comment": "A smart playlist that is explicitly private",
"public": false,
"all": [
{"is": {"loved": true}}
],
"sort": "title",
"order": "asc",
"limit": 100
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Public Playlist",
"comment": "A smart playlist that is public",
"public": true,
"all": [
{"inTheLast": {"lastPlayed": 30}}
],
"sort": "lastPlayed",
"order": "desc",
"limit": 50
}