diff --git a/consts/consts.go b/consts/consts.go index 42e9ec42f..86731de82 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -90,6 +90,7 @@ const ( I18nFolder = "i18n" ScanIgnoreFile = ".ndignore" + NoMediaFile = ".nomedia" ArtworkFolder = "artwork" // HashedArtworkFolder is a subtree of ArtworkFolder, kept apart from the name-addressed // upload folders beside it so Prune's sweep never reaches them. diff --git a/scanner/README.md b/scanner/README.md index b2c682381..0f80ddffb 100644 --- a/scanner/README.md +++ b/scanner/README.md @@ -175,7 +175,7 @@ flowchart TD 1. **Folder Traversal** - Uses `walkDirTree` to traverse the directory structure - Handles symbolic links and hidden files - - Processes `.ndignore` files for exclusions + - Processes `.ndignore` files for exclusions, and skips folders containing a `.nomedia` file - Maps files to appropriate types (audio, image, playlist) 2. **Metadata Extraction** diff --git a/scanner/ignore_checker.go b/scanner/ignore_checker.go index f0aedb079..d65d3791e 100644 --- a/scanner/ignore_checker.go +++ b/scanner/ignore_checker.go @@ -12,6 +12,8 @@ import ( ignore "github.com/sabhiram/go-gitignore" ) +const ignoreAllPattern = "**/*" + // IgnoreChecker manages .ndignore patterns using a stack-based approach. // Use Push() to add patterns when entering a folder, Pop() when leaving, // and ShouldIgnore() to check if a path should be ignored. @@ -102,8 +104,13 @@ func (ic *IgnoreChecker) ShouldIgnore(ctx context.Context, relPath string) bool // loadPatternsFromFolder reads the .ndignore file in the specified folder and returns the patterns. // If the file doesn't exist, returns an empty slice. -// If the file exists but is empty, returns a pattern to ignore everything ("**/*"). +// If the file exists but is empty, or a .nomedia file exists, returns a pattern to ignore everything. func (ic *IgnoreChecker) loadPatternsFromFolder(ctx context.Context, folder string) []string { + if _, err := fs.Stat(ic.fsys, path.Join(folder, consts.NoMediaFile)); err == nil { + log.Trace(ctx, "Scanner: .nomedia file found, ignoring everything", "path", folder) + return []string{ignoreAllPattern} + } + ignoreFilePath := path.Join(folder, consts.ScanIgnoreFile) var patterns []string @@ -138,7 +145,7 @@ func (ic *IgnoreChecker) loadPatternsFromFolder(ctx context.Context, folder stri // If the .ndignore file is empty, ignore everything if len(patterns) == 0 { log.Trace(ctx, "Scanner: .ndignore file is empty, ignoring everything", "path", folder) - patterns = []string{"**/*"} + patterns = []string{ignoreAllPattern} } return patterns diff --git a/scanner/ignore_checker_test.go b/scanner/ignore_checker_test.go index 5378ed4fa..610073275 100644 --- a/scanner/ignore_checker_test.go +++ b/scanner/ignore_checker_test.go @@ -37,6 +37,20 @@ var _ = Describe("IgnoreChecker", func() { }) }) + DescribeTable("when .nomedia file exists", + func(files fstest.MapFS) { + ic = newIgnoreChecker(files) + patterns := ic.loadPatternsFromFolder(ctx, ".") + Expect(patterns).To(Equal([]string{"**/*"})) + }, + Entry("empty", fstest.MapFS{".nomedia": &fstest.MapFile{}}), + Entry("with content", fstest.MapFS{".nomedia": &fstest.MapFile{Data: []byte("*.txt")}}), + Entry("alongside .ndignore", fstest.MapFS{ + ".nomedia": &fstest.MapFile{}, + ".ndignore": &fstest.MapFile{Data: []byte("*.txt")}, + }), + ) + DescribeTable("parsing .ndignore content", func(content string, expectedPatterns []string) { fsys := fstest.MapFS{ diff --git a/scanner/walk_dir_tree_test.go b/scanner/walk_dir_tree_test.go index 43939e5c2..826005c88 100644 --- a/scanner/walk_dir_tree_test.go +++ b/scanner/walk_dir_tree_test.go @@ -53,6 +53,9 @@ var _ = Describe("walk_dir_tree", func() { "root/h/.hidden.mp3": {}, "root/i/.git/config": {}, "root/i/.streams/stream.mp3": {}, + "root/j/.nomedia": {}, + "root/j/f1.mp3": {}, + "root/j/sub/f2.mp3": {}, }, } job = &scanJob{ @@ -100,6 +103,8 @@ var _ = Describe("walk_dir_tree", func() { Expect(folders["root/c"].audioFiles).To(BeEmpty()) Expect(folders["root/c"].imageFiles).To(BeEmpty()) Expect(folders).ToNot(HaveKey("root/d")) + Expect(folders["root/j"].audioFiles).To(BeEmpty()) + Expect(folders).ToNot(HaveKey("root/j/sub")) // By default (Scanner.IgnoreDotFolders == true), dot-prefixed // folders are skipped, dot-prefixed files are not indexed, and @@ -122,8 +127,8 @@ var _ = Describe("walk_dir_tree", func() { Expect(folders["root/f"].audioFiles).ToNot(HaveKey("evil.mp3")) } }, - Entry("with symlinks enabled", true, 11), - Entry("with symlinks disabled", false, 10), + Entry("with symlinks enabled", true, 12), + Entry("with symlinks disabled", false, 11), ) DescribeTable("dot-prefixed folders with IgnoreDotFolders disabled",