From acb9da6f93829eaef356d3effd1872798c778adb Mon Sep 17 00:00:00 2001 From: Nilesh Patel Date: Wed, 10 Dec 2025 12:23:05 -0800 Subject: Add curly brace and parentheses support for parsing attribute values from paths --- .../Library/PathExtensionsTests.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library') diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 940e3c2b1..74cd303ba 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -11,21 +11,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")] [InlineData("Superman: Red Son", "imdbid", null)] - [InlineData("Superman: Red Son", "something", null)] [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")] + [InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")] [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")] [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")] [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")] [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] + [InlineData("Superman: Red Son {tmdbid=3)", "tmdbid", "3")] + [InlineData("Superman: Red Son (tvdbid-6}", "tvdbid", "6")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")] + [InlineData("{tmdbid=618355}", "tmdbid", "618355")] + [InlineData("(tmdbid=618355)", "tmdbid", "618355")] [InlineData("[tmdbid-618355]", "tmdbid", "618355")] + [InlineData("{tmdbid-618355)", "tmdbid", null)] + [InlineData("[tmdbid-618355}", "tmdbid", null)] [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")] [InlineData("tmdbid=618355]", "tmdbid", null)] @@ -36,6 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)] [InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)] [InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")] + [InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)] + [InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)] + [InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")] public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult) { Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute)); -- cgit v1.2.3 From b9cf26db2f3d219340a17951c289a230d0ccf31a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 28 Dec 2025 07:22:20 -0500 Subject: Backport pull request #15746 from jellyfin/release-10.11.z Skip invalid ignore rules Original-merge: 6e60634c9f078cc01e343b07a0a6b2a5c230478c Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 48 +++++++++++- .../Library/DotIgnoreIgnoreRuleTest.cs | 87 +++++++++++++++++----- 2 files changed, 115 insertions(+), 20 deletions(-) (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 473ff8e1d..ef5d24c70 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule { // If file has content, base ignoring off the content .gitignore-style rules var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return CheckIgnoreRules(path, rules, isDirectory); + } + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// Whether to normalize backslashes to forward slashes (for Windows paths). + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath) + { var ignore = new Ignore.Ignore(); - ignore.Add(rules); + + // Add each rule individually to catch and skip invalid patterns + var validRulesAdded = 0; + foreach (var rule in rules) + { + try + { + ignore.Add(rule); + validRulesAdded++; + } + catch (RegexParseException) + { + // Ignore invalid patterns + } + } + + // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file) + if (validRulesAdded == 0) + { + return true; + } // Mitigate the problem of the Ignore library not handling Windows paths correctly. // See https://github.com/jellyfin/jellyfin/issues/15484 - var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + var pathToCheck = normalizePath ? path.NormalizePath('/') : path; // Add trailing slash for directories to match "folder/" if (isDirectory) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index d677c9f09..a7bbef7ed 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,30 +1,81 @@ +using Emby.Server.Implementations.Library; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; public class DotIgnoreIgnoreRuleTest { - [Fact] - public void Test() + private static readonly string[] _rule1 = ["SPs"]; + private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"]; + private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"]; + private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"]; + + public static TheoryData CheckIgnoreRulesTestData => + new() + { + // Basic pattern matching + { _rule1, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule1, "cd/sps/ffffff.mkv", false, true }, + { _rule1, "/cd/sps/ffffff.mkv", false, true }, + + // Negate pattern + { _rule2, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule2, "cd/sps/ffffff.mkv", false, true }, + { _rule2, "/cd/sps/ffffff.mkv", false, true }, + { _rule2, "f:/cd/sps/thebestshot.mkv", false, false }, + { _rule2, "cd/sps/thebestshot.mkv", false, false }, + { _rule2, "/cd/sps/thebestshot.mkv", false, false }, + + // Mixed valid and invalid patterns - skips invalid, applies valid + { _rule3, "test.txt", false, true }, + { _rule3, "videos/movie.mp4", false, true }, + { _rule3, "movie.mkv", false, true }, + { _rule3, "test.mp3", false, false }, + + // Only invalid patterns - falls back to ignore all + { _rule4, "any-file.txt", false, true }, + { _rule4, "any/path/to/file.mkv", false, true }, + }; + + public static TheoryData WindowsPathNormalizationTestData => + new() + { + // Windows paths with backslashes - should match when normalizePath is true + { _rule1, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule1, @"D:\media\sps\movie.mkv", false, true }, + { _rule1, @"\\server\share\sps\file.mkv", false, true }, + + // Negate pattern with Windows paths + { _rule2, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false }, + + // Directory matching with Windows paths + { _rule3, @"C:\videos\movie.mp4", false, true }, + { _rule3, @"D:\documents\test.txt", false, true }, + { _rule3, @"E:\music\song.mp3", false, false }, + }; + + [Theory] + [MemberData(nameof(CheckIgnoreRulesTestData))] + public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored) + { + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory)); + } + + [Theory] + [MemberData(nameof(WindowsPathNormalizationTestData))] + public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - 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")); + // With normalizePath=true, backslashes should be converted to forward slashes + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true)); } - [Fact] - public void TestNegatePattern() + [Theory] + [InlineData(@"C:\cd\sps\ffffff.mkv")] + [InlineData(@"D:\media\sps\movie.mkv")] + public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path) { - 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")); + // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes + Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } } -- cgit v1.2.3 From c9b7c5bb5665d2eb02381fa12dcce786fee9ae3d Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:43 -0500 Subject: Backport pull request #16029 from jellyfin/release-10.11.z Skip hidden directories and .ignore paths in library monitoring Original-merge: 2cb7fb52d2221d9daa39206089b578c2c0fcb549 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ Emby.Server.Implementations/Library/IgnorePatterns.cs | 1 + .../Library/IgnorePatternsTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library') diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index d87ad729e..7cff2a25b 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO return; } + var fileInfo = _fileSystem.GetFileSystemInfo(path); + if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null)) + { + return; + } + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too foreach (var i in _tempIgnoredPaths.Keys) { diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index fe3a1ce61..5fac2f6b0 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", + "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 07061cfc7..4cb6cb960 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } -- cgit v1.2.3 From 0c274af72c1d223c0fea7f4f4ac99e04f1e1e904 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:28 -0500 Subject: Backport pull request #16077 from jellyfin/release-10.11.z Revert hidden directory ignore pattern Original-merge: 644327eb762a907328c68ab9f5d61a151cd96897 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 5 ++++- .../Library/IgnorePatternsTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library') diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5fac2f6b0..59ccb9e2c 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library "**/lost+found", "**/subs/**", "**/subs", + "**/.snapshots/**", + "**/.snapshots", + "**/.snapshot/**", + "**/.snapshot", // Trickplay files "**/*.trickplay", @@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", - "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 4cb6cb960..07061cfc7 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/.hiddendir/file.mp4", false)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } -- cgit v1.2.3 From 8083ab78b558e4081caf4fd321e5ad8f74844f3e Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 2 Feb 2026 20:46:28 +0100 Subject: Fix tests --- .../Library/PathExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library') diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 74cd303ba..650d67b19 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -26,8 +26,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] - [InlineData("Superman: Red Son {tmdbid=3)", "tmdbid", "3")] - [InlineData("Superman: Red Son (tvdbid-6}", "tvdbid", "6")] + [InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")] + [InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")] [InlineData("{tmdbid=618355}", "tmdbid", "618355")] [InlineData("(tmdbid=618355)", "tmdbid", "618355")] -- cgit v1.2.3