diff --git a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs index c7b85c2040..deb7df4c06 100644 --- a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs +++ b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs @@ -51,6 +51,17 @@ private void WithNonExistingFolder() .Returns(false); } + private void GivenRootFolder(RootFolder rootFolder) + { + Mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(rootFolder); + + Mocker.GetMock() + .Setup(s => s.AllMoviePaths()) + .Returns(new Dictionary()); + } + [TestCase("D:\\TV Shows\\")] [TestCase("//server//folder")] public void should_be_able_to_add_root_dir(string path) @@ -278,20 +289,14 @@ public void should_get_unmapped_folders_inside_letter_subfolder() var subFolders = new[] { - "Movie1", - "Movie2", - "Movie3", + "Movie 1 (2001)", + "Movie 2 (2002)", + "Movie 3 (2003)", }; var folders = subFolders.Select(f => Path.Combine(subFolderPath, f)).ToArray(); - Mocker.GetMock() - .Setup(s => s.Get(It.IsAny())) - .Returns(rootFolder); - - Mocker.GetMock() - .Setup(s => s.AllMoviePaths()) - .Returns(new Dictionary()); + GivenRootFolder(rootFolder); Mocker.GetMock() .Setup(s => s.GetDirectories(rootFolder.Path)) @@ -305,5 +310,205 @@ public void should_get_unmapped_folders_inside_letter_subfolder() unmappedFolders.Count.Should().Be(3); } + + [Test] + public void should_get_top_level_movie_folders() + { + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var movieFolders = new[] + { + "Movie 1 (2001)", + "Movie 2 (2002)", + "Movie 3 (2003)", + }; + + var folders = movieFolders.Select(f => Path.Combine(rootFolderPath, f)).ToArray(); + + GivenRootFolder(rootFolder); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(folders); + + foreach (var folder in folders) + { + Mocker.GetMock() + .Setup(s => s.GetDirectories(folder)) + .Returns(Array.Empty()); + } + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Select(f => f.Name).Should().BeEquivalentTo(movieFolders); + } + + [Test] + public void should_get_nested_movie_folder_when_parent_is_only_a_grouping_folder() + { + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var groupingFolder = Path.Combine(rootFolderPath, "L"); + var movieFolder = Path.Combine(groupingFolder, "Ladder 49 (2004)"); + var movieFile = Path.Combine(movieFolder, "Ladder 49 (2004) [Bluray-1080p].mkv").AsOsAgnostic(); + + GivenRootFolder(rootFolder); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(new[] { groupingFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(groupingFolder)) + .Returns(new[] { movieFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(movieFolder)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(groupingFolder, false)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(movieFolder, false)) + .Returns(new[] { movieFile }); + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Should().HaveCount(1); + unmappedFolders[0].Name.Should().Be("Ladder 49 (2004)"); + unmappedFolders[0].Path.Should().Be(movieFolder); + unmappedFolders[0].RelativePath.Should().Be(Path.Combine("L", "Ladder 49 (2004)").AsOsAgnostic()); + } + + [Test] + public void should_not_return_grouping_folder_when_only_child_folder_contains_video_file() + { + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var groupingFolder = Path.Combine(rootFolderPath, "L"); + var movieFolder = Path.Combine(groupingFolder, "Ladder 49 (2004)"); + var movieFile = Path.Combine(movieFolder, "Ladder 49 (2004).mkv").AsOsAgnostic(); + + GivenRootFolder(rootFolder); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(new[] { groupingFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(groupingFolder)) + .Returns(new[] { movieFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(movieFolder)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(groupingFolder, false)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(movieFolder, false)) + .Returns(new[] { movieFile }); + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Should().NotContain(u => u.Name == "L"); + } + + [Test] + public void should_not_return_already_mapped_nested_movie_folder() + { + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var groupingFolder = Path.Combine(rootFolderPath, "L"); + var movieFolder = Path.Combine(groupingFolder, "Ladder 49 (2004)"); + var movieFile = Path.Combine(movieFolder, "Ladder 49 (2004).mkv").AsOsAgnostic(); + + Mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(rootFolder); + + Mocker.GetMock() + .Setup(s => s.AllMoviePaths()) + .Returns(new Dictionary { { 1, movieFolder } }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(new[] { groupingFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(groupingFolder)) + .Returns(new[] { movieFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(movieFolder)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(groupingFolder, false)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(movieFolder, false)) + .Returns(new[] { movieFile }); + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Should().BeEmpty(); + } + + [Test] + public void should_not_recurse_into_movie_folder_with_disc_subfolders() + { + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var movieFolder = Path.Combine(rootFolderPath, "Movie 1 (2001)"); + var discFolder = Path.Combine(movieFolder, "BDMV"); + + GivenRootFolder(rootFolder); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(new[] { movieFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(movieFolder)) + .Returns(new[] { discFolder }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(discFolder)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(movieFolder, false)) + .Returns(Array.Empty()); + + Mocker.GetMock() + .Setup(s => s.GetFiles(discFolder, false)) + .Returns(Array.Empty()); + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Should().HaveCount(1); + unmappedFolders[0].Name.Should().Be("Movie 1 (2001)"); + } } } diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 3277a16071..f12b94a157 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using NLog; using NzbDrone.Common; @@ -9,6 +10,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; using NzbDrone.Core.Organizer; @@ -48,6 +50,8 @@ public class RootFolderService : IRootFolderService ".grab" }; + private static readonly Regex MovieFolderYearRegex = new Regex(@"\(\d{4}\)", RegexOptions.Compiled); + public RootFolderService(IRootFolderRepository rootFolderRepository, IDiskProvider diskProvider, IMovieRepository movieRepository, @@ -157,16 +161,7 @@ private List GetUnmappedFolders(string path, Dictionary f == Path.DirectorySeparatorChar); - var possibleMovieFolders = _diskProvider.GetDirectories(path).ToList(); - - if (subFolderDepth > 0) - { - for (var i = 0; i < subFolderDepth; i++) - { - possibleMovieFolders = possibleMovieFolders.SelectMany(_diskProvider.GetDirectories).ToList(); - } - } + var possibleMovieFolders = GetPossibleMovieFolders(path); var unmappedFolders = possibleMovieFolders.Except(moviePaths.Select(s => s.Value), PathEqualityComparer.Instance).ToList(); @@ -197,6 +192,91 @@ private List GetUnmappedFolders(string path, Dictionary u.Name, StringComparer.InvariantCultureIgnoreCase).ToList(); } + private List GetPossibleMovieFolders(string path) + { + var subFolderDepth = _namingConfigService.GetConfig().MovieFolderFormat.Count(f => f == Path.DirectorySeparatorChar); + var possibleMovieFolders = new List(); + + FindPossibleMovieFolders(path, 0, subFolderDepth, possibleMovieFolders); + + return possibleMovieFolders; + } + + private void FindPossibleMovieFolders(string path, int currentDepth, int expectedMovieFolderDepth, List possibleMovieFolders) + { + var directories = (_diskProvider.GetDirectories(path) ?? Enumerable.Empty()).ToList(); + + foreach (var directory in directories) + { + var childDirectories = (_diskProvider.GetDirectories(directory) ?? Enumerable.Empty()).ToList(); + + if (currentDepth < expectedMovieFolderDepth) + { + FindPossibleMovieFolders(directory, currentDepth + 1, expectedMovieFolderDepth, possibleMovieFolders); + continue; + } + + if (ShouldRecurseIntoGroupingFolder(directory, childDirectories)) + { + FindPossibleMovieFolders(directory, currentDepth + 1, expectedMovieFolderDepth, possibleMovieFolders); + continue; + } + + if (IsPossibleMovieFolder(directory, childDirectories)) + { + possibleMovieFolders.Add(directory); + } + } + } + + private bool IsPossibleMovieFolder(string path, List childDirectories) + { + if (HasVideoFiles(path)) + { + return true; + } + + if (!childDirectories.Any()) + { + return true; + } + + return MovieFolderYearRegex.IsMatch(new DirectoryInfo(path).Name); + } + + private bool ShouldRecurseIntoGroupingFolder(string path, List childDirectories) + { + if (HasVideoFiles(path) || !childDirectories.Any()) + { + return false; + } + + var directoryName = new DirectoryInfo(path).Name; + + if (MovieFolderYearRegex.IsMatch(directoryName)) + { + return false; + } + + return childDirectories.All(ContainsMovieFolder); + } + + private bool ContainsMovieFolder(string path) + { + if (HasVideoFiles(path)) + { + return true; + } + + return MovieFolderYearRegex.IsMatch(new DirectoryInfo(path).Name); + } + + private bool HasVideoFiles(string path) + { + return (_diskProvider.GetFiles(path, false) ?? Enumerable.Empty()) + .Any(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))); + } + public RootFolder Get(int id, bool timeout) { var rootFolder = _rootFolderRepository.Get(id);