diff --git a/Shokofin/Configuration/MediaFolderConfigurationService.cs b/Shokofin/Configuration/MediaFolderConfigurationService.cs index f45a6a36..4d06d1ab 100644 --- a/Shokofin/Configuration/MediaFolderConfigurationService.cs +++ b/Shokofin/Configuration/MediaFolderConfigurationService.cs @@ -404,10 +404,7 @@ private async Task CreateConfigurationForPath(Guid lib } else { var foundLocations = new List<(int, string)>(); - var samplePaths = FileSystem.GetFilePaths(mediaFolder.Path, true) - .Where(path => NamingOptions.VideoFileExtensions.Contains(Path.GetExtension(path))) - .Take(101) // 101 as a tie breaker - .ToList(); + var samplePaths = GetSamplePaths(mediaFolder.Path).ToList(); Logger.LogDebug("Asking remote server if it knows any of the {Count} sampled files in {Path}. (Library={LibraryId})", samplePaths.Count > 100 ? 100 : samplePaths.Count, mediaFolder.Path, libraryId); foreach (var path in samplePaths) { @@ -480,5 +477,52 @@ private async Task CreateConfigurationForPath(Guid lib return mediaFolderConfig; } + /// + /// Max number of sample paths to return. We use an odd number as a tie + /// breaker in case of multiple different matches. + /// + private const int MaxSamplePaths = 101; + + /// + /// Gets the sample paths for the given media folder. + /// + /// The media folder to get the sample paths + /// for. + /// The sample paths for the given media folder. + private IEnumerable GetSamplePaths(string mediaFolder) + { + var count = 0; + var rootFiles = FileSystem.GetFilePaths(mediaFolder, false); + foreach (var filePath in rootFiles) + { + if (IgnorePatterns.ShouldIgnore(filePath)) + continue; + + yield return filePath; + + if (++count == MaxSamplePaths) + yield break; + } + + var rootFolders = FileSystem.GetDirectoryPaths(mediaFolder, false); + foreach (var directoryPath in rootFolders) + { + if (IgnorePatterns.ShouldIgnore(directoryPath)) + continue; + + var files = FileSystem.GetFilePaths(directoryPath, true); + foreach (var filePath in files) + { + if (IgnorePatterns.ShouldIgnore(filePath)) + continue; + + yield return filePath; + + if (++count == MaxSamplePaths) + yield break; + } + } + } + #endregion } diff --git a/Shokofin/Resolvers/ShokoIgnoreRule.cs b/Shokofin/Resolvers/ShokoIgnoreRule.cs index 1c6bcc10..cda8b21b 100644 --- a/Shokofin/Resolvers/ShokoIgnoreRule.cs +++ b/Shokofin/Resolvers/ShokoIgnoreRule.cs @@ -74,6 +74,12 @@ public async Task ShouldFilterItem(Folder? parent, FileSystemMetadata file if (!Lookup.IsEnabledForItem(parent, out var isSoleProvider)) return false; + // Don't even bother continuing if the file system entry is matching one of the ignore patterns. + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) { + Logger.LogTrace("Skipped ignored path {Path}", fileInfo.FullName); + return true; + } + trackerId = Plugin.Instance.Tracker.Add($"Should ignore path \"{fileInfo.FullName}\"."); if (fileInfo.IsDirectory && Plugin.Instance.IgnoredFolders.Contains(Path.GetFileName(fileInfo.FullName).ToLowerInvariant())) { Logger.LogDebug("Skipped excluded folder at path {Path}", fileInfo.FullName); diff --git a/Shokofin/Shokofin.csproj b/Shokofin/Shokofin.csproj index b108f5c7..acbf003e 100644 --- a/Shokofin/Shokofin.csproj +++ b/Shokofin/Shokofin.csproj @@ -11,6 +11,7 @@ + diff --git a/Shokofin/Utils/IgnorePatterns.cs b/Shokofin/Utils/IgnorePatterns.cs new file mode 100644 index 00000000..2ad43b47 --- /dev/null +++ b/Shokofin/Utils/IgnorePatterns.cs @@ -0,0 +1,134 @@ +using System; +using DotNet.Globbing; + +namespace Shokofin.Utils; + +/// +/// Glob patterns for files to ignore. +/// +/// +/// A one-to-one copy of +/// since it's not exposed through the abstraction used by plugins and I don't +/// care enough to rise a PR to expose it. +/// +public static class IgnorePatterns +{ + /// + /// Files matching these glob patterns will be ignored. + /// + private static readonly string[] _patterns = + { + "**/small.jpg", + "**/albumart.jpg", + + // We have neither non-greedy matching or character group repetitions, working around that here. + // https://github.com/dazinator/DotNet.Glob#patterns + // .*/sample\..{1,5} + "**/sample.?", + "**/sample.??", + "**/sample.???", // Matches sample.mkv + "**/sample.????", // Matches sample.webm + "**/sample.?????", + "**/*.sample.?", + "**/*.sample.??", + "**/*.sample.???", + "**/*.sample.????", + "**/*.sample.?????", + "**/sample/*", + + // Directories + "**/metadata/**", + "**/metadata", + "**/ps3_update/**", + "**/ps3_update", + "**/ps3_vprm/**", + "**/ps3_vprm", + "**/extrafanart/**", + "**/extrafanart", + "**/extrathumbs/**", + "**/extrathumbs", + "**/.actors/**", + "**/.actors", + "**/.wd_tv/**", + "**/.wd_tv", + "**/lost+found/**", + "**/lost+found", + + // Trickplay files + "**/*.trickplay", + "**/*.trickplay/**", + + // WMC temp recording directories that will constantly be written to + "**/TempRec/**", + "**/TempRec", + "**/TempSBE/**", + "**/TempSBE", + + // Synology + "**/eaDir/**", + "**/eaDir", + "**/@eaDir/**", + "**/@eaDir", + "**/#recycle/**", + "**/#recycle", + + // Qnap + "**/@Recycle/**", + "**/@Recycle", + "**/.@__thumb/**", + "**/.@__thumb", + "**/$RECYCLE.BIN/**", + "**/$RECYCLE.BIN", + "**/System Volume Information/**", + "**/System Volume Information", + "**/.grab/**", + "**/.grab", + + // Unix hidden files + "**/.*", + + // Mac - if you ever remove the above. + // "**/._*", + // "**/.DS_Store", + + // thumbs.db + "**/thumbs.db", + + // bts sync files + "**/*.bts", + "**/*.sync", + + // zfs + "**/.zfs/**", + "**/.zfs" + }; + + private static readonly GlobOptions _globOptions = new GlobOptions + { + Evaluation = + { + CaseInsensitive = true + } + }; + + private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions)); + + /// + /// Returns true if the supplied path should be ignored. + /// + /// The path to test. + /// Whether to ignore the path. + public static bool ShouldIgnore(ReadOnlySpan path) + { + int len = _globs.Length; + for (int i = 0; i < len; i++) + { + if (_globs[i].IsMatch(path)) + { + return true; + } + } + + return false; + } +}