aboutsummaryrefslogtreecommitdiff
path: root/Emby.Naming/Video
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Naming/Video')
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs87
-rw-r--r--Emby.Naming/Video/CleanDateTimeResult.cs22
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs49
-rw-r--r--Emby.Naming/Video/CleanStringResult.cs17
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs87
-rw-r--r--Emby.Naming/Video/ExtraResult.cs18
-rw-r--r--Emby.Naming/Video/ExtraRule.cs28
-rw-r--r--Emby.Naming/Video/ExtraRuleType.cs19
-rw-r--r--Emby.Naming/Video/FileStack.cs28
-rw-r--r--Emby.Naming/Video/FlagParser.cs35
-rw-r--r--Emby.Naming/Video/Format3DParser.cs81
-rw-r--r--Emby.Naming/Video/Format3DResult.cs28
-rw-r--r--Emby.Naming/Video/Format3DRule.cs17
-rw-r--r--Emby.Naming/Video/StackResolver.cs216
-rw-r--r--Emby.Naming/Video/StackResult.cs14
-rw-r--r--Emby.Naming/Video/StubResolver.cs44
-rw-r--r--Emby.Naming/Video/StubResult.cs28
-rw-r--r--Emby.Naming/Video/StubTypeRule.cs17
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs79
-rw-r--r--Emby.Naming/Video/VideoInfo.cs43
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs259
-rw-r--r--Emby.Naming/Video/VideoResolver.cs139
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);
+ }
+ }
+}