diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ae1a2fd71e..0dcce1ea18 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -269,3 +269,4 @@
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
+ - [Michael McElroy](https://github.com/mcmcelro)
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 3219472eff..5289065898 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -18,8 +18,9 @@ namespace Emby.Naming.Video
///
/// Path to file.
/// The naming options.
+ /// Top-level folder for the containing library.
/// Returns object.
- public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
{
var result = new ExtraResult();
@@ -69,7 +70,9 @@ namespace Emby.Naming.Video
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
- if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ 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;
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 12bc22a6ac..a3134f3f68 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -27,8 +27,9 @@ namespace Emby.Naming.Video
/// The naming options.
/// Indication we should consider multi-versions of content.
/// Whether to parse the name or use the filename.
+ /// Top-level folder for the containing library.
/// Returns enumerable of which groups files together when related.
- public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
+ public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
@@ -65,7 +66,7 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
.OfType()
.ToList()
};
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index db5bfdbf94..afbf6f8fae 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -17,10 +17,11 @@ namespace Emby.Naming.Video
/// The path.
/// The naming options.
/// Whether to parse the name or use the filename.
+ /// Top-level folder for the containing library.
/// VideoFileInfo.
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
{
- return Resolve(path, true, namingOptions, parseName);
+ return Resolve(path, true, namingOptions, parseName, libraryRoot);
}
///
@@ -28,10 +29,11 @@ namespace Emby.Naming.Video
///
/// The path.
/// The naming options.
+ /// Top-level folder for the containing library.
/// VideoFileInfo.
- public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
{
- return Resolve(path, false, namingOptions);
+ return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
}
///
@@ -41,9 +43,10 @@ namespace Emby.Naming.Video
/// if set to true [is folder].
/// The naming options.
/// Whether or not the name should be parsed for info.
+ /// Top-level folder for the containing library.
/// VideoFileInfo.
/// path is null.
- public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
+ public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
{
if (string.IsNullOrEmpty(path))
{
@@ -75,7 +78,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
+ var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
var name = Path.GetFileNameWithoutExtension(path);
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 62f1f3d3aa..c8026960df 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2709,7 +2709,7 @@ namespace Emby.Server.Implementations.Library
public IEnumerable FindExtras(BaseItem owner, IReadOnlyList fileSystemChildren, IDirectoryService directoryService)
{
- var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b4791b9456..b9f9f29723 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
- public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+ public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
{
- var extraResult = GetExtraInfo(path, _namingOptions);
+ var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
{
extraType = null;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 4debe722b9..f1aeb1340a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 2c33ab4929..51eb99f496 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -2,6 +2,7 @@ using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
using Xunit;
+
using MediaType = Emby.Naming.Common.MediaType;
namespace Jellyfin.Naming.Tests.Video
@@ -20,6 +21,9 @@ namespace Jellyfin.Naming.Tests.Video
{
Test("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);
}
@@ -43,6 +47,19 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-deletedscene.mp4", ExtraType.DeletedScene);
Test("300-interview.mp4", ExtraType.Interview);
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes);
+ Test("300-featurette.mp4", ExtraType.Featurette);
+ Test("300-short.mp4", ExtraType.Short);
+ Test("300-extra.mp4", ExtraType.Unknown);
+ Test("300-other.mp4", ExtraType.Unknown);
+ }
+
+ [Theory]
+ [InlineData(ExtraType.ThemeSong, "theme-music")]
+ public void TestDirectoriesAudioExtras(ExtraType type, string dirName)
+ {
+ Test(dirName + "/300.mp3", type);
+ Test("300/" + dirName + "/something.mp3", type);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp3", type);
}
[Theory]
@@ -52,11 +69,14 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData(ExtraType.Scene, "scenes")]
[InlineData(ExtraType.Sample, "samples")]
[InlineData(ExtraType.Short, "shorts")]
+ [InlineData(ExtraType.Trailer, "trailers")]
[InlineData(ExtraType.Featurette, "featurettes")]
[InlineData(ExtraType.Clip, "clips")]
[InlineData(ExtraType.ThemeVideo, "backdrops")]
+ [InlineData(ExtraType.Unknown, "extra")]
[InlineData(ExtraType.Unknown, "extras")]
- public void TestDirectories(ExtraType type, string dirName)
+ [InlineData(ExtraType.Unknown, "other")]
+ public void TestDirectoriesVideoExtras(ExtraType type, string dirName)
{
Test(dirName + "/300.mp4", type);
Test("300/" + dirName + "/something.mkv", type);
@@ -75,10 +95,44 @@ namespace Jellyfin.Naming.Tests.Video
Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null);
}
+ [Theory]
+ [InlineData(ExtraType.ThemeSong, "theme-music")]
+ public void TestTopLevelDirectoriesWithAudioExtraNames(ExtraType typicalType, string dirName)
+ {
+ string libraryRoot = "/data/something/" + dirName;
+ TestWithLibraryRoot(libraryRoot + "/300.mp3", libraryRoot, null);
+ TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mp3", libraryRoot, typicalType);
+ }
+
+ [Theory]
+ [InlineData(ExtraType.Trailer, "trailers")]
+ [InlineData(ExtraType.ThemeVideo, "backdrops")]
+ [InlineData(ExtraType.BehindTheScenes, "behind the scenes")]
+ [InlineData(ExtraType.DeletedScene, "deleted scenes")]
+ [InlineData(ExtraType.Interview, "interviews")]
+ [InlineData(ExtraType.Scene, "scenes")]
+ [InlineData(ExtraType.Sample, "samples")]
+ [InlineData(ExtraType.Short, "shorts")]
+ [InlineData(ExtraType.Featurette, "featurettes")]
+ [InlineData(ExtraType.Unknown, "extras")]
+ [InlineData(ExtraType.Unknown, "extra")]
+ [InlineData(ExtraType.Unknown, "other")]
+ [InlineData(ExtraType.Clip, "clips")]
+ public void TestTopLevelDirectoriesWithVideoExtraNames(ExtraType typicalType, string dirName)
+ {
+ string libraryRoot = "/data/something/" + dirName;
+ TestWithLibraryRoot(libraryRoot + "/300.mp4", libraryRoot, null);
+ TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mkv", libraryRoot, typicalType);
+ }
+
[Fact]
public void TestSample()
{
+ Test("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);
}
private void Test(string input, ExtraType? expectedType)
@@ -88,6 +142,12 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(expectedType, extraType);
}
+ private void TestWithLibraryRoot(string input, string libraryRoot, ExtraType? expectedType)
+ {
+ var extraType = ExtraRuleResolver.GetExtraInfo(input, _videoOptions, libraryRoot).ExtraType;
+ Assert.Equal(expectedType, extraType);
+ }
+
[Fact]
public void TestExtraInfo_InvalidRuleType()
{