diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js index 4a78b05c2..7858b6633 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js @@ -10,6 +10,7 @@ import styles from './ImportMovieRow.css'; function ImportMovieRow(props) { const { id, + relativePath, monitor, qualityProfileId, minimumAvailability, @@ -31,7 +32,7 @@ function ImportMovieRow(props) { /> - {id} + {relativePath} @@ -73,6 +74,7 @@ function ImportMovieRow(props) { ImportMovieRow.propTypes = { id: PropTypes.string.isRequired, + relativePath: PropTypes.string.isRequired, monitor: PropTypes.string.isRequired, qualityProfileId: PropTypes.number.isRequired, minimumAvailability: PropTypes.string.isRequired, diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js index c4fd8352d..29fd293e3 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js @@ -30,7 +30,7 @@ class ImportMovieTable extends Component { unmappedFolders.forEach((unmappedFolder) => { const id = unmappedFolder.name; - onMovieLookup(id, unmappedFolder.path); + onMovieLookup(id, unmappedFolder.path, unmappedFolder.relativePath); onSetImportMovieValue({ id, diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTableConnector.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTableConnector.js index 2717336c9..368f0a689 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTableConnector.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTableConnector.js @@ -25,10 +25,11 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onMovieLookup(name, path) { + onMovieLookup(name, path, relativePath) { dispatch(queueLookupMovie({ name, path, + relativePath, term: name })); }, diff --git a/frontend/src/Store/Actions/importMovieActions.js b/frontend/src/Store/Actions/importMovieActions.js index a3aa59a86..4e13cd6d0 100644 --- a/frontend/src/Store/Actions/importMovieActions.js +++ b/frontend/src/Store/Actions/importMovieActions.js @@ -66,6 +66,7 @@ export const actionHandlers = handleThunks({ const { name, path, + relativePath, term, topOfQueue = false } = payload; @@ -75,6 +76,7 @@ export const actionHandlers = handleThunks({ id: name, term, path, + relativePath, isFetching: false, isPopulated: false, error: null diff --git a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs index 7ecfa4a60..c7b85c204 100644 --- a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs +++ b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Movies; +using NzbDrone.Core.Organizer; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -19,9 +20,13 @@ namespace NzbDrone.Core.Test.RootFolderTests [TestFixture] public class RootFolderServiceFixture : CoreTest { + private NamingConfig _namingConfig; + [SetUp] public void Setup() { + _namingConfig = NamingConfig.Default; + Mocker.GetMock() .Setup(m => m.FolderExists(It.IsAny())) .Returns(true); @@ -33,6 +38,10 @@ namespace NzbDrone.Core.Test.RootFolderTests Mocker.GetMock() .Setup(s => s.All()) .Returns(new List()); + + Mocker.GetMock() + .Setup(c => c.GetConfig()) + .Returns(_namingConfig); } private void WithNonExistingFolder() @@ -254,5 +263,47 @@ namespace NzbDrone.Core.Test.RootFolderTests unmappedFolders.Count.Should().Be(3); unmappedFolders.Should().NotContain(u => u.Name == "BIN"); } + + [Test] + public void should_get_unmapped_folders_inside_letter_subfolder() + { + _namingConfig.MovieFolderFormat = "{Movie TitleFirstCharacter}\\{Movie Title}".AsOsAgnostic(); + + var rootFolderPath = @"C:\Test\Movies".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var subFolderPath = Path.Combine(rootFolderPath, "M"); + + var subFolders = new[] + { + "Movie1", + "Movie2", + "Movie3", + }; + + 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()); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(rootFolder.Path)) + .Returns(new[] { subFolderPath }); + + Mocker.GetMock() + .Setup(s => s.GetDirectories(subFolderPath)) + .Returns(folders); + + var unmappedFolders = Subject.Get(rootFolder.Id, false).UnmappedFolders; + + unmappedFolders.Count.Should().Be(3); + } } } diff --git a/src/NzbDrone.Core/RootFolders/RootFolder.cs b/src/NzbDrone.Core/RootFolders/RootFolder.cs index 7e6c5444b..26fc1b3d0 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolder.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolder.cs @@ -6,7 +6,6 @@ namespace NzbDrone.Core.RootFolders public class RootFolder : ModelBase { public string Path { get; set; } - public bool Accessible { get; set; } public long? FreeSpace { get; set; } public long? TotalSpace { get; set; } diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 5d0a0d45f..52da032b7 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Movies; +using NzbDrone.Core.Organizer; namespace NzbDrone.Core.RootFolders { @@ -28,6 +29,7 @@ namespace NzbDrone.Core.RootFolders private readonly IDiskProvider _diskProvider; private readonly IMovieRepository _movieRepository; private readonly IConfigService _configService; + private readonly INamingConfigService _namingConfigService; private readonly Logger _logger; private static readonly HashSet SpecialFolders = new HashSet @@ -47,12 +49,14 @@ namespace NzbDrone.Core.RootFolders IDiskProvider diskProvider, IMovieRepository movieRepository, IConfigService configService, + INamingConfigService namingConfigService, Logger logger) { _rootFolderRepository = rootFolderRepository; _diskProvider = diskProvider; _movieRepository = movieRepository; _configService = configService; + _namingConfigService = namingConfigService; _logger = logger; } @@ -145,7 +149,17 @@ namespace NzbDrone.Core.RootFolders 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 unmappedFolders = possibleMovieFolders.Except(moviePaths.Select(s => s.Value), PathEqualityComparer.Instance).ToList(); var recycleBinPath = _configService.RecycleBin; @@ -158,7 +172,12 @@ namespace NzbDrone.Core.RootFolders { if (string.IsNullOrWhiteSpace(recycleBinPath) || di.FullName.PathNotEquals(recycleBinPath)) { - results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName }); + results.Add(new UnmappedFolder + { + Name = di.Name, + Path = di.FullName, + RelativePath = path.GetRelativePath(di.FullName) + }); } } } diff --git a/src/NzbDrone.Core/RootFolders/UnmappedFolder.cs b/src/NzbDrone.Core/RootFolders/UnmappedFolder.cs index 3a8a919de..095edc7b2 100644 --- a/src/NzbDrone.Core/RootFolders/UnmappedFolder.cs +++ b/src/NzbDrone.Core/RootFolders/UnmappedFolder.cs @@ -1,8 +1,9 @@ -namespace NzbDrone.Core.RootFolders +namespace NzbDrone.Core.RootFolders { public class UnmappedFolder { public string Name { get; set; } public string Path { get; set; } + public string RelativePath { get; set; } } }