mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Simplify StackResolver
This commit is contained in:
parent
6030946d78
commit
220443eca1
@ -124,11 +124,11 @@ namespace Emby.Naming.Common
|
||||
token: "DSR")
|
||||
};
|
||||
|
||||
VideoFileStackingExpressions = new[]
|
||||
VideoFileStackingRules = new[]
|
||||
{
|
||||
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"^(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|part|pt|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"^(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
@ -765,9 +765,9 @@ namespace Emby.Naming.Common
|
||||
public Format3DRule[] Format3DRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
||||
/// Gets the file stacking rules.
|
||||
/// </summary>
|
||||
public string[] VideoFileStackingExpressions { get; set; }
|
||||
public FileStackRule[] VideoFileStackingRules { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||
@ -789,11 +789,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
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>
|
||||
/// Gets list of clean datetime regular expressions.
|
||||
/// </summary>
|
||||
@ -819,7 +814,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public void Compile()
|
||||
{
|
||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||
|
@ -12,25 +12,30 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
||||
/// </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>
|
||||
/// Gets or sets name of file stack.
|
||||
/// Gets the name of file stack.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of paths in stack.
|
||||
/// Gets the list of paths in stack.
|
||||
/// </summary>
|
||||
public List<string> Files { get; set; }
|
||||
public IReadOnlyList<string> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether stack is directory stack.
|
||||
/// Gets a value indicating whether stack is directory stack.
|
||||
/// </summary>
|
||||
public bool IsDirectoryStack { get; set; }
|
||||
public bool IsDirectoryStack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
Emby.Naming/Video/FileStackRule.cs
Normal file
48
Emby.Naming/Video/FileStackRule.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||
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))
|
||||
.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<string, StackMetadata>();
|
||||
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;
|
||||
|
||||
// (Title)(Volume)(Ignore)(Extension)
|
||||
var match1 = FindMatch(file1.FileName, exp, offset, cache);
|
||||
|
||||
if (match1.Success)
|
||||
var name = file.Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
var title1 = match1.Groups[1].Value;
|
||||
var volume1 = match1.Groups[2].Value;
|
||||
var ignore1 = match1.Groups[3].Value;
|
||||
var extension1 = match1.Groups[4].Value;
|
||||
name = Path.GetFileName(file.FullName);
|
||||
}
|
||||
|
||||
var j = i + 1;
|
||||
while (j < list.Count)
|
||||
var rule = namingOptions.VideoFileStackingRules[i];
|
||||
if (!rule.Match(name, out var stackParsingResult))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stackName = stackParsingResult.Value.StackName;
|
||||
var partNumber = stackParsingResult.Value.PartNumber;
|
||||
var partType = stackParsingResult.Value.PartType;
|
||||
|
||||
if (!potentialStacks.TryGetValue(stackName, out var stackResult))
|
||||
{
|
||||
stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
|
||||
potentialStacks[stackName] = stackResult;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (fileName, stack) in potentialStacks)
|
||||
{
|
||||
if (stack.Parts.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFileNameWithExtension(FileSystemMetadata file)
|
||||
private class StackMetadata
|
||||
{
|
||||
// For directories, dummy up an extension otherwise the expressions will fail
|
||||
var input = file.FullName;
|
||||
if (file.IsDirectory)
|
||||
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (offset < 0 || offset >= input.Length)
|
||||
{
|
||||
return Match.Empty;
|
||||
}
|
||||
public bool IsDirectory { get; }
|
||||
|
||||
if (!cache.TryGetValue((input, regex, offset), out var result))
|
||||
{
|
||||
result = regex.Match(input, offset, input.Length - offset);
|
||||
cache.Add((input, regex, offset), result);
|
||||
}
|
||||
public bool IsNumerical { get; }
|
||||
|
||||
return result;
|
||||
public string PartType { get; }
|
||||
|
||||
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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[]
|
||||
{
|
||||
|
@ -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<string>());
|
||||
Assert.False(stack.ContainsFile("XX", true));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user