Ver Fonte

Add back all old emby tests

Bond_009 há 5 anos atrás
pai
commit
35151553e3
34 ficheiros alterados com 3653 adições e 64 exclusões
  1. 21 20
      Emby.Naming/Common/NamingOptions.cs
  2. 1 1
      Emby.Naming/Video/CleanStringParser.cs
  3. 1 1
      Emby.Naming/Video/ExtraResolver.cs
  4. 4 1
      Emby.Naming/Video/ExtraResult.cs
  5. 4 1
      Emby.Naming/Video/ExtraRule.cs
  6. 2 2
      Emby.Naming/Video/StubResolver.cs
  7. 4 2
      Emby.Naming/Video/VideoFileInfo.cs
  8. 5 4
      Emby.Naming/Video/VideoListResolver.cs
  9. 1 1
      Emby.Naming/Video/VideoResolver.cs
  10. 4 27
      Emby.Server.Implementations/Library/LibraryManager.cs
  11. 4 1
      MediaBrowser.Model/Entities/MediaType.cs
  12. 2 0
      jellyfin.ruleset
  13. 57 0
      tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
  14. 40 0
      tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
  15. 61 0
      tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs
  16. 69 0
      tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
  17. 421 0
      tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
  18. 127 0
      tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
  19. 3 3
      tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
  20. 56 0
      tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs
  21. 105 0
      tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
  22. 112 0
      tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
  23. 305 0
      tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
  24. 95 0
      tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
  25. 15 0
      tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs
  26. 143 0
      tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
  27. 133 0
      tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
  28. 77 0
      tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
  29. 78 0
      tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
  30. 438 0
      tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
  31. 478 0
      tests/Jellyfin.Naming.Tests/Video/StackTests.cs
  32. 55 0
      tests/Jellyfin.Naming.Tests/Video/StubTests.cs
  33. 457 0
      tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
  34. 275 0
      tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs

+ 21 - 20
Emby.Naming/Common/NamingOptions.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
 
 namespace Emby.Naming.Common
 {
@@ -173,7 +174,7 @@ namespace Emby.Naming.Common
 
             CleanDateTimes = new[]
             {
-                @"(.+[^ _\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9][0-9]|20[0-1][0-9])([ _\,\.\(\)\[\]\-][^0-9]|$)"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9][0-9]|20[0-1][0-9])([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9][0-9]|20[0-1][0-9])*"
             };
 
             CleanStrings = new[]
@@ -336,7 +337,7 @@ namespace Emby.Naming.Common
 
                 // *** End Kodi Standard Naming
 
