From a6e37e3ac1dfca644bc28c38c5e4d675d52d7c43 Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Sun, 9 Aug 2026 16:55:56 +0200 Subject: [PATCH 1/4] fix(scanner): use forward slashes in folder watcher path handling on Windows (#5381) Signed-off-by: junkerderprovinz --- scanner/scanner_test.go | 2 -- scanner/watcher.go | 21 +++++++++++++-------- scanner/watcher_test.go | 6 ------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 4ce8cce1e..5863f1fe3 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -245,7 +245,6 @@ var _ = Describe("Scanner", Ordered, func() { }) It("should update the album", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") Expect(runScanner(ctx, true)).To(Succeed()) albums, err := ds.Album().GetAll(ctx, model.QueryOptions{Filters: squirrel.Eq{"album.name": "Help!"}}) @@ -533,7 +532,6 @@ var _ = Describe("Scanner", Ordered, func() { var beatlesMBID = uuid.NewString() BeforeEach(func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") By("Having two MP3 albums") beatles := _t{ "artist": "The Beatles", diff --git a/scanner/watcher.go b/scanner/watcher.go index 1ac5468f0..8c1b3f558 100644 --- a/scanner/watcher.go +++ b/scanner/watcher.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io/fs" + "path" "path/filepath" "strings" "sync" @@ -250,6 +251,10 @@ func (w *watcher) processLibraryEvents(ctx context.Context, lib *model.Library, log.Error(ctx, "Error getting relative path", "libraryID", lib.ID, "absolutePath", absLibPath, "path", path, err) continue } + // fs.FS paths always use "/", but filepath.Rel returns OS separators, + // so normalize before feeding the value to fs lookups and downstream + // notifications (otherwise back-slashes break matching on Windows). + path = filepath.ToSlash(path) if isIgnoredPath(ctx, fsys, path) { log.Trace(ctx, "Ignoring change", "libraryID", lib.ID, "path", path) @@ -282,13 +287,13 @@ func (w *watcher) processLibraryEvents(ctx context.Context, lib *model.Library, // resolveFolderPath takes a path (which may be a file or directory) and returns // the folder path to scan. If the path is a file, it walks up to find the parent // directory. Returns empty string if the path should scan the library root. -func resolveFolderPath(fsys fs.FS, path string) string { +func resolveFolderPath(fsys fs.FS, p string) string { // Handle root paths immediately - if path == "." || path == "" { + if p == "." || p == "" { return "" } - folderPath := path + folderPath := p for { info, err := fs.Stat(fsys, folderPath) if err == nil && info.IsDir() { @@ -299,13 +304,13 @@ func resolveFolderPath(fsys fs.FS, path string) string { // Reached root, scan entire library return "" } - // Walk up the tree - dir, _ := filepath.Split(folderPath) - if dir == "" || dir == "." { + // Walk up the tree. fs.FS paths always use "/", so use path.Dir + // (filepath would emit "\" on Windows and never match the slash keys). + parent := path.Dir(folderPath) + if parent == "." || parent == folderPath { return "" } - // Remove trailing slash - folderPath = filepath.Clean(dir) + folderPath = parent } } diff --git a/scanner/watcher_test.go b/scanner/watcher_test.go index 12c94602c..0a35958dd 100644 --- a/scanner/watcher_test.go +++ b/scanner/watcher_test.go @@ -382,7 +382,6 @@ var _ = Describe("Watcher", func() { }) It("should NOT send notification when nested ignored folder is deleted", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") startEventProcessing() // Simulate deletion of music/rock/artist/temp (matches **/temp) @@ -396,7 +395,6 @@ var _ = Describe("Watcher", func() { }) It("should send notification for non-ignored nested folder", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") startEventProcessing() // Simulate change in music/rock/artist (doesn't match any pattern) @@ -421,7 +419,6 @@ var _ = Describe("Watcher", func() { }) It("should NOT send notification for file changes in ignored folders", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") startEventProcessing() // Simulate file change in rock/_TEMP/file.mp3 @@ -504,13 +501,11 @@ var _ = Describe("resolveFolderPath", func() { }) It("walks up to parent directory when given a file path", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") result := resolveFolderPath(mockFS, "artist1/album1/track1.mp3") Expect(result).To(Equal("artist1/album1")) }) It("walks up multiple levels if needed", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") result := resolveFolderPath(mockFS, "artist1/album1/nonexistent/file.mp3") Expect(result).To(Equal("artist1/album1")) }) @@ -531,7 +526,6 @@ var _ = Describe("resolveFolderPath", func() { }) It("handles nested file paths correctly", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") result := resolveFolderPath(mockFS, "artist1/album2/song.flac") Expect(result).To(Equal("artist1/album2")) }) From 2983e0dd9a9a5cb53231cc3bb79568bcf131020b Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Fri, 9 Oct 2026 20:48:48 +0200 Subject: [PATCH 2/4] refactor(scanner): shorten watcher path separator comments Signed-off-by: junkerderprovinz --- scanner/watcher.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scanner/watcher.go b/scanner/watcher.go index 8c1b3f558..da1969916 100644 --- a/scanner/watcher.go +++ b/scanner/watcher.go @@ -251,9 +251,8 @@ func (w *watcher) processLibraryEvents(ctx context.Context, lib *model.Library, log.Error(ctx, "Error getting relative path", "libraryID", lib.ID, "absolutePath", absLibPath, "path", path, err) continue } - // fs.FS paths always use "/", but filepath.Rel returns OS separators, - // so normalize before feeding the value to fs lookups and downstream - // notifications (otherwise back-slashes break matching on Windows). + // filepath.Rel returns OS separators, but fs.FS lookups and scan targets + // need "/" (os.DirFS rejects "\" on Windows). path = filepath.ToSlash(path) if isIgnoredPath(ctx, fsys, path) { @@ -304,8 +303,7 @@ func resolveFolderPath(fsys fs.FS, p string) string { // Reached root, scan entire library return "" } - // Walk up the tree. fs.FS paths always use "/", so use path.Dir - // (filepath would emit "\" on Windows and never match the slash keys). + // Walk up the tree with path.Dir, since fs.FS paths use "/" on every OS parent := path.Dir(folderPath) if parent == "." || parent == folderPath { return "" From 9ebdf7c0ab913735770f9f616703511870f044b3 Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Fri, 9 Oct 2026 20:48:48 +0200 Subject: [PATCH 3/4] test(scanner): run artwork re-enqueue and quick scan artist stats specs on Windows Signed-off-by: junkerderprovinz --- scanner/scanner_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 5863f1fe3..9a97fcb2f 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -226,7 +226,6 @@ var _ = Describe("Scanner", Ordered, func() { }) It("should re-enqueue the album artwork even though it already resolved", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") Expect(runScanner(ctx, true)).To(Succeed()) resolveQueuedArtwork() @@ -1199,7 +1198,6 @@ var _ = Describe("Scanner", Ordered, func() { }) It("should update artist stats during quick scans when new albums are added", func() { - tests.SkipOnWindows("path separator bug (#TBD-path-sep-scanner)") // Don't use the mocked artist repo for this test - we need the real one ds.MockedArtist = nil From 57dd97e910c3d62fcf9fef087a7a27e41a32ebaf Mon Sep 17 00:00:00 2001 From: junkerderprovinz Date: Thu, 15 Oct 2026 13:45:26 +0200 Subject: [PATCH 4/4] test(scanner): skip move detection specs on Windows --- scanner/scanner_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 9a97fcb2f..8d99b17f8 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -597,6 +597,7 @@ var _ = Describe("Scanner", Ordered, func() { }) It("detects a file was moved to a different folder", func() { + tests.SkipOnWindows("coarse clock can give a new track the scan start time, so the move is missed") By("Storing the original ID") original, err := findByPath("The Beatles/Revolver/02 - Eleanor Rigby.mp3") Expect(err).ToNot(HaveOccurred()) @@ -629,6 +630,7 @@ var _ = Describe("Scanner", Ordered, func() { }) It("detects a move after a scan is interrupted by an error", func() { + tests.SkipOnWindows("coarse clock can give a new track the scan start time, so the move is missed") By("Storing the original ID") By("Moving the file to a different folder") fsys.Move("The Beatles/Revolver/01 - Taxman.mp3", "The Beatles/Help!/01 - Taxman.mp3") @@ -656,6 +658,7 @@ var _ = Describe("Scanner", Ordered, func() { }) It("detects file format upgrades", func() { + tests.SkipOnWindows("coarse clock can give a new track the scan start time, so the move is missed") By("Storing the original ID") original, err := findByPath("The Beatles/Revolver/02 - Eleanor Rigby.mp3") Expect(err).ToNot(HaveOccurred())