diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index d5f6e594cf..9849de0573 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -202,6 +202,7 @@
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
- [theshoeshiner](https://github.com/theshoeshiner)
+ - [TokerX](https://github.com/TokerX)
# Emby Contributors
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 192235baeb..1c518f0cca 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -572,6 +572,18 @@ namespace Emby.Naming.Common
"trailer",
MediaType.Video),
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Filename,
+ "sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.Filename,
+ "theme",
+ MediaType.Audio),
+
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
@@ -593,13 +605,7 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
- " trailer",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Sample,
- ExtraRuleType.Filename,
- "sample",
+ "- trailer",
MediaType.Video),
new ExtraRule(
@@ -623,15 +629,9 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
- " sample",
+ "- sample",
MediaType.Video),
- new ExtraRule(
- ExtraType.ThemeSong,
- ExtraRuleType.Filename,
- "theme",
- MediaType.Audio),
-
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 5289065898..2e0caa612f 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -22,67 +22,45 @@ namespace Emby.Naming.Video
/// Returns object.
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
{
- var result = new ExtraResult();
+ ExtraResult result = new ExtraResult();
- for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
+ bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
+ bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
+
+ ReadOnlySpan pathSpan = path.AsSpan();
+ ReadOnlySpan fileName = Path.GetFileName(pathSpan);
+ ReadOnlySpan fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ ReadOnlySpan trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
+ ReadOnlySpan directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+ string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
+
+ foreach (ExtraRule rule in namingOptions.VideoExtraRules)
{
- var rule = namingOptions.VideoExtraRules[i];
- if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
- || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
+ if ((rule.MediaType == MediaType.Audio && !isAudioFile)
+ || (rule.MediaType == MediaType.Video && !isVideoFile))
{
continue;
}
- var pathSpan = path.AsSpan();
- if (rule.RuleType == ExtraRuleType.Filename)
+ bool isMatch = rule.RuleType switch
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
+ ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
+ ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
+ _ => false,
+ };
- if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Suffix)
+ if (!isMatch)
{
- // Trim the digits from the end of the filename so we can recognize things like -trailer2
- var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
-
- if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Regex)
- {
- var filename = Path.GetFileName(path.AsSpan());
-
- var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
- if (isMatch)
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.DirectoryName)
- {
- var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
- string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
- if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
- && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
- {
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
+ continue;
}
- if (result.ExtraType is not null)
- {
- return result;
- }
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ return result;
}
return result;
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index f29a0b3ad7..f9538fbad6 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -38,7 +38,8 @@ namespace Emby.Server.Implementations.Library
}
// Don't ignore top level folders
- if (fileInfo.IsDirectory && parent is AggregateFolder)
+ if (fileInfo.IsDirectory
+ && (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
{
return false;
}
@@ -48,35 +49,21 @@ namespace Emby.Server.Implementations.Library
return true;
}
- var filename = fileInfo.Name;
+ if (parent is null)
+ {
+ return false;
+ }
if (fileInfo.IsDirectory)
{
- if (parent is not null)
- {
- // Ignore extras for unsupported types
- if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
- && parent is not AggregateFolder
- && parent is not UserRootFolder)
- {
- return true;
- }
- }
- }
- else
- {
- if (parent is not null)
- {
- // Don't resolve theme songs
- if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
- && AudioFileParser.IsAudioFile(filename, _namingOptions))
- {
- return true;
- }
- }
+ // Ignore extras for unsupported types
+ return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
+ && parent is not UserRootFolder;
}
- return false;
+ // Don't resolve theme songs
+ return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
+ && AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 51eb99f496..6d887c5771 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-trailer.mp4", ExtraType.Trailer);
Test("300.trailer.mp4", ExtraType.Trailer);
Test("300_trailer.mp4", ExtraType.Trailer);
- Test("300 trailer.mp4", ExtraType.Trailer);
+ Test("300 - trailer.mp4", ExtraType.Trailer);
Test("theme.mp3", ExtraType.ThemeSong);
}
@@ -132,7 +132,14 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-sample.mp4", ExtraType.Sample);
Test("300.sample.mp4", ExtraType.Sample);
Test("300_sample.mp4", ExtraType.Sample);
- Test("300 sample.mp4", ExtraType.Sample);
+ Test("300 - sample.mp4", ExtraType.Sample);
+ }
+
+ [Fact]
+ public void TestSuffixPartOfTitle()
+ {
+ Test("I Live In A Trailer.mp4", null);
+ Test("The DNA Sample.mp4", null);
}
private void Test(string input, ExtraType? expectedType)
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 377f82eac7..d3164ba9c9 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -87,7 +87,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300.mkv",
- "300 trailer.mkv"
+ "300 - trailer.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs
new file mode 100644
index 0000000000..0495c209dc
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/CoreResolutionIgnoreRuleTest.cs
@@ -0,0 +1,129 @@
+using System;
+using System.IO;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Emby.Server.Implementations.Library;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class CoreResolutionIgnoreRuleTest
+{
+ private readonly CoreResolutionIgnoreRule _rule;
+ private readonly NamingOptions _namingOptions;
+ private readonly Mock _appPathsMock;
+
+ public CoreResolutionIgnoreRuleTest()
+ {
+ _namingOptions = new NamingOptions();
+
+ _namingOptions.AllExtrasTypesFolderNames.TryAdd("extras", ExtraType.Trailer);
+
+ _appPathsMock = new Mock();
+ _appPathsMock.SetupGet(x => x.RootFolderPath).Returns("/server/root");
+
+ _rule = new CoreResolutionIgnoreRule(_namingOptions, _appPathsMock.Object);
+ }
+
+ private FileSystemMetadata MakeFileSystemMetadata(string fullName, bool isDirectory = false)
+ => new FileSystemMetadata { FullName = fullName, Name = Path.GetFileName(fullName), IsDirectory = isDirectory };
+
+ private BaseItem MakeParent(string name = "Parent", bool isTopParent = false, Type? type = null)
+ {
+ return type switch
+ {
+ Type t when t == typeof(Folder) => CreateMock(name, isTopParent).Object,
+ Type t when t == typeof(AggregateFolder) => CreateMock(name, isTopParent).Object,
+ Type t when t == typeof(UserRootFolder) => CreateMock(name, isTopParent).Object,
+ _ => CreateMock(name, isTopParent).Object
+ };
+ }
+
+ private static Mock CreateMock(string name, bool isTopParent)
+ where T : BaseItem
+ {
+ var mock = new Mock();
+ mock.SetupGet(p => p.Name).Returns(name);
+ mock.SetupGet(p => p.IsTopParent).Returns(isTopParent);
+ return mock;
+ }
+
+ [Fact]
+ public void TestApplicationFolder()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/server/root/extras", isDirectory: true),
+ null));
+
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/server/root/small.jpg"),
+ null));
+ }
+
+ [Fact]
+ public void TestTopLevelDirectory()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("Series/Extras", true),
+ MakeParent(type: typeof(AggregateFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("Series/Extras/Extras", true),
+ MakeParent(isTopParent: true)));
+ }
+
+ [Fact]
+ public void TestIgnorePatterns()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Media/big.jpg"),
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Media/small.jpg"),
+ MakeParent()));
+ }
+
+ [Fact]
+ public void TestExtrasTypesFolderNames()
+ {
+ FileSystemMetadata fileSystemMetadata = MakeFileSystemMetadata("/Movies/Up/extras", true);
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(AggregateFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(UserRootFolder))));
+
+ Assert.False(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ null));
+
+ Assert.True(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ fileSystemMetadata,
+ MakeParent(type: typeof(Folder))));
+ }
+
+ [Fact]
+ public void TestThemeSong()
+ {
+ Assert.False(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Movies/Up/intro.mp3"),
+ MakeParent()));
+
+ Assert.True(_rule.ShouldIgnore(
+ MakeFileSystemMetadata("/Movies/Up/theme.mp3"),
+ MakeParent()));
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index 6964c920bc..3d8ea15a31 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -301,7 +301,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
var versionInfo = fixture.Create();
versionInfo.Version = new Version(1, 0).ToString();
- versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
+ versionInfo.Timestamp = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
var packageInfo = fixture.Create();
packageInfo.Versions = new[] { versionInfo };