-                // [bar] Foo - 1 [baz]
+                // [bar] Foo - 1 [baz]
                 new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>(\w+\s*?)+?)[-\s_]+(?<epnumber>\d+).*$")
                 {
                     IsNamed = true
@@ -420,126 +421,126 @@ namespace Emby.Naming.Common
             {
                 new ExtraRule
                 {
-                    ExtraType = "trailer",
+                    ExtraType = ExtraType.Trailer,
                     RuleType = ExtraRuleType.Filename,
                     Token = "trailer",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "trailer",
+                    ExtraType = ExtraType.Trailer,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-trailer",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "trailer",
+                    ExtraType = ExtraType.Trailer,
                     RuleType = ExtraRuleType.Suffix,
                     Token = ".trailer",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "trailer",
+                    ExtraType = ExtraType.Trailer,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "_trailer",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "trailer",
+                    ExtraType = ExtraType.Trailer,
                     RuleType = ExtraRuleType.Suffix,
                     Token = " trailer",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "sample",
+                    ExtraType = ExtraType.Sample,
                     RuleType = ExtraRuleType.Filename,
                     Token = "sample",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "sample",
+                    ExtraType = ExtraType.Sample,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-sample",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "sample",
+                    ExtraType = ExtraType.Sample,
                     RuleType = ExtraRuleType.Suffix,
                     Token = ".sample",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "sample",
+                    ExtraType = ExtraType.Sample,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "_sample",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "sample",
+                    ExtraType = ExtraType.Sample,
                     RuleType = ExtraRuleType.Suffix,
                     Token = " sample",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "themesong",
+                    ExtraType = ExtraType.ThemeSong,
                     RuleType = ExtraRuleType.Filename,
                     Token = "theme",
                     MediaType = MediaType.Audio
                 },
                 new ExtraRule
                 {
-                    ExtraType = "scene",
+                    ExtraType = ExtraType.Scene,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-scene",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "clip",
+                    ExtraType = ExtraType.Clip,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-clip",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "interview",
+                    ExtraType = ExtraType.Interview,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-interview",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "behindthescenes",
+                    ExtraType = ExtraType.BehindTheScenes,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-behindthescenes",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "deletedscene",
+                    ExtraType = ExtraType.DeletedScene,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-deleted",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "featurette",
+                    ExtraType = ExtraType.Clip,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-featurette",
                     MediaType = MediaType.Video
                 },
                 new ExtraRule
                 {
-                    ExtraType = "short",
+                    ExtraType = ExtraType.Clip,
                     RuleType = ExtraRuleType.Suffix,
                     Token = "-short",
                     MediaType = MediaType.Video

+ 1 - 1
Emby.Naming/Video/CleanStringParser.cs

@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
 namespace Emby.Naming.Video
 {
     /// <summary>
-    /// http://kodi.wiki/view/Advancedsettings.xml#video
+    /// <see href="http://kodi.wiki/view/Advancedsettings.xml#video" />.
     /// </summary>
     public class CleanStringParser
     {

+ 1 - 1
Emby.Naming/Video/ExtraResolver.cs

@@ -20,7 +20,7 @@ namespace Emby.Naming.Video
         {
             return _options.VideoExtraRules
                 .Select(i => GetExtraInfo(path, i))
-                .FirstOrDefault(i => !string.IsNullOrEmpty(i.ExtraType)) ?? new ExtraResult();
+                .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
         }
 
         private ExtraResult GetExtraInfo(string path, ExtraRule rule)

+ 4 - 1
Emby.Naming/Video/ExtraResult.cs

@@ -1,3 +1,5 @@
+using MediaBrowser.Model.Entities;
+
 namespace Emby.Naming.Video
 {
     public class ExtraResult
@@ -6,7 +8,8 @@ namespace Emby.Naming.Video
         /// Gets or sets the type of the extra.
         /// </summary>
         /// <value>The type of the extra.</value>
-        public string ExtraType { get; set; }
+        public ExtraType? ExtraType { get; set; }
+
         /// <summary>
         /// Gets or sets the rule.
         /// </summary>

+ 4 - 1
Emby.Naming/Video/ExtraRule.cs

@@ -9,16 +9,19 @@ namespace Emby.Naming.Video
         /// </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; }
+        public MediaBrowser.Model.Entities.ExtraType 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>

+ 2 - 2
Emby.Naming/Video/StubResolver.cs

@@ -11,14 +11,14 @@ namespace Emby.Naming.Video
         {
             if (path == null)
             {
-                return default(StubResult);
+                return default;
             }
 
             var extension = Path.GetExtension(path);
 
             if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
             {
-                return default(StubResult);
+                return default;
             }
 
             var result = new StubResult()

+ 4 - 2
Emby.Naming/Video/VideoFileInfo.cs

@@ -1,3 +1,5 @@
+using MediaBrowser.Model.Entities;
+
 namespace Emby.Naming.Video
 {
     /// <summary>
@@ -30,10 +32,10 @@ namespace Emby.Naming.Video
         public int? Year { get; set; }
 
         /// <summary>
-        /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc.
+        /// Gets or sets the type of the extra, e.g. trailer, theme song, behind the scenes, etc.
         /// </summary>
         /// <value>The type of the extra.</value>
-        public string ExtraType { get; set; }
+        public ExtraType? ExtraType { get; set; }
 
         /// <summary>
         /// Gets or sets the extra rule.

+ 5 - 4
Emby.Naming/Video/VideoListResolver.cs

@@ -4,6 +4,7 @@ using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 
 namespace Emby.Naming.Video
@@ -29,7 +30,7 @@ namespace Emby.Naming.Video
             // 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))
+                .Where(i => i.ExtraType == null)
                 .Select(i => new FileSystemMetadata
                 {
                     FullName = i.Path,
@@ -76,7 +77,7 @@ namespace Emby.Naming.Video
             }
 
             var standaloneMedia = remainingFiles
-                .Where(i => string.IsNullOrEmpty(i.ExtraType))
+                .Where(i => i.ExtraType == null)
                 .ToList();
 
             foreach (var media in standaloneMedia)
@@ -145,7 +146,7 @@ namespace Emby.Naming.Video
             if (list.Count == 1)
             {
                 var trailers = remainingFiles
-                    .Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase))
+                    .Where(i => i.ExtraType == ExtraType.Trailer)
                     .ToList();
 
                 list[0].Extras.AddRange(trailers);
@@ -226,7 +227,7 @@ namespace Emby.Naming.Video
             }
 
             return remainingFiles
-                .Where(i => !string.IsNullOrEmpty(i.ExtraType))
+                .Where(i => i.ExtraType == null)
                 .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
                 .ToList();
         }

+ 1 - 1
Emby.Naming/Video/VideoResolver.cs

@@ -91,7 +91,7 @@ namespace Emby.Naming.Video
             {
                 var cleanDateTimeResult = CleanDateTime(name);
 
-                if (string.IsNullOrEmpty(extraResult.ExtraType))
+                if (extraResult.ExtraType == null)
                 {
                     name = CleanString(cleanDateTimeResult.Name).Name;
                 }

+ 4 - 27
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -2556,7 +2556,7 @@ namespace Emby.Server.Implementations.Library
 
             if (currentVideo != null)
             {
-                files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+                files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
             }
 
             var resolvers = new IItemResolver[]
@@ -2606,7 +2606,7 @@ namespace Emby.Server.Implementations.Library
 
             if (currentVideo != null)
             {
-                files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+                files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
             }
 
             return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
@@ -2710,7 +2710,7 @@ namespace Emby.Server.Implementations.Library
 
             if (!string.Equals(newPath, path, StringComparison.Ordinal))
             {
-                if (to.IndexOf('/') != -1)
+                if (to.IndexOf('/', StringComparison.Ordinal) != -1)
                 {
                     newPath = newPath.Replace('\\', '/');
                 }
@@ -2731,30 +2731,7 @@ namespace Emby.Server.Implementations.Library
 
             var result = resolver.GetExtraInfo(item.Path);
 
-            if (string.Equals(result.ExtraType, "deletedscene", StringComparison.OrdinalIgnoreCase))
-            {
-                item.ExtraType = ExtraType.DeletedScene;
-            }
-            else if (string.Equals(result.ExtraType, "behindthescenes", StringComparison.OrdinalIgnoreCase))
-            {
-                item.ExtraType = ExtraType.BehindTheScenes;
-            }
-            else if (string.Equals(result.ExtraType, "interview", StringComparison.OrdinalIgnoreCase))
-            {
-                item.ExtraType = ExtraType.Interview;
-            }
-            else if (string.Equals(result.ExtraType, "scene", StringComparison.OrdinalIgnoreCase))
-            {
-                item.ExtraType = ExtraType.Scene;
-            }
-            else if (string.Equals(result.ExtraType, "sample", StringComparison.OrdinalIgnoreCase))
-            {
-                item.ExtraType = ExtraType.Sample;
-            }
-            else
-            {
-                item.ExtraType = ExtraType.Clip;
-            }
+            item.ExtraType = result.ExtraType;
         }
 
         public List<PersonInfo> GetPeople(InternalPeopleQuery query)

+ 4 - 1
MediaBrowser.Model/Entities/MediaType.cs

@@ -3,20 +3,23 @@ namespace MediaBrowser.Model.Entities
     /// <summary>
     /// Class MediaType
     /// </summary>
-    public class MediaType
+    public static class MediaType
     {
         /// <summary>
         /// The video
         /// </summary>
         public const string Video = "Video";
+
         /// <summary>
         /// The audio
         /// </summary>
         public const string Audio = "Audio";
+
         /// <summary>
         /// The photo
         /// </summary>
         public const string Photo = "Photo";
+
         /// <summary>
         /// The book
         /// </summary>

+ 2 - 0
jellyfin.ruleset

@@ -24,6 +24,8 @@
     <Rule Id="SA1413" Action="None" />
     <!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
     <Rule Id="SA1512" Action="None" />
+    <!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
+    <Rule Id="SA1515" Action="None" />
     <!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
     <Rule Id="SA1633" Action="None" />
   </Rules>

+ 57 - 0
tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs

@@ -0,0 +1,57 @@
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Music
+{
+    public class MultiDiscAlbumTests
+    {
+        [Fact]
+        public void TestMultiDiscAlbums()
+        {
+            Assert.False(IsMultiDiscAlbumFolder(@"blah blah"));
+            Assert.False(IsMultiDiscAlbumFolder(@"d:/music\weezer/03 Pinkerton"));
+            Assert.False(IsMultiDiscAlbumFolder(@"d:/music/michael jackson/Bad (2012 Remaster)"));
+
+            Assert.True(IsMultiDiscAlbumFolder(@"cd1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disc1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disk1"));
+
+            // Add a space
+            Assert.True(IsMultiDiscAlbumFolder(@"cd 1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disc 1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disk 1"));
+
+            Assert.True(IsMultiDiscAlbumFolder(@"cd  - 1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disc- 1"));
+            Assert.True(IsMultiDiscAlbumFolder(@"disk - 1"));
+
+            Assert.True(IsMultiDiscAlbumFolder(@"Disc 01 (Hugo Wolf · 24 Lieder)"));
+            Assert.True(IsMultiDiscAlbumFolder(@"Disc 04 (Encores and Folk Songs)"));
+            Assert.True(IsMultiDiscAlbumFolder(@"Disc04 (Encores and Folk Songs)"));
+            Assert.True(IsMultiDiscAlbumFolder(@"Disc 04(Encores and Folk Songs)"));
+            Assert.True(IsMultiDiscAlbumFolder(@"Disc04(Encores and Folk Songs)"));
+
+            Assert.True(IsMultiDiscAlbumFolder(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2"));
+        }
+
+        [Fact]
+        public void TestMultiDiscAlbums1()
+        {
+            Assert.False(IsMultiDiscAlbumFolder(@"[1985] Oppurtunities (Let's make lots of money) (1985)"));
+        }
+
+        [Fact]
+        public void TestMultiDiscAlbums2()
+        {
+            Assert.False(IsMultiDiscAlbumFolder(@"Blah 04(Encores and Folk Songs)"));
+        }
+
+        private bool IsMultiDiscAlbumFolder(string path)
+        {
+            var parser = new AlbumParser(new NamingOptions());
+
+            return parser.ParseMultiPart(path).IsMultiPart;
+        }
+    }
+}

+ 40 - 0
tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs

@@ -0,0 +1,40 @@
+using Emby.Naming.Common;
+using Emby.Naming.Subtitles;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Subtitles
+{
+    public class SubtitleParserTests
+    {
+        private SubtitleParser GetParser()
+        {
+            var options = new NamingOptions();
+
+            return new SubtitleParser(options);
+        }
+
+        [Fact]
+        public void TestSubtitles()
+        {
+            Test("The Skin I Live In (2011).srt", null, false, false);
+            Test("The Skin I Live In (2011).eng.srt", "eng", false, false);
+            Test("The Skin I Live In (2011).eng.default.srt", "eng", true, false);
+            Test("The Skin I Live In (2011).eng.forced.srt", "eng", false, true);
+            Test("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true);
+            Test("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true);
+
+            Test("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true);
+        }
+
+        private void Test(string input, string language, bool isDefault, bool isForced)
+        {
+            var parser = GetParser();
+
+            var result = parser.ParseFile(input);
+
+            Assert.Equal(language, result.Language, true);
+            Assert.Equal(isDefault, result.IsDefault);
+            Assert.Equal(isForced, result.IsForced);
+        }
+    }
+}

+ 61 - 0
tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs

@@ -0,0 +1,61 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class AbsoluteEpisodeNumberTests
+    {
+        [Fact]
+        public void TestAbsoluteEpisodeNumber1()
+        {
+            Assert.Equal(12, GetEpisodeNumberFromFile(@"The Simpsons/12.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber2()
+        {
+            Assert.Equal(12, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 12.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber3()
+        {
+            Assert.Equal(82, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 82.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber4()
+        {
+            Assert.Equal(112, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 112.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber5()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/Foo_ep_02.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber6()
+        {
+            Assert.Equal(889, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 889.avi"));
+        }
+
+        [Fact]
+        public void TestAbsoluteEpisodeNumber7()
+        {
+            Assert.Equal(101, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 101.avi"));
+        }
+
+        private int? GetEpisodeNumberFromFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false, null, null, true);
+
+            return result.EpisodeNumber;
+        }
+    }
+}

+ 69 - 0
tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs

@@ -0,0 +1,69 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class DailyEpisodeTests
+    {
+        [Fact]
+        public void TestDailyEpisode1()
+        {
+            Test(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14);
+        }
+
+        [Fact]
+        public void TestDailyEpisode2()
+        {
+            Test(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestDailyEpisode3()
+        {
+            Test(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestDailyEpisode4()
+        {
+            Test(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15);
+        }
+
+        [Fact]
+        public void TestDailyEpisode5()
+        {
+            Test(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20);
+        }
+
+        [Fact]
+        public void TestDailyEpisode6()
+        {
+            Test(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestDailyEpisode7()
+        {
+            Test(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25);
+        }
+
+        private void Test(string path, string seriesName, int? year, int? month, int? day)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            Assert.Null(result.SeasonNumber);
+            Assert.Null(result.EpisodeNumber);
+            Assert.Equal(year, result.Year);
+            Assert.Equal(month, result.Month);
+            Assert.Equal(day, result.Day);
+            Assert.Equal(seriesName, result.SeriesName, true);
+        }
+    }
+}

+ 421 - 0
tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs

@@ -0,0 +1,421 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class EpisodeNumberTests
+    {
+        [Fact]
+        public void TestEpisodeNumber1()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/S02E03 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber40()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber41()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber42()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber43()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber44()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber45()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber46()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber47()
+        {
+            Assert.Equal(36, GetEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber50()
+        {
+            // This convention is not currently supported, just adding in case we want to look at it in the future
+            Assert.Equal(1, GetEpisodeNumberFromFile(@"2016/Season s2016e1.mp4"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber51()
+        {
+            // This convention is not currently supported, just adding in case we want to look at it in the future
+            Assert.Equal(1, GetEpisodeNumberFromFile(@"2016/Season 2016x1.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber52()
+        {
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode - 16.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber53()
+        {
+            // This is not supported. Expected to fail, although it would be a good one to add support for.
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16.avi"));
+        }
+        [Fact]
+        public void TestEpisodeNumber54()
+        {
+            // This is not supported. Expected to fail, although it would be a good one to add support for.
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16 - Some Title.avi"));
+        }
+        [Fact]
+        public void TestEpisodeNumber55()
+        {
+            // This is not supported. Expected to fail, although it would be a good one to add support for.
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Season 3 Episode 16.avi"));
+        }
+        [Fact]
+        public void TestEpisodeNumber56()
+        {
+            // This is not supported. Expected to fail, although it would be a good one to add support for.
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Season 3 Episode 16 - Some Title.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber57()
+        {
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 Some Title.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber58()
+        {
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 - 12 Some Title.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber59()
+        {
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 - 12 Angry Men.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber60()
+        {
+            Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 12 Some Title.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber61()
+        {
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 12 Angry Men.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber62()
+        {
+            // This is not supported. Expected to fail, although it would be a good one to add support for.
+            Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.E03.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber63()
+        {
+            Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.S04E03.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber64()
+        {
+            Assert.Equal(368, GetEpisodeNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber65()
+        {
+            // Not supported yet
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber30()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber31()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber32()
+        {
+            Assert.Equal(9, GetEpisodeNumberFromFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber33()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber34()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber35()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber36()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber37()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber38()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber39()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber20()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber21()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber22()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber23()
+        {
+            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber24()
+        {
+            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber25()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber26()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber27()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumber28()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber29()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber11()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber12()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber13()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber14()
+        {
+            Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber15()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber16()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber17()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber18()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber19()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber2()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber3()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber4()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber5()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber6()
+        {
+            Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber7()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber8()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber9()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber10()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber48()
+        {
+            Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/2. Infestation.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumber49()
+        {
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi"));
+        }
+
+        private int? GetEpisodeNumberFromFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodePathParser(options)
+                .Parse(path, false);
+
+            return result.EpisodeNumber;
+        }
+    }
+}

+ 127 - 0
tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs

@@ -0,0 +1,127 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class EpisodeNumberWithoutSeasonTests
+    {
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason1()
+        {
+            Assert.Equal(8, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason2()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02 - Ep Name.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason3()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason4()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02 - Ep Name.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason5()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02-Ep Name.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason6()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/02.EpName.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason7()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason8()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons - 02 Ep Name.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumberWithoutSeason9()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumberWithoutSeason10()
+        {
+            Assert.Equal(2, GetEpisodeNumberFromFile(@"The Simpsons/The Simpsons 5 - 02 Ep Name.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumberWithoutSeason11()
+        {
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"Seinfeld/Seinfeld 0807 The Checks.avi"));
+            Assert.Equal(8, GetSeasonNumberFromFile(@"Seinfeld/Seinfeld 0807 The Checks.avi"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason12()
+        {
+            Assert.Equal(7, GetEpisodeNumberFromFile(@"GJ Club (2013)/GJ Club - 07.mkv"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestEpisodeNumberWithoutSeason13()
+        {
+            // This is not supported anymore after removing the episode number 365+ hack from EpisodePathParser
+            Assert.Equal(13, GetEpisodeNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 13.mkv"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason14()
+        {
+            Assert.Equal(3, GetSeasonNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 317.mkv"));
+            Assert.Equal(17, GetEpisodeNumberFromFile(@"Case Closed (1996-2007)/Case Closed - 317.mkv"));
+        }
+
+        [Fact]
+        public void TestEpisodeNumberWithoutSeason15()
+        {
+            Assert.Equal(2017, GetSeasonNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
+        }
+
+        private int? GetEpisodeNumberFromFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            return result.EpisodeNumber;
+        }
+
+        private int? GetSeasonNumberFromFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            return result.SeasonNumber;
+        }
+
+    }
+}

+ 3 - 3
tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs → tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs

@@ -2,7 +2,7 @@ using Emby.Naming.Common;
 using Emby.Naming.TV;
 using Xunit;
 
-namespace Jellyfin.Naming.Tests
+namespace Jellyfin.Naming.Tests.TV
 {
     public class EpisodePathParserTest
     {
@@ -23,7 +23,7 @@ namespace Jellyfin.Naming.Tests
             Assert.Equal(episode, res.EpisodeNumber);
 
             // testing other paths delimeter
-            var res2 = p.Parse(path.Replace('/', '\\'), false);
+            var res2 = p.Parse(path.Replace('/', '/'), false);
             Assert.True(res2.Success);
             Assert.Equal(name, res2.SeriesName);
             Assert.Equal(season, res2.SeasonNumber);
@@ -45,7 +45,7 @@ namespace Jellyfin.Naming.Tests
             Assert.Equal(episode, res.EpisodeNumber);
 
             // testing other paths delimeter
-            var res2 = p.Parse(path.Replace('/', '\\'), false, fillExtendedInfo: false);
+            var res2 = p.Parse(path.Replace('/', '/'), false, fillExtendedInfo: false);
             Assert.True(res2.Success);
             Assert.Equal(name, res2.SeriesName);
             Assert.Null(res2.SeasonNumber);

+ 56 - 0
tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs

@@ -0,0 +1,56 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class EpisodeWithoutSeasonTests
+    {
+        // FIXME
+        // [Fact]
+        public void TestWithoutSeason1()
+        {
+            Test(@"/server/anything_ep02.mp4", "anything", null, 2);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestWithoutSeason2()
+        {
+            Test(@"/server/anything_ep_02.mp4", "anything", null, 2);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestWithoutSeason3()
+        {
+            Test(@"/server/anything_part.II.mp4", "anything", null, null);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestWithoutSeason4()
+        {
+            Test(@"/server/anything_pt.II.mp4", "anything", null, null);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestWithoutSeason5()
+        {
+            Test(@"/server/anything_pt_II.mp4", "anything", null, null);
+        }
+
+        private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            Assert.Equal(seasonNumber, result.SeasonNumber);
+            Assert.Equal(episodeNumber, result.EpisodeNumber);
+            Assert.Equal(seriesName, result.SeriesName, true);
+        }
+    }
+}

+ 105 - 0
tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs

@@ -0,0 +1,105 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class MultiEpisodeTests
+    {
+        [Fact]
+        public void TestGetEndingEpisodeNumberFromFile()
+        {
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/4x01 – 20 Hours in America (1).mkv"));
+
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02x03 - 04 Ep Name.mp4"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/My show name 02x03 - 04 Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+            Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+            Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 1/S01E23-E24-E26 - The Woman.mp4"));
+
+
+            // Four Digits seasons
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+            Assert.Equal(15, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+            Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+            Assert.Equal(26, GetEndingEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+
+            // Without season number
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/02.avi"));
+
+            Assert.Equal(3, GetEndingEpisodeNumberFromFile(@"Season 1/02-03 - blah.avi"));
+            Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 2/02-04 - blah 14 blah.avi"));
+            Assert.Equal(5, GetEndingEpisodeNumberFromFile(@"Season 1/02-05 - blah-02 a.avi"));
+            Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 2/02-04.avi"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
+
+            // With format specification that must not be detected as ending episode number
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-1080p.mkv"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-720p.mkv"));
+            Assert.Null(GetEndingEpisodeNumberFromFile(@"Season 1/series-s09e14-720i.mkv"));
+            Assert.Equal(4, GetEndingEpisodeNumberFromFile(@"Season 1/MOONLIGHTING_s01e01-e04.mkv"));
+        }
+
+        [Fact]
+        public void TestGetEndingEpisodeNumberFromFolder()
+        {
+            Assert.Equal(4, GetEndingEpisodeNumberFromFolder(@"Season 1/MOONLIGHTING_s01e01-e04"));
+        }
+
+        private int? GetEndingEpisodeNumberFromFolder(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodePathParser(options)
+                .Parse(path, true);
+
+            return result.EndingEpsiodeNumber;
+        }
+
+        private int? GetEndingEpisodeNumberFromFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodePathParser(options)
+                .Parse(path, false);
+
+            return result.EndingEpsiodeNumber;
+        }
+    }
+}

+ 112 - 0
tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs

@@ -0,0 +1,112 @@
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class SeasonFolderTests
+    {
+        [Fact]
+        public void TestGetSeasonNumberFromPath1()
+        {
+            Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season 1"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath2()
+        {
+            Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Season 2"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath3()
+        {
+            Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Season 02"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath4()
+        {
+            Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season 1"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath5()
+        {
+            Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Seinfeld/S02"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath6()
+        {
+            Assert.Equal(2, GetSeasonNumberFromPath(@"/Drive/Seinfeld/2"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath7()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromPath(@"/Drive/Season 2009"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath8()
+        {
+            Assert.Equal(1, GetSeasonNumberFromPath(@"/Drive/Season1"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath9()
+        {
+            Assert.Equal(4, GetSeasonNumberFromPath(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath10()
+        {
+            Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Season 7 (2016)"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath11()
+        {
+            Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Staffel 7 (2016)"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath12()
+        {
+            Assert.Equal(7, GetSeasonNumberFromPath(@"/Drive/Stagione 7 (2016)"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath14()
+        {
+            Assert.Null(GetSeasonNumberFromPath(@"/Drive/Season (8)"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath13()
+        {
+            Assert.Equal(3, GetSeasonNumberFromPath(@"/Drive/3.Staffel"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath15()
+        {
+            Assert.Null(GetSeasonNumberFromPath(@"/Drive/s06e05"));
+        }
+
+        [Fact]
+        public void TestGetSeasonNumberFromPath16()
+        {
+            Assert.Null(GetSeasonNumberFromPath(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv"));
+        }
+
+        private int? GetSeasonNumberFromPath(string path)
+        {
+            var result = new SeasonPathParser()
+                .Parse(path, true, true);
+
+            return result.SeasonNumber;
+        }
+    }
+}

+ 305 - 0
tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs

@@ -0,0 +1,305 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class SeasonNumberTests
+    {
+        private int? GetSeasonNumberFromEpisodeFile(string path)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            return result.SeasonNumber;
+        }
+
+        [Fact]
+        public void TestSeasonNumber1()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"/Show/Season 02/S02E03 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber2()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber3()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber4()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01xE02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber5()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber6()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber7()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01xE02 blah.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestSeasonNumber8()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname 01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber9()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber10()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/seriesname S01E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber11()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber12()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber13()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber14()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber15()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber16()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber17()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber18()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber19()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber20()
+        {
+            Assert.Equal(2, GetSeasonNumberFromEpisodeFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber21()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber22()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Season 1/S01E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber23()
+        {
+            Assert.Equal(25, GetSeasonNumberFromEpisodeFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber24()
+        {
+            Assert.Equal(25, GetSeasonNumberFromEpisodeFile(@"The Simpsons/The Simpsons.S25E09.Steal this episode.mp4"));
+        }
+
+        [Fact]
+        public void TestSeasonNumber25()
+        {
+            Assert.Equal(2016, GetSeasonNumberFromEpisodeFile(@"2016/Season s2016e1.mp4"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestSeasonNumber26()
+        {
+            // This convention is not currently supported, just adding in case we want to look at it in the future
+            Assert.Equal(2016, GetSeasonNumberFromEpisodeFile(@"2016/Season 2016x1.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber1()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber2()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber3()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber4()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009xE02 blah.avi"));
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestFourDigitSeasonNumber5()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname 2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber6()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname S2009x02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber7()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/seriesname S2009E02 blah.avi"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber8()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber9()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber10()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber11()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber12()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber13()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber14()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber15()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber16()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber17()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber18()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber19()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestFourDigitSeasonNumber20()
+        {
+            Assert.Equal(2009, GetSeasonNumberFromEpisodeFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
+        }
+
+        [Fact]
+        public void TestNoSeriesFolder()
+        {
+            Assert.Equal(1, GetSeasonNumberFromEpisodeFile(@"Series/1-12 - The Woman.mp4"));
+        }
+    }
+}

+ 95 - 0
tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs

@@ -0,0 +1,95 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+    public class SimpleEpisodeTests
+    {
+        [Fact]
+        public void TestSimpleEpisodePath1()
+        {
+            Test(@"/server/anything_s01e02.mp4", "anything", 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath2()
+        {
+            Test(@"/server/anything_s1e2.mp4", "anything", 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath3()
+        {
+            Test(@"/server/anything_s01.e02.mp4", "anything", 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath4()
+        {
+            Test(@"/server/anything_s01_e02.mp4", "anything", 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath5()
+        {
+            Test(@"/server/anything_102.mp4", "anything", 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath6()
+        {
+            Test(@"/server/anything_1x02.mp4", "anything", 1, 2);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestSimpleEpisodePath7()
+        {
+            Test(@"/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath8()
+        {
+            Test(@"/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1);
+        }
+
+
+        [Fact]
+        public void TestSimpleEpisodePath9()
+        {
+            Test(@"/server/Temp/S01E02 foo.mp4", string.Empty, 1, 2);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath10()
+        {
+            Test(@"Series/4-12 - The Woman.mp4", string.Empty, 4, 12);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath11()
+        {
+            Test(@"Series/4x12 - The Woman.mp4", string.Empty, 4, 12);
+        }
+
+        [Fact]
+        public void TestSimpleEpisodePath12()
+        {
+            Test(@"Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32);
+        }
+
+        private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+        {
+            var options = new NamingOptions();
+
+            var result = new EpisodeResolver(options)
+                .Resolve(path, false);
+
+            Assert.Equal(seasonNumber, result.SeasonNumber);
+            Assert.Equal(episodeNumber, result.EpisodeNumber);
+            Assert.Equal(seriesName, result.SeriesName, true);
+        }
+    }
+}

+ 15 - 0
tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs

@@ -0,0 +1,15 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public abstract class BaseVideoTest
+    {
+        protected VideoResolver GetParser()
+        {
+            var options = new NamingOptions();
+
+            return new VideoResolver(options);
+        }
+    }
+}

+ 143 - 0
tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs

@@ -0,0 +1,143 @@
+using System.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class CleanDateTimeTests : BaseVideoTest
+    {
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTime()
+        {
+            Test(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013);
+            Test(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013);
+            Test(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013);
+            Test(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013);
+
+            Test(@"300 (2006).mkv", "300", 2006);
+            Test(@"d:/movies/300 (2006).mkv", "300", 2006);
+            Test(@"300 2 (2006).mkv", "300 2", 2006);
+            Test(@"300 - 2 (2006).mkv", "300 - 2", 2006);
+            Test(@"300 2001 (2006).mkv", "300 2001", 2006);
+
+            Test(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013);
+            Test(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013);
+
+            Test(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTime1()
+        {
+            Test(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithoutFileExtension()
+        {
+            Test(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013);
+            Test(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013);
+            Test(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013);
+            Test(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013);
+
+            Test(@"300 (2006)", "300", 2006);
+            Test(@"d:/movies/300 (2006)", "300", 2006);
+            Test(@"300 2 (2006)", "300 2", 2006);
+            Test(@"300 - 2 (2006)", "300 - 2", 2006);
+            Test(@"300 2001 (2006)", "300 2001", 2006);
+
+            Test(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006);
+            Test(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006);
+        }
+
+        [Fact]
+        public void TestCleanDateTimeWithoutDate()
+        {
+            Test(@"American.Psycho.mkv", "American.Psycho.mkv", null);
+            Test(@"American Psycho.mkv", "American Psycho.mkv", null);
+        }
+
+        [Fact]
+        public void TestCleanDateTimeWithBracketedName()
+        {
+            Test(@"[rec].mkv", "[rec].mkv", null);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithoutExtension()
+        {
+            Test(@"St. Vincent (2014)", "St. Vincent", 2014);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithoutDate1()
+        {
+            Test("Super movie(2009).mp4", "Super movie", 2009);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithoutParenthesis()
+        {
+            Test("Drug War 2013.mp4", "Drug War", 2013);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithMultipleYears()
+        {
+            Test("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithYearAndResolution()
+        {
+            Test("First Man 2018 1080p.mkv", "First Man", 2018);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithYearAndResolution1()
+        {
+            Test("First Man (2018) 1080p.mkv", "First Man", 2018);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateTimeWithSceneRelease()
+        {
+            Test("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestYearInBrackets()
+        {
+            Test("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018);
+        }
+
+        private void Test(string input, string expectedName, int? expectedYear)
+        {
+            input = Path.GetFileName(input);
+
+            var result = GetParser().CleanDateTime(input);
+
+            Assert.Equal(expectedName, result.Name, true);
+            Assert.Equal(expectedYear, result.Year);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateAndStringsSequence()
+        {
+            // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+
+            Test(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014);
+        }
+    }
+}

+ 133 - 0
tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs

@@ -0,0 +1,133 @@
+using System;
+using System.Globalization;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class CleanStringTests : BaseVideoTest
+    {
+        // FIXME
+        // [Fact]
+        public void TestCleanString()
+        {
+            Test("Super movie 480p.mp4", "Super movie");
+            Test("Super movie 480p 2001.mp4", "Super movie");
+            Test("Super movie [480p].mp4", "Super movie");
+            Test("480 Super movie [tmdbid=12345].mp4", "480 Super movie");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanString1()
+        {
+            Test("Super movie(2009).mp4", "Super movie(2009).mp4");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanString2()
+        {
+            Test("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestStringWithoutDate()
+        {
+            Test(@"American.Psycho.mkv", "American.Psycho.mkv");
+            Test(@"American Psycho.mkv", "American Psycho.mkv");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestNameWithBrackets()
+        {
+            Test(@"[rec].mkv", "[rec].mkv");
+        }
+
+        // FIXME
+        // [Fact]
+        public void Test4k()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestUltraHd()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestUHd()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestHDR()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.HDR.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestHDC()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.HDC.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestHDC1()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon-HDC.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestBDrip()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestBDripHDC()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMulti()
+        {
+            Test("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestLeadingBraces()
+        {
+            // Not actually supported, just reported by a user
+            Test("[0004] - After The Sunset.el.mkv", "After The Sunset");
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestTrailingBraces()
+        {
+            Test("After The Sunset - [0004].mkv", "After The Sunset");
+        }
+
+        private void Test(string input, string expectedName)
+        {
+            var result = GetParser().CleanString(input).ToString();
+
+            Assert.Equal(expectedName, result, true);
+        }
+    }
+}

+ 77 - 0
tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs

@@ -0,0 +1,77 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class ExtraTests : BaseVideoTest
+    {
+        // Requirements
+        // movie-deleted = ExtraType deletedscene
+
+        // All of the above rules should be configurable through the options objects (ideally, even the ExtraTypes)
+
+        [Fact]
+        public void TestKodiExtras()
+        {
+            var videoOptions = new NamingOptions();
+
+            Test("trailer.mp4", ExtraType.Trailer, videoOptions);
+            Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
+
+            Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
+        }
+
+        [Fact]
+        public void TestExpandedExtras()
+        {
+            var videoOptions = new NamingOptions();
+
+            Test("trailer.mp4", ExtraType.Trailer, videoOptions);
+            Test("trailer.mp3", null, videoOptions);
+            Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
+
+            Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
+            Test("theme.mkv", null, videoOptions);
+
+            Test("300-scene.mp4", ExtraType.Scene, videoOptions);
+            Test("300-scene2.mp4", ExtraType.Scene, videoOptions);
+            Test("300-clip.mp4", ExtraType.Clip, videoOptions);
+
+            Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions);
+            Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions);
+            Test("300-interview.mp4", ExtraType.Interview, videoOptions);
+            Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions);
+        }
+
+        [Fact]
+        public void TestSample()
+        {
+            var videoOptions = new NamingOptions();
+
+            Test("300-sample.mp4", ExtraType.Sample, videoOptions);
+        }
+
+        private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
+        {
+            var parser = GetExtraTypeParser(videoOptions);
+
+            var extraType = parser.GetExtraInfo(input).ExtraType;
+
+            if (expectedType == null)
+            {
+                Assert.Null(extraType);
+            }
+            else
+            {
+                Assert.Equal(expectedType, extraType);
+            }
+        }
+
+        private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
+        {
+            return new ExtraResolver(videoOptions);
+        }
+    }
+}

+ 78 - 0
tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs

@@ -0,0 +1,78 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class Format3DTests : BaseVideoTest
+    {
+        [Fact]
+        public void TestKodiFormat3D()
+        {
+            var options = new NamingOptions();
+
+            Test("Super movie.3d.mp4", false, null, options);
+            Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
+            Test("Super movie.3d.sbs.mp4", true, "sbs", options);
+            Test("Super movie.3d.htab.mp4", true, "htab", options);
+            Test("Super movie.3d.tab.mp4", true, "tab", options);
+            Test("Super movie 3d hsbs.mp4", true, "hsbs", options);
+        }
+
+        [Fact]
+        public void Test3DName()
+        {
+            var result =
+                GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+
+            Assert.Equal("hsbs", result.Format3D);
+            Assert.Equal("Oblivion", result.Name);
+        }
+
+        [Fact]
+        public void TestExpandedFormat3D()
+        {
+            // These were introduced for Media Browser 3
+            // Kodi conventions are preferred but these still need to be supported
+            var options = new NamingOptions();
+
+            Test("Super movie.3d.mp4", false, null, options);
+            Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
+            Test("Super movie.3d.sbs.mp4", true, "sbs", options);
+            Test("Super movie.3d.htab.mp4", true, "htab", options);
+            Test("Super movie.3d.tab.mp4", true, "tab", options);
+
+            Test("Super movie.hsbs.mp4", true, "hsbs", options);
+            Test("Super movie.sbs.mp4", true, "sbs", options);
+            Test("Super movie.htab.mp4", true, "htab", options);
+            Test("Super movie.tab.mp4", true, "tab", options);
+            Test("Super movie.sbs3d.mp4", true, "sbs3d", options);
+            Test("Super movie.3d.mvc.mp4", true, "mvc", options);
+
+            Test("Super movie [3d].mp4", false, null, options);
+            Test("Super movie [hsbs].mp4", true, "hsbs", options);
+            Test("Super movie [fsbs].mp4", true, "fsbs", options);
+            Test("Super movie [ftab].mp4", true, "ftab", options);
+            Test("Super movie [htab].mp4", true, "htab", options);
+            Test("Super movie [sbs3d].mp4", true, "sbs3d", options);
+        }
+
+        private void Test(string input, bool is3D, string format3D, NamingOptions options)
+        {
+            var parser = new Format3DParser(options);
+
+            var result = parser.Parse(input);
+
+            Assert.Equal(is3D, result.Is3D);
+
+            if (format3D == null)
+            {
+                Assert.Null(result.Format3D);
+            }
+            else
+            {
+                Assert.Equal(format3D, result.Format3D, true);
+            }
+        }
+    }
+}

+ 438 - 0
tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs

@@ -0,0 +1,438 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class MultiVersionTests
+    {
+        // FIXME
+        // [Fact]
+        public void TestMultiEdition1()
+        {
+            var files = new[]
+            {
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Single(result[0].Extras);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiEdition2()
+        {
+            var files = new[]
+            {
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
+                @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Single(result[0].Extras);
+            Assert.Equal(2, result[0].AlternateVersions.Count);
+        }
+
+        [Fact]
+        public void TestMultiEdition3()
+        {
+            // This is currently not supported and will fail, but we should try to figure it out
+            var files = new[]
+            {
+                @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
+                @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Single(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestLetterFolders()
+        {
+            var files = new[]
+            {
+                @"/movies/M/Movie 1.mkv",
+                @"/movies/M/Movie 2.mkv",
+                @"/movies/M/Movie 3.mkv",
+                @"/movies/M/Movie 4.mkv",
+                @"/movies/M/Movie 5.mkv",
+                @"/movies/M/Movie 6.mkv",
+                @"/movies/M/Movie 7.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(7, result.Count);
+            Assert.Empty(result[0].Extras);
+            Assert.Empty(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersionLimit()
+        {
+            var files = new[]
+            {
+                @"/movies/Movie/Movie.mkv",
+                @"/movies/Movie/Movie-2.mkv",
+                @"/movies/Movie/Movie-3.mkv",
+                @"/movies/Movie/Movie-4.mkv",
+                @"/movies/Movie/Movie-5.mkv",
+                @"/movies/Movie/Movie-6.mkv",
+                @"/movies/Movie/Movie-7.mkv",
+                @"/movies/Movie/Movie-8.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Equal(7, result[0].AlternateVersions.Count);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersionLimit2()
+        {
+            var files = new[]
+            {
+                @"/movies/Mo/Movie 1.mkv",
+                @"/movies/Mo/Movie 2.mkv",
+                @"/movies/Mo/Movie 3.mkv",
+                @"/movies/Mo/Movie 4.mkv",
+                @"/movies/Mo/Movie 5.mkv",
+                @"/movies/Mo/Movie 6.mkv",
+                @"/movies/Mo/Movie 7.mkv",
+                @"/movies/Mo/Movie 8.mkv",
+                @"/movies/Mo/Movie 9.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(9, result.Count);
+            Assert.Empty(result[0].Extras);
+            Assert.Empty(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion3()
+        {
+            var files = new[]
+            {
+                @"/movies/Movie/Movie 1.mkv",
+                @"/movies/Movie/Movie 2.mkv",
+                @"/movies/Movie/Movie 3.mkv",
+                @"/movies/Movie/Movie 4.mkv",
+                @"/movies/Movie/Movie 5.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(5, result.Count);
+            Assert.Empty(result[0].Extras);
+            Assert.Empty(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion4()
+        {
+            // Test for false positive
+
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man.mkv",
+                @"/movies/Iron Man/Iron Man (2008).mkv",
+                @"/movies/Iron Man/Iron Man (2009).mkv",
+                @"/movies/Iron Man/Iron Man (2010).mkv",
+                @"/movies/Iron Man/Iron Man (2011).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(5, result.Count);
+            Assert.Empty(result[0].Extras);
+            Assert.Empty(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion5()
+        {
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man.mkv",
+                @"/movies/Iron Man/Iron Man-720p.mkv",
+                @"/movies/Iron Man/Iron Man-test.mkv",
+                @"/movies/Iron Man/Iron Man-bluray.mkv",
+                @"/movies/Iron Man/Iron Man-3d.mkv",
+                @"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
+                @"/movies/Iron Man/Iron Man-3d.hsbs.mkv",
+                @"/movies/Iron Man/Iron Man[test].mkv",
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Equal(7, result[0].AlternateVersions.Count);
+            Assert.False(result[0].AlternateVersions[2].Is3D);
+            Assert.True(result[0].AlternateVersions[3].Is3D);
+            Assert.True(result[0].AlternateVersions[4].Is3D);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion6()
+        {
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man.mkv",
+                @"/movies/Iron Man/Iron Man - 720p.mkv",
+                @"/movies/Iron Man/Iron Man - test.mkv",
+                @"/movies/Iron Man/Iron Man - bluray.mkv",
+                @"/movies/Iron Man/Iron Man - 3d.mkv",
+                @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
+                @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv",
+                @"/movies/Iron Man/Iron Man [test].mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Equal(7, result[0].AlternateVersions.Count);
+            Assert.False(result[0].AlternateVersions[3].Is3D);
+            Assert.True(result[0].AlternateVersions[4].Is3D);
+            Assert.True(result[0].AlternateVersions[5].Is3D);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion7()
+        {
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man - B (2006).mkv",
+                @"/movies/Iron Man/Iron Man - C (2007).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(2, result.Count);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion8()
+        {
+            // This is not actually supported yet
+
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man.mkv",
+                @"/movies/Iron Man/Iron Man_720p.mkv",
+                @"/movies/Iron Man/Iron Man_test.mkv",
+                @"/movies/Iron Man/Iron Man_bluray.mkv",
+                @"/movies/Iron Man/Iron Man_3d.mkv",
+                @"/movies/Iron Man/Iron Man_3d-hsbs.mkv",
+                @"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Equal(6, result[0].AlternateVersions.Count);
+            Assert.False(result[0].AlternateVersions[2].Is3D);
+            Assert.True(result[0].AlternateVersions[3].Is3D);
+            Assert.True(result[0].AlternateVersions[4].Is3D);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion9()
+        {
+            // Test for false positive
+
+            var files = new[]
+            {
+                @"/movies/Iron Man/Iron Man (2007).mkv",
+                @"/movies/Iron Man/Iron Man (2008).mkv",
+                @"/movies/Iron Man/Iron Man (2009).mkv",
+                @"/movies/Iron Man/Iron Man (2010).mkv",
+                @"/movies/Iron Man/Iron Man (2011).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(5, result.Count);
+            Assert.Empty(result[0].Extras);
+            Assert.Empty(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion10()
+        {
+            var files = new[]
+            {
+                @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
+                @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Single(result[0].AlternateVersions);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestMultiVersion11()
+        {
+            // Currently not supported but we should probably handle this.
+
+            var files = new[]
+            {
+                @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
+                @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+            Assert.Empty(result[0].Extras);
+            Assert.Single(result[0].AlternateVersions);
+        }
+
+        private VideoListResolver GetResolver()
+        {
+            var options = new NamingOptions();
+            return new VideoListResolver(options);
+        }
+    }
+}

+ 478 - 0
tests/Jellyfin.Naming.Tests/Video/StackTests.cs

@@ -0,0 +1,478 @@
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class StackTests : BaseVideoTest
+    {
+        [Fact]
+        public void TestSimpleStack()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006) part1.mkv",
+                "Bad Boys (2006) part2.mkv",
+                "Bad Boys (2006) part3.mkv",
+                "Bad Boys (2006) part4.mkv",
+                "Bad Boys (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "Bad Boys (2006)", 4);
+        }
+
+        [Fact]
+        public void TestFalsePositives()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006).mkv",
+                "Bad Boys (2007).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestFalsePositives2()
+        {
+            var files = new[]
+            {
+                "Bad Boys 2006.mkv",
+                "Bad Boys 2007.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestFalsePositives3()
+        {
+            var files = new[]
+            {
+                "300 (2006).mkv",
+                "300 (2007).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestFalsePositives4()
+        {
+            var files = new[]
+            {
+                "300 2006.mkv",
+                "300 2007.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestFalsePositives5()
+        {
+            var files = new[]
+            {
+                "Star Trek 1 - The motion picture.mkv",
+                "Star Trek 2- The wrath of khan.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestFalsePositives6()
+        {
+            var files = new[]
+            {
+                "Red Riding in the Year of Our Lord 1983 (2009).mkv",
+                "Red Riding in the Year of Our Lord 1980 (2009).mkv",
+                "Red Riding in the Year of Our Lord 1974 (2009).mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestStackName()
+        {
+            var files = new[]
+            {
+                "d:/movies/300 2006 part1.mkv",
+                "d:/movies/300 2006 part2.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "300 2006", 2);
+        }
+
+        [Fact]
+        public void TestDirtyNames()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006).part1.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+                "Bad Boys (2006).part2.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+                "Bad Boys (2006).part3.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+                "Bad Boys (2006).part4.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
+                "Bad Boys (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
+        }
+
+        [Fact]
+        public void TestNumberedFiles()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006).mkv",
+                "Bad Boys (2006) 1.mkv",
+                "Bad Boys (2006) 2.mkv",
+                "Bad Boys (2006) 3.mkv",
+                "Bad Boys (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestSimpleStackWithNumericName()
+        {
+            var files = new[]
+            {
+                "300 (2006) part1.mkv",
+                "300 (2006) part2.mkv",
+                "300 (2006) part3.mkv",
+                "300 (2006) part4.mkv",
+                "300 (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+        }
+
+        [Fact]
+        public void TestMixedExpressionsNotAllowed()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006) part1.mkv",
+                "Bad Boys (2006) part2.mkv",
+                "Bad Boys (2006) part3.mkv",
+                "Bad Boys (2006) parta.mkv",
+                "Bad Boys (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "Bad Boys (2006)", 3);
+        }
+
+        [Fact]
+        public void TestDualStacks()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006) part1.mkv",
+                "Bad Boys (2006) part2.mkv",
+                "Bad Boys (2006) part3.mkv",
+                "Bad Boys (2006) part4.mkv",
+                "Bad Boys (2006)-trailer.mkv",
+                "300 (2006) part1.mkv",
+                "300 (2006) part2.mkv",
+                "300 (2006) part3.mkv",
+                "300 (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Equal(2, result.Stacks.Count);
+            TestStackInfo(result.Stacks[1], "Bad Boys (2006)", 4);
+            TestStackInfo(result.Stacks[0], "300 (2006)", 3);
+        }
+
+        [Fact]
+        public void TestDirectories()
+        {
+            var files = new[]
+            {
+                "blah blah - cd 1",
+                "blah blah - cd 2"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveDirectories(files);
+
+            Assert.Single(result.Stacks);
+            TestStackInfo(result.Stacks[0], "blah blah", 2);
+        }
+
+        [Fact]
+        public void TestFalsePositive()
+        {
+            var files = new[]
+            {
+                "300a.mkv",
+                "300b.mkv",
+                "300c.mkv",
+                "300-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+
+            TestStackInfo(result.Stacks[0], "300", 3);
+        }
+
+        [Fact]
+        public void TestFailSequence()
+        {
+            var files = new[]
+            {
+                "300 part1.mkv",
+                "300 part2.mkv",
+                "Avatar",
+                "Avengers part1.mkv",
+                "Avengers part2.mkv",
+                "Avengers part3.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Equal(2, result.Stacks.Count);
+
+            TestStackInfo(result.Stacks[0], "300", 2);
+            TestStackInfo(result.Stacks[1], "Avengers", 3);
+        }
+
+        [Fact]
+        public void TestMixedExpressions()
+        {
+            var files = new[]
+            {
+                "Bad Boys (2006) part1.mkv",
+                "Bad Boys (2006) part2.mkv",
+                "Bad Boys (2006) part3.mkv",
+                "Bad Boys (2006) part4.mkv",
+                "Bad Boys (2006)-trailer.mkv",
+                "300 (2006) parta.mkv",
+                "300 (2006) partb.mkv",
+                "300 (2006) partc.mkv",
+                "300 (2006) partd.mkv",
+                "300 (2006)-trailer.mkv",
+                "300a.mkv",
+                "300b.mkv",
+                "300c.mkv",
+                "300-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Equal(3, result.Stacks.Count);
+
+            TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+            TestStackInfo(result.Stacks[1], "300", 3);
+            TestStackInfo(result.Stacks[2], "Bad Boys (2006)", 4);
+        }
+
+        [Fact]
+        public void TestAlphaLimitOfFour()
+        {
+            var files = new[]
+            {
+                "300 (2006) parta.mkv",
+                "300 (2006) partb.mkv",
+                "300 (2006) partc.mkv",
+                "300 (2006) partd.mkv",
+                "300 (2006) parte.mkv",
+                "300 (2006) partf.mkv",
+                "300 (2006) partg.mkv",
+                "300 (2006)-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+
+            TestStackInfo(result.Stacks[0], "300 (2006)", 4);
+        }
+
+        [Fact]
+        public void TestMixed()
+        {
+            var files = new[]
+            {
+                new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false},
+                new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false},
+                new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true},
+                new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true},
+                new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true}
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files);
+
+            Assert.Equal(2, result.Stacks.Count);
+            TestStackInfo(result.Stacks[0], "300 (2006)", 3);
+            TestStackInfo(result.Stacks[1], "Bad Boys (2006)", 2);
+        }
+
+        [Fact]
+        public void TestDirectories2()
+        {
+            //TestDirectory(@"blah blah", false, @"blah blah");
+            //TestDirectory(@"d:/music/weezer/03 Pinkerton", false, "03 Pinkerton");
+            //TestDirectory(@"d:/music/michael jackson/Bad (2012 Remaster)", false, "Bad (2012 Remaster)");
+
+            //TestDirectory(@"blah blah - cd1", true, "blah blah");
+            //TestDirectory(@"blah blah - disc1", true, "blah blah");
+            //TestDirectory(@"blah blah - disk1", true, "blah blah");
+            //TestDirectory(@"blah blah - pt1", true, "blah blah");
+            //TestDirectory(@"blah blah - part1", true, "blah blah");
+            //TestDirectory(@"blah blah - dvd1", true, "blah blah");
+
+            //// Add a space
+            //TestDirectory(@"blah blah - cd 1", true, "blah blah");
+            //TestDirectory(@"blah blah - disc 1", true, "blah blah");
+            //TestDirectory(@"blah blah - disk 1", true, "blah blah");
+            //TestDirectory(@"blah blah - pt 1", true, "blah blah");
+            //TestDirectory(@"blah blah - part 1", true, "blah blah");
+            //TestDirectory(@"blah blah - dvd 1", true, "blah blah");
+
+            //// Not case sensitive
+            //TestDirectory(@"blah blah - Disc1", true, "blah blah");
+        }
+
+        [Fact]
+        public void TestNamesWithoutParts()
+        {
+            // No stacking here because there is no part/disc/etc
+            var files = new[]
+            {
+                "Harry Potter and the Deathly Hallows.mkv",
+                "Harry Potter and the Deathly Hallows 1.mkv",
+                "Harry Potter and the Deathly Hallows 2.mkv",
+                "Harry Potter and the Deathly Hallows 3.mkv",
+                "Harry Potter and the Deathly Hallows 4.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Empty(result.Stacks);
+        }
+
+        [Fact]
+        public void TestNumbersAppearingBeforePartNumber()
+        {
+            // No stacking here because there is no part/disc/etc
+            var files = new[]
+            {
+                "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part1.mkv",
+                "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveFiles(files);
+
+            Assert.Single(result.Stacks);
+            Assert.Equal(2, result.Stacks[0].Files.Count);
+        }
+
+        [Fact]
+        public void TestMultiDiscs()
+        {
+            // No stacking here because there is no part/disc/etc
+            var files = new[]
+            {
+                @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
+                @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.ResolveDirectories(files);
+
+            Assert.Single(result.Stacks);
+            Assert.Equal(2, result.Stacks[0].Files.Count);
+        }
+
+        private void TestStackInfo(FileStack stack, string name, int fileCount)
+        {
+            Assert.Equal(fileCount, stack.Files.Count);
+            Assert.Equal(name, stack.Name);
+        }
+
+        private StackResolver GetResolver()
+        {
+            return new StackResolver(new NamingOptions());
+        }
+    }
+}

+ 55 - 0
tests/Jellyfin.Naming.Tests/Video/StubTests.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Globalization;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class StubTests : BaseVideoTest
+    {
+        [Fact]
+        public void TestStubs()
+        {
+            Test("video.mkv", false, null);
+            Test("video.disc", true, null);
+            Test("video.dvd.disc", true, "dvd");
+            Test("video.hddvd.disc", true, "hddvd");
+            Test("video.bluray.disc", true, "bluray");
+            Test("video.brrip.disc", true, "bluray");
+            Test("video.bd25.disc", true, "bluray");
+            Test("video.bd50.disc", true, "bluray");
+            Test("video.vhs.disc", true, "vhs");
+            Test("video.hdtv.disc", true, "tv");
+            Test("video.pdtv.disc", true, "tv");
+            Test("video.dsr.disc", true, "tv");
+        }
+
+        [Fact]
+        public void TestStubName()
+        {
+            var result =
+                GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+
+            Assert.Equal("Oblivion", result.Name);
+        }
+
+        private void Test(string path, bool isStub, string stubType)
+        {
+            var options = new NamingOptions();
+
+            var resultStubType = StubResolver.ResolveFile(path, options);
+
+            Assert.Equal(isStub, resultStubType.IsStub);
+
+            if (stubType == null)
+            {
+                Assert.Null(resultStubType.StubType);
+            }
+            else
+            {
+                Assert.Equal(stubType, resultStubType.StubType, true);
+            }
+        }
+    }
+}

+ 457 - 0
tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs

@@ -0,0 +1,457 @@
+using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class VideoListResolverTests
+    {
+        // FIXME
+        // [Fact]
+        public void TestStackAndExtras()
+        {
+            // No stacking here because there is no part/disc/etc
+            var files = new[]
+            {
+                "Harry Potter and the Deathly Hallows-trailer.mkv",
+                "Harry Potter and the Deathly Hallows.trailer.mkv",
+                "Harry Potter and the Deathly Hallows part1.mkv",
+                "Harry Potter and the Deathly Hallows part2.mkv",
+                "Harry Potter and the Deathly Hallows part3.mkv",
+                "Harry Potter and the Deathly Hallows part4.mkv",
+                "Batman-deleted.mkv",
+                "Batman-sample.mkv",
+                "Batman-trailer.mkv",
+                "Batman part1.mkv",
+                "Batman part2.mkv",
+                "Batman part3.mkv",
+                "Avengers.mkv",
+                "Avengers-trailer.mkv",
+
+                // Despite having a keyword in the name that will return an ExtraType, there's no original video to match it to
+                // So this is just a standalone video
+                "trailer.mkv",
+
+                // Same as above
+                "WillyWonka-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(5, result.Count);
+
+            Assert.Equal(3, result[1].Files.Count);
+            Assert.Equal(3, result[1].Extras.Count);
+            Assert.Equal("Batman", result[1].Name);
+
+            Assert.Equal(4, result[2].Files.Count);
+            Assert.Equal(2, result[2].Extras.Count);
+            Assert.Equal("Harry Potter and the Deathly Hallows", result[2].Name);
+        }
+
+        [Fact]
+        public void TestWithMetadata()
+        {
+            var files = new[]
+            {
+                "300.mkv",
+                "300.nfo"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestWithExtra()
+        {
+            var files = new[]
+            {
+                "300.mkv",
+                "300 trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestVariationWithFolderName()
+        {
+            var files = new[]
+            {
+                "X-Men Days of Future Past - 1080p.mkv",
+                "X-Men Days of Future Past-trailer.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestTrailer2()
+        {
+            var files = new[]
+            {
+                "X-Men Days of Future Past - 1080p.mkv",
+                "X-Men Days of Future Past-trailer.mp4",
+                "X-Men Days of Future Past-trailer2.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestDifferentNames()
+        {
+            var files = new[]
+            {
+                "Looper (2012)-trailer.mkv",
+                "Looper.2012.bluray.720p.x264.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestSeparateFiles()
+        {
+            // These should be considered separate, unrelated videos
+            var files = new[]
+            {
+                "My video 1.mkv",
+                "My video 2.mkv",
+                "My video 3.mkv",
+                "My video 4.mkv",
+                "My video 5.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(5, result.Count);
+        }
+
+        [Fact]
+        public void TestMultiDisc()
+        {
+            var files = new[]
+            {
+                @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
+                @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = true,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestPoundSign()
+        {
+            // These should be considered separate, unrelated videos
+            var files = new[]
+            {
+                @"My movie #1.mp4",
+                @"My movie #2.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = true,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(2, result.Count);
+        }
+
+        [Fact]
+        public void TestStackedWithTrailer()
+        {
+            var files = new[]
+            {
+                @"No (2012) part1.mp4",
+                @"No (2012) part2.mp4",
+                @"No (2012) part1-trailer.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestStackedWithTrailer2()
+        {
+            var files = new[]
+            {
+                @"No (2012) part1.mp4",
+                @"No (2012) part2.mp4",
+                @"No (2012)-trailer.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestExtrasByFolderName()
+        {
+            var files = new[]
+            {
+                @"/Movies/Top Gun (1984)/movie.mp4",
+                @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
+                @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
+                @"trailer.mp4"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestDoubleTags()
+        {
+            var files = new[]
+            {
+                @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
+                @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
+                @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
+                @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(2, result.Count);
+        }
+
+        [Fact]
+        public void TestArgumentOutOfRangeException()
+        {
+            var files = new[]
+            {
+                @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestColony()
+        {
+            var files = new[]
+            {
+                @"The Colony.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestFourSisters()
+        {
+            var files = new[]
+            {
+                @"Four Sisters and a Wedding - A.avi",
+                @"Four Sisters and a Wedding - B.avi"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestMovieTrailer()
+        {
+            var files = new[]
+            {
+                @"/Server/Despicable Me/Despicable Me (2010).mkv",
+                @"/Server/Despicable Me/movie-trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestTrailerFalsePositives()
+        {
+            var files = new[]
+            {
+                @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv",
+                @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv",
+                @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv",
+                @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Equal(4, result.Count);
+        }
+
+        [Fact]
+        public void TestSubfolders()
+        {
+            var files = new[]
+            {
+                @"/Movies/Despicable Me/Despicable Me.mkv",
+                @"/Movies/Despicable Me/trailers/trailer.mkv"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+
+            }).ToList()).ToList();
+
+            Assert.Single(result);
+        }
+
+        private VideoListResolver GetResolver()
+        {
+            var options = new NamingOptions();
+            return new VideoListResolver(options);
+        }
+    }
+}

+ 275 - 0
tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs

@@ -0,0 +1,275 @@
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Video
+{
+    public class VideoResolverTests : BaseVideoTest
+    {
+        // FIXME
+        // [Fact]
+        public void TestSimpleFile()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("Brave", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestSimpleFile2()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv");
+
+            Assert.Equal(1995, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("Bad Boys", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestSimpleFileWithNumericName()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("300", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestExtra()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal(ExtraType.Trailer, result.ExtraType);
+            Assert.Equal("Brave (2006)-trailer", result.Name);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestExtraWithNumericName()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("300 (2006)-trailer", result.Name);
+            Assert.Equal(ExtraType.Trailer, result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestStubFileWithNumericName()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).bluray.disc");
+
+            Assert.Equal(2006, result.Year);
+            Assert.True(result.IsStub);
+            Assert.Equal("bluray", result.StubType);
+            Assert.False(result.Is3D);
+            Assert.Equal("300", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestStubFile()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).bluray.disc");
+
+            Assert.Equal(2006, result.Year);
+            Assert.True(result.IsStub);
+            Assert.Equal("bluray", result.StubType);
+            Assert.False(result.Is3D);
+            Assert.Equal("Brave", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestExtraStubWithNumericNameNotSupported()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc");
+
+            Assert.Equal(2006, result.Year);
+            Assert.True(result.IsStub);
+            Assert.Equal("bluray", result.StubType);
+            Assert.False(result.Is3D);
+            Assert.Equal("300", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestExtraStubNotSupported()
+        {
+            // Using a stub for an extra is currently not supported
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc");
+
+            Assert.Equal(2006, result.Year);
+            Assert.True(result.IsStub);
+            Assert.Equal("bluray", result.StubType);
+            Assert.False(result.Is3D);
+            Assert.Equal("brave", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void Test3DFileWithNumericName()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.True(result.Is3D);
+            Assert.Equal("sbs", result.Format3D);
+            Assert.Equal("300", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestBad3DFileWithNumericName()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("300", result.Name);
+            Assert.Null(result.ExtraType);
+            Assert.Null(result.Format3D);
+        }
+
+        // FIXME
+        // [Fact]
+        public void Test3DFile()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv");
+
+            Assert.Equal(2006, result.Year);
+            Assert.False(result.IsStub);
+            Assert.True(result.Is3D);
+            Assert.Equal("sbs", result.Format3D);
+            Assert.Equal("brave", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        [Fact]
+        public void TestNameWithoutDate()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/American Psycho/American.Psycho.mkv");
+
+            Assert.Null(result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Null(result.Format3D);
+            Assert.Equal("American.Psycho", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateAndStringsSequence()
+        {
+            var parser = GetParser();
+
+            // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+            var result =
+                parser.ResolveFile(@"/server/Movies/3.Days.to.Kill/3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv");
+
+            Assert.Equal(2014, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Null(result.Format3D);
+            Assert.Equal("3.Days.to.Kill", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        // FIXME
+        // [Fact]
+        public void TestCleanDateAndStringsSequence1()
+        {
+            var parser = GetParser();
+
+            // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+            var result =
+                parser.ResolveFile(@"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv");
+
+            Assert.Equal(2005, result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Null(result.Format3D);
+            Assert.Equal("3 days to kill", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+
+        [Fact]
+        public void TestFolderNameWithExtension()
+        {
+            var parser = GetParser();
+
+            var result =
+                parser.ResolveFile(@"/server/Movies/7 Psychos.mkv/7 Psychos.mkv");
+
+            Assert.Null(result.Year);
+            Assert.False(result.IsStub);
+            Assert.False(result.Is3D);
+            Assert.Equal("7 Psychos", result.Name);
+            Assert.Null(result.ExtraType);
+        }
+    }
+}