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")
|
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();
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.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))
|
||||||
|
{
|
||||||
|
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];
|
continue;
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
stackResult.Parts.Add(partNumber, file);
|
||||||
{
|
break;
|
||||||
yield return stack;
|
|
||||||
i += stack.Files.Count - 1;
|
|
||||||
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
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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[]
|
||||||
{
|
{
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user