From f8674115d27ddb6ab5f8fbf33bd0e05f102bede0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:01:52 -0700 Subject: [PATCH] fix(playlists): import smart playlists when PlaylistsPath uses Windows separators Both the relative folder path and each PlaylistsPath pattern are converted with filepath.ToSlash before doublestar.Match. The existing in-path example has its Windows skip removed and joins its patterns with filepath.ListSeparator. New cases cover a path from filepath.Join, the same folder written with forward slashes, and neighboring directories that stay outside the configured path after normalization. Fixes #6276 Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- core/playlists/import_test.go | 41 +++++++++++++++++++++++++++++++++-- core/playlists/playlists.go | 4 +++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/playlists/import_test.go b/core/playlists/import_test.go index 25960f0fe..050bbf48b 100644 --- a/core/playlists/import_test.go +++ b/core/playlists/import_test.go @@ -1140,16 +1140,53 @@ var _ = Describe("Playlists - Import", func() { }) It("returns true if folder is in PlaylistsPath", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-playlists)") - conf.Server.PlaylistsPath = "other/**:playlists/**" + conf.Server.PlaylistsPath = strings.Join([]string{"other/**", "playlists/**"}, string(filepath.ListSeparator)) Expect(playlists.InPath(folder)).To(BeTrue()) }) + It("returns true if folder matches a PlaylistsPath joined with the OS separator", func() { + nsp := model.Folder{ + LibraryPath: "/music", + Path: "Playlists", + Name: "navidrome", + } + conf.Server.PlaylistsPath = filepath.Join("Playlists", "navidrome") + Expect(playlists.InPath(nsp)).To(BeTrue()) + }) + + It("returns true if folder matches a forward-slash PlaylistsPath", func() { + nsp := model.Folder{ + LibraryPath: "/music", + Path: "Playlists", + Name: "navidrome", + } + conf.Server.PlaylistsPath = "Playlists/navidrome" + Expect(playlists.InPath(nsp)).To(BeTrue()) + }) + It("returns false if folder is not in PlaylistsPath", func() { conf.Server.PlaylistsPath = "other" Expect(playlists.InPath(folder)).To(BeFalse()) }) + It("returns false for a different directory after normalization", func() { + nsp := model.Folder{ + LibraryPath: "/music", + Path: "Playlists", + Name: "navidrome", + } + sibling := model.Folder{ + LibraryPath: "/music", + Path: "Playlists", + Name: "other", + } + conf.Server.PlaylistsPath = filepath.Join("Other", "dir") + Expect(playlists.InPath(nsp)).To(BeFalse()) + + conf.Server.PlaylistsPath = filepath.Join("Playlists", "navidrome", "**") + Expect(playlists.InPath(sibling)).To(BeFalse()) + }) + It("returns true if for a playlist in root of MusicFolder if PlaylistsPath is '.'", func() { conf.Server.PlaylistsPath = "." Expect(playlists.InPath(folder)).To(BeFalse()) diff --git a/core/playlists/playlists.go b/core/playlists/playlists.go index c9bc03b97..9bf03a726 100644 --- a/core/playlists/playlists.go +++ b/core/playlists/playlists.go @@ -78,8 +78,10 @@ func InPath(folder model.Folder) bool { return true } rel, _ := filepath.Rel(folder.LibraryPath, folder.AbsolutePath()) + // doublestar splits only on / and treats \ as an escape, so normalize OS separators first. + rel = filepath.ToSlash(rel) for path := range strings.SplitSeq(conf.Server.PlaylistsPath, string(filepath.ListSeparator)) { - if match, _ := doublestar.Match(path, rel); match { + if match, _ := doublestar.Match(filepath.ToSlash(path), rel); match { return true } }