diff options
| author | stefan <stefan@hegedues.at> | 2018-09-12 19:26:21 +0200 |
|---|---|---|
| committer | stefan <stefan@hegedues.at> | 2018-09-12 19:26:21 +0200 |
| commit | 48facb797ed912e4ea6b04b17d1ff190ac2daac4 (patch) | |
| tree | 8dae77a31670a888d733484cb17dd4077d5444e8 /Emby.Naming/Video | |
| parent | c32d8656382a0eacb301692e0084377fc433ae9b (diff) | |
Update to 3.5.2 and .net core 2.1
Diffstat (limited to 'Emby.Naming/Video')
22 files changed, 1355 insertions, 0 deletions
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs new file mode 100644 index 0000000000..572dd1c600 --- /dev/null +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -0,0 +1,87 @@ +using System; +using Emby.Naming.Common; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Emby.Naming.Video +{ + /// <summary> + /// http://kodi.wiki/view/Advancedsettings.xml#video + /// </summary> + public class CleanDateTimeParser + { + private readonly NamingOptions _options; + + public CleanDateTimeParser(NamingOptions options) + { + _options = options; + } + + public CleanDateTimeResult Clean(string name) + { + var originalName = name; + + try + { + var extension = Path.GetExtension(name) ?? string.Empty; + // Check supported extensions + if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) && + !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + // Dummy up a file extension because the expressions will fail without one + // This is tricky because we can't just check Path.GetExtension for empty + // If the input is "St. Vincent (2014)", it will produce ". Vincent (2014)" as the extension + name += ".mkv"; + } + } + catch (ArgumentException) + { + + } + + var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i)) + .FirstOrDefault(i => i.HasChanged) ?? + new CleanDateTimeResult { Name = originalName }; + + if (result.HasChanged) + { + return result; + } + + // Make a second pass, running clean string first + var cleanStringResult = new CleanStringParser().Clean(name, _options.CleanStringRegexes); + + if (!cleanStringResult.HasChanged) + { + return result; + } + + return _options.CleanDateTimeRegexes.Select(i => Clean(cleanStringResult.Name, i)) + .FirstOrDefault(i => i.HasChanged) ?? + result; + } + + private CleanDateTimeResult Clean(string name, Regex expression) + { + var result = new CleanDateTimeResult(); + + var match = expression.Match(name); + + if (match.Success && match.Groups.Count == 4) + { + int year; + if (match.Groups[1].Success && match.Groups[2].Success && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out year)) + { + name = match.Groups[1].Value; + result.Year = year; + result.HasChanged = true; + } + } + + result.Name = name; + return result; + } + } +} diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs new file mode 100644 index 0000000000..946fd953c1 --- /dev/null +++ b/Emby.Naming/Video/CleanDateTimeResult.cs @@ -0,0 +1,22 @@ + +namespace Emby.Naming.Video +{ + public class CleanDateTimeResult + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the year. + /// </summary> + /// <value>The year.</value> + public int? Year { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance has changed. + /// </summary> + /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value> + public bool HasChanged { get; set; } + } +} diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs new file mode 100644 index 0000000000..bddf9589b6 --- /dev/null +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Emby.Naming.Video +{ + /// <summary> + /// http://kodi.wiki/view/Advancedsettings.xml#video + /// </summary> + public class CleanStringParser + { + public CleanStringResult Clean(string name, IEnumerable<Regex> expressions) + { + var hasChanged = false; + + foreach (var exp in expressions) + { + var result = Clean(name, exp); + + if (!string.IsNullOrEmpty(result.Name)) + { + name = result.Name; + hasChanged = hasChanged || result.HasChanged; + } + } + + return new CleanStringResult + { + Name = name, + HasChanged = hasChanged + }; + } + + private CleanStringResult Clean(string name, Regex expression) + { + var result = new CleanStringResult(); + + var match = expression.Match(name); + + if (match.Success) + { + result.HasChanged = true; + name = name.Substring(0, match.Index); + } + + result.Name = name; + return result; + } + } +} diff --git a/Emby.Naming/Video/CleanStringResult.cs b/Emby.Naming/Video/CleanStringResult.cs new file mode 100644 index 0000000000..0282863e06 --- /dev/null +++ b/Emby.Naming/Video/CleanStringResult.cs @@ -0,0 +1,17 @@ + +namespace Emby.Naming.Video +{ + public class CleanStringResult + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance has changed. + /// </summary> + /// <value><c>true</c> if this instance has changed; otherwise, <c>false</c>.</value> + public bool HasChanged { get; set; } + } +} diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs new file mode 100644 index 0000000000..bde1a47656 --- /dev/null +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -0,0 +1,87 @@ +using Emby.Naming.Audio; +using Emby.Naming.Common; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Emby.Naming.Video +{ + public class ExtraResolver + { + private readonly NamingOptions _options; + + public ExtraResolver(NamingOptions options) + { + _options = options; + } + + public ExtraResult GetExtraInfo(string path) + { + return _options.VideoExtraRules + .Select(i => GetExtraInfo(path, i)) + .FirstOrDefault(i => !string.IsNullOrEmpty(i.ExtraType)) ?? new ExtraResult(); + } + + private ExtraResult GetExtraInfo(string path, ExtraRule rule) + { + var result = new ExtraResult(); + + if (rule.MediaType == MediaType.Audio) + { + if (!new AudioFileParser(_options).IsAudioFile(path)) + { + return result; + } + } + else if (rule.MediaType == MediaType.Video) + { + if (!new VideoResolver(_options).IsVideoFile(path)) + { + return result; + } + } + else + { + return result; + } + + if (rule.RuleType == ExtraRuleType.Filename) + { + var filename = Path.GetFileNameWithoutExtension(path); + + if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + + else if (rule.RuleType == ExtraRuleType.Suffix) + { + var filename = Path.GetFileNameWithoutExtension(path); + + if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + + else if (rule.RuleType == ExtraRuleType.Regex) + { + var filename = Path.GetFileName(path); + + var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); + + if (regex.IsMatch(filename)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + + return result; + } + } +} diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs new file mode 100644 index 0000000000..ca79af9da7 --- /dev/null +++ b/Emby.Naming/Video/ExtraResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Emby.Naming.Video +{ + public class ExtraResult + { + /// <summary> + /// Gets or sets the type of the extra. + /// </summary> + /// <value>The type of the extra.</value> + public string ExtraType { get; set; } + /// <summary> + /// Gets or sets the rule. + /// </summary> + /// <value>The rule.</value> + public ExtraRule Rule { get; set; } + } +} diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs new file mode 100644 index 0000000000..ef83b3cd6c --- /dev/null +++ b/Emby.Naming/Video/ExtraRule.cs @@ -0,0 +1,28 @@ +using Emby.Naming.Common; + +namespace Emby.Naming.Video +{ + public class ExtraRule + { + /// <summary> + /// Gets or sets the token. + /// </summary> + /// <value>The token.</value> + public string Token { get; set; } + /// <summary> + /// Gets or sets the type of the extra. + /// </summary> + /// <value>The type of the extra.</value> + public string ExtraType { get; set; } + /// <summary> + /// Gets or sets the type of the rule. + /// </summary> + /// <value>The type of the rule.</value> + public ExtraRuleType RuleType { get; set; } + /// <summary> + /// Gets or sets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + public MediaType MediaType { get; set; } + } +} diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs new file mode 100644 index 0000000000..323c7cef60 --- /dev/null +++ b/Emby.Naming/Video/ExtraRuleType.cs @@ -0,0 +1,19 @@ + +namespace Emby.Naming.Video +{ + public enum ExtraRuleType + { + /// <summary> + /// The suffix + /// </summary> + Suffix = 0, + /// <summary> + /// The filename + /// </summary> + Filename = 1, + /// <summary> + /// The regex + /// </summary> + Regex = 2 + } +} diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs new file mode 100644 index 0000000000..2feea4cb3a --- /dev/null +++ b/Emby.Naming/Video/FileStack.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Naming.Video +{ + public class FileStack + { + public string Name { get; set; } + public List<string> Files { get; set; } + public bool IsDirectoryStack { get; set; } + + public FileStack() + { + Files = new List<string>(); + } + + public bool ContainsFile(string file, bool IsDirectory) + { + if (IsDirectoryStack == IsDirectory) + { + return Files.Contains(file, StringComparer.OrdinalIgnoreCase); + } + + return false; + } + } +} diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs new file mode 100644 index 0000000000..a2c541eeb2 --- /dev/null +++ b/Emby.Naming/Video/FlagParser.cs @@ -0,0 +1,35 @@ +using Emby.Naming.Common; +using System; +using System.IO; + +namespace Emby.Naming.Video +{ + public class FlagParser + { + private readonly NamingOptions _options; + + public FlagParser(NamingOptions options) + { + _options = options; + } + + public string[] GetFlags(string path) + { + return GetFlags(path, _options.VideoFlagDelimiters); + } + + public string[] GetFlags(string path, char[] delimeters) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. + + var file = Path.GetFileName(path); + + return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries); + } + } +} diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs new file mode 100644 index 0000000000..42737b4834 --- /dev/null +++ b/Emby.Naming/Video/Format3DParser.cs @@ -0,0 +1,81 @@ +using Emby.Naming.Common; +using System; +using System.Linq; + +namespace Emby.Naming.Video +{ + public class Format3DParser + { + private readonly NamingOptions _options; + + public Format3DParser(NamingOptions options) + { + _options = options; + } + + public Format3DResult Parse(string path) + { + var delimeters = _options.VideoFlagDelimiters.ToList(); + delimeters.Add(' '); + + return Parse(new FlagParser(_options).GetFlags(path, delimeters.ToArray())); + } + + internal Format3DResult Parse(string[] videoFlags) + { + foreach (var rule in _options.Format3DRules) + { + var result = Parse(videoFlags, rule); + + if (result.Is3D) + { + return result; + } + } + + return new Format3DResult(); + } + + private Format3DResult Parse(string[] videoFlags, Format3DRule rule) + { + var result = new Format3DResult(); + + if (string.IsNullOrEmpty(rule.PreceedingToken)) + { + result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase)); + result.Is3D = !string.IsNullOrEmpty(result.Format3D); + + if (result.Is3D) + { + result.Tokens.Add(rule.Token); + } + } + else + { + var foundPrefix = false; + string format = null; + + foreach (var flag in videoFlags) + { + if (foundPrefix) + { + result.Tokens.Add(rule.PreceedingToken); + + if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase)) + { + format = flag; + result.Tokens.Add(rule.Token); + } + break; + } + foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase); + } + + result.Is3D = foundPrefix && !string.IsNullOrEmpty(format); + result.Format3D = format; + } + + return result; + } + } +} diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs new file mode 100644 index 0000000000..147ccfc057 --- /dev/null +++ b/Emby.Naming/Video/Format3DResult.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Emby.Naming.Video +{ + public class Format3DResult + { + /// <summary> + /// Gets or sets a value indicating whether [is3 d]. + /// </summary> + /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> + public bool Is3D { get; set; } + /// <summary> + /// Gets or sets the format3 d. + /// </summary> + /// <value>The format3 d.</value> + public string Format3D { get; set; } + /// <summary> + /// Gets or sets the tokens. + /// </summary> + /// <value>The tokens.</value> + public List<string> Tokens { get; set; } + + public Format3DResult() + { + Tokens = new List<string>(); + } + } +} diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs new file mode 100644 index 0000000000..3c173efbc7 --- /dev/null +++ b/Emby.Naming/Video/Format3DRule.cs @@ -0,0 +1,17 @@ + +namespace Emby.Naming.Video +{ + public class Format3DRule + { + /// <summary> + /// Gets or sets the token. + /// </summary> + /// <value>The token.</value> + public string Token { get; set; } + /// <summary> + /// Gets or sets the preceeding token. + /// </summary> + /// <value>The preceeding token.</value> + public string PreceedingToken { get; set; } + } +} diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs new file mode 100644 index 0000000000..2a71255368 --- /dev/null +++ b/Emby.Naming/Video/StackResolver.cs @@ -0,0 +1,216 @@ +using Emby.Naming.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using MediaBrowser.Model.IO; + +namespace Emby.Naming.Video +{ + public class StackResolver + { + private readonly NamingOptions _options; + + public StackResolver(NamingOptions options) + { + _options = options; + } + + public StackResult ResolveDirectories(IEnumerable<string> files) + { + return Resolve(files.Select(i => new FileSystemMetadata + { + FullName = i, + IsDirectory = true + })); + } + + public StackResult ResolveFiles(IEnumerable<string> files) + { + return Resolve(files.Select(i => new FileSystemMetadata + { + FullName = i, + IsDirectory = false + })); + } + + public StackResult ResolveAudioBooks(IEnumerable<FileSystemMetadata> files) + { + var result = new StackResult(); + foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) + { + var stack = new FileStack(); + stack.Name = Path.GetFileName(directory.Key); + stack.IsDirectoryStack = false; + foreach (var file in directory) + { + if (file.IsDirectory) + continue; + stack.Files.Add(file.FullName); + } + result.Stacks.Add(stack); + } + return result; + } + + public StackResult Resolve(IEnumerable<FileSystemMetadata> files) + { + var result = new StackResult(); + + var resolver = new VideoResolver(_options); + + var list = files + .Where(i => i.IsDirectory || (resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))) + .OrderBy(i => i.FullName) + .ToList(); + + var expressions = _options.VideoFileStackingRegexes; + + for (var i = 0; i < list.Count; i++) + { + var offset = 0; + + var file1 = list[i]; + + var expressionIndex = 0; + while (expressionIndex < expressions.Length) + { + var exp = expressions[expressionIndex]; + var stack = new FileStack(); + + // (Title)(Volume)(Ignore)(Extension) + var match1 = FindMatch(file1, exp, offset); + + if (match1.Success) + { + var title1 = match1.Groups[1].Value; + var volume1 = match1.Groups[2].Value; + var ignore1 = match1.Groups[3].Value; + var extension1 = match1.Groups[4].Value; + + var j = i + 1; + while (j < list.Count) + { + var file2 = list[j]; + + if (file1.IsDirectory != file2.IsDirectory) + { + j++; + continue; + } + + // (Title)(Volume)(Ignore)(Extension) + var match2 = FindMatch(file2, exp, offset); + + 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)) + { + if (stack.Files.Count == 0) + { + stack.Name = title1 + ignore1; + stack.IsDirectoryStack = file1.IsDirectory; + //stack.Name = title1 + ignore1 + extension1; + 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) + { + expressionIndex = expressions.Length; + } + } + else + { + // No match 1 + offset = 0; + expressionIndex++; + } + + if (stack.Files.Count > 1) + { + result.Stacks.Add(stack); + i += stack.Files.Count - 1; + break; + } + } + } + + return result; + } + + private string GetRegexInput(FileSystemMetadata file) + { + // For directories, dummy up an extension otherwise the expressions will fail + var input = !file.IsDirectory + ? file.FullName + : file.FullName + ".mkv"; + + return Path.GetFileName(input); + } + + private Match FindMatch(FileSystemMetadata input, Regex regex, int offset) + { + var regexInput = GetRegexInput(input); + + if (offset < 0 || offset >= regexInput.Length) + { + return Match.Empty; + } + + return regex.Match(regexInput, offset); + } + } +} diff --git a/Emby.Naming/Video/StackResult.cs b/Emby.Naming/Video/StackResult.cs new file mode 100644 index 0000000000..920a7dea73 --- /dev/null +++ b/Emby.Naming/Video/StackResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Emby.Naming.Video +{ + public class StackResult + { + public List<FileStack> Stacks { get; set; } + + public StackResult() + { + Stacks = new List<FileStack>(); + } + } +} diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs new file mode 100644 index 0000000000..69f1f50fa0 --- /dev/null +++ b/Emby.Naming/Video/StubResolver.cs @@ -0,0 +1,44 @@ +using Emby.Naming.Common; +using System; +using System.IO; +using System.Linq; + +namespace Emby.Naming.Video +{ + public class StubResolver + { + private readonly NamingOptions _options; + + public StubResolver(NamingOptions options) + { + _options = options; + } + + public StubResult ResolveFile(string path) + { + var result = new StubResult(); + var extension = Path.GetExtension(path) ?? string.Empty; + + if (_options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + result.IsStub = true; + + path = Path.GetFileNameWithoutExtension(path); + + var token = (Path.GetExtension(path) ?? string.Empty).TrimStart('.'); + + foreach (var rule in _options.StubTypes) + { + if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) + { + result.StubType = rule.StubType; + result.Tokens.Add(token); + break; + } + } + } + + return result; + } + } +} diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs new file mode 100644 index 0000000000..c9d06c9a7f --- /dev/null +++ b/Emby.Naming/Video/StubResult.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Emby.Naming.Video +{ + public class StubResult + { + /// <summary> + /// Gets or sets a value indicating whether this instance is stub. + /// </summary> + /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value> + public bool IsStub { get; set; } + /// <summary> + /// Gets or sets the type of the stub. + /// </summary> + /// <value>The type of the stub.</value> + public string StubType { get; set; } + /// <summary> + /// Gets or sets the tokens. + /// </summary> + /// <value>The tokens.</value> + public List<string> Tokens { get; set; } + + public StubResult() + { + Tokens = new List<string>(); + } + } +} diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs new file mode 100644 index 0000000000..66ebfc3a26 --- /dev/null +++ b/Emby.Naming/Video/StubTypeRule.cs @@ -0,0 +1,17 @@ + +namespace Emby.Naming.Video +{ + public class StubTypeRule + { + /// <summary> + /// Gets or sets the token. + /// </summary> + /// <value>The token.</value> + public string Token { get; set; } + /// <summary> + /// Gets or sets the type of the stub. + /// </summary> + /// <value>The type of the stub.</value> + public string StubType { get; set; } + } +} diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs new file mode 100644 index 0000000000..96839c31ef --- /dev/null +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -0,0 +1,79 @@ + +namespace Emby.Naming.Video +{ + /// <summary> + /// Represents a single video file + /// </summary> + public class VideoFileInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + /// <summary> + /// Gets or sets the container. + /// </summary> + /// <value>The container.</value> + public string Container { get; set; } + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the year. + /// </summary> + /// <value>The year.</value> + public int? Year { get; set; } + /// <summary> + /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc. + /// </summary> + /// <value>The type of the extra.</value> + public string ExtraType { get; set; } + /// <summary> + /// Gets or sets the extra rule. + /// </summary> + /// <value>The extra rule.</value> + public ExtraRule ExtraRule { get; set; } + /// <summary> + /// Gets or sets the format3 d. + /// </summary> + /// <value>The format3 d.</value> + public string Format3D { get; set; } + /// <summary> + /// Gets or sets a value indicating whether [is3 d]. + /// </summary> + /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> + public bool Is3D { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is stub. + /// </summary> + /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value> + public bool IsStub { get; set; } + /// <summary> + /// Gets or sets the type of the stub. + /// </summary> + /// <value>The type of the stub.</value> + public string StubType { get; set; } + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public bool IsDirectory { get; set; } + /// <summary> + /// Gets the file name without extension. + /// </summary> + /// <value>The file name without extension.</value> + public string FileNameWithoutExtension + { + get { return !IsDirectory ? System.IO.Path.GetFileNameWithoutExtension(Path) : System.IO.Path.GetFileName(Path); } + } + + public override string ToString() + { + // Makes debugging easier + return Name ?? base.ToString(); + } + } +} diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs new file mode 100644 index 0000000000..f4d311b975 --- /dev/null +++ b/Emby.Naming/Video/VideoInfo.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Emby.Naming.Video +{ + /// <summary> + /// Represents a complete video, including all parts and subtitles + /// </summary> + public class VideoInfo + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + /// <summary> + /// Gets or sets the year. + /// </summary> + /// <value>The year.</value> + public int? Year { get; set; } + /// <summary> + /// Gets or sets the files. + /// </summary> + /// <value>The files.</value> + public List<VideoFileInfo> Files { get; set; } + /// <summary> + /// Gets or sets the extras. + /// </summary> + /// <value>The extras.</value> + public List<VideoFileInfo> Extras { get; set; } + /// <summary> + /// Gets or sets the alternate versions. + /// </summary> + /// <value>The alternate versions.</value> + public List<VideoFileInfo> AlternateVersions { get; set; } + + public VideoInfo() + { + Files = new List<VideoFileInfo>(); + Extras = new List<VideoFileInfo>(); + AlternateVersions = new List<VideoFileInfo>(); + } + } +} diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs new file mode 100644 index 0000000000..47be28104d --- /dev/null +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -0,0 +1,259 @@ +using Emby.Naming.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Model.IO; +using System.Text.RegularExpressions; + +namespace Emby.Naming.Video +{ + public class VideoListResolver + { + private readonly NamingOptions _options; + + public VideoListResolver(NamingOptions options) + { + _options = options; + } + + public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true) + { + var videoResolver = new VideoResolver(_options); + + var videoInfos = files + .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) + .Where(i => i != null) + .ToList(); + + // Filter out all extras, otherwise they could cause stacks to not be resolved + // See the unit test TestStackedWithTrailer + var nonExtras = videoInfos + .Where(i => string.IsNullOrEmpty(i.ExtraType)) + .Select(i => new FileSystemMetadata + { + FullName = i.Path, + IsDirectory = i.IsDirectory + }); + + var stackResult = new StackResolver(_options) + .Resolve(nonExtras); + + var remainingFiles = videoInfos + .Where(i => !stackResult.Stacks.Any(s => s.ContainsFile(i.Path, i.IsDirectory))) + .ToList(); + + var list = new List<VideoInfo>(); + + foreach (var stack in stackResult.Stacks) + { + var info = new VideoInfo + { + Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList(), + Name = stack.Name + }; + + info.Year = info.Files.First().Year; + + var extraBaseNames = new List<string> + { + stack.Name, + Path.GetFileNameWithoutExtension(stack.Files[0]) + }; + + var extras = GetExtras(remainingFiles, extraBaseNames); + + if (extras.Count > 0) + { + remainingFiles = remainingFiles + .Except(extras) + .ToList(); + + info.Extras = extras; + } + + list.Add(info); + } + + var standaloneMedia = remainingFiles + .Where(i => string.IsNullOrEmpty(i.ExtraType)) + .ToList(); + + foreach (var media in standaloneMedia) + { + var info = new VideoInfo + { + Files = new List<VideoFileInfo> { media }, + Name = media.Name + }; + + info.Year = info.Files.First().Year; + + var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension }); + + remainingFiles = remainingFiles + .Except(extras.Concat(new[] { media })) + .ToList(); + + info.Extras = extras; + + list.Add(info); + } + + if (supportMultiVersion) + { + list = GetVideosGroupedByVersion(list) + .ToList(); + } + + // If there's only one resolved video, use the folder name as well to find extras + if (list.Count == 1) + { + var info = list[0]; + var videoPath = list[0].Files[0].Path; + var parentPath = Path.GetDirectoryName(videoPath); + + if (!string.IsNullOrEmpty(parentPath)) + { + var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath)); + if (!string.IsNullOrEmpty(folderName)) + { + var extras = GetExtras(remainingFiles, new List<string> { folderName }); + + remainingFiles = remainingFiles + .Except(extras) + .ToList(); + + info.Extras.AddRange(extras); + } + } + + // Add the extras that are just based on file name as well + var extrasByFileName = remainingFiles + .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename) + .ToList(); + + remainingFiles = remainingFiles + .Except(extrasByFileName) + .ToList(); + + info.Extras.AddRange(extrasByFileName); + } + + // If there's only one video, accept all trailers + // Be lenient because people use all kinds of mish mash conventions with trailers + if (list.Count == 1) + { + var trailers = remainingFiles + .Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + list[0].Extras.AddRange(trailers); + + remainingFiles = remainingFiles + .Except(trailers) + .ToList(); + } + + // Whatever files are left, just add them + list.AddRange(remainingFiles.Select(i => new VideoInfo + { + Files = new List<VideoFileInfo> { i }, + Name = i.Name, + Year = i.Year + })); + + var orderedList = list.OrderBy(i => i.Name); + + return orderedList; + } + + private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) + { + if (videos.Count == 0) + { + return videos; + } + + var list = new List<VideoInfo>(); + + var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); + + if (!string.IsNullOrEmpty(folderName) && folderName.Length > 1) + { + if (videos.All(i => i.Files.Count == 1 && IsEligibleForMultiVersion(folderName, i.Files[0].Path))) + { + // Enforce the multi-version limit + if (videos.Count <= 8 && HaveSameYear(videos)) + { + var ordered = videos.OrderBy(i => i.Name).ToList(); + + list.Add(ordered[0]); + + list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList(); + list[0].Name = folderName; + list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); + + return list; + } + } + } + + return videos; + //foreach (var video in videos.OrderBy(i => i.Name)) + //{ + // var match = list + // .FirstOrDefault(i => string.Equals(i.Name, video.Name, StringComparison.OrdinalIgnoreCase)); + + // if (match != null && video.Files.Count == 1 && match.Files.Count == 1) + // { + // match.AlternateVersions.Add(video.Files[0]); + // match.Extras.AddRange(video.Extras); + // } + // else + // { + // list.Add(video); + // } + //} + + //return list; + } + + private bool HaveSameYear(List<VideoInfo> videos) + { + return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; + } + + private bool IsEligibleForMultiVersion(string folderName, string testFilename) + { + testFilename = Path.GetFileNameWithoutExtension(testFilename); + + if (string.Equals(folderName, testFilename, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + { + testFilename = testFilename.Substring(folderName.Length).Trim(); + return testFilename.StartsWith("-", StringComparison.OrdinalIgnoreCase)||Regex.Replace(testFilename, @"\[([^]]*)\]", "").Trim() == String.Empty; + } + + return false; + } + + private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames) + { + foreach (var name in baseNames.ToList()) + { + var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd(); + baseNames.Add(trimmedName); + } + + return remainingFiles + .Where(i => !string.IsNullOrEmpty(i.ExtraType)) + .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) + .ToList(); + } + } +} diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs new file mode 100644 index 0000000000..c4951c728e --- /dev/null +++ b/Emby.Naming/Video/VideoResolver.cs @@ -0,0 +1,139 @@ +using Emby.Naming.Common; +using System; +using System.IO; +using System.Linq; + +namespace Emby.Naming.Video +{ + public class VideoResolver + { + private readonly NamingOptions _options; + + public VideoResolver(NamingOptions options) + { + _options = options; + } + + /// <summary> + /// Resolves the directory. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>VideoFileInfo.</returns> + public VideoFileInfo ResolveDirectory(string path) + { + return Resolve(path, true); + } + + /// <summary> + /// Resolves the file. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>VideoFileInfo.</returns> + public VideoFileInfo ResolveFile(string path) + { + return Resolve(path, false); + } + + /// <summary> + /// Resolves the specified path. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="IsDirectory">if set to <c>true</c> [is folder].</param> + /// <returns>VideoFileInfo.</returns> + /// <exception cref="System.ArgumentNullException">path</exception> + public VideoFileInfo Resolve(string path, bool IsDirectory, bool parseName = true) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var isStub = false; + string container = null; + string stubType = null; + + if (!IsDirectory) + { + var extension = Path.GetExtension(path) ?? string.Empty; + // Check supported extensions + if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + var stubResult = new StubResolver(_options).ResolveFile(path); + + isStub = stubResult.IsStub; + + // It's not supported. Check stub extensions + if (!isStub) + { + return null; + } + + stubType = stubResult.StubType; + } + + container = extension.TrimStart('.'); + } + + var flags = new FlagParser(_options).GetFlags(path); + var format3DResult = new Format3DParser(_options).Parse(flags); + + var extraResult = new ExtraResolver(_options).GetExtraInfo(path); + + var name = !IsDirectory + ? Path.GetFileNameWithoutExtension(path) + : Path.GetFileName(path); + + int? year = null; + + if (parseName) + { + var cleanDateTimeResult = CleanDateTime(name); + + if (string.IsNullOrEmpty(extraResult.ExtraType)) + { + name = cleanDateTimeResult.Name; + name = CleanString(name).Name; + } + + year = cleanDateTimeResult.Year; + } + + return new VideoFileInfo + { + Path = path, + Container = container, + IsStub = isStub, + Name = name, + Year = year, + StubType = stubType, + Is3D = format3DResult.Is3D, + Format3D = format3DResult.Format3D, + ExtraType = extraResult.ExtraType, + IsDirectory = IsDirectory, + ExtraRule = extraResult.Rule + }; + } + + public bool IsVideoFile(string path) + { + var extension = Path.GetExtension(path) ?? string.Empty; + return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + } + + public bool IsStubFile(string path) + { + var extension = Path.GetExtension(path) ?? string.Empty; + return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + } + + public CleanStringResult CleanString(string name) + { + return new CleanStringParser().Clean(name, _options.CleanStringRegexes); + } + + public CleanDateTimeResult CleanDateTime(string name) + { + return new CleanDateTimeParser(_options).Clean(name); + } + } +} |
