mirror of
https://github.com/Radarr/Radarr
synced 2026-05-07 14:01:25 +02:00
Fixed: Bulk import grouped nested movie folders
This commit is contained in:
parent
4b85fab05b
commit
3679f7facc
2 changed files with 305 additions and 20 deletions
|
|
@ -51,6 +51,17 @@ private void WithNonExistingFolder()
|
|||
.Returns(false);
|
||||
}
|
||||
|
||||
private void GivenRootFolder(RootFolder rootFolder)
|
||||
{
|
||||
Mocker.GetMock<IRootFolderRepository>()
|
||||
.Setup(s => s.Get(It.IsAny<int>()))
|
||||
.Returns(rootFolder);
|
||||
|
||||
Mocker.GetMock<IMovieRepository>()
|
||||
.Setup(s => s.AllMoviePaths())
|
||||
.Returns(new Dictionary<int, string>());
|
||||
}
|
||||
|
||||
[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<IRootFolderRepository>()
|
||||
.Setup(s => s.Get(It.IsAny<int>()))
|
||||
.Returns(rootFolder);
|
||||
|
||||
Mocker.GetMock<IMovieRepository>()
|
||||
.Setup(s => s.AllMoviePaths())
|
||||
.Returns(new Dictionary<int, string>());
|
||||
GivenRootFolder(rootFolder);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.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<RootFolder>.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<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(folders);
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(folder))
|
||||
.Returns(Array.Empty<string>());
|
||||
}
|
||||
|
||||
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<RootFolder>.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<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(new[] { groupingFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(groupingFolder))
|
||||
.Returns(new[] { movieFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(movieFolder))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetFiles(groupingFolder, false))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.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<RootFolder>.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<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(new[] { groupingFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(groupingFolder))
|
||||
.Returns(new[] { movieFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(movieFolder))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetFiles(groupingFolder, false))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.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<RootFolder>.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<IRootFolderRepository>()
|
||||
.Setup(s => s.Get(It.IsAny<int>()))
|
||||
.Returns(rootFolder);
|
||||
|
||||
Mocker.GetMock<IMovieRepository>()
|
||||
.Setup(s => s.AllMoviePaths())
|
||||
.Returns(new Dictionary<int, string> { { 1, movieFolder } });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(new[] { groupingFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(groupingFolder))
|
||||
.Returns(new[] { movieFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(movieFolder))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetFiles(groupingFolder, false))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.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<RootFolder>.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<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(new[] { movieFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(movieFolder))
|
||||
.Returns(new[] { discFolder });
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(discFolder))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetFiles(movieFolder, false))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetFiles(discFolder, false))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders;
|
||||
|
||||
unmappedFolders.Should().HaveCount(1);
|
||||
unmappedFolders[0].Name.Should().Be("Movie 1 (2001)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UnmappedFolder> GetUnmappedFolders(string path, Dictionary<int, str
|
|||
return results;
|
||||
}
|
||||
|
||||
var subFolderDepth = _namingConfigService.GetConfig().MovieFolderFormat.Count(f => 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<UnmappedFolder> GetUnmappedFolders(string path, Dictionary<int, str
|
|||
return results.OrderBy(u => u.Name, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
private List<string> GetPossibleMovieFolders(string path)
|
||||
{
|
||||
var subFolderDepth = _namingConfigService.GetConfig().MovieFolderFormat.Count(f => f == Path.DirectorySeparatorChar);
|
||||
var possibleMovieFolders = new List<string>();
|
||||
|
||||
FindPossibleMovieFolders(path, 0, subFolderDepth, possibleMovieFolders);
|
||||
|
||||
return possibleMovieFolders;
|
||||
}
|
||||
|
||||
private void FindPossibleMovieFolders(string path, int currentDepth, int expectedMovieFolderDepth, List<string> possibleMovieFolders)
|
||||
{
|
||||
var directories = (_diskProvider.GetDirectories(path) ?? Enumerable.Empty<string>()).ToList();
|
||||
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
var childDirectories = (_diskProvider.GetDirectories(directory) ?? Enumerable.Empty<string>()).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<string> childDirectories)
|
||||
{
|
||||
if (HasVideoFiles(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!childDirectories.Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return MovieFolderYearRegex.IsMatch(new DirectoryInfo(path).Name);
|
||||
}
|
||||
|
||||
private bool ShouldRecurseIntoGroupingFolder(string path, List<string> 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<string>())
|
||||
.Any(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file)));
|
||||
}
|
||||
|
||||
public RootFolder Get(int id, bool timeout)
|
||||
{
|
||||
var rootFolder = _rootFolderRepository.Get(id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue