Simplify StackResolver

This commit is contained in:
cvium 2021-12-10 14:23:31 +01:00
parent 6030946d78
commit 220443eca1
7 changed files with 131 additions and 177 deletions

View File

@ -124,11 +124,11 @@ namespace Emby.Naming.Common
token: "DSR") token: "DSR")
}; };
VideoFileStackingExpressions = new[] VideoFileStackingRules = new[]
{ {
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$", new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$", new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
"^(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$" new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
}; };
CleanDateTimes = new[] CleanDateTimes = new[]
@ -765,9 +765,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; } public Format3DRule[] Format3DRules { get; set; }
/// <summary> /// <summary>
/// Gets or sets list of raw video file-stacking expressions strings. /// Gets the file stacking rules.
/// </summary> /// </summary>
public string[] VideoFileStackingExpressions { get; set; } public FileStackRule[] VideoFileStackingRules { get; }
/// <summary> /// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings. /// Gets or sets list of raw clean DateTimes regular expressions strings.
@ -789,11 +789,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public ExtraRule[] VideoExtraRules { get; set; } public ExtraRule[] VideoExtraRules { get; set; }
/// <summary>
/// Gets list of video file-stack regular expressions.
/// </summary>
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary> /// <summary>
/// Gets list of clean datetime regular expressions. /// Gets list of clean datetime regular expressions.
/// </summary> /// </summary>
@ -819,7 +814,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public void Compile() public void Compile()
{ {
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();

View File

@ -12,25 +12,30 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class. /// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary> /// </summary>
public FileStack() /// <param name="name">The stack name.</param>
/// <param name="isDirectory">Whether the stack files are directories.</param>
/// <param name="files">The stack files.</param>
public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
{ {
Files = new List<string>(); Name = name;
IsDirectoryStack = isDirectory;
Files = files;
} }
/// <summary> /// <summary>
/// Gets or sets name of file stack. /// Gets the name of file stack.
/// </summary> /// </summary>
public string Name { get; set; } = string.Empty; public string Name { get; }
/// <summary> /// <summary>
/// Gets or sets list of paths in stack. /// Gets the list of paths in stack.
/// </summary> /// </summary>
public List<string> Files { get; set; } public IReadOnlyList<string> Files { get; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether stack is directory stack. /// Gets a value indicating whether stack is directory stack.
/// </summary> /// </summary>
public bool IsDirectoryStack { get; set; } public bool IsDirectoryStack { get; }
/// <summary> /// <summary>
/// Helper function to determine if path is in the stack. /// Helper function to determine if path is in the stack.
@ -45,12 +50,7 @@ namespace Emby.Naming.Video
return false; return false;
} }
if (IsDirectoryStack == isDirectory) return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparer.OrdinalIgnoreCase);
{
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
}
return false;
} }
} }
} }

View File

@ -0,0 +1,48 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video;
/// <summary>
/// Regex based rule for file stacking (eg. disc1, disc2).
/// </summary>
public class FileStackRule
{
private readonly Regex _tokenRegex;
/// <summary>
/// Initializes a new instance of the <see cref="FileStackRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
public FileStackRule(string token, bool isNumerical)
{
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
IsNumerical = isNumerical;
}
/// <summary>
/// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
/// </summary>
public bool IsNumerical { get; }
/// <summary>
/// Match the input against the rule regex.
/// </summary>
/// <param name="input">The input.</param>
/// <param name="result">The part type and number or <c>null</c>.</param>
/// <returns>A value indicating whether the input matched the rule.</returns>
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;
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.AudioBook; using Emby.Naming.AudioBook;
using Emby.Naming.Common; using Emby.Naming.Common;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -51,19 +50,13 @@ namespace Emby.Naming.Video
{ {
foreach (var file in directory) foreach (var file in directory)
{ {
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false }; var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
stack.Files.Add(file.Path);
yield return stack; yield return stack;
} }
} }
else else
{ {
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false }; var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
foreach (var file in directory)
{
stack.Files.Add(file.Path);
}
yield return stack; yield return stack;
} }
} }
@ -77,166 +70,87 @@ namespace Emby.Naming.Video
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions) public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
{ {
var list = files var potentialFiles = files
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions)) .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
.OrderBy(i => i.FullName) .OrderBy(i => i.FullName);
.Select(f => (f.IsDirectory, FileName: GetFileNameWithExtension(f), f.FullName))
.ToList();
// TODO is there a "nicer" way? var potentialStacks = new Dictionary<string, StackMetadata>();
var cache = new Dictionary<(string, Regex, int), Match>(); foreach (var file in potentialFiles)
var expressions = namingOptions.VideoFileStackingRegexes;
for (var i = 0; i < list.Count; i++)
{ {
var offset = 0; for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
var file1 = list[i];
var expressionIndex = 0;
while (expressionIndex < expressions.Length)
{ {
var exp = expressions[expressionIndex]; var name = file.Name;
FileStack? stack = null; if (string.IsNullOrEmpty(name))
// (Title)(Volume)(Ignore)(Extension)
var match1 = FindMatch(file1.FileName, exp, offset, cache);
if (match1.Success)
{ {
var title1 = match1.Groups[1].Value; name = Path.GetFileName(file.FullName);
var volume1 = match1.Groups[2].Value; }
var ignore1 = match1.Groups[3].Value;
var extension1 = match1.Groups[4].Value;
var j = i + 1; var rule = namingOptions.VideoFileStackingRules[i];
while (j < list.Count) if (!rule.Match(name, out var stackParsingResult))
{ {
var file2 = list[j];
if (file1.IsDirectory != file2.IsDirectory)
{
j++;
continue; continue;
} }
// (Title)(Volume)(Ignore)(Extension) var stackName = stackParsingResult.Value.StackName;
var match2 = FindMatch(file2.FileName, exp, offset, cache); var partNumber = stackParsingResult.Value.PartNumber;
var partType = stackParsingResult.Value.PartType;
if (match2.Success) if (!potentialStacks.TryGetValue(stackName, out var stackResult))
{ {
var title2 = match2.Groups[1].Value; stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
var volume2 = match2.Groups[2].Value; potentialStacks[stackName] = stackResult;
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); if (stackResult.Parts.Count > 0)
} {
else if (stackResult.IsDirectory != file.IsDirectory
|| !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
|| stackResult.ContainsPart(partNumber))
{
continue;
}
if (rule.IsNumerical != stackResult.IsNumerical)
{ {
// Sequel
offset = 0;
expressionIndex++;
break; break;
} }
} }
else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
{ stackResult.Parts.Add(partNumber, file);
// False positive, try again with offset
offset = match1.Groups[3].Index;
break;
}
else
{
// Extension mismatch
offset = 0;
expressionIndex++;
break; break;
} }
} }
else
foreach (var (fileName, stack) in potentialStacks)
{ {
// Title mismatch if (stack.Parts.Count < 2)
offset = 0;
expressionIndex++;
break;
}
}
else
{ {
// No match 2, next expression continue;
offset = 0;
expressionIndex++;
break;
} }
j++; yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
}
if (j == list.Count)
{
expressionIndex = expressions.Length;
}
}
else
{
// No match 1
offset = 0;
expressionIndex++;
}
if (stack?.Files.Count > 1)
{
yield return stack;
i += stack.Files.Count - 1;
break;
}
}
} }
} }
private static string GetFileNameWithExtension(FileSystemMetadata file) private class StackMetadata
{ {
// For directories, dummy up an extension otherwise the expressions will fail public StackMetadata(bool isDirectory, bool isNumerical, string partType)
var input = file.FullName;
if (file.IsDirectory)
{ {
input = Path.ChangeExtension(input, "mkv"); Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
IsDirectory = isDirectory;
IsNumerical = isNumerical;
PartType = partType;
} }
return Path.GetFileName(input); public Dictionary<string, FileSystemMetadata> Parts { get; }
}
private static Match FindMatch(string input, Regex regex, int offset, Dictionary<(string, Regex, int), Match> cache) public bool IsDirectory { get; }
{
if (offset < 0 || offset >= input.Length)
{
return Match.Empty;
}
if (!cache.TryGetValue((input, regex, offset), out var result)) public bool IsNumerical { get; }
{
result = regex.Match(input, offset, input.Length - offset);
cache.Add((input, regex, offset), result);
}
return result; public string PartType { get; }
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
} }
} }
} }

View File

@ -10,7 +10,6 @@ namespace Jellyfin.Naming.Tests.Common
{ {
var options = new NamingOptions(); var options = new NamingOptions();
Assert.NotEmpty(options.VideoFileStackingRegexes);
Assert.NotEmpty(options.CleanDateTimeRegexes); Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes); Assert.NotEmpty(options.CleanStringRegexes);
Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);

View File

@ -128,7 +128,7 @@ namespace Jellyfin.Naming.Tests.Video
} }
[Fact] [Fact]
public void TestDirtyNames() public void ResolveFiles_GivenPartInMiddleOfName_ReturnsNoStack()
{ {
var files = new[] var files = new[]
{ {
@ -141,12 +141,11 @@ namespace Jellyfin.Naming.Tests.Video
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result); Assert.Empty(result);
TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
} }
[Fact] [Fact]
public void TestNumberedFiles() public void ResolveFiles_FileNamesWithMissingPartType_ReturnsNoStack()
{ {
var files = new[] var files = new[]
{ {

View File

@ -489,7 +489,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact] [Fact]
public void TestDirectoryStack() public void TestDirectoryStack()
{ {
var stack = new FileStack(); var stack = new FileStack(string.Empty, false, Array.Empty<string>());
Assert.False(stack.ContainsFile("XX", true)); Assert.False(stack.ContainsFile("XX", true));
} }
} }