diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 86c79166d1..b97e9d7637 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -124,11 +124,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
- VideoFileStackingExpressions = new[]
+ VideoFileStackingRules = new[]
{
- "^(?
.*?)(?[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[0-9]+)(?.*?)(?\\.[^.]+)$",
- "^(?.*?)(?[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[a-d])(?.*?)(?\\.[^.]+)$",
- "^(?.*?)(?[ ._-]*[a-d])(?.*?)(?\\.[^.]+)$"
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?[a-d])(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -765,9 +765,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
///
- /// Gets or sets list of raw video file-stacking expressions strings.
+ /// Gets the file stacking rules.
///
- public string[] VideoFileStackingExpressions { get; set; }
+ public FileStackRule[] VideoFileStackingRules { get; }
///
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -789,11 +789,6 @@ namespace Emby.Naming.Common
///
public ExtraRule[] VideoExtraRules { get; set; }
- ///
- /// Gets list of video file-stack regular expressions.
- ///
- public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty();
-
///
/// Gets list of clean datetime regular expressions.
///
@@ -819,7 +814,6 @@ namespace Emby.Naming.Common
///
public void Compile()
{
- VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index a4a4716ca9..bd635a9f78 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
///
/// Initializes a new instance of the class.
///
- public FileStack()
+ /// The stack name.
+ /// Whether the stack files are directories.
+ /// The stack files.
+ public FileStack(string name, bool isDirectory, IReadOnlyList files)
{
- Files = new List();
+ Name = name;
+ IsDirectoryStack = isDirectory;
+ Files = files;
}
///
- /// Gets or sets name of file stack.
+ /// Gets the name of file stack.
///
- public string Name { get; set; } = string.Empty;
+ public string Name { get; }
///
- /// Gets or sets list of paths in stack.
+ /// Gets the list of paths in stack.
///
- public List Files { get; set; }
+ public IReadOnlyList Files { get; }
///
- /// Gets or sets a value indicating whether stack is directory stack.
+ /// Gets a value indicating whether stack is directory stack.
///
- public bool IsDirectoryStack { get; set; }
+ public bool IsDirectoryStack { get; }
///
/// Helper function to determine if path is in the stack.
@@ -45,12 +50,7 @@ namespace Emby.Naming.Video
return false;
}
- if (IsDirectoryStack == isDirectory)
- {
- return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
- }
-
- return false;
+ return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparer.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
new file mode 100644
index 0000000000..36a765dfb8
--- /dev/null
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+///
+/// Regex based rule for file stacking (eg. disc1, disc2).
+///
+public class FileStackRule
+{
+ private readonly Regex _tokenRegex;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Token.
+ /// Whether the file stack rule uses numerical or alphabetical numbering.
+ public FileStackRule(string token, bool isNumerical)
+ {
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ IsNumerical = isNumerical;
+ }
+
+ ///
+ /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+ ///
+ public bool IsNumerical { get; }
+
+ ///
+ /// Match the input against the rule regex.
+ ///
+ /// The input.
+ /// The part type and number or null.
+ /// A value indicating whether the input matched the rule.
+ public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+ {
+ result = null;
+ var match = _tokenRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "vol";
+ result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+ return true;
+ }
+}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index be73f69db1..7900e09c2d 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -51,19 +50,13 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
- var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
- stack.Files.Add(file.Path);
+ var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
yield return stack;
}
}
else
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
- {
- stack.Files.Add(file.Path);
- }
-
+ var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
yield return stack;
}
}
@@ -77,166 +70,87 @@ namespace Emby.Naming.Video
/// Enumerable of videos.
public static IEnumerable Resolve(IEnumerable files, NamingOptions namingOptions)
{
- var list = files
+ var potentialFiles = files
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
- .OrderBy(i => i.FullName)
- .Select(f => (f.IsDirectory, FileName: GetFileNameWithExtension(f), f.FullName))
- .ToList();
+ .OrderBy(i => i.FullName);
- // TODO is there a "nicer" way?
- var cache = new Dictionary<(string, Regex, int), Match>();
-
- var expressions = namingOptions.VideoFileStackingRegexes;
-
- for (var i = 0; i < list.Count; i++)
+ var potentialStacks = new Dictionary();
+ foreach (var file in potentialFiles)
{
- var offset = 0;
-
- var file1 = list[i];
-
- var expressionIndex = 0;
- while (expressionIndex < expressions.Length)
+ for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
{
- var exp = expressions[expressionIndex];
- FileStack? stack = null;
+ var name = file.Name;
+ if (string.IsNullOrEmpty(name))
+ {
+ name = Path.GetFileName(file.FullName);
+ }
+
+ var rule = namingOptions.VideoFileStackingRules[i];
+ if (!rule.Match(name, out var stackParsingResult))
+ {
+ continue;
+ }
- // (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1.FileName, exp, offset, cache);
+ var stackName = stackParsingResult.Value.StackName;
+ var partNumber = stackParsingResult.Value.PartNumber;
+ var partType = stackParsingResult.Value.PartType;
- if (match1.Success)
+ if (!potentialStacks.TryGetValue(stackName, out var stackResult))
{
- var title1 = match1.Groups[1].Value;
- var volume1 = match1.Groups[2].Value;
- var ignore1 = match1.Groups[3].Value;
- var extension1 = match1.Groups[4].Value;
+ stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+ potentialStacks[stackName] = stackResult;
+ }
- var j = i + 1;
- while (j < list.Count)
+ if (stackResult.Parts.Count > 0)
+ {
+ if (stackResult.IsDirectory != file.IsDirectory
+ || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+ || stackResult.ContainsPart(partNumber))
{
- var file2 = list[j];
-
- if (file1.IsDirectory != file2.IsDirectory)
- {
- j++;
- continue;
- }
-
- // (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2.FileName, exp, offset, cache);
-
- if (match2.Success)
- {
- var title2 = match2.Groups[1].Value;
- var volume2 = match2.Groups[2].Value;
- var ignore2 = match2.Groups[3].Value;
- var extension2 = match2.Groups[4].Value;
-
- if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
- {
- if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
- && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
- {
- stack ??= new FileStack();
- if (stack.Files.Count == 0)
- {
- stack.Name = title1 + ignore1;
- stack.IsDirectoryStack = file1.IsDirectory;
- stack.Files.Add(file1.FullName);
- }
-
- stack.Files.Add(file2.FullName);
- }
- else
- {
- // Sequel
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
- {
- // False positive, try again with offset
- offset = match1.Groups[3].Index;
- break;
- }
- else
- {
- // Extension mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // Title mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // No match 2, next expression
- offset = 0;
- expressionIndex++;
- break;
- }
-
- j++;
+ continue;
}
- if (j == list.Count)
+ if (rule.IsNumerical != stackResult.IsNumerical)
{
- expressionIndex = expressions.Length;
+ break;
}
}
- else
- {
- // No match 1
- offset = 0;
- expressionIndex++;
- }
- if (stack?.Files.Count > 1)
- {
- yield return stack;
- i += stack.Files.Count - 1;
- break;
- }
+ stackResult.Parts.Add(partNumber, file);
+ break;
}
}
- }
- private static string GetFileNameWithExtension(FileSystemMetadata file)
- {
- // For directories, dummy up an extension otherwise the expressions will fail
- var input = file.FullName;
- if (file.IsDirectory)
+ foreach (var (fileName, stack) in potentialStacks)
{
- input = Path.ChangeExtension(input, "mkv");
- }
+ if (stack.Parts.Count < 2)
+ {
+ continue;
+ }
- return Path.GetFileName(input);
+ yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+ }
}
- private static Match FindMatch(string input, Regex regex, int offset, Dictionary<(string, Regex, int), Match> cache)
+ private class StackMetadata
{
- if (offset < 0 || offset >= input.Length)
+ public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{
- return Match.Empty;
+ Parts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ IsDirectory = isDirectory;
+ IsNumerical = isNumerical;
+ PartType = partType;
}
- if (!cache.TryGetValue((input, regex, offset), out var result))
- {
- result = regex.Match(input, offset, input.Length - offset);
- cache.Add((input, regex, offset), result);
- }
+ public Dictionary Parts { get; }
+
+ public bool IsDirectory { get; }
+
+ public bool IsNumerical { get; }
+
+ public string PartType { get; }
- return result;
+ public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
index 3892d00f61..58aaed023a 100644
--- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
+++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
@@ -10,7 +10,6 @@ namespace Jellyfin.Naming.Tests.Common
{
var options = new NamingOptions();
- Assert.NotEmpty(options.VideoFileStackingRegexes);
Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes);
Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 41da0e0771..368c3592ef 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -128,7 +128,7 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
- public void TestDirtyNames()
+ public void ResolveFiles_GivenPartInMiddleOfName_ReturnsNoStack()
{
var files = new[]
{
@@ -141,12 +141,11 @@ namespace Jellyfin.Naming.Tests.Video
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
- Assert.Single(result);
- TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
+ Assert.Empty(result);
}
[Fact]
- public void TestNumberedFiles()
+ public void ResolveFiles_FileNamesWithMissingPartType_ReturnsNoStack()
{
var files = new[]
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index b171d739a8..cda4967613 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -489,7 +489,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestDirectoryStack()
{
- var stack = new FileStack();
+ var stack = new FileStack(string.Empty, false, Array.Empty());
Assert.False(stack.ContainsFile("XX", true));
}
}