diff --git a/Directory.Packages.props b/Directory.Packages.props
index cdce608d9c..a24b081dd2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,6 +21,7 @@
+
@@ -88,4 +89,4 @@
-
\ No newline at end of file
+
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index d99923b4fc..15843730e9 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -64,6 +64,10 @@
+
+
+
+
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index b01fd93a7b..f29a0b3ad7 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
{
if (parent is not null)
{
- // Ignore extras folders but allow it at the collection level
+ // Ignore extras for unsupported types
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
&& parent is not AggregateFolder
&& parent is not UserRootFolder)
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library
{
if (parent is not null)
{
- // Don't resolve these into audio files
+ // Don't resolve theme songs
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
{
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
new file mode 100644
index 0000000000..2c186c9173
--- /dev/null
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+///
+/// Resolver rule class for ignoring files via .ignore.
+///
+public class DotIgnoreIgnoreRule : IResolverIgnoreRule
+{
+ private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
+ {
+ var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
+ if (ignoreFile.Exists)
+ {
+ return ignoreFile;
+ }
+
+ var parentDir = directory.Parent;
+ if (parentDir == null || parentDir.FullName == directory.FullName)
+ {
+ return null;
+ }
+
+ return FindIgnoreFile(parentDir);
+ }
+
+ ///
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
+ {
+ return IsIgnored(fileInfo, parent);
+ }
+
+ ///
+ /// Checks whether or not the file is ignored.
+ ///
+ /// The file information.
+ /// The parent BaseItem.
+ /// True if the file should be ignored.
+ public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
+ {
+ var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
+ if (string.IsNullOrEmpty(parentDirPath))
+ {
+ return false;
+ }
+
+ var folder = new DirectoryInfo(parentDirPath);
+ var ignoreFile = FindIgnoreFile(folder);
+ if (ignoreFile is null)
+ {
+ return false;
+ }
+
+ string ignoreFileString;
+ using (var reader = ignoreFile.OpenText())
+ {
+ ignoreFileString = reader.ReadToEnd();
+ }
+
+ if (string.IsNullOrEmpty(ignoreFileString))
+ {
+ // Ignore directory if we just have the file
+ return true;
+ }
+
+ // If file has content, base ignoring off the content .gitignore-style rules
+ var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ var ignore = new Ignore.Ignore();
+ ignore.Add(ignoreRules);
+
+ return ignore.IsIgnored(fileInfo.FullName);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index bb45dd87e9..25ddade829 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using DotNet.Globbing;
namespace Emby.Server.Implementations.Library
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a6eddbbc3b..21c953fb2d 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -649,10 +649,11 @@ namespace Emby.Server.Implementations.Library
args.FileSystemChildren = files;
}
- // Check to see if we should resolve based on our contents
- if (args.IsDirectory && !ShouldResolvePathContents(args))
+ // Filter content based on ignore rules
+ if (args.IsDirectory)
{
- return null;
+ var filtered = args.GetActualFileSystemChildren().ToArray();
+ args.FileSystemChildren = filtered ?? [];
}
return ResolveItem(args, resolvers);
@@ -683,17 +684,6 @@ namespace Emby.Server.Implementations.Library
return newList;
}
- ///
- /// Determines whether a path should be ignored based on its contents - called after the contents have been read.
- ///
- /// The args.
- /// true if XXXX, false otherwise.
- private static bool ShouldResolvePathContents(ItemResolveArgs args)
- {
- // Ignore any folders containing a file called .ignore
- return !args.ContainsFileSystemEntryByName(".ignore");
- }
-
public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
@@ -2724,16 +2714,18 @@ namespace Emby.Server.Implementations.Library
public IEnumerable FindExtras(BaseItem owner, IReadOnlyList fileSystemChildren, IDirectoryService directoryService)
{
+ // Apply .ignore rules
+ var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
}
- var count = fileSystemChildren.Count;
+ var count = filtered.Count;
for (var i = 0; i < count; i++)
{
- var current = fileSystemChildren[i];
+ var current = filtered[i];
if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
{
var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
new file mode 100644
index 0000000000..d677c9f091
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
@@ -0,0 +1,30 @@
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class DotIgnoreIgnoreRuleTest
+{
+ [Fact]
+ public void Test()
+ {
+ var ignore = new Ignore.Ignore();
+ ignore.Add("SPs");
+ Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
+ Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
+ Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
+ }
+
+ [Fact]
+ public void TestNegatePattern()
+ {
+ var ignore = new Ignore.Ignore();
+ ignore.Add("SPs");
+ ignore.Add("!thebestshot.mkv");
+ Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
+ Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
+ Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
+ Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv"));
+ Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv"));
+ Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv"));
+ }
+}