From 81d2b18ce1c079c2a9dc3de037c9dceea16733fd Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 4 Feb 2023 15:18:32 -0800 Subject: [PATCH] New: Use Series Folder Format to improve unmapped folders within root folders --- .../ImportSeries/Import/ImportSeriesRow.js | 4 +- .../ImportSeries/Import/ImportSeriesTable.js | 2 +- .../Import/ImportSeriesTableConnector.js | 3 +- .../src/Store/Actions/importSeriesActions.js | 2 + .../RootFolderServiceFixture.cs | 51 +++++++++++++++++++ src/NzbDrone.Core/RootFolders/RootFolder.cs | 1 - .../RootFolders/RootFolderService.cs | 30 +++++++++-- .../RootFolders/UnmappedFolder.cs | 3 +- 8 files changed, 86 insertions(+), 10 deletions(-) diff --git a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesRow.js b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesRow.js index 89a2d8260..7777c768f 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesRow.js +++ b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesRow.js @@ -10,6 +10,7 @@ import styles from './ImportSeriesRow.css'; function ImportSeriesRow(props) { const { id, + relativePath, monitor, qualityProfileId, seasonFolder, @@ -32,7 +33,7 @@ function ImportSeriesRow(props) { /> - {id} + {relativePath} @@ -84,6 +85,7 @@ function ImportSeriesRow(props) { ImportSeriesRow.propTypes = { id: PropTypes.string.isRequired, + relativePath: PropTypes.string.isRequired, monitor: PropTypes.string.isRequired, qualityProfileId: PropTypes.number.isRequired, seriesType: PropTypes.string.isRequired, diff --git a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.js b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.js index 45e23a1b7..aa78925f2 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.js +++ b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.js @@ -32,7 +32,7 @@ class ImportSeriesTable extends Component { unmappedFolders.forEach((unmappedFolder) => { const id = unmappedFolder.name; - onSeriesLookup(id, unmappedFolder.path); + onSeriesLookup(id, unmappedFolder.path, unmappedFolder.relativePath); onSetImportSeriesValue({ id, diff --git a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTableConnector.js b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTableConnector.js index 09dfae097..8a26fbe79 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTableConnector.js +++ b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTableConnector.js @@ -26,10 +26,11 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onSeriesLookup(name, path) { + onSeriesLookup(name, path, relativePath) { dispatch(queueLookupSeries({ name, path, + relativePath, term: name })); }, diff --git a/frontend/src/Store/Actions/importSeriesActions.js b/frontend/src/Store/Actions/importSeriesActions.js index 46cbb5add..75ec76591 100644 --- a/frontend/src/Store/Actions/importSeriesActions.js +++ b/frontend/src/Store/Actions/importSeriesActions.js @@ -67,6 +67,7 @@ export const actionHandlers = handleThunks({ const { name, path, + relativePath, term, topOfQueue = false } = payload; @@ -76,6 +77,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 0e3906652..2e283f31c 100644 --- a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs +++ b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs @@ -7,6 +7,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Core.Organizer; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -17,9 +18,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); @@ -31,6 +36,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() @@ -140,5 +149,47 @@ namespace NzbDrone.Core.Test.RootFolderTests unmappedFolders.Count.Should().BeGreaterThan(0); unmappedFolders.Should().NotContain(u => u.Name == subFolder); } + + [Test] + public void should_get_unmapped_folders_inside_letter_subfolder() + { + _namingConfig.SeriesFolderFormat = "{Series TitleFirstCharacter}\\{Series Title}"; + + var rootFolderPath = @"C:\Test\TV".AsOsAgnostic(); + var rootFolder = Builder.CreateNew() + .With(r => r.Path = rootFolderPath) + .Build(); + + var subFolderPath = Path.Combine(rootFolderPath, "S"); + + var subFolders = new[] + { + "Series1", + "Series2", + "Series3", + }; + + 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.AllSeriesPaths()) + .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 441099da3..23b75631c 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; using NzbDrone.Core.Tv; namespace NzbDrone.Core.RootFolders @@ -26,6 +27,7 @@ namespace NzbDrone.Core.RootFolders private readonly IRootFolderRepository _rootFolderRepository; private readonly IDiskProvider _diskProvider; private readonly ISeriesRepository _seriesRepository; + private readonly INamingConfigService _namingConfigService; private readonly Logger _logger; private static readonly HashSet SpecialFolders = new HashSet @@ -44,11 +46,13 @@ namespace NzbDrone.Core.RootFolders public RootFolderService(IRootFolderRepository rootFolderRepository, IDiskProvider diskProvider, ISeriesRepository seriesRepository, + INamingConfigService namingConfigService, Logger logger) { _rootFolderRepository = rootFolderRepository; _diskProvider = diskProvider; _seriesRepository = seriesRepository; + _namingConfigService = namingConfigService; _logger = logger; } @@ -139,13 +143,28 @@ namespace NzbDrone.Core.RootFolders return results; } + var subFolderDepth = _namingConfigService.GetConfig().SeriesFolderFormat.Count(f => f == Path.DirectorySeparatorChar); var possibleSeriesFolders = _diskProvider.GetDirectories(path).ToList(); + + if (subFolderDepth > 0) + { + for (int i = 0; i < subFolderDepth; i++) + { + possibleSeriesFolders = possibleSeriesFolders.SelectMany(_diskProvider.GetDirectories).ToList(); + } + } + var unmappedFolders = possibleSeriesFolders.Except(seriesPaths.Select(s => s.Value), PathEqualityComparer.Instance).ToList(); - foreach (string unmappedFolder in unmappedFolders) + foreach (var unmappedFolder in unmappedFolders) { var di = new DirectoryInfo(unmappedFolder.Normalize()); - 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) + }); } var setToRemove = SpecialFolders; @@ -181,8 +200,8 @@ namespace NzbDrone.Core.RootFolders private void GetDetails(RootFolder rootFolder, Dictionary seriesPaths, bool timeout) { - Task.Run(() => - { + // Task.Run(() => + // { if (_diskProvider.FolderExists(rootFolder.Path)) { rootFolder.Accessible = true; @@ -190,7 +209,8 @@ namespace NzbDrone.Core.RootFolders rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path); rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path, seriesPaths); } - }).Wait(timeout ? 5000 : -1); + + // }).Wait(timeout ? 5000 : -1); } } } 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; } } }